dev-jaanvi #1

Open
jaanvi wants to merge 155 commits from dev-jaanvi into main
39 changed files with 38885 additions and 1 deletions

58
.gitignore vendored Normal file
View file

@ -0,0 +1,58 @@
# Node.js dependencies
node_modules/
# Logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Environment variables
.env
.env.local
.env.*.local
# Build output
build/
dist/
# Dependency directories
.cache/
.docker/
# Coverage reports
coverage/
# System files
.DS_Store
Thumbs.db
*.swp
*.swo
# IDE-specific files
.idea/
*.iml
.vscode/
*.sublime-workspace
# Temporary files
tmp/
temp/
# Generated files
*.lock
*.env.local
*.env.development.local
*.env.test.local
*.env.production.local
# MacOS specific files
.AppleDouble
.LSOverride
# Windows specific files
desktop.ini
# Miscellaneous
!.gitkeep

8
.prettierrc Normal file
View file

@ -0,0 +1,8 @@
{
"tabWidth": 4,
"useTabs": true,
"semi": true,
"singleQuote": false,
"bracketSpacing": true
}

View file

@ -1,2 +1,46 @@
# bulk-email
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).

16096
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

76
package.json Normal file
View file

@ -0,0 +1,76 @@
{
"name": "digi-ev-admin-dashboard",
"version": "0.1.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/icons-material": "^6.4.5",
"@mui/material": "^6.4.5",
"@mui/x-charts": "^7.27.1",
"@mui/x-data-grid": "^7.27.0",
"@mui/x-date-pickers": "^7.27.0",
"@react-spring/web": "^9.7.5",
"@reduxjs/toolkit": "^2.5.0",
"@types/babel__core": "^7.20.5",
"add": "^2.0.6",
"axios": "^1.7.9",
"clsx": "^2.1.1",
"cra-template-typescript": "1.2.0",
"dayjs": "^1.11.13",
"highcharts": "^12.1.2",
"mui-phone-number": "^3.0.3",
"mui-tel-input": "^7.0.0",
"prop-types": "^15.8.1",
"react": "^18.0.0",
"react-cookie": "^7.2.2",
"react-dom": "^18.0.0",
"react-dropzone": "^14.3.5",
"react-hook-form": "^7.54.2",
"react-minimal-pie-chart": "^9.1.0",
"react-redux": "^9.2.0",
"react-router-dom": "^7.1.1",
"react-scripts": "5.0.1",
"recharts": "^2.15.1",
"sonner": "^1.7.4",
"uuid": "^11.1.0",
"web-vitals": "^4.2.4",
"xlsx": "^0.18.5"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/node": "^22.10.5",
"@types/react": "^19.0.4",
"@types/react-dom": "^19.0.2",
"typescript": "^5.7.3"
},
"resolutions": {
"eslint-config-react-app": "7.0.1"
},
"eslintConfig": {
"extends": []
}
}

21011
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

28
public/bg.svg Normal file
View file

@ -0,0 +1,28 @@
<svg width="1920" height="751" viewBox="0 0 1920 751" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.1" filter="url(#filter0_f_1407_4696)">
<path d="M357.765 483.542C426.213 434.499 1144.46 549.989 1258.37 579.451C1372.28 608.913 1757.57 743.868 1689.13 792.911C1620.68 841.954 527.424 923.713 413.507 894.245C299.59 864.776 289.318 532.588 357.765 483.542Z" fill="#0DB3D5"/>
</g>
<g opacity="0.2" filter="url(#filter1_f_1407_4696)">
<path d="M605.311 371.098C596.35 427.875 656.872 561.013 526.007 556.64C395.141 552.267 122.44 412.046 131.401 355.264C140.363 298.481 198.295 172.294 329.163 176.666C460.031 181.038 614.27 314.317 605.311 371.098Z" fill="#EAF1B8"/>
</g>
<g opacity="0.2" filter="url(#filter2_f_1407_4696)">
<path d="M1222 456.032C1222 409.041 1493.74 156 1551.77 156C1609.81 156 1882 384.378 1882 431.369C1882 478.359 1515.47 735 1457.43 735C1399.39 735 1222 503.022 1222 456.032Z" fill="#B59EDB"/>
</g>
<defs>
<filter id="filter0_f_1407_4696" x="6.99048" y="163.563" width="1998.22" height="1044.92" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="154" result="effect1_foregroundBlur_1407_4696"/>
</filter>
<filter id="filter1_f_1407_4696" x="-176.814" y="-131.445" width="1096.48" height="996.19" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="154" result="effect1_foregroundBlur_1407_4696"/>
</filter>
<filter id="filter2_f_1407_4696" x="914" y="-152" width="1276" height="1195" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="154" result="effect1_foregroundBlur_1407_4696"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.1 KiB

34
public/bgev.svg Normal file
View file

@ -0,0 +1,34 @@
<svg width="1920" height="1425" viewBox="0 0 1920 1425" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1407_4268)">
<rect width="1920" height="1425" fill="#111111"/>
<g opacity="0.1" filter="url(#filter0_f_1407_4268)">
<path d="M357.765 483.542C426.213 434.499 1144.46 549.989 1258.37 579.451C1372.28 608.913 1757.57 743.868 1689.13 792.911C1620.68 841.955 527.425 923.713 413.508 894.245C299.591 864.776 289.318 532.588 357.765 483.542Z" fill="#0DB3D5"/>
</g>
<g opacity="0.2" filter="url(#filter1_f_1407_4268)">
<path d="M605.311 371.098C596.349 427.875 656.872 561.013 526.007 556.64C395.141 552.267 122.439 412.046 131.401 355.264C140.363 298.481 198.295 172.294 329.163 176.666C460.031 181.038 614.27 314.317 605.311 371.098Z" fill="#EAF1B8"/>
</g>
<g opacity="0.2" filter="url(#filter2_f_1407_4268)">
<path d="M1222 456.032C1222 409.042 1493.74 156 1551.77 156C1609.81 156 1882 384.379 1882 431.369C1882 478.359 1515.47 735 1457.43 735C1399.39 735 1222 503.022 1222 456.032Z" fill="#B59EDB"/>
</g>
</g>
<defs>
<filter id="filter0_f_1407_4268" x="6.99072" y="163.564" width="1998.22" height="1044.92" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="154" result="effect1_foregroundBlur_1407_4268"/>
</filter>
<filter id="filter1_f_1407_4268" x="-176.814" y="-131.445" width="1096.48" height="996.19" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="154" result="effect1_foregroundBlur_1407_4268"/>
</filter>
<filter id="filter2_f_1407_4268" x="914" y="-152" width="1276" height="1195" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
<feGaussianBlur stdDeviation="154" result="effect1_foregroundBlur_1407_4268"/>
</filter>
<clipPath id="clip0_1407_4268">
<rect width="1920" height="1425" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

26
public/index.html Normal file
View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Gilroy:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="../src/index.css">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Bulk of email sending to users/students reactApp"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>DigiEV - Eco-friendly Charge</title>
</head>
<body>
<div id="root"></div>
</body>
</html>

25
public/manifest.json Normal file
View file

@ -0,0 +1,25 @@
{
"short_name": "BUlK Email Sending App",
"name": "Create React App to send BUlK-Email",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

0
public/mobileLogo.png Normal file
View file

3
public/robots.txt Normal file
View file

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

51
src/App.css Normal file
View file

@ -0,0 +1,51 @@
@import url("https://fonts.googleapis.com/css2?family=Fredoka:wght@300..700&display=swap");
/* @import url("https://fonts.googleapis.com/css2?family=Baloo+2:wght@400..800&display=swap"); */
html,
body {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
overflow: hidden; /* Prevents scrolling */
font-family: "Fredoka";
}
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

8
src/App.test.js Normal file
View file

@ -0,0 +1,8 @@
import { render, screen } from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
render(<App />);
const linkElement = screen.getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

13
src/App.tsx Normal file
View file

@ -0,0 +1,13 @@
import { BrowserRouter as Router } from "react-router-dom";
import AppRouter from "./router";
function App() {
return (
<Router>
<AppRouter />
</Router>
);
}
export default App;

View file

@ -0,0 +1,380 @@
import * as React from "react";
import { styled } from "@mui/material/styles";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell, { tableCellClasses } from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import Paper from "@mui/material/Paper";
import {
Box,
Pagination,
TextField,
Typography,
InputAdornment,
} from "@mui/material";
import SearchIcon from "@mui/icons-material/Search";
import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward";
import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward";
// Styled Components
const StyledTableCell = styled(TableCell)(({ theme }) => ({
[`&.${tableCellClasses.head}`]: {
backgroundColor: theme.palette.common.black, // instead of "#000000"
color: theme.palette.primary.light,
fontWeight: 600,
fontSize: "16px",
padding: "12px 16px",
borderBottom: "none",
position: "sticky",
top: 0,
zIndex: 10,
transition: "background-color 0.2s ease",
"&:hover": {
backgroundColor: theme.palette.grey[900],
cursor: "pointer",
},
},
[`&.${tableCellClasses.body}`]: {
fontSize: "16px",
padding: "12px 16px",
borderBottom: "none",
color: theme.palette.text.primary,
transition: "background-color 0.2s ease",
fontWeight: 500,
},
}));
const StyledTableRow = styled(TableRow)(({ theme }) => ({
backgroundColor: "#DFECF1",
transition: "background-color 0.2s ease",
"&:hover": {
backgroundColor: "#D0E1E9",
},
}));
const StyledTableContainer = styled(TableContainer)(({ theme }) => ({
borderRadius: "12px",
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.1)",
"&::-webkit-scrollbar": {
height: "6px",
},
"&::-webkit-scrollbar-track": {
background: "#D0E1E9",
},
"&::-webkit-scrollbar-thumb": {
background: "#000000",
borderRadius: "3px",
},
}));
// Interfaces
export interface Column {
id: string;
label: string;
align?: "left" | "center" | "right";
}
interface Row {
[key: string]: any;
}
interface CustomTableProps {
columns: Column[];
rows: Row[];
tableType: string;
}
// Sorting State Interface
interface SortConfig {
key: string;
direction: "asc" | "desc" | null;
}
const CustomTable: React.FC<CustomTableProps> = ({
columns,
rows,
tableType,
}) => {
const [searchQuery, setSearchQuery] = React.useState("");
const [currentPage, setCurrentPage] = React.useState(1);
const [sortConfig, setSortConfig] = React.useState<SortConfig>({
key: "",
direction: null,
});
const usersPerPage = 10;
// Helper Functions
const getTitleByType = (type: string) => {
const titles: { [key: string]: string } = {
backlog: "Student Backlog",
tools: "Tools",
};
return titles[type] || "List";
};
const getEmptyMessage = (type: string) => {
const messages: { [key: string]: string } = {
backlog: "No students available",
tools: "No tools available",
};
return messages[type] || "No data available";
};
const isImage = (value: any) => {
if (typeof value === "string") {
return value.startsWith("http") || value.startsWith("data:image");
}
return false;
};
// Sorting Logic
const handleSort = (key: string) => {
setSortConfig((prev) => {
if (prev.key === key) {
if (prev.direction === "asc") return { key, direction: "desc" };
if (prev.direction === "desc") return { key, direction: null };
return { key, direction: "asc" };
}
return { key, direction: "asc" };
});
};
const sortedRows = React.useMemo(() => {
if (!sortConfig.key || !sortConfig.direction) return [...rows];
return [...rows].sort((a, b) => {
const aValue = a[sortConfig.key];
const bValue = b[sortConfig.key];
if (aValue == null && bValue == null) return 0;
if (aValue == null) return sortConfig.direction === "asc" ? 1 : -1;
if (bValue == null) return sortConfig.direction === "asc" ? -1 : 1;
if (typeof aValue === "number" && typeof bValue === "number") {
return sortConfig.direction === "asc"
? aValue - bValue
: bValue - aValue;
}
const aStr = String(aValue).toLowerCase();
const bStr = String(bValue).toLowerCase();
return sortConfig.direction === "asc"
? aStr.localeCompare(bStr)
: bStr.localeCompare(aStr);
});
}, [rows, sortConfig]);
// Filtering Logic
const filteredRows = sortedRows.filter((row) => {
if (!searchQuery.trim()) return true;
const lowerCaseQuery = searchQuery.toLowerCase().trim();
return (
(row.name && row.name.toLowerCase().includes(lowerCaseQuery)) ||
(row.email && row.email.toLowerCase().includes(lowerCaseQuery))
);
});
// Pagination Logic
const indexOfLastRow = currentPage * usersPerPage;
const indexOfFirstRow = indexOfLastRow - usersPerPage;
const currentRows = filteredRows.slice(indexOfFirstRow, indexOfLastRow);
const handlePageChange = (
_event: React.ChangeEvent<unknown>,
value: number
) => {
setCurrentPage(value);
};
return (
<Box
sx={{
width: "100%",
padding: "20px 8px",
backgroundColor: "#D0E1E9",
borderRadius: "12px",
}}
>
<Box
sx={{
display: "flex",
flexDirection: { xs: "column", md: "row" },
justifyContent: "space-between",
alignItems: { xs: "flex-start", md: "center" },
flexWrap: "wrap",
mb: 3,
gap: 2,
}}
>
<Typography
sx={{
fontWeight: 600,
fontSize: "30px",
letterSpacing: "-0.5px",
}}
>
{getTitleByType(tableType)}
</Typography>
<TextField
variant="outlined"
placeholder="Search"
sx={{
width: { xs: "100%", sm: "300px" },
"& .MuiOutlinedInput-root": {
borderRadius: "8px",
height: "40px",
backgroundColor: "#FFFFFF",
"& fieldset": { borderColor: "#D1D5DB" },
"&:hover fieldset": { borderColor: "#9CA3AF" },
"&.Mui-focused fieldset": {
borderColor: "#3B82F6",
},
},
"& .MuiInputBase-input": {
fontSize: "14px",
color: "#1A1A1A",
},
}}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon sx={{ color: "#6B7280" }} />
</InputAdornment>
),
}}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</Box>
<StyledTableContainer>
<Table sx={{ minWidth: "750px", tableLayout: "auto" }}>
<TableHead>
<TableRow>
{columns.map((column) => (
<StyledTableCell
key={column.id}
align={column.align}
onClick={
column.id !== "select" &&
column.id !== "status"
? () => handleSort(column.id)
: undefined
}
sx={{
cursor:
column.id !== "select" &&
column.id !== "status"
? "pointer"
: "default",
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
gap: 1,
}}
>
{column.label}
{column.id !== "select" &&
column.id !== "status" &&
sortConfig.key === column.id &&
sortConfig.direction &&
(sortConfig.direction === "asc" ? (
<ArrowUpwardIcon
sx={{
fontSize: "16px",
color: "#FFFFFF",
}}
/>
) : (
<ArrowDownwardIcon
sx={{
fontSize: "16px",
color: "#FFFFFF",
}}
/>
))}
</Box>
</StyledTableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{currentRows.length === 0 ? (
<StyledTableRow>
<StyledTableCell
colSpan={columns.length}
sx={{ textAlign: "center", py: 4 }}
>
<Typography
sx={{
color: "#6B7280",
fontSize: "16px",
}}
>
{getEmptyMessage(tableType)}
</Typography>
</StyledTableCell>
</StyledTableRow>
) : (
currentRows.map((row, rowIndex) => (
<StyledTableRow key={rowIndex}>
{columns.map((column) => (
<StyledTableCell
key={column.id}
align={column.align}
>
{isImage(row[column.id]) ? (
<img
src={row[column.id]}
alt="Row"
style={{
width: "40px",
height: "40px",
borderRadius: "50%",
objectFit: "cover",
}}
/>
) : (
row[column.id]
)}
</StyledTableCell>
))}
</StyledTableRow>
))
)}
</TableBody>
</Table>
</StyledTableContainer>
<Box sx={{ display: "flex", justifyContent: "flex-end", mt: 3 }}>
<Pagination
count={Math.ceil(filteredRows.length / usersPerPage)}
page={currentPage}
onChange={handlePageChange}
sx={{
"& .MuiPaginationItem-root": {
color: "#1A1A1A",
fontSize: "16px",
},
"& .Mui-selected": {
backgroundColor: "#000000",
color: "#FFFFFF",
"&:hover": {
backgroundColor: "#000000",
},
},
}}
/>
</Box>
</Box>
);
};
export default CustomTable;

View file

@ -0,0 +1,24 @@
import React from 'react';
import { CircularProgress, Box, Typography } from '@mui/material';
function LoadingComponent() {
return (
<Box
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
height: '100vh',
width: '100%',
flexDirection: 'column',
}}
>
<CircularProgress size={50} color="primary" />
<Typography variant="h6" sx={{ marginTop: 2 }}>
Loading...
</Typography>
</Box>
);
}
export default LoadingComponent;

View file

@ -0,0 +1,8 @@
export const MESSAGES = {
STUDENTS_LOADED: "Students loaded from file.",
STUDENT_REMOVED_LOCAL: "Student removed from local backlog.",
STUDENT_REMOVED_SERVER: "Student removed from backlog.",
FAILED_TO_REMOVE: "Failed to remove student.",
FAILED_TO_FETCH: "Failed to fetch students",
};

8
src/global.d.ts vendored Normal file
View file

@ -0,0 +1,8 @@
declare module "*.css";
declare module "@mui/styles/defaultTheme" {
interface DefaultTheme extends Theme {
vars: object;
}
}

20
src/hooks/useAuth.js Normal file
View file

@ -0,0 +1,20 @@
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
const useAuth = () => {
const dispatch = useDispatch();
const { isAuthenticated } = useSelector((state) => ({
isAuthenticated: state.auth.isAuthenticated,
}));
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
dispatch(checkUserAuth()).finally(() => setLoading(false));
}, [dispatch]);
return { isAuthenticated, loading };
};
export default useAuth;

35
src/index.css Normal file
View file

@ -0,0 +1,35 @@
@import url("https://fonts.googleapis.com/css2?family=Fredoka:wght@300..700&display=swap");
/* @import url("https://fonts.googleapis.com/css2?family=Baloo+2:wght@400..800&display=swap"); */
/* @import url('https://fonts.googleapis.com/css2?family=Quicksand:wght@300..700&display=swap'); */
body {
margin: 0;
font-family: "Fredoka";
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}
.mui-typography {
font-family: "Fredoka";
background-color: rgb(7, 127, 233);
}
.css-1w8ddxu-MuiBarElement-root {
width: 19px !important;
border-radius: 50px !important;
rx: 8;
ry: 8;
}
/* @font-face {
font-family: '"Publica Sans Round Medium", sans-serif';
src: url("../public/fonts/PublicaSansRound-Md.otf") format("otf");
font-weight: 500;
font-style: normal;
font-display: swap;
} */

28
src/index.tsx Normal file
View file

@ -0,0 +1,28 @@
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import reportWebVitals from "./reportWebVitals";
import App from "./App";
import { Provider } from "react-redux";
import store from "./redux/store/store.ts";
import { Toaster } from "sonner";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
<Toaster
position="top-right"
richColors
closeButton
duration={3000}
/>
</Provider>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

View file

@ -0,0 +1,82 @@
import * as React from "react";
import { Box, Stack } from "@mui/material";
import { Outlet } from "react-router-dom";
import SideMenu from "../../components/SideMenu/sideMenu";
import AppNavbar from "../../components/AppNavbar";
import Header from "../../components/Header";
import AppTheme from "../../shared-theme/AppTheme";
interface LayoutProps {
customStyles?: React.CSSProperties;
}
const DashboardLayout: React.FC<LayoutProps> = ({ customStyles }) => {
return (
<AppTheme>
<Box
sx={{
display: "flex",
// height: "auto",
flexDirection: { xs: "column", md: "row" },
width: "100%",
height: "100vh", // Full height from root, not auto
overflow: "hidden",
margin: 0,
padding: 0,
}}
>
{/* SideMenu - Responsive, shown only on large screens */}
<Box
sx={{
display: { xs: "none", md: "block" },
// width: 250,
}}
>
<SideMenu />
</Box>
{/* Navbar - Always visible */}
<AppNavbar />
<Box
component="main"
sx={(theme) => ({
display: "flex",
flexDirection: "column",
height: "100vh",
flexGrow: 1,
backgroundColor: theme.palette.background.default,
overflow: "auto",
overflowX: "hidden",
...customStyles,
mt: { xs: 8, md: 0 },
padding: 0,
})}
>
<Box sx={{ height: "84px", flex: "0 0 84px" }}>
<Header />
</Box>
<Stack
spacing={2}
sx={{
padding: "30px",
display: "flex",
flex: 1,
justifyItems: "center",
alignItems: "center",
// mx: { xs: 1, sm: 3 },
// pb: 5,
mt: { xs: 3, md: 0 },
width: "100%",
boxSizing: "border-box",
}}
>
<Outlet />
</Stack>
</Box>
</Box>
</AppTheme>
);
};
export default DashboardLayout;

38
src/lib/https.ts Normal file
View file

@ -0,0 +1,38 @@
import axios from "axios";
const http = axios.create({
baseURL: process.env.REACT_APP_BACKEND_URL,
});
http.interceptors.request.use((config) => {
const authToken = localStorage.getItem("authToken");
if (authToken) {
config.headers.Authorization = `Bearer ${authToken}`;
}
return config;
});
http.interceptors.response.use(
(response) => response,
(error) => {
if (error.response) {
const status = error.response.status;
const requestUrl = error.config.url; // Get the API route
// Handle token expiration (401) but NOT for login failures
if (status === 401 && !requestUrl.includes("/login")) {
localStorage.removeItem("authToken");
window.location.href = "/login";
}
// Handle forbidden access
if (status === 403) {
localStorage.removeItem("authToken");
window.location.href = "/login";
}
}
return Promise.reject(error);
}
);
export default http;

1
src/logo.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -0,0 +1,175 @@
import { useState } from "react";
import { useDispatch, useSelector } from "react-redux";
import * as XLSX from "xlsx";
import { v4 as uuidv4 } from "uuid";
import { useNavigate } from "react-router-dom";
import { AppDispatch, RootState } from "../../redux/store/store";
import { loadStudents } from "../../redux/slices/backlogSlice";
import { toast } from "sonner";
import { Box, Button, Typography } from "@mui/material";
import CustomTable, { Column } from "../../components/CustomTable/customTable";
export default function BacklogUploadPage() {
const dispatch = useDispatch<AppDispatch>();
const navigate = useNavigate();
const students = useSelector(
(state: RootState) => state.backlogReducer.students
);
const [fileUploaded, setFileUploaded] = useState(students.length > 0);
const [selectedStudentIds, setSelectedStudentIds] = useState<string[]>([]);
const handleFileUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = (evt) => {
const data = new Uint8Array(evt.target.result as ArrayBuffer);
const workbook = XLSX.read(data, { type: "array" });
const sheet = workbook.Sheets[workbook.SheetNames[0]];
const parsedData: { name: string; email: string }[] =
XLSX.utils.sheet_to_json(sheet);
const studentsWithId = parsedData.map((student) => ({
...student,
id: uuidv4(),
status: "Pending",
}));
dispatch(loadStudents(studentsWithId));
setFileUploaded(true);
setSelectedStudentIds([]); // Reset selection after upload
};
reader.readAsArrayBuffer(file);
};
const handleCheckboxChange = (id: string) => {
setSelectedStudentIds((prev) =>
prev.includes(id) ? prev.filter((s) => s !== id) : [...prev, id]
);
};
const handleNavigateToEmail = () => {
if (selectedStudentIds.length === 0) {
toast.warning("Please select at least one student.");
return;
}
navigate("/email", { state: { selectedIds: selectedStudentIds } });
};
const columns: Column[] = [
{ id: "select", label: "Select", align: "center" },
{ id: "name", label: "Name", align: "left" },
{ id: "email", label: "Email", align: "left" },
{ id: "status", label: "Status", align: "center" },
];
const rows = students.map((student) => ({
id: student.id,
select: (
<input
type="checkbox"
checked={selectedStudentIds.includes(student.id)}
onChange={() => handleCheckboxChange(student.id)}
disabled={student.status === "Sent"}
/>
),
name: student.name,
email: student.email,
status:
student.status === "Sent" ? (
<span style={{ color: "#10B981" }}> Sent</span>
) : (
<span style={{ color: "#F59E0B" }}> Pending</span>
),
}));
return (
<Box
sx={{
width: "100vw",
minHeight: "100vh",
display: "flex",
flexDirection: "column",
p: { xs: 2, sm: 4 },
backgroundColor: "#F5F8FA",
boxSizing: "border-box",
}}
>
<Typography
sx={{
fontWeight: 600,
fontSize: { xs: "20px", sm: "24px" },
mb: 3,
color: "#1A1A1A",
}}
>
📂 Upload Student Backlog File
</Typography>
{!fileUploaded ? (
<Box sx={{ maxWidth: "600px" }}>
<label htmlFor="upload-input">
<input
id="upload-input"
type="file"
accept=".xlsx, .xls"
onChange={handleFileUpload}
style={{ display: "none" }}
/>
<Button
variant="contained"
component="span"
sx={{
backgroundColor: "#1F2937",
color: "#fff",
borderRadius: "8px",
textTransform: "none",
fontWeight: "500",
}}
>
📁 Choose Excel File
</Button>
</label>
<Typography
sx={{
fontSize: "14px",
color: "#6B7280",
mt: 1,
}}
>
* Excel must contain columns: <b>name</b>, <b>email</b>
</Typography>
</Box>
) : (
<Box sx={{ flex: 1, display: "flex", flexDirection: "column" }}>
<CustomTable
columns={columns}
rows={rows}
tableType="backlog"
/>
<Box sx={{ display: "flex", gap: 2, mt: 3 }}>
<Button
variant="contained"
onClick={handleNavigateToEmail}
sx={{
backgroundColor: "#111827",
color: "#fff",
borderRadius: "8px",
fontSize: "16px",
padding: "8px 24px",
textTransform: "none",
"&:hover": { backgroundColor: "#1F2937" },
}}
>
Send Email
</Button>
</Box>
</Box>
)}
</Box>
);
}

View file

@ -0,0 +1,131 @@
import { Box, Button, TextField, Typography, Paper } from "@mui/material";
import { useLocation, useNavigate } from "react-router-dom";
import { useSelector, useDispatch } from "react-redux";
import { useState } from "react";
import { RootState, AppDispatch } from "../../redux/store/store";
import { markStudentAsSent } from "../../redux/slices/backlogSlice";
import { toast } from "sonner";
export default function EmailPage() {
const location = useLocation();
const navigate = useNavigate();
const dispatch = useDispatch<AppDispatch>();
const { selectedIds } = location.state || { selectedIds: [] };
const students = useSelector(
(state: RootState) => state.backlogReducer.students
);
const selectedStudents = students.filter((student) =>
selectedIds.includes(student.id)
);
const [subject, setSubject] = useState("");
const [message, setMessage] = useState("");
const [attachment, setAttachment] = useState<File | null>(null);
const handleSend = () => {
if (!subject || !message) {
toast.warning("Subject and Message are required.");
return;
}
dispatch(markStudentAsSent(selectedIds));
toast.success("Emails sent successfully.");
navigate("/");
};
const handleCancel = () => {
jaanvi marked this conversation as resolved
Review

Remove all console logs

Remove all console logs
navigate("/");
};
return (
<Box
sx={{
minHeight: "100vh",
display: "flex",
justifyContent: "center",
alignItems: "center",
backgroundColor: "#F5F8FA",
p: 2,
}}
>
<Paper
elevation={3}
sx={{ p: 4, maxWidth: "600px", width: "100%" }}
>
<Typography variant="h5" gutterBottom>
📧 Compose Email
</Typography>
<Typography sx={{ mb: 2 }}>
<b>To:</b> {selectedStudents.map((s) => s.name).join(", ")}
</Typography>
<TextField
fullWidth
label="Subject"
variant="outlined"
value={subject}
onChange={(e) => setSubject(e.target.value)}
sx={{ mb: 2 }}
/>
<TextField
fullWidth
label="Message"
variant="outlined"
multiline
rows={6}
value={message}
onChange={(e) => setMessage(e.target.value)}
sx={{ mb: 2 }}
/>
<Button
variant="outlined"
component="label"
sx={{ mb: 2, textTransform: "none" }}
>
📎 Upload Attachment
<input
type="file"
hidden
onChange={(e) =>
setAttachment(e.target.files?.[0] || null)
}
/>
</Button>
{attachment && (
<Typography variant="body2" sx={{ mb: 2 }}>
Selected: {attachment.name}
</Typography>
)}
<Box
sx={{
display: "flex",
justifyContent: "space-between",
mt: 2,
}}
>
<Button
variant="contained"
color="primary"
onClick={handleSend}
sx={{ textTransform: "none" }}
>
Send
</Button>
<Button
variant="outlined"
onClick={handleCancel}
sx={{ textTransform: "none" }}
>
Cancel
</Button>
</Box>
</Paper>
</Box>
);
}

View file

@ -0,0 +1,103 @@
import React from "react";
import { Box, Typography, Button, Card, Grid } from "@mui/material";
import ElectricCarIcon from "@mui/icons-material/ElectricCar";
import { pulse } from "./style.css";
const NotFoundPage = () => {
return (
<Box
jaanvi marked this conversation as resolved
Review

put it in the css file

put it in the css file
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column",
height: "100vh",
background:
"linear-gradient(135deg,rgb(12, 14, 15),rgb(10, 10, 10))",
padding: 2,
}}
>
<Card
sx={{
backgroundColor: "#272727",
padding: "40px",
borderRadius: "20px",
boxShadow: "0 8px 24px rgba(0, 0, 0, 0.7)",
textAlign: "center",
maxWidth: "500px",
width: "100%",
color: "#E0E0E0",
animation: `${pulse} 1.5s infinite`,
}}
>
<ElectricCarIcon
sx={{
fontSize: 100,
color: "#52ACDF",
marginBottom: 2,
}}
/>
<Typography
variant="h3"
sx={{ fontWeight: 700, color: "#FFFFFF", marginBottom: 1 }}
>
404
</Typography>
<Typography
variant="h5"
sx={{ fontWeight: 500, color: "#FFFFFF", marginBottom: 3 }}
>
Oops! Page Not Found
</Typography>
<Typography
variant="body2"
sx={{ color: "#B0B0B0", marginBottom: 4 }}
>
The path youre looking for seems to be off the grid. Maybe
the charging station is offline?
</Typography>
<Grid container spacing={2} justifyContent="center">
<Grid item>
<Button
variant="contained"
sx={{
backgroundColor: "#52ACDF",
color: "#FFFFFF",
padding: "12px 32px",
"&:hover": {
backgroundColor: "#52ACDF",
opacity: 0.9,
},
fontWeight: 600,
}}
onClick={() => (window.location.href = "/")}
>
Back to Login
</Button>
</Grid>
<Grid item>
<Button
variant="outlined"
sx={{
borderColor: "#52ACDF",
color: "#52ACDF",
padding: "12px 32px",
"&:hover": {
backgroundColor: "#52ACDF",
color: "#FFFFFF",
},
}}
onClick={() => window.history.back()}
>
Go Back
</Button>
</Grid>
</Grid>
</Card>
</Box>
);
};
export default NotFoundPage;

View file

@ -0,0 +1,12 @@
import { keyframes } from "@emotion/react";
export const pulse = keyframes`
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
`;

12
src/redux/reducers.ts Normal file
View file

@ -0,0 +1,12 @@
import { combineReducers } from "@reduxjs/toolkit";
import backlogReducer from "../redux/slices/backlogSlice.ts";
const rootReducer = combineReducers({
backlogReducer,
});
export type RootState = ReturnType<typeof rootReducer>;
export default rootReducer;

View file

@ -0,0 +1,123 @@
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
import http from "../../lib/https";
import { toast } from "sonner";
import { MESSAGES } from "../../constants/messages";
export interface Student {
id: string;
name: string;
email: string;
status: "Pending" | "Sent";
}
// State shape
interface BacklogState {
students: Student[];
isLoading: boolean;
error: string | null;
}
// Initial state
const initialState: BacklogState = {
students: [],
isLoading: false,
error: null,
};
export const fetchBacklogStudents = createAsyncThunk<
Student[],
void,
{ rejectValue: string }
>("backlog/fetchBacklogStudents", async (_, { rejectWithValue }) => {
try {
const response = await http.get("/students/backlog");
return response.data;
} catch (error: any) {
return rejectWithValue(
error.response?.data?.message || "Failed to fetch students"
);
}
});
export const removeStudentFromServer = createAsyncThunk<
string, // returning student ID
string, // input student ID
{ rejectValue: string }
>("backlog/removeStudentFromServer", async (studentId, { rejectWithValue }) => {
try {
await http.delete(`/students/${studentId}`);
toast.success(MESSAGES.STUDENT_REMOVED_SERVER);
return studentId;
} catch (error: any) {
toast.error(MESSAGES.FAILED_TO_REMOVE);
return rejectWithValue(
error.response?.data?.message || "Remove operation failed"
);
}
});
const backlogSlice = createSlice({
name: "backlog",
initialState,
reducers: {
loadStudents: (
state,
action: PayloadAction<Omit<Student, "status">[]>
) => {
state.students = action.payload.map((student) => ({
...student,
status: "Pending",
}));
toast.success(MESSAGES.STUDENTS_LOADED);
},
removeStudent: (state, action: PayloadAction<string>) => {
state.students = state.students.filter(
(student) => student.id !== action.payload
);
toast.success(MESSAGES.STUDENT_REMOVED_LOCAL);
},
markStudentAsSent: (state, action: PayloadAction<string[]>) => {
action.payload.forEach((id) => {
const student = state.students.find((s) => s.id === id);
if (student) {
student.status = "Sent";
}
});
},
},
extraReducers: (builder) => {
builder
.addCase(fetchBacklogStudents.pending, (state) => {
state.isLoading = true;
state.error = null;
})
.addCase(
fetchBacklogStudents.fulfilled,
(state, action: PayloadAction<Student[]>) => {
state.students = action.payload;
state.isLoading = false;
}
)
.addCase(
fetchBacklogStudents.rejected,
(state, action: PayloadAction<string | undefined>) => {
state.isLoading = false;
state.error = action.payload || "Failed to fetch";
}
)
.addCase(removeStudentFromServer.fulfilled, (state, action) => {
state.students = state.students.filter(
(student) => student.id !== action.payload
);
});
},
});
export const { loadStudents, removeStudent, markStudentAsSent } =
backlogSlice.actions;
export default backlogSlice.reducer;

9
src/redux/store/store.ts Normal file
View file

@ -0,0 +1,9 @@
import { configureStore } from "@reduxjs/toolkit";
import rootReducer from "../reducers.ts";
const store = configureStore({
reducer: rootReducer,
});
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
export default store;

13
src/reportWebVitals.js Normal file
View file

@ -0,0 +1,13 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
};
export default reportWebVitals;

24
src/router.tsx Normal file
View file

@ -0,0 +1,24 @@
import { Routes as BaseRoutes, Navigate, Route } from "react-router-dom";
import React, { lazy, Suspense } from "react";
import LoadingComponent from "./components/Loading";
import BacklogUploadPage from "./pages/BacklogUploadPage/index.tsx";
import EmailPage from "./pages/EmailPage/index.tsx";
const NotFoundPage = lazy(() => import("./pages/NotFoundPage"));
// Combined Router Component
export default function AppRouter() {
return (
<Suspense fallback={<LoadingComponent />}>
<BaseRoutes>
{/* Default Route */}
<Route path="" element={<BacklogUploadPage />} />
<Route path="/email" element={<EmailPage />} />
{/* Catch-all Route */}
<Route path="*" element={<NotFoundPage />} />
</BaseRoutes>
</Suspense>
);
}

5
src/setupTests.js Normal file
View file

@ -0,0 +1,5 @@
// jest-dom adds custom jest matchers for asserting on DOM nodes.
// allows you to do things like:
// expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom
import '@testing-library/jest-dom';

View file

@ -0,0 +1,88 @@
import * as React from "react";
import { ThemeProvider, Theme, createTheme } from "@mui/material/styles";
import type { ThemeOptions } from "@mui/material/styles";
import { inputsCustomizations } from "./customizations/inputs";
import { dataDisplayCustomizations } from "./customizations/dataDisplay";
import { feedbackCustomizations } from "./customizations/feedback";
import { navigationCustomizations } from "./customizations/navigation";
import { surfacesCustomizations } from "./customizations/surfaces";
import { colorSchemes, shadows, shape } from "./themePrimitives";
interface AppThemeProps {
children: React.ReactNode;
disableCustomTheme?: boolean;
themeComponents?: ThemeOptions["components"];
}
export default function AppTheme(props: AppThemeProps) {
const { children, disableCustomTheme, themeComponents } = props;
const theme = React.useMemo(() => {
return disableCustomTheme
? {}
: createTheme({
palette: {
mode: "light",
background: {
default: "#DFECF1",
paper: "#D0E1E9",
},
text: {
primary: "#000000",
secondary: "#454545",
},
success: {
main: "#00C853",
},
error: {
main: "#F44336",
},
divider: "#E0E0E0",
},
typography: {
fontFamily: "Fredoka",
h4: {
fontSize: "2rem",
fontWeight: 600,
},
h6: {
fontSize: "16px",
fontWeight: 500,
},
body1: {
fontSize: "1rem",
},
body2: {
fontSize: "0.875rem",
color: "#b0b0b0",
fontWeight: 400,
},
},
cssVariables: {
colorSchemeSelector: "data-mui-color-scheme",
cssVarPrefix: "template",
},
shadows,
shape: {
borderRadius: 8,
},
components: {
...inputsCustomizations,
...dataDisplayCustomizations,
...feedbackCustomizations,
...navigationCustomizations,
...surfacesCustomizations,
...themeComponents,
},
});
}, [disableCustomTheme, themeComponents]);
if (disableCustomTheme) {
return <React.Fragment>{children}</React.Fragment>;
}
return (
<ThemeProvider theme={theme} disableTransitionOnChange>
{children}
</ThemeProvider>
);
}

30
tsconfig.app.json Normal file
View file

@ -0,0 +1,30 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2020",
"useDefineForClassFields": true,
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": [
"src"
]
}

32
tsconfig.json Normal file
View file

@ -0,0 +1,32 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
],
"compilerOptions": {
"baseUrl": ".",
"jsx": "react-jsx",
"types": [
"react",
"react-dom"
],
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"moduleResolution": "node",
"esModuleInterop": true,
"resolveJsonModule": true,
"paths": {
"@/*": [
"./src/*"
]
}
}
}

22
tsconfig.node.json Normal file
View file

@ -0,0 +1,22 @@
{
"compilerOptions": {
"jsx": "react",
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
}
}