From b05c997bc1318f1a8296a4632c188232eadf1c8f Mon Sep 17 00:00:00 2001 From: Bruno Tot Date: Thu, 14 Nov 2024 15:27:54 +0100 Subject: [PATCH] chore: frontend fixes for mobile table layout --- .../controllers/UserController.ts | 16 +-- .../app-vite-react/package.json | 1 - .../impl/ClientDatatable/ClientDatatable.tsx | 62 ++++++---- .../Datatable/impl/ClientDatatable/types.ts | 1 + .../src/app/components/Datatable/types.ts | 7 +- .../components/ClientResponsiveTable.tsx | 113 ++++++++++++++++++ .../components/ResponsiveTable.tsx | 104 ---------------- .../manage-users/components/Sidenav.tsx | 35 ------ .../manage-users/components/index.ts | 2 +- .../admin-settings/manage-users/index.tsx | 88 ++++++++++---- .../manage-users/pages/create-user/index.tsx | 10 +- .../manage-users/pages/edit-user/index.tsx | 6 + .../src/app/provider/ConfirmProvider.tsx | 4 +- .../src/app/provider/SnackbarProvider.tsx | 84 +++++++++++++ .../app-vite-react/src/app/providers.tsx | 2 + pnpm-lock.yaml | 21 +--- 16 files changed, 334 insertions(+), 222 deletions(-) create mode 100644 packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/components/ClientResponsiveTable.tsx delete mode 100644 packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/components/ResponsiveTable.tsx delete mode 100644 packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/components/Sidenav.tsx create mode 100644 packages/mern-sample-app/app-vite-react/src/app/provider/SnackbarProvider.tsx diff --git a/packages/mern-sample-app/app-node-express/src/app/infrastructure/controllers/UserController.ts b/packages/mern-sample-app/app-node-express/src/app/infrastructure/controllers/UserController.ts index a3bbdab2..ac8d4388 100644 --- a/packages/mern-sample-app/app-node-express/src/app/infrastructure/controllers/UserController.ts +++ b/packages/mern-sample-app/app-node-express/src/app/infrastructure/controllers/UserController.ts @@ -14,6 +14,14 @@ import { VALIDATORS } from "../validators"; export class UserController { @autowired() private userService: UserService; + @contract(contracts.User.findAll, withRouteSecured(Role.Enum["avr-admin"])) + async findAll(): RouteOutput { + return { + status: 200, + body: await this.userService.findAll(), + }; + } + @contract(contracts.User.findOneByUsername, withRouteSecured(Role.Enum["avr-admin"])) async findOneByUsername( payload: RouteInput, @@ -34,14 +42,6 @@ export class UserController { }; } - @contract(contracts.User.findAll, withRouteSecured(Role.Enum["avr-admin"])) - async findAll(): RouteOutput { - return { - status: 200, - body: await this.userService.findAll(), - }; - } - @contract(contracts.User.findAllPaginated, withRouteSecured(Role.Enum["avr-admin"])) async findAllPaginated( payload: RouteInput, diff --git a/packages/mern-sample-app/app-vite-react/package.json b/packages/mern-sample-app/app-vite-react/package.json index 5d68ef36..c3f5f1d5 100644 --- a/packages/mern-sample-app/app-vite-react/package.json +++ b/packages/mern-sample-app/app-vite-react/package.json @@ -37,7 +37,6 @@ "i18next-http-backend": "^2.5.0", "jwt-decode": "^4.0.0", "keycloak-js": "^25.0.4", - "material-ui-confirm": "^3.0.16", "material-ui-popup-state": "^5.1.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/impl/ClientDatatable/ClientDatatable.tsx b/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/impl/ClientDatatable/ClientDatatable.tsx index 8a69ce9d..5a7b3403 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/impl/ClientDatatable/ClientDatatable.tsx +++ b/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/impl/ClientDatatable/ClientDatatable.tsx @@ -2,6 +2,7 @@ import type { ClientDatatableProps } from "@org/app-vite-react/app/components/Da import type { DtBaseOrder } from "@org/app-vite-react/app/components/Datatable/types"; import type { MouseEvent } from "react"; +import * as mui from "@mui/material"; import { TableContainer, Table, @@ -13,14 +14,12 @@ import { } from "@mui/material"; import { DtSortableCell } from "@org/app-vite-react/app/components/Datatable/components/DtSortableCell/DtSortableCell"; import { DEFAULT_PAGINATION_OPTIONS } from "@org/app-vite-react/app/components/Datatable/types"; +import { ClientResponsiveTable } from "@org/app-vite-react/app/pages/admin-settings/manage-users/components"; import { Fragment, useMemo, useState } from "react"; - -export function ClientDatatable({ - data, - columns, - disablePagination = false, -}: ClientDatatableProps) { +export function ClientDatatable(props: ClientDatatableProps) { + const { data, columns, disablePagination = false, renderMobileRow } = props; + const matchesMobile = mui.useMediaQuery("(max-width:678px)"); const [sortData, setSortData] = useState([]); const [paginationOptions, setPaginationOptions] = useState(DEFAULT_PAGINATION_OPTIONS); @@ -69,6 +68,38 @@ export function ClientDatatable({ setSortData([{ id, direction: "desc" }]); }; + const paginationComponent = disablePagination ? ( + <> + ) : ( + `${from}-${to} to ${count}`} + rowsPerPageOptions={[5, 10, 25, 50, 100]} + count={data.length} + page={paginationOptions.page} + rowsPerPage={paginationOptions.rowsPerPage} + showFirstButton + showLastButton + onPageChange={(_, newPage) => onPageChange(newPage)} + onRowsPerPageChange={e => onRowsPerPageChange(+e.target.value)} + classes={{ toolbar: "toolbar-class" }} + /> + ); + + if (matchesMobile) { + return ( + <> + + {paginationComponent} + + ); + } + return ( <> @@ -112,7 +143,7 @@ export function ClientDatatable({ > {columns.map(({ id, align, renderBody }) => ( - {renderBody(item)} + {renderBody(item, { cleanup: () => {} })} ))} @@ -120,22 +151,7 @@ export function ClientDatatable({ - {!disablePagination && ( - `${from}-${to} to ${count}`} - rowsPerPageOptions={[10, 25, 50, 100]} - count={data.length} - page={paginationOptions.page} - rowsPerPage={paginationOptions.rowsPerPage} - showFirstButton - showLastButton - onPageChange={(_, newPage) => onPageChange(newPage)} - onRowsPerPageChange={e => onRowsPerPageChange(+e.target.value)} - classes={{ toolbar: "toolbar-class" }} - /> - )} + {paginationComponent} ); } diff --git a/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/impl/ClientDatatable/types.ts b/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/impl/ClientDatatable/types.ts index c30a542a..464f3f71 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/impl/ClientDatatable/types.ts +++ b/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/impl/ClientDatatable/types.ts @@ -10,4 +10,5 @@ export type ClientDatatableProps = { data: T[]; columns: DtClientColumn[]; disablePagination?: boolean; + renderMobileRow: (item: T) => React.ReactNode; }; diff --git a/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/types.ts b/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/types.ts index 40f94df0..c88bf6aa 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/types.ts +++ b/packages/mern-sample-app/app-vite-react/src/app/components/Datatable/types.ts @@ -1,9 +1,12 @@ import type { PaginationOptions } from "@org/lib-api-client"; import type { ReactNode } from "react"; +export type DtBaseColumnConfig = { + cleanup: () => void; +}; export type DtBaseColumnAlign = "left" | "center" | "right"; export type DtBaseColumnRenderHeader = () => ReactNode; -export type DtBaseColumnRenderBody = (value: T) => ReactNode; +export type DtBaseColumnRenderBody = (value: T, config: DtBaseColumnConfig) => ReactNode; export type DtBaseOrder = DtBaseSortItem[]; export type DtBaseSortItem = { id: string; direction: "asc" | "desc" }; export type DtBaseColumn = { @@ -16,7 +19,7 @@ export type DtBaseColumn = { export const DEFAULT_PAGINATION_OPTIONS: PaginationOptions = { order: [], page: 0, - rowsPerPage: 10, + rowsPerPage: 5, search: "", filters: {}, }; diff --git a/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/components/ClientResponsiveTable.tsx b/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/components/ClientResponsiveTable.tsx new file mode 100644 index 00000000..7fafbaca --- /dev/null +++ b/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/components/ClientResponsiveTable.tsx @@ -0,0 +1,113 @@ +import type { ClientDatatableProps } from "@org/app-vite-react/app/components/Datatable"; +import * as icons from "@mui/icons-material"; +import * as mui from "@mui/material"; +import React, { useState } from "react"; + +export type ClientResponsiveTableProps = ClientDatatableProps & { + renderRow: (item: T) => React.ReactNode; +}; + +export function ClientResponsiveTable({ + columns, + data, + disablePagination, + renderRow, +}: ClientResponsiveTableProps) { + const [selectedItem, setSelectedItem] = useState(null); + const [dialogOpen, setDialogOpen] = useState(false); + + const handleItemClick = (item: T) => { + setSelectedItem(item); + setDialogOpen(true); + }; + + const cleanup = () => { + setDialogOpen(false); + //setSelectedItem(null); // Reset selected item when dialog closes + }; + + return ( + <> + + {data.map((item, index) => ( + handleItemClick(item)} + sx={{ + cursor: "pointer", + //backgroundColor: "red", + borderRadius: 1, + //marginBottom: 1, + //padding: 2, + "&:hover": { + //backgroundColor: "rgba(255, 255, 255, 0.1)", + }, + }} + > + {renderRow(item)} + + ))} + + + + + Item Details + theme.palette.grey[500], + }} + > + + + + + + {selectedItem && ( + + {columns.map((column, idx) => ( + + + {column.renderHeader()} + + + {column.renderBody(selectedItem, { cleanup })} + + + ))} + + )} + + + + + Close + + + + + ); +} diff --git a/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/components/ResponsiveTable.tsx b/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/components/ResponsiveTable.tsx deleted file mode 100644 index 9cc3d63d..00000000 --- a/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/components/ResponsiveTable.tsx +++ /dev/null @@ -1,104 +0,0 @@ -import * as icons from "@mui/icons-material"; -import * as mui from "@mui/material"; -import React from "react"; - -export function ResponsiveTable() { - // Define data - const data = [ - { id: 1, name: "Item 1", description: "Description 1", detail: "Detail of Item 1" }, - { id: 2, name: "Item 2", description: "Description 2", detail: "Detail of Item 2" }, - { id: 3, name: "Item 3", description: "Description 3", detail: "Detail of Item 3" }, - { id: 4, name: "Item 4", description: "Description 4", detail: "Detail of Item 4" }, - // Add more items as needed - ]; - - // State variables - const [filterText, setFilterText] = React.useState(""); - const [filteredData, setFilteredData] = React.useState(data); - const [selectedItemIndex, setSelectedItemIndex] = React.useState(null); - - // Update filteredData when filterText changes - React.useEffect(() => { - const filtered = data.filter( - item => - item.name.toLowerCase().includes(filterText.toLowerCase()) || - item.description.toLowerCase().includes(filterText.toLowerCase()), - ); - setFilteredData(filtered); - setSelectedItemIndex(null); - }, [filterText]); - - // Handlers - const handleSelectItem = (index: number) => { - setSelectedItemIndex(index); - }; - - const handlePrevious = () => { - setSelectedItemIndex(prevIndex => - prevIndex !== null && prevIndex > 0 ? prevIndex - 1 : prevIndex, - ); - }; - - const handleNext = () => { - setSelectedItemIndex(prevIndex => - prevIndex !== null && prevIndex < filteredData.length - 1 ? prevIndex + 1 : prevIndex, - ); - }; - - return ( - - setFilterText(e.target.value)} - sx={{ mb: 2 }} - /> - - {selectedItemIndex !== null ? ( - // Display selected item - - {filteredData[selectedItemIndex].name} - - {filteredData[selectedItemIndex].description} - - {filteredData[selectedItemIndex].detail} - - } - > - Previous - - } - > - Next - - - - ) : ( - // Display list of items - - {filteredData.length === 0 ? ( - No items found. - ) : ( - - {filteredData.map((item, index) => ( - handleSelectItem(index)}> - - - ))} - - )} - - )} - - ); -} diff --git a/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/components/Sidenav.tsx b/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/components/Sidenav.tsx deleted file mode 100644 index 3212dd28..00000000 --- a/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/components/Sidenav.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import * as icons from "@mui/icons-material"; -import * as mui from "@mui/material"; - -export type SidenavProps = { - children: React.ReactNode; - open: boolean; - onClose: () => void; -}; - -export function Sidenav({ children, onClose, open }: SidenavProps) { - const theme = mui.useTheme(); - return ( - - - {theme.direction === "ltr" ? : } - - - {open ? children : null} - - ); -} diff --git a/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/components/index.ts b/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/components/index.ts index 00baacc8..7771b191 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/components/index.ts +++ b/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/components/index.ts @@ -1,3 +1,3 @@ export * from "./FixedBadge"; export * from "./UserForm"; -export * from "./ResponsiveTable"; +export * from "./ClientResponsiveTable"; diff --git a/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/index.tsx b/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/index.tsx index fc9f6b24..9ac7bf6f 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/index.tsx +++ b/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/index.tsx @@ -7,6 +7,9 @@ import { DatatableContainer, ClientDatatable } from "@org/app-vite-react/app/com import { DatatableFilterButton } from "@org/app-vite-react/app/components/Datatable/components/DatatableFilterButton"; import { InputText } from "@org/app-vite-react/app/forms"; import { useConfirmContext } from "@org/app-vite-react/app/provider/ConfirmProvider"; +import { useSnackbarContext } from "@org/app-vite-react/app/provider/SnackbarProvider"; +import { sigDirection } from "@org/app-vite-react/app/signals/sigDirection"; +import { sigUser } from "@org/app-vite-react/app/signals/sigUser"; import { tsrClient, tsrQuery } from "@org/app-vite-react/lib/@ts-rest"; import { useZodForm } from "@org/app-vite-react/lib/react-hook-form"; import { z } from "@org/lib-commons"; @@ -65,20 +68,12 @@ export default function ManageUsersPage() { const navigate = useNavigate(); const confirmAction = useConfirmContext(); + const snack = useSnackbarContext(); - const handleDeleteClick = (user: UserDto) => { - confirmAction({ - title: "Warning", - message: `Are you sure You want to delete user "${user.username}"?`, - onConfirm: async () => { - await tsrClient.User.deleteUser({ - query: { - id: user.id!, - }, - }); - refetch(); - }, - }); + const alignLeft = sigDirection.value === "rtl" ? "right" : "left"; + + const onSearch = (model: UserFilters) => { + setFilters(model); }; /*const [paginationOptions, setPaginationOptions] = useState({ @@ -101,20 +96,10 @@ export default function ManageUsersPage() { return
Error
; } - const onSearch = (model: UserFilters) => { - setFilters(model); - }; - - function handleEdit(user: UserDto) { - navigate(`/admin/users/${user.username}/edit`); - } - return ( <> {/*Manage users*/} - {/**/} - user.username} //paginationOptions={paginationOptions} //onPaginationOptionsChange={paginationOptions => setPaginationOptions(paginationOptions)} + renderMobileRow={user => ( + + + {user.username} + + + {user.email || "--"} + + + )} columns={[ { id: "username", renderHeader: () => "Username", renderBody: user => user.username, + align: alignLeft, sort: (o1, o2) => { return (o1.username ?? "").localeCompare(o2.username ?? ""); }, @@ -161,6 +168,7 @@ export default function ManageUsersPage() { id: "Email", renderHeader: () => "Email", renderBody: user => user.email || "-", + align: alignLeft, sort: (o1, o2) => { return (o1.email ?? "").localeCompare(o2.email ?? ""); }, @@ -169,6 +177,7 @@ export default function ManageUsersPage() { id: "First name", renderHeader: () => "First name", renderBody: user => user.firstName, + align: alignLeft, sort: (o1, o2) => { return (o1.firstName ?? "").localeCompare(o2.firstName ?? ""); }, @@ -177,6 +186,7 @@ export default function ManageUsersPage() { id: "Last name", renderHeader: () => "Last name", renderBody: user => user.lastName, + align: alignLeft, sort: (o1, o2) => { return (o1.lastName ?? "").localeCompare(o2.lastName ?? ""); }, @@ -185,11 +195,13 @@ export default function ManageUsersPage() { id: "roles", renderHeader: () => "Roles", renderBody: user => user.roles?.join(", "), + align: alignLeft, }, { id: "password", renderHeader: () => "Password", renderBody: user => (user.hasCredentials ? "Yes" : "No"), + align: alignLeft, sort: (o1, o2) => { return (o1.hasCredentials ? "Yes" : "No").localeCompare( o2.hasCredentials ? "Yes" : "No", @@ -199,16 +211,40 @@ export default function ManageUsersPage() { { id: "actions", renderHeader: () => "Actions", - renderBody: user => ( + align: alignLeft, + renderBody: (user, { cleanup }) => ( handleDeleteClick(user)} + disabled={user.username === sigUser.value?.username} + onClick={() => { + confirmAction({ + title: "Warning", + message: `Are you sure You want to delete user "${user.username}"?`, + onConfirm: async () => { + await tsrClient.User.deleteUser({ + query: { + id: user.id!, + }, + }); + snack({ + severity: "success", + body: `User "${user.username}" has been deleted.`, + }); + cleanup(); + refetch(); + }, + }); + }} > Delete - handleEdit(user)}> + navigate(`/admin/users/${user.username}/edit`)} + > Edit diff --git a/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/pages/create-user/index.tsx b/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/pages/create-user/index.tsx index 277fe809..fc07b49f 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/pages/create-user/index.tsx +++ b/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/pages/create-user/index.tsx @@ -1,10 +1,12 @@ import type { UserForm as UserFormModel } from "@org/lib-api-client"; +import { useSnackbarContext } from "@org/app-vite-react/app/provider/SnackbarProvider"; import { tsrClient } from "@org/app-vite-react/lib/@ts-rest"; import { useNavigate } from "react-router-dom"; import { UserForm } from "../../components"; +// TODO: Move this to shared package. // eslint-disable-next-line react-refresh/only-export-components export const DEFAULT_USER_FORM_STATE: UserFormModel = { id: "", @@ -19,15 +21,17 @@ export const DEFAULT_USER_FORM_STATE: UserFormModel = { export default function CreateUserPage() { const navigate = useNavigate(); + const snack = useSnackbarContext(); const handleSubmit = async (model: UserFormModel) => { - // Handle form submission - // eslint-disable-next-line no-console - console.log("Form submitted:", model); await tsrClient.User.createUser({ body: model, }); navigate("/admin/users"); + snack({ + body: `User ${model.username} successfully created`, + severity: "success", + }); }; return ( diff --git a/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/pages/edit-user/index.tsx b/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/pages/edit-user/index.tsx index de3a2797..197b8bf4 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/pages/edit-user/index.tsx +++ b/packages/mern-sample-app/app-vite-react/src/app/pages/admin-settings/manage-users/pages/edit-user/index.tsx @@ -1,5 +1,6 @@ import type { UserForm as UserFormModel } from "@org/lib-api-client"; +import { useSnackbarContext } from "@org/app-vite-react/app/provider/SnackbarProvider"; import { tsrClient } from "@org/app-vite-react/lib/@ts-rest"; import React from "react"; import { useNavigate, useParams } from "react-router-dom"; @@ -8,6 +9,7 @@ import { UserForm } from "../../components"; export default function EditUserPage() { const navigate = useNavigate(); + const snack = useSnackbarContext(); const { username: selectedUsername } = useParams<{ username: string }>(); const [selectedUserForm, setSelectedUserForm] = React.useState( @@ -34,6 +36,10 @@ export default function EditUserPage() { body: model, }); navigate("/admin/users"); + snack({ + body: "User updated successfully", + severity: "success", + }); }; return ( diff --git a/packages/mern-sample-app/app-vite-react/src/app/provider/ConfirmProvider.tsx b/packages/mern-sample-app/app-vite-react/src/app/provider/ConfirmProvider.tsx index 5df67059..571e8f5a 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/provider/ConfirmProvider.tsx +++ b/packages/mern-sample-app/app-vite-react/src/app/provider/ConfirmProvider.tsx @@ -6,7 +6,7 @@ export type ConfirmDialogProps = ConfirmVisualProps & { open: boolean; }; -export function ConfirmDialog({ +function ConfirmDialog({ title, message, open, @@ -43,6 +43,7 @@ export type ConfirmVisualProps = { onConfirm: () => Promise; }; +// eslint-disable-next-line react-refresh/only-export-components export function useConfirm() { const [open, setOpen] = React.useState(false); @@ -75,6 +76,7 @@ export const ConfirmContext = React.createContext< ((confirmVisualProps: ConfirmVisualProps) => void) | undefined >(undefined); +// eslint-disable-next-line react-refresh/only-export-components export function useConfirmContext() { const confirmAction = React.useContext(ConfirmContext); if (!confirmAction) { diff --git a/packages/mern-sample-app/app-vite-react/src/app/provider/SnackbarProvider.tsx b/packages/mern-sample-app/app-vite-react/src/app/provider/SnackbarProvider.tsx new file mode 100644 index 00000000..1f6e60f3 --- /dev/null +++ b/packages/mern-sample-app/app-vite-react/src/app/provider/SnackbarProvider.tsx @@ -0,0 +1,84 @@ +import * as mui from "@mui/material"; +import React from "react"; + +type SnackbarProps = { + severity?: NonNullable; + variant?: NonNullable; + autoHideDuration?: number; + open: boolean; + onClose: () => void; + body: React.ReactNode; +}; + +function Snackbar({ + body, + open, + onClose, + severity = "info", + variant = "filled", + autoHideDuration = 5000, +}: SnackbarProps) { + return ( + + + {body} + + + ); +} + +export type PartialSnackbarProps = Omit; + +// eslint-disable-next-line react-refresh/only-export-components +export function useSnackbar() { + const [open, setOpen] = React.useState(false); + + const onClose = () => { + setOpen(false); + }; + + const [localSnackbarProps, setLocalSnackbarProps] = React.useState({ + body: "", + }); + + const snack = (snackbarProps: PartialSnackbarProps) => { + setLocalSnackbarProps(snackbarProps); + setOpen(true); + }; + + const snackbarProps: SnackbarProps = { + ...localSnackbarProps, + open, + onClose, + }; + + return { snackbarProps, snack }; +} + +export const SnackbarContext = React.createContext< + ((snackbarProps: PartialSnackbarProps) => void) | undefined +>(undefined); + +// eslint-disable-next-line react-refresh/only-export-components +export function useSnackbarContext() { + const snack = React.useContext(SnackbarContext); + if (!snack) { + throw new Error("useSnackbarContext must be used within a SnackbarProvider"); + } + return snack; +} +export function SnackbarProvider({ children }: React.PropsWithChildren) { + const { snackbarProps, snack } = useSnackbar(); + + return ( + + + {children} + + ); +} diff --git a/packages/mern-sample-app/app-vite-react/src/app/providers.tsx b/packages/mern-sample-app/app-vite-react/src/app/providers.tsx index 35d1692d..8a07c3a9 100644 --- a/packages/mern-sample-app/app-vite-react/src/app/providers.tsx +++ b/packages/mern-sample-app/app-vite-react/src/app/providers.tsx @@ -1,4 +1,5 @@ import { ConfirmProvider } from "@org/app-vite-react/app/provider/ConfirmProvider"; +import { SnackbarProvider } from "@org/app-vite-react/app/provider/SnackbarProvider"; import { MuiStylesProvider, MuiThemeProvider } from "@org/app-vite-react/lib/@mui"; import { QueryClientProvider } from "@org/app-vite-react/lib/@tanstack"; import { KeycloakProvider } from "@org/app-vite-react/lib/keycloak-js"; @@ -7,6 +8,7 @@ import { type Provider } from "@org/app-vite-react/server/ReactApp"; export const providers = [ MuiStylesProvider, MuiThemeProvider, + SnackbarProvider, ConfirmProvider, KeycloakProvider, QueryClientProvider, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c2a23f7..b43f4e4f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -243,9 +243,6 @@ importers: keycloak-js: specifier: ^25.0.4 version: 25.0.6 - material-ui-confirm: - specifier: ^3.0.16 - version: 3.0.16(@mui/material@6.1.5)(react-dom@18.3.1)(react@18.3.1) material-ui-popup-state: specifier: ^5.1.0 version: 5.3.1(@mui/material@6.1.5)(react@18.3.1) @@ -3428,8 +3425,8 @@ packages: fsevents: 2.3.3 dev: true - /chromedriver@130.0.1: - resolution: {integrity: sha512-JH+OxDZ7gVv02r9oXwj4mQ8JCtj62g0fCD1LMUUYdB/4mPxn/E2ys+1IzXItoE7vXM9fGVc9R1akvXLqwwuSww==} + /chromedriver@131.0.0: + resolution: {integrity: sha512-ukYmdCox2eRsjpCYUB4AOLV1fSfWQ1ZPfcUc0PIUWZKoyjyXKEl8i4DJ14bcNzNbEvaVx2Z2pnx/nLK2CM+ruQ==} engines: {node: '>=18'} hasBin: true requiresBuild: true @@ -6185,7 +6182,7 @@ packages: dependencies: jwk-to-pem: 2.0.6 optionalDependencies: - chromedriver: 130.0.1 + chromedriver: 131.0.0 transitivePeerDependencies: - debug - supports-color @@ -6432,18 +6429,6 @@ packages: uc.micro: 2.1.0 dev: true - /material-ui-confirm@3.0.16(@mui/material@6.1.5)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-aJoa/FM/U/86qztoljlk8FWmjSJbAMzDWCdWbDqU5WwB0WzcWPyGrhBvIqihR9uKdHKBf1YrvMjn68uOrfsXAg==} - peerDependencies: - '@mui/material': '>= 5.0.0' - react: ^17.0.0 || ^18.0.0 - react-dom: ^17.0.0 || ^18.0.0 - dependencies: - '@mui/material': 6.1.5(@emotion/react@11.13.3)(@emotion/styled@11.13.0)(@types/react@18.3.11)(react-dom@18.3.1)(react@18.3.1) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - dev: false - /material-ui-popup-state@5.3.1(@mui/material@6.1.5)(react@18.3.1): resolution: {integrity: sha512-mmx1DsQwF/2cmcpHvS/QkUwOQG2oAM+cDEQU0DaZVYnvwKyTB3AFgu8l1/E+LQFausmzpSJoljwQSZXkNvt7eA==} engines: {node: '>=16'}