Skip to content

Commit

Permalink
toast notification on invalid files
Browse files Browse the repository at this point in the history
  • Loading branch information
rotembarsela committed Aug 24, 2024
1 parent 3386728 commit b0172cf
Show file tree
Hide file tree
Showing 9 changed files with 170 additions and 21 deletions.
51 changes: 51 additions & 0 deletions client/src/components/Toast.tsx
Original file line number Diff line number Diff line change
@@ -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<ToastProps> = ({
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 (
<div
className={clsx(
"fixed top-4 right-4 text-white p-4 rounded shadow-lg",
getBackgroundColor()
)}
>
{title && <strong className="block text-lg">{title}</strong>}
<div>
<p className={clsx(title && "mt-2")}>{body}</p>
</div>
</div>
);
};

export default Toast;
13 changes: 13 additions & 0 deletions client/src/context/ToastContext.tsx
Original file line number Diff line number Diff line change
@@ -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<ToastContextType | undefined>(
undefined
);
52 changes: 52 additions & 0 deletions client/src/context/ToastProvider.tsx
Original file line number Diff line number Diff line change
@@ -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<ToastMessage[]>([]);

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 (
<ToastContext.Provider value={{ addToast }}>
{children}
<div className="fixed top-4 right-4 space-y-2">
{toasts.map((toast) => (
<Toast
key={toast.id}
title={toast.title}
body={toast.message}
type={toast.type}
onClose={() => removeToast(toast.id)}
/>
))}
</div>
</ToastContext.Provider>
);
};
10 changes: 10 additions & 0 deletions client/src/context/useToast.tsx
Original file line number Diff line number Diff line change
@@ -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;
};
25 changes: 16 additions & 9 deletions client/src/routes/excels.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,19 @@ 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<Excel[]>([]);
const [pendingFiles, setPendingFiles] = useState<Excel[]>([]);
const [excelInfo, setExcelInfo] = useState<ExcelSpreadsheet | null>(null);

const handleFilesSelected = (files: File[]) => {
const newPendingFiles: Excel[] = files.map((file) => ({
id: "",
id: String(Date.now() + Math.random()),
name: file.name,
}));

Expand All @@ -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<ExcelsUploadResponse>(
`/excels/upload/${selectedUser.id}`,
"POST",
formData
);
const { message, pendingFiles, uploadedFiles, invalidFiles } =
await APIFetcher<ExcelsUploadResponse>(
`/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);
}
Expand Down
17 changes: 10 additions & 7 deletions client/src/routes/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<AppProvider>
<div className="min-h-[100dvh] grid grid-rows-[auto_1fr_auto]">
<Header />
<PageLayout>
<Outlet />
</PageLayout>
<Footer />
</div>
<ToastProvider>
<div className="min-h-[100dvh] grid grid-rows-[auto_1fr_auto]">
<Header />
<PageLayout>
<Outlet />
</PageLayout>
<Footer />
</div>
</ToastProvider>
</AppProvider>
);
}
Expand Down
1 change: 1 addition & 0 deletions client/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export type ExcelsUploadResponse = {
message: string;
pendingFiles: Excel[];
uploadedFiles: Excel[];
invalidFiles: string[];
};

export type ExcelsListResponse = {
Expand Down
5 changes: 3 additions & 2 deletions server/src/excel/excel.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Express.Multer.File>,
) {
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');
Expand All @@ -56,6 +56,7 @@ export class ExcelController {
message: 'Files processed successfully',
pendingFiles,
uploadedFiles,
invalidFiles,
};
}

Expand Down
17 changes: 14 additions & 3 deletions server/src/excel/excel.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export class ExcelService {
public async uploadFiles(
userId: number,
files: Express.Multer.File[],
): Promise<void> {
): Promise<string[]> {
const { userPendingDir } = this.getUserDirectory(userId);

for (const file of files) {
Expand All @@ -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<void> {
public async processPendingFiles(userId: number): Promise<string[]> {
const { userUploadDir } = this.getUserDirectory(userId);
const invalidFiles: string[] = [];

Expand All @@ -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
Expand Down Expand Up @@ -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}`);
Expand Down

0 comments on commit b0172cf

Please sign in to comment.