diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bfa0621 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ + +.idea/* diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/backend/package-lock.json b/backend/package-lock.json index 8056623..6ec0d65 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -6,6 +6,7 @@ "": { "name": "meiro-task-be", "dependencies": { + "@fastify/cors": "8.5.0", "fastify": "4.25.2" }, "devDependencies": { @@ -37,6 +38,15 @@ "fast-uri": "^2.0.0" } }, + "node_modules/@fastify/cors": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-8.5.0.tgz", + "integrity": "sha512-/oZ1QSb02XjP0IK1U0IXktEsw/dUBTxJOW7IpIeO8c/tNalw/KjoNSJv1Sf6eqoBPO+TDGkifq6ynFK3v68HFQ==", + "dependencies": { + "fastify-plugin": "^4.0.0", + "mnemonist": "0.39.6" + } + }, "node_modules/@fastify/deepmerge": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@fastify/deepmerge/-/deepmerge-1.3.0.tgz", @@ -321,6 +331,11 @@ "toad-cache": "^3.3.0" } }, + "node_modules/fastify-plugin": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-4.5.1.tgz", + "integrity": "sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==" + }, "node_modules/fastq": { "version": "1.16.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.16.0.tgz", @@ -430,11 +445,24 @@ "node": ">=10" } }, + "node_modules/mnemonist": { + "version": "0.39.6", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.6.tgz", + "integrity": "sha512-A/0v5Z59y63US00cRSLiloEIw3t5G+MiKz4BhX21FI+YBJXBOGW0ohFxTxO08dsOYlzxo87T7vGfZKYp2bcAWA==", + "dependencies": { + "obliterator": "^2.0.1" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/obliterator": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.4.tgz", + "integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==" + }, "node_modules/on-exit-leak-free": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/frontend/.prettierignore b/frontend/.prettierignore new file mode 100644 index 0000000..d155fdb --- /dev/null +++ b/frontend/.prettierignore @@ -0,0 +1,4 @@ +# Add files here to ignore them from prettier formatting +/dist +/coverage +/.nx/cache \ No newline at end of file diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 0000000..fc56e31 --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,7 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": true, + "singleQuote": true, + "printWidth": 120 +} diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..9d0b4bc --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,30 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default { + // other rules... + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json'], + tsconfigRootDir: __dirname, + }, +}; +``` + +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..5339bc4 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,10 @@ +import eslint from '@eslint/js'; +import tslint from 'typescript-eslint'; + +export default [ + eslint.configs.recommended, + ...tslint.configs.recommended, + { + ignores: ['node_modules', 'dist'], + }, +]; diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..9131e32 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,13 @@ + + + + + + + meiro - FE test scenario + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..2eb1ce1 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,47 @@ +{ + "name": "frontend", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "lint": "eslint .", + "format:check": "prettier --check .", + "format:fix": "prettier --write ." + }, + "dependencies": { + "@emotion/react": "^11.11.4", + "@emotion/styled": "^11.11.5", + "@fontsource/open-sans": "^5.0.28", + "@mui/icons-material": "^5.15.18", + "@mui/material": "^5.15.18", + "@tanstack/react-query": "^5.37.1", + "axios": "^1.7.2", + "dayjs": "^1.11.11", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.23.1", + "tslib": "^2.6.2" + }, + "devDependencies": { + "@eslint/js": "^9.3.0", + "@types/react": "^18.2.66", + "@types/react-dom": "^18.2.22", + "@typescript-eslint/eslint-plugin": "^7.2.0", + "@typescript-eslint/parser": "^7.2.0", + "@vitejs/plugin-react": "^4.2.1", + "eslint": "^8.0.1", + "eslint-config-standard": "^17.1.0", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", + "eslint-plugin-promise": "^6.0.0", + "eslint-plugin-react-hooks": "^4.6.0", + "eslint-plugin-react-refresh": "^0.4.6", + "prettier": "^3.2.5", + "typescript": "^5.2.2", + "typescript-eslint": "^7.10.0", + "vite": "^5.2.0" + } +} diff --git a/frontend/public/vite.svg b/frontend/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/frontend/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/App/App.tsx b/frontend/src/App/App.tsx new file mode 100644 index 0000000..b1da9e9 --- /dev/null +++ b/frontend/src/App/App.tsx @@ -0,0 +1,22 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ThemeProvider, CssBaseline } from '@mui/material'; +import { theme } from '../styles'; +import AppRouter from './AppRouter'; + +import '@fontsource/open-sans/300.css'; +import '@fontsource/open-sans/500.css'; +import '@fontsource/open-sans/700.css'; +import '@fontsource/open-sans/800.css'; + +const queryClient = new QueryClient(); + +const App = () => ( + + + + + + +); + +export default App; diff --git a/frontend/src/App/AppRouter.tsx b/frontend/src/App/AppRouter.tsx new file mode 100644 index 0000000..5f8a200 --- /dev/null +++ b/frontend/src/App/AppRouter.tsx @@ -0,0 +1,43 @@ +import { lazy } from 'react'; +import { RouterProvider, createBrowserRouter } from 'react-router-dom'; +import { ROUTES } from '../constants'; +import { PageLayout, PagePreloader } from '../components'; + +const Error = lazy(() => import('../modules/Error/Error')); +const Home = lazy(() => import('../modules/Home/Home')); +const Attributes = lazy(() => import('../modules/Attributes/Attributes')); + +const AppRouter = () => { + const router = createBrowserRouter( + [ + { + path: '*', + element: , + }, + { + element: , + children: [ + { + path: ROUTES.home.path, + element: , + }, + { + path: ROUTES.attributes.path, + element: , + children: [ + { + path: `${ROUTES.attributes.path}/:id`, + element: , + }, + ], + }, + ], + }, + ], + { basename: '' } + ); + + return } />; +}; + +export default AppRouter; diff --git a/frontend/src/App/index.ts b/frontend/src/App/index.ts new file mode 100644 index 0000000..c866729 --- /dev/null +++ b/frontend/src/App/index.ts @@ -0,0 +1 @@ +export { default as App } from './App'; diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/frontend/src/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/components/ConfirmDialog/ConfirmDialog.tsx b/frontend/src/components/ConfirmDialog/ConfirmDialog.tsx new file mode 100644 index 0000000..f9b8fb0 --- /dev/null +++ b/frontend/src/components/ConfirmDialog/ConfirmDialog.tsx @@ -0,0 +1,45 @@ +import { ReactNode } from 'react'; +import { Dialog, DialogTitle, DialogContent, DialogActions, IconButton, Button } from '@mui/material'; +import CloseIcon from '@mui/icons-material/Close'; +import { WithChildren } from '../../types'; + +export interface ConfirmDialogProps extends WithChildren { + title?: ReactNode; + open: boolean; + onClose: () => void; + onConfirm: () => void; +} + +const ConfirmDialog = ({ children, title, open, onClose, onConfirm }: ConfirmDialogProps) => { + const confirmHandler = () => { + onClose(); + onConfirm(); + }; + + return ( + + {title && {title}} + theme.palette.grey[500], + }} + onClick={onClose} + > + + + {children} + + + + + + ); +}; + +export default ConfirmDialog; diff --git a/frontend/src/components/ConfirmDialog/index.ts b/frontend/src/components/ConfirmDialog/index.ts new file mode 100644 index 0000000..0f29ed1 --- /dev/null +++ b/frontend/src/components/ConfirmDialog/index.ts @@ -0,0 +1,2 @@ +export { default as ConfirmDialog } from './ConfirmDialog'; +export type { ConfirmDialogProps } from './ConfirmDialog'; diff --git a/frontend/src/components/Footer/Footer.tsx b/frontend/src/components/Footer/Footer.tsx new file mode 100644 index 0000000..dcd42ae --- /dev/null +++ b/frontend/src/components/Footer/Footer.tsx @@ -0,0 +1,27 @@ +import { styled, Box, Container, Typography, Stack, BoxProps } from '@mui/material'; +import { PROJECT } from '../../config'; +import { CONTAINER_MAX_WIDTH_DEFAULT } from '../../constants'; + +const Wrapper = styled(Box)(({ theme: { spacing } }) => ({ + padding: spacing(2), +})); + +interface FooterProps { + wrapperProps?: Partial>; +} + +const Footer = ({ wrapperProps }: FooterProps) => { + return ( + + + + + {PROJECT.meta.name} - {PROJECT.meta.description} ({PROJECT.meta.year}) + + + + + ); +}; + +export default Footer; diff --git a/frontend/src/components/Footer/index.ts b/frontend/src/components/Footer/index.ts new file mode 100644 index 0000000..da94c29 --- /dev/null +++ b/frontend/src/components/Footer/index.ts @@ -0,0 +1 @@ +export { default as Footer } from './Footer'; diff --git a/frontend/src/components/Header/Header.tsx b/frontend/src/components/Header/Header.tsx new file mode 100644 index 0000000..b5d8af1 --- /dev/null +++ b/frontend/src/components/Header/Header.tsx @@ -0,0 +1,104 @@ +import { useState, MouseEvent } from 'react'; +import { Link } from 'react-router-dom'; +import { Box, Toolbar, AppBar, IconButton, Typography, Menu, Container, Button, MenuItem } from '@mui/material'; +import MenuIcon from '@mui/icons-material/Menu'; +import { PROJECT } from '../../config'; +import { CONTAINER_MAX_WIDTH_DEFAULT, HEADER_DESKTOP_HEIGHT, MAIN_MENU } from '../../constants'; + +const Header = () => { + const [anchorElNav, setAnchorElNav] = useState(null); + + const openNavMenuHandler = (event: MouseEvent) => setAnchorElNav(event.currentTarget); + + const closeNavMenuHandler = () => setAnchorElNav(null); + + return ( + + + + + {PROJECT.meta.name} + + + + + + + + {MAIN_MENU.map((item) => ( + + + {item.label} + + + ))} + + + + {PROJECT.meta.name} + + + + {MAIN_MENU.map((item) => ( + + ))} + + + + + ); +}; + +export default Header; diff --git a/frontend/src/components/Header/index.ts b/frontend/src/components/Header/index.ts new file mode 100644 index 0000000..5653319 --- /dev/null +++ b/frontend/src/components/Header/index.ts @@ -0,0 +1 @@ +export { default as Header } from './Header'; diff --git a/frontend/src/components/ViewHeading/ViewHeading.tsx b/frontend/src/components/ViewHeading/ViewHeading.tsx new file mode 100644 index 0000000..07c8595 --- /dev/null +++ b/frontend/src/components/ViewHeading/ViewHeading.tsx @@ -0,0 +1,17 @@ +import { Typography } from '@mui/material'; + +export interface ViewHeadingProps { + title?: string; + description?: string; +} + +const ViewHeading = ({ title, description }: ViewHeadingProps) => { + return ( +
+ {title && {title}} + {description && {description}} +
+ ); +}; + +export default ViewHeading; diff --git a/frontend/src/components/ViewHeading/index.ts b/frontend/src/components/ViewHeading/index.ts new file mode 100644 index 0000000..5eee009 --- /dev/null +++ b/frontend/src/components/ViewHeading/index.ts @@ -0,0 +1,2 @@ +export { default as ViewHeading } from './ViewHeading'; +export type { ViewHeadingProps } from './ViewHeading'; diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts new file mode 100644 index 0000000..91188fd --- /dev/null +++ b/frontend/src/components/index.ts @@ -0,0 +1,6 @@ +export * from './ConfirmDialog'; +export * from './Footer'; +export * from './Header'; +export * from './layout'; +export * from './ViewHeading'; +export * from './preloader'; diff --git a/frontend/src/components/layout/PageLayout.tsx b/frontend/src/components/layout/PageLayout.tsx new file mode 100644 index 0000000..b5042b1 --- /dev/null +++ b/frontend/src/components/layout/PageLayout.tsx @@ -0,0 +1,35 @@ +import { Suspense } from 'react'; +import { Outlet } from 'react-router-dom'; +import { styled, Box } from '@mui/material'; +import { HEADER_DESKTOP_HEIGHT } from '../../constants'; +import { LayoutPreloader } from '../preloader'; +import { Header } from '../Header'; +import { Footer } from '../Footer'; + +const Wrapper = styled(Box)({ + width: '100%', + height: '100%', + display: 'flex', + flexDirection: 'column', +}); + +const Main = styled(Box)({ + paddingTop: HEADER_DESKTOP_HEIGHT, + flex: 1, +}); + +const PageLayout = () => { + return ( + +
+
+ }> + + +
+