Repository: m6v3l9/react-material-admin Branch: main Commit: 32a2764e44f4 Files: 125 Total size: 225.7 KB Directory structure: gitextract_ux3vinbg/ ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── public/ │ ├── 404.html │ ├── index.html │ ├── locales/ │ │ ├── en/ │ │ │ └── translation.json │ │ └── fr/ │ │ └── translation.json │ ├── manifest.json │ └── robots.txt ├── src/ │ ├── App.test.tsx │ ├── App.tsx │ ├── AppRoutes.tsx │ ├── admin/ │ │ ├── components/ │ │ │ ├── AdminAppBar.tsx │ │ │ ├── AdminDrawer.tsx │ │ │ ├── AdminToolbar.tsx │ │ │ └── RecentNotifications.tsx │ │ ├── config/ │ │ │ ├── activity.ts │ │ │ └── notification.ts │ │ ├── hooks/ │ │ │ ├── useActivityLogs.ts │ │ │ ├── useNotifications.ts │ │ │ ├── useProfileInfo.ts │ │ │ └── useUpdateProfileInfo.ts │ │ ├── pages/ │ │ │ ├── Admin.tsx │ │ │ ├── Dashboard.tsx │ │ │ ├── Faq.tsx │ │ │ ├── HelpCenter.tsx │ │ │ ├── Home.tsx │ │ │ ├── Profile.tsx │ │ │ ├── ProfileActivity.tsx │ │ │ ├── ProfileInformation.tsx │ │ │ └── ProfilePassword.tsx │ │ ├── types/ │ │ │ ├── activityLog.ts │ │ │ ├── notification.ts │ │ │ └── profileInfo.ts │ │ └── widgets/ │ │ ├── AchievementWidget.tsx │ │ ├── ActivityWidget.tsx │ │ ├── BudgetWidget.tsx │ │ ├── CircleProgressWidget.tsx │ │ ├── FollowersWidget.tsx │ │ ├── MeetingWidgets.tsx │ │ ├── OverviewWidget.tsx │ │ ├── PersonalTargetsWidget.tsx │ │ ├── ProgressWidget.tsx │ │ ├── SalesByAgeWidget.tsx │ │ ├── SalesByCategoryWidget.tsx │ │ ├── SalesHistoryWidget.tsx │ │ ├── TeamProgressWidget.tsx │ │ ├── UsersWidget.tsx │ │ ├── ViewsWidget.tsx │ │ └── WelcomeWidget.tsx │ ├── auth/ │ │ ├── contexts/ │ │ │ └── AuthProvider.tsx │ │ ├── hooks/ │ │ │ ├── useForgotPassword.ts │ │ │ ├── useForgotPasswordSubmit.ts │ │ │ ├── useLogin.ts │ │ │ ├── useLogout.ts │ │ │ ├── useRegister.ts │ │ │ ├── useUpdatePassword.ts │ │ │ └── useUserInfo.ts │ │ ├── pages/ │ │ │ ├── ForgotPassword.tsx │ │ │ ├── ForgotPasswordSubmit.tsx │ │ │ ├── Login.tsx │ │ │ └── Register.tsx │ │ └── types/ │ │ └── userInfo.ts │ ├── calendar/ │ │ ├── components/ │ │ │ ├── Calendar.tsx │ │ │ └── EventDialog.tsx │ │ ├── hooks/ │ │ │ ├── useAddEvent.ts │ │ │ ├── useDeleteEvent.ts │ │ │ ├── useEvents.ts │ │ │ └── useUpdateEvent.ts │ │ ├── pages/ │ │ │ └── CalendarApp.tsx │ │ └── types/ │ │ └── event.ts │ ├── core/ │ │ ├── components/ │ │ │ ├── BoxedLayout.tsx │ │ │ ├── ConfirmDialog.tsx │ │ │ ├── Empty.tsx │ │ │ ├── Footer.tsx │ │ │ ├── Loader.tsx │ │ │ ├── Logo.tsx │ │ │ ├── PrivateRoute.tsx │ │ │ ├── QueryWrapper.tsx │ │ │ ├── Result.tsx │ │ │ ├── SelectToolbar.tsx │ │ │ ├── SettingsDrawer.tsx │ │ │ └── SvgContainer.tsx │ │ ├── config/ │ │ │ ├── i18n.ts │ │ │ └── layout.ts │ │ ├── contexts/ │ │ │ ├── SettingsProvider.tsx │ │ │ └── SnackbarProvider.tsx │ │ ├── hooks/ │ │ │ ├── useDateLocale.ts │ │ │ ├── useLocalStorage.ts │ │ │ └── usePageTracking.ts │ │ ├── pages/ │ │ │ ├── Forbidden.tsx │ │ │ ├── NotFound.tsx │ │ │ └── UnderConstructions.tsx │ │ ├── theme/ │ │ │ ├── components.tsx │ │ │ ├── index.ts │ │ │ ├── mixins.ts │ │ │ ├── palette.ts │ │ │ ├── shape.ts │ │ │ ├── transitions.ts │ │ │ └── typography.ts │ │ └── utils/ │ │ ├── crudUtils.ts │ │ └── selectUtils.ts │ ├── index.tsx │ ├── landing/ │ │ ├── components/ │ │ │ └── LandingLayout.tsx │ │ └── pages/ │ │ └── Landing.tsx │ ├── mocks/ │ │ ├── activityLogs.json │ │ ├── events.json │ │ ├── notifications.json │ │ ├── profileInfo.json │ │ ├── server.ts │ │ ├── userInfo.json │ │ └── users.json │ ├── react-app-env.d.ts │ ├── reportWebVitals.ts │ ├── setupTests.ts │ └── users/ │ ├── components/ │ │ ├── UserDialog.tsx │ │ └── UserTable.tsx │ ├── hooks/ │ │ ├── useAddUser.ts │ │ ├── useDeleteUsers.ts │ │ ├── useUpdateUser.ts │ │ └── useUsers.ts │ ├── pages/ │ │ └── UserManagement.tsx │ └── types/ │ └── user.ts └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # dependencies /node_modules /.pnp .pnp.js # testing /coverage # production /build # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: LICENSE ================================================ Copyright 2021 Vaniya Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================

React Material Admin logo

React Material Admin

react-material-admin is a free and open-source admin application including many real-world examples. It is based on React and Material-UI.

[![react-material-admin-demo](https://cdn.dribbble.com/users/6538082/screenshots/15805144/media/5687464c7190019afb748863ac6957d3.png?compress=1&resize=1200x900)](https://m6v3l9.github.io/react-material-admin/) ## Getting Started ``` # Install dependencies yarn install # Run the app yarn start ``` This will automatically open [http://localhost:3000](http://localhost:3000). ## Features ``` - Admin - Home - Dashboard/Charts - FAQ - Help Center - Profile Activity - Profile Information - Profile Password - Auth - Forgot Password - Forgot Password Submit - Login - Register - Calendar App - Core - Forbidden - Not Found - Under Constructions - Landing - User Management ``` ## Technologies | Package | Description | Docs | | --------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------- | | Analytics | Google Analytics | [Docs](https://analytics.google.com/analytics/web/react-ga) | | Bundle Size Analyzer | Source map explorer | [Docs](https://create-react-app.dev/docs/analyzing-the-bundle-size) | | Charts | Recharts | [Docs](https://recharts.org/) | | CI | Github CI | [Docs]() | | Code Splitting | Route-based code splitting (included in React) | [Docs](https://reactjs.org/docs/code-splitting.html#route-based-code-splitting) | | Components | Material-UI | [Docs](https://material-ui.com/) | | Data Fetching | React Query Toolkit | [Docs](https://react-query.tanstack.com/) | | Deployment | Github Pages | [Docs](https://create-react-app.dev/docs/deployment#github-pages) | | Environment Variables | Dotenv (included in Create React App) | [Docs](https://create-react-app.dev/docs/adding-custom-environment-variables) | | Error Monitoring | Sentry | [Docs](https://docs.sentry.io/platforms/javascript/guides/react/) | | Form | Formik | [Docs](https://formik.org/) | | I18N | react-i18next | [Docs](https://react.i18next.com/) | | Routing | React Router | [Docs](https://reactrouter.com/) | | Theming (+ dark mode) | Material-UI | [Docs](https://material-ui.com/customization/theming/) | | Toolchain | Create React App | [Docs](https://create-react-app.dev/) | | TypeScript | TypeScript | [Docs](https://create-react-app.dev/docs/adding-typescript/) | | Validation | Yup | [Docs](https://github.com/jquense/yup) | ## Coming Soon | Package | Description | Docs | | ------------ | ------------------------------------------- | ------------------------------- | | Drag & Drop | Add Projects page with Drag & Drop features | | | E2E Testing | Cypress | [Docs](https://www.cypress.io/) | | Unit Testing | Jest | [Docs](https://jestjs.io/) | ## License This project is licensed under the terms of the [MIT license](/LICENSE). ================================================ FILE: package.json ================================================ { "name": "react-material-admin", "version": "0.1.0-alpha", "description": "Free and open-source admin application made with React", "author": "m6v3l9 ", "homepage": "https://m6v3l9.github.io/react-material-admin", "license": "MIT", "dependencies": { "@emotion/react": "^11.1.5", "@emotion/styled": "^11.1.5", "@fullcalendar/daygrid": "5.7.0", "@fullcalendar/react": "5.7.0", "@material-ui/core": "^5.0.0-alpha.35", "@material-ui/icons": "^5.0.0-alpha.35", "@material-ui/lab": "5.0.0-alpha.35", "@sentry/react": "^6.4.1", "@testing-library/jest-dom": "^5.11.4", "@testing-library/react": "^11.1.0", "@testing-library/user-event": "^12.1.10", "@types/jest": "^26.0.15", "@types/node": "^12.0.0", "@types/react": "^17.0.0", "@types/react-dom": "^17.0.0", "axios": "^0.21.1", "axios-mock-adapter": "^1.19.0", "date-fns": "^2.19.0", "env-cmd": "^10.1.0", "formik": "^2.2.6", "gh-pages": "^3.1.0", "history": "^5.0.0", "i18next": "^20.1.0", "i18next-browser-languagedetector": "^6.1.0", "i18next-xhr-backend": "^3.2.2", "react": "^17.0.2", "react-dom": "^17.0.2", "react-error-boundary": "^3.1.3", "react-i18next": "^11.8.11", "react-query": "^3.16.0", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-scripts": "4.0.3", "recharts": "^2.0.9", "source-map-explorer": "^2.5.2", "typescript": "^4.1.2", "web-vitals": "^1.0.1", "yup": "^0.32.9" }, "scripts": { "analyze": "source-map-explorer 'build/static/js/*.js'", "build": "react-scripts build", "build:staging": "env-cmd -f .env.staging npm run build", "build:production": "env-cmd -f .env.production npm run build", "predeploy": "yarn run build:production", "deploy": "gh-pages -d build", "start": "react-scripts start", "test": "react-scripts test" }, "eslintConfig": { "extends": [ "react-app", "react-app/jest" ] }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } } ================================================ FILE: public/404.html ================================================ Single Page Apps for GitHub Pages ================================================ FILE: public/index.html ================================================ %REACT_APP_NAME%
================================================ FILE: public/locales/en/translation.json ================================================ { "admin": { "drawer": { "menu": { "calendar": "Calendar", "dashboard": "Dashboard", "help": "Help", "home": "Home", "projects": "Projects", "settings": "Settings", "userManagement": "Users" } }, "header": { "notifications": { "empty": { "title": "Your notification list is empty" }, "seeAll": "See all notifications" } }, "home": { "achievement": { "action": "View Profile", "description": "You have completed {{progress}}% of your profile. Your current progress is great.", "title": "Congratulations {{name}}" }, "followers": { "units": { "likes": "Likes", "love": "Love", "smiles": "Smiles" } }, "meeting": { "title": "Meetings" }, "targets": { "income": "Income", "followers": "Followers", "title": "Targets", "views": "Views" }, "views": { "action": "View Dashboard", "unit": "Views" }, "welcome": { "message": "This page is designed to give some important information about the application. Let's make someting together!", "subTitle": "Welcome back!", "title": "Hi {{name}}," } } }, "auth": { "forgotPassword": { "form": { "action": "Send Confirmation", "back": "Back to login", "email": { "label": "E-mail Address" } }, "notifications": { "success": "Email has been sent!" }, "subTitle": "To get a validation code, enter the e-mail address associated to your account.", "title": "Get validation code" }, "forgotPasswordSubmit": { "form": { "action": "Reset Password", "back": "Back to login", "code": { "label": "Code" }, "confirmPassword": { "label": "Confirm Password" }, "newPassword": { "label": "New Password" } }, "notifications": { "success": "Your password has been changed!" }, "subTitle": "Enter the code you received by mail and choose a new password.", "title": "Change your password" }, "login": { "forgotPasswordLink": "Forgot password?", "form": { "email": { "label": "E-mail Address" }, "password": { "label": "Password" } }, "newAccountLink": "Don't have an account? Sign Up!", "submit": "Sign in", "title": "Sign in" }, "register": { "back": "Back to login", "form": { "email": { "label": "E-mail Address" }, "firstName": { "label": "First Name" }, "gender": { "label": "Gender", "options": { "f": "F", "m": "M", "n": "NC" } }, "lastName": { "label": "Last Name" } }, "notifications": { "success": "Your account has been created successfully!" }, "submit": "Register", "title": "Register" } }, "calendar": { "confirmations": { "delete": "Are you sure you want to delete this event?" }, "form": { "color": { "label": "Color" }, "description": { "label": "Description" }, "end": { "label": "End Date" }, "start": { "label": "Start Date" }, "title": { "label": "Title" } }, "modal": { "add": { "action": "Add", "title": "Add Event" }, "edit": { "action": "Edit", "title": "Edit Event" } }, "notifications": { "addSuccess": "{{event}} has been added!", "deleteSuccess": "Event has been deleted!", "updateSuccess": "{{event}} has been updated!" }, "title": "Calendar" }, "common": { "backHome": "Back to Home", "cancel": "Cancel", "confirm": "Confirm", "confirmation": "Confirmation", "delete": "Delete", "edit": "Edit", "errors": { "forbidden": { "subTitle": "You don't have access to view this page" }, "notFound": { "subTitle": "Sorry, we couldn’t find the page you’re looking for. Perhaps you’ve mistyped the URL? Be sure to check your spelling.", "title": "Oops!" }, "unexpected": { "subTitle": "Something went wrong! If the problem persists, contact us!", "title": "Oops!" }, "underConstructions": { "subTitle": "We are actively working on this page.", "title": "Under constructions!" } }, "reset": "Reset", "retry": "Try Again", "selected": "Selected", "snackbar": { "error": "Error", "success": "Success" }, "today": "Today", "update": "Update", "validations": { "email": "Invalid email address", "max": "Must be {{size}} characters or less", "min": "Must be {{size}} characters or more", "passwordMatch": "Password does not match", "required": "Required" } }, "dashboard": { "activity": { "title": "Activity" }, "budget": { "legend": { "unit": "Budget ($K)" }, "title": "Budget" }, "orderProgress": { "title": "Orders" }, "overview": { "orders": "Orders", "sales": "Sales", "users": "Users", "visits": "Visits" }, "progress": { "title": "Progress" }, "salesByAge": { "title": "Sales by Age" }, "salesByCategory": { "legend": { "books": "Books", "movies": "Movies & TV", "software": "Software" }, "title": "Sales by Category" }, "salesHistory": { "title": "Sales History", "unit": "$ today" }, "salesProgress": { "title": "Sales" }, "teams": { "columns": { "progress": "Progress", "team": "Team", "value": "Value" }, "title": "Teams Progress" }, "users": { "title": "Recent Users" }, "visitProgress": { "title": "Visits" }, "title": "Dashboard" }, "faq": { "noAnswerLink": "Can't find it here? Check out our Help Center.", "questions": { "title1": "What is React Admin?", "answer1": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada lacus ex, sit amet blandit leo lobortiseget.", "title2": "How does it work?", "answer2": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada lacus ex, sit amet blandit leo lobortiseget.", "title3": "What are the features available?", "answer3": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada lacus ex, sit amet blandit leo lobortiseget.", "title4": "Who is maintaining the project?", "answer4": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada lacus ex, sit amet blandit leo lobortiseget.", "title5": "What is the license of the project?", "answer5": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada lacus ex, sit amet blandit leo lobortiseget.", "title6": "How can I contribute to the project?", "answer6": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada lacus ex, sit amet blandit leo lobortiseget." }, "title": "Frequently Asked Questions" }, "help": { "menu": { "contact": "Contact", "faq": "FAQ", "guide": "Guide", "support": "Support" }, "title": "Help Center" }, "landing": { "cta": { "main": "Sign in", "mainAuth": "Continue as {{name}}", "secondary": "Source code" }, "features": { "more": "More on Github", "title": "Main Features" }, "title": "Free and open-source admin application made with React and Material-UI" }, "notifications": { "newComment": "{{user}} has written a new comment on your profile", "unreadMessages": "You have {{quantity}} unread messages" }, "profile": { "activity": { "empty": "Logs of your activity will show up here!", "logs": { "eventAdded": "You added a new event: {{resource}}", "eventUpdated": "You updated event: {{resource}}", "userAdded": "You added a new user: {{resource}}", "userDeleted": "You deleted user: {{resource}}", "userUpdated": "You updated user: {{resource}}" } }, "completion": { "title": "Profile Completion" }, "info": { "form": { "email": { "label": "Email Address" }, "firstName": { "label": "First Name" }, "gender": { "label": "Gender", "options": { "f": "Female", "m": "Male", "n": "NC" } }, "lastName": { "label": "Last Name" } }, "title": "Update profile" }, "menu": { "activity": "Activity", "info": "Information", "password": "Password" }, "notifications": { "informationUpdated": "Information updated!", "passwordChanged": "Password changed!" }, "password": { "form": { "current": { "label": "Current Password" }, "confirm": { "label": "Confirm Password" }, "new": { "label": "New Password" } }, "title": "Change your password" } }, "settings": { "drawer": { "direction": { "label": "Direction", "options": { "ltr": "LTR", "rtl": "RTL" } }, "language": { "label": "Language", "options": { "en": "English", "fr": "Français" } }, "mode": { "label": "Mode", "options": { "dark": "Dark", "light": "Light" } }, "sidebar": { "label": "Sidebar", "options": { "collapsed": "Collapsed", "full": "Full" } }, "title": "Settings" } }, "userManagement": { "confirmations": { "delete": "Are you sure you want to delete this user?" }, "form": { "disabled": { "label": "Disabled" }, "email": { "label": "Email Address" }, "firstName": { "label": "First Name" }, "gender": { "label": "Gender", "options": { "f": "Female", "m": "Male", "n": "NC" } }, "lastName": { "label": "Last Name" }, "role": { "label": "Role" } }, "modal": { "add": { "action": "Add", "title": "Add User" }, "edit": { "action": "Edit", "title": "Edit User" } }, "notifications": { "addSuccess": "{{user}} has been added!", "deleteSuccess": "User(s) have been deleted!", "updateSuccess": "{{user}} has been updated!" }, "table": { "headers": { "actions": "Actions", "gender": "Gender", "role": "Role", "status": "Status", "user": "User" } }, "toolbar": { "title": "Users" } } } ================================================ FILE: public/locales/fr/translation.json ================================================ { "admin": { "drawer": { "menu": { "calendar": "Calendrier", "dashboard": "Dashboard", "help": "Aide", "home": "Home", "projects": "Projets", "settings": "Paramètres", "userManagement": "Utilisateurs" } }, "header": { "notifications": { "empty": { "title": "Votre liste de notifications est vide" }, "seeAll": "Voir toutes les notifications" } }, "home": { "achievement": { "action": "Voir mon profil", "description": "Vous avez complété {{progress}}% de votre profile. Vos progrès sont fantastiques.", "title": "Félicitations {{name}}" }, "followers": { "units": { "likes": "Likes", "love": "Love", "smiles": "Smiles" } }, "meeting": { "title": "Réunions" }, "targets": { "income": "Revenus", "followers": "Followers", "title": "Objectifs", "views": "Vues" }, "views": { "action": "Voir le dashboard", "unit": "Vues" }, "welcome": { "message": "Cette page a été conçue pour afficher les informations importantes de l'application", "subTitle": "Heureux de vous revoir!", "title": "Bonjour {{name}}," } } }, "auth": { "forgotPassword": { "form": { "action": "Envoyer le code", "back": "Retourner sur la page de login", "email": { "label": "Adresse E-mail" } }, "notifications": { "success": "Un e-mail a été envoyé!" }, "subTitle": "Pour obtenir un code de validation, entrez d'abord l'adresse e-mail que vous avez ajoutée à votre compte.", "title": "Obtenir un code de validation" }, "forgotPasswordSubmit": { "form": { "action": "Réinitialiser le mot de passe", "back": "Retourner sur la page de login", "code": { "label": "Code" }, "confirmPassword": { "label": "Confirmer le mot de passe" }, "newPassword": { "label": "Nouveau mot de passe" } }, "notifications": { "success": "Votre mot de passe a été modifié!" }, "subTitle": "Entrez le code reçu via e-mail et choisissez votre nouveau mot de passe.", "title": "Changer le mot de passe" }, "login": { "forgotPasswordLink": "Mot de passe oublié?", "form": { "email": { "label": "Adresse E-mail" }, "password": { "label": "Mot de passe" } }, "newAccountLink": "Vous n'avez pas encore de compte? Enregistrez-vous!", "submit": "Se connecter", "title": "Se connecter" }, "register": { "back": "Retourner sur la page de login", "form": { "email": { "label": "Adresse E-mail" }, "firstName": { "label": "Prénom" }, "gender": { "label": "Genre", "options": { "f": "F", "m": "M", "n": "NC" } }, "lastName": { "label": "Nom" } }, "notifications": { "success": "Votre compte a été créé avec succès!" }, "submit": "S'enregistrer", "title": "S'enregistrer" } }, "calendar": { "confirmations": { "delete": "Etes-vous certain de vouloir supprimer cet évènement?" }, "form": { "color": { "label": "Couleur" }, "description": { "label": "Description" }, "end": { "label": "Date de fin" }, "start": { "label": "Date de début" }, "title": { "label": "Titre" } }, "modal": { "add": { "action": "Ajouter", "title": "Ajouter un évènement" }, "edit": { "action": "Modifier", "title": "Modifier l'évènement" } }, "notifications": { "addSuccess": "{{event}} a été ajouté!", "deleteSuccess": "L'évènement a été supprimé!", "updateSuccess": "{{event}} a été modifié!" }, "title": "Calendrier" }, "common": { "backHome": "Retourner sur la page d'accueil", "cancel": "Annuler", "confirm": "Confirmer", "confirmation": "Confirmation", "delete": "Supprimer", "edit": "Editer", "errors": { "forbidden": { "subTitle": "Vous n'avez pas les accès suffisant pour accéder à cette page" }, "notFound": { "subTitle": "Désolé, nous n'avons pas pu trouver la page que vous cherchez. L'URL peut être incorrect.", "title": "Oups!" }, "unexpected": { "subTitle": "Une erreur s'est produite! Si le problème persiste, contactez-nous!", "title": "Oups!" }, "underConstructions": { "subTitle": "Nous travaillons activement sur cette page", "title": "Chantier en cours!" } }, "reset": "Reset", "retry": "Réessayer", "selected": "sélectionné(s)", "snackbar": { "error": "Erreur", "success": "Succès" }, "today": "Aujourd'hui", "update": "Modifier", "validations": { "email": "Adresse e-mail invalide", "max": "Maximum {{size}} caractères autorisés", "min": "Minimum {{size}} caractères autorisés", "passwordMatch": "Les mots de passe ne correspondent pas", "required": "Requis" } }, "dashboard": { "activity": { "title": "Activité" }, "budget": { "legend": { "unit": "Budget ($K)" }, "title": "Budget" }, "orderProgress": { "title": "Commandes" }, "overview": { "orders": "Commandes", "sales": "Ventes", "users": "Utilisateurs", "visits": "Visites" }, "progress": { "title": "Progrès" }, "salesByAge": { "title": "Ventes par tranche d'âge" }, "salesByCategory": { "legend": { "books": "Livres", "movies": "Films et TV", "software": "Software" }, "title": "Ventes par catégorie" }, "salesHistory": { "title": "Historique des ventes", "unit": "$ aujourd'hui" }, "salesProgress": { "title": "Ventes" }, "teams": { "columns": { "progress": "Progrès", "team": "Equipe", "value": "Valeur" }, "title": "Progrès des équipes" }, "users": { "title": "Utilisateurs récents" }, "visitProgress": { "title": "Visites" }, "title": "Dashboard" }, "faq": { "noAnswerLink": "Vous ne trouvez pas la réponse à votre question? Retournez à la page d'aide.", "questions": { "title1": "Qu'est ce que React Admin?", "answer1": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada lacus ex, sit amet blandit leo lobortiseget.", "title2": "Comment fonctionne-t-il?", "answer2": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada lacus ex, sit amet blandit leo lobortiseget.", "title3": "Quelles sont les fonctionnalités disponibles?", "answer3": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada lacus ex, sit amet blandit leo lobortiseget.", "title4": "Qui maintient le projet?", "answer4": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada lacus ex, sit amet blandit leo lobortiseget.", "title5": "Quelle est la licence utilisée?", "answer5": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada lacus ex, sit amet blandit leo lobortiseget.", "title6": "Comment puis-je contribuer au projet?", "answer6": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse malesuada lacus ex, sit amet blandit leo lobortiseget." }, "title": "Questions Fréquemment Posées" }, "help": { "menu": { "contact": "Contact", "faq": "FAQ", "guide": "Guide", "support": "Support" }, "title": "Centre d'aide" }, "landing": { "cta": { "main": "Se connecter", "mainAuth": "Continuer en tant que {{name}}", "secondary": "Code source" }, "features": { "more": "Plus sur Github", "title": "Principales fonctionnalités" }, "title": "Open-source dashboard application fait avec React" }, "notifications": { "newComment": "{{user}} a écrit un commentaire sur votre profil", "unreadMessages": "Vous avez {{quantity}} messages non-lus" }, "profile": { "activity": { "empty": "Les logs de votre activité seront affichés ici!", "logs": { "eventAdded": "Vous avez ajouté un nouvel évènement: {{resource}}", "eventUpdated": "Vous avez modifié l'évènement: {{resource}}", "userAdded": "Vous avez ajouté un nouvel utilisateur: {{resource}}", "userDeleted": "Vous avez supprimé l'utilisateur: {{resource}}", "userUpdated": "Vous avez modifié l'utilisateur: {{resource}}" } }, "completion": { "title": "Avancée du profil" }, "info": { "form": { "email": { "label": "Adresse Email" }, "firstName": { "label": "Prénom" }, "gender": { "label": "Genre", "options": { "f": "Femme", "m": "Homme", "n": "NC" } }, "lastName": { "label": "Nom" } }, "title": "Modifier le profil" }, "menu": { "activity": "Activité", "info": "Information", "password": "Mot de passe" }, "notifications": { "informationUpdated": "Information modifiée!", "passwordChanged": "Mot de passe modifié!" }, "password": { "form": { "current": { "label": "Mot de passe actuel" }, "confirm": { "label": "Confirmer le mot de passe" }, "new": { "label": "Nouveau mot de passe" } }, "title": "Changer votre mot de passe" } }, "settings": { "drawer": { "direction": { "label": "Direction", "options": { "ltr": "LTR", "rtl": "RTL" } }, "language": { "label": "Langage", "options": { "en": "English", "fr": "Français" } }, "mode": { "label": "Mode", "options": { "dark": "Foncé", "light": "Clair" } }, "sidebar": { "label": "Barre latérale", "options": { "collapsed": "Réduite", "full": "Complète" } }, "title": "Paramètres" } }, "userManagement": { "confirmations": { "delete": "Etes-vous certain de vouloir supprimer cet utilisateur?" }, "form": { "disabled": { "label": "Désactivé" }, "email": { "label": "Addresse E-mail" }, "firstName": { "label": "Prénom" }, "gender": { "label": "Genre", "options": { "f": "Femme", "m": "Homme", "n": "NC" } }, "lastName": { "label": "Nom" }, "role": { "label": "Rôle" } }, "modal": { "add": { "action": "Ajouter", "title": "Ajouter un utilisateur" }, "edit": { "action": "Modifier", "title": "Modifier l'utilisateur" } }, "notifications": { "addSuccess": "{{user}} a été ajouté!", "deleteSuccess": "L(es) utilisateur(s) ont été supprimés!", "updateSuccess": "{{user}} a été modifié!" }, "table": { "headers": { "actions": "Actions", "gender": "Genre", "role": "Rôle", "status": "Statut", "user": "Utilisateur" } }, "toolbar": { "title": "Utilisateurs" } } } ================================================ FILE: public/manifest.json ================================================ { "short_name": "React Admin", "name": "React Material Admin", "icons": [ { "src": "favicon.ico", "sizes": "64x64 32x32 24x24 16x16", "type": "image/x-icon" }, { "src": "logo192.png", "type": "image/png", "sizes": "192x192" }, { "src": "logo512.png", "type": "image/png", "sizes": "512x512" } ], "start_url": ".", "display": "standalone", "theme_color": "#000000", "background_color": "#ffffff" } ================================================ FILE: public/robots.txt ================================================ # https://www.robotstxt.org/robotstxt.html User-agent: * Disallow: ================================================ FILE: src/App.test.tsx ================================================ import { render, screen } from "@testing-library/react"; import React from "react"; import App from "./App"; test("renders learn react link", () => { render(); const linkElement = screen.getByText(/learn react/i); expect(linkElement).toBeInTheDocument(); }); ================================================ FILE: src/App.tsx ================================================ import * as Sentry from "@sentry/react"; import React from "react"; import { QueryClient, QueryClientProvider } from "react-query"; import { ReactQueryDevtools } from "react-query/devtools"; import AppRoutes from "./AppRoutes"; import AuthProvider from "./auth/contexts/AuthProvider"; import Loader from "./core/components/Loader"; import QueryWrapper from "./core/components/QueryWrapper"; import SettingsProvider from "./core/contexts/SettingsProvider"; import SnackbarProvider from "./core/contexts/SnackbarProvider"; import usePageTracking from "./core/hooks/usePageTracking"; if (process.env.NODE_ENV === "production") { Sentry.init({ dsn: process.env.REACT_APP_SENTRY_DSN, }); } // Create a client const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false, retry: 0, suspense: true, }, }, }); function App() { usePageTracking(); return ( }> ); } export default App; ================================================ FILE: src/AppRoutes.tsx ================================================ import { lazy } from "react"; import { Navigate, Route, Routes } from "react-router-dom"; import PrivateRoute from "./core/components/PrivateRoute"; // Admin const Admin = lazy(() => import("./admin/pages/Admin")); const Dashboard = lazy(() => import("./admin/pages/Dashboard")); const Faq = lazy(() => import("./admin/pages/Faq")); const HelpCenter = lazy(() => import("./admin/pages/HelpCenter")); const Home = lazy(() => import("./admin/pages/Home")); const Profile = lazy(() => import("./admin/pages/Profile")); const ProfileActivity = lazy(() => import("./admin/pages/ProfileActivity")); const ProfileInformation = lazy( () => import("./admin/pages/ProfileInformation") ); const ProfilePassword = lazy(() => import("./admin/pages/ProfilePassword")); // Auth const ForgotPassword = lazy(() => import("./auth/pages/ForgotPassword")); const ForgotPasswordSubmit = lazy( () => import("./auth/pages/ForgotPasswordSubmit") ); const Login = lazy(() => import("./auth/pages/Login")); const Register = lazy(() => import("./auth/pages/Register")); // Calendar const CalendarApp = lazy(() => import("./calendar/pages/CalendarApp")); // Core const Forbidden = lazy(() => import("./core/pages/Forbidden")); const NotFound = lazy(() => import("./core/pages/NotFound")); const UnderConstructions = lazy( () => import("./core/pages/UnderConstructions") ); // Landing const Landing = lazy(() => import("./landing/pages/Landing")); // Users const UserManagement = lazy(() => import("./users/pages/UserManagement")); const AppRoutes = () => { return ( } /> }> } /> } /> } /> } /> } /> }> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> ); }; export default AppRoutes; ================================================ FILE: src/admin/components/AdminAppBar.tsx ================================================ import AppBar from "@material-ui/core/AppBar"; import { drawerCollapsedWidth, drawerWidth } from "../../core/config/layout"; import { useSettings } from "../../core/contexts/SettingsProvider"; type AdminAppBarProps = { children: React.ReactNode; }; const AdminAppBar = ({ children }: AdminAppBarProps) => { const { collapsed } = useSettings(); const width = collapsed ? drawerCollapsedWidth : drawerWidth; return ( {children} ); }; export default AdminAppBar; ================================================ FILE: src/admin/components/AdminDrawer.tsx ================================================ import Avatar from "@material-ui/core/Avatar"; import Box from "@material-ui/core/Box"; import Drawer from "@material-ui/core/Drawer"; import List from "@material-ui/core/List"; import ListItem from "@material-ui/core/ListItem"; import ListItemAvatar from "@material-ui/core/ListItemAvatar"; import ListItemText from "@material-ui/core/ListItemText"; import AccountTreeIcon from "@material-ui/icons/AccountTree"; import BarChartIcon from "@material-ui/icons/BarChart"; import EventIcon from "@material-ui/icons/Event"; import HelpCenterIcon from "@material-ui/icons/HelpCenter"; import HomeIcon from "@material-ui/icons/Home"; import PeopleIcon from "@material-ui/icons/People"; import PersonIcon from "@material-ui/icons/Person"; import SettingsIcon from "@material-ui/icons/Settings"; import { useTranslation } from "react-i18next"; import { NavLink } from "react-router-dom"; import { useAuth } from "../../auth/contexts/AuthProvider"; import Logo from "../../core/components/Logo"; import { drawerCollapsedWidth, drawerWidth } from "../../core/config/layout"; type AdminDrawerProps = { collapsed: boolean; mobileOpen: boolean; onDrawerToggle: () => void; onSettingsToggle: () => void; }; export const menuItems = [ { icon: HomeIcon, key: "admin.drawer.menu.home", path: "/admin", }, { icon: BarChartIcon, key: "admin.drawer.menu.dashboard", path: "/admin/dashboard", }, { icon: PeopleIcon, key: "admin.drawer.menu.userManagement", path: "/admin/user-management", }, { icon: EventIcon, key: "admin.drawer.menu.calendar", path: "/admin/calendar", }, { icon: AccountTreeIcon, key: "admin.drawer.menu.projects", path: "/admin/projects", }, { icon: HelpCenterIcon, key: "admin.drawer.menu.help", path: "/admin/help", }, ]; const AdminDrawer = ({ collapsed, mobileOpen, onDrawerToggle, onSettingsToggle, }: AdminDrawerProps) => { const { userInfo } = useAuth(); const { t } = useTranslation(); const width = collapsed ? drawerCollapsedWidth : drawerWidth; const drawer = ( {menuItems.map((item) => ( ))} {userInfo && ( )} ); return ( {/* The implementation can be swapped with js to avoid SEO duplication of links. */} {drawer} {drawer} ); }; export default AdminDrawer; ================================================ FILE: src/admin/components/AdminToolbar.tsx ================================================ import IconButton from "@material-ui/core/IconButton"; import Toolbar from "@material-ui/core/Toolbar"; import Typography from "@material-ui/core/Typography"; import MenuIcon from "@material-ui/icons/Menu"; import { useSettings } from "../../core/contexts/SettingsProvider"; type AdminToolbarProps = { children?: React.ReactNode; title?: string; }; const AdminToolbar = ({ children, title }: AdminToolbarProps) => { const { toggleDrawer } = useSettings(); return ( {title} {children} ); }; export default AdminToolbar; ================================================ FILE: src/admin/components/RecentNotifications.tsx ================================================ import Avatar from "@material-ui/core/Avatar"; import Badge from "@material-ui/core/Badge"; import Box from "@material-ui/core/Box"; import Button from "@material-ui/core/Button"; import IconButton from "@material-ui/core/IconButton"; import List from "@material-ui/core/List"; import ListItem from "@material-ui/core/ListItem"; import ListItemAvatar from "@material-ui/core/ListItemAvatar"; import ListItemText from "@material-ui/core/ListItemText"; import Popover from "@material-ui/core/Popover"; import NotificationsIcon from "@material-ui/icons/Notifications"; import PersonIcon from "@material-ui/icons/Person"; import formatDistanceToNow from "date-fns/formatDistanceToNow"; import { useMemo, useState } from "react"; import { Trans, useTranslation } from "react-i18next"; import { NavLink } from "react-router-dom"; import Empty from "../../core/components/Empty"; import Loader from "../../core/components/Loader"; import Result from "../../core/components/Result"; import { useDateLocale } from "../../core/hooks/useDateLocale"; import { notificationKeys } from "../config/notification"; import { useNotifications } from "../hooks/useNotifications"; const RecentNotifications = () => { const locale = useDateLocale(); const { t } = useTranslation(); const [anchorEl, setAnchorEl] = useState(null); const { data, isError, isLoading } = useNotifications(); const open = Boolean(anchorEl); const unreadCount = useMemo( () => data && data.filter((notification) => notification.unread).length, [data] ); const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); }; const handleClose = () => { setAnchorEl(null); }; return ( {!isLoading && !isError && data && data.length > 0 && ( {data.map((notification) => ( }} defaults="{{ user }} did someting {{ quantity }} times" i18nKey={notificationKeys[notification.code]} values={notification.params} /> } secondary={formatDistanceToNow( new Date(notification.createdAt), { addSuffix: true, locale } )} /> ))} )} {!isLoading && !isError && (!data || data.length === 0) && ( )} {isError && ( )} {isLoading && } ); }; export default RecentNotifications; ================================================ FILE: src/admin/config/activity.ts ================================================ export const logKeys: { [key: string]: string } = { eventAdded: "profile.activity.logs.eventAdded", eventUpdated: "profile.activity.logs.eventUpdated", userAdded: "profile.activity.logs.eventAdded", userDeleted: "profile.activity.logs.userDeleted", userUpdated: "profile.activity.logs.userUpdated", }; ================================================ FILE: src/admin/config/notification.ts ================================================ export const notificationKeys: { [key: string]: string } = { newComment: "notifications.newComment", unreadMessages: "notifications.unreadMessages", }; ================================================ FILE: src/admin/hooks/useActivityLogs.ts ================================================ import axios from "axios"; import { useQuery } from "react-query"; import { ActivityLog } from "../types/activityLog"; const fetchActivityLogs = async (): Promise => { const { data } = await axios.get("/api/activity-logs"); return data; }; export function useActivityLogs() { return useQuery("activity-logs", () => fetchActivityLogs()); } ================================================ FILE: src/admin/hooks/useNotifications.ts ================================================ import axios from "axios"; import { useQuery } from "react-query"; import { Notification } from "../types/notification"; const fetchNotifications = async (): Promise => { const { data } = await axios.get("/api/notifications"); return data; }; export function useNotifications() { return useQuery("notifications", () => fetchNotifications(), { suspense: false, }); } ================================================ FILE: src/admin/hooks/useProfileInfo.ts ================================================ import axios from "axios"; import { useQuery } from "react-query"; import { ProfileInfo } from "../types/profileInfo"; const fetchProfileInfo = async (): Promise => { const { data } = await axios.get("/api/profile-info"); return data; }; export function useProfileInfo() { return useQuery("profile-info", () => fetchProfileInfo()); } ================================================ FILE: src/admin/hooks/useUpdateProfileInfo.ts ================================================ import axios from "axios"; import { useMutation, useQueryClient } from "react-query"; import { ProfileInfo } from "../types/profileInfo"; const updateProfileInfo = async ( profileInfo: ProfileInfo ): Promise => { const { data } = await axios.put("/api/profile-info", profileInfo); return data; }; export function useUpdateProfileInfo() { const queryClient = useQueryClient(); const { isLoading, mutateAsync } = useMutation(updateProfileInfo, { onSuccess: (profileInfo: ProfileInfo) => { queryClient.setQueryData(["profile-info"], profileInfo); }, }); return { isUpdating: isLoading, updateProfileInfo: mutateAsync }; } ================================================ FILE: src/admin/pages/Admin.tsx ================================================ import Box from "@material-ui/core/Box"; import Toolbar from "@material-ui/core/Toolbar"; import { useState } from "react"; import { Outlet } from "react-router-dom"; import QueryWrapper from "../../core/components/QueryWrapper"; import SettingsDrawer from "../../core/components/SettingsDrawer"; import { useSettings } from "../../core/contexts/SettingsProvider"; import AdminDrawer from "../components/AdminDrawer"; const AdminLayout = () => { const [settingsOpen, setSettingsOpen] = useState(false); const { collapsed, open, toggleDrawer } = useSettings(); const handleSettingsToggle = () => { setSettingsOpen(!settingsOpen); }; return ( ); }; export default AdminLayout; ================================================ FILE: src/admin/pages/Dashboard.tsx ================================================ import Grid from "@material-ui/core/Grid"; import AttachMoneyIcon from "@material-ui/icons/AttachMoney"; import ShoppingBasketIcon from "@material-ui/icons/ShoppingBasket"; import SupervisorAccountIcon from "@material-ui/icons/SupervisorAccount"; import React from "react"; import { useTranslation } from "react-i18next"; import AdminAppBar from "../components/AdminAppBar"; import AdminToolbar from "../components/AdminToolbar"; import ActivityWidget from "../widgets/ActivityWidget"; import BudgetWidget from "../widgets/BudgetWidget"; import CircleProgressWidget from "../widgets/CircleProgressWidget"; import OverviewWidget from "../widgets/OverviewWidget"; import ProgressWidget from "../widgets/ProgressWidget"; import SalesByAgeWidget from "../widgets/SalesByAgeWidget"; import SalesByCategoryWidget from "../widgets/SalesByCategoryWidget"; import SalesHistoryWidget from "../widgets/SalesHistoryWidget"; import TeamProgressWidget from "../widgets/TeamProgressWidget"; import UsersWidget from "../widgets/UsersWidget"; const overviewItems = [ { unit: "dashboard.overview.visits", value: "20 700", }, { unit: "dashboard.overview.sales", value: "$ 1 550", }, { unit: "dashboard.overview.orders", value: "149", }, { unit: "dashboard.overview.users", value: "657", }, ]; const Dashboard = () => { const { t } = useTranslation(); return ( {overviewItems.map((item, index) => ( ))} } mb={2} title={t("dashboard.visitProgress.title")} value={75} /> } mb={2} title={t("dashboard.orderProgress.title")} value={50} /> } title={t("dashboard.salesProgress.title")} value={25} /> ); }; export default Dashboard; ================================================ FILE: src/admin/pages/Faq.tsx ================================================ import Accordion from "@material-ui/core/Accordion"; import AccordionDetails from "@material-ui/core/AccordionDetails"; import AccordionSummary from "@material-ui/core/AccordionSummary"; import Container from "@material-ui/core/Container"; import Link from "@material-ui/core/Link"; import Typography from "@material-ui/core/Typography"; import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; import React from "react"; import { useTranslation } from "react-i18next"; import { Link as RouterLink } from "react-router-dom"; import AdminAppBar from "../components/AdminAppBar"; import AdminToolbar from "../components/AdminToolbar"; const questions = [ { title: "faq.questions.title1", answer: "faq.questions.answer1", }, { title: "faq.questions.title2", answer: "faq.questions.answer2", }, { title: "faq.questions.title3", answer: "faq.questions.answer3", }, { title: "faq.questions.title4", answer: "faq.questions.answer4", }, { title: "faq.questions.title5", answer: "faq.questions.answer5", }, { title: "faq.questions.title6", answer: "faq.questions.answer6", }, ]; const Faq = () => { const { t } = useTranslation(); return ( {t("faq.title")} {questions.map((question, index) => ( }> {t(question.title)} {t(question.answer)} ))} {t("faq.noAnswerLink")} ); }; export default Faq; ================================================ FILE: src/admin/pages/HelpCenter.tsx ================================================ import Avatar from "@material-ui/core/Avatar"; import Badge from "@material-ui/core/Badge"; import Card from "@material-ui/core/Card"; import CardActionArea from "@material-ui/core/CardActionArea"; import CardContent from "@material-ui/core/CardContent"; import CardHeader from "@material-ui/core/CardHeader"; import Container from "@material-ui/core/Container"; import Grid from "@material-ui/core/Grid"; import Typography from "@material-ui/core/Typography"; import HelpIcon from "@material-ui/icons/Help"; import MailIcon from "@material-ui/icons/Mail"; import SchoolIcon from "@material-ui/icons/School"; import SupportIcon from "@material-ui/icons/Support"; import React from "react"; import { useTranslation } from "react-i18next"; import { Link as RouterLink } from "react-router-dom"; import { ReactComponent as HelpSvg } from "../../core/assets/help.svg"; import SvgContainer from "../../core/components/SvgContainer"; import AdminAppBar from "../components/AdminAppBar"; import AdminToolbar from "../components/AdminToolbar"; const HelpCenter = () => { const { t } = useTranslation(); return ( } /> {t("help.menu.guide")} } /> {t("help.menu.faq")} } /> {t("help.menu.support")} } /> {t("help.menu.contact")} ); }; export default HelpCenter; ================================================ FILE: src/admin/pages/Home.tsx ================================================ import Grid from "@material-ui/core/Grid"; import React from "react"; import AdminAppBar from "../components/AdminAppBar"; import AdminToolbar from "../components/AdminToolbar"; import RecentNotifications from "../components/RecentNotifications"; import AchievementWidget from "../widgets/AchievementWidget"; import FollowersWidget from "../widgets/FollowersWidget"; import MeetingWidgets from "../widgets/MeetingWidgets"; import PersonalTargetsWidget from "../widgets/PersonalTargetsWidget"; import ViewsWidget from "../widgets/ViewsWidget"; import WelcomeWidget from "../widgets/WelcomeWidget"; const Home = () => { return ( ); }; export default Home; ================================================ FILE: src/admin/pages/Profile.tsx ================================================ import Avatar from "@material-ui/core/Avatar"; import Box from "@material-ui/core/Box"; import Fab from "@material-ui/core/Fab"; import Grid from "@material-ui/core/Grid"; import Tab from "@material-ui/core/Tab"; import Tabs from "@material-ui/core/Tabs"; import Typography from "@material-ui/core/Typography"; import ExitToAppIcon from "@material-ui/icons/ExitToApp"; import PersonIcon from "@material-ui/icons/Person"; import React from "react"; import { useTranslation } from "react-i18next"; import { NavLink, Outlet } from "react-router-dom"; import { useAuth } from "../../auth/contexts/AuthProvider"; import QueryWrapper from "../../core/components/QueryWrapper"; import { useSnackbar } from "../../core/contexts/SnackbarProvider"; import AdminAppBar from "../components/AdminAppBar"; import AdminToolbar from "../components/AdminToolbar"; import CircleProgressWidget from "../widgets/CircleProgressWidget"; const profileMenuItems = [ { key: "profile.menu.activity", path: "", }, { key: "profile.menu.info", path: "./information", }, { key: "profile.menu.password", path: "./password", }, ]; const Profile = () => { const { isLoggingOut, logout, userInfo } = useAuth(); const snackbar = useSnackbar(); const { t } = useTranslation(); const handleLogout = () => { logout().catch(() => snackbar.error(t("common.errors.unexpected.subTitle")) ); }; return ( {`${userInfo?.firstName} ${userInfo?.lastName}`} {userInfo?.role} {profileMenuItems.map((item) => ( ))} ); }; export default Profile; ================================================ FILE: src/admin/pages/ProfileActivity.tsx ================================================ import Box from "@material-ui/core/Box"; import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; import Typography from "@material-ui/core/Typography"; import Timeline from "@material-ui/lab/Timeline"; import TimelineConnector from "@material-ui/lab/TimelineConnector"; import TimelineContent from "@material-ui/lab/TimelineContent"; import TimelineDot from "@material-ui/lab/TimelineDot"; import TimelineItem from "@material-ui/lab/TimelineItem"; import TimelineSeparator from "@material-ui/lab/TimelineSeparator"; import formatDistanceToNow from "date-fns/formatDistanceToNow"; import { Trans, useTranslation } from "react-i18next"; import Empty from "../../core/components/Empty"; import { useDateLocale } from "../../core/hooks/useDateLocale"; import { logKeys } from "../config/activity"; import { useActivityLogs } from "../hooks/useActivityLogs"; const ProfileActivity = () => { const locale = useDateLocale(); const { t } = useTranslation(); const { data } = useActivityLogs(); if (!data || data.length === 0) { return ; } return ( {data.map((log) => ( }} defaults="You modify resource {{ resouce }}" i18nKey={logKeys[log.code]} values={log.params} /> {formatDistanceToNow(new Date(log.createdAt), { addSuffix: true, locale, })} ))} ); }; export default ProfileActivity; ================================================ FILE: src/admin/pages/ProfileInformation.tsx ================================================ import Button from "@material-ui/core/Button"; import Card from "@material-ui/core/Card"; import CardActions from "@material-ui/core/CardActions"; import CardContent from "@material-ui/core/CardContent"; import CardHeader from "@material-ui/core/CardHeader"; import FormControl from "@material-ui/core/FormControl"; import FormControlLabel from "@material-ui/core/FormControlLabel"; import FormLabel from "@material-ui/core/FormLabel"; import Radio from "@material-ui/core/Radio"; import RadioGroup from "@material-ui/core/RadioGroup"; import TextField from "@material-ui/core/TextField"; import LoadingButton from "@material-ui/lab/LoadingButton"; import { useFormik } from "formik"; import { useTranslation } from "react-i18next"; import * as Yup from "yup"; import { useUpdateProfileInfo } from "../../admin/hooks/useUpdateProfileInfo"; import { useSnackbar } from "../../core/contexts/SnackbarProvider"; import { useProfileInfo } from "../hooks/useProfileInfo"; import { ProfileInfo } from "../types/profileInfo"; const genders = [ { label: "profile.info.form.gender.options.f", value: "F" }, { label: "profile.info.form.gender.options.m", value: "M" }, { label: "profile.info.form.gender.options.n", value: "NC" }, ]; const ProfileInformation = () => { const snackbar = useSnackbar(); const { t } = useTranslation(); const { data } = useProfileInfo(); const { isUpdating, updateProfileInfo } = useUpdateProfileInfo(); const formik = useFormik({ initialValues: { email: data ? data.email : "", firstName: data ? data.firstName : "", gender: data ? data.gender : undefined, job: data ? data.job : "", lastName: data ? data.lastName : "", }, validationSchema: Yup.object({ email: Yup.string() .email(t("common.validations.email")) .required(t("common.validations.required")), firstName: Yup.string() .max(20, t("common.validations.max", { size: 20 })) .required(t("common.validations.required")), lastName: Yup.string() .max(30, t("common.validations.max", { size: 30 })) .required(t("common.validations.required")), }), onSubmit: (values) => handleSubmit(values), }); const handleSubmit = async (values: Partial) => { updateProfileInfo({ ...values, id: data?.id } as ProfileInfo) .then(() => { snackbar.success(t("profile.notifications.informationUpdated")); }) .catch(() => { snackbar.error(t("common.errors.unexpected.subTitle")); }); }; return (
{t("profile.info.form.gender.label")} {genders.map((gender) => ( } label={t(gender.label)} /> ))} {t("common.update")}
); }; export default ProfileInformation; ================================================ FILE: src/admin/pages/ProfilePassword.tsx ================================================ import Card from "@material-ui/core/Card"; import CardActions from "@material-ui/core/CardActions"; import CardContent from "@material-ui/core/CardContent"; import CardHeader from "@material-ui/core/CardHeader"; import TextField from "@material-ui/core/TextField"; import LoadingButton from "@material-ui/lab/LoadingButton"; import { useFormik } from "formik"; import { useTranslation } from "react-i18next"; import * as Yup from "yup"; import { useUpdatePassword } from "../../auth/hooks/useUpdatePassword"; import { useSnackbar } from "../../core/contexts/SnackbarProvider"; const ProfilePassword = () => { const snackbar = useSnackbar(); const { t } = useTranslation(); const { isUpdating, updatePassword } = useUpdatePassword(); const formik = useFormik({ initialValues: { oldPassword: "", newPassword: "", confirmPassword: "", }, validationSchema: Yup.object({ oldPassword: Yup.string() .min(8, t("common.validations.min", { size: 8 })) .required(t("common.validations.required")), newPassword: Yup.string() .min(8, t("common.validations.min", { size: 8 })) .required(t("common.validations.required")), confirmPassword: Yup.string() .oneOf([Yup.ref("newPassword")], t("common.validations.passwordMatch")) .required(t("common.validations.required")), }), onSubmit: (values) => handleUpdatePassword(values.oldPassword, values.newPassword), }); const handleUpdatePassword = async ( oldPassword: string, newPassword: string ) => { updatePassword({ oldPassword, newPassword }) .then(() => { formik.resetForm(); snackbar.success(t("profile.notifications.passwordChanged")); }) .catch(() => { snackbar.error(t("common.errors.unexpected.subTitle")); }); }; return (
{t("common.update")}
); }; export default ProfilePassword; ================================================ FILE: src/admin/types/activityLog.ts ================================================ export interface ActivityLog { id: string; actor: string; code: string; createdAt: number; params?: { [key: string]: string }; } ================================================ FILE: src/admin/types/notification.ts ================================================ export interface Notification { id: string; code: string; createdAt: number; params?: { quantity?: string; user?: string; }; unread: boolean; } ================================================ FILE: src/admin/types/profileInfo.ts ================================================ export interface ProfileInfo { id: string; avatar?: string; email: string; firstName: string; gender?: "F" | "M" | "NC"; job: string; lastName: string; } ================================================ FILE: src/admin/widgets/AchievementWidget.tsx ================================================ import Avatar from "@material-ui/core/Avatar"; import Button from "@material-ui/core/Button"; import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; import Typography from "@material-ui/core/Typography"; import StarIcon from "@material-ui/icons/Star"; import { useTranslation } from "react-i18next"; import { Link as RouterLink } from "react-router-dom"; import { useAuth } from "../../auth/contexts/AuthProvider"; const AchievementWidget = () => { const { userInfo } = useAuth(); const { t } = useTranslation(); return ( {t("admin.home.achievement.title", { name: userInfo?.firstName })} {t("admin.home.achievement.description", { progress: userInfo?.progress, })} ); }; export default AchievementWidget; ================================================ FILE: src/admin/widgets/ActivityWidget.tsx ================================================ import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; import CardHeader from "@material-ui/core/CardHeader"; import { useTheme } from "@material-ui/core/styles"; import { useTranslation } from "react-i18next"; import { Line, LineChart, ResponsiveContainer, Tooltip, XAxis } from "recharts"; const data = [ { name: "Jan", pv: 2400, }, { name: "Feb", pv: 1398, }, { name: "Mar", pv: 9800, }, { name: "Apr", pv: 3908, }, { name: "May", pv: 4800, }, { name: "Jun", pv: 3800, }, { name: "Jul", pv: 4300, }, ]; const ActivityWidget = () => { const { t } = useTranslation(); const theme = useTheme(); return ( ); }; export default ActivityWidget; ================================================ FILE: src/admin/widgets/BudgetWidget.tsx ================================================ import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; import CardHeader from "@material-ui/core/CardHeader"; import { useTheme } from "@material-ui/core/styles"; import { useTranslation } from "react-i18next"; import { PolarAngleAxis, Radar, RadarChart, ResponsiveContainer, Tooltip, } from "recharts"; const data = [ { subject: "Marketing", A: 110, }, { subject: "Research", A: 98, }, { subject: "Sales", A: 86, }, { subject: "Ops", A: 99, }, { subject: "HR", A: 85, }, { subject: "Dev", A: 65, }, ]; const BudgetWidget = () => { const { t } = useTranslation(); const theme = useTheme(); return ( ); }; export default BudgetWidget; ================================================ FILE: src/admin/widgets/CircleProgressWidget.tsx ================================================ import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; import CardHeader from "@material-ui/core/CardHeader"; import useTheme from "@material-ui/core/styles/useTheme"; import { PolarAngleAxis, RadialBar, RadialBarChart, ResponsiveContainer, } from "recharts"; type CircleProgressWidgetProps = { height?: number; title: string; value: number; }; const CircleProgressWidget = ({ height = 120, title, value, }: CircleProgressWidgetProps) => { const theme = useTheme(); return ( ); }; export default CircleProgressWidget; ================================================ FILE: src/admin/widgets/FollowersWidget.tsx ================================================ import Avatar from "@material-ui/core/Avatar"; import Box from "@material-ui/core/Box"; import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; import Typography from "@material-ui/core/Typography"; import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown"; import ArrowDropUpIcon from "@material-ui/icons/ArrowDropUp"; import ArrowRightIcon from "@material-ui/icons/ArrowRight"; import EmojiEmotionsIcon from "@material-ui/icons/EmojiEmotions"; import FavoriteIcon from "@material-ui/icons/Favorite"; import ThumbUpIcon from "@material-ui/icons/ThumbUp"; import React from "react"; import { useTranslation } from "react-i18next"; const socials = [ { bgcolor: "primary.main", icon: , name: "Likes", trend: , unitKey: "admin.home.followers.units.likes", value: "26,789", }, { bgcolor: "error.main", icon: , name: "Love", trend: , unitKey: "admin.home.followers.units.love", value: "6,754", }, { bgcolor: "warning.main", icon: , name: "Smiles", trend: , unitKey: "admin.home.followers.units.smiles", value: "52,789", }, ]; const FollowersWidget = () => { const { t } = useTranslation(); return ( {socials.map((social) => ( {social.icon} {social.value} {t(social.name)} {social.trend} ))} ); }; export default FollowersWidget; ================================================ FILE: src/admin/widgets/MeetingWidgets.tsx ================================================ import Avatar from "@material-ui/core/Avatar"; import Box from "@material-ui/core/Box"; import Button from "@material-ui/core/Button"; import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; import IconButton from "@material-ui/core/IconButton"; import Typography from "@material-ui/core/Typography"; import AddIcon from "@material-ui/icons/Add"; import ChevronRightIcon from "@material-ui/icons/ChevronRight"; import React from "react"; import { useTranslation } from "react-i18next"; import { Link as RouterLink } from "react-router-dom"; const meetings = [ { id: "1", person: "Emmy Anderson", date: "8:00 - 10:00", image: "img/portrait-1.jpg", }, { id: "2", person: "Joy McGlynn", date: "11:00 - 12:00", image: "img/portrait-2.jpg", }, { id: "3", person: "Mara Dach", date: "14:00 - 15:00", image: "img/portrait-3.jpg", }, ]; const MeetingWidgets = () => { const { t } = useTranslation(); return ( {t("admin.home.meeting.title")} {meetings.map((meeting) => ( {meeting.person} {meeting.date} ))} ); }; export default MeetingWidgets; ================================================ FILE: src/admin/widgets/OverviewWidget.tsx ================================================ import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; import Typography from "@material-ui/core/Typography"; type OverviewWidgetProps = { color?: "primary" | "warning" | "error"; description: string; title: string; }; const OverviewWidget = ({ description, title }: OverviewWidgetProps) => { return ( {title} {description} ); }; export default OverviewWidget; ================================================ FILE: src/admin/widgets/PersonalTargetsWidget.tsx ================================================ import Box from "@material-ui/core/Box"; import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; import CardHeader from "@material-ui/core/CardHeader"; import LinearProgress from "@material-ui/core/LinearProgress"; import List from "@material-ui/core/List"; import ListItem from "@material-ui/core/ListItem"; import ListItemText from "@material-ui/core/ListItemText"; import Typography from "@material-ui/core/Typography"; import { useTranslation } from "react-i18next"; const targets = [ { name: "Views", nameKey: "admin.home.targets.views", value: 75 }, { name: "Followers", nameKey: "admin.home.targets.followers", value: 50 }, { name: "Income", nameKey: "admin.home.targets.income", value: 25 }, ]; const PersonalTargetsWidget = () => { const { t } = useTranslation(); return ( {targets.map((target) => ( {t(target.nameKey)} {`${target.value}%`} = 75 ? "primary.main" : target.value <= 25 ? "error.main" : "warning.main", }} color="inherit" variant="determinate" value={target.value} /> ))} ); }; export default PersonalTargetsWidget; ================================================ FILE: src/admin/widgets/ProgressWidget.tsx ================================================ import Avatar from "@material-ui/core/Avatar"; import Box from "@material-ui/core/Box"; import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; import LinearProgress from "@material-ui/core/LinearProgress"; import Typography from "@material-ui/core/Typography"; type ProgressWidgetProps = { avatar: React.ReactNode; mb?: number; title: string; value: number; }; const ProgressWidget = ({ avatar, mb = 0, title, value, }: ProgressWidgetProps) => { return ( {avatar} {title} {`${value}%`} ); }; export default ProgressWidget; ================================================ FILE: src/admin/widgets/SalesByAgeWidget.tsx ================================================ import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; import CardHeader from "@material-ui/core/CardHeader"; import { useTheme } from "@material-ui/core/styles"; import { useTranslation } from "react-i18next"; import { Legend, PolarAngleAxis, RadialBar, RadialBarChart, ResponsiveContainer, } from "recharts"; const SalesByAgeWidget = () => { const { t } = useTranslation(); const theme = useTheme(); const data = [ { name: "18-39", uv: 30, fill: theme.palette.text.secondary, }, { name: "40-59", uv: 45, fill: theme.palette.error.main, }, { name: "60-79", uv: 60, fill: theme.palette.warning.main, }, { name: "80+", uv: 75, fill: theme.palette.primary.main, }, ]; return ( ); }; export default SalesByAgeWidget; ================================================ FILE: src/admin/widgets/SalesByCategoryWidget.tsx ================================================ import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; import CardHeader from "@material-ui/core/CardHeader"; import { useTheme } from "@material-ui/core/styles"; import { useTranslation } from "react-i18next"; import { Legend, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts"; const SalesByCategoryWidget = () => { const { t } = useTranslation(); const theme = useTheme(); const data = [ { name: t("dashboard.salesByCategory.legend.books"), fill: theme.palette.primary.main, value: 400, }, { name: t("dashboard.salesByCategory.legend.movies"), fill: theme.palette.warning.main, value: 300, }, { name: t("dashboard.salesByCategory.legend.software"), fill: theme.palette.error.main, value: 300, }, ]; return ( ); }; export default SalesByCategoryWidget; ================================================ FILE: src/admin/widgets/SalesHistoryWidget.tsx ================================================ import Box from "@material-ui/core/Box"; import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; import CardHeader from "@material-ui/core/CardHeader"; import { useTheme } from "@material-ui/core/styles"; import Typography from "@material-ui/core/Typography"; import TrendingUpIcon from "@material-ui/icons/TrendingUp"; import { useTranslation } from "react-i18next"; import { Bar, BarChart, ResponsiveContainer } from "recharts"; type SalesWidgetProps = { value: number; }; const SalesWidget = ({ value }: SalesWidgetProps) => { const { t } = useTranslation(); const theme = useTheme(); const data = [ { name: "Mon", uv: 4000, }, { name: "Tue", uv: 3000, }, { name: "Wed", uv: 2000, }, { name: "Thu", uv: 2780, }, { name: "Fri", uv: 1890, }, { name: "Sat", uv: 2390, }, ]; return ( {value} {t("dashboard.salesHistory.unit")} ); }; export default SalesWidget; ================================================ FILE: src/admin/widgets/TeamProgressWidget.tsx ================================================ import Box from "@material-ui/core/Box"; import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; import CardHeader from "@material-ui/core/CardHeader"; import LinearProgress from "@material-ui/core/LinearProgress"; import Table from "@material-ui/core/Table"; import TableBody from "@material-ui/core/TableBody"; import TableCell from "@material-ui/core/TableCell"; import TableContainer from "@material-ui/core/TableContainer"; import TableHead from "@material-ui/core/TableHead"; import TableRow from "@material-ui/core/TableRow"; import Typography from "@material-ui/core/Typography"; import { useTranslation } from "react-i18next"; const teams = [ { id: "1", color: "primary.main", name: "Marketing Team", progress: 75, value: 122, }, { id: "2", color: "warning.main", name: "Operations Team", progress: 50, value: 82, }, { id: "3", color: "error.main", name: "Sales Team", progress: 25, value: 39, }, { id: "4", color: "text.secondary", name: "Research Team", progress: 10, value: 9, }, ]; const TeamProgressWidget = () => { const { t } = useTranslation(); return ( {t("dashboard.teams.columns.team")} {t("dashboard.teams.columns.progress")} {t("dashboard.teams.columns.value")} {teams.map((team) => ( {team.name} {`${team.progress}%`} {team.value} ))}
); }; export default TeamProgressWidget; ================================================ FILE: src/admin/widgets/UsersWidget.tsx ================================================ import Avatar from "@material-ui/core/Avatar"; import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; import CardHeader from "@material-ui/core/CardHeader"; import IconButton from "@material-ui/core/IconButton"; import List from "@material-ui/core/List"; import ListItem from "@material-ui/core/ListItem"; import ListItemAvatar from "@material-ui/core/ListItemAvatar"; import ListItemSecondaryAction from "@material-ui/core/ListItemSecondaryAction"; import ListItemText from "@material-ui/core/ListItemText"; import { useTheme } from "@material-ui/core/styles"; import ChevronRightIcon from "@material-ui/icons/ChevronRight"; import PersonIcon from "@material-ui/icons/Person"; import { useTranslation } from "react-i18next"; import { Link as RouterLink } from "react-router-dom"; const users = [ { id: "1", firstName: "Rhys", gender: "M", lastName: "Arriaga", role: "Admin", }, { id: "2", firstName: "Laura", gender: "F", lastName: "Core", role: "Member", }, { id: "3", firstName: "Joshua", gender: "M", lastName: "Jagger", role: "Member", }, ]; const UsersWidget = () => { const theme = useTheme(); const { t } = useTranslation(); return ( {users.map((user) => ( ))} ); }; export default UsersWidget; ================================================ FILE: src/admin/widgets/ViewsWidget.tsx ================================================ import Avatar from "@material-ui/core/Avatar"; import Box from "@material-ui/core/Box"; import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; import IconButton from "@material-ui/core/IconButton"; import useTheme from "@material-ui/core/styles/useTheme"; import Typography from "@material-ui/core/Typography"; import ChevronRightIcon from "@material-ui/icons/ChevronRight"; import DashboardIcon from "@material-ui/icons/Dashboard"; import { useTranslation } from "react-i18next"; import { Link as RouterLink } from "react-router-dom"; import { Area, AreaChart, ResponsiveContainer, Tooltip, XAxis } from "recharts"; const data = [ { name: "Jan", fb: 2.5, }, { name: "Feb", fb: 1.4, }, { name: "Mar", fb: 6, }, { name: "Avr", fb: 4, }, ]; const views = "6.967.431"; const ViewsWidget = () => { const theme = useTheme(); const { t } = useTranslation(); return ( {t("admin.home.views.unit")} {views} {t("admin.home.views.action")} ); }; export default ViewsWidget; ================================================ FILE: src/admin/widgets/WelcomeWidget.tsx ================================================ import Card from "@material-ui/core/Card"; import CardContent from "@material-ui/core/CardContent"; import Typography from "@material-ui/core/Typography"; import { useTranslation } from "react-i18next"; import { useAuth } from "../../auth/contexts/AuthProvider"; import { ReactComponent as WelcomeSvg } from "../../core/assets/welcome.svg"; import SvgContainer from "../../core/components/SvgContainer"; const WelcomeWidget = () => { const { userInfo } = useAuth(); const { t } = useTranslation(); return ( {t("admin.home.welcome.title", { name: userInfo?.firstName })} {t("admin.home.welcome.subTitle")} {t("admin.home.welcome.message")} ); }; export default WelcomeWidget; ================================================ FILE: src/auth/contexts/AuthProvider.tsx ================================================ import React, { createContext, useContext } from "react"; import { useLocalStorage } from "../../core/hooks/useLocalStorage"; import { useLogin } from "../hooks/useLogin"; import { useLogout } from "../hooks/useLogout"; import { useUserInfo } from "../hooks/useUserInfo"; import { UserInfo } from "../types/userInfo"; interface AuthContextInterface { hasRole: (roles?: string[]) => {}; isLoggingIn: boolean; isLoggingOut: boolean; login: (email: string, password: string) => Promise; logout: () => Promise; userInfo?: UserInfo; } export const AuthContext = createContext({} as AuthContextInterface); type AuthProviderProps = { children?: React.ReactNode; }; const AuthProvider = ({ children }: AuthProviderProps) => { const [authKey, setAuthKey] = useLocalStorage("authkey", ""); const { isLoggingIn, login } = useLogin(); const { isLoggingOut, logout } = useLogout(); const { data: userInfo } = useUserInfo(authKey); const hasRole = (roles?: string[]) => { if (!roles || roles.length === 0) { return true; } if (!userInfo) { return false; } return roles.includes(userInfo.role); }; const handleLogin = async (email: string, password: string) => { return login({ email, password }) .then((key: string) => { setAuthKey(key); return key; }) .catch((err) => { throw err; }); }; const handleLogout = async () => { return logout() .then((data) => { setAuthKey(""); return data; }) .catch((err) => { throw err; }); }; return ( {children} ); }; export function useAuth() { return useContext(AuthContext); } export default AuthProvider; ================================================ FILE: src/auth/hooks/useForgotPassword.ts ================================================ import axios from "axios"; import { useMutation } from "react-query"; const forgotPassword = async ({ email }: { email: string }) => { const { data } = await axios.post("/api/forgot-password", { email }); return data; }; export function useForgotPassword() { const { isLoading, mutateAsync } = useMutation(forgotPassword); return { isLoading, forgotPassword: mutateAsync }; } ================================================ FILE: src/auth/hooks/useForgotPasswordSubmit.ts ================================================ import axios from "axios"; import { useMutation } from "react-query"; const forgotPasswordSubmit = async ({ code, newPassword, }: { code: string; newPassword: string; }) => { const { data } = await axios.post("/api/forgot-password-submit", { code, newPassword, }); return data; }; export function useForgotPasswordSubmit() { const { isLoading, mutateAsync } = useMutation(forgotPasswordSubmit); return { isLoading, forgotPasswordSubmit: mutateAsync }; } ================================================ FILE: src/auth/hooks/useLogin.ts ================================================ import axios from "axios"; import { useMutation } from "react-query"; const login = async ({ email, password, }: { email: string; password: string; }): Promise => { const { data } = await axios.post("/api/login", { email, password }); return data; }; export function useLogin() { const { isLoading, mutateAsync } = useMutation(login); return { isLoggingIn: isLoading, login: mutateAsync }; } ================================================ FILE: src/auth/hooks/useLogout.ts ================================================ import axios from "axios"; import { useMutation } from "react-query"; const logout = async (): Promise => { const { data } = await axios.post("/api/logout"); return data; }; export function useLogout() { const { isLoading, mutateAsync } = useMutation(logout); return { isLoggingOut: isLoading, logout: mutateAsync }; } ================================================ FILE: src/auth/hooks/useRegister.ts ================================================ import axios from "axios"; import { useMutation } from "react-query"; import { UserInfo } from "../types/userInfo"; const register = async (userInfo: UserInfo): Promise => { const { data } = await axios.post("/api/register", userInfo); return data; }; export function useRegister() { const { isLoading, mutateAsync } = useMutation(register); return { isRegistering: isLoading, register: mutateAsync }; } ================================================ FILE: src/auth/hooks/useUpdatePassword.ts ================================================ import axios from "axios"; import { useMutation } from "react-query"; const updatePassword = async ({ oldPassword, newPassword, }: { oldPassword: string; newPassword: string; }) => { const { data } = await axios.put("/api/password", { oldPassword, newPassword, }); return data; }; export function useUpdatePassword() { const { isLoading, mutateAsync } = useMutation(updatePassword); return { isUpdating: isLoading, updatePassword: mutateAsync }; } ================================================ FILE: src/auth/hooks/useUserInfo.ts ================================================ import axios from "axios"; import { useQuery } from "react-query"; import { UserInfo } from "../types/userInfo"; const fetchUserInfo = async (key?: string): Promise => { const { data } = await axios.get("/api/user-info", { params: { key } }); return data; }; export function useUserInfo(key?: string) { return useQuery(["user-info", key], () => fetchUserInfo(key), { enabled: !!key, }); } ================================================ FILE: src/auth/pages/ForgotPassword.tsx ================================================ import Box from "@material-ui/core/Box"; import Button from "@material-ui/core/Button"; import TextField from "@material-ui/core/TextField"; import Typography from "@material-ui/core/Typography"; import LoadingButton from "@material-ui/lab/LoadingButton"; import { useFormik } from "formik"; import { useTranslation } from "react-i18next"; import { Link as RouterLink, useNavigate } from "react-router-dom"; import * as Yup from "yup"; import BoxedLayout from "../../core/components/BoxedLayout"; import { useSnackbar } from "../../core/contexts/SnackbarProvider"; import { useForgotPassword } from "../hooks/useForgotPassword"; const ForgotPassword = () => { const navigate = useNavigate(); const snackbar = useSnackbar(); const { t } = useTranslation(); const { forgotPassword, isLoading } = useForgotPassword(); const formik = useFormik({ initialValues: { email: "", }, validationSchema: Yup.object({ email: Yup.string() .email(t("common.validations.email")) .required(t("common.validations.required")), }), onSubmit: ({ email }) => handleForgotPassword(email), }); const handleForgotPassword = async (email: string) => { forgotPassword({ email }) .then(() => { snackbar.success(t("auth.forgotPassword.notifications.success")); navigate(`/${process.env.PUBLIC_URL}/forgot-password-submit`); }) .catch(() => { snackbar.error(t("common.errors.unexpected.subTitle")); }); }; return ( {t("auth.forgotPassword.title")} {t("auth.forgotPassword.subTitle")} {t("auth.forgotPassword.form.action")} ); }; export default ForgotPassword; ================================================ FILE: src/auth/pages/ForgotPasswordSubmit.tsx ================================================ import Box from "@material-ui/core/Box"; import Button from "@material-ui/core/Button"; import TextField from "@material-ui/core/TextField"; import Typography from "@material-ui/core/Typography"; import LoadingButton from "@material-ui/lab/LoadingButton"; import { useFormik } from "formik"; import { useTranslation } from "react-i18next"; import { Link, useNavigate } from "react-router-dom"; import * as Yup from "yup"; import BoxedLayout from "../../core/components/BoxedLayout"; import { useSnackbar } from "../../core/contexts/SnackbarProvider"; import { useForgotPasswordSubmit } from "../hooks/useForgotPasswordSubmit"; const ForgotPasswordSubmit = () => { const navigate = useNavigate(); const snackbar = useSnackbar(); const { t } = useTranslation(); const { forgotPasswordSubmit, isLoading } = useForgotPasswordSubmit(); const formik = useFormik({ initialValues: { code: "", newPassword: "", confirmPassword: "", }, validationSchema: Yup.object({ code: Yup.string().required(t("common.validations.required")), newPassword: Yup.string().required(t("common.validations.required")), confirmPassword: Yup.string().required(t("common.validations.required")), }), onSubmit: ({ code, newPassword }) => handleSubmitPassword(code, newPassword), }); const handleSubmitPassword = async (code: string, newPassword: string) => { forgotPasswordSubmit({ code, newPassword }) .then(() => { snackbar.success(t("auth.forgotPasswordSubmit.notifications.success")); navigate(`/${process.env.PUBLIC_URL}/login`); }) .catch(() => { snackbar.error(t("common.errors.unexpected.subTitle")); }); }; return ( {t("auth.forgotPasswordSubmit.title")} {t("auth.forgotPasswordSubmit.subTitle")} {t("auth.forgotPasswordSubmit.form.action")} ); }; export default ForgotPasswordSubmit; ================================================ FILE: src/auth/pages/Login.tsx ================================================ import Box from "@material-ui/core/Box"; import Button from "@material-ui/core/Button"; import Grid from "@material-ui/core/Grid"; import Link from "@material-ui/core/Link"; import Paper from "@material-ui/core/Paper"; import TextField from "@material-ui/core/TextField"; import Typography from "@material-ui/core/Typography"; import LoadingButton from "@material-ui/lab/LoadingButton"; import { useFormik } from "formik"; import { useTranslation } from "react-i18next"; import { Link as RouterLink, useNavigate } from "react-router-dom"; import * as Yup from "yup"; import BoxedLayout from "../../core/components/BoxedLayout"; import { useSnackbar } from "../../core/contexts/SnackbarProvider"; import { useAuth } from "../contexts/AuthProvider"; const Login = () => { const { isLoggingIn, login } = useAuth(); const navigate = useNavigate(); const snackbar = useSnackbar(); const { t } = useTranslation(); const handleLogin = (email: string, password: string) => { login(email, password) .then(() => navigate(`/${process.env.PUBLIC_URL}/admin`, { replace: true }) ) .catch(() => snackbar.error(t("common.errors.unexpected.subTitle"))); }; const formik = useFormik({ initialValues: { email: "demo@example.com", password: "guWEK<'r/-47-XG3", }, validationSchema: Yup.object({ email: Yup.string() .email(t("common.validations.email")) .required(t("common.validations.required")), password: Yup.string() .min(8, t("common.validations.min", { size: 8 })) .required(t("common.validations.required")), }), onSubmit: (values) => handleLogin(values.email, values.password), }); return ( {t("auth.login.title")} {t("auth.login.forgotPasswordLink")} {t("auth.login.submit")} ); }; export default Login; ================================================ FILE: src/auth/pages/Register.tsx ================================================ import Box from "@material-ui/core/Box"; import Button from "@material-ui/core/Button"; import FormControl from "@material-ui/core/FormControl"; import FormControlLabel from "@material-ui/core/FormControlLabel"; import FormLabel from "@material-ui/core/FormLabel"; import Radio from "@material-ui/core/Radio"; import RadioGroup from "@material-ui/core/RadioGroup"; import TextField from "@material-ui/core/TextField"; import Typography from "@material-ui/core/Typography"; import LoadingButton from "@material-ui/lab/LoadingButton"; import { useFormik } from "formik"; import { useTranslation } from "react-i18next"; import { Link, useNavigate } from "react-router-dom"; import * as Yup from "yup"; import BoxedLayout from "../../core/components/BoxedLayout"; import { useSnackbar } from "../../core/contexts/SnackbarProvider"; import { useRegister } from "../hooks/useRegister"; import { UserInfo } from "../types/userInfo"; const genders = [ { label: "auth.register.form.gender.options.f", value: "F" }, { label: "auth.register.form.gender.options.m", value: "M" }, { label: "auth.register.form.gender.options.n", value: "NC" }, ]; const Register = () => { const navigate = useNavigate(); const snackbar = useSnackbar(); const { t } = useTranslation(); const { isRegistering, register } = useRegister(); const formik = useFormik({ initialValues: { email: "", firstName: "", gender: "F", lastName: "", }, validationSchema: Yup.object({ email: Yup.string() .email("Invalid email address") .required(t("common.validations.required")), firstName: Yup.string() .max(20, t("common.validations.max", { size: 20 })) .required(t("common.validations.required")), lastName: Yup.string() .max(30, t("common.validations.max", { size: 30 })) .required(t("common.validations.required")), }), onSubmit: (values) => handleRegister(values), }); const handleRegister = async (values: Partial) => { register(values as UserInfo) .then(() => { snackbar.success(t("auth.register.notifications.success")); navigate(`/${process.env.PUBLIC_URL}/login`); }) .catch(() => { snackbar.error(t("common.errors.unexpected.subTitle")); }); }; return ( {t("auth.register.title")} {t("auth.register.form.gender.label")} {genders.map((gender) => ( } key={gender.value} disabled={isRegistering} label={t(gender.label)} value={gender.value} /> ))} {t("auth.register.submit")} ); }; export default Register; ================================================ FILE: src/auth/types/userInfo.ts ================================================ export interface UserInfo { id: string; avatar?: string; email: string; firstName: string; job: string; lastName: string; progress: number; role: string; } ================================================ FILE: src/calendar/components/Calendar.tsx ================================================ import FullCalendar, { CalendarOptions, EventClickArg, } from "@fullcalendar/react"; import dayGridPlugin from "@fullcalendar/daygrid"; import Box from "@material-ui/core/Box"; import Button from "@material-ui/core/Button"; import IconButton from "@material-ui/core/IconButton"; import { alpha, experimentalStyled as styled, useTheme, } from "@material-ui/core/styles"; import Typography from "@material-ui/core/Typography"; import ArrowLeftIcon from "@material-ui/icons/ArrowLeft"; import ArrowRightIcon from "@material-ui/icons/ArrowRight"; import EventIcon from "@material-ui/icons/Event"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { Event, eventColors } from "../types/event"; const StyledWrapper = styled("div")( ({ theme }) => ` .fc-theme-standard .fc-scrollgrid { border-color: ${theme.palette.divider}; } .fc th { border-right: none; border-left: none; padding: 10px 0; } .fc-theme-standard .fc-scrollgrid { border-right: none; border-left: none; border-bottom: none; } .fc-theme-standard td, .fc-theme-standard th { border-right: none; } .fc-theme-standard td, .fc-theme-standard th { border-color: ${theme.palette.divider}; } .fc .fc-daygrid-day-number { color: ${theme.palette.text.secondary}; font-size: 14px; font-weight: ${theme.typography.fontWeightBold}; padding: 12px; } .fc .fc-daygrid-day.fc-day-today { background-color: ${alpha(theme.palette.primary.main, 0.1)}; } ` ); type CalendarProps = { events: Event[]; onEventClick: (event?: Event) => void; } & CalendarOptions; const Calendar = ({ events, onEventClick, ...calendarProps }: CalendarProps) => { const theme = useTheme(); const { i18n, t } = useTranslation(); const [viewTitle, setViewTitle] = useState(""); const [calendarRef, setCalendarRef] = useState(null); const onCalendarRefSet = useCallback((ref) => { if (ref !== null) { setCalendarRef(ref); } }, []); const handleEventClick = (arg: EventClickArg) => { if (onEventClick) { const event = events.find((e) => e.id === arg.event.id); onEventClick(event); } }; const handleNext = () => { if (calendarRef) { calendarRef.getApi().next(); setViewTitle(calendarRef.getApi().getCurrentData().viewTitle); } }; const handlePrev = () => { if (calendarRef) { calendarRef.getApi().prev(); setViewTitle(calendarRef.getApi().getCurrentData().viewTitle); } }; const handleToday = () => { if (calendarRef) { calendarRef.getApi().today(); setViewTitle(calendarRef.getApi().getCurrentData().viewTitle); } }; useEffect(() => { if (calendarRef) { setViewTitle(calendarRef.getApi().getCurrentData().viewTitle); } }, [calendarRef, i18n.language]); const eventSource = useMemo(() => { return events.map((event: Event) => { if (event.color && eventColors.includes(event.color)) { return { ...event, color: theme.palette[event.color].main }; } return event; }); }, [events, theme]); return ( {/* Start - Custom Header Bar */} {viewTitle} {/* End - Custom Header Bar */} ); }; export default Calendar; ================================================ FILE: src/calendar/components/EventDialog.tsx ================================================ import Box from "@material-ui/core/Box"; import Button from "@material-ui/core/Button"; import Dialog from "@material-ui/core/Dialog"; import DialogActions from "@material-ui/core/DialogActions"; import DialogContent from "@material-ui/core/DialogContent"; import DialogTitle from "@material-ui/core/DialogTitle"; import FormControl from "@material-ui/core/FormControl"; import FormLabel from "@material-ui/core/FormLabel"; import IconButton from "@material-ui/core/IconButton"; import Radio from "@material-ui/core/Radio"; import RadioGroup from "@material-ui/core/RadioGroup"; import TextField from "@material-ui/core/TextField"; import DeleteIcon from "@material-ui/icons/Delete"; import LoadingButton from "@material-ui/lab/LoadingButton"; import MobileDateTimePicker from "@material-ui/lab/MobileDateTimePicker"; import { getTime } from "date-fns"; import { useFormik } from "formik"; import { useTranslation } from "react-i18next"; import * as Yup from "yup"; import { Event, EventColor, eventColors } from "../types/event"; type EventDialogProps = { onAdd: (event: Partial) => void; onClose: () => void; onDelete: (eventId: string) => void; onUpdate: (event: Event) => void; open: boolean; processing: boolean; event?: Event; }; type EventFormValues = { title: string; description?: string; start: Date; end: Date; color?: EventColor; }; const EventDialog = ({ onAdd, onClose, onDelete, onUpdate, open, processing, event, }: EventDialogProps) => { const { t } = useTranslation(); const editMode = Boolean(event && event.id); const convertFormValues = (values: EventFormValues): Partial => { return { ...values, start: getTime(values.start), end: getTime(values.end), }; }; const handleSubmit = (values: EventFormValues) => { const newEvent = convertFormValues(values); if (event && event.id) { onUpdate({ ...newEvent, id: event.id } as Event); } else { onAdd(newEvent); } }; const formik = useFormik({ initialValues: { title: event ? event.title : "", description: event ? event.description : undefined, start: event ? new Date(event.start) : new Date(), end: event ? new Date(event.end) : new Date(), color: event ? event.color : "primary", }, validationSchema: Yup.object({ title: Yup.string() .max(30, t("common.validations.max", { size: 30 })) .required(t("common.validations.required")), description: Yup.string().max( 100, t("common.validations.max", { size: 100 }) ), start: Yup.date().required(t("common.validations.required")), end: Yup.date().required(t("common.validations.required")), color: Yup.string(), }), onSubmit: handleSubmit, }); return (
{editMode ? t("calendar.modal.edit.title") : t("calendar.modal.add.title")} formik.setFieldValue("start", date) } renderInput={(params) => ( )} /> formik.setFieldValue("end", date)} renderInput={(params) => ( )} /> {t("calendar.form.color.label")} {eventColors.map((color) => ( ))} {event && event.id && ( onDelete(event.id)} disabled={processing} > )} {editMode ? t("calendar.modal.edit.action") : t("calendar.modal.add.action")}
); }; export default EventDialog; ================================================ FILE: src/calendar/hooks/useAddEvent.ts ================================================ import axios from "axios"; import { useMutation, useQueryClient } from "react-query"; import { addOne } from "../../core/utils/crudUtils"; import { Event } from "../types/event"; const addEvent = async (event: Event): Promise => { const { data } = await axios.post("/api/events", event); return data; }; export function useAddEvent() { const queryClient = useQueryClient(); const { isLoading, mutateAsync } = useMutation(addEvent, { onSuccess: (event: Event) => { queryClient.setQueryData(["events"], (oldEvents) => addOne(oldEvents, event) ); }, }); return { isAdding: isLoading, addEvent: mutateAsync }; } ================================================ FILE: src/calendar/hooks/useDeleteEvent.ts ================================================ import axios from "axios"; import { useMutation, useQueryClient } from "react-query"; import { removeOne } from "../../core/utils/crudUtils"; import { Event } from "../types/event"; const deleteEvent = async (eventId: string): Promise => { const { data } = await axios.delete("/api/events", { data: eventId }); return data; }; export function useDeleteEvent() { const queryClient = useQueryClient(); const { isLoading, mutateAsync } = useMutation(deleteEvent, { onSuccess: (eventId: string) => { queryClient.setQueryData(["events"], (oldEvents) => removeOne(oldEvents, eventId) ); }, }); return { isDeleting: isLoading, deleteEvent: mutateAsync }; } ================================================ FILE: src/calendar/hooks/useEvents.ts ================================================ import axios from "axios"; import { useQuery } from "react-query"; import { Event } from "../types/event"; const fetchEvents = async (): Promise => { const { data } = await axios.get("/api/events"); return data; }; export function useEvents() { return useQuery("events", () => fetchEvents()); } ================================================ FILE: src/calendar/hooks/useUpdateEvent.ts ================================================ import axios from "axios"; import { useMutation, useQueryClient } from "react-query"; import { updateOne } from "../../core/utils/crudUtils"; import { Event } from "../types/event"; const updateEvent = async (event: Event): Promise => { const { data } = await axios.put("/api/events", event); return data; }; export function useUpdateEvent() { const queryClient = useQueryClient(); const { isLoading, mutateAsync } = useMutation(updateEvent, { onSuccess: (event: Event) => { queryClient.setQueryData(["events"], (oldEvents) => updateOne(oldEvents, event) ); }, }); return { isUpdating: isLoading, updateEvent: mutateAsync }; } ================================================ FILE: src/calendar/pages/CalendarApp.tsx ================================================ import Card from "@material-ui/core/Card"; import Fab from "@material-ui/core/Fab"; import AddIcon from "@material-ui/icons/Add"; import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import AdminAppBar from "../../admin/components/AdminAppBar"; import AdminToolbar from "../../admin/components/AdminToolbar"; import { useAddEvent } from "../../calendar/hooks/useAddEvent"; import ConfirmDialog from "../../core/components/ConfirmDialog"; import { useSnackbar } from "../../core/contexts/SnackbarProvider"; import Calendar from "../components/Calendar"; import EventDialog from "../components/EventDialog"; import { useDeleteEvent } from "../hooks/useDeleteEvent"; import { useEvents } from "../hooks/useEvents"; import { useUpdateEvent } from "../hooks/useUpdateEvent"; import { Event } from "../types/event"; const CalendarApp = () => { const snackbar = useSnackbar(); const { t } = useTranslation(); const [eventDeleted, setEventDeleted] = useState(undefined); const [eventUpdated, setEventUpdated] = useState(undefined); const [openConfirmDeleteDialog, setOpenConfirmDeleteDialog] = useState(false); const [openEventDialog, setOpenEventDialog] = useState(false); const { addEvent, isAdding } = useAddEvent(); const { deleteEvent, isDeleting } = useDeleteEvent(); const { data } = useEvents(); const { isUpdating, updateEvent } = useUpdateEvent(); const processing = isAdding || isDeleting || isUpdating; const handleAddEvent = async (event: Partial) => { addEvent(event as Event) .then(() => { snackbar.success( t("calendar.notifications.addSuccess", { event: event.title }) ); setOpenEventDialog(false); }) .catch(() => { snackbar.error(t("common.errors.unexpected.subTitle")); }); }; const handleDeleteEvent = async () => { if (eventDeleted) { deleteEvent(eventDeleted) .then(() => { snackbar.success(t("calendar.notifications.deleteSuccess")); setOpenConfirmDeleteDialog(false); setOpenEventDialog(false); }) .catch(() => { snackbar.error(t("common.errors.unexpected.subTitle")); }); } }; const handleUpdateEvent = async (event: Event) => { updateEvent(event) .then(() => { snackbar.success( t("calendar.notifications.updateSuccess", { event: event.title }) ); setOpenEventDialog(false); }) .catch(() => { snackbar.error(t("common.errors.unexpected.subTitle")); }); }; const handleCloseConfirmDeleteDialog = () => { setOpenConfirmDeleteDialog(false); }; const handleCloseEventDialog = () => { setEventUpdated(undefined); setOpenEventDialog(false); }; const handleOpenConfirmDeleteDialog = (eventId: string) => { setEventDeleted(eventId); setOpenConfirmDeleteDialog(true); }; const handleOpenEventDialog = (event?: Event) => { setEventUpdated(event); setOpenEventDialog(true); }; return ( handleOpenEventDialog()} size="small" > {openEventDialog && ( )} ); }; export default CalendarApp; ================================================ FILE: src/calendar/types/event.ts ================================================ export interface Event { id: string; title: string; description?: string; start: number; end: number; color?: EventColor; } export const eventColors = ["primary", "warning", "error", "success"] as const; export type EventColor = typeof eventColors[number]; ================================================ FILE: src/core/components/BoxedLayout.tsx ================================================ import AppBar from "@material-ui/core/AppBar"; import Box from "@material-ui/core/Box"; import Container from "@material-ui/core/Container"; import GlobalStyles from "@material-ui/core/GlobalStyles"; import IconButton from "@material-ui/core/IconButton"; import useTheme from "@material-ui/core/styles/useTheme"; import Toolbar from "@material-ui/core/Toolbar"; import SettingsIcon from "@material-ui/icons/Settings"; import React, { useState } from "react"; import Logo from "./Logo"; import SettingsDrawer from "./SettingsDrawer"; type BoxedLayoutProps = { children: React.ReactNode; }; const BoxedLayout = ({ children }: BoxedLayoutProps) => { const theme = useTheme(); const [settingsOpen, setSettingsOpen] = useState(false); const handleSettingsToggle = () => { setSettingsOpen(!settingsOpen); }; return ( {children} ); }; export default BoxedLayout; ================================================ FILE: src/core/components/ConfirmDialog.tsx ================================================ import Button from "@material-ui/core/Button"; import Dialog from "@material-ui/core/Dialog"; import DialogActions from "@material-ui/core/DialogActions"; import DialogContent from "@material-ui/core/DialogContent"; import DialogContentText from "@material-ui/core/DialogContentText"; import DialogTitle from "@material-ui/core/DialogTitle"; import LoadingButton from "@material-ui/lab/LoadingButton"; import { useTranslation } from "react-i18next"; import { ReactComponent as ConfirmSvg } from "../assets/confirm.svg"; import SvgContainer from "./SvgContainer"; type ConfirmDialogProps = { description?: string; onClose: () => void; onConfirm: () => void; open: boolean; pending: boolean; title: string; }; const ConfirmDialog = ({ description, onClose, onConfirm, open, pending, title, }: ConfirmDialogProps) => { const { t } = useTranslation(); return ( {title} {description && ( {description} )} {t("common.confirm")} ); }; export default ConfirmDialog; ================================================ FILE: src/core/components/Empty.tsx ================================================ import { ReactComponent as EmptySvg } from "../assets/empty.svg"; import Result from "./Result"; type EmptyProps = { message?: string; title: string; }; const Empty = ({ message, title }: EmptyProps) => { return } subTitle={message} title={title} />; }; export default Empty; ================================================ FILE: src/core/components/Footer.tsx ================================================ import Box from "@material-ui/core/Box"; import Link from "@material-ui/core/Link"; import Typography from "@material-ui/core/Typography"; import { Link as RouterLink } from "react-router-dom"; const Footer = () => { return ( {"© "} {process.env.REACT_APP_NAME} {" "} {new Date().getFullYear()} {"."} ); }; export default Footer; ================================================ FILE: src/core/components/Loader.tsx ================================================ import { useTheme } from "@material-ui/core/styles"; import Logo from "./Logo"; const Loader = () => { const theme = useTheme(); return ( ); }; export default Loader; ================================================ FILE: src/core/components/Logo.tsx ================================================ import Box, { BoxProps } from "@material-ui/core/Box"; import { ReactComponent as LogoSvg } from "../assets/logo.svg"; type LogoProps = { colored?: boolean; size?: number; } & BoxProps; const Logo = ({ colored = false, size = 40, ...boxProps }: LogoProps) => { return ( ); }; export default Logo; ================================================ FILE: src/core/components/PrivateRoute.tsx ================================================ import { Navigate, Route, RouteProps } from "react-router"; import { useAuth } from "../../auth/contexts/AuthProvider"; type PrivateRouteProps = { roles?: string[]; } & RouteProps; const PrivateRoute = ({ children, roles, ...routeProps }: PrivateRouteProps) => { const { hasRole, userInfo } = useAuth(); if (userInfo) { if (!hasRole(roles)) { return ; } return ; } else { return ; } }; export default PrivateRoute; ================================================ FILE: src/core/components/QueryWrapper.tsx ================================================ import Button from "@material-ui/core/Button"; import React from "react"; import { ErrorBoundary } from "react-error-boundary"; import { useTranslation } from "react-i18next"; import { useQueryErrorResetBoundary } from "react-query"; import Loader from "./Loader"; import Result from "./Result"; type QueryWrapperProps = { children: React.ReactNode; }; const QueryWrapper = ({ children }: QueryWrapperProps) => { const { reset } = useQueryErrorResetBoundary(); const { t } = useTranslation(); return ( ( resetErrorBoundary()} variant="contained"> {t("common.retry")} } status="error" subTitle={t("common.errors.unexpected.subTitle")} title={t("common.errors.unexpected.title")} /> )} > }>{children} ); }; export default QueryWrapper; ================================================ FILE: src/core/components/Result.tsx ================================================ import Box from "@material-ui/core/Box"; import Container from "@material-ui/core/Container"; import Typography from "@material-ui/core/Typography"; import React from "react"; import { ReactComponent as ErrorSvg } from "../assets/error.svg"; import { ReactComponent as SuccessSvg } from "../assets/success.svg"; import SvgContainer from "./SvgContainer"; type ResultImageProps = { customImage?: React.ReactNode; status?: "error" | "success"; }; const ResultImage = ({ customImage, status }: ResultImageProps) => { let image = customImage; if (!image) { if (status === "error") { image = ; } else if (status === "success") { image = ; } } return image ? {image} : null; }; type ResultProps = { extra?: React.ReactNode; image?: React.ReactNode; maxWidth?: "xs" | "sm"; status?: "error" | "success"; subTitle?: string; title: string; }; const Result = ({ extra, image, maxWidth = "xs", status, subTitle, title, }: ResultProps) => { return ( {title} {subTitle && {subTitle}} {extra && {extra}} ); }; export default Result; ================================================ FILE: src/core/components/SelectToolbar.tsx ================================================ import Box from "@material-ui/core/Box"; import Fab from "@material-ui/core/Fab"; import Toolbar from "@material-ui/core/Toolbar"; import Tooltip from "@material-ui/core/Tooltip"; import CloseIcon from "@material-ui/icons/Close"; import DeleteIcon from "@material-ui/icons/Delete"; import { useTranslation } from "react-i18next"; interface SelectToolbarProps { onCancel: () => void; onDelete: (userIds: string[]) => void; processing: boolean; selected: string[]; } const SelectToolbar = ({ onCancel, onDelete, processing, selected, }: SelectToolbarProps) => { const { t } = useTranslation(); const numSelected = selected.length; return ( {numSelected} {t("common.selected")} {numSelected > 0 && ( onDelete(selected)} > )} ); }; export default SelectToolbar; ================================================ FILE: src/core/components/SettingsDrawer.tsx ================================================ import Box from "@material-ui/core/Box"; import Drawer from "@material-ui/core/Drawer"; import FormControl from "@material-ui/core/FormControl"; import FormControlLabel from "@material-ui/core/FormControlLabel"; import IconButton from "@material-ui/core/IconButton"; import Radio from "@material-ui/core/Radio"; import RadioGroup from "@material-ui/core/RadioGroup"; import ToggleButton from "@material-ui/core/ToggleButton"; import ToggleButtonGroup from "@material-ui/core/ToggleButtonGroup"; import Typography from "@material-ui/core/Typography"; import CloseIcon from "@material-ui/icons/Close"; import { useTranslation } from "react-i18next"; import { drawerWidth } from "../config/layout"; import { useSettings } from "../contexts/SettingsProvider"; type SettingsDrawerProps = { onDrawerToggle: () => void; open: boolean; }; const SettingsDrawer = ({ onDrawerToggle, open }: SettingsDrawerProps) => { const { changeCollapsed, changeDirection, changeMode, collapsed, direction, mode, } = useSettings(); const { i18n, t } = useTranslation(); const handleDirectionChange = (_: any, direction: "ltr" | "rtl") => { changeDirection(direction); }; const handleLanguageChange = (event: React.ChangeEvent) => { i18n.changeLanguage((event.target as HTMLInputElement).value); }; const handleModeChange = (_: any, mode: string) => { changeMode(mode); }; const handleSidebarChange = (_: any, collapsed: boolean) => { changeCollapsed(collapsed); }; return ( {t("settings.drawer.title")} {t("settings.drawer.language.label")} } label={t("settings.drawer.language.options.en")} /> } label={t("settings.drawer.language.options.fr")} /> {t("settings.drawer.mode.label")} {t("settings.drawer.mode.options.light")} {t("settings.drawer.mode.options.dark")} {t("settings.drawer.direction.label")} {t("settings.drawer.direction.options.ltr")} {t("settings.drawer.direction.options.rtl")} {t("settings.drawer.sidebar.label")} {t("settings.drawer.sidebar.options.collapsed")} {t("settings.drawer.sidebar.options.full")} ); }; export default SettingsDrawer; ================================================ FILE: src/core/components/SvgContainer.tsx ================================================ import Box from "@material-ui/core/Box"; import { useTheme } from "@material-ui/core/styles"; import React from "react"; type SvgContainerProps = { children: React.ReactNode; }; const SvgContainer = ({ children }: SvgContainerProps) => { const theme = useTheme(); return ( {children} ); }; export default SvgContainer; ================================================ FILE: src/core/config/i18n.ts ================================================ import i18n from "i18next"; import LanguageDetector from "i18next-browser-languagedetector"; import Backend from "i18next-xhr-backend"; import { initReactI18next } from "react-i18next"; i18n .use(Backend) .use(LanguageDetector) .use(initReactI18next) // passes i18n down to react-i18next .init({ backend: { loadPath: `${process.env.PUBLIC_URL}/locales/{{lng}}/translation.json`, }, fallbackLng: "en", interpolation: { escapeValue: false, // not needed for react as it escapes by default }, supportedLngs: ["en", "fr"], }); ================================================ FILE: src/core/config/layout.ts ================================================ export const drawerCollapsedWidth = 104; export const drawerWidth = 280; ================================================ FILE: src/core/contexts/SettingsProvider.tsx ================================================ import { ThemeProvider as MuiThemeProvider } from "@material-ui/core"; import CssBaseline from "@material-ui/core/CssBaseline"; import AdapterDateFns from "@material-ui/lab/AdapterDateFns"; import LocalizationProvider from "@material-ui/lab/LocalizationProvider"; import React, { createContext, useContext, useEffect, useMemo, useState, } from "react"; import { useLocalStorage } from "../hooks/useLocalStorage"; import { createTheme } from "../theme"; interface SettingsContextInterface { collapsed: boolean; direction: string; mode: string; open: boolean; changeCollapsed: (collapsed: boolean) => void; changeDirection: (direction: "ltr" | "rtl") => void; changeMode: (mode: string) => void; toggleDrawer: () => void; } export const SettingsContext = createContext({} as SettingsContextInterface); type SettingsProviderProps = { children: React.ReactNode; }; const SettingsProvider = ({ children }: SettingsProviderProps) => { const [collapsed, setCollapsed] = useLocalStorage("sidebarcollapsed", false); const [direction, setDirection] = useLocalStorage("direction", "ltr"); const [mode, setMode] = useLocalStorage("mode", "light"); const [open, setOpen] = useState(false); useEffect(() => { document.body.dir = direction; }, [direction]); const theme = useMemo( () => createTheme(direction as "ltr" | "rtl", mode as "dark" | "light"), [direction, mode] ); const changeCollapsed = (collapsed: boolean) => { if (typeof collapsed === "boolean") { setCollapsed(collapsed); } }; const changeDirection = (direction: "ltr" | "rtl") => { if (direction) { setDirection(direction); } }; const changeMode = (mode: string) => { if (mode) { setMode(mode); } }; const toggleDrawer = () => { setOpen(!open); }; return ( {children} ); }; export function useSettings() { return useContext(SettingsContext); } export default SettingsProvider; ================================================ FILE: src/core/contexts/SnackbarProvider.tsx ================================================ import Alert, { Color } from "@material-ui/core/Alert"; import AlertTitle from "@material-ui/core/AlertTitle"; import Snackbar from "@material-ui/core/Snackbar"; import React, { createContext, useContext, useState } from "react"; import { useTranslation } from "react-i18next"; interface SnackbarContextInterface { error: (newMessage: string) => void; success: (newMessage: string) => void; } export const SnackbarContext = createContext({} as SnackbarContextInterface); type SnackbarProviderProps = { children: React.ReactNode; }; const SnackbarProvider = ({ children }: SnackbarProviderProps) => { const { t } = useTranslation(); const [open, setOpen] = useState(false); const [message, setMessage] = useState(""); const [title, setTitle] = useState(""); const [severity, setSeverity] = useState(undefined); const handleClose = ( event: React.SyntheticEvent | React.MouseEvent, reason?: string ) => { if (reason === "clickaway") { return; } setOpen(false); }; const error = (newMessage: string) => { setTitle(t("common.snackbar.error")); setMessage(newMessage); setSeverity("error"); setOpen(true); }; const success = (newMessage: string) => { setTitle(t("common.snackbar.success")); setMessage(newMessage); setSeverity("success"); setOpen(true); }; return ( {children} {title} {message} ); }; export function useSnackbar() { return useContext(SnackbarContext); } export default SnackbarProvider; ================================================ FILE: src/core/hooks/useDateLocale.ts ================================================ import { Locale } from "date-fns"; import en from "date-fns/locale/en-US"; import fr from "date-fns/locale/fr"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; const locales: { [key: string]: Locale } = { en, fr }; export function useDateLocale(): Locale | undefined { const [locale, setLocale] = useState(undefined); const { i18n } = useTranslation(); useEffect(() => { setLocale(locales[i18n.language]); }, [i18n.language]); return locale; } ================================================ FILE: src/core/hooks/useLocalStorage.ts ================================================ // https://usehooks.com/useLocalStorage/ import { useState } from "react"; export function useLocalStorage( key: string, initialValue: T ): [T, (value: T) => void] { // State to store our value // Pass initial state function to useState so logic is only executed once const [storedValue, setStoredValue] = useState(() => { try { // Get from local storage by key const item = window.localStorage.getItem(key); // Parse stored json or if none return initialValue return item ? JSON.parse(item) : initialValue; } catch (error) { // If error also return initialValue console.log(error); return initialValue; } }); // Return a wrapped version of useState's setter function that ... // ... persists the new value to localStorage. const setValue = (value: T) => { try { // Allow value to be a function so we have same API as useState const valueToStore = value instanceof Function ? value(storedValue) : value; // Save state setStoredValue(valueToStore); // Save to local storage window.localStorage.setItem(key, JSON.stringify(valueToStore)); } catch (error) { // A more advanced implementation would handle the error case console.log(error); } }; return [storedValue, setValue]; } ================================================ FILE: src/core/hooks/usePageTracking.ts ================================================ import { useEffect, useState } from "react"; import { useLocation } from "react-router-dom"; const usePageTracking = () => { const location = useLocation(); const [initialized, setInitialized] = useState(false); useEffect(() => { const trackingId = process.env.REACT_APP_GA_TRACKING_ID; if (trackingId) { setInitialized(true); } }, []); useEffect(() => { if (initialized) { (window as any).gtag("send", "page_view", { page_location: window.location.href, page_path: window.location.pathname, }); } }, [initialized, location]); }; export default usePageTracking; ================================================ FILE: src/core/pages/Forbidden.tsx ================================================ import Button from "@material-ui/core/Button"; import { useTranslation } from "react-i18next"; import { Link as RouterLink } from "react-router-dom"; import { ReactComponent as ForbiddenSvg } from "../assets/403.svg"; import Result from "../components/Result"; const Forbidden = () => { const { t } = useTranslation(); return ( {t("common.backHome")} } image={} maxWidth="sm" subTitle={t("common.errors.forbidden.subTitle")} title={t("common.errors.unexpected.title")} /> ); }; export default Forbidden; ================================================ FILE: src/core/pages/NotFound.tsx ================================================ import Button from "@material-ui/core/Button"; import { useTranslation } from "react-i18next"; import { Link as RouterLink } from "react-router-dom"; import Result from "../../core/components/Result"; import { ReactComponent as NotFoundSvg } from "../assets/404.svg"; const NotFound = () => { const { t } = useTranslation(); return ( {t("common.backHome")} } image={} maxWidth="sm" subTitle={t("common.errors.notFound.subTitle")} title={t("common.errors.notFound.title")} /> ); }; export default NotFound; ================================================ FILE: src/core/pages/UnderConstructions.tsx ================================================ import Button from "@material-ui/core/Button"; import { useTranslation } from "react-i18next"; import { Link as RouterLink } from "react-router-dom"; import Result from "../../core/components/Result"; import { ReactComponent as ConstructionsSvg } from "../assets/constructions.svg"; const UnderConstructions = () => { const { t } = useTranslation(); return ( {t("common.backHome")} } image={} maxWidth="sm" subTitle={t("common.errors.underConstructions.subTitle")} title={t("common.errors.underConstructions.title")} /> ); }; export default UnderConstructions; ================================================ FILE: src/core/theme/components.tsx ================================================ import { Theme } from "@material-ui/core"; import CheckCircle from "@material-ui/icons/CheckCircle"; import RadioButtonUnchecked from "@material-ui/icons/RadioButtonUnchecked"; import RemoveCircle from "@material-ui/icons/RemoveCircle"; export const createThemeComponents = (theme: Theme) => ({ MuiAccordion: { styleOverrides: { root: { borderRadius: theme.shape.borderRadius, marginBottom: theme.spacing(3), "&.Mui-expanded:last-of-type": { marginBottom: theme.spacing(3), }, "&:before": { content: "none", }, }, }, }, MuiAccordionDetails: { styleOverrides: { root: { padding: theme.spacing(1, 3, 3), }, }, }, MuiAccordionSummary: { styleOverrides: { root: { padding: theme.spacing(3), }, content: { margin: 0, }, }, }, MuiAppBar: { defaultProps: { elevation: 0, }, styleOverrides: { root: { "&.MuiAppBar-colorDefault": { backgroundColor: theme.palette.background.default, color: theme.palette.text.primary, }, }, }, }, MuiAvatar: { styleOverrides: { root: { color: "inherit", backgroundColor: theme.palette.background.default, }, }, }, MuiButton: { defaultProps: { disableElevation: true, }, styleOverrides: { root: { padding: "16px 24px", textTransform: "none" as any, }, label: { fontWeight: theme.typography.fontWeightMedium, }, text: { padding: "16px 16px", }, }, }, MuiButtonBase: { defaultProps: { disableRipple: true, // No more ripple, on the whole application }, }, MuiCardActions: { styleOverrides: { root: { justifyContent: "flex-end", padding: "0 24px 24px 24px", }, }, }, MuiCardContent: { styleOverrides: { root: { padding: theme.spacing(3), }, }, }, MuiCardHeader: { styleOverrides: { root: { padding: "24px 24px 0 24px", }, }, }, MuiCheckbox: { defaultProps: { checkedIcon: , indeterminateIcon: , icon: , }, }, MuiChip: { styleOverrides: { label: { fontWeight: theme.typography.fontWeightMedium, }, }, }, MuiDialogActions: { styleOverrides: { root: { padding: 24, }, }, }, MuiDialogTitle: { styleOverrides: { root: { padding: 24, "& .MuiTypography-root": { fontSize: "1.25rem", }, }, }, }, MuiDrawer: { styleOverrides: { paper: { border: "none; !important", }, }, }, MuiFab: { styleOverrides: { root: { boxShadow: "none", lineHeight: "inherit", textTransform: "none" as any, "&.MuiFab-secondary": { color: theme.palette.text.primary, }, }, }, }, MuiFilledInput: { defaultProps: { disableUnderline: true, }, styleOverrides: { root: { borderRadius: theme.shape.borderRadius, }, }, }, MuiInternalClock: { styleOverrides: { clock: { backgroundColor: theme.palette.background.default, }, }, }, MuiInternalDateTimePickerTabs: { styleOverrides: { tabs: { backgroundColor: theme.palette.background.default, "& MuiTabs-indicator": { height: 0, }, }, }, }, MuiLinearProgress: { styleOverrides: { root: { borderRadius: 16, height: 12, }, }, }, MuiList: { defaultProps: { disablePadding: true, }, }, MuiListItem: { styleOverrides: { root: { borderRadius: 16, paddingTop: 12, paddingBottom: 12, "&.Mui-selected": { backgroundColor: theme.palette.background.default, color: theme.palette.text.primary, }, }, }, }, MuiListItemIcon: { styleOverrides: { root: { minWidth: 40, }, }, }, MuiMenu: { styleOverrides: { list: { paddingRight: 8, paddingLeft: 8, }, }, }, MuiMenuItem: { styleOverrides: { root: { paddingTop: 12, paddingBottom: 12, }, }, }, MuiOutlinedInput: { styleOverrides: { input: { "&:-webkit-autofill": { WebkitBoxShadow: `0 0 0 30px ${theme.palette.background.paper} inset`, }, }, }, }, MuiPaper: { defaultProps: { elevation: 0, }, styleOverrides: { root: { backgroundImage: "none", }, }, }, MuiRadio: { defaultProps: { color: "primary" as "primary", }, }, MuiTab: { styleOverrides: { root: { borderRadius: "50rem", padding: "10px 16px", maxWidth: "initial !important", minHeight: "initial !important", minWidth: "initial !important", textTransform: "none" as any, "&.Mui-selected": { backgroundColor: theme.palette.background.paper, color: theme.palette.text.primary, }, }, }, }, MuiTableCell: { styleOverrides: { root: { borderBottom: `1px solid ${theme.palette.divider}`, padding: "24px 16px", }, sizeSmall: { padding: "12px 16px", }, }, }, MuiTimeline: { styleOverrides: { root: { padding: "0 0 0 16px", }, }, }, MuiTimelineContent: { styleOverrides: { root: { padding: "12px 16px", }, }, }, MuiToggleButton: { styleOverrides: { root: { color: theme.palette.text.secondary, borderRadius: "12px !important", border: "none", textTransform: "none" as any, "&.Mui-selected": { backgroundColor: theme.palette.background.paper, color: theme.palette.text.primary, }, }, }, }, MuiToggleButtonGroup: { styleOverrides: { root: { backgroundColor: theme.palette.background.default, padding: 5, }, }, }, }); ================================================ FILE: src/core/theme/index.ts ================================================ import { createTheme as createMuiTheme } from "@material-ui/core"; import { createThemeComponents } from "./components"; import mixins from "./mixins"; import { darkPalette, lightPalette } from "./palette"; import shape from "./shape"; import transitions from "./transitions"; import typography from "./typography"; export const createTheme = ( direction: "ltr" | "rtl", mode: "dark" | "light" ) => { const palette = mode === "dark" ? darkPalette : lightPalette; // Create base theme const baseTheme = createMuiTheme({ direction, mixins, palette, shape, transitions, typography, }); // Inject base theme to be used in components return createMuiTheme( { components: createThemeComponents(baseTheme), }, baseTheme ); }; ================================================ FILE: src/core/theme/mixins.ts ================================================ const mixins = { toolbar: { minHeight: 80, "@media (min-width:600px)": { minHeight: 104, }, "@media (min-width:0px) and (orientation: landscape)": { minHeight: 80, }, }, }; export default mixins; ================================================ FILE: src/core/theme/palette.ts ================================================ import { PaletteMode } from "@material-ui/core"; const palette = { grey: { "50": "#ECEFF1", "100": "#CFD8DC", "200": "#B0BEC5", "300": "#90A4AE", "400": "#78909C", "500": "#607D8B", "600": "#546E7A", "700": "#455A64", "800": "#37474F", "900": "#263238", }, }; export const darkPalette = { ...palette, contrastThreshold: 4.5, mode: "dark" as PaletteMode, error: { main: "#FF8A65", }, info: { main: "#4FC3F7", }, primary: { main: "#64B5F6", contrastText: palette.grey[900], }, secondary: { main: palette.grey[900], }, success: { main: "#81C784", }, warning: { main: "#FFD54F", }, text: { primary: palette.grey[100], secondary: palette.grey[300], disabled: palette.grey[600], }, divider: palette.grey[700], background: { paper: palette.grey[900], default: palette.grey[800], }, action: { selectedOpacity: 0, selected: palette.grey[800], }, }; export const lightPalette = { ...palette, contrastThreshold: 3, mode: "light" as PaletteMode, error: { main: "#FF3D00", }, info: { main: "#00B0FF", }, primary: { main: "#2962FF", contrastText: "#FFF", }, secondary: { main: "#FFF", }, success: { main: "#00E676", }, warning: { main: "#FFC400", }, text: { primary: palette.grey[700], secondary: palette.grey[500], disabled: palette.grey[300], }, divider: palette.grey[100], background: { paper: "#FFF", default: palette.grey[50], }, action: { selectedOpacity: 0, selected: palette.grey[50], }, }; ================================================ FILE: src/core/theme/shape.ts ================================================ const shape = { borderRadius: 16, }; export default shape; ================================================ FILE: src/core/theme/transitions.ts ================================================ const transitions = { duration: { shortest: 75, shorter: 100, short: 125, standard: 150, complex: 175, enteringScreen: 115, leavingScreen: 95, }, }; export default transitions; ================================================ FILE: src/core/theme/typography.ts ================================================ const typography = { fontFamily: "Nunito, sans-serif", fontWeightMedium: 700, fontWeightBold: 800, h1: { fontWeight: 800, fontSize: "2rem", letterSpacing: 0, }, h2: { fontWeight: 800, fontSize: "1.5rem", letterSpacing: 0, }, h3: { fontWeight: 800, fontSize: "1.375rem", letterSpacing: 0, }, h4: { fontWeight: 800, fontSize: "1.25rem", letterSpacing: 0, }, h5: { fontWeight: 800, fontSize: "1.125rem", letterSpacing: 0, }, h6: { fontWeight: 700, fontSize: "1rem", letterSpacing: 0, }, subtitle1: { letterSpacing: 0, }, subtitle2: { letterSpacing: 0, }, body1: { letterSpacing: 0, }, body2: { letterSpacing: 0, }, button: { letterSpacing: 0, }, caption: { letterSpacing: 0, }, overline: { letterSpacing: 0, }, }; export default typography; ================================================ FILE: src/core/utils/crudUtils.ts ================================================ export function addOne(items: T[] = [], newItem: T) { return [...items, newItem]; } export function removeOne( items: T[] = [], itemId: string ) { return items.filter((item) => item.id !== itemId); } export function removeMany( items: T[] = [], itemIds: string[] ) { return items.filter((item) => !itemIds.includes(item.id)); } export function updateOne( items: T[] = [], updatedItem: T ) { return items.map((item) => (item.id === updatedItem.id ? updatedItem : item)); } ================================================ FILE: src/core/utils/selectUtils.ts ================================================ export const selectAll = (list: any, key = "id") => list.map((item: any) => item[key]); export const selectOne = (selected: any, id: string) => { const selectedIndex = selected.indexOf(id); let newSelected: string[] = []; if (selectedIndex === -1) { newSelected = newSelected.concat(selected, id); } else if (selectedIndex === 0) { newSelected = newSelected.concat(selected.slice(1)); } else if (selectedIndex === selected.length - 1) { newSelected = newSelected.concat(selected.slice(0, -1)); } else if (selectedIndex > 0) { newSelected = newSelected.concat( selected.slice(0, selectedIndex), selected.slice(selectedIndex + 1) ); } return newSelected; }; ================================================ FILE: src/index.tsx ================================================ import React from "react"; import ReactDOM from "react-dom"; import { BrowserRouter } from "react-router-dom"; import App from "./App"; import "./core/config/i18n"; import "./mocks/server"; import reportWebVitals from "./reportWebVitals"; ReactDOM.render( , document.getElementById("root") ); // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals(); ================================================ FILE: src/landing/components/LandingLayout.tsx ================================================ import AppBar from "@material-ui/core/AppBar"; import IconButton from "@material-ui/core/IconButton"; import Paper from "@material-ui/core/Paper"; import Toolbar from "@material-ui/core/Toolbar"; import Typography from "@material-ui/core/Typography"; import SettingsIcon from "@material-ui/icons/Settings"; import React, { useState } from "react"; import Footer from "../../core/components/Footer"; import Logo from "../../core/components/Logo"; import SettingsDrawer from "../../core/components/SettingsDrawer"; type LandingLayoutProps = { children: React.ReactNode; }; const LandingLayout = ({ children }: LandingLayoutProps) => { const [settingsOpen, setSettingsOpen] = useState(false); const handleSettingsToggle = () => { setSettingsOpen(!settingsOpen); }; return ( {process.env.REACT_APP_NAME}
{children}