Repository: JoseArron/concrete-works-testing-rms
Pull request #51: chore: add toast notifications on pages and use native toast style
Author: login: JoseArron
Created: 2025-05-10T09:03:46Z | Merged: 2025-05-10T09:19:33Z

### What does this pull request do?

- [x] Adds toast notifications for actions such as project creation, deletion, and updates.

### Related Issues

Link all issues related to this pull request

- [x] 🔗 Part of [DEV-81](https://linear.app/4sure-se/issue/DEV-81/toast-notifications)


### Type of Change

Select all that apply:

- [ ] ✨ New feature
- [ ] 🐛 Bug fix
- [ ] 📚 Docs
- [ ] 🛠️ Refactoring
- [x] 🎨 UI/UX Style
- [ ] ⚙️ Workflow/Config
- [ ] 🧪 Testing

### Screenshots (Optional)
![Screenshot 2025-05-10 170120](https://github.com/user-attachments/assets/d5c79469-222d-417b-9c56-97e6dbb7ca9c)
![Screenshot 2025-05-10 170126](https://github.com/user-attachments/assets/5fce6cc9-da43-436f-ab7a-2ad025ec5e58)
![Screenshot 2025-05-10 170139](https://github.com/user-attachments/assets/ba6cfee9-c032-41b0-8e29-830b9ce7c5ee)
![Screenshot 2025-05-10 170144](https://github.com/user-attachments/assets/5a1b668e-dfa4-49ad-8444-b42a64b0b63a)
![Screenshot 2025-05-10 170157](https://github.com/user-attachments/assets/5d5fe1d6-3be4-4970-8c82-c506b33ee072)
![Screenshot 2025-05-10 170056](https://github.com/user-attachments/assets/d228a5e4-7522-4100-96cd-f9b7363934e8)
![Screenshot 2025-05-10 170107](https://github.com/user-attachments/assets/ba1bfde8-c60e-4245-b210-15b13a1bc80e)

### Additional Information



Diff:
diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx
index 34c6c1e..e292daf 100644
--- a/src/app/(auth)/layout.tsx
+++ b/src/app/(auth)/layout.tsx
@@ -30,7 +30,7 @@ export default function AuthLayout({
                     <div className="flex h-[calc(100vh-4rem)] flex-col items-center justify-center overflow-auto">
                         <main className="w-full max-w-md p-4">{children}</main>
                     </div>
-                    <Toaster />
+                    <Toaster richColors />
                 </AuthProvider>
             </body>
         </html>
diff --git a/src/app/(main)/layout.tsx b/src/app/(main)/layout.tsx
index 9029ff2..0478334 100644
--- a/src/app/(main)/layout.tsx
+++ b/src/app/(main)/layout.tsx
@@ -36,7 +36,7 @@ export default function MainLayout({
                             </main>
                         </SidebarInset>
                     </SidebarProvider>
-                    <Toaster />
+                    <Toaster richColors />
                 </AuthProvider>
             </body>
         </html>
diff --git a/src/app/(main)/projects/[id]/_components/action-buttons/export-pdf-button.tsx b/src/app/(main)/projects/[id]/_components/action-buttons/export-pdf-button.tsx
index f60ea96..468e84e 100644
--- a/src/app/(main)/projects/[id]/_components/action-buttons/export-pdf-button.tsx
+++ b/src/app/(main)/projects/[id]/_components/action-buttons/export-pdf-button.tsx
@@ -5,14 +5,17 @@ import {
     DropdownMenuItem,
     DropdownMenuTrigger,
 } from "@/components/ui/dropdown-menu";
+import { toast } from "sonner";
+
 import type { Projects } from "@/lib/types/project";
+import { tryCatch } from "@/lib/utils";
 import { ClipboardCheck, FileText, ListCheck } from "lucide-react";
 
 interface ExportPdfButtonProps {
     project: Projects;
     disabled?: boolean;
-    downloadQCP: (project: Projects) => void;
-    downloadSOT: (project: Projects) => void;
+    downloadQCP: (project: Projects) => Promise<void>;
+    downloadSOT: (project: Projects) => Promise<void>;
 }
 
 export function ExportPdfButton({
@@ -21,6 +24,28 @@ export function ExportPdfButton({
     downloadQCP,
     downloadSOT,
 }: ExportPdfButtonProps) {
+    const handleDownloadQCP = async () => {
+        if (disabled) return;
+        const { error } = await tryCatch(downloadQCP(project));
+
+        if (error) {
+            toast.error("Failed to download QCP Report.");
+        } else {
+            toast.success("Successfully downloaded QCP Report.");
+        }
+    };
+
+    const handleDownloadSOT = async () => {
+        if (disabled) return;
+        const { error } = await tryCatch(downloadSOT(project));
+
+        if (error) {
+            toast.error("Failed to download SOT Report.");
+        } else {
+            toast.success("Successfully downloaded SOT Report.");
+        }
+    };
+
     return (
         <DropdownMenu>
             <DropdownMenuTrigger asChild>
@@ -37,14 +62,14 @@ export function ExportPdfButton({
 
             <DropdownMenuContent className="min-w-[180px] rounded-md border border-gray-200 bg-white p-1 shadow-lg">
                 <DropdownMenuItem
-                    onClick={() => downloadQCP(project)}
+                    onClick={() => handleDownloadQCP()}
                     className="flex cursor-pointer items-center rounded-sm px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 focus:bg-gray-100 focus:outline-none"
                 >
                     <ClipboardCheck className="mr-2 h-4 w-4 text-orange-500" />
                     Quality Control Program
                 </DropdownMenuItem>
                 <DropdownMenuItem
-                    onClick={() => downloadSOT(project)}
+                    onClick={() => handleDownloadSOT()}
                     className="flex cursor-pointer items-center rounded-sm px-3 py-2 text-sm text-gray-700 hover:bg-gray-100 focus:bg-gray-100 focus:outline-none"
                 >
                     <ListCheck className="mr-2 h-4 w-4 text-blue-500" />
diff --git a/src/app/(main)/projects/[id]/_components/table/table.tsx b/src/app/(main)/projects/[id]/_components/table/table.tsx
index 853da69..c24d367 100644
--- a/src/app/(main)/projects/[id]/_components/table/table.tsx
+++ b/src/app/(main)/projects/[id]/_components/table/table.tsx
@@ -8,9 +8,14 @@ import {
     TableHeader,
     TableRow,
 } from "@/components/ui/table";
+import type { Callbacks } from "@/lib/types/actions.types";
 import type { Projects } from "@/lib/types/project";
-import type { TestUpdateType } from "@/lib/types/project-test/project-test.types";
-import { tryCatch } from "@/lib/utils";
+import type {
+    ProjectMaterialTest,
+    ProjectWorkItemTest,
+    TestUpdateType,
+} from "@/lib/types/project-test/project-test.types";
+import { withCallbacks } from "@/lib/utils";
 import { updateProjectTestOnFile } from "@/server/actions/projects/update-test-on-file";
 import { ArchiveX } from "lucide-react";
 import { Fragment, useState, useTransition } from "react";
@@ -40,30 +45,47 @@ export function ProjectWorkItemsTable({
     ) => {
         if (isReadOnly) return;
 
-        startTransition(async () => {
-            setProjectData((prevData) =>
-                updateProjectTest(prevData, testId, changeAmount, testType),
-            );
+        const callbacks: Callbacks<
+            ProjectWorkItemTest | ProjectMaterialTest | null,
+            string
+        > = {
+            onSuccess: () => {
+                toast.success("Test count updated successfully");
+                startTransition(() => {
+                    setProjectData((prevData) =>
+                        updateProjectTest(
+                            prevData,
+                            testId,
+                            changeAmount,
+                            testType,
+                        ),
+                    );
+                });
+            },
+            onError: (error) => {
+                toast.error(error);
+                startTransition(() => {
+                    setProjectData((prevData) =>
+                        updateProjectTest(
+                            prevData,
+                            testId,
+                            -changeAmount,
+                            testType,
+                        ),
+                    );
+                });
+            },
+        };
 
-            const result = await tryCatch(
-                updateProjectTestOnFile(testId, changeAmount, testType),
+        startTransition(async () => {
+            const boundAction = updateProjectTestOnFile.bind(
+                null,
+                testId,
+                changeAmount,
+                testType,
             );
-
-            if (result.error) {
-                toast.error("An error occurred while updating the test count");
-                // revert state on error
-                setProjectData((prevData) =>
-                    updateProjectTest(
-                        prevData,
-                        testId,
-                        -changeAmount,
-                        testType,
-                    ),
-                );
-                throw result.error;
-            } else if (result.data) {
-                toast.success(`Test count updated successfully`);
-            }
+            const action = withCallbacks(boundAction, callbacks);
+            await action();
         });
     };
 
diff --git a/src/app/(main)/projects/[id]/_components/test-columns/test-counter.tsx b/src/app/(main)/projects/[id]/_components/test-columns/test-counter.tsx
index 75c310a..5b6382a 100644
--- a/src/app/(main)/projects/[id]/_components/test-columns/test-counter.tsx
+++ b/src/app/(main)/projects/[id]/_components/test-columns/test-counter.tsx
@@ -13,7 +13,6 @@ interface TestCounterProps {
         amount: number,
         type: TestUpdateType,
     ) => Promise<void>;
-    isLoading?: boolean;
     isReadOnly?: boolean;
 }
 
@@ -22,7 +21,6 @@ export const TestCounter = ({
     value,
     type,
     updateTestAction,
-    // isLoading: parentIsLoading = false,
     isReadOnly = false,
 }: TestCounterProps) => {
     const [optimisticValue, addOptimisticValue] = useOptimistic(
diff --git a/src/app/(main)/projects/[id]/edit/_components/update-project-form/update-project-form.tsx b/src/app/(main)/projects/[id]/edit/_components/update-project-form/update-project-form.tsx
index bef5b2b..3e7251b 100644
--- a/src/app/(main)/projects/[id]/edit/_components/update-project-form/update-project-form.tsx
+++ b/src/app/(main)/projects/[id]/edit/_components/update-project-form/update-project-form.tsx
@@ -20,6 +20,7 @@ import { formLayout } from "@/app/(main)/projects/_components/project-form/proje
 import ButtonWithLoader from "@/components/custom/button-with-loader";
 import { Edit } from "lucide-react";
 import { startTransition } from "react";
+import { toast } from "sonner";
 import { updateProjectFormConfig } from "./update-project-form.config";
 import { updateProjectSchema } from "./update-project-form.schema";
 
@@ -44,6 +45,7 @@ export function UpdateProjectForm({
     const callbacks: Callbacks<ProjectDTO | null, ProjectActionErrors> = {
         // on server action success
         onSuccess: (_data) => {
+            toast.success("Project updated successfully.");
             startTransition(() => {
                 router.push(`/projects/${projectId}`);
                 form.reset();
diff --git a/src/app/(main)/projects/[id]/work-items/_components/add-project-work-item-form/add-project-work-item-form.tsx b/src/app/(main)/projects/[id]/work-items/_components/add-project-work-item-form/add-project-work-item-form.tsx
index daae9ec..4047d10 100644
--- a/src/app/(main)/projects/[id]/work-items/_components/add-project-work-item-form/add-project-work-item-form.tsx
+++ b/src/app/(main)/projects/[id]/work-items/_components/add-project-work-item-form/add-project-work-item-form.tsx
@@ -23,6 +23,7 @@ import type {
 import { withCallbacks } from "@/lib/utils";
 
 import { startTransition } from "react";
+import { toast } from "sonner";
 import { createProjectWorkItemSchema } from "./add-project-work-item-form.schema";
 import { WorkItemSelect } from "./work-item-select";
 
@@ -49,11 +50,11 @@ export function AddProjectWorkItemForm({
         ProjectWorkItemActionErrors
     > = {
         // on server action success
-        onSuccess: (data) => {
+        onSuccess: (_data) => {
+            toast.success("Work item added to project successfully");
             startTransition(() => {
                 form.reset();
             });
-            console.log("success", data);
         },
         // on server action error
         onError: (error) => {
diff --git a/src/app/(main)/projects/[id]/work-items/_components/project-work-items-table/project-work-item-row.tsx b/src/app/(main)/projects/[id]/work-items/_components/project-work-items-table/project-work-item-row.tsx
index 8570bfa..41f776d 100644
--- a/src/app/(main)/projects/[id]/work-items/_components/project-work-items-table/project-work-item-row.tsx
+++ b/src/app/(main)/projects/[id]/work-items/_components/project-work-items-table/project-work-item-row.tsx
@@ -1,7 +1,6 @@
 "use client";
 
 import { startTransition, useEffect, useRef } from "react";
-import { toast } from "sonner";
 
 import { TableCell, TableRow } from "@/components/ui/table";
 import { useFormAction } from "@/hooks/use-form-action";
@@ -12,6 +11,7 @@ import type {
     ProjectWorkItemDTO,
 } from "@/lib/types/project-work-item/project-work-item.types";
 import { withCallbacks } from "@/lib/utils";
+import { toast } from "sonner";
 import {
     UpdateProjectWorkItemActions,
     UpdateProjectWorkItemForm,
@@ -48,13 +48,21 @@ export function ProjectWorkItemRow({
     > = {
         onSuccess: (_data) => {
             toast.success("Quantity updated successfully.");
-            onCancel();
+            startTransition(() => {
+                form.reset();
+                onCancel();
+            });
+        },
+        onError: (error) => {
+            if (error.general)
+                form.setError("root", { message: error.general[0] });
+            if (error.quantity)
+                form.setError("quantity", { message: error.quantity[0] });
         },
     };
 
     const {
         form,
-        actionState,
         isPending: isSubmittingEdit,
         submitAction: submitUpdateAction,
     } = useFormAction({
@@ -97,7 +105,6 @@ export function ProjectWorkItemRow({
                     <UpdateProjectWorkItemForm
                         form={form}
                         formRef={formRef}
-                        actionState={actionState}
                         fieldDetails={{
                             name: "quantity",
                             default: item.quantity,
diff --git a/src/app/(main)/projects/[id]/work-items/_components/project-work-items-table/project-work-items-table.tsx b/src/app/(main)/projects/[id]/work-items/_components/project-work-items-table/project-work-items-table.tsx
index 064d60f..c95cc69 100644
--- a/src/app/(main)/projects/[id]/work-items/_components/project-work-items-table/project-work-items-table.tsx
+++ b/src/app/(main)/projects/[id]/work-items/_components/project-work-items-table/project-work-items-table.tsx
@@ -66,9 +66,7 @@ export function ProjectWorkItemsTable({
                     result.error?.general?.[0] ?? "Failed to delete work item.";
                 toast.error(errorMsg);
             } else {
-                toast.success(
-                    `Work item ${result.data?.itemNo ?? ""} deleted.`,
-                );
+                toast.success(`Work ${result.data?.itemNo ?? ""} deleted.`);
             }
         });
     };
@@ -104,7 +102,7 @@ export function ProjectWorkItemsTable({
                 </TableRow>
             </TableHeader>
             <TableBody>
-                {data.map((item) => {
+                {optimisticItems.map((item) => {
                     const isEditingThisRow = editingItemIds.includes(item.id);
                     const isDeletingThisRow =
                         isDeletePending && deletingId === item.id;
diff --git a/src/app/(main)/projects/[id]/work-items/_components/update-project-work-item-form/update-actions.tsx b/src/app/(main)/projects/[id]/work-items/_components/update-project-work-item-form/update-actions.tsx
index 63b23d8..cfb2f1d 100644
--- a/src/app/(main)/projects/[id]/work-items/_components/update-project-work-item-form/update-actions.tsx
+++ b/src/app/(main)/projects/[id]/work-items/_components/update-project-work-item-form/update-actions.tsx
@@ -32,14 +32,14 @@ export function UpdateProjectWorkItemActions({
                             type="button"
                             variant="ghost"
                             size="icon"
-                            className="h-8 w-8 cursor-pointer text-green-600 hover:bg-green-100 hover:text-green-700"
+                            className="h-8 w-8 cursor-pointer"
                             disabled={isDisabled || isSubmittingEdit}
                             aria-label="Save quantity"
                         >
                             {isSubmittingEdit ? (
-                                <Loader className="h-4 w-4 animate-spin" />
+                                <Loader className="text-muted-600 h-4 w-4 animate-spin" />
                             ) : (
-                                <Save className="h-4 w-4" />
+                                <Save className="h-4 w-4 text-green-600 hover:bg-green-100 hover:text-green-700" />
                             )}
                         </Button>
                     </TooltipTrigger>
diff --git a/src/app/(main)/projects/[id]/work-items/_components/update-project-work-item-form/update-work-item-form.tsx b/src/app/(main)/projects/[id]/work-items/_components/update-project-work-item-form/update-work-item-form.tsx
index 728681f..b8acca7 100644
--- a/src/app/(main)/projects/[id]/work-items/_components/update-project-work-item-form/update-work-item-form.tsx
+++ b/src/app/(main)/projects/[id]/work-items/_components/update-project-work-item-form/update-work-item-form.tsx
@@ -1,9 +1,6 @@
 import { Form as UIForm } from "@/components/ui/form";
 import { type FieldConfig } from "@/lib/types/form.types";
-import {
-    type ProjectWorkItemActionState,
-    type UpdateProjectWorkItemDTO,
-} from "@/lib/types/project-work-item/project-work-item.types";
+import { type UpdateProjectWorkItemDTO } from "@/lib/types/project-work-item/project-work-item.types";
 import Form from "next/form";
 import { type UseFormReturn } from "react-hook-form";
 import { UpdateProjectWorkItemField } from "./update-project-work-item-field";
@@ -13,7 +10,6 @@ interface UpdateProjectWorkItemFormProps {
     fieldDetails: FieldConfig<UpdateProjectWorkItemDTO>;
     formRef: React.RefObject<HTMLFormElement | null>;
     isDisabled?: boolean;
-    actionState: ProjectWorkItemActionState;
     handleFormSubmit: (e?: React.BaseSyntheticEvent) => Promise<void>;
     submitAction: (payload: FormData) => void;
 }
@@ -23,7 +19,6 @@ export function UpdateProjectWorkItemForm({
     fieldDetails,
     formRef,
     isDisabled = false,
-    actionState,
     handleFormSubmit,
     submitAction,
 }: UpdateProjectWorkItemFormProps) {
@@ -46,9 +41,9 @@ export function UpdateProjectWorkItemForm({
                     style={{ display: "none" }}
                 />
             </Form>
-            {!actionState.success && actionState.error && (
+            {form.formState.errors.root && (
                 <p className="mt-1 text-xs text-destructive">
-                    {actionState.error.general ?? ""}
+                    {form.formState.errors.root.message ?? ""}
                 </p>
             )}
         </UIForm>
diff --git a/src/app/(main)/projects/_components/project-list/project-item.tsx b/src/app/(main)/projects/_components/project-list/project-item.tsx
index 5d798ec..66961f1 100644
--- a/src/app/(main)/projects/_components/project-list/project-item.tsx
+++ b/src/app/(main)/projects/_components/project-list/project-item.tsx
@@ -35,9 +35,10 @@ export function ProjectItem({
                 href={`/projects/${data.id}`}
                 className={disabled ? "pointer-events-none" : ""}
                 aria-disabled={disabled}
+                tabIndex={disabled ? -1 : undefined}
             >
                 <Card
-                    className={`transition-shadow hover:shadow-md ${disabled ? "opacity-50" : ""} h-72`}
+                    className={`group-hover: h-72 transition-all duration-150 ease-in-out group-hover:bg-muted/35 ${disabled ? "cursor-not-allowed opacity-50" : "group-hover:shadow-lg hover:shadow-md"}`}
                 >
                     <CardHeader className="pb-2">
                         <div className="min-h-[72px] w-auto max-w-[485px] space-y-2 overflow-hidden">
@@ -71,14 +72,16 @@ export function ProjectItem({
                     </CardContent>
                 </Card>
             </Link>
-            <div className="absolute top-2 right-2 z-10">
-                <DeleteDialog
-                    entityId={data.id}
-                    entityName={"project"}
-                    entityAlias={data.contractId}
-                    onDeleteAction={onDeleteAction}
-                />
-            </div>
+            {!disabled && (
+                <div className="absolute top-2 right-2 z-10 opacity-0 transition-opacity duration-150 ease-in-out group-hover:opacity-100">
+                    <DeleteDialog
+                        entityId={data.id}
+                        entityName={"project"}
+                        entityAlias={data.contractId}
+                        onDeleteAction={onDeleteAction}
+                    />
+                </div>
+            )}
         </div>
     );
 }
diff --git a/src/app/(main)/projects/_components/project-list/project-list.tsx b/src/app/(main)/projects/_components/project-list/project-list.tsx
index 765046a..d6abd6e 100644
--- a/src/app/(main)/projects/_components/project-list/project-list.tsx
+++ b/src/app/(main)/projects/_components/project-list/project-list.tsx
@@ -9,7 +9,7 @@ import type {
 import type { Callbacks } from "@/lib/types/actions.types";
 import { withCallbacks } from "@/lib/utils";
 import { deleteProject } from "@/server/actions/projects/delete-project";
-import { useOptimistic, useState, useTransition } from "react";
+import { startTransition, useOptimistic, useState } from "react";
 import { toast } from "sonner";
 import { ProjectItem } from "./project-item";
 import { ProjectListHeader } from "./project-list-header";
@@ -27,7 +27,10 @@ export function ProjectList({ data }: ProjectListProps) {
             toast.success("Project deleted successfully.");
         },
         onError: (error) => {
-            toast.error(error.general?.[0] ?? "Failed to delete project.");
+            startTransition(() => {
+                setDeletingId(null);
+                toast.error(error.general?.[0] ?? "Failed to delete project.");
+            });
         },
     };
 
@@ -46,22 +49,27 @@ export function ProjectList({ data }: ProjectListProps) {
         },
     );
 
-    const [, startTransition] = useTransition();
     const [deletingId, setDeletingId] = useState<string | null>(null);
 
     const deleteAction = async (id: string) => {
         setDeletingId(id);
-        const deleteProjectAction = withCallbacks(
-            deleteProject.bind(null, id),
-            callbacks,
-        );
-        const res = await deleteProjectAction();
 
+        // start a transition to delete the project
         startTransition(async () => {
-            if (res.success && res.data) {
-                setProjects({ type: "DELETE", payload: { id: res.data.id } });
-                setDeletingId(null);
-            }
+            const deleteProjectAction = withCallbacks(
+                deleteProject.bind(null, id),
+                callbacks,
+            );
+
+            await deleteProjectAction();
+
+            // make the transition wait for the state update to finish
+            startTransition(() => {
+                setProjects({
+                    type: "DELETE",
+                    payload: { id: id },
+                });
+            });
         });
     };
 
diff --git a/src/app/(main)/projects/new/_components/create-project-form/create-project-form.tsx b/src/app/(main)/projects/new/_components/create-project-form/create-project-form.tsx
index 81f22fb..3bb38f7 100644
--- a/src/app/(main)/projects/new/_components/create-project-form/create-project-form.tsx
+++ b/src/app/(main)/projects/new/_components/create-project-form/create-project-form.tsx
@@ -22,6 +22,7 @@ import {
 } from "@/app/(main)/projects/_components/project-form";
 import { Plus } from "lucide-react";
 import { startTransition } from "react";
+import { toast } from "sonner";
 import { createProjectFormConfig } from "./create-project-form.config";
 import { createProjectSchema } from "./create-project-form.schema";
 
@@ -43,6 +44,7 @@ export function CreateProjectForm({
     const callbacks: Callbacks<ProjectDTO | null, ProjectActionErrors> = {
         // on server action success
         onSuccess: (data) => {
+            toast.success("Project created successfully");
             startTransition(() => {
                 router.push(`/projects/${data?.id}/work-items`);
                 form.reset();
diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx
index 8dbeb87..1f6d05e 100644
--- a/src/components/ui/sonner.tsx
+++ b/src/components/ui/sonner.tsx
@@ -5,26 +5,26 @@ import type { ToasterProps } from "sonner";
 import { Toaster as Sonner } from "sonner";
 
 const Toaster = ({ ...props }: ToasterProps) => {
-    const { theme = "system" } = useTheme();
+    const { theme = "light" } = useTheme();
 
     return (
         <Sonner
             theme={theme as ToasterProps["theme"]}
             className="toaster group"
-            toastOptions={{
-                unstyled: true,
-                classNames: {
-                    toast: "group font-sans text-sm border rounded-md shadow-lg p-4 flex gap-3",
-                    success: "bg-green-600 border-green-700 text-white",
-                    error: "bg-red-600 border-red-700 text-white",
-                    warning: "bg-amber-500 border-amber-600 text-white",
-                    info: "bg-blue-500 border-blue-600 text-white",
-                    default:
-                        "bg-gray-700 border-gray-800 text-white dark:bg-gray-800 dark:border-gray-700",
-                    title: "font-medium text-base",
-                    description: "text-sm opacity-90",
-                },
-            }}
+            // toastOptions={{
+            //     unstyled: true,
+            //     classNames: {
+            //         toast: "group font-sans text-sm border rounded-md shadow-lg p-4 flex gap-3",
+            //         success: "bg-green-200 border-green-900 text-green-900",
+            //         error: "bg-red-600 border-red-700 text-white",
+            //         warning: "bg-amber-500 border-amber-600 text-white",
+            //         info: "bg-blue-500 border-blue-600 text-white",
+            //         default:
+            //             "bg-gray-700 border-gray-800 text-white dark:bg-gray-800 dark:border-gray-700",
+            //         title: "font-medium text-base",
+            //         description: "text-sm opacity-90",
+            //     },
+            // }}
             {...props}
         />
     );
diff --git a/src/lib/types/project-test/project-test.types.ts b/src/lib/types/project-test/project-test.types.ts
index 3e45ca9..749fb62 100644
--- a/src/lib/types/project-test/project-test.types.ts
+++ b/src/lib/types/project-test/project-test.types.ts
@@ -2,21 +2,23 @@ import { type ActionState } from "../actions.types";
 
 export type TestUpdateType = "material" | "workItem";
 
+export type ProjectWorkItemTest = {
+    id: string;
+    testId: string;
+    projectWorkItemId: string;
+    onFile: number;
+    createdAt: Date;
+};
+
+export type ProjectMaterialTest = {
+    id: string;
+    testId: string;
+    projectMaterialId: string;
+    onFile: number;
+    createdAt: Date;
+};
+
 export type ProjectTestActionState = ActionState<
-    | {
-          id: string;
-          testId: string;
-          projectWorkItemId: string;
-          onFile: number;
-          createdAt: Date;
-      }
-    | {
-          id: string;
-          testId: string;
-          projectMaterialId: string;
-          onFile: number;
-          createdAt: Date;
-      }
-    | null,
+    ProjectWorkItemTest | ProjectMaterialTest | null,
     string
 >;
diff --git a/src/lib/utils/error-handler.ts b/src/lib/utils/error-handler.ts
index d6273ac..4b8c6f3 100644
--- a/src/lib/utils/error-handler.ts
+++ b/src/lib/utils/error-handler.ts
@@ -53,6 +53,11 @@ const handlePrismaError = (
 export function errorHandler(error: Error): CustomError {
     if (error instanceof Prisma.PrismaClientKnownRequestError) {
         return handlePrismaError(error);
+    } else if (error instanceof Prisma.PrismaClientUnknownRequestError) {
+        return {
+            message: "An unknown error occurred.",
+            statusCode: 500,
+        };
     } else {
         return {
             message: error.message || "An unexpected error occurred.",


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR introduces native toast notifications for various pages and updates the toast styles across the application. Key changes include:
- Adding toast notifications for actions such as project creation, deletion, and updates.
- Updating the Toaster component to use a native rich-color style.
- Refining type definitions and UI components to incorporate consistent toast-based feedback.

### Reviewed Changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 2 comments.

<details>
<summary>Show a summary per file</summary>

| File | Description |
| ---- | ----------- |
| src/lib/utils/error-handler.ts | Updated error fallback logic using nullish coalescing. |
| src/lib/types/project-test/project-test.types.ts | Refactored type definitions for project tests. |
| src/components/ui/sonner.tsx | Modified theme default and commented out custom toastOptions block. |
| src/app/(main)/projects/new/_components/create-project-form/create-project-form.tsx | Added toast success message on project creation. |
| src/app/(main)/projects/_components/project-list/project-list.tsx | Adjusted toast notifications in deletion callbacks with state transitions. |
| src/app/(main)/projects/_components/project-list/project-item.tsx | Enhanced accessibility by adding tabIndex and conditionally rendering DeleteDialog. |
| src/app/(main)/projects/[id]/work-items/_components/update-project-work-item-form/update-work-item-form.tsx | Simplified error display logic using formState errors. |
| src/app/(main)/projects/[id]/work-items/_components/update-project-work-item-form/update-actions.tsx | Tweaked icon styling for update actions. |
| src/app/(main)/projects/[id]/work-items/_components/project-work-items-table/project-work-items-table.tsx | Updated toast messages and used optimistic items for state updates. |
| src/app/(main)/projects/[id]/work-items/_components/project-work-item-row/project-work-item-row.tsx | Streamlined form reset and callback handling via startTransition. |
| src/app/(main)/projects/[id]/work-items/_components/add-project-work-item-form/add-project-work-item-form.tsx | Revised toast message posture for work item addition. |
| src/app/(main)/projects/[id]/edit/_components/update-project-form/update-project-form.tsx | Integrated toast messaging on project update. |
| src/app/(main)/projects/[id]/_components/table/table.tsx | Enhanced test count update callbacks with immediate toast feedback. |
| src/app/(main)/projects/[id]/_components/action-buttons/export-pdf-button.tsx | Improved PDF download functions with enhanced toast error/success handling. |
| src/app/(main)/layout.tsx & src/app/(auth)/layout.tsx | Updated Toaster component usage to include native rich-colors styling. |
</details>





state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-10T09:04:24Z

Review comments:
diff_hunk: @@ -53,9 +53,14 @@ const handlePrismaError = (
 export function errorHandler(error: Error): CustomError {
     if (error instanceof Prisma.PrismaClientKnownRequestError) {
         return handlePrismaError(error);
+    } else if (error instanceof Prisma.PrismaClientUnknownRequestError) {
+        return {
+            message: "An unknown error occurred.",
+            statusCode: 500,
+        };
     } else {
         return {
-            message: error.message || "An unexpected error occurred.",
+            message: error.message ?? "An unexpected error occurred.",
path: src/lib/utils/error-handler.ts
user: login: Copilot
body: [nitpick] Using the nullish coalescing operator (??) means that if error.message is an empty string, the fallback message will not be used. Please verify that this behavior is intentional or consider a check that also handles empty string values.
```suggestion
            message: error.message || "An unexpected error occurred.",
```
created_at: 2025-05-10T09:04:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -5,26 +5,26 @@ import type { ToasterProps } from "sonner";
 import { Toaster as Sonner } from "sonner";
 
 const Toaster = ({ ...props }: ToasterProps) => {
-    const { theme = "system" } = useTheme();
+    const { theme = "light" } = useTheme();
 
     return (
         <Sonner
             theme={theme as ToasterProps["theme"]}
             className="toaster group"
-            toastOptions={{
-                unstyled: true,
-                classNames: {
-                    toast: "group font-sans text-sm border rounded-md shadow-lg p-4 flex gap-3",
-                    success: "bg-green-600 border-green-700 text-white",
-                    error: "bg-red-600 border-red-700 text-white",
-                    warning: "bg-amber-500 border-amber-600 text-white",
-                    info: "bg-blue-500 border-blue-600 text-white",
-                    default:
-                        "bg-gray-700 border-gray-800 text-white dark:bg-gray-800 dark:border-gray-700",
-                    title: "font-medium text-base",
-                    description: "text-sm opacity-90",
-                },
-            }}
+            // toastOptions={{
+            //     unstyled: true,
+            //     classNames: {
+            //         toast: "group font-sans text-sm border rounded-md shadow-lg p-4 flex gap-3",
+            //         success: "bg-green-200 border-green-900 text-green-900",
+            //         error: "bg-red-600 border-red-700 text-white",
+            //         warning: "bg-amber-500 border-amber-600 text-white",
+            //         info: "bg-blue-500 border-blue-600 text-white",
+            //         default:
+            //             "bg-gray-700 border-gray-800 text-white dark:bg-gray-800 dark:border-gray-700",
+            //         title: "font-medium text-base",
+            //         description: "text-sm opacity-90",
+            //     },
+            // }}
path: src/components/ui/sonner.tsx
user: login: Copilot
body: [nitpick] Consider removing the commented-out toastOptions block if it is no longer needed, in order to keep the codebase clean and maintainable.
```suggestion
// Removed the commented-out toastOptions block for code cleanliness.
```
created_at: 2025-05-10T09:04:24Z
author_association: CONTRIBUTOR

PR comments:
user: login: linear[bot]
created_at: 2025-05-10T09:03:48Z
body: <p><a href="https://linear.app/4sure-se/issue/DEV-81/toast-notifications">DEV-81 Toast Notifications</a></p>
author_association: NONE
user: login: vercel[bot]
created_at: 2025-05-10T09:03:51Z
body: [vc]: #rD8SmLzvtFqjAeZsDMnw7ad4+shJe85QXcYT8dEUtf4=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJjb25jcmV0ZS13b3Jrcy10ZXN0aW5nLXJtcyIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJjb25jcmV0ZS13b3Jrcy10ZXN0aW5nLXJtcy1naXQtY2hvcmUtZC0wNzZjZTEteW9udXJhZXMtcHJvamVjdHMudmVyY2VsLmFwcCJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20veW9udXJhZXMtcHJvamVjdHMvY29uY3JldGUtd29ya3MtdGVzdGluZy1ybXMvQUhyckticURzUFU1UFBZdG1MOUdMQ1lQU0M5bSIsInByZXZpZXdVcmwiOiJjb25jcmV0ZS13b3Jrcy10ZXN0aW5nLXJtcy1naXQtY2hvcmUtZC0wNzZjZTEteW9udXJhZXMtcHJvamVjdHMudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsInJvb3REaXJlY3RvcnkiOm51bGx9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **concrete-works-testing-rms** | ✅ Ready ([Inspect](https://vercel.com/yonuraes-projects/concrete-works-testing-rms/AHrrKbqDsPU5PPYtmL9GLCYPSC9m)) | [Visit Preview](https://concrete-works-testing-rms-git-chore-d-076ce1-yonuraes-projects.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/concrete-works-testing-rms-git-chore-d-076ce1-yonuraes-projects.vercel.app?via=pr-comment-feedback-link) | May 10, 2025 9:17am |


author_association: NONE