Compare commits
No commits in common. "dev" and "main" have entirely different histories.
58
.gitignore
vendored
58
.gitignore
vendored
|
@ -1,58 +0,0 @@
|
|||
# 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
|
|
@ -1,8 +0,0 @@
|
|||
{
|
||||
"tabWidth": 4,
|
||||
"useTabs": true,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"bracketSpacing": true
|
||||
}
|
||||
|
46
README.md
46
README.md
|
@ -1,46 +1,2 @@
|
|||
# Getting Started with Create React App
|
||||
# bulk-email
|
||||
|
||||
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 can’t go back!**
|
||||
|
||||
If you aren’t 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 you’re on your own.
|
||||
|
||||
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t 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
16096
package-lock.json
generated
File diff suppressed because it is too large
Load diff
76
package.json
76
package.json
|
@ -1,76 +0,0 @@
|
|||
{
|
||||
"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": []
|
||||
}
|
||||
}
|
21008
pnpm-lock.yaml
21008
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -1,28 +0,0 @@
|
|||
<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>
|
Before Width: | Height: | Size: 2.1 KiB |
|
@ -1,34 +0,0 @@
|
|||
<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>
|
Before Width: | Height: | Size: 2.2 KiB |
|
@ -1,26 +0,0 @@
|
|||
<!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>
|
|
@ -1,25 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
51
src/App.css
51
src/App.css
|
@ -1,51 +0,0 @@
|
|||
@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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
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
13
src/App.tsx
|
@ -1,13 +0,0 @@
|
|||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
import AppRouter from "./router";
|
||||
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Router>
|
||||
<AppRouter />
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
|
@ -1,380 +0,0 @@
|
|||
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;
|
|
@ -1,24 +0,0 @@
|
|||
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;
|
|
@ -1,8 +0,0 @@
|
|||
|
||||
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
8
src/global.d.ts
vendored
|
@ -1,8 +0,0 @@
|
|||
declare module "*.css";
|
||||
|
||||
declare module "@mui/styles/defaultTheme" {
|
||||
interface DefaultTheme extends Theme {
|
||||
vars: object;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
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;
|
|
@ -1,35 +0,0 @@
|
|||
@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;
|
||||
} */
|
|
@ -1,28 +0,0 @@
|
|||
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();
|
|
@ -1,82 +0,0 @@
|
|||
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;
|
|
@ -1,38 +0,0 @@
|
|||
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 +0,0 @@
|
|||
<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>
|
Before Width: | Height: | Size: 2.6 KiB |
|
@ -1,175 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
|
@ -1,131 +0,0 @@
|
|||
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 = () => {
|
||||
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>
|
||||
);
|
||||
}
|
|
@ -1,103 +0,0 @@
|
|||
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
|
||||
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 you’re 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;
|
|
@ -1,12 +0,0 @@
|
|||
import { keyframes } from "@emotion/react";
|
||||
export const pulse = keyframes`
|
||||
0% {
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
}
|
||||
`;
|
|
@ -1,12 +0,0 @@
|
|||
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;
|
|
@ -1,123 +0,0 @@
|
|||
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;
|
|
@ -1,9 +0,0 @@
|
|||
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;
|
|
@ -1,13 +0,0 @@
|
|||
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;
|
|
@ -1,24 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
// 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';
|
|
@ -1,88 +0,0 @@
|
|||
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>
|
||||
);
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
]
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
{
|
||||
"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/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
{
|
||||
"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
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue