dev-jaanvi #1
58
.gitignore
vendored
Normal file
58
.gitignore
vendored
Normal 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
8
.prettierrc
Normal file
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"tabWidth": 4,
|
||||
"useTabs": true,
|
||||
"semi": true,
|
||||
"singleQuote": false,
|
||||
"bracketSpacing": true
|
||||
}
|
||||
|
46
README.md
46
README.md
|
@ -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 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
Normal file
16096
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
76
package.json
Normal file
76
package.json
Normal 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
21011
pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
28
public/bg.svg
Normal file
28
public/bg.svg
Normal 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
34
public/bgev.svg
Normal 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
26
public/index.html
Normal 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
25
public/manifest.json
Normal 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
0
public/mobileLogo.png
Normal file
3
public/robots.txt
Normal file
3
public/robots.txt
Normal file
|
@ -0,0 +1,3 @@
|
|||
# https://www.robotstxt.org/robotstxt.html
|
||||
User-agent: *
|
||||
Disallow:
|
51
src/App.css
Normal file
51
src/App.css
Normal 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
8
src/App.test.js
Normal 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
13
src/App.tsx
Normal 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;
|
380
src/components/CustomTable/customTable.tsx
Normal file
380
src/components/CustomTable/customTable.tsx
Normal 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;
|
24
src/components/Loading/index.tsx
Normal file
24
src/components/Loading/index.tsx
Normal 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;
|
8
src/constants/messages.tsx
Normal file
8
src/constants/messages.tsx
Normal 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
8
src/global.d.ts
vendored
Normal 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
20
src/hooks/useAuth.js
Normal 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
35
src/index.css
Normal 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
28
src/index.tsx
Normal 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();
|
82
src/layouts/DashboardLayout/index.tsx
Normal file
82
src/layouts/DashboardLayout/index.tsx
Normal 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
38
src/lib/https.ts
Normal 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
1
src/logo.svg
Normal 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 |
175
src/pages/BacklogUploadPage/index.tsx
Normal file
175
src/pages/BacklogUploadPage/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
131
src/pages/EmailPage/index.tsx
Normal file
131
src/pages/EmailPage/index.tsx
Normal 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
|
||||
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>
|
||||
);
|
||||
}
|
103
src/pages/NotFoundPage/index.tsx
Normal file
103
src/pages/NotFoundPage/index.tsx
Normal 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
VishalMaurya
commented
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 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;
|
12
src/pages/NotFoundPage/style.css.tsx
Normal file
12
src/pages/NotFoundPage/style.css.tsx
Normal 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
12
src/redux/reducers.ts
Normal 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;
|
123
src/redux/slices/backlogSlice.ts
Normal file
123
src/redux/slices/backlogSlice.ts
Normal 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
9
src/redux/store/store.ts
Normal 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
13
src/reportWebVitals.js
Normal 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
24
src/router.tsx
Normal 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
5
src/setupTests.js
Normal 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';
|
88
src/shared-theme/AppTheme.tsx
Normal file
88
src/shared-theme/AppTheme.tsx
Normal 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
30
tsconfig.app.json
Normal 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
32
tsconfig.json
Normal 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
22
tsconfig.node.json
Normal 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
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue
Remove all console logs