Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
256 changes: 256 additions & 0 deletions Client/src/components/errorPage/ReportError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useAppDispatch, errorActions } from "@/redux";
import { Button } from "@/components/ui/button";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { toast } from "@/components/ui/use-toast";
import { ErrorDocument } from "@polylink/shared/types";
import { CreateErrorReportData } from "@/redux/error/errorSlice";

// Define the Zod schema for error reporting
const errorReportSchema = z.object({
title: z.string().min(1, "Title is required"),
description: z.string().min(1, "Description is required"),
type: z.enum(["bug", "feature-request", "improvement", "other"]),
severity: z.enum(["low", "medium", "high", "critical"]),
stepsToReproduce: z.string().optional(),
expectedBehavior: z.string().optional(),
actualBehavior: z.string().optional(),
environment: z
.object({
browser: z.string().optional(),
operatingSystem: z.string().optional(),
device: z.string().optional(),
})
.optional(),
});

type ErrorReportForm = Omit<
ErrorDocument,
"_id" | "createdAt" | "updatedAt" | "status" | "userId"
>;

export function ReportError() {
const dispatch = useAppDispatch();

const form = useForm<ErrorReportForm>({
resolver: zodResolver(errorReportSchema),
defaultValues: {
title: "",
description: "",
type: "bug",
severity: "medium",
stepsToReproduce: "",
expectedBehavior: "",
actualBehavior: "",
environment: {
browser: "",
operatingSystem: "",
device: "",
},
},
});

const onSubmit = async (data: ErrorReportForm) => {
try {
await dispatch(
errorActions.submitErrorReport(data as CreateErrorReportData)
).unwrap();
toast({
title: "Success",
description: "Error report submitted successfully",
});
form.reset();
} catch (error) {
toast({
title: "Error",
description: "Failed to submit error report. Please try again.",
variant: "destructive",
});
}
};

return (
<div className="mx-auto">
<Card className="shadow-lg">
<CardHeader>
<CardTitle>Report an Issue</CardTitle>
<CardDescription>
Help us improve by reporting any issues you encounter
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-5">
<div>
<label className="block text-sm font-medium mb-1">Title</label>
<Input {...form.register("title")} placeholder="Issue title" />
{form.formState.errors.title && (
<p className="mt-1 text-xs text-red-500">
{form.formState.errors.title.message}
</p>
)}
</div>

<div>
<label className="block text-sm font-medium mb-1">
Description
</label>
<Textarea
{...form.register("description")}
placeholder="Describe the issue in detail"
rows={4}
/>
{form.formState.errors.description && (
<p className="mt-1 text-xs text-red-500">
{form.formState.errors.description.message}
</p>
)}
</div>

<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">Type</label>
<Select
onValueChange={(v: string) =>
form.setValue(
"type",
v as "bug" | "feature-request" | "improvement" | "other"
)
}
defaultValue={form.getValues("type")}
>
<SelectTrigger className="w-full bg-white dark:bg-gray-800 border-2 border-slate-200 dark:border-slate-600">
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="bug">Bug</SelectItem>
<SelectItem value="feature-request">Feature</SelectItem>
<SelectItem value="improvement">Improvement</SelectItem>
<SelectItem value="other">Other</SelectItem>
</SelectContent>
</Select>
</div>

<div>
<label className="block text-sm font-medium mb-1">
Severity
</label>
<Select
onValueChange={(v: string) =>
form.setValue(
"severity",
v as "low" | "medium" | "high" | "critical"
)
}
defaultValue={form.getValues("severity")}
>
<SelectTrigger className="w-full bg-white dark:bg-gray-800 border-2 border-slate-200 dark:border-slate-600">
<SelectValue placeholder="Select severity" />
</SelectTrigger>
<SelectContent>
<SelectItem value="low">Low</SelectItem>
<SelectItem value="medium">Medium</SelectItem>
<SelectItem value="high">High</SelectItem>
<SelectItem value="critical">Critical</SelectItem>
</SelectContent>
</Select>
</div>
</div>

<div>
<label className="block text-sm font-medium mb-1">
Steps to Reproduce
</label>
<Textarea
{...form.register("stepsToReproduce")}
placeholder="Optional"
rows={3}
/>
</div>

<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">
Expected Behavior
</label>
<Textarea
{...form.register("expectedBehavior")}
placeholder="Optional"
rows={2}
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">
Actual Behavior
</label>
<Textarea
{...form.register("actualBehavior")}
placeholder="Optional"
rows={2}
/>
</div>
</div>

<div>
<h3 className="text-sm font-medium mb-2">
Environment Details (Optional)
</h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<label className="block text-sm mb-1">Browser</label>
<Input
{...form.register("environment.browser")}
placeholder="e.g. Chrome 90"
/>
</div>
<div>
<label className="block text-sm mb-1">
Operating System
</label>
<Input
{...form.register("environment.operatingSystem")}
placeholder="e.g. Windows 10"
/>
</div>
<div>
<label className="block text-sm mb-1">Device</label>
<Input
{...form.register("environment.device")}
placeholder="e.g. Desktop"
/>
</div>
</div>
</div>

<div className="text-right">
<Button type="submit" variant="default" className="py-2 px-6">
Submit
</Button>
</div>
</form>
</Form>
</CardContent>
</Card>
</div>
);
}

export default ReportError;
59 changes: 33 additions & 26 deletions Client/src/pages/ErrorPage/ErrorPage.tsx
Original file line number Diff line number Diff line change
@@ -1,55 +1,62 @@
import { useNavigate } from "react-router-dom";
import { Button } from "@/components/ui/button";
import SplashLayout from "@/components/layout/splashPage/SplashLayout";
import ReportError from "@/components/errorPage/ReportError";
import { IoBugOutline } from "react-icons/io5";

const ERROR_TEXT = {
title: "404",
subtitle: "Page Not Found",
description: "The page you're looking for doesn't exist or has been moved.",
homeButtonText: "Go Home",
backButtonText: "Go Back",
};
const ErrorPage = () => {
const navigate = useNavigate();
const ERROR_TEXT = {
title: "404",
subtitle: "Page Not Found",
description: "The page you're looking for doesn't exist or has been moved.",
homeButtonText: "Go Home",
backButtonText: "Go Back",
};

return (
<SplashLayout>
<div className="bg-slate-900 dark:bg-slate-900 min-h-screen flex items-center justify-center -mt-20">
<div className="text-center space-y-4 w-full max-w-[40rem] px-6">
<div className="space-y-4">
<h1
className="text-8xl font-extrabold text-gray-700 dark:text-gray-500"
aria-label="404 error"
>
{ERROR_TEXT.title}
</h1>
<h2 className="text-2xl lg:text-3xl font-semibold text-gray-800 dark:text-gray-200">
{ERROR_TEXT.subtitle}
</h2>
</div>
<div className="min-h-screen flex items-center justify-center bg-gray-100 dark:bg-gray-900 p-4">
<div className="bg-white dark:bg-gray-900 border-2 border-slate-200 dark:border-slate-800 rounded-2xl shadow-xl p-8 max-w-5xl w-full text-center space-y-6">
<IoBugOutline
className="mx-auto text-7xl text-red-500"
aria-hidden="true"
/>

<h1 className="text-6xl font-extrabold text-gray-900 dark:text-gray-100">
{ERROR_TEXT.title}
</h1>

<p className="text-sm lg:text-base text-gray-600 dark:text-gray-300">
<h2 className="text-2xl font-semibold text-gray-700 dark:text-gray-200">
{ERROR_TEXT.subtitle}
</h2>

<p className="text-base text-gray-600 dark:text-gray-400">
{ERROR_TEXT.description}
</p>

<div className="flex flex-col lg:flex-row gap-3 justify-center pt-5">
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Button
variant="default"
variant="outline"
onClick={() => navigate(-1)}
className="w-full lg:w-[20rem] hover:bg-slate-800 text-xl lg:text-2xl py-8 lg:py-10 px-12"
className="flex-1 py-3 px-6 text-sm font-medium bg-slate-900 dark:hover:bg-slate-900 text-white"
aria-label="Go back to previous page"
>
{ERROR_TEXT.backButtonText}
</Button>
<Button
variant="default"
onClick={() => navigate("/chat")}
className="w-full lg:w-[20rem] hover:bg-slate-700 text-xl lg:text-2xl py-8 lg:py-10 px-12"
onClick={() => navigate("/")}
className="flex-1 py-3 px-6 text-sm font-medium"
aria-label="Go to home page"
>
{ERROR_TEXT.homeButtonText}
</Button>
</div>

<hr className="border-gray-200 dark:border-gray-700 my-4" />

<ReportError />
</div>
</div>
</SplashLayout>
Expand Down
32 changes: 32 additions & 0 deletions Client/src/redux/error/crudError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ErrorDocument } from "@polylink/shared/types";
import { environment, serverUrl } from "@/helpers/getEnvironmentVars";

type CreateErrorReportData = Omit<
ErrorDocument,
"_id" | "createdAt" | "updatedAt" | "status"
>;
Comment on lines +4 to +7
Copy link

Copilot AI Jun 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The CreateErrorReportData type is duplicated between crudError.ts and the Redux slice. Consider centralizing this type in the shared types to prevent drift and ensure consistency.

Suggested change
type CreateErrorReportData = Omit<
ErrorDocument,
"_id" | "createdAt" | "updatedAt" | "status"
>;
import { CreateErrorReportData } from "@polylink/shared/types";

Copilot uses AI. Check for mistakes.

export async function createErrorReport(
errorData: CreateErrorReportData
): Promise<{ message: string; errorId: string }> {
try {
const response = await fetch(`${serverUrl}/errors`, {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(errorData),
});

if (!response.ok) {
throw new Error(`Error: ${response.status}`);
}

const data = await response.json();
return data as { message: string; errorId: string };
} catch (error) {
if (environment === "dev") {
console.error("Failed to create error report: ", error);
}
throw error;
}
}
Loading
Loading