diff --git a/public/img/ats-logo-desktop.svg b/public/img/ats-logo-desktop.svg new file mode 100644 index 0000000..625b02c --- /dev/null +++ b/public/img/ats-logo-desktop.svg @@ -0,0 +1,26 @@ + + + + + + + + diff --git a/src/App.tsx b/src/App.tsx index a3fb757..0e8c7c8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,7 @@ import { Admin, Resource } from "react-admin"; import { Layout } from "./Layout"; import { AdminDashboard, PersonList, PersonShow, PersonEdit } from '@admin'; -import { authProvider, dataProvider } from "@core"; +import { authProvider, dataProvider, i18nProvider, darkTheme, lightTheme } from "@core"; export const App = () => ( ( dashboard={AdminDashboard} dataProvider={dataProvider} authProvider={authProvider} + i18nProvider={i18nProvider} + theme={lightTheme} + darkTheme={darkTheme} > ( - - {children} - - -); +export const Layout = ({ children }: { children: ReactNode }) => { + const [open] = useSidebarState(); + + return ( + + "dark" === theme.palette.mode ? '#313131' : '#fafafb', + position: "relative", + display: "flex", + flexDirection: "column", + minHeight: "100vw", + zIndex: 1, + }} + > + + + + "dark" === theme.palette.mode ? blueGrey[900] : "white", + position: "relative", + }} + direction="column" + > + + + {open && Ats Logo} + + + + + + + {children} + + + + + + ); +}; diff --git a/src/admin-components/admin-dashboard.tsx b/src/admin-components/admin-dashboard.tsx index 4d4601c..5a0d81d 100644 --- a/src/admin-components/admin-dashboard.tsx +++ b/src/admin-components/admin-dashboard.tsx @@ -3,7 +3,7 @@ import { Title } from "react-admin"; export const AdminDashboard = () => { return ( - + <CardContent> <Typography> diff --git a/src/admin-components/admin-menu.tsx b/src/admin-components/admin-menu.tsx index d229bff..b15751a 100644 --- a/src/admin-components/admin-menu.tsx +++ b/src/admin-components/admin-menu.tsx @@ -1,8 +1,20 @@ import { Menu } from "react-admin"; +import { DashboardTwoTone, Person4TwoTone } from '@mui/icons-material'; +import { AdminMenuItem, AdminMenuLabel } from "@core"; export const AdminMenu = () => ( <Menu> - <Menu.DashboardItem /> - <Menu.ResourceItem name="person" /> + <AdminMenuItem + to="/" + leftIcon={<DashboardTwoTone />} + primaryText="ra.page.dashboard" + /> + <AdminMenuLabel label="ra.menu.main.userManagement" id="people"> + <AdminMenuItem + to="/person" + leftIcon={<Person4TwoTone />} + primaryText="ra.menu.people" + /> + </AdminMenuLabel> </Menu> ); diff --git a/src/admin-components/resources/person/components/array-field.tsx b/src/admin-components/resources/person/components/array-field.tsx index 7885319..1b3422d 100644 --- a/src/admin-components/resources/person/components/array-field.tsx +++ b/src/admin-components/resources/person/components/array-field.tsx @@ -1,12 +1,16 @@ -import React, { FC } from 'react'; +import { FC } from "react"; import { isObject } from "lodash"; -import { useRecordContext } from 'react-admin'; -import { Chip, Stack } from '@mui/material'; +import { useRecordContext } from "react-admin"; +import { Chip, Stack } from "@mui/material"; interface FieldProps { source: string; } +interface People { + name: string; +} + export const ArrayField: FC<FieldProps> = ({ source }) => { const record = useRecordContext(); const objects = record?.[source]; @@ -15,10 +19,14 @@ export const ArrayField: FC<FieldProps> = ({ source }) => { return ( <Stack direction="row" gap={1}> - {objects.map((currentObject: string, key: number) => { + {objects.map((currentObject: People | string, key: number) => { if (isObject(currentObject)) { return ( - <Chip key={`${key}-people`} size="small" label={currentObject.name} /> + <Chip + key={`${key}-people`} + size="small" + label={currentObject.name} + /> ); } diff --git a/src/admin-components/resources/person/edit.tsx b/src/admin-components/resources/person/edit.tsx index 2d80f6d..e862b55 100644 --- a/src/admin-components/resources/person/edit.tsx +++ b/src/admin-components/resources/person/edit.tsx @@ -1,12 +1,11 @@ -import React from "react"; import { - BooleanField, BooleanInput, + BooleanInput, DateInput, Edit, Labeled, SimpleForm, TextInput, -} from 'react-admin'; +} from "react-admin"; import { Box, Card, CardContent, CardHeader, Stack } from "@mui/material"; export const PersonEdit = () => { @@ -33,7 +32,7 @@ export const PersonEdit = () => { <TextInput source="lastName" /> </Stack> </Labeled> - <TextInput source="email" fullWidth="false" /> + <TextInput source="email" /> <BooleanInput source="userAccess" label="Access to the Platform" diff --git a/src/admin-components/resources/person/list.tsx b/src/admin-components/resources/person/list.tsx index e6bbd9f..dd36825 100644 --- a/src/admin-components/resources/person/list.tsx +++ b/src/admin-components/resources/person/list.tsx @@ -1,4 +1,6 @@ -import React, { Fragment, useState } from "react"; +import { Fragment, useState } from "react"; +import { useQuery } from "@tanstack/react-query"; +import { Paper } from "@mui/material"; import { BooleanField, Datagrid, @@ -12,7 +14,6 @@ import { useDataProvider, WrapperField, } from "react-admin"; -import { useQuery } from "@tanstack/react-query"; import { ArrayField, FilterName } from "@admin"; export const PersonList = () => { @@ -45,41 +46,43 @@ export const PersonList = () => { </TopToolbar> } /> - <Datagrid - resource="person" - data={data?.content} - total={data?.totalElements} - isPending={isPending} - sort={undefined} - bulkActionButtons={false} - > - <TextField source="firstName" /> - <TextField source="lastName" /> - <TextField source="email" label="Email User ID" /> - <TextField source="id" label="User ID" /> - <TextField source="status" /> - <BooleanField source="userAccess" label="Access to the Platform" /> - <WrapperField label="Company"> - <ArrayField source="parties" /> - </WrapperField> - <WrapperField label="Programs assigned"> - <ArrayField source="assignedPrograms" /> - </WrapperField> - <WrapperField label="Functions assigned"> - <ArrayField source="assignedFunctions" /> - </WrapperField> - </Datagrid> - <ListContextProvider - value={{ - page, - perPage, - total: data?.totalElements ?? 0, - setPage, - setPerPage, - }} - > - <Pagination /> - </ListContextProvider> + <Paper elevation={3}> + <Datagrid + resource="person" + data={data?.content} + total={data?.totalElements} + isPending={isPending} + sort={undefined} + bulkActionButtons={false} + > + <TextField source="firstName" /> + <TextField source="lastName" /> + <TextField source="email" label="Email User ID" /> + <TextField source="id" label="User ID" /> + <TextField source="status" /> + <BooleanField source="userAccess" label="Access to the Platform" /> + <WrapperField label="Company"> + <ArrayField source="parties" /> + </WrapperField> + <WrapperField label="Programs assigned"> + <ArrayField source="assignedPrograms" /> + </WrapperField> + <WrapperField label="Functions assigned"> + <ArrayField source="assignedFunctions" /> + </WrapperField> + </Datagrid> + <ListContextProvider + value={{ + page, + perPage, + total: data?.totalElements ?? 0, + setPage, + setPerPage, + }} + > + <Pagination /> + </ListContextProvider> + </Paper> </Fragment> ); }; diff --git a/src/admin-components/resources/person/show.tsx b/src/admin-components/resources/person/show.tsx index 56c6343..06af1c1 100644 --- a/src/admin-components/resources/person/show.tsx +++ b/src/admin-components/resources/person/show.tsx @@ -21,7 +21,7 @@ export const PersonShow = () => { return ( <Show> <SimpleShowLayout> - <Card sx={{ border: 1 }}> + <Card> <CardHeader title="General info" /> <CardContent> <Stack @@ -76,7 +76,7 @@ export const PersonShow = () => { </Stack> </CardContent> </Card> - <Card sx={{ border: 1 }}> + <Card> <CardHeader title="Platform info" /> <CardContent> <Stack diff --git a/src/admin-core/authProvider.ts b/src/admin-core/authProvider.ts index 0eaf4c2..684ac63 100644 --- a/src/admin-core/authProvider.ts +++ b/src/admin-core/authProvider.ts @@ -12,14 +12,13 @@ export const authProvider: AuthProvider = { ); } - const responseCSRF = await csrf(); - - console.log(responseCSRF); - const { access_token } = await responseLogin.json(); localStorage.setItem("user", access_token); localStorage.setItem("token", access_token); + const responseCSRF = await csrf(); + console.log(responseCSRF); + return Promise.resolve(); }, logout: () => { diff --git a/src/admin-core/components/admin-appbar.tsx b/src/admin-core/components/admin-appbar.tsx new file mode 100644 index 0000000..71286ca --- /dev/null +++ b/src/admin-core/components/admin-appbar.tsx @@ -0,0 +1,43 @@ +/** + * This file is part of the SplendidBear Websites' projects. + * + * Copyright (c) 2023 @ www.splendidbear.org + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { Fragment } from "react"; +import { + AppBar, + LoadingIndicator, + TitlePortal, + ToggleThemeButton, +} from "react-admin"; +import { blueGrey, grey } from "@mui/material/colors"; + +export const AdminAppbar = () => ( + <AppBar + sx={{ + bgcolor: (theme) => + "dark" === theme.palette.mode ? blueGrey[800] : grey[200], + position: "relative", + minHeight: 50, + borderRadius: 0, + "& > .MuiToolbar-root": { + borderRadius: 0, + }, + boxShadow: "none", + color: (theme) => + "dark" === theme.palette.mode ? "white" : blueGrey[600], + }} + toolbar={ + <Fragment> + <ToggleThemeButton /> + <LoadingIndicator /> + </Fragment> + } + > + <TitlePortal /> + </AppBar> +); diff --git a/src/admin-core/components/admin-menu-item.tsx b/src/admin-core/components/admin-menu-item.tsx new file mode 100644 index 0000000..1b2b66b --- /dev/null +++ b/src/admin-core/components/admin-menu-item.tsx @@ -0,0 +1,53 @@ +/** + * This file is part of the SplendidBear Websites' projects. + * + * Copyright (c) 2024 @ www.splendidbear.org + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { FC, JSX, useMemo } from "react"; +import { isObject } from "lodash"; +import { Tooltip } from "@mui/material"; +import { Menu, useSidebarState, useTranslate } from "react-admin"; + +interface AdminMenuItemProps { + to: string; + leftIcon: JSX.Element; + primaryText: string; + primaryTextTooltip: string; +} + +export const AdminMenuItem: FC<AdminMenuItemProps> = ({ + to, + leftIcon, + primaryText, + primaryTextTooltip = "n/a", + ...rest +}) => { + const t = useTranslate(); + const [open] = useSidebarState(); + const isTextObject = useMemo(() => isObject(primaryText), [primaryText]); + + if (!open) { + return ( + <Tooltip + title={t(isTextObject ? primaryTextTooltip : primaryText)} + placement="right" + arrow + > + <Menu.Item to={to} leftIcon={leftIcon} {...rest} /> + </Tooltip> + ); + } + + return ( + <Menu.Item + to={to} + leftIcon={leftIcon} + primaryText={isTextObject ? primaryText : t(primaryText)} + {...rest} + /> + ); +}; diff --git a/src/admin-core/components/admin-menu-label.tsx b/src/admin-core/components/admin-menu-label.tsx new file mode 100644 index 0000000..32d358e --- /dev/null +++ b/src/admin-core/components/admin-menu-label.tsx @@ -0,0 +1,56 @@ +/** + * This file is part of the SplendidBear Websites' projects. + * + * Copyright (c) 2025 @ www.splendidbear.org + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import { FC, Fragment, JSX, useState } from "react"; +import { motion } from "framer-motion"; +import { Collapse, Divider, Stack } from "@mui/material"; +import { ChevronLeft } from "@mui/icons-material"; +import { grey } from "@mui/material/colors"; +import { useSidebarState, useTranslate } from "react-admin"; + +interface AdminMenuLabelProps { + label: string; + children: JSX.Element[]; + id: string; + isDefaultOpen: boolean; +} + +export const AdminMenuLabel: FC<AdminMenuLabelProps> = ({ + label, + children, + isDefaultOpen = false, +}) => { + const t = useTranslate(); + const [open] = useSidebarState(); + const [isMenuOpened, setIsMenuOpened] = useState(isDefaultOpen); + + return ( + <Fragment> + <Stack + sx={{ + justifyContent: "space-between", + alignItems: "center", + m: "5px 12px", + cursor: "pointer", + }} + direction="row" + onClick={() => setIsMenuOpened(!isMenuOpened)} + > + <Divider textAlign="left">{open && t(label)}</Divider> + <motion.div + animate={{ rotate: isMenuOpened ? -90 : 0 }} + transition={{ duration: 0.25 }} + > + <ChevronLeft sx={{ color: grey[400] }} /> + </motion.div> + </Stack> + <Collapse in={isMenuOpened}>{children}</Collapse> + </Fragment> + ); +}; diff --git a/src/admin-core/components/admin-sidebar.tsx b/src/admin-core/components/admin-sidebar.tsx new file mode 100644 index 0000000..1762bdf --- /dev/null +++ b/src/admin-core/components/admin-sidebar.tsx @@ -0,0 +1,45 @@ +import type { ReactNode } from "react"; +import PropTypes from "prop-types"; +import { Box } from "@mui/material"; +import { useLocale, useSidebarState } from "react-admin"; +import { blueGrey } from "@mui/material/colors"; + +export const AdminSidebar = ({ children }: { children: ReactNode }) => { + const [open] = useSidebarState(); + /** force redraw on locale change */ + useLocale(); + + return ( + <Box + sx={{ + "& > .MuiPaper-root": { + bgcolor: blueGrey[900], + width: open ? "250px" : "50px", + }, + "& .MuiMenuItem-root": { + padding: open ? "6px 16px" : 0, + ...(open + ? {} + : { + display: "flex", + alignItems: "center", + justifyContent: "center", + width: "100%", + "&.RaMenuItemLink-active": { + borderRadius: 0, + }, + }), + }, + "& .MuiChip-label": { + color: "white", + }, + }} + > + {children} + </Box> + ); +}; + +AdminSidebar.propTypes = { + children: PropTypes.node.isRequired, +}; diff --git a/src/admin-core/components/index.ts b/src/admin-core/components/index.ts new file mode 100644 index 0000000..99a0a9a --- /dev/null +++ b/src/admin-core/components/index.ts @@ -0,0 +1,4 @@ +export { AdminSidebar } from './admin-sidebar'; +export { AdminAppbar } from './admin-appbar'; +export { AdminMenuItem } from './admin-menu-item'; +export { AdminMenuLabel } from './admin-menu-label'; diff --git a/src/admin-core/hooks/useQueryEngine.ts b/src/admin-core/hooks/useQueryEngine.ts index f47915f..1b00121 100644 --- a/src/admin-core/hooks/useQueryEngine.ts +++ b/src/admin-core/hooks/useQueryEngine.ts @@ -4,10 +4,8 @@ export const useQueryEngine = () => { "Content-Type": "application/json", Origin: "http://localhost:3000", Referer: "http://localhost:3000", - "Access-Control-Allow-Origin": "http://localhost:3000", - "Access-Control-Allow-Headers": + "Access-Control-Request-Headers": "Origin, X-Requested-With, Content-Type, Accept, X-XSRF-TOKEN, Authorization, Cookie", - "Access-Control-Allow-Credentials": true, }; const response = async (request: Request) => { @@ -53,7 +51,9 @@ export const useQueryEngine = () => { ...defaultHeaders, Authorization: `Bearer ${localStorage.getItem("user")}`, "X-XSRF-TOKEN": localStorage.getItem("user"), - "Access-Control-Request-Method": options?.method ? options.method : "GET", + "Access-Control-Request-Method": options?.method + ? options.method + : "GET", ...(options?.headers ? { ...options.headers } : {}), }), }), diff --git a/src/admin-core/i18nProvider.ts b/src/admin-core/i18nProvider.ts new file mode 100644 index 0000000..a10e22d --- /dev/null +++ b/src/admin-core/i18nProvider.ts @@ -0,0 +1,14 @@ +import polyglotI18nProvider from "ra-i18n-polyglot"; +import fr from "ra-language-french"; +import { enMessages } from './messages'; + +const translations = { en: enMessages, fr }; + +export const i18nProvider = polyglotI18nProvider( + (locale: string) => translations[locale], + "en", // default locale + [ + { locale: "en", name: "English" }, + { locale: "fr", name: "Français" }, + ], +); diff --git a/src/admin-core/index.ts b/src/admin-core/index.ts index bbdbaa9..41888cd 100644 --- a/src/admin-core/index.ts +++ b/src/admin-core/index.ts @@ -1,4 +1,8 @@ export { dataProviderExtension } from './data-provider-extension'; -export { dataProvider } from './dataProvider.ts'; -export { authProvider } from './authProvider.ts'; +export { dataProvider } from './dataProvider'; +export { authProvider } from './authProvider'; +export { lightTheme, darkTheme } from './theme'; +export { i18nProvider } from './i18nProvider'; export * from './hooks'; +export * from './components'; +export * from './messages'; diff --git a/src/admin-core/messages/en-messages.ts b/src/admin-core/messages/en-messages.ts new file mode 100644 index 0000000..52604d6 --- /dev/null +++ b/src/admin-core/messages/en-messages.ts @@ -0,0 +1,13 @@ +import en from "ra-language-english"; +import { deepmerge } from "@mui/utils"; + +export const enMessages = deepmerge(en, { + ra: { + menu: { + main: { + userManagement: "User management", + }, + people: "Users", + }, + }, +}); diff --git a/src/admin-core/messages/index.ts b/src/admin-core/messages/index.ts new file mode 100644 index 0000000..b1baf63 --- /dev/null +++ b/src/admin-core/messages/index.ts @@ -0,0 +1 @@ +export { enMessages } from './en-messages'; diff --git a/src/admin-core/theme.ts b/src/admin-core/theme.ts new file mode 100644 index 0000000..4cfbc95 --- /dev/null +++ b/src/admin-core/theme.ts @@ -0,0 +1,16 @@ +import { defaultTheme, RaThemeOptions } from "react-admin"; +import { deepmerge } from "@mui/utils"; + +export const lightTheme: RaThemeOptions = deepmerge(defaultTheme, { + components: { + '& .Apex-Sidebar': { + + } + }, +}); + +export const darkTheme: RaThemeOptions = deepmerge(lightTheme, { + palette: { + mode: "dark", + }, +}); diff --git a/tsconfig.json b/tsconfig.json index ea9d0cd..f54711e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,5 +7,15 @@ { "path": "./tsconfig.node.json" } - ] + ], + "compilerOptions": { + "paths": { + "@admin*": [ + "./src/admin-components/*" + ], + "@core*": [ + "./src/admin-core/*" + ], + } + } }