@campxdev/campx-web-utils 1.1.9 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/dist/cjs/index.js +1 -1
  2. package/dist/esm/index.js +1 -1
  3. package/dist/index.d.ts +2 -152
  4. package/export.ts +9 -0
  5. package/package.json +7 -4
  6. package/src/App.css +38 -0
  7. package/src/App.tsx +16 -0
  8. package/src/AppContent.tsx +19 -0
  9. package/src/components/ActivityLog.tsx +96 -0
  10. package/src/components/ChangePassword.tsx +183 -0
  11. package/src/config/axios.ts +146 -0
  12. package/src/context/ConfirmDialogProvider.tsx +78 -0
  13. package/src/context/ErrorBoundary/ErrorBoundary.tsx +131 -0
  14. package/src/context/ErrorBoundary/Login.tsx +206 -0
  15. package/src/context/Providers.tsx +76 -0
  16. package/src/context/SnackbarProvider.tsx +42 -0
  17. package/src/context/application-store.ts +86 -0
  18. package/src/context/export.ts +5 -0
  19. package/src/hooks/export.ts +1 -0
  20. package/src/hooks/useConfirm.ts +57 -0
  21. package/src/index.css +13 -0
  22. package/src/index.tsx +19 -0
  23. package/src/layout/AppLayout/AppLayout.tsx +155 -0
  24. package/src/layout/AppLayout/components/HelpDocs.tsx +121 -0
  25. package/src/logo.svg +1 -0
  26. package/src/react-app-env.d.ts +1 -0
  27. package/src/reportWebVitals.ts +15 -0
  28. package/src/routes/main.tsx +32 -0
  29. package/src/selectors/BatchSelector.tsx +15 -0
  30. package/src/selectors/CourseSelector.tsx +16 -0
  31. package/src/selectors/DepartmentSelector.tsx +19 -0
  32. package/src/selectors/EmployeesSelector.tsx +20 -0
  33. package/src/selectors/FeeTypeSelector.tsx +15 -0
  34. package/src/selectors/HostelBlocksSelector.tsx +19 -0
  35. package/src/selectors/HostelFloorSelector.tsx +19 -0
  36. package/src/selectors/HostelRoomSelector.tsx +20 -0
  37. package/src/selectors/PrintFormatSelector.tsx +17 -0
  38. package/src/selectors/ProgramSelector.tsx +16 -0
  39. package/src/selectors/RegulationSelector.tsx +19 -0
  40. package/src/selectors/SemesterSelector.tsx +16 -0
  41. package/src/selectors/YearRangeSelector.tsx +26 -0
  42. package/src/selectors/export.ts +13 -0
  43. package/src/selectors/utils.tsx +39 -0
  44. package/src/setupTests.ts +5 -0
  45. package/src/utils/constants.ts +7 -0
  46. package/src/utils/functions.tsx +11 -0
  47. package/dist/cjs/types/src/layout/AppLayout/AppLayoutV2.d.ts +0 -150
  48. package/dist/esm/types/src/layout/AppLayout/AppLayoutV2.d.ts +0 -150
@@ -0,0 +1,146 @@
1
+ import Axios from 'axios';
2
+ import Cookies from 'js-cookie';
3
+ import { ApplicationStore } from '../context/application-store';
4
+ import { institutionCode, isDevelopment, tenantCode } from '../utils/constants';
5
+
6
+ // Declare the extended axios types to include our custom property
7
+ declare module 'axios' {
8
+ export interface AxiosRequestConfig {
9
+ skipInstitutionCode?: boolean;
10
+ }
11
+ }
12
+
13
+ declare global {
14
+ interface ImportMeta {
15
+ env: {
16
+ VITE_APP_API_HOST?: string;
17
+ // Add other Vite environment variables as needed
18
+ };
19
+ }
20
+ }
21
+
22
+ // Get base URL from environment variables
23
+ const getBaseUrl = (): string => {
24
+ // For Vite
25
+ if (typeof import.meta !== 'undefined' && import.meta.env) {
26
+ return import.meta.env.VITE_APP_API_HOST || '';
27
+ }
28
+
29
+ // For Create React App
30
+ return process.env.REACT_APP_API_HOST || '';
31
+ };
32
+
33
+ // Format query parameters
34
+ const formatParams = (params: any) => {
35
+ return Object.fromEntries(
36
+ Object.entries(params ?? {}).map(([key, value]) => [
37
+ key,
38
+ value === '__empty__' ? '' : value,
39
+ ]),
40
+ );
41
+ };
42
+
43
+ // Create axios instance
44
+ export const axios = Axios.create({
45
+ baseURL: getBaseUrl(),
46
+ withCredentials: true,
47
+ headers: {
48
+ 'x-tenant-id': tenantCode,
49
+ 'x-institution-code': institutionCode,
50
+ },
51
+ });
52
+
53
+ // Request interceptor
54
+ axios.interceptors.request.use(
55
+ (config) => {
56
+ // Format query parameters
57
+ const params = formatParams(config?.params);
58
+
59
+ // Add session tokens from cookies if in development
60
+ const sessionKey = Cookies.get('campx_session_key');
61
+ const openPaymentsKey = Cookies.get('campx_open_payments_key');
62
+ const studentSessionKey = Cookies.get('campx_student_key');
63
+
64
+ if (isDevelopment && sessionKey) {
65
+ config.headers.set('campx_session_key', sessionKey);
66
+ }
67
+
68
+ if (isDevelopment && studentSessionKey) {
69
+ config.headers.set('campx_student_key', studentSessionKey);
70
+ }
71
+
72
+ if (openPaymentsKey) {
73
+ config.headers.set('campx_open_payments_key', openPaymentsKey);
74
+ }
75
+
76
+ // Skip sending X-institution-code if the flag is set
77
+ if (config.skipInstitutionCode) {
78
+ config.headers.delete('x-institution-code');
79
+ }
80
+
81
+ return {
82
+ ...config,
83
+ params,
84
+ };
85
+ },
86
+ (error) => {
87
+ return Promise.reject(error);
88
+ },
89
+ );
90
+
91
+ // Response interceptor
92
+ axios.interceptors.response.use(
93
+ (response) => {
94
+ // Check if the response has hideSnackbar property
95
+ const shouldHideSnackbar = response.data?.hideSnackbar === true;
96
+
97
+ // Handle successful responses
98
+ if (
99
+ !shouldHideSnackbar &&
100
+ response.config.method &&
101
+ ['put', 'post', 'delete', 'patch'].includes(response.config.method) &&
102
+ [200, 201, 202].includes(response.status)
103
+ ) {
104
+ // Use ApplicationStore for snackbar
105
+ ApplicationStore.update((s) => {
106
+ s.snackbar.open = true;
107
+ s.snackbar.message = response.data.message || 'Operation successful';
108
+ s.snackbar.severity = 'success';
109
+ });
110
+ }
111
+
112
+ return response;
113
+ },
114
+ (err) => {
115
+ // Check if the error response has hideSnackbar property
116
+ const shouldHideSnackbar = err.response?.data?.hideSnackbar === true;
117
+
118
+ // Handle error responses
119
+ if (!shouldHideSnackbar && [400, 422].includes(err.response?.status)) {
120
+ // Use ApplicationStore for error snackbar
121
+ ApplicationStore.update((s) => {
122
+ s.snackbar.open = true;
123
+ s.snackbar.message = err.response.data.message || 'Bad Request';
124
+ s.snackbar.severity = 'error';
125
+ });
126
+ }
127
+
128
+ if (
129
+ !shouldHideSnackbar &&
130
+ err.response?.config.method &&
131
+ ['put', 'post', 'delete', 'patch'].includes(err.response.config.method) &&
132
+ [404].includes(err.response.status)
133
+ ) {
134
+ // Use ApplicationStore for not found error snackbar
135
+ ApplicationStore.update((s) => {
136
+ s.snackbar.open = true;
137
+ s.snackbar.message = err.response.data.message || 'Not Found';
138
+ s.snackbar.severity = 'error';
139
+ });
140
+ }
141
+
142
+ return Promise.reject(err);
143
+ },
144
+ );
145
+
146
+ export default axios;
@@ -0,0 +1,78 @@
1
+ import { ConfirmDialog, ConfirmDialogType } from '@campxdev/react-blueprint';
2
+ import { createContext, ReactNode } from 'react';
3
+ import { ApplicationStore } from './application-store';
4
+
5
+ interface ConfirmStateProps {
6
+ showConfirmDialog: (
7
+ title: string,
8
+ message: string,
9
+ onConfirm: () => void,
10
+ onCancel: () => void,
11
+ type?: ConfirmDialogType,
12
+ confirmButtonText?: string,
13
+ cancelButtonText?: string,
14
+ ) => void;
15
+ }
16
+
17
+ const ConfirmContext = createContext<ConfirmStateProps>({
18
+ showConfirmDialog: () => {},
19
+ });
20
+
21
+ export const ConfirmDialogProvider = ({
22
+ children,
23
+ }: {
24
+ children: ReactNode;
25
+ }) => {
26
+ const confirmDialog = ApplicationStore.useState((s) => s.confirmDialog);
27
+
28
+ const showConfirmDialog = (
29
+ title: string,
30
+ message: string,
31
+ onConfirm: () => void,
32
+ onCancel: () => void,
33
+ type: ConfirmDialogType = 'confirm',
34
+ confirmButtonText?: string,
35
+ cancelButtonText?: string,
36
+ ) => {
37
+ ApplicationStore.update((s) => {
38
+ s.confirmDialog = {
39
+ isOpen: true,
40
+ title,
41
+ message,
42
+ onConfirm,
43
+ onCancel,
44
+ type,
45
+ confirmButtonText,
46
+ cancelButtonText,
47
+ };
48
+ });
49
+ };
50
+
51
+ const handleCloseDialog = () => {
52
+ ApplicationStore.update((s) => {
53
+ s.confirmDialog.isOpen = false;
54
+ });
55
+ };
56
+
57
+ return (
58
+ <ConfirmContext.Provider value={{ showConfirmDialog }}>
59
+ {children}
60
+ <ConfirmDialog
61
+ isOpen={confirmDialog.isOpen}
62
+ title={confirmDialog.title}
63
+ message={confirmDialog.message}
64
+ type={confirmDialog.type}
65
+ confirmButtonText={confirmDialog.confirmButtonText}
66
+ cancelButtonText={confirmDialog.cancelButtonText}
67
+ onConfirm={() => {
68
+ confirmDialog.onConfirm();
69
+ handleCloseDialog();
70
+ }}
71
+ onCancel={() => {
72
+ confirmDialog.onCancel();
73
+ handleCloseDialog();
74
+ }}
75
+ />
76
+ </ConfirmContext.Provider>
77
+ );
78
+ };
@@ -0,0 +1,131 @@
1
+ import {
2
+ Button,
3
+ Dialog,
4
+ InternalServerError,
5
+ NoInterneConnection,
6
+ ResourceNotFound,
7
+ UnAuthorized,
8
+ } from '@campxdev/react-blueprint';
9
+ import { Alert, Box, styled } from '@mui/material';
10
+ import Cookies from 'js-cookie';
11
+ import { ReactNode, useState } from 'react';
12
+ import { ErrorBoundary as ReactErrorBoundary } from 'react-error-boundary';
13
+ import { QueryErrorResetBoundary } from 'react-query';
14
+ import { useLocation, useNavigate } from 'react-router-dom';
15
+ import { Login } from './Login';
16
+
17
+ export type ErrorBoundaryProps = {
18
+ children: ReactNode;
19
+ };
20
+
21
+ const StyledAlert = styled(Alert)(({ theme }) => ({
22
+ height: '60px',
23
+ border: `1px solid ${theme.palette.error.main}`,
24
+ display: 'flex',
25
+ alignItems: 'center',
26
+ '& .MuiAlert-message': {
27
+ padding: 0,
28
+ },
29
+ '& .MuiTypography-root': {
30
+ margin: 0,
31
+ },
32
+ position: 'relative',
33
+ '& .retryBtn': {
34
+ color: '#661B2A',
35
+ position: 'absolute',
36
+ right: 8,
37
+ top: 8,
38
+ },
39
+ }));
40
+
41
+ export const ErrorBoundary = (props: ErrorBoundaryProps) => {
42
+ const location = useLocation();
43
+ return (
44
+ <QueryErrorResetBoundary>
45
+ {({ reset }) => (
46
+ <ReactErrorBoundary
47
+ key={location?.pathname}
48
+ onReset={reset}
49
+ FallbackComponent={ErrorFallback}
50
+ >
51
+ {props.children}
52
+ </ReactErrorBoundary>
53
+ )}
54
+ </QueryErrorResetBoundary>
55
+ );
56
+ };
57
+
58
+ const ErrorFallback = ({ error, resetErrorBoundary }: any) => {
59
+ if (error?.response?.status) {
60
+ switch (error?.response?.status) {
61
+ case 401:
62
+ return <UnAuth />;
63
+
64
+ case 500:
65
+ return <InternalServerError resetBoundary={resetErrorBoundary} />;
66
+ case 404:
67
+ return <ResourceNotFound message={error?.response?.data?.message} />;
68
+ }
69
+ }
70
+
71
+ if (error?.code === 'ERR_NETWORK') {
72
+ return <NoInterneConnection resetBoundary={resetErrorBoundary} />;
73
+ }
74
+
75
+ return (
76
+ <Box sx={{ marginTop: '16px', padding: '20px' }}>
77
+ <StyledAlert severity="error">
78
+ {error?.response?.data?.message ?? error?.message}
79
+ <Button
80
+ className="retryBtn"
81
+ onClick={() => resetErrorBoundary()}
82
+ size="small"
83
+ color="error"
84
+ variant="outlined"
85
+ >
86
+ Try Again
87
+ </Button>
88
+ </StyledAlert>
89
+ </Box>
90
+ );
91
+ };
92
+
93
+ const UnAuth = () => {
94
+ const navigate = useNavigate();
95
+ const [isModalOpen, setModalOpen] = useState(false);
96
+
97
+ const sessionCookie = Cookies.get('campx_session_key');
98
+
99
+ const handleLoginClick = () => {
100
+ if (window.location.hostname.split('.')[1] === 'localhost') {
101
+ setModalOpen(true);
102
+ } else {
103
+ if (!sessionCookie) {
104
+ navigate('/auth/login');
105
+ }
106
+ }
107
+ };
108
+
109
+ return (
110
+ <UnAuthorized
111
+ component={
112
+ <>
113
+ <Button
114
+ sx={{
115
+ marginTop: '20px',
116
+ }}
117
+ variant="contained"
118
+ onClick={handleLoginClick}
119
+ >
120
+ Click Here To Login
121
+ </Button>
122
+ <Dialog
123
+ open={isModalOpen}
124
+ onClose={() => setModalOpen(false)}
125
+ content={({ close }) => <Login close={close} />}
126
+ />
127
+ </>
128
+ }
129
+ />
130
+ );
131
+ };
@@ -0,0 +1,206 @@
1
+ import { Button, Icons, TextField } from '@campxdev/react-blueprint';
2
+ import { yupResolver } from '@hookform/resolvers/yup';
3
+ import { Box, Stack } from '@mui/material';
4
+ import DeviceDetector from 'device-detector-js';
5
+ import Cookies from 'js-cookie';
6
+ import { useEffect, useState } from 'react';
7
+ import { Controller, useForm } from 'react-hook-form';
8
+ import { useMutation } from 'react-query';
9
+
10
+ import { useNavigate } from 'react-router-dom';
11
+ import * as Yup from 'yup';
12
+ import { axios } from '../../config/axios';
13
+ import { isDevelopment } from '../../utils/constants';
14
+
15
+ type DeviceInformation = {
16
+ deviceType: string;
17
+ clientName: string;
18
+ os: string;
19
+ osVersion: string;
20
+ latitude: number | null;
21
+ longitude: number | null;
22
+ tokenType: string;
23
+ };
24
+
25
+ export type DeviceState = {
26
+ deviceInformation: DeviceInformation;
27
+ isLocationAllowed: boolean;
28
+ };
29
+
30
+ type LoginDetails = {
31
+ username: string;
32
+ password: string;
33
+ };
34
+
35
+ const performLogin = async (body: any) => {
36
+ if (!body.latitude || !body.longitude) {
37
+ const response = await fetch(
38
+ 'https://www.googleapis.com/geolocation/v1/geolocate?key=AIzaSyB2YCpo1yi107RYj1LdZu2DCcpcO93reFY',
39
+ {
40
+ method: 'POST',
41
+ headers: {
42
+ 'Content-Type': 'application/json',
43
+ },
44
+ body: JSON.stringify({
45
+ considerIp: 'true',
46
+ }),
47
+ },
48
+ );
49
+ const data = await response.json();
50
+ body.latitude = data.location.lat;
51
+ body.longitude = data.location.lng;
52
+ }
53
+ return axios.post(`/auth-server/auth-v2/login`, body).then((res) => res.data);
54
+ };
55
+
56
+ const validationSchema = Yup.object().shape({
57
+ username: Yup.string().required('Username is required'),
58
+ password: Yup.string().required('Password is required'),
59
+ });
60
+
61
+ export const Login = ({ close }: { close: () => void }) => {
62
+ const [deviceState, setDeviceState] = useState<DeviceState>({
63
+ deviceInformation: {
64
+ deviceType: 'browser',
65
+ clientName: 'unknown',
66
+ os: 'unknown',
67
+ osVersion: 'unknown',
68
+ latitude: null,
69
+ longitude: null,
70
+ tokenType: 'WEB',
71
+ },
72
+ isLocationAllowed: true,
73
+ });
74
+ const navigate = useNavigate();
75
+ useEffect(() => {
76
+ navigator.geolocation.getCurrentPosition((position) => {
77
+ setDeviceState((s) => ({
78
+ ...s,
79
+ deviceInformation: {
80
+ ...s.deviceInformation,
81
+ latitude: position.coords.latitude,
82
+ longitude: position.coords.longitude,
83
+ },
84
+ }));
85
+ });
86
+ navigator.permissions
87
+ .query({ name: 'geolocation' })
88
+ .then((permissionStatus) => {
89
+ if (permissionStatus.state === 'denied') {
90
+ setDeviceState((s) => ({
91
+ ...s,
92
+ isLocationAllowed: false,
93
+ }));
94
+ }
95
+ });
96
+
97
+ const detector = new DeviceDetector();
98
+ const deviceInfo = detector.parse(navigator.userAgent);
99
+ setDeviceState((s) => ({
100
+ ...s,
101
+ deviceInformation: {
102
+ ...s.deviceInformation,
103
+ clientName: deviceInfo.client?.name || s.deviceInformation.clientName,
104
+ os: deviceInfo.os?.name || s.deviceInformation.os,
105
+ osVersion: deviceInfo.os?.version || s.deviceInformation.osVersion,
106
+ },
107
+ }));
108
+ }, []);
109
+
110
+ const {
111
+ handleSubmit,
112
+ control,
113
+ formState: { errors },
114
+ } = useForm<LoginDetails>({
115
+ resolver: yupResolver(validationSchema),
116
+ });
117
+
118
+ const { mutate, isLoading } = useMutation(performLogin, {
119
+ onSuccess(data) {
120
+ if (isDevelopment) {
121
+ Cookies.remove('campx_session_key');
122
+ Cookies.set('campx_session_key', data.session.token);
123
+ close();
124
+ navigate(`/${data.session.institutionCode}/workspace`);
125
+ window.location.reload();
126
+ }
127
+ },
128
+ });
129
+
130
+ const onSubmit = async (body: LoginDetails) => {
131
+ mutate({
132
+ ...body,
133
+ ...deviceState.deviceInformation,
134
+ loginId: body.username,
135
+ loginType: 'USER',
136
+ });
137
+ };
138
+
139
+ return (
140
+ <>
141
+ <Stack gap="16px" justifyContent="space-between" alignItems="center">
142
+ <Box height="36px" width="194.8px">
143
+ <Icons.CampxFullLogoIcon />
144
+ </Box>
145
+ <Stack gap="10px" justifyContent="center" alignItems="center">
146
+ <Controller
147
+ control={control}
148
+ render={({ field }: { field: any }) => {
149
+ return (
150
+ <TextField
151
+ size="medium"
152
+ name="username"
153
+ sx={{
154
+ width: '400px',
155
+ }}
156
+ placeholder="Enter Username or Email"
157
+ containerProps={{ my: '0' }}
158
+ onChange={field.onChange}
159
+ label="Username or Email"
160
+ error={!!errors.username}
161
+ required
162
+ />
163
+ );
164
+ }}
165
+ name="username"
166
+ />
167
+ <Controller
168
+ control={control}
169
+ render={({ field }: { field: any }) => {
170
+ return (
171
+ <TextField
172
+ size="medium"
173
+ name="password"
174
+ sx={{
175
+ width: '400px',
176
+ }}
177
+ placeholder="Enter password"
178
+ onChange={field.onChange}
179
+ containerProps={{ my: '0' }}
180
+ label="Password"
181
+ type="password"
182
+ error={!!errors.password}
183
+ required
184
+ />
185
+ );
186
+ }}
187
+ name="password"
188
+ />
189
+ <Button
190
+ type="submit"
191
+ color="primary"
192
+ variant="contained"
193
+ sx={{
194
+ width: '400px',
195
+ }}
196
+ onClick={handleSubmit(onSubmit)}
197
+ disabled={isLoading}
198
+ loading={isLoading}
199
+ >
200
+ Login
201
+ </Button>
202
+ </Stack>
203
+ </Stack>
204
+ </>
205
+ );
206
+ };
@@ -0,0 +1,76 @@
1
+ import { lightTheme, MuiThemeProvider } from '@campxdev/react-blueprint';
2
+ import { LocalizationProvider } from '@mui/x-date-pickers';
3
+ import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
4
+ import {
5
+ combineReducers,
6
+ configureStore,
7
+ ReducersMapObject,
8
+ } from '@reduxjs/toolkit';
9
+ import { ReactNode, useEffect } from 'react';
10
+ import { QueryClient, QueryClientProvider } from 'react-query';
11
+ import { Provider } from 'react-redux';
12
+ import { BrowserRouter } from 'react-router-dom';
13
+ import { institutionCode, tenantCode, workspace } from '../utils/constants';
14
+ import { ConfirmDialogProvider } from './ConfirmDialogProvider';
15
+ import { SnackbarProvider } from './SnackbarProvider';
16
+
17
+ // We'll add this export to ensure Router is properly exported
18
+ export { BrowserRouter } from 'react-router-dom';
19
+
20
+ export const Providers = ({
21
+ children,
22
+ basename,
23
+ theme = lightTheme,
24
+ reducers = {},
25
+ }: {
26
+ children: ReactNode;
27
+ basename?: string;
28
+ theme?: any;
29
+ reducers?: ReducersMapObject;
30
+ }) => {
31
+ const queryClient = new QueryClient({
32
+ defaultOptions: {
33
+ queries: {
34
+ refetchOnWindowFocus: false,
35
+ retry: false,
36
+ useErrorBoundary: true,
37
+ },
38
+ },
39
+ });
40
+
41
+ var baseName =
42
+ tenantCode && institutionCode && workspace
43
+ ? `/${institutionCode}/${workspace}`
44
+ : '/';
45
+ useEffect(() => {
46
+ if (
47
+ window.location.pathname === '/' &&
48
+ institutionCode &&
49
+ workspace &&
50
+ tenantCode
51
+ ) {
52
+ window.location.replace(
53
+ window.location.origin + `/${institutionCode}/${workspace}`,
54
+ );
55
+ }
56
+ }, [institutionCode, tenantCode, workspace]);
57
+
58
+ const store = configureStore({
59
+ reducer: combineReducers(reducers),
60
+ });
61
+ return (
62
+ <Provider store={store}>
63
+ <BrowserRouter basename={basename ?? baseName}>
64
+ <QueryClientProvider client={queryClient}>
65
+ <MuiThemeProvider theme={theme}>
66
+ <SnackbarProvider>
67
+ <LocalizationProvider dateAdapter={AdapterDateFns}>
68
+ <ConfirmDialogProvider>{children}</ConfirmDialogProvider>
69
+ </LocalizationProvider>
70
+ </SnackbarProvider>
71
+ </MuiThemeProvider>
72
+ </QueryClientProvider>
73
+ </BrowserRouter>
74
+ </Provider>
75
+ );
76
+ };
@@ -0,0 +1,42 @@
1
+ import { Severity, Snackbar } from '@campxdev/react-blueprint';
2
+ import { createContext, ReactNode } from 'react';
3
+ import { ApplicationStore } from './application-store';
4
+
5
+ interface SnackbarContextProps {
6
+ showSnackbar: (message: string, severity?: Severity) => void;
7
+ }
8
+
9
+ const SnackbarContext = createContext<SnackbarContextProps | undefined>(
10
+ undefined,
11
+ );
12
+
13
+ interface SnackbarProviderProps {
14
+ children: ReactNode;
15
+ }
16
+
17
+ export const SnackbarProvider = ({ children }: SnackbarProviderProps) => {
18
+ const snackbar = ApplicationStore.useState((s) => s.snackbar);
19
+ const showSnackbar = (message: string, severity: Severity = 'info') => {
20
+ ApplicationStore.update((s) => {
21
+ s.snackbar.open = true;
22
+ s.snackbar.message = message;
23
+ s.snackbar.severity = severity;
24
+ });
25
+ };
26
+ return (
27
+ <SnackbarContext.Provider value={{ showSnackbar }}>
28
+ {children}
29
+ <Snackbar
30
+ open={snackbar.open}
31
+ message={snackbar.message}
32
+ severity={snackbar.severity}
33
+ autoHideDuration={1500}
34
+ onClose={() => {
35
+ ApplicationStore.update((s) => {
36
+ s.snackbar.open = false;
37
+ });
38
+ }}
39
+ />
40
+ </SnackbarContext.Provider>
41
+ );
42
+ };