Compare commits

...

10 Commits

Author SHA1 Message Date
9a490bd993 feat: add i18next (#11) 2024-11-03 12:53:20 +03:00
7ff8a99505 Merge pull request 'feat: add eslint (#12)' (#15) from feature/eslint into main
Reviewed-on: #15
2024-11-03 12:28:50 +03:00
RustamRu
43c283ed0e feat: add eslint (#12) 2024-11-03 12:23:47 +03:00
660019d107 fix: delete space mocks v2 2024-11-03 11:59:10 +03:00
75c55a11fb fix: delete space mocks 2024-11-03 11:55:13 +03:00
9b2c8be1d9 fix: delete pl and space 2024-11-03 11:53:12 +03:00
52c9ecd3c8 feat: make layout, form, order pages and wizards (#6) (#13)
Reviewed-on: #13
Reviewed-by: Primakov Alexandr Alexandrovich <primakovpro@gmail.com>
Reviewed-by: Rustam <kagapov.rustam@yandex.ru>
Co-authored-by: ilnaz <237x237@gmail.com>
Co-committed-by: ilnaz <237x237@gmail.com>
2024-11-03 11:44:23 +03:00
6cf619d88e Merge pull request 'feat: add routing (#4)' (#5) from feature/routing into main
Reviewed-on: #5
2024-10-27 16:08:28 +03:00
d8d6731046 Обновить package.json 2024-10-19 12:13:03 +03:00
177bedf381 Merge pull request 'feat: project init (#2)' (#3) from feature/init into main
Reviewed-on: #3
Reviewed-by: Primakov Alexandr Alexandrovich <primakovpro@gmail.com>
Reviewed-by: Rustam <kagapov.rustam@yandex.ru>
2024-10-19 12:11:18 +03:00
30 changed files with 4799 additions and 45 deletions

1
.gitignore vendored
View File

@@ -130,3 +130,4 @@ dist
.yarn/install-state.gz
.pnp.*
.idea

8
.prettierrc.json Normal file
View File

@@ -0,0 +1,8 @@
{
"singleQuote": true,
"jsxSingleQuote": true,
"arrowParens": "always",
"max-len": ["error", 140, 2],
"tabWidth": 2,
"useTabs": false
}

View File

@@ -1,3 +1,5 @@
/* eslint-disable no-undef */
/* eslint-disable @typescript-eslint/no-require-imports */
const pkg = require("./package");
module.exports = {

39
eslint.config.mjs Normal file
View File

@@ -0,0 +1,39 @@
import globals from "globals";
import pluginJs from "@eslint/js";
import tseslint from "typescript-eslint";
import pluginReact from "eslint-plugin-react";
import stylistic from '@stylistic/eslint-plugin';
export default [
{ files: ["**/*.{js,mjs,cjs,ts,jsx,tsx}"] },
{ languageOptions: { globals: globals.browser } },
pluginJs.configs.recommended,
...tseslint.configs.recommended,
pluginReact.configs.flat.recommended,
{
plugins: {
'@stylistic': stylistic
},
"rules": {
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"warn", // or "error"
{
"argsIgnorePattern": "^_",
"varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_"
}
],
"sort-imports": ["error", {
"ignoreCase": false,
"ignoreDeclarationSort": true,
"ignoreMemberSort": true,
"memberSyntaxSortOrder": ["none", "all", "multiple", "single"],
"allowSeparatedGroups": true
}],
semi: ["error", "always"],
'@stylistic/indent': ['error', 2],
'react/prop-types': 'off'
},
}
];

5
locales/ru.json Normal file
View File

@@ -0,0 +1,5 @@
{
"dry-wash.arm.masters.add": "Добавить",
"dry-wash.order.status.progress": "Выполняется",
"dry-wash.order.status.complete": "Завершено"
}

4334
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{
"name": "dry-wash-pl",
"name": "dry-wash",
"version": "0.0.0",
"description": "<a id=\"readme-top\"></a>",
"main": "./src/index.tsx",
@@ -8,16 +8,37 @@
"start": "brojs server --port=8099 --with-open-browser",
"build": "npm run clean && brojs build --dev",
"build:prod": "npm run clean && brojs build",
"clean": "rimraf dist"
"clean": "rimraf dist",
"eslint": "npx eslint .",
"eslint:fix": "npx eslint . --fix",
"preversion": "npm run eslint"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@brojs/cli": "^1.3.0",
"@brojs/i18nextreactconfig": "^1.3.3",
"@chakra-ui/icons": "^2.2.4",
"@chakra-ui/react": "^2.4.2",
"@emotion/react": "^11.4.1",
"@emotion/styled": "^11.3.0",
"@types/react": "^18.3.12",
"express": "^4.21.1",
"framer-motion": "^6.2.8",
"i18next": "^23.16.4",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.27.0"
},
"devDependencies": {
"@eslint/js": "^9.14.0",
"@stylistic/eslint-plugin": "^2.10.1",
"@types/react-dom": "^18.3.1",
"eslint": "^9.14.0",
"eslint-plugin-react": "^7.37.2",
"globals": "^15.11.0",
"prettier": "3.3.3",
"typescript-eslint": "^8.12.2"
}
}

View File

@@ -1,12 +1,15 @@
import React from "react";
import { BrowserRouter } from "react-router-dom";
import Routers from "./routes";
import React from 'react';
import { BrowserRouter } from 'react-router-dom';
import Routers from './routes';
import { ChakraProvider, theme as chakraTheme } from '@chakra-ui/react';
const App = () => {
return (
<BrowserRouter>
<Routers></Routers>
</BrowserRouter>
<ChakraProvider theme={chakraTheme}>
<BrowserRouter>
<Routers></Routers>
</BrowserRouter>
</ChakraProvider>
);
};

View File

@@ -0,0 +1,17 @@
import { Box, Flex } from '@chakra-ui/react';
import Sidebar from '../Sidebar';
import Orders from '../Orders';
import Masters from '../Masters';
import React from 'react';
const LayoutArm = ({ currentPage, onSelectPage }) => (
<Flex h='100vh'>
<Sidebar onSelectPage={onSelectPage} />
<Box flex='1' bg='gray.50'>
{currentPage === 'orders' && <Orders />}
{currentPage === 'masters' && <Masters />}
</Box>
</Flex>
);
export default LayoutArm;

View File

@@ -0,0 +1 @@
export { default } from './LayoutArm';

View File

@@ -0,0 +1,24 @@
import React from 'react';
import {
Menu,
MenuButton,
MenuList,
MenuItem,
IconButton,
} from '@chakra-ui/react';
import { EditIcon } from '@chakra-ui/icons';
const MasterActionsMenu = () => {
return (
<Menu>
<MenuButton icon={<EditIcon />} as={IconButton} variant='outline' />
<MenuList>
<MenuItem>Посмотреть профиль</MenuItem>
<MenuItem>Изменить расписание</MenuItem>
<MenuItem>Удалить мастера</MenuItem>
</MenuList>
</Menu>
);
};
export default MasterActionsMenu;

View File

@@ -0,0 +1 @@
export { default } from './MasterActionsMenu';

View File

@@ -0,0 +1,28 @@
import React from 'react';
import { Badge, Link, Stack, Td, Tr } from '@chakra-ui/react';
import MasterActionsMenu from '../MasterActionsMenu';
const MasterItem = ({ name, schedule, phone }) => {
return (
<Tr>
<Td>{name}</Td>
<Td>
<Stack direction='row'>
{schedule.map((time, index) => (
<Badge colorScheme={'green'} key={index}>
{time}
</Badge>
))}
</Stack>
</Td>
<Td>
<Link href='tel:'>{phone}</Link>
</Td>
<Td>
<MasterActionsMenu />
</Td>
</Tr>
);
};
export default MasterItem;

View File

@@ -0,0 +1 @@
export { default } from './MasterItem';

View File

@@ -0,0 +1,65 @@
import React, { useState } from 'react';
import {
Button,
FormControl,
FormLabel,
Input,
Drawer,
DrawerBody,
DrawerCloseButton,
DrawerContent,
DrawerFooter,
DrawerHeader,
DrawerOverlay,
} from '@chakra-ui/react';
const MasterDrawer = ({ isOpen, onClose }) => {
const [newMaster, setNewMaster] = useState({ name: '', phone: '' });
const handleSave = () => {
console.log(`Сохранение мастера: ${newMaster}`);
onClose();
};
return (
<Drawer isOpen={isOpen} onClose={onClose} size='md'>
<DrawerOverlay />
<DrawerContent>
<DrawerCloseButton />
<DrawerHeader>Добавить нового мастера</DrawerHeader>
<DrawerBody>
<FormControl mb='4'>
<FormLabel>ФИО</FormLabel>
<Input
value={newMaster.name}
onChange={(e) =>
setNewMaster({ ...newMaster, name: e.target.value })
}
placeholder='Введите ФИО'
/>
</FormControl>
<FormControl>
<FormLabel>Номер телефона</FormLabel>
<Input
value={newMaster.phone}
onChange={(e) =>
setNewMaster({ ...newMaster, phone: e.target.value })
}
placeholder='Введите номер телефона'
/>
</FormControl>
</DrawerBody>
<DrawerFooter>
<Button colorScheme='teal' mr={3} onClick={handleSave}>
Сохранить
</Button>
<Button variant='ghost' onClick={onClose}>
Отменить
</Button>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
};
export default MasterDrawer;

View File

@@ -0,0 +1 @@
export { default } from './MasterDrawer';

View File

@@ -0,0 +1,51 @@
import React from 'react';
import {
Box,
Heading,
Table,
Thead,
Tbody,
Tr,
Th,
Button,
useDisclosure,
Flex,
} from '@chakra-ui/react';
import { mastersData } from '../../mocks';
import MasterItem from '../MasterItem';
import MasterDrawer from '../MasterModal';
import i18next from 'i18next';
const TABLE_HEADERS = ['Имя', 'Актуальная занятость', 'Телефон', 'Действия'];
const Masters = () => {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<Box p='8'>
<Flex justifyContent='space-between' alignItems='center' mb='5'>
<Heading size='lg'>Мастера</Heading>
<Button colorScheme='green' onClick={onOpen}>
+ {i18next.t('dry-wash.arm.masters.add')}
</Button>
</Flex>
<Table variant='simple' colorScheme='blackAlpha'>
<Thead>
<Tr>
{TABLE_HEADERS.map((name) => (
<Th key={name}>{name}</Th>
))}
</Tr>
</Thead>
<Tbody>
{mastersData.map((master, index) => (
<MasterItem key={index} {...master} />
))}
</Tbody>
</Table>
<MasterDrawer isOpen={isOpen} onClose={onClose} />
</Box>
);
};
export default Masters;

View File

@@ -0,0 +1 @@
export { default } from './Masters';

View File

@@ -0,0 +1,43 @@
import React, { useState } from 'react';
import { Td, Tr, Link, Select } from '@chakra-ui/react';
import i18next from 'i18next';
const statuses = ['pending', 'progress', 'working', 'canceled', 'complete'];
const OrderItem = ({
carNumber,
washTime,
orderDate,
status,
phone,
location,
}) => {
const [statusSelect, setStatus] = useState(status);
return (
<Tr>
<Td>{carNumber}</Td>
<Td>{washTime}</Td>
<Td>{orderDate}</Td>
<Td>
<Select
value={statusSelect}
onChange={(e) => setStatus(e.target.value)}
placeholder='Выберите статус'
>
{statuses.map((status) => (
<option key={status} value={status}>
{i18next.t(`dry-wash.order.status.${status}`)}
</option>
))}
</Select>
</Td>
<Td>
<Link href='tel:'>{phone}</Link>
</Td>
<Td>{location}</Td>
</Tr>
);
};
export default OrderItem;

View File

@@ -0,0 +1 @@
export { default } from './OrderItem';

View File

@@ -0,0 +1,38 @@
import { Box, Heading, Table, Thead, Tbody, Tr, Th } from '@chakra-ui/react';
import React from 'react';
import { ordersData } from '../../mocks';
import OrderItem from '../OrderItem';
const Orders = () => {
const TABLE_HEADERS = [
'Номер машины',
'Время мойки',
'Дата заказа',
'Статус',
'Телефон',
'Расположение',
];
return (
<Box p='8'>
<Heading size='lg' mb='5'>
Заказы
</Heading>
<Table variant='simple' colorScheme='blackAlpha'>
<Thead>
<Tr>
{TABLE_HEADERS.map((name, key) => (
<Th key={key}>{name}</Th>
))}
</Tr>
</Thead>
<Tbody>
{ordersData.map((order, index) => (
<OrderItem key={index} {...order} />
))}
</Tbody>
</Table>
</Box>
);
};
export default Orders;

View File

@@ -0,0 +1 @@
export { default } from './Orders';

View File

@@ -0,0 +1,42 @@
import { Box, Button, Heading, VStack } from '@chakra-ui/react';
import React from 'react';
import { Divider } from '@chakra-ui/react';
const Sidebar = ({ onSelectPage }) => (
<Box
borderRight='1px solid black'
bg='gray.50'
color='white'
w='250px'
p='5'
pt='8'
>
<Heading color='green' size='lg' mb='5'>
Сухой мастер
</Heading>
<VStack align='start' spacing='4'>
<Divider />
<Button
onClick={() => onSelectPage('orders')}
w='100%'
colorScheme='green'
variant='ghost'
>
Заказы
</Button>
<Divider />
<Button
onClick={() => onSelectPage('masters')}
w='100%'
colorScheme='green'
variant='ghost'
>
Мастера
</Button>
<Divider />
</VStack>
</Box>
);
export default Sidebar;

View File

@@ -0,0 +1 @@
export { default } from './Sidebar';

View File

@@ -1,22 +1,28 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable react/display-name */
import React from 'react';
import ReactDOM from 'react-dom/client';
import { i18nextReactInitConfig } from '@brojs/i18nextreactconfig';
import App from './app';
export default () => <App/>;
let rootElement: ReactDOM.Root
export const mount = (Сomponent, element = document.getElementById('app')) => {
const rootElement = ReactDOM.createRoot(element);
rootElement.render(<Сomponent/>);
import i18next from 'i18next';
if(module.hot) {
module.hot.accept('./app', ()=> {
rootElement.render(<Сomponent/>);
})
i18next.t = i18next.t.bind(i18next);
const i18nextPromise = i18nextReactInitConfig(i18next);
export default () => <App />;
let rootElement: ReactDOM.Root;
export const mount = async (
Component,
element = document.getElementById('app'),
) => {
const rootElement = ReactDOM.createRoot(element);
await i18nextPromise;
rootElement.render(<Component />);
if (module.hot) {
module.hot.accept('./app', async () => {
await i18next.reloadResources();
rootElement.render(<Component />);
});
}
};

35
src/mocks/index.ts Normal file
View File

@@ -0,0 +1,35 @@
export const mastersData = [
{
id: 'masters1',
name: 'Иван Иванов',
schedule: ['15:00 - 16:30', '17:00 - 18:00'],
phone: '+7 900 123 45 67',
},
{
id: 'masters2',
name: 'Сергей Петров',
schedule: ['10:00 - 12:30', '14:00 - 15:30', '16:00 - 17:00'],
phone: '+7 900 234 56 78',
},
];
export const ordersData = [
{
id: 'order1',
carNumber: 'A123BC',
washTime: '10:30',
orderDate: '2024-10-31',
status: 'progress',
phone: '79001234567',
location: 'Казань, ул. Баумана, 1',
},
{
id: 'order2',
carNumber: 'B456CD',
washTime: '11:00',
orderDate: '2024-10-31',
status: 'complete',
phone: '79002345678',
location: 'Казань, ул. Кремлёвская, 3',
},
];

View File

@@ -1,7 +1,10 @@
import React from "react";
import React, { useState } from 'react';
import LayoutArm from '../../components/LayoutArm';
const Page = () => {
return <h1>Arm </h1>;
const [currentPage, setCurrentPage] = useState('orders');
return <LayoutArm currentPage={currentPage} onSelectPage={setCurrentPage} />;
};
export default Page;

View File

@@ -1,15 +1,15 @@
import React from "react";
import { Routes, Route } from "react-router-dom";
import Arm from "./pages/arm";
import Order from "./pages/order-view";
import Landing from "./pages/landing";
import React from 'react';
import { Routes, Route } from 'react-router-dom';
import Arm from './pages/arm';
import Order from './pages/order-view';
import Landing from './pages/landing';
const Routers = () => {
return (
<Routes>
<Route path="/dry-wash-pl" element={<Landing />}></Route>
<Route path="/dry-wash-pl/order" element={<Order />}></Route>
<Route path="/dry-wash-pl/arm" element={<Arm />}></Route>
<Route path='/dry-wash' element={<Landing />}></Route>
<Route path='/dry-wash/order' element={<Order />}></Route>
<Route path='/dry-wash/arm' element={<Arm />}></Route>
</Routes>
);
};

View File

@@ -1,3 +1,5 @@
/* eslint-disable no-undef */
/* eslint-disable @typescript-eslint/no-require-imports */
const router = require('express').Router();
module.exports = router;

5
types.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare interface NodeModule {
hot?: {
accept: (path: string, callback: () => void) => void;
};
}