@campxdev/campx-web-utils 1.1.8 → 1.2.0

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 (43) hide show
  1. package/export.ts +9 -0
  2. package/package.json +7 -4
  3. package/src/App.css +38 -0
  4. package/src/App.tsx +16 -0
  5. package/src/AppContent.tsx +19 -0
  6. package/src/components/ActivityLog.tsx +96 -0
  7. package/src/components/ChangePassword.tsx +183 -0
  8. package/src/config/axios.ts +146 -0
  9. package/src/context/ConfirmDialogProvider.tsx +78 -0
  10. package/src/context/ErrorBoundary/ErrorBoundary.tsx +131 -0
  11. package/src/context/ErrorBoundary/Login.tsx +206 -0
  12. package/src/context/Providers.tsx +76 -0
  13. package/src/context/SnackbarProvider.tsx +42 -0
  14. package/src/context/application-store.ts +86 -0
  15. package/src/context/export.ts +5 -0
  16. package/src/hooks/export.ts +1 -0
  17. package/src/hooks/useConfirm.ts +57 -0
  18. package/src/index.css +13 -0
  19. package/src/index.tsx +19 -0
  20. package/src/layout/AppLayout/AppLayout.tsx +155 -0
  21. package/src/layout/AppLayout/components/HelpDocs.tsx +121 -0
  22. package/src/logo.svg +1 -0
  23. package/src/react-app-env.d.ts +1 -0
  24. package/src/reportWebVitals.ts +15 -0
  25. package/src/routes/main.tsx +32 -0
  26. package/src/selectors/BatchSelector.tsx +15 -0
  27. package/src/selectors/CourseSelector.tsx +16 -0
  28. package/src/selectors/DepartmentSelector.tsx +19 -0
  29. package/src/selectors/EmployeesSelector.tsx +20 -0
  30. package/src/selectors/FeeTypeSelector.tsx +15 -0
  31. package/src/selectors/HostelBlocksSelector.tsx +19 -0
  32. package/src/selectors/HostelFloorSelector.tsx +19 -0
  33. package/src/selectors/HostelRoomSelector.tsx +20 -0
  34. package/src/selectors/PrintFormatSelector.tsx +17 -0
  35. package/src/selectors/ProgramSelector.tsx +16 -0
  36. package/src/selectors/RegulationSelector.tsx +19 -0
  37. package/src/selectors/SemesterSelector.tsx +16 -0
  38. package/src/selectors/YearRangeSelector.tsx +26 -0
  39. package/src/selectors/export.ts +13 -0
  40. package/src/selectors/utils.tsx +39 -0
  41. package/src/setupTests.ts +5 -0
  42. package/src/utils/constants.ts +7 -0
  43. package/src/utils/functions.tsx +11 -0
@@ -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
+ };
@@ -0,0 +1,86 @@
1
+ import { ConfirmDialogType, Severity } from '@campxdev/react-blueprint';
2
+ import { Store } from 'pullstate';
3
+
4
+ type Classroom = {
5
+ id: number;
6
+ batchName?: string;
7
+ branchCode?: string;
8
+ courseId: number;
9
+ currentSemester: number;
10
+ name: string;
11
+ programId: number;
12
+ regulationId?: number;
13
+ section?: string;
14
+ timetableSlotTemplateId?: number;
15
+ };
16
+
17
+ export type ApplicationStoreType = {
18
+ isLoginDialogOpen: boolean;
19
+ snackbar: {
20
+ open: boolean;
21
+ message: string;
22
+ severity: Severity;
23
+ };
24
+ confirmDialog: {
25
+ isOpen: boolean;
26
+ title: string;
27
+ message: string;
28
+ onConfirm: () => void;
29
+ onCancel: () => void;
30
+ type: ConfirmDialogType;
31
+ confirmButtonText?: string;
32
+ cancelButtonText?: string;
33
+ };
34
+ user?: {
35
+ globalUserId: string;
36
+ tenantId: string;
37
+ uniqueId: number;
38
+ fullName: string;
39
+ email?: string;
40
+ employeeId?: string;
41
+ departmentIds?: string[];
42
+ designationId?: string;
43
+ institutionIds: number[];
44
+ isAdminUser?: boolean;
45
+ isSuperUser?: boolean;
46
+ studentId?: number;
47
+ admissionId?: string;
48
+ mStudentId?: string;
49
+ semNo?: number;
50
+ rollNo?: string;
51
+ courseId?: number;
52
+ branchCode?: string;
53
+ classroom?: Classroom;
54
+ };
55
+ institutionInfo?: {
56
+ institutions?: [];
57
+ current?: any;
58
+ };
59
+ asset?: {
60
+ favIcon?: string;
61
+ institutionLogo?: string;
62
+ };
63
+ };
64
+
65
+ export const initialApplicationState: ApplicationStoreType = {
66
+ isLoginDialogOpen: false,
67
+ snackbar: {
68
+ open: false,
69
+ message: '',
70
+ severity: 'success',
71
+ },
72
+ confirmDialog: {
73
+ isOpen: false,
74
+ title: '',
75
+ message: '',
76
+ onConfirm: () => {},
77
+ onCancel: () => {},
78
+ type: 'confirm',
79
+ },
80
+ user: undefined,
81
+ institutionInfo: undefined,
82
+ asset: undefined,
83
+ };
84
+ export const ApplicationStore = new Store<ApplicationStoreType>(
85
+ initialApplicationState,
86
+ );
@@ -0,0 +1,5 @@
1
+ export * from './application-store';
2
+ export * from './ConfirmDialogProvider';
3
+ export * from './ErrorBoundary/ErrorBoundary';
4
+ export * from './Providers';
5
+ export * from './SnackbarProvider';
@@ -0,0 +1 @@
1
+ export * from './useConfirm';
@@ -0,0 +1,57 @@
1
+ import { ConfirmDialogType } from '@campxdev/react-blueprint';
2
+ import { ApplicationStore } from '../context/application-store';
3
+
4
+ const defaultConfirmDialogState = {
5
+ isOpen: false,
6
+ title: '',
7
+ message: '',
8
+ confirmButtonText: '',
9
+ cancelButtonText: '',
10
+ type: 'confirm' as ConfirmDialogType,
11
+ onConfirm: () => {},
12
+ onCancel: () => {},
13
+ };
14
+
15
+ export const useConfirm = () => {
16
+ const isConfirmed = ({
17
+ title,
18
+ message,
19
+ confirmButtonText,
20
+ cancelButtonText,
21
+ type = 'confirm',
22
+ }: {
23
+ title: string;
24
+ message: string;
25
+ confirmButtonText?: string;
26
+ cancelButtonText?: string;
27
+ type?: ConfirmDialogType;
28
+ }): Promise<boolean> =>
29
+ new Promise((resolve) => {
30
+ ApplicationStore.update((s) => {
31
+ s.confirmDialog = {
32
+ isOpen: true,
33
+ title,
34
+ message,
35
+ confirmButtonText,
36
+ cancelButtonText,
37
+ type,
38
+ onConfirm: () => {
39
+ resolve(true);
40
+ resetDialog();
41
+ },
42
+ onCancel: () => {
43
+ resolve(false);
44
+ resetDialog();
45
+ },
46
+ };
47
+ });
48
+ });
49
+
50
+ const resetDialog = () => {
51
+ ApplicationStore.update((s) => {
52
+ s.confirmDialog = defaultConfirmDialogState;
53
+ });
54
+ };
55
+
56
+ return isConfirmed;
57
+ };
package/src/index.css ADDED
@@ -0,0 +1,13 @@
1
+ body {
2
+ margin: 0;
3
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4
+ 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5
+ sans-serif;
6
+ -webkit-font-smoothing: antialiased;
7
+ -moz-osx-font-smoothing: grayscale;
8
+ }
9
+
10
+ code {
11
+ font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12
+ monospace;
13
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import ReactDOM from 'react-dom/client';
3
+ import App from './App';
4
+ import './index.css';
5
+ import reportWebVitals from './reportWebVitals';
6
+
7
+ const root = ReactDOM.createRoot(
8
+ document.getElementById('root') as HTMLElement,
9
+ );
10
+ root.render(
11
+ <React.StrictMode>
12
+ <App />
13
+ </React.StrictMode>,
14
+ );
15
+
16
+ // If you want to start measuring performance in your app, pass a function
17
+ // to log results (for example: reportWebVitals(console.log))
18
+ // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
19
+ reportWebVitals();