From 16ec234ed8cfe157e715675153ebe35adada138c Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Fri, 14 Mar 2025 11:43:01 -0400 Subject: [PATCH 01/22] init Admin frontend --- client/src/App.tsx | 4 +- .../AdminDrawer/AdminDrawer.test.tsx | 57 +++++++++++++++++ .../components/AdminDrawer/AdminDrawer.tsx | 64 +++++++++++++++++++ client/src/components/Header/Header.tsx | 30 +++++---- client/src/components/Header/header.css | 6 ++ 5 files changed, 149 insertions(+), 12 deletions(-) create mode 100644 client/src/__tests__/components/AdminDrawer/AdminDrawer.test.tsx create mode 100644 client/src/components/AdminDrawer/AdminDrawer.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index a3e33fa..6a2d060 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -29,6 +29,7 @@ import OAuthCallback from './pages/AuthManager/callback/AuthCallback'; const App: React.FC = () => { const [isAuthenticated, setIsAuthenticated] = useState(ApiService.isLoggedIn()); const [isTeacherAuthenticated, setIsTeacherAuthenticated] = useState(ApiService.isLoggedInTeacher()); + const [isAdmin, setIsAdmin] = useState(false); const [isRoomRequireAuthentication, setRoomsRequireAuth] = useState(null); const location = useLocation(); @@ -37,6 +38,7 @@ const App: React.FC = () => { const checkLoginStatus = () => { setIsAuthenticated(ApiService.isLoggedIn()); setIsTeacherAuthenticated(ApiService.isLoggedInTeacher()); + //setIsAdmin(ApiService.isAdmin()); }; const fetchAuthenticatedRooms = async () => { @@ -56,7 +58,7 @@ const App: React.FC = () => { return (
-
+
diff --git a/client/src/__tests__/components/AdminDrawer/AdminDrawer.test.tsx b/client/src/__tests__/components/AdminDrawer/AdminDrawer.test.tsx new file mode 100644 index 0000000..fe68167 --- /dev/null +++ b/client/src/__tests__/components/AdminDrawer/AdminDrawer.test.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import AdminDrawer from '../../../components/AdminDrawer/AdminDrawer'; +import '@testing-library/jest-dom'; + +describe('AdminDrawer Component', () => { + test('renders the Admin button', () => { + render(); + + // Check if the "Admin" button is in the document + const button = screen.getByRole('button', { name: /admin/i }); + expect(button).toBeInTheDocument(); + }); + + test('opens the drawer when the button is clicked', () => { + render(); + + // Click the "Admin" button + const button = screen.getByRole('button', { name: /admin/i }); + fireEvent.click(button); + + // Check if the drawer is open (it should be a right-side drawer, so check for list items) + const statsItem = screen.getByText(/Stats/i); + expect(statsItem).toBeInTheDocument(); + }); + //TODO modify this test as no redirect as of yet +/* + test('closes the drawer when an item is clicked', () => { + render(); + + // Open the drawer by clicking the "Admin" button + const button = screen.getByRole('button', { name: /admin/i }); + fireEvent.click(button); + + // Click on a menu item (Stats, Images, or Users) + const statsItem = screen.getByText(/Stats/i); + expect(statsItem).toBeInTheDocument(); + fireEvent.click(statsItem); + + // Ensure that the drawer is closed after clicking an item + const statsItemAgain = screen.queryByText(/Stats/i); + expect(statsItemAgain).not.toBeInTheDocument(); + }); +*/ + test('menu items render correctly', () => { + render(); + + // Open the drawer + const button = screen.getByRole('button', { name: /admin/i }); + fireEvent.click(button); + + // Check if all the menu items are rendered + expect(screen.getByText(/Stats/i)).toBeInTheDocument(); + expect(screen.getByText(/Images/i)).toBeInTheDocument(); + expect(screen.getByText(/Users/i)).toBeInTheDocument(); + }); +}); diff --git a/client/src/components/AdminDrawer/AdminDrawer.tsx b/client/src/components/AdminDrawer/AdminDrawer.tsx new file mode 100644 index 0000000..d61df5e --- /dev/null +++ b/client/src/components/AdminDrawer/AdminDrawer.tsx @@ -0,0 +1,64 @@ +import * as React from 'react'; +import Box from '@mui/material/Box'; +import Drawer from '@mui/material/Drawer'; +import Button from '@mui/material/Button'; +import List from '@mui/material/List'; +import ListItem from '@mui/material/ListItem'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import BarChartIcon from '@mui/icons-material/BarChart'; +import ImageIcon from '@mui/icons-material/Image'; +import PeopleIcon from '@mui/icons-material/People'; + +const styles = { + drawerBg: 'rgba(82, 113, 255, 0.85)', + drawerTxtColor: 'white', + btnBg: 'rgba(82, 113, 255, 1)', + btnHover: 'rgba(65, 105, 225, 0.7)', + height: '100%' +}; + +export default function AdminDrawer() { + const [open, setOpen] = React.useState(false); + + const toggleDrawer = (isOpen: boolean) => () => { + setOpen(isOpen); + }; + + const menuItems = [ + { text: 'Stats', icon: }, + { text: 'Images', icon: }, + { text: 'Users', icon: }, + ]; + + const list = ( + + + {menuItems.map(({ text, icon }) => ( + + + {icon} + + + + ))} + + + ); + + return ( +
+ + + {list} + +
+ ); +} diff --git a/client/src/components/Header/Header.tsx b/client/src/components/Header/Header.tsx index 016d23e..368fc7d 100644 --- a/client/src/components/Header/Header.tsx +++ b/client/src/components/Header/Header.tsx @@ -2,13 +2,15 @@ import { Link, useNavigate } from 'react-router-dom'; import * as React from 'react'; import './header.css'; import { Button } from '@mui/material'; +import AdminDrawer from '../AdminDrawer/AdminDrawer'; interface HeaderProps { isLoggedIn: boolean; + isAdmin: boolean; handleLogout: () => void; } -const Header: React.FC = ({ isLoggedIn, handleLogout }) => { +const Header: React.FC = ({ isLoggedIn, isAdmin, handleLogout }) => { const navigate = useNavigate(); return ( @@ -21,18 +23,24 @@ const Header: React.FC = ({ isLoggedIn, handleLogout }) => { /> {isLoggedIn && ( - +
+ + { isAdmin && } + + +
)} + {!isLoggedIn && (
diff --git a/client/src/components/Header/header.css b/client/src/components/Header/header.css index 379a60d..07aa8cf 100644 --- a/client/src/components/Header/header.css +++ b/client/src/components/Header/header.css @@ -11,4 +11,10 @@ .header img { cursor: pointer; +} + +.button-group { + display: flex; + flex-wrap: wrap; + gap: 10px; } \ No newline at end of file From 967a2ac6d70505d955dd024f84b0e3c3add95d45 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Fri, 14 Mar 2025 12:50:24 -0400 Subject: [PATCH 02/22] init admin backend --- server/app.js | 10 +++- server/controllers/admin.js | 44 ++++++++++++++ server/models/admin.js | 114 ++++++++++++++++++++++++++++++++++++ server/routers/admin.js | 12 ++++ 4 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 server/controllers/admin.js create mode 100644 server/models/admin.js create mode 100644 server/routers/admin.js diff --git a/server/app.js b/server/app.js index 938d2f0..ea9a9be 100644 --- a/server/app.js +++ b/server/app.js @@ -20,6 +20,8 @@ const users = require('./models/users.js'); const userModel = new users(db, foldersModel); const images = require('./models/images.js'); const imageModel = new images(db); +const Admin = require('./models/admin.js'); +const adminModel = new Admin(db); // instantiate the controllers const usersController = require('./controllers/users.js'); @@ -32,6 +34,8 @@ const quizController = require('./controllers/quiz.js'); const quizControllerInstance = new quizController(quizModel, foldersModel); const imagesController = require('./controllers/images.js'); const imagesControllerInstance = new imagesController(imageModel); +const AdminController = require('./controllers/admin.js'); +const AdminControllerInstance = new AdminController(adminModel); // export the controllers module.exports.users = usersControllerInstance; @@ -39,6 +43,7 @@ module.exports.rooms = roomsControllerInstance; module.exports.folders = foldersControllerInstance; module.exports.quizzes = quizControllerInstance; module.exports.images = imagesControllerInstance; +module.exports.admin = AdminControllerInstance; //import routers (instantiate controllers as side effect) const userRouter = require('./routers/users.js'); @@ -48,6 +53,7 @@ const quizRouter = require('./routers/quiz.js'); const imagesRouter = require('./routers/images.js') const AuthManager = require('./auth/auth-manager.js') const authRouter = require('./routers/auth.js') +const adminRouter = require('./routers/admin.js') // Setup environment dotenv.config(); @@ -100,6 +106,7 @@ app.use('/api/folder', folderRouter); app.use('/api/quiz', quizRouter); app.use('/api/image', imagesRouter); app.use('/api/auth', authRouter); +app.use('/api/admin', adminRouter); // Add Auths methods const session = require('express-session'); @@ -113,11 +120,9 @@ app.use(session({ let _authManager = new AuthManager(app,null,userModel); app.use(errorHandler); -// Start server async function start() { const port = process.env.PORT || 4400; - // Check DB connection await db.connect(); db.getConnection(); console.log(`Connexion MongoDB établie`); @@ -127,7 +132,6 @@ async function start() { }); } -// Graceful shutdown on SIGINT (Ctrl+C) process.on('SIGINT', async () => { console.log('Shutting down...'); await db.closeConnection(); diff --git a/server/controllers/admin.js b/server/controllers/admin.js new file mode 100644 index 0000000..88669c5 --- /dev/null +++ b/server/controllers/admin.js @@ -0,0 +1,44 @@ +const AppError = require('../middleware/AppError.js'); +const { MISSING_REQUIRED_PARAMETER, IMAGE_NOT_FOUND } = require('../constants/errorCodes'); + +class AdminController { + + constructor(model) { + this.model = model; + } + + get = async (req, res, next) => { + try { + const users = await this.model.getUsers(); + + return res.status(200).json({ + users: users + }); + } catch (error) { + return next(error); + } + }; + + delete = async (req, res, next) => { + try { + const { id } = req.params; + + if (!id) { + throw new AppError(MISSING_REQUIRED_PARAMETER); + } + + const user = await this.model.deleteUser(id); + + if (!user) { + throw new AppError(IMAGE_NOT_FOUND); + } + + return res.status(200).json({ user: user }); + } catch (error) { + return next(error); + } + }; + +} + +module.exports = AdminController; diff --git a/server/models/admin.js b/server/models/admin.js new file mode 100644 index 0000000..a7634d7 --- /dev/null +++ b/server/models/admin.js @@ -0,0 +1,114 @@ +const { ObjectId } = require('mongodb'); + +class Admin { + + constructor(db) { + this.db = db; + } + + async getUsers() { + await this.db.connect() + const conn = this.db.getConnection(); + + const usrColl = conn.collection('users'); + + const result = await usrColl.find({}).toArray(); + + if (!result) return null; + + return result; + } + + async deleteUser(id) { + let deleted = false; + await this.db.connect() + const conn = this.db.getConnection(); + + const usrColl = conn.collection('users'); + + const result = await usrColl.deleteOne({ _id: ObjectId.createFromHexString(id) }); + + if (result) deleted = true; + + return deleted; + } + + async getQuizzes() { + await this.db.connect() + const conn = this.db.getConnection(); + + const quizColl = conn.collection('files'); + + const result = await quizColl.find({}).toArray(); + + if (!result) return null; + + return result; + } + + async deleteQuiz(id) { + let deleted = false; + await this.db.connect() + const conn = this.db.getConnection(); + + const quizColl = conn.collection('files'); + + const result = await quizColl.deleteOne({ _id: ObjectId.createFromHexString(id) }); + + if (result) deleted = true; + + return deleted; + } + + async getImages(page, limit) { + await this.db.connect() + const conn = this.db.getConnection(); + + const imagesCollection = conn.collection('images'); + + + const total = await imagesCollection.countDocuments(); + if (!total || total === 0) return { images: [], total }; + + const result = await imagesCollection.find({}) + .sort({ created_at: 1 }) + .skip((page - 1) * limit) + .limit(limit) + .toArray(); + + const objImages = result.map(image => ({ + id: image._id, + user: image.userId, + file_name: image.file_name, + file_content: image.file_content.toString('base64'), + mime_type: image.mime_type + })); + + let respObj = { + images: objImages, + total: total + } + + return respObj; + } + + async deleteImage(uid, imgId) { + let resp = false; + await this.db.connect() + const conn = this.db.getConnection(); + const quizColl = conn.collection('files'); + const rgxImg = new RegExp(`/api/image/get/${imgId}`); + + const result = await quizColl.find({ userId: uid, content: { $regex: rgxImg }}).toArray(); + if(!result || result.length < 1){ + const imgsColl = conn.collection('images'); + const isDeleted = await imgsColl.deleteOne({ _id: ObjectId.createFromHexString(imgId) }); + if(isDeleted){ + resp = true; + } + } + return { deleted: resp }; + } +} + +module.exports = Admin; diff --git a/server/routers/admin.js b/server/routers/admin.js new file mode 100644 index 0000000..691c1f9 --- /dev/null +++ b/server/routers/admin.js @@ -0,0 +1,12 @@ +const express = require('express'); +const router = express.Router(); +const admin = require('../app.js').admin; +const asyncHandler = require('./routerUtils.js'); + +const jwt = require('../middleware/jwtToken.js'); + + +router.get("/get", jwt.authenticate, asyncHandler(admin.get)); +router.delete("/delete", jwt.authenticate, asyncHandler(admin.delete)); + +module.exports = router; From 985764a064d58e2ed02c2e9aea524d15ef9f2df8 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Fri, 14 Mar 2025 13:00:19 -0400 Subject: [PATCH 03/22] modified routes --- server/controllers/admin.js | 67 +++++++++++++++++++++++++++++++++++-- server/routers/admin.js | 8 +++-- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/server/controllers/admin.js b/server/controllers/admin.js index 88669c5..fb7071c 100644 --- a/server/controllers/admin.js +++ b/server/controllers/admin.js @@ -7,7 +7,7 @@ class AdminController { this.model = model; } - get = async (req, res, next) => { + getUsers = async (req, res, next) => { try { const users = await this.model.getUsers(); @@ -18,8 +18,71 @@ class AdminController { return next(error); } }; + + getQuizzes = async (req, res, next) => { + try { + const quizzes = await this.model.getQuizzes(); - delete = async (req, res, next) => { + return res.status(200).json({ quizzes }); + } catch (error) { + return next(error); + } + }; + + getImages = async (req, res, next) => { + try { + const page = parseInt(req.query.page) || 1; + const limit = parseInt(req.query.limit) || 10; + + const imgs = await this.model.getImages(page, limit); + + return res.status(200).json({ imgs }); + } catch (error) { + return next(error); + } + }; + + deleteUser = async (req, res, next) => { + try { + const { id } = req.params; + + if (!id) { + throw new AppError(MISSING_REQUIRED_PARAMETER); + } + + const user = await this.model.deleteUser(id); + + if (!user) { + throw new AppError(IMAGE_NOT_FOUND); + } + + return res.status(200).json({ user: user }); + } catch (error) { + return next(error); + } + }; + + deleteQuiz = async (req, res, next) => { + try { + const { id } = req.params; + + if (!id) { + throw new AppError(MISSING_REQUIRED_PARAMETER); + } + + const user = await this.model.deleteUser(id); + + if (!user) { + throw new AppError(IMAGE_NOT_FOUND); + } + + return res.status(200).json({ user: user }); + } catch (error) { + return next(error); + } + }; + + deleteImage = async (req, res, next) => { try { const { id } = req.params; diff --git a/server/routers/admin.js b/server/routers/admin.js index 691c1f9..b15aa07 100644 --- a/server/routers/admin.js +++ b/server/routers/admin.js @@ -6,7 +6,11 @@ const asyncHandler = require('./routerUtils.js'); const jwt = require('../middleware/jwtToken.js'); -router.get("/get", jwt.authenticate, asyncHandler(admin.get)); -router.delete("/delete", jwt.authenticate, asyncHandler(admin.delete)); +router.get("/getUsers", asyncHandler(admin.getUsers)); +router.get("/getQuizzes", asyncHandler(admin.getQuizzes)); +router.get("/getImages", asyncHandler(admin.getImages)); +router.delete("/deleteUser", asyncHandler(admin.deleteUser)); +router.delete("/deleteQuiz", asyncHandler(admin.deleteQuiz)); +router.delete("/deleteImage", jwt.authenticate, asyncHandler(admin.deleteImage)); module.exports = router; From f006dfd195490382962ee9c5a8c7567519c199dd Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Sun, 16 Mar 2025 00:52:22 -0400 Subject: [PATCH 04/22] FIX frontend admin --- client/src/App.tsx | 12 +- client/src/Types/ImageType.tsx | 17 +++ client/src/Types/QuizType.tsx | 8 ++ client/src/Types/UserType.tsx | 11 ++ .../components/AdminDrawer/AdminDrawer.tsx | 17 ++- client/src/pages/Admin/Images.tsx | 114 +++++++++++++++++ client/src/pages/Admin/Stats.tsx | 57 +++++++++ client/src/pages/Admin/Users.tsx | 57 +++++++++ client/src/services/ApiService.tsx | 119 +++++++++++++++++- server/controllers/admin.js | 2 +- server/models/admin.js | 18 +-- server/routers/admin.js | 1 - 12 files changed, 407 insertions(+), 26 deletions(-) create mode 100644 client/src/Types/ImageType.tsx create mode 100644 client/src/Types/UserType.tsx create mode 100644 client/src/pages/Admin/Images.tsx create mode 100644 client/src/pages/Admin/Stats.tsx create mode 100644 client/src/pages/Admin/Users.tsx diff --git a/client/src/App.tsx b/client/src/App.tsx index 6a2d060..fb20f4f 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -26,10 +26,15 @@ import Footer from './components/Footer/Footer'; import ApiService from './services/ApiService'; import OAuthCallback from './pages/AuthManager/callback/AuthCallback'; +import Users from './pages/Admin/Users'; +import Images from './pages/Admin/Images'; +import Stats from './pages/Admin/Stats'; + + const App: React.FC = () => { const [isAuthenticated, setIsAuthenticated] = useState(ApiService.isLoggedIn()); const [isTeacherAuthenticated, setIsTeacherAuthenticated] = useState(ApiService.isLoggedInTeacher()); - const [isAdmin, setIsAdmin] = useState(false); + //const [isAdmin, setIsAdmin] = useState(false); const [isRoomRequireAuthentication, setRoomsRequireAuth] = useState(null); const location = useLocation(); @@ -100,6 +105,11 @@ const App: React.FC = () => { {/* Pages authentification sélection */} } /> + + } /> + } /> + } /> +
diff --git a/client/src/Types/ImageType.tsx b/client/src/Types/ImageType.tsx new file mode 100644 index 0000000..d0a1541 --- /dev/null +++ b/client/src/Types/ImageType.tsx @@ -0,0 +1,17 @@ +export interface ImageType { + id: string; + file_content: string; + file_name: string; + mime_type: string; +} + +export interface ImagesResponse { + images: ImageType[]; + total: number; +} + +export interface ImagesParams { + page: number; + limit: number; + uid?: string; +} \ No newline at end of file diff --git a/client/src/Types/QuizType.tsx b/client/src/Types/QuizType.tsx index b5e2b08..5c2eb0d 100644 --- a/client/src/Types/QuizType.tsx +++ b/client/src/Types/QuizType.tsx @@ -9,3 +9,11 @@ export interface QuizType { created_at: Date; updated_at: Date; } + +export interface QuizTypeShort { + _id: string; + userId: string; + title: string; + created_at: Date; + updated_at: Date; +} \ No newline at end of file diff --git a/client/src/Types/UserType.tsx b/client/src/Types/UserType.tsx new file mode 100644 index 0000000..db6336d --- /dev/null +++ b/client/src/Types/UserType.tsx @@ -0,0 +1,11 @@ +export interface UserType { + id: string; + name: string; + email: string; + created_at: string; + roles: string[]; +} + +export interface UsersResponse { + users: UserType[]; +} \ No newline at end of file diff --git a/client/src/components/AdminDrawer/AdminDrawer.tsx b/client/src/components/AdminDrawer/AdminDrawer.tsx index d61df5e..900634d 100644 --- a/client/src/components/AdminDrawer/AdminDrawer.tsx +++ b/client/src/components/AdminDrawer/AdminDrawer.tsx @@ -10,6 +10,7 @@ import ListItemText from '@mui/material/ListItemText'; import BarChartIcon from '@mui/icons-material/BarChart'; import ImageIcon from '@mui/icons-material/Image'; import PeopleIcon from '@mui/icons-material/People'; +import { useNavigate } from 'react-router-dom'; const styles = { drawerBg: 'rgba(82, 113, 255, 0.85)', @@ -21,23 +22,29 @@ const styles = { export default function AdminDrawer() { const [open, setOpen] = React.useState(false); + const navigate = useNavigate(); const toggleDrawer = (isOpen: boolean) => () => { setOpen(isOpen); }; + const handleNavigation = (path: string) => { + navigate(path); + setOpen(false); + }; + const menuItems = [ - { text: 'Stats', icon: }, - { text: 'Images', icon: }, - { text: 'Users', icon: }, + { text: 'Stats', icon: , path: '/admin/stats' }, + { text: 'Images', icon: , path: '/admin/images' }, + { text: 'Users', icon: , path: '/admin/users' }, ]; const list = ( - {menuItems.map(({ text, icon }) => ( + {menuItems.map(({ text, icon, path }) => ( - + handleNavigation(path)}> {icon} diff --git a/client/src/pages/Admin/Images.tsx b/client/src/pages/Admin/Images.tsx new file mode 100644 index 0000000..ad6326e --- /dev/null +++ b/client/src/pages/Admin/Images.tsx @@ -0,0 +1,114 @@ +import React, { useState, useEffect } from "react"; +import { + Table, + TableBody, + TableCell, + TableContainer, + TableRow, + TableHead, + IconButton, + Paper, + Box, + CircularProgress, + Button, + Typography +} from "@mui/material"; +import DeleteIcon from "@mui/icons-material/Delete"; +import { ImageType } from "../../Types/ImageType"; +import ApiService from '../../services/ApiService'; + +const Images: React.FC = () => { + const [images, setImages] = useState([]); + const [totalImg, setTotalImg] = useState(0); + const [imgPage, setImgPage] = useState(1); + const [imgLimit] = useState(10); + const [loading, setLoading] = useState(false); + + const fetchImages = async (page: number, limit: number) => { + setLoading(true); + const data = await ApiService.getImages(page, limit); + setImages(data.images); + setTotalImg(data.total); + setLoading(false); + }; + + useEffect(() => { + fetchImages(imgPage, imgLimit); + }, [imgPage]); + + + const handleDelete = async (id: string) => { + setLoading(true); + const isDeleted = await ApiService.deleteImage(id); + setLoading(false); + if (isDeleted) { + setImages(images.filter(image => image.id !== id)); + } + }; + + const handleNextPage = () => { + if ((imgPage * imgLimit) < totalImg) { + setImgPage(prev => prev + 1); + } + }; + + const handlePrevPage = () => { + if (imgPage > 1) { + setImgPage(prev => prev - 1); + } + }; + + return ( + + {loading ? ( + + + + ) : ( + + + + + + Nom + Image ID + + + + {images.map((obj: ImageType) => ( + + + {`Image + + {obj.file_name} + + {obj.id} + + + handleDelete(obj.id)} color="error"> + + + + + ))} + +
+
+ )} + + + + +
+ ); +}; + +export default Images; diff --git a/client/src/pages/Admin/Stats.tsx b/client/src/pages/Admin/Stats.tsx new file mode 100644 index 0000000..6c42b70 --- /dev/null +++ b/client/src/pages/Admin/Stats.tsx @@ -0,0 +1,57 @@ +import React, { useState, useEffect } from "react"; +import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, IconButton } from "@mui/material"; +import DeleteIcon from "@mui/icons-material/Delete"; +import ApiService from '../../services/ApiService'; +import { QuizTypeShort } from "../../Types/QuizType"; + +const Users: React.FC = () => { + const [quizzes, setQuizzes] = useState([]); + + useEffect(() => { + const fetchUsers = async () => { + try { + const data = await ApiService.getQuizzes(); + setQuizzes(data); + } catch (error) { + console.error("Error fetching quizzes:", error); + } + }; + fetchUsers(); + }, []); + + const handleDelete = (id: string) => { + setQuizzes(quizzes.filter(quiz => quiz._id !== id)); + }; + + return ( + + + + + Enseignant + Titre + Crée + Modifié + + + + {quizzes.map((quiz) => ( + + {quiz.userId} + {quiz.title} + {new Date(quiz.created_at).toLocaleDateString()} + {new Date(quiz.updated_at).toLocaleDateString()} + + handleDelete(quiz._id)} color="error"> + + + + + ))} + +
+
+ ); +}; + +export default Users; diff --git a/client/src/pages/Admin/Users.tsx b/client/src/pages/Admin/Users.tsx new file mode 100644 index 0000000..059f4c0 --- /dev/null +++ b/client/src/pages/Admin/Users.tsx @@ -0,0 +1,57 @@ +import React, { useState, useEffect } from "react"; +import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, IconButton } from "@mui/material"; +import DeleteIcon from "@mui/icons-material/Delete"; +import ApiService from '../../services/ApiService'; +import { UserType } from "../../Types/UserType"; + +const Users: React.FC = () => { + const [users, setUsers] = useState([]); + + useEffect(() => { + const fetchUsers = async () => { + try { + const data = await ApiService.getUsers(); + setUsers(data); + } catch (error) { + console.error("Error fetching users:", error); + } + }; + fetchUsers(); + }, []); + + const handleDelete = (email: string) => { + setUsers(users.filter(user => user.email !== email)); + }; + + return ( + + + + + Nom + Courriel + Crée + Roles + + + + {users.map((user) => ( + + {user.name} + {user.email} + {new Date(user.created_at).toLocaleDateString()} + {user.roles?.join(", ")} + + handleDelete(user.email)} color="error"> + + + + + ))} + +
+
+ ); +}; + +export default Users; diff --git a/client/src/services/ApiService.tsx b/client/src/services/ApiService.tsx index b13a369..0991bc5 100644 --- a/client/src/services/ApiService.tsx +++ b/client/src/services/ApiService.tsx @@ -3,8 +3,10 @@ import { jwtDecode } from 'jwt-decode'; import { ENV_VARIABLES } from '../constants'; import { FolderType } from 'src/Types/FolderType'; -import { QuizType } from 'src/Types/QuizType'; +import { QuizType, QuizTypeShort } from 'src/Types/QuizType'; import { RoomType } from 'src/Types/RoomType'; +import { UserType } from 'src/Types/UserType'; +import { ImagesResponse, ImagesParams } from 'src/Types/ImageType'; type ApiResponse = boolean | string; @@ -1009,6 +1011,7 @@ public async login(email: string, password: string): Promise { return `Une erreur inattendue s'est produite.`; } } + public async getRoomTitle(roomId: string): Promise { try { if (!roomId) { @@ -1167,8 +1170,120 @@ public async login(email: string, password: string): Promise { return `ERROR : Une erreur inattendue s'est produite.` } } - // NOTE : Get Image pas necessaire + public async getUsers(): Promise { + try { + + const url: string = this.constructRequestUrl(`/admin/getUsers`); + const headers = this.constructRequestHeaders(); + const result: AxiosResponse = await axios.get(url, { headers }); + + if (result.status !== 200) { + throw new Error(`L'obtention des titres des salles a échoué. Status: ${result.status}`); + } + console.log(result.data); + + return result.data.users; + } catch (error) { + console.log("Error details: ", error); + + if (axios.isAxiosError(error)) { + const err = error as AxiosError; + const data = err.response?.data as { error: string } | undefined; + const msg = data?.error || 'Erreur serveur inconnue lors de la requête.'; + throw new Error(`L'enregistrement a échoué. Status: ${msg}`); + } + + throw new Error(`ERROR : Une erreur inattendue s'est produite.`); + } + } + + public async getImages(page: number, limit: number): Promise { + try { + const url: string = this.constructRequestUrl(`/admin/getImages`); + const headers = this.constructRequestHeaders(); + let params : ImagesParams = { page: page, limit: limit }; + + const result: AxiosResponse = await axios.get(url, { params: params, headers: headers }); + + if (result.status !== 200) { + throw new Error(`L'affichage des images a échoué. Status: ${result.status}`); + } + console.log(result.data); + const images = result.data.data; + + return images; + + } catch (error) { + console.log("Error details: ", error); + + if (axios.isAxiosError(error)) { + const err = error as AxiosError; + const data = err.response?.data as { error: string } | undefined; + const msg = data?.error || 'Erreur serveur inconnue lors de la requête.'; + throw new Error(`L'enregistrement a échoué. Status: ${msg}`); + } + + throw new Error(`ERROR : Une erreur inattendue s'est produite.`); + } + } + + public async deleteImage(imgId: string): Promise { + try { + const url: string = this.constructRequestUrl(`/admin/deleteImage`); + const headers = this.constructRequestHeaders(); + let params = { imgId: imgId }; + + const result: AxiosResponse = await axios.delete(url, { params: params, headers: headers }); + + if (result.status !== 200) { + throw new Error(`La suppression de l'image a échoué. Status: ${result.status}`); + } + + const deleted = result.data.deleted; + return deleted; + + } catch (error) { + console.log("Error details: ", error); + + if (axios.isAxiosError(error)) { + const err = error as AxiosError; + const data = err.response?.data as { error: string } | undefined; + const msg = data?.error || 'Erreur serveur inconnue lors de la requête.'; + throw new Error(`L'enregistrement a échoué. Status: ${msg}`); + } + + throw new Error(`ERROR : Une erreur inattendue s'est produite.`); + } + } + + public async getQuizzes(): Promise { + try { + const url: string = this.constructRequestUrl(`/admin/getQuizzes`); + const headers = this.constructRequestHeaders(); + const result: AxiosResponse = await axios.get(url, { headers }); + + if (result.status !== 200) { + throw new Error(`L'affichage des images a échoué. Status: ${result.status}`); + } + const quiz = result.data.quizzes; + + return quiz; + + } catch (error) { + console.log("Error details: ", error); + + if (axios.isAxiosError(error)) { + const err = error as AxiosError; + const data = err.response?.data as { error: string } | undefined; + const msg = data?.error || 'Erreur serveur inconnue lors de la requête.'; + throw new Error(`L'enregistrement a échoué. Status: ${msg}`); + } + + throw new Error(`ERROR : Une erreur inattendue s'est produite.`); + } + } + } const apiService = new ApiService(); diff --git a/server/controllers/admin.js b/server/controllers/admin.js index fb7071c..6487901 100644 --- a/server/controllers/admin.js +++ b/server/controllers/admin.js @@ -36,7 +36,7 @@ class AdminController { const imgs = await this.model.getImages(page, limit); - return res.status(200).json({ imgs }); + return res.status(200).json({ data: imgs }); } catch (error) { return next(error); } diff --git a/server/models/admin.js b/server/models/admin.js index a7634d7..df4b8f1 100644 --- a/server/models/admin.js +++ b/server/models/admin.js @@ -38,27 +38,13 @@ class Admin { const conn = this.db.getConnection(); const quizColl = conn.collection('files'); - - const result = await quizColl.find({}).toArray(); + const projection = { content: 0, folderName: 0, folderId: 0 }; + const result = await quizColl.find({}, projection).toArray(); if (!result) return null; return result; } - - async deleteQuiz(id) { - let deleted = false; - await this.db.connect() - const conn = this.db.getConnection(); - - const quizColl = conn.collection('files'); - - const result = await quizColl.deleteOne({ _id: ObjectId.createFromHexString(id) }); - - if (result) deleted = true; - - return deleted; - } async getImages(page, limit) { await this.db.connect() diff --git a/server/routers/admin.js b/server/routers/admin.js index b15aa07..3f547da 100644 --- a/server/routers/admin.js +++ b/server/routers/admin.js @@ -10,7 +10,6 @@ router.get("/getUsers", asyncHandler(admin.getUsers)); router.get("/getQuizzes", asyncHandler(admin.getQuizzes)); router.get("/getImages", asyncHandler(admin.getImages)); router.delete("/deleteUser", asyncHandler(admin.deleteUser)); -router.delete("/deleteQuiz", asyncHandler(admin.deleteQuiz)); router.delete("/deleteImage", jwt.authenticate, asyncHandler(admin.deleteImage)); module.exports = router; From 3124d23df37640322b1bbc9a391931486a0f2943 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Sun, 16 Mar 2025 20:26:55 -0400 Subject: [PATCH 05/22] FIX admin delete image route --- client/package-lock.json | 26 +++---- client/package.json | 2 +- client/src/Types/QuizType.tsx | 5 ++ client/src/pages/Admin/Images.tsx | 3 +- client/src/pages/Admin/Stats.tsx | 120 +++++++++++++++++++++-------- client/src/services/ApiService.tsx | 12 ++- server/controllers/admin.js | 37 ++------- server/models/admin.js | 16 +++- server/models/quiz.js | 1 + server/routers/admin.js | 8 +- 10 files changed, 139 insertions(+), 91 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index e2c6890..19cd5cd 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -16,7 +16,7 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@mui/icons-material": "^6.4.6", "@mui/lab": "^5.0.0-alpha.153", - "@mui/material": "^6.4.6", + "@mui/material": "^6.4.7", "@types/uuid": "^9.0.7", "axios": "^1.8.1", "dompurify": "^3.2.3", @@ -3397,9 +3397,9 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.6.tgz", - "integrity": "sha512-rho5Q4IscbrVmK9rCrLTJmjLjfH6m/NcqKr/mchvck0EIXlyYUB9+Z0oVmkt/+Mben43LMRYBH8q/Uzxj/c4Vw==", + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.7.tgz", + "integrity": "sha512-XjJrKFNt9zAKvcnoIIBquXyFyhfrHYuttqMsoDS7lM7VwufYG4fAPw4kINjBFg++fqXM2BNAuWR9J7XVIuKIKg==", "license": "MIT", "funding": { "type": "opencollective", @@ -3474,14 +3474,14 @@ } }, "node_modules/@mui/material": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.6.tgz", - "integrity": "sha512-6UyAju+DBOdMogfYmLiT3Nu7RgliorimNBny1pN/acOjc+THNFVE7hlxLyn3RDONoZJNDi/8vO4AQQr6dLAXqA==", + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.7.tgz", + "integrity": "sha512-K65StXUeGAtFJ4ikvHKtmDCO5Ab7g0FZUu2J5VpoKD+O6Y3CjLYzRi+TMlI3kaL4CL158+FccMoOd/eaddmeRQ==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", - "@mui/core-downloads-tracker": "^6.4.6", - "@mui/system": "^6.4.6", + "@mui/core-downloads-tracker": "^6.4.7", + "@mui/system": "^6.4.7", "@mui/types": "^7.2.21", "@mui/utils": "^6.4.6", "@popperjs/core": "^2.11.8", @@ -3502,7 +3502,7 @@ "peerDependencies": { "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", - "@mui/material-pigment-css": "^6.4.6", + "@mui/material-pigment-css": "^6.4.7", "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -3584,9 +3584,9 @@ } }, "node_modules/@mui/material/node_modules/@mui/system": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.6.tgz", - "integrity": "sha512-FQjWwPec7pMTtB/jw5f9eyLynKFZ6/Ej9vhm5kGdtmts1z5b7Vyn3Rz6kasfYm1j2TfrfGnSXRvvtwVWxjpz6g==", + "version": "6.4.7", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.7.tgz", + "integrity": "sha512-7wwc4++Ak6tGIooEVA9AY7FhH2p9fvBMORT4vNLMAysH3Yus/9B9RYMbrn3ANgsOyvT3Z7nE+SP8/+3FimQmcg==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", diff --git a/client/package.json b/client/package.json index b6d62e4..45352e6 100644 --- a/client/package.json +++ b/client/package.json @@ -20,7 +20,7 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@mui/icons-material": "^6.4.6", "@mui/lab": "^5.0.0-alpha.153", - "@mui/material": "^6.4.6", + "@mui/material": "^6.4.7", "@types/uuid": "^9.0.7", "axios": "^1.8.1", "dompurify": "^3.2.3", diff --git a/client/src/Types/QuizType.tsx b/client/src/Types/QuizType.tsx index 5c2eb0d..f97809f 100644 --- a/client/src/Types/QuizType.tsx +++ b/client/src/Types/QuizType.tsx @@ -16,4 +16,9 @@ export interface QuizTypeShort { title: string; created_at: Date; updated_at: Date; +} + +export interface QuizResponse { + quizzes: QuizTypeShort[]; + total: number; } \ No newline at end of file diff --git a/client/src/pages/Admin/Images.tsx b/client/src/pages/Admin/Images.tsx index ad6326e..a03f70d 100644 --- a/client/src/pages/Admin/Images.tsx +++ b/client/src/pages/Admin/Images.tsx @@ -10,8 +10,7 @@ import { Paper, Box, CircularProgress, - Button, - Typography + Button } from "@mui/material"; import DeleteIcon from "@mui/icons-material/Delete"; import { ImageType } from "../../Types/ImageType"; diff --git a/client/src/pages/Admin/Stats.tsx b/client/src/pages/Admin/Stats.tsx index 6c42b70..50563e5 100644 --- a/client/src/pages/Admin/Stats.tsx +++ b/client/src/pages/Admin/Stats.tsx @@ -1,56 +1,114 @@ import React, { useState, useEffect } from "react"; -import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, IconButton } from "@mui/material"; +import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, IconButton, Grid, Typography, CircularProgress, Box } from "@mui/material"; import DeleteIcon from "@mui/icons-material/Delete"; import ApiService from '../../services/ApiService'; import { QuizTypeShort } from "../../Types/QuizType"; const Users: React.FC = () => { const [quizzes, setQuizzes] = useState([]); + const [monthlyQuizzes, setMonthlyQuizzes] = useState(0); + const [totalUsers, setTotalUsers] = useState(0); + const [loading, setLoading] = useState(true); useEffect(() => { - const fetchUsers = async () => { + const fetchStats = async () => { try { - const data = await ApiService.getQuizzes(); - setQuizzes(data); + const data = await ApiService.getStats(); + setQuizzes(data.quizzes); + setTotalUsers(data.total); + + const currentMonth = new Date().getMonth(); + const currentYear = new Date().getFullYear(); + const filteredQuizzes = data.quizzes.filter((quiz: QuizTypeShort) => { + const quizDate = new Date(quiz.created_at); + return quizDate.getMonth() === currentMonth && quizDate.getFullYear() === currentYear; + }); + + setMonthlyQuizzes(filteredQuizzes.length === 0 ? 10 : 0); } catch (error) { console.error("Error fetching quizzes:", error); + } finally { + setLoading(false); } }; - fetchUsers(); + fetchStats(); }, []); const handleDelete = (id: string) => { setQuizzes(quizzes.filter(quiz => quiz._id !== id)); }; + const totalQuizzes = quizzes.length; + + if (loading) { + return ( + + + + ); + } + return ( - - - - - Enseignant - Titre - Crée - Modifié - - - - {quizzes.map((quiz) => ( - - {quiz.userId} - {quiz.title} - {new Date(quiz.created_at).toLocaleDateString()} - {new Date(quiz.updated_at).toLocaleDateString()} - - handleDelete(quiz._id)} color="error"> - - - + + + + Quiz du Mois + + + + {monthlyQuizzes} + + + + + Quiz total + + + + {totalQuizzes} + + + + + Enseignants + + + + {totalUsers} + + + + + + {/* Table */} + +
+ + + Enseignant + Titre + Crée + Modifié - ))} - -
-
+ + + {quizzes.map((quiz) => ( + + {quiz.userId} + {quiz.title} + {new Date(quiz.created_at).toLocaleDateString()} + {new Date(quiz.updated_at).toLocaleDateString()} + + handleDelete(quiz._id)} color="error"> + + + + + ))} + + + + ); }; diff --git a/client/src/services/ApiService.tsx b/client/src/services/ApiService.tsx index 0991bc5..209f370 100644 --- a/client/src/services/ApiService.tsx +++ b/client/src/services/ApiService.tsx @@ -3,7 +3,7 @@ import { jwtDecode } from 'jwt-decode'; import { ENV_VARIABLES } from '../constants'; import { FolderType } from 'src/Types/FolderType'; -import { QuizType, QuizTypeShort } from 'src/Types/QuizType'; +import { QuizType, QuizResponse } from 'src/Types/QuizType'; import { RoomType } from 'src/Types/RoomType'; import { UserType } from 'src/Types/UserType'; import { ImagesResponse, ImagesParams } from 'src/Types/ImageType'; @@ -1181,7 +1181,6 @@ public async login(email: string, password: string): Promise { if (result.status !== 200) { throw new Error(`L'obtention des titres des salles a échoué. Status: ${result.status}`); } - console.log(result.data); return result.data.users; } catch (error) { @@ -1209,7 +1208,6 @@ public async login(email: string, password: string): Promise { if (result.status !== 200) { throw new Error(`L'affichage des images a échoué. Status: ${result.status}`); } - console.log(result.data); const images = result.data.data; return images; @@ -1257,18 +1255,18 @@ public async login(email: string, password: string): Promise { } } - public async getQuizzes(): Promise { + public async getStats(): Promise { try { - const url: string = this.constructRequestUrl(`/admin/getQuizzes`); + const url: string = this.constructRequestUrl(`/admin/getStats`); const headers = this.constructRequestHeaders(); const result: AxiosResponse = await axios.get(url, { headers }); if (result.status !== 200) { throw new Error(`L'affichage des images a échoué. Status: ${result.status}`); } - const quiz = result.data.quizzes; + const resp = result.data.data; - return quiz; + return resp; } catch (error) { console.log("Error details: ", error); diff --git a/server/controllers/admin.js b/server/controllers/admin.js index 6487901..37de936 100644 --- a/server/controllers/admin.js +++ b/server/controllers/admin.js @@ -19,11 +19,11 @@ class AdminController { } }; - getQuizzes = async (req, res, next) => { + getStats = async (req, res, next) => { try { - const quizzes = await this.model.getQuizzes(); + const data = await this.model.getStats(); - return res.status(200).json({ quizzes }); + return res.status(200).json({ data }); } catch (error) { return next(error); } @@ -62,41 +62,20 @@ class AdminController { } }; - deleteQuiz = async (req, res, next) => { - try { - const { id } = req.params; - - if (!id) { - throw new AppError(MISSING_REQUIRED_PARAMETER); - } - - const user = await this.model.deleteUser(id); - - if (!user) { - throw new AppError(IMAGE_NOT_FOUND); - } - - return res.status(200).json({ user: user }); - } catch (error) { - return next(error); - } - }; - deleteImage = async (req, res, next) => { try { - const { id } = req.params; - - if (!id) { + const { imgId } = req.query; + if (!imgId) { throw new AppError(MISSING_REQUIRED_PARAMETER); } - const user = await this.model.deleteUser(id); + const deleted = await this.model.deleteImage(imgId); - if (!user) { + if (!deleted) { throw new AppError(IMAGE_NOT_FOUND); } - return res.status(200).json({ user: user }); + return res.status(200).json({ deleted }); } catch (error) { return next(error); } diff --git a/server/models/admin.js b/server/models/admin.js index df4b8f1..d85b217 100644 --- a/server/models/admin.js +++ b/server/models/admin.js @@ -33,17 +33,25 @@ class Admin { return deleted; } - async getQuizzes() { + async getStats() { await this.db.connect() const conn = this.db.getConnection(); + const usrColl = conn.collection('users'); + const total = await usrColl.countDocuments(); const quizColl = conn.collection('files'); + const projection = { content: 0, folderName: 0, folderId: 0 }; const result = await quizColl.find({}, projection).toArray(); if (!result) return null; - return result; + let respObj = { + quizzes: result, + total: total + } + + return respObj; } async getImages(page, limit) { @@ -78,14 +86,14 @@ class Admin { return respObj; } - async deleteImage(uid, imgId) { + async deleteImage(imgId) { let resp = false; await this.db.connect() const conn = this.db.getConnection(); const quizColl = conn.collection('files'); const rgxImg = new RegExp(`/api/image/get/${imgId}`); - const result = await quizColl.find({ userId: uid, content: { $regex: rgxImg }}).toArray(); + const result = await quizColl.find({ content: { $regex: rgxImg }}).toArray(); if(!result || result.length < 1){ const imgsColl = conn.collection('images'); const isDeleted = await imgsColl.deleteOne({ _id: ObjectId.createFromHexString(imgId) }); diff --git a/server/models/quiz.js b/server/models/quiz.js index b388659..99527bd 100644 --- a/server/models/quiz.js +++ b/server/models/quiz.js @@ -70,6 +70,7 @@ class Quiz { return true; } + async deleteQuizzesByFolderId(folderId) { await this.db.connect(); const conn = this.db.getConnection(); diff --git a/server/routers/admin.js b/server/routers/admin.js index 3f547da..04e43b9 100644 --- a/server/routers/admin.js +++ b/server/routers/admin.js @@ -6,10 +6,10 @@ const asyncHandler = require('./routerUtils.js'); const jwt = require('../middleware/jwtToken.js'); -router.get("/getUsers", asyncHandler(admin.getUsers)); -router.get("/getQuizzes", asyncHandler(admin.getQuizzes)); -router.get("/getImages", asyncHandler(admin.getImages)); -router.delete("/deleteUser", asyncHandler(admin.deleteUser)); +router.get("/getUsers", jwt.authenticate, asyncHandler(admin.getUsers)); +router.get("/getStats", jwt.authenticate, asyncHandler(admin.getStats)); +router.get("/getImages", jwt.authenticate, asyncHandler(admin.getImages)); +router.delete("/deleteUser", jwt.authenticate, asyncHandler(admin.deleteUser)); router.delete("/deleteImage", jwt.authenticate, asyncHandler(admin.deleteImage)); module.exports = router; From c4b90a8ee5953c2814668f404c2c132dd9728513 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Mon, 17 Mar 2025 19:06:25 -0400 Subject: [PATCH 06/22] FIX - email au lieu du userId --- client/src/Types/QuizType.tsx | 2 +- client/src/pages/Admin/Stats.tsx | 2 +- server/models/admin.js | 29 +++++++++++++++++++++++++---- 3 files changed, 27 insertions(+), 6 deletions(-) diff --git a/client/src/Types/QuizType.tsx b/client/src/Types/QuizType.tsx index f97809f..5078eda 100644 --- a/client/src/Types/QuizType.tsx +++ b/client/src/Types/QuizType.tsx @@ -12,7 +12,7 @@ export interface QuizType { export interface QuizTypeShort { _id: string; - userId: string; + email: string; title: string; created_at: Date; updated_at: Date; diff --git a/client/src/pages/Admin/Stats.tsx b/client/src/pages/Admin/Stats.tsx index 50563e5..334ecea 100644 --- a/client/src/pages/Admin/Stats.tsx +++ b/client/src/pages/Admin/Stats.tsx @@ -94,7 +94,7 @@ const Users: React.FC = () => { {quizzes.map((quiz) => ( - {quiz.userId} + {quiz.email} {quiz.title} {new Date(quiz.created_at).toLocaleDateString()} {new Date(quiz.updated_at).toLocaleDateString()} diff --git a/server/models/admin.js b/server/models/admin.js index d85b217..95e317d 100644 --- a/server/models/admin.js +++ b/server/models/admin.js @@ -41,10 +41,31 @@ class Admin { const quizColl = conn.collection('files'); - const projection = { content: 0, folderName: 0, folderId: 0 }; - const result = await quizColl.find({}, projection).toArray(); - - if (!result) return null; + const result = await quizColl.aggregate([ + { + $addFields: { userId: { $toObjectId: "$userId" } } + }, + { + $lookup: { + from: "users", + localField: "userId", + foreignField: "_id", + as: "user" + } + }, + { + $unwind: "$user" + }, + { + $project: { + _id: 1, + email: "$user.email", + title: 1, + created_at: 1, + updated_at: 1 + } + } + ]).toArray(); let respObj = { quizzes: result, From dd1c032e17dde82dcc5bbb898c7b7b731a586e32 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Mon, 17 Mar 2025 19:31:50 -0400 Subject: [PATCH 07/22] ajout tests backend --- server/__tests__/admin.test.js | 104 +++++++++++++++++++++++++++++++++ server/models/admin.js | 2 +- 2 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 server/__tests__/admin.test.js diff --git a/server/__tests__/admin.test.js b/server/__tests__/admin.test.js new file mode 100644 index 0000000..ac60b45 --- /dev/null +++ b/server/__tests__/admin.test.js @@ -0,0 +1,104 @@ +const { ObjectId } = require('mongodb'); +const Admin = require('../models/admin'); // Adjust the path if needed + +// Mock database connection +const mockDb = { + connect: jest.fn(), + getConnection: jest.fn() +}; + +const mockCollectionUsers = { + find: jest.fn().mockReturnThis(), + toArray: jest.fn(), + deleteOne: jest.fn(), + countDocuments: jest.fn(), + aggregate: jest.fn().mockReturnThis(), + skip: jest.fn().mockReturnThis(), + limit: jest.fn().mockReturnThis(), + sort: jest.fn().mockReturnThis() +}; + +const mockCollectionFiles = { ...mockCollectionUsers }; +const mockCollectionImages = { ...mockCollectionUsers }; + +mockDb.getConnection.mockReturnValue({ + collection: jest.fn((name) => { + if (name === 'users') return mockCollectionUsers; + if (name === 'files') return mockCollectionFiles; + if (name === 'images') return mockCollectionImages; + }) +}); + +describe('Admin class', () => { + let admin; + + beforeEach(() => { + jest.clearAllMocks(); + admin = new Admin(mockDb); + }); + + test('getUsers should return users', async () => { + const mockUsers = [{ _id: new ObjectId(), email: 'test@example.com' }]; + mockCollectionUsers.toArray.mockResolvedValue(mockUsers); + + const users = await admin.getUsers(); + expect(users).toEqual(mockUsers); + }); + + test('deleteUser should return true when user is deleted', async () => { + mockCollectionUsers.deleteOne.mockResolvedValue({ deletedCount: 1 }); + + const result = await admin.deleteUser(new ObjectId().toHexString()); + expect(result).toBe(true); + }); + + test('deleteUser should return false if no user was deleted', async () => { + mockCollectionUsers.deleteOne.mockResolvedValue({ deletedCount: 0 }); + + const result = await admin.deleteUser(new ObjectId().toHexString()); + expect(result).toBe(false); + }); + + test('getStats should return correct stats', async () => { + mockCollectionUsers.countDocuments.mockResolvedValue(10); + mockCollectionFiles.toArray.mockResolvedValue([{ _id: new ObjectId(), email: 'user@example.com', title: 'Test Quiz' }]); + + const stats = await admin.getStats(); + expect(stats).toEqual({ quizzes: [{ _id: expect.any(ObjectId), email: 'user@example.com', title: 'Test Quiz' }], total: 10 }); + }); + + test('getImages should return paginated images', async () => { + mockCollectionImages.countDocuments.mockResolvedValue(5); + mockCollectionImages.toArray.mockResolvedValue([ + { _id: new ObjectId(), userId: 'user1', file_name: 'image.png', file_content: Buffer.from('data'), mime_type: 'image/png' } + ]); + + const images = await admin.getImages(1, 10); + expect(images).toEqual({ + images: [{ + id: expect.any(ObjectId), + user: 'user1', + file_name: 'image.png', + file_content: expect.any(String), + mime_type: 'image/png' + }], + total: 5 + }); + }); + + test('deleteImage should return true when an image is deleted', async () => { + mockCollectionFiles.toArray.mockResolvedValue([]); + mockCollectionImages.deleteOne.mockResolvedValue({ deletedCount: 1 }); + + const result = await admin.deleteImage(new ObjectId().toHexString()); + expect(result).toEqual({ deleted: true }); + }); + + test('deleteImage should return false when an image is not deleted', async () => { + mockCollectionFiles.toArray.mockResolvedValue([{ _id: new ObjectId(), email: 'user@example.com', title: 'Test Quiz' }]); + mockCollectionImages.deleteOne.mockResolvedValue({ deletedCount: 0 }); + + const result = await admin.deleteImage(new ObjectId().toHexString()); + expect(result).toEqual({ deleted: false }); + }); +}); diff --git a/server/models/admin.js b/server/models/admin.js index 95e317d..5b82020 100644 --- a/server/models/admin.js +++ b/server/models/admin.js @@ -28,7 +28,7 @@ class Admin { const result = await usrColl.deleteOne({ _id: ObjectId.createFromHexString(id) }); - if (result) deleted = true; + if (result && result.deletedCount > 0) deleted = true; return deleted; } From be65a2b582bfda4ce3b57a705a6ba392e35babd6 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Mon, 17 Mar 2025 19:32:36 -0400 Subject: [PATCH 08/22] removed comments --- server/__tests__/admin.test.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/server/__tests__/admin.test.js b/server/__tests__/admin.test.js index ac60b45..b5a398a 100644 --- a/server/__tests__/admin.test.js +++ b/server/__tests__/admin.test.js @@ -1,7 +1,6 @@ const { ObjectId } = require('mongodb'); -const Admin = require('../models/admin'); // Adjust the path if needed +const Admin = require('../models/admin'); -// Mock database connection const mockDb = { connect: jest.fn(), getConnection: jest.fn() From abfebeb9923393c8af4a05438c3ec750d768d331 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Mon, 17 Mar 2025 20:47:07 -0400 Subject: [PATCH 09/22] added filters --- client/src/pages/Admin/Stats.tsx | 66 +++++++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 5 deletions(-) diff --git a/client/src/pages/Admin/Stats.tsx b/client/src/pages/Admin/Stats.tsx index 334ecea..8dab712 100644 --- a/client/src/pages/Admin/Stats.tsx +++ b/client/src/pages/Admin/Stats.tsx @@ -1,30 +1,39 @@ import React, { useState, useEffect } from "react"; -import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, IconButton, Grid, Typography, CircularProgress, Box } from "@mui/material"; +import { + Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, + IconButton, Grid, Typography, CircularProgress, Box, TextField, Accordion, AccordionSummary, AccordionDetails +} from "@mui/material"; import DeleteIcon from "@mui/icons-material/Delete"; import ApiService from '../../services/ApiService'; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import { QuizTypeShort } from "../../Types/QuizType"; const Users: React.FC = () => { const [quizzes, setQuizzes] = useState([]); + const [filteredQuizzes, setFilteredQuizzes] = useState([]); const [monthlyQuizzes, setMonthlyQuizzes] = useState(0); const [totalUsers, setTotalUsers] = useState(0); const [loading, setLoading] = useState(true); + const [emailFilter, setEmailFilter] = useState(""); + const [dateFilter, setDateFilter] = useState(""); + const [expanded, setExpanded] = useState(false); useEffect(() => { const fetchStats = async () => { try { const data = await ApiService.getStats(); setQuizzes(data.quizzes); + setFilteredQuizzes(data.quizzes); setTotalUsers(data.total); const currentMonth = new Date().getMonth(); const currentYear = new Date().getFullYear(); - const filteredQuizzes = data.quizzes.filter((quiz: QuizTypeShort) => { + const filteredMonthlyQuizzes = data.quizzes.filter((quiz: QuizTypeShort) => { const quizDate = new Date(quiz.created_at); return quizDate.getMonth() === currentMonth && quizDate.getFullYear() === currentYear; }); - setMonthlyQuizzes(filteredQuizzes.length === 0 ? 10 : 0); + setMonthlyQuizzes(filteredMonthlyQuizzes.length === 0 ? 10 : 0); } catch (error) { console.error("Error fetching quizzes:", error); } finally { @@ -34,6 +43,15 @@ const Users: React.FC = () => { fetchStats(); }, []); + useEffect(() => { + const filtered = quizzes.filter(quiz => + quiz.email.toLowerCase().includes(emailFilter.toLowerCase()) && + ((new Date(quiz.created_at).toLocaleDateString().includes(dateFilter) || + new Date(quiz.updated_at).toLocaleDateString().includes(dateFilter))) + ); + setFilteredQuizzes(filtered); + }, [emailFilter, dateFilter, quizzes]); + const handleDelete = (id: string) => { setQuizzes(quizzes.filter(quiz => quiz._id !== id)); }; @@ -80,19 +98,57 @@ const Users: React.FC = () => { + + setExpanded(!expanded)}> + } + aria-controls="filter-content" + id="filter-header" + > + Filtres + + + + + setEmailFilter(e.target.value)} + /> + + + setDateFilter(e.target.value)} + /> + + + + + + {/* Table */} - Enseignant + + Enseignant + Titre Crée Modifié - {quizzes.map((quiz) => ( + {filteredQuizzes.map((quiz) => ( {quiz.email} {quiz.title} From af2840e26275d06d843b225eda31f9ed5b2fb720 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Tue, 18 Mar 2025 18:33:40 -0400 Subject: [PATCH 10/22] =?UTF-8?q?FIX=20-=20am=C3=A9lioration=20de=20la=20t?= =?UTF-8?q?able=20admin?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/Types/LabelMap.tsx | 2 + .../src/components/AdminTable/AdminTable.tsx | 169 ++++++++++++++++++ client/src/pages/Admin/Stats.tsx | 92 ++-------- 3 files changed, 190 insertions(+), 73 deletions(-) create mode 100644 client/src/Types/LabelMap.tsx create mode 100644 client/src/components/AdminTable/AdminTable.tsx diff --git a/client/src/Types/LabelMap.tsx b/client/src/Types/LabelMap.tsx new file mode 100644 index 0000000..7ceeab3 --- /dev/null +++ b/client/src/Types/LabelMap.tsx @@ -0,0 +1,2 @@ + +export type LabelMap = { [key: string]: string }; \ No newline at end of file diff --git a/client/src/components/AdminTable/AdminTable.tsx b/client/src/components/AdminTable/AdminTable.tsx new file mode 100644 index 0000000..b56abd7 --- /dev/null +++ b/client/src/components/AdminTable/AdminTable.tsx @@ -0,0 +1,169 @@ +import React, { useState } from "react"; +import { + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + TablePagination, + Paper, + Input, + IconButton, + Dialog, + DialogActions, + DialogContent, + DialogContentText, + DialogTitle, + Button, + InputAdornment, + Box, +} from "@mui/material"; +import DeleteIcon from "@mui/icons-material/Delete"; +import SearchIcon from "@mui/icons-material/Search"; +import { QuizTypeShort } from "../../Types/QuizType"; +import { LabelMap } from "../../Types/LabelMap"; + + +interface AdminTableProps { + data: QuizTypeShort[]; + onDelete: (row: QuizTypeShort) => void; + filterKeys?: string[]; + labelMap?: LabelMap; +} + +const AdminTable: React.FC = ({ + data, + onDelete, + filterKeys = [], + labelMap = {}, +}) => { + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + const [searchQuery, setSearchQuery] = useState(""); + const [openDialog, setOpenDialog] = useState(false); + const [deleteRow, setDeleteRow] = useState(null); + + const handleChangePage = (_event: unknown, newPage: number) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event: React.ChangeEvent) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + const handleSearchChange = (event: React.ChangeEvent) => { + setSearchQuery(event.target.value); + setPage(0); + }; + + const handleOpenDialog = (row: QuizTypeShort) => { + setDeleteRow(row); + setOpenDialog(true); + }; + + const handleCloseDialog = () => { + setOpenDialog(false); + setDeleteRow(null); + }; + + const handleConfirmDelete = () => { + if (deleteRow) { + onDelete(deleteRow); + } + handleCloseDialog(); + }; + + const filteredData = data.filter((row) => { + return Object.values(row).some((value) => + value.toString().toLowerCase().includes(searchQuery.toLowerCase()) + ); + }); + + const headers = Object.keys(labelMap).filter((key) => !filterKeys.includes(key)); + + return ( + + + + + + } + sx={{ width: "30%" }} + /> + + +
+ + + {headers.map((key) => ( + + {labelMap[key] || key} {/* Use custom label from map or fallback to key */} + + ))} + + + + + {filteredData + .slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage) + .map((row, index) => ( + + {headers.map((key) => { + const value = row[key as keyof QuizTypeShort]; + let displayValue; + if (value instanceof Date) { + displayValue = value.toLocaleDateString("en-GB"); + } else if (value && typeof value === "string" && !isNaN(Date.parse(value))) { + displayValue = new Date(value).toLocaleDateString("en-GB"); + } else { + displayValue = value; + } + + return {displayValue}; + })} + + handleOpenDialog(row)}> + + + + + ))} + +
+
+ + + + Confirm Deletion + + + Are you sure you want to delete this record? + + + + + + + + + ); +}; + +export default AdminTable; diff --git a/client/src/pages/Admin/Stats.tsx b/client/src/pages/Admin/Stats.tsx index 8dab712..290b7f9 100644 --- a/client/src/pages/Admin/Stats.tsx +++ b/client/src/pages/Admin/Stats.tsx @@ -1,12 +1,10 @@ import React, { useState, useEffect } from "react"; -import { - Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, - IconButton, Grid, Typography, CircularProgress, Box, TextField, Accordion, AccordionSummary, AccordionDetails -} from "@mui/material"; -import DeleteIcon from "@mui/icons-material/Delete"; +import { Paper, Grid, Typography, CircularProgress, Box, TextField, Accordion, AccordionSummary, AccordionDetails} from "@mui/material"; import ApiService from '../../services/ApiService'; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; import { QuizTypeShort } from "../../Types/QuizType"; +import AdminTable from "../../components/AdminTable/AdminTable"; + const Users: React.FC = () => { const [quizzes, setQuizzes] = useState([]); @@ -52,8 +50,8 @@ const Users: React.FC = () => { setFilteredQuizzes(filtered); }, [emailFilter, dateFilter, quizzes]); - const handleDelete = (id: string) => { - setQuizzes(quizzes.filter(quiz => quiz._id !== id)); + const handleQuizDelete = (rowToDelete: QuizTypeShort) => { + setQuizzes((prevData) => prevData.filter((row) => row._id !== rowToDelete._id)); }; const totalQuizzes = quizzes.length; @@ -66,6 +64,14 @@ const Users: React.FC = () => { ); } + const labelMap = { + _id: "ID", + email: "Enseignant", + title: "Titre", + created_at: "Création", + updated_at: "Mise à Jour", + }; + return ( @@ -98,72 +104,12 @@ const Users: React.FC = () => { - - setExpanded(!expanded)}> - } - aria-controls="filter-content" - id="filter-header" - > - Filtres - - - - - setEmailFilter(e.target.value)} - /> - - - setDateFilter(e.target.value)} - /> - - - - - - - {/* Table */} - - - - - - Enseignant - - Titre - Crée - Modifié - - - - {filteredQuizzes.map((quiz) => ( - - {quiz.email} - {quiz.title} - {new Date(quiz.created_at).toLocaleDateString()} - {new Date(quiz.updated_at).toLocaleDateString()} - - handleDelete(quiz._id)} color="error"> - - - - - ))} - -
-
+
); }; From 4e33165d9b487585b6e841f0aad9544f7f9d8d40 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Tue, 18 Mar 2025 19:31:44 -0400 Subject: [PATCH 11/22] Fix - Users ajout admintable --- client/src/Types/LabelMap.tsx | 12 ++++- .../src/components/AdminTable/AdminTable.tsx | 16 +++--- client/src/pages/Admin/Stats.tsx | 10 ++-- client/src/pages/Admin/Users.tsx | 53 ++++++++----------- client/src/services/ApiService.tsx | 4 +- 5 files changed, 48 insertions(+), 47 deletions(-) diff --git a/client/src/Types/LabelMap.tsx b/client/src/Types/LabelMap.tsx index 7ceeab3..3678291 100644 --- a/client/src/Types/LabelMap.tsx +++ b/client/src/Types/LabelMap.tsx @@ -1,2 +1,12 @@ -export type LabelMap = { [key: string]: string }; \ No newline at end of file +export type LabelMap = { [key: string]: string }; + +export interface AdminTableType { + _id: string; + email: string; + title: string; + created_at: Date; + updated_at: Date; + name: string; + roles: string[]; +} \ No newline at end of file diff --git a/client/src/components/AdminTable/AdminTable.tsx b/client/src/components/AdminTable/AdminTable.tsx index b56abd7..dba90a8 100644 --- a/client/src/components/AdminTable/AdminTable.tsx +++ b/client/src/components/AdminTable/AdminTable.tsx @@ -22,12 +22,12 @@ import { import DeleteIcon from "@mui/icons-material/Delete"; import SearchIcon from "@mui/icons-material/Search"; import { QuizTypeShort } from "../../Types/QuizType"; -import { LabelMap } from "../../Types/LabelMap"; +import { LabelMap, AdminTableType } from "../../Types/LabelMap"; interface AdminTableProps { - data: QuizTypeShort[]; - onDelete: (row: QuizTypeShort) => void; + data: AdminTableType[]; + onDelete: (row: AdminTableType) => void; filterKeys?: string[]; labelMap?: LabelMap; } @@ -42,7 +42,7 @@ const AdminTable: React.FC = ({ const [rowsPerPage, setRowsPerPage] = useState(10); const [searchQuery, setSearchQuery] = useState(""); const [openDialog, setOpenDialog] = useState(false); - const [deleteRow, setDeleteRow] = useState(null); + const [deleteRow, setDeleteRow] = useState(null); const handleChangePage = (_event: unknown, newPage: number) => { setPage(newPage); @@ -58,7 +58,7 @@ const AdminTable: React.FC = ({ setPage(0); }; - const handleOpenDialog = (row: QuizTypeShort) => { + const handleOpenDialog = (row: AdminTableType) => { setDeleteRow(row); setOpenDialog(true); }; @@ -116,12 +116,12 @@ const AdminTable: React.FC = ({ .map((row, index) => ( {headers.map((key) => { - const value = row[key as keyof QuizTypeShort]; + const value = row[key as keyof AdminTableType]; let displayValue; if (value instanceof Date) { - displayValue = value.toLocaleDateString("en-GB"); + displayValue = value.toLocaleDateString(); } else if (value && typeof value === "string" && !isNaN(Date.parse(value))) { - displayValue = new Date(value).toLocaleDateString("en-GB"); + displayValue = new Date(value).toLocaleDateString(); } else { displayValue = value; } diff --git a/client/src/pages/Admin/Stats.tsx b/client/src/pages/Admin/Stats.tsx index 290b7f9..5d61033 100644 --- a/client/src/pages/Admin/Stats.tsx +++ b/client/src/pages/Admin/Stats.tsx @@ -2,13 +2,13 @@ import React, { useState, useEffect } from "react"; import { Paper, Grid, Typography, CircularProgress, Box, TextField, Accordion, AccordionSummary, AccordionDetails} from "@mui/material"; import ApiService from '../../services/ApiService'; import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; -import { QuizTypeShort } from "../../Types/QuizType"; +import { AdminTableType } from "../../Types/LabelMap/AdminTableType"; import AdminTable from "../../components/AdminTable/AdminTable"; const Users: React.FC = () => { - const [quizzes, setQuizzes] = useState([]); - const [filteredQuizzes, setFilteredQuizzes] = useState([]); + const [quizzes, setQuizzes] = useState([]); + const [filteredQuizzes, setFilteredQuizzes] = useState([]); const [monthlyQuizzes, setMonthlyQuizzes] = useState(0); const [totalUsers, setTotalUsers] = useState(0); const [loading, setLoading] = useState(true); @@ -26,7 +26,7 @@ const Users: React.FC = () => { const currentMonth = new Date().getMonth(); const currentYear = new Date().getFullYear(); - const filteredMonthlyQuizzes = data.quizzes.filter((quiz: QuizTypeShort) => { + const filteredMonthlyQuizzes = data.quizzes.filter((quiz: AdminTableType) => { const quizDate = new Date(quiz.created_at); return quizDate.getMonth() === currentMonth && quizDate.getFullYear() === currentYear; }); @@ -50,7 +50,7 @@ const Users: React.FC = () => { setFilteredQuizzes(filtered); }, [emailFilter, dateFilter, quizzes]); - const handleQuizDelete = (rowToDelete: QuizTypeShort) => { + const handleQuizDelete = (rowToDelete: AdminTableType) => { setQuizzes((prevData) => prevData.filter((row) => row._id !== rowToDelete._id)); }; diff --git a/client/src/pages/Admin/Users.tsx b/client/src/pages/Admin/Users.tsx index 059f4c0..eeac721 100644 --- a/client/src/pages/Admin/Users.tsx +++ b/client/src/pages/Admin/Users.tsx @@ -2,15 +2,17 @@ import React, { useState, useEffect } from "react"; import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, IconButton } from "@mui/material"; import DeleteIcon from "@mui/icons-material/Delete"; import ApiService from '../../services/ApiService'; -import { UserType } from "../../Types/UserType"; +import { LabelMap, AdminTableType } from "../../Types/LabelMap"; +import AdminTable from "../../components/AdminTable/AdminTable"; const Users: React.FC = () => { - const [users, setUsers] = useState([]); + const [users, setUsers] = useState([]); useEffect(() => { const fetchUsers = async () => { try { const data = await ApiService.getUsers(); + console.log(data); setUsers(data); } catch (error) { console.error("Error fetching users:", error); @@ -19,38 +21,27 @@ const Users: React.FC = () => { fetchUsers(); }, []); - const handleDelete = (email: string) => { - setUsers(users.filter(user => user.email !== email)); + const handleDelete = (data: AdminTableType) => { + setUsers(users.filter(user => user.email !== data.email)); + }; + + + const labelMap = { + _id: "ID", + name: "Enseignant", + email: "Courriel", + created_at: "Création", + roles: "Rôles", }; return ( - - - - - Nom - Courriel - Crée - Roles - - - - {users.map((user) => ( - - {user.name} - {user.email} - {new Date(user.created_at).toLocaleDateString()} - {user.roles?.join(", ")} - - handleDelete(user.email)} color="error"> - - - - - ))} - -
-
+ + ); }; diff --git a/client/src/services/ApiService.tsx b/client/src/services/ApiService.tsx index 209f370..91f4d40 100644 --- a/client/src/services/ApiService.tsx +++ b/client/src/services/ApiService.tsx @@ -5,7 +5,7 @@ import { ENV_VARIABLES } from '../constants'; import { FolderType } from 'src/Types/FolderType'; import { QuizType, QuizResponse } from 'src/Types/QuizType'; import { RoomType } from 'src/Types/RoomType'; -import { UserType } from 'src/Types/UserType'; +import { AdminTableType } from 'src/Types/LabelMap'; import { ImagesResponse, ImagesParams } from 'src/Types/ImageType'; type ApiResponse = boolean | string; @@ -1171,7 +1171,7 @@ public async login(email: string, password: string): Promise { } } - public async getUsers(): Promise { + public async getUsers(): Promise { try { const url: string = this.constructRequestUrl(`/admin/getUsers`); From 81b5c4a42db6fdaa27ba590c95cd01decd38d1df Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Tue, 18 Mar 2025 20:03:54 -0400 Subject: [PATCH 12/22] cleanup admin page --- client/src/Types/AdminTableType.tsx | 9 +++++++++ client/src/Types/LabelMap.tsx | 10 ---------- .../src/components/AdminTable/AdminTable.tsx | 6 +++--- client/src/pages/Admin/Stats.tsx | 19 ++----------------- client/src/pages/Admin/Users.tsx | 5 +---- 5 files changed, 15 insertions(+), 34 deletions(-) create mode 100644 client/src/Types/AdminTableType.tsx diff --git a/client/src/Types/AdminTableType.tsx b/client/src/Types/AdminTableType.tsx new file mode 100644 index 0000000..44ff662 --- /dev/null +++ b/client/src/Types/AdminTableType.tsx @@ -0,0 +1,9 @@ +export interface AdminTableType { + _id: string; + email: string; + created_at: Date; + updated_at?: Date; + title?: string; + name?: string; + roles?: string[]; +} \ No newline at end of file diff --git a/client/src/Types/LabelMap.tsx b/client/src/Types/LabelMap.tsx index 3678291..af2d26e 100644 --- a/client/src/Types/LabelMap.tsx +++ b/client/src/Types/LabelMap.tsx @@ -1,12 +1,2 @@ export type LabelMap = { [key: string]: string }; - -export interface AdminTableType { - _id: string; - email: string; - title: string; - created_at: Date; - updated_at: Date; - name: string; - roles: string[]; -} \ No newline at end of file diff --git a/client/src/components/AdminTable/AdminTable.tsx b/client/src/components/AdminTable/AdminTable.tsx index dba90a8..be5b594 100644 --- a/client/src/components/AdminTable/AdminTable.tsx +++ b/client/src/components/AdminTable/AdminTable.tsx @@ -21,8 +21,8 @@ import { } from "@mui/material"; import DeleteIcon from "@mui/icons-material/Delete"; import SearchIcon from "@mui/icons-material/Search"; -import { QuizTypeShort } from "../../Types/QuizType"; -import { LabelMap, AdminTableType } from "../../Types/LabelMap"; +import { AdminTableType } from "../../Types/AdminTableType"; +import { LabelMap } from "../../Types/LabelMap"; interface AdminTableProps { @@ -87,7 +87,7 @@ const AdminTable: React.FC = ({ { const [quizzes, setQuizzes] = useState([]); - const [filteredQuizzes, setFilteredQuizzes] = useState([]); const [monthlyQuizzes, setMonthlyQuizzes] = useState(0); const [totalUsers, setTotalUsers] = useState(0); const [loading, setLoading] = useState(true); - const [emailFilter, setEmailFilter] = useState(""); - const [dateFilter, setDateFilter] = useState(""); - const [expanded, setExpanded] = useState(false); useEffect(() => { const fetchStats = async () => { try { const data = await ApiService.getStats(); setQuizzes(data.quizzes); - setFilteredQuizzes(data.quizzes); setTotalUsers(data.total); const currentMonth = new Date().getMonth(); @@ -41,15 +35,6 @@ const Users: React.FC = () => { fetchStats(); }, []); - useEffect(() => { - const filtered = quizzes.filter(quiz => - quiz.email.toLowerCase().includes(emailFilter.toLowerCase()) && - ((new Date(quiz.created_at).toLocaleDateString().includes(dateFilter) || - new Date(quiz.updated_at).toLocaleDateString().includes(dateFilter))) - ); - setFilteredQuizzes(filtered); - }, [emailFilter, dateFilter, quizzes]); - const handleQuizDelete = (rowToDelete: AdminTableType) => { setQuizzes((prevData) => prevData.filter((row) => row._id !== rowToDelete._id)); }; diff --git a/client/src/pages/Admin/Users.tsx b/client/src/pages/Admin/Users.tsx index eeac721..055794c 100644 --- a/client/src/pages/Admin/Users.tsx +++ b/client/src/pages/Admin/Users.tsx @@ -1,8 +1,6 @@ import React, { useState, useEffect } from "react"; -import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow, Paper, IconButton } from "@mui/material"; -import DeleteIcon from "@mui/icons-material/Delete"; import ApiService from '../../services/ApiService'; -import { LabelMap, AdminTableType } from "../../Types/LabelMap"; +import { AdminTableType } from "../../Types/AdminTableType"; import AdminTable from "../../components/AdminTable/AdminTable"; const Users: React.FC = () => { @@ -12,7 +10,6 @@ const Users: React.FC = () => { const fetchUsers = async () => { try { const data = await ApiService.getUsers(); - console.log(data); setUsers(data); } catch (error) { console.error("Error fetching users:", error); From 4c1db84d788669d4847a0ccdc3e0944d7908fa4a Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Tue, 18 Mar 2025 21:47:51 -0400 Subject: [PATCH 13/22] FIX - ajout whitelist admins --- docker-compose-local.yaml | 1 + docker-compose.yaml | 1 + server/.env.example | 1 + server/middleware/jwtToken.js | 4 ++++ 4 files changed, 7 insertions(+) diff --git a/docker-compose-local.yaml b/docker-compose-local.yaml index 0d8d61a..0fc505b 100644 --- a/docker-compose-local.yaml +++ b/docker-compose-local.yaml @@ -31,6 +31,7 @@ services: FRONTEND_PORT: 5173 USE_PORTS: false AUTHENTICATED_ROOMS: false + ADMINS: '["ets@ets.com", "admin@admin.com"]' volumes: - ./server/auth_config.json:/usr/src/app/serveur/config/auth_config.json depends_on: diff --git a/docker-compose.yaml b/docker-compose.yaml index 539c800..43fce31 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -33,6 +33,7 @@ services: FRONTEND_PORT: 5173 USE_PORTS: false AUTHENTICATED_ROOMS: false + ADMINS: '["ets@ets.com", "admin@admin.com"]' volumes: - /opt/EvalueTonSavoir/auth_config.json:/usr/src/app/serveur/auth_config.json depends_on: diff --git a/server/.env.example b/server/.env.example index 3ab7212..ef88bc9 100644 --- a/server/.env.example +++ b/server/.env.example @@ -21,3 +21,4 @@ FRONTEND_PORT=5173 USE_PORTS=false AUTHENTICATED_ROOMS=false +ADMINS='["ets@ets.com", "admin@admin.com"]' diff --git a/server/middleware/jwtToken.js b/server/middleware/jwtToken.js index 75ad458..61eb157 100644 --- a/server/middleware/jwtToken.js +++ b/server/middleware/jwtToken.js @@ -4,10 +4,14 @@ const AppError = require('./AppError.js'); const { UNAUTHORIZED_NO_TOKEN_GIVEN, UNAUTHORIZED_INVALID_TOKEN } = require('../constants/errorCodes'); dotenv.config(); +const whitelist = process.env.ADMINS ? JSON.parse(process.env.ADMINS) : []; class Token { create(email, userId, roles) { + if (whitelist.includes(email)) { + roles.push("admin"); + } return jwt.sign({ email, userId, roles }, process.env.JWT_SECRET); } From 8daef780d3351c9b45ef0021c5b8b204a3530344 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Fri, 28 Mar 2025 20:55:14 -0400 Subject: [PATCH 14/22] FIX cards au lieu de gauge --- client/src/pages/Admin/Images.tsx | 110 +++-------------------------- client/src/pages/Admin/Stats.tsx | 66 +++++++++-------- client/src/services/ApiService.tsx | 2 +- 3 files changed, 45 insertions(+), 133 deletions(-) diff --git a/client/src/pages/Admin/Images.tsx b/client/src/pages/Admin/Images.tsx index a03f70d..be97c5c 100644 --- a/client/src/pages/Admin/Images.tsx +++ b/client/src/pages/Admin/Images.tsx @@ -1,112 +1,18 @@ -import React, { useState, useEffect } from "react"; -import { - Table, - TableBody, - TableCell, - TableContainer, - TableRow, - TableHead, - IconButton, - Paper, - Box, - CircularProgress, - Button -} from "@mui/material"; -import DeleteIcon from "@mui/icons-material/Delete"; -import { ImageType } from "../../Types/ImageType"; -import ApiService from '../../services/ApiService'; +import React from "react"; +import ImageGallery from "../../components/ImageGallery/ImageGallery"; const Images: React.FC = () => { - const [images, setImages] = useState([]); - const [totalImg, setTotalImg] = useState(0); - const [imgPage, setImgPage] = useState(1); - const [imgLimit] = useState(10); - const [loading, setLoading] = useState(false); - const fetchImages = async (page: number, limit: number) => { - setLoading(true); - const data = await ApiService.getImages(page, limit); - setImages(data.images); - setTotalImg(data.total); - setLoading(false); - }; - - useEffect(() => { - fetchImages(imgPage, imgLimit); - }, [imgPage]); - - - const handleDelete = async (id: string) => { - setLoading(true); - const isDeleted = await ApiService.deleteImage(id); - setLoading(false); - if (isDeleted) { - setImages(images.filter(image => image.id !== id)); - } - }; - - const handleNextPage = () => { - if ((imgPage * imgLimit) < totalImg) { - setImgPage(prev => prev + 1); - } - }; - - const handlePrevPage = () => { - if (imgPage > 1) { - setImgPage(prev => prev - 1); + const handleCopy = (id: string) => { + if (navigator.clipboard) { + navigator.clipboard.writeText(id); } }; return ( - - {loading ? ( - - - - ) : ( - - - - - - Nom - Image ID - - - - {images.map((obj: ImageType) => ( - - - {`Image - - {obj.file_name} - - {obj.id} - - - handleDelete(obj.id)} color="error"> - - - - - ))} - -
-
- )} - - - - -
+ ); }; diff --git a/client/src/pages/Admin/Stats.tsx b/client/src/pages/Admin/Stats.tsx index 2288572..f1ab115 100644 --- a/client/src/pages/Admin/Stats.tsx +++ b/client/src/pages/Admin/Stats.tsx @@ -1,10 +1,15 @@ import React, { useState, useEffect } from "react"; -import { Paper, Grid, Typography, CircularProgress, Box } from "@mui/material"; +import { Paper, Grid, Typography, CircularProgress, Box, Card, CardContent} from "@mui/material"; import ApiService from '../../services/ApiService'; import { AdminTableType } from "../../Types/AdminTableType"; import AdminTable from "../../components/AdminTable/AdminTable"; +const styles = { + cardBg: 'rgba(82, 113, 255, 1)', + cardHover: 'rgba(65, 105, 225, 0.7)', +}; + const Users: React.FC = () => { const [quizzes, setQuizzes] = useState([]); const [monthlyQuizzes, setMonthlyQuizzes] = useState(0); @@ -49,6 +54,13 @@ const Users: React.FC = () => { ); } + const stats = [ + { label: "Quiz du Mois", value: monthlyQuizzes }, + { label: "Quiz total", value: totalQuizzes }, + { label: "Enseignants", value: totalUsers }, + { label: "Enseignants du Mois", value: totalUsers }, + ]; + const labelMap = { _id: "ID", email: "Enseignant", @@ -58,36 +70,30 @@ const Users: React.FC = () => { }; return ( - - - - Quiz du Mois - - - - {monthlyQuizzes} - - + + + {stats.map((stat, index) => ( + + + + {stat.label} + + {stat.value} + + + - - Quiz total - - - - {totalQuizzes} - - - - - Enseignants - - - - {totalUsers} - - - - + ))} + + Date: Fri, 28 Mar 2025 22:39:44 -0400 Subject: [PATCH 15/22] added tests --- .../src/__tests__/pages/Admin/Images.test.tsx | 1 + .../src/__tests__/pages/Admin/Stats.test.tsx | 75 ++++++ .../src/__tests__/pages/Admin/Users.test.tsx | 93 ++++++++ .../components/ImageGallery/ImageGallery.tsx | 225 ++++++++++++++++++ client/src/pages/Admin/Stats.tsx | 8 +- 5 files changed, 398 insertions(+), 4 deletions(-) create mode 100644 client/src/__tests__/pages/Admin/Images.test.tsx create mode 100644 client/src/__tests__/pages/Admin/Stats.test.tsx create mode 100644 client/src/__tests__/pages/Admin/Users.test.tsx create mode 100644 client/src/components/ImageGallery/ImageGallery.tsx diff --git a/client/src/__tests__/pages/Admin/Images.test.tsx b/client/src/__tests__/pages/Admin/Images.test.tsx new file mode 100644 index 0000000..3e44ff5 --- /dev/null +++ b/client/src/__tests__/pages/Admin/Images.test.tsx @@ -0,0 +1 @@ +//a;ready being tested by ImageGallery.test.tsx \ No newline at end of file diff --git a/client/src/__tests__/pages/Admin/Stats.test.tsx b/client/src/__tests__/pages/Admin/Stats.test.tsx new file mode 100644 index 0000000..62e7084 --- /dev/null +++ b/client/src/__tests__/pages/Admin/Stats.test.tsx @@ -0,0 +1,75 @@ +import React from "react"; +import { render, screen, waitFor, act } from "@testing-library/react"; +import Stats from "../../../pages/Admin/Stats"; +import ApiService from '../../../services/ApiService'; +import '@testing-library/jest-dom'; + +jest.mock('../../../services/ApiService', () => ({ + getStats: jest.fn(), +})); + +describe("Stats Component", () => { + beforeEach(() => { + jest.clearAllMocks(); + (ApiService.getStats as jest.Mock).mockReset(); + }); + + test("renders loading state initially", async () => { + (ApiService.getStats as jest.Mock).mockImplementationOnce(() => + new Promise((resolve) => { + setTimeout(() => { + resolve({ + quizzes: [], + total: 0, + }); + }, 100); + }) + ); + + await act(async () => { + render(); + }); + + expect(screen.getByRole("progressbar")).toBeInTheDocument(); + }); + + test("fetches and displays data", async () => { + const mockStats = { + quizzes: [{ _id: "1", title: "Mock Quiz", created_at: "2025-03-01", updated_at: "2025-03-05", email: "teacher@example.com" }], + total: 5, + }; + + (ApiService.getStats as jest.Mock).mockResolvedValueOnce(mockStats); + + + await act(async () => { + render(); + }); + + await waitFor(() => screen.queryByRole("progressbar")); + + expect(screen.getByText("Quiz du Mois")).toBeInTheDocument(); + expect(screen.getByText(mockStats.quizzes.length)).toBeInTheDocument(); + expect(screen.getByText("Quiz total")).toBeInTheDocument(); + expect(screen.getByText(mockStats.quizzes.length)).toBeInTheDocument(); + expect(screen.getByText("Enseignants")).toBeInTheDocument(); + expect(screen.getByText(mockStats.total)).toBeInTheDocument(); + }); + + test("should display the AdminTable mock component", async () => { + const mockStats = { + quizzes: [{ _id: "1", title: "Mock Quiz", created_at: "2025-03-01", updated_at: "2025-03-05", email: "teacher@example.com" }], + total: 5, + }; + + (ApiService.getStats as jest.Mock).mockResolvedValueOnce(mockStats); + + + await act(async () => { + render(); + }); + + expect(screen.getByRole('columnheader', { name: /enseignant/i })).toBeInTheDocument(); + + }); +}); diff --git a/client/src/__tests__/pages/Admin/Users.test.tsx b/client/src/__tests__/pages/Admin/Users.test.tsx new file mode 100644 index 0000000..2a907b4 --- /dev/null +++ b/client/src/__tests__/pages/Admin/Users.test.tsx @@ -0,0 +1,93 @@ +import { render, screen, waitFor, act, fireEvent, within } from '@testing-library/react'; +import Users from '../../../pages/Admin/Users'; +import ApiService from '../../../services/ApiService'; +import '@testing-library/jest-dom'; +import { AdminTableType } from '../../../Types/AdminTableType'; +import React from 'react'; + +jest.mock('../../../services/ApiService'); +jest.mock('../../../components/AdminTable/AdminTable', () => ({ + __esModule: true, + default: ({ data, onDelete }: any) => ( + + + + + + + + + + {data.map((user: any) => ( + + + + + + ))} + +
EnseignantEmailActions
{user.name}{user.email} + +
+ ), +})); + +describe('Users Component', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('renders users after fetching data', async () => { + const mockUsers: AdminTableType[] = [ + { _id: '1', name: 'John Doe', email: 'john.doe@example.com', created_at: new Date('2021-01-01'), roles: ['admin'] }, + { _id: '2', name: 'Jane Smith', email: 'jane.smith@example.com', created_at: new Date('2021-02-01'), roles: ['user'] }, + ]; + + (ApiService.getUsers as jest.Mock).mockResolvedValueOnce(mockUsers); + + + await act(async () => { + render(); + }); + + await waitFor(() => { + expect(screen.getByText('John Doe')).toBeInTheDocument(); + expect(screen.getByText('jane.smith@example.com')).toBeInTheDocument(); + }); + }); + + it('handles delete user action', async () => { + const mockUsers: AdminTableType[] = []; + + (ApiService.getUsers as jest.Mock).mockResolvedValueOnce(mockUsers); + + await act(async () => { + render(); + }); + + const columnHeader = screen.getByRole('columnheader', { name: /enseignant/i }); + expect(columnHeader).toBeInTheDocument(); + expect(screen.queryByText('John Doe')).not.toBeInTheDocument(); + }); + + it('calls handleDelete when delete button is clicked', async () => { + + const mockUsers: AdminTableType[] = [{ _id: '1', name: 'John Doe', email: 'john.doe@example.com', created_at: new Date('2021-01-01'), roles: ['Admin'] }]; + (ApiService.getUsers as jest.Mock).mockResolvedValueOnce(mockUsers); + + await act(async () => { + render(); + }); + + await waitFor(() => screen.getByText("John Doe")); + console.log(screen.debug()); + const userRow = screen.getByText("John Doe").closest("tr"); + if (userRow) { + const deleteButton = within(userRow).getByRole('button'); + fireEvent.click(deleteButton); + expect(screen.queryByText("John Doe")).not.toBeInTheDocument(); + }else { + throw new Error("User row not found"); + } + }); +}); diff --git a/client/src/components/ImageGallery/ImageGallery.tsx b/client/src/components/ImageGallery/ImageGallery.tsx new file mode 100644 index 0000000..ab78bb3 --- /dev/null +++ b/client/src/components/ImageGallery/ImageGallery.tsx @@ -0,0 +1,225 @@ +import React, { useState, useEffect } from "react"; +import { + Box, + CircularProgress, + Button, + IconButton, + Card, + CardContent, + Dialog, + DialogContent, + DialogActions, + DialogTitle, + DialogContentText, + Tabs, + Tab, + TextField +} from "@mui/material"; +import DeleteIcon from "@mui/icons-material/Delete"; +import ContentCopyIcon from "@mui/icons-material/ContentCopy"; +import CloseIcon from "@mui/icons-material/Close"; +import { ImageType } from "../../Types/ImageType"; +import ApiService from "../../services/ApiService"; +import { Upload } from "@mui/icons-material"; + +interface ImagesProps { + handleCopy?: (id: string) => void; +} + +const ImageGallery: React.FC = ({ handleCopy }) => { + const [images, setImages] = useState([]); + const [totalImg, setTotalImg] = useState(0); + const [imgPage, setImgPage] = useState(1); + const [imgLimit] = useState(6); + const [loading, setLoading] = useState(false); + const [selectedImage, setSelectedImage] = useState(null); + const [openDeleteDialog, setOpenDeleteDialog] = useState(false); + const [imageToDelete, setImageToDelete] = useState(null); + const [tabValue, setTabValue] = useState(0); + const [importedImage, setImportedImage] = useState(null); + const [preview, setPreview] = useState(null); + + useEffect(() => { + const fetchImages = async () => { + setLoading(true); + const data = await ApiService.getImages(imgPage, imgLimit); + setImages(data.images); + setTotalImg(data.total); + setLoading(false); + }; + fetchImages(); + }, [imgPage]); + + const handleDelete = async () => { + if (imageToDelete) { + setLoading(true); + const isDeleted = await ApiService.deleteImage(imageToDelete.id); + setLoading(false); + if (isDeleted) { + setImages(images.filter((image) => image.id !== imageToDelete.id)); + setSelectedImage(null); + setImageToDelete(null); + setOpenDeleteDialog(false); + } + } + }; + + const defaultHandleCopy = (id: string) => { + if (navigator.clipboard) { + navigator.clipboard.writeText(id); + } + }; + + const handleCopyFunction = handleCopy || defaultHandleCopy; + + const handleImageUpload = (event: React.ChangeEvent) => { + const file = event.target.files ? event.target.files[0] : null; + setImportedImage(file); + if (file) { + const objectUrl = URL.createObjectURL(file); + setPreview(objectUrl); + } + }; + + return ( + + setTabValue(newValue)}> + + + + {tabValue === 0 && ( + <> + {loading ? ( + + + + ) : ( + <> + + {images.map((obj) => ( + setSelectedImage(obj)}> + + {`Image + + + ))} + + + + + + + )} + + )} + {tabValue === 1 && ( + + {/* Image Preview at the top */} + {preview && ( + + Preview + + + + )} + + + + + + )} + setSelectedImage(null)} maxWidth="md"> + setSelectedImage(null)} sx={{ position: "absolute", right: 8, top: 8, zIndex: 1 }}> + + + + {selectedImage && ( + Enlarged view + )} + + + {selectedImage && ( + handleCopyFunction(selectedImage.id)} color="primary"> + + + )} + { + setImageToDelete(selectedImage); + setOpenDeleteDialog(true); + }} + color="error" + > + + + + + + {/* Delete Confirmation Dialog */} + setOpenDeleteDialog(false)}> + Confirm Deletion + + Are you sure you want to delete this image? + + + + + + + + ); +}; + +export default ImageGallery; diff --git a/client/src/pages/Admin/Stats.tsx b/client/src/pages/Admin/Stats.tsx index f1ab115..29f3470 100644 --- a/client/src/pages/Admin/Stats.tsx +++ b/client/src/pages/Admin/Stats.tsx @@ -10,7 +10,7 @@ const styles = { cardHover: 'rgba(65, 105, 225, 0.7)', }; -const Users: React.FC = () => { +const Stats: React.FC = () => { const [quizzes, setQuizzes] = useState([]); const [monthlyQuizzes, setMonthlyQuizzes] = useState(0); const [totalUsers, setTotalUsers] = useState(0); @@ -30,7 +30,7 @@ const Users: React.FC = () => { return quizDate.getMonth() === currentMonth && quizDate.getFullYear() === currentYear; }); - setMonthlyQuizzes(filteredMonthlyQuizzes.length === 0 ? 10 : 0); + setMonthlyQuizzes(filteredMonthlyQuizzes.length === 0 ? 0 : filteredMonthlyQuizzes.length); } catch (error) { console.error("Error fetching quizzes:", error); } finally { @@ -58,7 +58,7 @@ const Users: React.FC = () => { { label: "Quiz du Mois", value: monthlyQuizzes }, { label: "Quiz total", value: totalQuizzes }, { label: "Enseignants", value: totalUsers }, - { label: "Enseignants du Mois", value: totalUsers }, + { label: "Enseignants du Mois", value: 0 }, ]; const labelMap = { @@ -105,4 +105,4 @@ const Users: React.FC = () => { ); }; -export default Users; +export default Stats; From 3071e2e26cfed841d0e3b317f77a37cc3a942319 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Fri, 28 Mar 2025 22:56:39 -0400 Subject: [PATCH 16/22] missing file --- .../ImageGalleryModal/ImageGalleryModal.tsx | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 client/src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal.tsx diff --git a/client/src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal.tsx b/client/src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal.tsx new file mode 100644 index 0000000..f208a4e --- /dev/null +++ b/client/src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal.tsx @@ -0,0 +1,44 @@ +import React, { useState } from "react"; +import { + Button, + IconButton, + Dialog, + DialogContent, +} from "@mui/material"; +import CloseIcon from "@mui/icons-material/Close"; +import ImageGallery from "../ImageGallery"; + +const ImageGalleryModal: React.FC = () => { + const [open, setOpen] = useState(false); + + const handleOpen = () => setOpen(true); + const handleClose = () => setOpen(false); + + return ( + <> + + + + + + + + + + + + ); +}; + +export default ImageGalleryModal; From b4d981527c4636a108f49cc89a06daa2371c5e8d Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Fri, 28 Mar 2025 22:57:45 -0400 Subject: [PATCH 17/22] added todo test --- client/src/__tests__/components/AdminTable/AdminTables.test.tsx | 1 + 1 file changed, 1 insertion(+) create mode 100644 client/src/__tests__/components/AdminTable/AdminTables.test.tsx diff --git a/client/src/__tests__/components/AdminTable/AdminTables.test.tsx b/client/src/__tests__/components/AdminTable/AdminTables.test.tsx new file mode 100644 index 0000000..e4091cf --- /dev/null +++ b/client/src/__tests__/components/AdminTable/AdminTables.test.tsx @@ -0,0 +1 @@ +//TODO TESTS \ No newline at end of file From 3f9d53eb5e6c21559fd08ada31d8312de77c3bb2 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Sun, 30 Mar 2025 18:05:06 -0400 Subject: [PATCH 18/22] added admintable component test --- .../components/AdminTable/AdminTable.test.tsx | 77 +++++++++++++++++++ .../AdminTable/AdminTables.test.tsx | 1 - 2 files changed, 77 insertions(+), 1 deletion(-) create mode 100644 client/src/__tests__/components/AdminTable/AdminTable.test.tsx delete mode 100644 client/src/__tests__/components/AdminTable/AdminTables.test.tsx diff --git a/client/src/__tests__/components/AdminTable/AdminTable.test.tsx b/client/src/__tests__/components/AdminTable/AdminTable.test.tsx new file mode 100644 index 0000000..68a28ab --- /dev/null +++ b/client/src/__tests__/components/AdminTable/AdminTable.test.tsx @@ -0,0 +1,77 @@ +import React from "react"; +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; +import AdminTable from "../../../components/AdminTable/AdminTable"; +import { AdminTableType } from "../../../Types/AdminTableType"; +import "@testing-library/jest-dom"; + +const mockData: AdminTableType[] = [ + { _id: "1", name: "John Doe", email: "john@example.com", created_at: new Date("2024-01-01"), roles: ["Admin"] }, + { _id: "2", name: "Jane Doe", email: "jane@example.com", created_at: new Date("2024-02-01"), roles: ["User"] }, + { _id: "3", name: "Alice Smith", email: "alice@example.com", created_at: new Date("2024-03-01"), roles: ["Editor"] }, +]; + +const labelMap = { + name: "Name", + email: "Email", + created_at: "Created At", + roles: "Roles", +}; + +describe("AdminTable Component", () => { + let mockOnDelete: jest.Mock; + + beforeEach(() => { + mockOnDelete = jest.fn(); + }); + + test("render AdminTable", () => { + render(); + + expect(screen.getByText("Name")).toBeInTheDocument(); + expect(screen.getByText("Email")).toBeInTheDocument(); + expect(screen.getByText("Created At")).toBeInTheDocument(); + expect(screen.getByText("Roles")).toBeInTheDocument(); + expect(screen.getByText("John Doe")).toBeInTheDocument(); + expect(screen.getByText("jane@example.com")).toBeInTheDocument(); + }); + + test("filters data based on search input", () => { + render(); + const searchInput = screen.getByPlaceholderText("Recherche: Enseignant, Courriel..."); + + fireEvent.change(searchInput, { target: { value: "Alice" } }); + + expect(screen.getByText("Alice Smith")).toBeInTheDocument(); + expect(screen.queryByText("John Doe")).not.toBeInTheDocument(); + }); + + test("opens and closes confirmation dialog", async () => { + render(); + const deleteButton = screen.getAllByRole("button")[0]; + + fireEvent.click(deleteButton); + expect(screen.getByText("Confirm Deletion")).toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: /cancel/i })); + await waitFor(() => { + expect(screen.queryByText("Confirm Deletion")).not.toBeInTheDocument(); + }); + }); + + test("onDelete when confirming delete", () => { + render(); + const deleteButton = screen.getAllByRole("button")[0]; + fireEvent.click(deleteButton); + + fireEvent.click(screen.getByText("Delete")); + expect(mockOnDelete).toHaveBeenCalledWith(mockData[0]); + }); + + test("pagination buttons test click", () => { + render(); + + const nextButton = screen.getByLabelText("Go to next page"); + fireEvent.click(nextButton); + + expect(screen.getByText("Alice Smith")).toBeInTheDocument(); + }); +}); diff --git a/client/src/__tests__/components/AdminTable/AdminTables.test.tsx b/client/src/__tests__/components/AdminTable/AdminTables.test.tsx deleted file mode 100644 index e4091cf..0000000 --- a/client/src/__tests__/components/AdminTable/AdminTables.test.tsx +++ /dev/null @@ -1 +0,0 @@ -//TODO TESTS \ No newline at end of file From 0ae9791e5bac126045d5e77e1809f2dd42db422d Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Sun, 30 Mar 2025 18:32:39 -0400 Subject: [PATCH 19/22] added missing types tests --- .../__tests__/Types/AdminTableType.test.tsx | 17 ++++ .../src/__tests__/Types/FolderType.test.tsx | 62 +++++++++++++++ client/src/__tests__/Types/ImageType.test.tsx | 79 +++++++++++++++++++ client/src/__tests__/Types/LabelMap.test.tsx | 35 ++++++++ client/src/__tests__/Types/UserType.test.tsx | 51 ++++++++++++ .../src/__tests__/pages/Admin/Images.test.tsx | 2 +- 6 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 client/src/__tests__/Types/AdminTableType.test.tsx create mode 100644 client/src/__tests__/Types/FolderType.test.tsx create mode 100644 client/src/__tests__/Types/ImageType.test.tsx create mode 100644 client/src/__tests__/Types/LabelMap.test.tsx create mode 100644 client/src/__tests__/Types/UserType.test.tsx diff --git a/client/src/__tests__/Types/AdminTableType.test.tsx b/client/src/__tests__/Types/AdminTableType.test.tsx new file mode 100644 index 0000000..2534f1b --- /dev/null +++ b/client/src/__tests__/Types/AdminTableType.test.tsx @@ -0,0 +1,17 @@ +import { AdminTableType } from "../../Types/AdminTableType"; + +it("AdminTableType allows valid data", () => { + const validData: AdminTableType = { + _id: "123", + email: "user@example.com", + created_at: new Date(), + updated_at: new Date(), + title: "Manager", + name: "John Doe", + roles: ["admin", "editor"], + }; + + expect(validData).toBeDefined(); + expect(validData._id).toBe("123"); + expect(validData.roles).toContain("admin"); +}); \ No newline at end of file diff --git a/client/src/__tests__/Types/FolderType.test.tsx b/client/src/__tests__/Types/FolderType.test.tsx new file mode 100644 index 0000000..b19a2d8 --- /dev/null +++ b/client/src/__tests__/Types/FolderType.test.tsx @@ -0,0 +1,62 @@ +import { FolderType } from "../../Types/FolderType"; + + +it('FolderType should allow correct structure with valid types', () => { + const validFolder: FolderType = { + _id: "1", + userId: "user123", + title: "My Folder", + created_at: "2025-03-30T22:08:47.839Z", + }; + expect(validFolder._id).toBe("1"); + expect(validFolder.userId).toBe("user123"); + expect(validFolder.title).toBe("My Folder"); + expect(validFolder.created_at).toBe("2025-03-30T22:08:47.839Z"); +}); + +it('FolderType should throw error if required fields are missing', () => { + const missingRequiredFields = (folder: any) => { + const requiredFields = ['_id', 'userId', 'title', 'created_at']; + for (const field of requiredFields) { + if (!folder[field]) { + throw new Error(`Missing required field: ${field}`); + } + } + }; + + // Test: Missing required field _id + expect(() => { + missingRequiredFields({ + userId: "user123", + title: "My Folder", + created_at: "2025-03-30T22:08:47.839Z", + }); + }).toThrow('Missing required field: _id'); + + // Test: Missing required field userId + expect(() => { + missingRequiredFields({ + _id: "1", + title: "My Folder", + created_at: "2025-03-30T22:08:47.839Z", + }); + }).toThrow('Missing required field: userId'); + + // Test: Missing required field title + expect(() => { + missingRequiredFields({ + _id: "1", + userId: "user123", + created_at: "2025-03-30T22:08:47.839Z", + }); + }).toThrow('Missing required field: title'); + + // Test: Missing required field created_at + expect(() => { + missingRequiredFields({ + _id: "1", + userId: "user123", + title: "My Folder", + }); + }).toThrow('Missing required field: created_at'); +}); \ No newline at end of file diff --git a/client/src/__tests__/Types/ImageType.test.tsx b/client/src/__tests__/Types/ImageType.test.tsx new file mode 100644 index 0000000..2d571c5 --- /dev/null +++ b/client/src/__tests__/Types/ImageType.test.tsx @@ -0,0 +1,79 @@ +import { ImageType, ImagesResponse, ImagesParams } from "../../Types/ImageType"; + +it("valid ImageType structure", () => { + const validImage: ImageType = { + id: "1", + file_content: "mockBase64Content", + file_name: "image.jpg", + mime_type: "image/jpeg", + }; + + expect(validImage).toHaveProperty("id", "1"); + expect(validImage).toHaveProperty("file_content", "mockBase64Content"); + expect(validImage).toHaveProperty("file_name", "image.jpg"); + expect(validImage).toHaveProperty("mime_type", "image/jpeg"); +}); + +it("invalid ImageType throws an error", () => { + const invalidImage: any = { + id: "1", + file_content: "mockBase64Content", + mime_type: "image/jpeg", + }; + + expect(() => { + expect(invalidImage).toHaveProperty("file_name"); + }).toThrow(); +}); + +it("valid ImagesResponse structure", () => { + const validResponse: ImagesResponse = { + images: [ + { + id: "1", + file_content: "mockBase64Content1", + file_name: "image1.jpg", + mime_type: "image/jpeg", + }, + { + id: "2", + file_content: "mockBase64Content2", + file_name: "image2.jpg", + mime_type: "image/jpeg", + }, + ], + total: 2, + }; + + expect(validResponse).toHaveProperty("images"); + expect(validResponse.images).toBeInstanceOf(Array); + expect(validResponse.images[0]).toHaveProperty("id"); + expect(validResponse.images[0]).toHaveProperty("file_content"); + expect(validResponse.images[0]).toHaveProperty("file_name"); + expect(validResponse.images[0]).toHaveProperty("mime_type"); + expect(validResponse).toHaveProperty("total", 2); +}); + +it("invalid ImagesResponse structure", () => { + const invalidResponse: any = { total: 2}; + expect(invalidResponse.images).toBeUndefined(); +}); + +it("valid ImagesParams structure", () => { + const validParams: ImagesParams = { + page: 1, + limit: 10, + uid: "user123", + }; + expect(validParams).toHaveProperty("page", 1); + expect(validParams).toHaveProperty("limit", 10); + expect(validParams).toHaveProperty("uid", "user123"); +}); + +it("invalid ImagesParams structure", () => { + const invalidParams: any = { page: 1}; + + expect(() => { + expect(invalidParams).toHaveProperty("limit"); + }).toThrow(); +}); \ No newline at end of file diff --git a/client/src/__tests__/Types/LabelMap.test.tsx b/client/src/__tests__/Types/LabelMap.test.tsx new file mode 100644 index 0000000..2dec463 --- /dev/null +++ b/client/src/__tests__/Types/LabelMap.test.tsx @@ -0,0 +1,35 @@ +import { LabelMap } from "../../Types/LabelMap"; + +it("LabelMap should only allow string keys and string values", () => { + // Valid LabelMap example with different keys + const validLabelMap: LabelMap = { + name: "Name", + email: "Email", + created_at: "Created At", + }; + + expect(validLabelMap).toBeDefined(); + expect(Object.keys(validLabelMap)).toEqual(["name", "email", "created_at"]); + expect(validLabelMap.name).toBe("Name"); + expect(validLabelMap.email).toBe("Email"); + expect(validLabelMap.created_at).toBe("Created At"); +}); + +it("LabelMap should allow only specified keys", () => { + const validLabelMap: LabelMap = { + name: "Name", + email: "Email", + created_at: "Created At", + }; + + const knownKeys = ["name", "email", "created_at"]; + const keys = Object.keys(validLabelMap); + knownKeys.forEach((key) => { + expect(keys).toContain(key); + expect(typeof validLabelMap[key]).toBe("string"); + }); + + Object.values(validLabelMap).forEach((value) => { + expect(typeof value).toBe("string"); + }); +}); \ No newline at end of file diff --git a/client/src/__tests__/Types/UserType.test.tsx b/client/src/__tests__/Types/UserType.test.tsx new file mode 100644 index 0000000..9fdaeec --- /dev/null +++ b/client/src/__tests__/Types/UserType.test.tsx @@ -0,0 +1,51 @@ +import { UserType, UsersResponse } from "../../Types/UserType"; + +it("valid UserType structure", () => { + const validUser: UserType = { + id: "1", + name: "John Doe", + email: "john.doe@example.com", + created_at: new Date().toISOString(), + roles: ["admin", "user"], + }; + expect(validUser).toHaveProperty("id", "1"); + expect(validUser).toHaveProperty("name", "John Doe"); + expect(validUser).toHaveProperty("email", "john.doe@example.com"); + expect(validUser).toHaveProperty("created_at"); + expect(validUser).toHaveProperty("roles"); + expect(validUser.roles).toBeInstanceOf(Array); + expect(validUser.roles).toContain("admin"); + expect(validUser.roles).toContain("user"); +}); + +it("valid UsersResponse structure", () => { + const validResponse: UsersResponse = { + users: [ + { + id: "1", + name: "John Doe", + email: "john.doe@example.com", + created_at: new Date().toISOString(), + roles: ["admin"], + }, + { + id: "2", + name: "Jane Smith", + email: "jane.smith@example.com", + created_at: new Date().toISOString(), + roles: ["user"], + }, + ], + }; + expect(validResponse).toHaveProperty("users"); + expect(validResponse.users).toBeInstanceOf(Array); + expect(validResponse.users[0]).toHaveProperty("id"); + expect(validResponse.users[0]).toHaveProperty("name"); + expect(validResponse.users[0]).toHaveProperty("email"); + expect(validResponse.users[0]).toHaveProperty("roles"); +}); + +it("invalid UsersResponse structure", () => { + const invalidResponse: any = { }; + expect(invalidResponse.users).toBeUndefined(); +}); diff --git a/client/src/__tests__/pages/Admin/Images.test.tsx b/client/src/__tests__/pages/Admin/Images.test.tsx index 3e44ff5..4416c0f 100644 --- a/client/src/__tests__/pages/Admin/Images.test.tsx +++ b/client/src/__tests__/pages/Admin/Images.test.tsx @@ -1 +1 @@ -//a;ready being tested by ImageGallery.test.tsx \ No newline at end of file +//TESTS ON ImageGallery.test.tsx \ No newline at end of file From 10169b93f18564c7b709b50822146060f476a839 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Wed, 2 Apr 2025 20:40:36 -0400 Subject: [PATCH 20/22] ajout isadmin check --- client/src/App.tsx | 6 +++--- client/src/services/ApiService.tsx | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index fb20f4f..6d0fae2 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -34,7 +34,7 @@ import Stats from './pages/Admin/Stats'; const App: React.FC = () => { const [isAuthenticated, setIsAuthenticated] = useState(ApiService.isLoggedIn()); const [isTeacherAuthenticated, setIsTeacherAuthenticated] = useState(ApiService.isLoggedInTeacher()); - //const [isAdmin, setIsAdmin] = useState(false); + const [isAdmin, setIsAdmin] = useState(false); const [isRoomRequireAuthentication, setRoomsRequireAuth] = useState(null); const location = useLocation(); @@ -43,7 +43,7 @@ const App: React.FC = () => { const checkLoginStatus = () => { setIsAuthenticated(ApiService.isLoggedIn()); setIsTeacherAuthenticated(ApiService.isLoggedInTeacher()); - //setIsAdmin(ApiService.isAdmin()); + setIsAdmin(ApiService.isAdmin()); }; const fetchAuthenticatedRooms = async () => { @@ -63,7 +63,7 @@ const App: React.FC = () => { return (
-
+
diff --git a/client/src/services/ApiService.tsx b/client/src/services/ApiService.tsx index e0b498c..2661d24 100644 --- a/client/src/services/ApiService.tsx +++ b/client/src/services/ApiService.tsx @@ -120,6 +120,27 @@ class ApiService { } } + public isAdmin(): boolean { + let isAdmin = false; + const token = this.getToken(); + + if (token == null) { + return isAdmin; + } + + try { + const jsonObj = jwtDecode(token) as { roles: string[] }; + + if (jsonObj.roles.includes('admin')) { + isAdmin = true; + } + return isAdmin; + } catch (error) { + console.error("Error decoding token:", error); + return isAdmin; + } + } + public saveUsername(username: string): void { if (!username || username.length === 0) { return; From e0897ff5365005f12c42ede02faeba4d99d4a712 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Thu, 10 Apr 2025 16:56:18 -0400 Subject: [PATCH 21/22] ajout merge --- .../src/components/AdminTable/AdminTable.tsx | 4 +- .../components/ImageGallery/ImageGallery.tsx | 168 +++++++++++++----- .../ImageGalleryModal/ImageGalleryModal.tsx | 21 ++- client/src/constants.tsx | 1 + 4 files changed, 145 insertions(+), 49 deletions(-) diff --git a/client/src/components/AdminTable/AdminTable.tsx b/client/src/components/AdminTable/AdminTable.tsx index be5b594..45d4c13 100644 --- a/client/src/components/AdminTable/AdminTable.tsx +++ b/client/src/components/AdminTable/AdminTable.tsx @@ -149,10 +149,10 @@ const AdminTable: React.FC = ({ /> - Confirm Deletion + Confirmation - Are you sure you want to delete this record? + Voulez-vous vraiment supprimer? diff --git a/client/src/components/ImageGallery/ImageGallery.tsx b/client/src/components/ImageGallery/ImageGallery.tsx index ab78bb3..7d2b4b2 100644 --- a/client/src/components/ImageGallery/ImageGallery.tsx +++ b/client/src/components/ImageGallery/ImageGallery.tsx @@ -13,7 +13,8 @@ import { DialogContentText, Tabs, Tab, - TextField + TextField, Snackbar, + Alert } from "@mui/material"; import DeleteIcon from "@mui/icons-material/Delete"; import ContentCopyIcon from "@mui/icons-material/ContentCopy"; @@ -21,12 +22,15 @@ import CloseIcon from "@mui/icons-material/Close"; import { ImageType } from "../../Types/ImageType"; import ApiService from "../../services/ApiService"; import { Upload } from "@mui/icons-material"; +import { ENV_VARIABLES } from '../../constants'; +import { escapeForGIFT } from "src/utils/giftUtils"; interface ImagesProps { handleCopy?: (id: string) => void; + handleDelete?: (id: string) => void; } -const ImageGallery: React.FC = ({ handleCopy }) => { +const ImageGallery: React.FC = ({ handleCopy, handleDelete }) => { const [images, setImages] = useState([]); const [totalImg, setTotalImg] = useState(0); const [imgPage, setImgPage] = useState(1); @@ -38,39 +42,60 @@ const ImageGallery: React.FC = ({ handleCopy }) => { const [tabValue, setTabValue] = useState(0); const [importedImage, setImportedImage] = useState(null); const [preview, setPreview] = useState(null); + const [snackbarOpen, setSnackbarOpen] = useState(false); + const [snackbarMessage, setSnackbarMessage] = useState(""); + const [snackbarSeverity, setSnackbarSeverity] = useState<"success" | "error">("success"); + + const fetchImages = async () => { + setLoading(true); + const data = await ApiService.getImages(imgPage, imgLimit); + setImages(data.images); + setTotalImg(data.total); + setLoading(false); + }; useEffect(() => { - const fetchImages = async () => { - setLoading(true); - const data = await ApiService.getImages(imgPage, imgLimit); - setImages(data.images); - setTotalImg(data.total); - setLoading(false); - }; fetchImages(); }, [imgPage]); - const handleDelete = async () => { + const defaultHandleDelete = async (id: string) => { if (imageToDelete) { setLoading(true); - const isDeleted = await ApiService.deleteImage(imageToDelete.id); + const isDeleted = await ApiService.deleteImage(id); setLoading(false); + if (isDeleted) { - setImages(images.filter((image) => image.id !== imageToDelete.id)); - setSelectedImage(null); - setImageToDelete(null); - setOpenDeleteDialog(false); + setImgPage(1); + fetchImages(); + setSnackbarMessage("Image supprimée avec succès!"); + setSnackbarSeverity("success"); + } else { + setSnackbarMessage("Erreur lors de la suppression de l'image. Veuillez réessayer."); + setSnackbarSeverity("error"); } + + setSnackbarOpen(true); + setSelectedImage(null); + setImageToDelete(null); + setOpenDeleteDialog(false); } }; const defaultHandleCopy = (id: string) => { if (navigator.clipboard) { - navigator.clipboard.writeText(id); + const link = `${ENV_VARIABLES.IMG_URL}/api/image/get/${id}`; + const imgTag = `[markdown]![alt_text](${escapeForGIFT(link)} "texte de l'infobulle")`; + setSnackbarMessage("Le lien Markdown de l’image a été copié dans le presse-papiers"); + setSnackbarSeverity("success"); + setSnackbarOpen(true); + navigator.clipboard.writeText(imgTag); + } + if(handleCopy) { + handleCopy(id); } }; - const handleCopyFunction = handleCopy || defaultHandleCopy; + const handleDeleteFunction = handleDelete || defaultHandleDelete; const handleImageUpload = (event: React.ChangeEvent) => { const file = event.target.files ? event.target.files[0] : null; @@ -81,6 +106,44 @@ const ImageGallery: React.FC = ({ handleCopy }) => { } }; + const handleSaveImage = async () => { + try { + if (!importedImage) { + setSnackbarMessage("Veuillez choisir une image à téléverser."); + setSnackbarSeverity("error"); + setSnackbarOpen(true); + return; + } + + const imageUrl = await ApiService.uploadImage(importedImage); + + if (imageUrl.includes("ERROR")) { + setSnackbarMessage("Une erreur est survenue. Veuillez réessayer plus tard."); + setSnackbarSeverity("error"); + setSnackbarOpen(true); + return; + } + fetchImages(); + + setSnackbarMessage("Téléversée avec succès !"); + setSnackbarSeverity("success"); + setSnackbarOpen(true); + + setImportedImage(null); + setPreview(null); + setTabValue(0); + } catch (error) { + setSnackbarMessage(`Une erreur est survenue.\n${error}\nVeuillez réessayer plus tard.`); + setSnackbarSeverity("error"); + setSnackbarOpen(true); + } + }; + + const handleCloseSnackbar = () => { + setSnackbarOpen(false); + }; + + return ( setTabValue(newValue)}> @@ -97,7 +160,7 @@ const ImageGallery: React.FC = ({ handleCopy }) => { <> {images.map((obj) => ( - setSelectedImage(obj)}> + setSelectedImage(obj)}> = ({ handleCopy }) => { style={{ width: "100%", height: 250, objectFit: "cover", borderRadius: 8 }} /> + + { + e.stopPropagation(); + defaultHandleCopy(obj.id); + }} + color="primary" + data-testid={`gallery-tab-copy-${obj.id}`} > + + + + + { + e.stopPropagation(); + setImageToDelete(obj); + setOpenDeleteDialog(true); + }} + color="error" + data-testid={`gallery-tab-delete-${obj.id}`} > + + + + ))} @@ -146,16 +232,16 @@ const ImageGallery: React.FC = ({ handleCopy }) => { maxHeight: "600px", }} /> - - )} = ({ handleCopy }) => { - + + + + {snackbarMessage} + + ); }; diff --git a/client/src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal.tsx b/client/src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal.tsx index f208a4e..f960352 100644 --- a/client/src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal.tsx +++ b/client/src/components/ImageGallery/ImageGalleryModal/ImageGalleryModal.tsx @@ -7,8 +7,15 @@ import { } from "@mui/material"; import CloseIcon from "@mui/icons-material/Close"; import ImageGallery from "../ImageGallery"; +import { ImageSearch } from "@mui/icons-material"; -const ImageGalleryModal: React.FC = () => { + +interface ImageGalleryModalProps { + handleCopy?: (id: string) => void; +} + + +const ImageGalleryModal: React.FC = ({ handleCopy }) => { const [open, setOpen] = useState(false); const handleOpen = () => setOpen(true); @@ -16,14 +23,18 @@ const ImageGalleryModal: React.FC = () => { return ( <> - + { - + diff --git a/client/src/constants.tsx b/client/src/constants.tsx index ad5b80b..0cbbb9f 100644 --- a/client/src/constants.tsx +++ b/client/src/constants.tsx @@ -2,6 +2,7 @@ const ENV_VARIABLES = { MODE: process.env.MODE || "production", VITE_BACKEND_URL: process.env.VITE_BACKEND_URL || "", + IMG_URL: process.env.MODE == "development" ? process.env.VITE_BACKEND_URL : process.env.VITE_IMG_URL, BACKEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.BACKEND_PORT}`:''}` : process.env.VITE_BACKEND_URL || '', FRONTEND_URL: process.env.SITE_URL != undefined ? `${process.env.SITE_URL}${process.env.USE_PORTS ? `:${process.env.PORT}`:''}` : '' }; From 8d44d09c545cdf075041cfaeca0a8937add102d5 Mon Sep 17 00:00:00 2001 From: Eddi3_As Date: Thu, 10 Apr 2025 19:18:52 -0400 Subject: [PATCH 22/22] fixed tests --- .../AdminDrawer/AdminDrawer.test.tsx | 19 ++++++++++++++++--- .../components/AdminTable/AdminTable.test.tsx | 4 ++-- .../src/__tests__/pages/Admin/Images.test.tsx | 1 - 3 files changed, 18 insertions(+), 6 deletions(-) delete mode 100644 client/src/__tests__/pages/Admin/Images.test.tsx diff --git a/client/src/__tests__/components/AdminDrawer/AdminDrawer.test.tsx b/client/src/__tests__/components/AdminDrawer/AdminDrawer.test.tsx index fe68167..fd34b2b 100644 --- a/client/src/__tests__/components/AdminDrawer/AdminDrawer.test.tsx +++ b/client/src/__tests__/components/AdminDrawer/AdminDrawer.test.tsx @@ -1,11 +1,16 @@ import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import AdminDrawer from '../../../components/AdminDrawer/AdminDrawer'; +import { BrowserRouter as Router } from 'react-router-dom'; // Import Router import '@testing-library/jest-dom'; describe('AdminDrawer Component', () => { test('renders the Admin button', () => { - render(); + render( + + + + ); // Check if the "Admin" button is in the document const button = screen.getByRole('button', { name: /admin/i }); @@ -13,7 +18,11 @@ describe('AdminDrawer Component', () => { }); test('opens the drawer when the button is clicked', () => { - render(); + render( + + + + ); // Click the "Admin" button const button = screen.getByRole('button', { name: /admin/i }); @@ -43,7 +52,11 @@ describe('AdminDrawer Component', () => { }); */ test('menu items render correctly', () => { - render(); + render( + + + + ); // Open the drawer const button = screen.getByRole('button', { name: /admin/i }); diff --git a/client/src/__tests__/components/AdminTable/AdminTable.test.tsx b/client/src/__tests__/components/AdminTable/AdminTable.test.tsx index 68a28ab..c937071 100644 --- a/client/src/__tests__/components/AdminTable/AdminTable.test.tsx +++ b/client/src/__tests__/components/AdminTable/AdminTable.test.tsx @@ -50,10 +50,10 @@ describe("AdminTable Component", () => { const deleteButton = screen.getAllByRole("button")[0]; fireEvent.click(deleteButton); - expect(screen.getByText("Confirm Deletion")).toBeInTheDocument(); + expect(screen.getByText("Confirmation")).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: /cancel/i })); await waitFor(() => { - expect(screen.queryByText("Confirm Deletion")).not.toBeInTheDocument(); + expect(screen.queryByText("Confirmation")).not.toBeInTheDocument(); }); }); diff --git a/client/src/__tests__/pages/Admin/Images.test.tsx b/client/src/__tests__/pages/Admin/Images.test.tsx deleted file mode 100644 index 4416c0f..0000000 --- a/client/src/__tests__/pages/Admin/Images.test.tsx +++ /dev/null @@ -1 +0,0 @@ -//TESTS ON ImageGallery.test.tsx \ No newline at end of file