From b0172cf7b1aafb5dc29ac6aded5d0a086a743bfd Mon Sep 17 00:00:00 2001 From: Rotem Bar Sela Date: Sat, 24 Aug 2024 17:10:22 +0300 Subject: [PATCH] toast notification on invalid files --- client/src/components/Toast.tsx | 51 +++++++++++++++++++++++++++ client/src/context/ToastContext.tsx | 13 +++++++ client/src/context/ToastProvider.tsx | 52 ++++++++++++++++++++++++++++ client/src/context/useToast.tsx | 10 ++++++ client/src/routes/excels.tsx | 25 ++++++++----- client/src/routes/root.tsx | 17 +++++---- client/src/types/index.ts | 1 + server/src/excel/excel.controller.ts | 5 +-- server/src/excel/excel.service.ts | 17 +++++++-- 9 files changed, 170 insertions(+), 21 deletions(-) create mode 100644 client/src/components/Toast.tsx create mode 100644 client/src/context/ToastContext.tsx create mode 100644 client/src/context/ToastProvider.tsx create mode 100644 client/src/context/useToast.tsx diff --git a/client/src/components/Toast.tsx b/client/src/components/Toast.tsx new file mode 100644 index 0000000..796a215 --- /dev/null +++ b/client/src/components/Toast.tsx @@ -0,0 +1,51 @@ +import React, { useEffect } from "react"; +import clsx from "clsx"; + +interface ToastProps { + title?: string; + body: string; + onClose: () => void; + type?: "success" | "error" | "warning" | "info"; +} + +const Toast: React.FC = ({ + title, + body, + onClose, + type = "info", +}) => { + useEffect(() => { + const timer = setTimeout(onClose, 5000); + return () => clearTimeout(timer); + }, [onClose]); + + const getBackgroundColor = () => { + switch (type) { + case "success": + return "bg-green-500"; + case "error": + return "bg-red-500"; + case "warning": + return "bg-yellow-500"; + case "info": + default: + return "bg-blue-500"; + } + }; + + return ( +
+ {title && {title}} +
+

{body}

+
+
+ ); +}; + +export default Toast; diff --git a/client/src/context/ToastContext.tsx b/client/src/context/ToastContext.tsx new file mode 100644 index 0000000..a73b8b8 --- /dev/null +++ b/client/src/context/ToastContext.tsx @@ -0,0 +1,13 @@ +import { createContext } from "react"; + +interface ToastContextType { + addToast: ( + title: string | undefined, + messages: string | string[], + type?: "success" | "error" | "warning" | "info" + ) => void; +} + +export const ToastContext = createContext( + undefined +); diff --git a/client/src/context/ToastProvider.tsx b/client/src/context/ToastProvider.tsx new file mode 100644 index 0000000..320b6b4 --- /dev/null +++ b/client/src/context/ToastProvider.tsx @@ -0,0 +1,52 @@ +import { ReactNode, useState } from "react"; +import { ToastContext } from "./ToastContext"; +import Toast from "../components/Toast"; + +interface ToastMessage { + id: number; + title?: string; + message: string; + type: "success" | "error" | "warning" | "info"; +} + +export const ToastProvider: React.FC<{ children: ReactNode }> = ({ + children, +}) => { + const [toasts, setToasts] = useState([]); + + const addToast = ( + title: string | undefined, + messages: string | string[], + type: "success" | "error" | "warning" | "info" = "info" + ) => { + const messageBody = Array.isArray(messages) + ? messages.join(", ") + : messages; + + setToasts((prevToasts) => [ + ...prevToasts, + { id: Date.now() + Math.random(), title, message: messageBody, type }, + ]); + }; + + const removeToast = (id: number) => { + setToasts((prevToasts) => prevToasts.filter((toast) => toast.id !== id)); + }; + + return ( + + {children} +
+ {toasts.map((toast) => ( + removeToast(toast.id)} + /> + ))} +
+
+ ); +}; diff --git a/client/src/context/useToast.tsx b/client/src/context/useToast.tsx new file mode 100644 index 0000000..4bbeb5a --- /dev/null +++ b/client/src/context/useToast.tsx @@ -0,0 +1,10 @@ +import { useContext } from "react"; +import { ToastContext } from "./ToastContext"; + +export const useToast = () => { + const context = useContext(ToastContext); + if (!context) { + throw new Error("useToast must be used within a ToastProvider"); + } + return context; +}; diff --git a/client/src/routes/excels.tsx b/client/src/routes/excels.tsx index 0027f0e..0e064bb 100644 --- a/client/src/routes/excels.tsx +++ b/client/src/routes/excels.tsx @@ -12,9 +12,11 @@ import { } from "../types"; import { utils } from "../utils/utils"; import FileInfo from "../components/excels/FileInfo"; +import { useToast } from "../context/useToast"; function Excels() { const { selectedUser } = useAppContext(); + const { addToast } = useToast(); const [uploadedFiles, setUploadedFiles] = useState([]); const [pendingFiles, setPendingFiles] = useState([]); @@ -22,7 +24,7 @@ function Excels() { const handleFilesSelected = (files: File[]) => { const newPendingFiles: Excel[] = files.map((file) => ({ - id: "", + id: String(Date.now() + Math.random()), name: file.name, })); @@ -48,17 +50,22 @@ function Excels() { // change to /excels/uploads/${selectedUser.id} // on the server it will be /excels/:folderName/:userId try { - const response = await APIFetcher( - `/excels/upload/${selectedUser.id}`, - "POST", - formData - ); + const { message, pendingFiles, uploadedFiles, invalidFiles } = + await APIFetcher( + `/excels/upload/${selectedUser.id}`, + "POST", + formData + ); await utils.sleep(2500); - console.log("Upload successful:", response.message); - setPendingFiles(response.pendingFiles); - setUploadedFiles(response.uploadedFiles); + console.log("Upload successful:", message); + setPendingFiles(pendingFiles); + setUploadedFiles(uploadedFiles); + + if (invalidFiles.length) { + addToast("Invalid files:", invalidFiles, "error"); + } } catch (error) { console.error("Upload error:", (error as Error).message); } diff --git a/client/src/routes/root.tsx b/client/src/routes/root.tsx index 15c850e..d091878 100644 --- a/client/src/routes/root.tsx +++ b/client/src/routes/root.tsx @@ -3,17 +3,20 @@ import Header from "../components/Header"; import Footer from "../components/Footer"; import { AppProvider } from "../context/AppProvider"; import PageLayout from "../components/PageLayout"; +import { ToastProvider } from "../context/ToastProvider"; function Root() { return ( -
-
- - - -
-
+ +
+
+ + + +
+
+
); } diff --git a/client/src/types/index.ts b/client/src/types/index.ts index f642c9a..24e2965 100644 --- a/client/src/types/index.ts +++ b/client/src/types/index.ts @@ -30,6 +30,7 @@ export type ExcelsUploadResponse = { message: string; pendingFiles: Excel[]; uploadedFiles: Excel[]; + invalidFiles: string[]; }; export type ExcelsListResponse = { diff --git a/server/src/excel/excel.controller.ts b/server/src/excel/excel.controller.ts index 351db80..6c457b8 100755 --- a/server/src/excel/excel.controller.ts +++ b/server/src/excel/excel.controller.ts @@ -39,12 +39,12 @@ export class ExcelController { // TODO: change to /:folderName/:userId, POST @Post('upload/:userId') - @UseInterceptors(FilesInterceptor('files', 10)) + @UseInterceptors(FilesInterceptor('files', 5)) async uploadFiles( @Param('userId') userId: number, @UploadedFiles() files: Array, ) { - await this.excelService.uploadFiles(userId, files); + const invalidFiles = await this.excelService.uploadFiles(userId, files); const pendingFiles = await this.excelService.listFiles(userId, 'pending'); const uploadedFiles = await this.excelService.listFiles(userId, 'uploads'); @@ -56,6 +56,7 @@ export class ExcelController { message: 'Files processed successfully', pendingFiles, uploadedFiles, + invalidFiles, }; } diff --git a/server/src/excel/excel.service.ts b/server/src/excel/excel.service.ts index 17b6439..b130ccc 100755 --- a/server/src/excel/excel.service.ts +++ b/server/src/excel/excel.service.ts @@ -45,7 +45,7 @@ export class ExcelService { public async uploadFiles( userId: number, files: Express.Multer.File[], - ): Promise { + ): Promise { const { userPendingDir } = this.getUserDirectory(userId); for (const file of files) { @@ -58,10 +58,10 @@ export class ExcelService { this.pendingQueue.push(pendingFilePath); } - await this.processPendingFiles(userId); + return await this.processPendingFiles(userId); } - public async processPendingFiles(userId: number): Promise { + public async processPendingFiles(userId: number): Promise { const { userUploadDir } = this.getUserDirectory(userId); const invalidFiles: string[] = []; @@ -81,6 +81,8 @@ export class ExcelService { // Handle invalid files, e.g., logging, notifying, or saving the list to a database. console.log('Invalid files:', invalidFiles); } + + return invalidFiles; } /* DONT USE @@ -240,6 +242,15 @@ export class ExcelService { const requiredColumns = ['Name', 'Email', 'Israeli ID', 'Phone']; const firstRow = jsonData[0]; + const columnNames = Object.keys(firstRow); + + for (const column of columnNames) { + if (!requiredColumns.includes(column)) { + console.error(`Unexpected column found: ${column}`); + return false; + } + } + for (const column of requiredColumns) { if (!firstRow.hasOwnProperty(column)) { console.error(`Missing required column: ${column}`);