@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.
- package/dist/cjs/index.js +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/index.d.ts +2 -152
- package/export.ts +9 -0
- package/package.json +7 -4
- package/src/App.css +38 -0
- package/src/App.tsx +16 -0
- package/src/AppContent.tsx +19 -0
- package/src/components/ActivityLog.tsx +96 -0
- package/src/components/ChangePassword.tsx +183 -0
- package/src/config/axios.ts +146 -0
- package/src/context/ConfirmDialogProvider.tsx +78 -0
- package/src/context/ErrorBoundary/ErrorBoundary.tsx +131 -0
- package/src/context/ErrorBoundary/Login.tsx +206 -0
- package/src/context/Providers.tsx +76 -0
- package/src/context/SnackbarProvider.tsx +42 -0
- package/src/context/application-store.ts +86 -0
- package/src/context/export.ts +5 -0
- package/src/hooks/export.ts +1 -0
- package/src/hooks/useConfirm.ts +57 -0
- package/src/index.css +13 -0
- package/src/index.tsx +19 -0
- package/src/layout/AppLayout/AppLayout.tsx +155 -0
- package/src/layout/AppLayout/components/HelpDocs.tsx +121 -0
- package/src/logo.svg +1 -0
- package/src/react-app-env.d.ts +1 -0
- package/src/reportWebVitals.ts +15 -0
- package/src/routes/main.tsx +32 -0
- package/src/selectors/BatchSelector.tsx +15 -0
- package/src/selectors/CourseSelector.tsx +16 -0
- package/src/selectors/DepartmentSelector.tsx +19 -0
- package/src/selectors/EmployeesSelector.tsx +20 -0
- package/src/selectors/FeeTypeSelector.tsx +15 -0
- package/src/selectors/HostelBlocksSelector.tsx +19 -0
- package/src/selectors/HostelFloorSelector.tsx +19 -0
- package/src/selectors/HostelRoomSelector.tsx +20 -0
- package/src/selectors/PrintFormatSelector.tsx +17 -0
- package/src/selectors/ProgramSelector.tsx +16 -0
- package/src/selectors/RegulationSelector.tsx +19 -0
- package/src/selectors/SemesterSelector.tsx +16 -0
- package/src/selectors/YearRangeSelector.tsx +26 -0
- package/src/selectors/export.ts +13 -0
- package/src/selectors/utils.tsx +39 -0
- package/src/setupTests.ts +5 -0
- package/src/utils/constants.ts +7 -0
- package/src/utils/functions.tsx +11 -0
- package/dist/cjs/types/src/layout/AppLayout/AppLayoutV2.d.ts +0 -150
- 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
|
+
};
|