diff --git a/src/App.tsx b/src/App.tsx index 4ae35c4..d672a94 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -12,6 +12,7 @@ import ViewEvent from "./features/Events/v1/Pages/ViewEvent"; import { startAutoUpdater } from "./system/updater/autoUpdater"; import LoginPage from "./features/Auth/v1/Pages/LoginPage"; import SignUpPage from "./features/Auth/v1/Pages/SignUpPage"; +import ProjectDetailPage from "./projects/[id]/page"; function App() { useEffect(() => { @@ -25,6 +26,10 @@ function App() { } /> } /> + }> + } /> + + {/* Protected / Org Routes */} }> } /> diff --git a/src/projects/[id]/components/Header.tsx b/src/projects/[id]/components/Header.tsx new file mode 100644 index 0000000..cc7c05d --- /dev/null +++ b/src/projects/[id]/components/Header.tsx @@ -0,0 +1,123 @@ +import { CalendarClock, PencilLine, Rocket, ShieldCheck, Trash2, XCircle } from "lucide-react"; + +import { Button } from "@/shadcnComponet/ui/button"; +import { cn } from "@/lib/utils"; + +import type { ProjectPermissions, ProjectRecord, UserRole, ViewerContext } from "../types"; + +type HeaderProps = { + project: ProjectRecord; + viewer: ViewerContext; + permissions: ProjectPermissions; + isWorking: boolean; + onEdit: () => void; + onDelete: () => void; + onSubmit: () => void; + onApprove: () => void; + onReject: () => void; +}; + +const statusTheme: Record = { + draft: "bg-slate-100 text-slate-700 ring-slate-200", + submitted: "bg-amber-100 text-amber-800 ring-amber-200", + under_review: "bg-sky-100 text-sky-800 ring-sky-200", + approved: "bg-emerald-100 text-emerald-800 ring-emerald-200", + rejected: "bg-rose-100 text-rose-800 ring-rose-200", +}; + +const roleLabel: Record = { + participant: "Participant", + judge: "Judge", + organizer: "Organizer", + admin: "Admin", +}; + +function getStatusLabel(status: ProjectRecord["status"]) { + return status.replace("_", " ").replace(/\b\w/g, (value) => value.toUpperCase()); +} + +export default function Header({ + project, + viewer, + permissions, + isWorking, + onEdit, + onDelete, + onSubmit, + onApprove, + onReject, +}: HeaderProps) { + return ( +
+
+
+
+ +
+
+
+ + {getStatusLabel(project.status)} + + + + {roleLabel[viewer.role]} view + +
+ +
+

{project.eventType}

+

+ {project.title} +

+

+ + {project.eventName} +

+
+
+ +
+ {permissions.canSubmit && ( + + )} + + {permissions.canEdit && ( + + )} + + {(permissions.canDeleteDraft || permissions.canDeleteAny) && ( + + )} + + {permissions.canModerate && ( + <> + + + + )} +
+
+
+ ); +} diff --git a/src/projects/[id]/components/ModerationPanel.tsx b/src/projects/[id]/components/ModerationPanel.tsx new file mode 100644 index 0000000..9401f8d --- /dev/null +++ b/src/projects/[id]/components/ModerationPanel.tsx @@ -0,0 +1,74 @@ +import { AlertTriangle, CheckCircle2, Trash2, XCircle } from "lucide-react"; + +import { Button } from "@/shadcnComponet/ui/button"; + +import type { ProjectRecord, ScoreSummary } from "../types"; + +type ModerationPanelProps = { + project: ProjectRecord; + scoreSummary: ScoreSummary; + isWorking: boolean; + onApprove: () => void; + onReject: () => void; + onDelete: () => void; +}; + +export default function ModerationPanel({ + project, + scoreSummary, + isWorking, + onApprove, + onReject, + onDelete, +}: ModerationPanelProps) { + return ( +
+
+

+ Moderation +

+

Organizer controls

+
+ +
+

+ + Review context +

+
+
+

Current status

+

+ {project.status.replace("_", " ")} +

+
+
+

Average score

+

+ {scoreSummary.averageScore === null ? "Pending" : `${scoreSummary.averageScore.toFixed(1)} / 40`} +

+
+
+

Judges completed

+

{scoreSummary.judgeCount}

+
+
+
+ +
+ + + +
+
+ ); +} diff --git a/src/projects/[id]/components/Overview.tsx b/src/projects/[id]/components/Overview.tsx new file mode 100644 index 0000000..6cd324a --- /dev/null +++ b/src/projects/[id]/components/Overview.tsx @@ -0,0 +1,211 @@ +import { Globe, Github, Link2, Save, X } from "lucide-react"; +import { useState } from "react"; + +import { Button } from "@/shadcnComponet/ui/button"; + +import type { ProjectRecord, ProjectUpdateInput } from "../types"; + +type OverviewProps = { + project: ProjectRecord; + canEdit: boolean; + isEditing: boolean; + isSaving: boolean; + validationErrors: string[]; + onEditingChange: (editing: boolean) => void; + onSave: (values: ProjectUpdateInput) => Promise; +}; + +type FormState = { + title: string; + description: string; + techStack: string; + repositoryUrl: string; + demoUrl: string; +}; + +function toFormState(project: ProjectRecord): FormState { + return { + title: project.title, + description: project.description, + techStack: project.techStack.join(", "), + repositoryUrl: project.repositoryUrl, + demoUrl: project.demoUrl, + }; +} + +export default function Overview({ + project, + canEdit, + isEditing, + isSaving, + validationErrors, + onEditingChange, + onSave, +}: OverviewProps) { + const [form, setForm] = useState(() => toFormState(project)); + + async function handleSubmit() { + await onSave({ + title: form.title, + description: form.description, + techStack: form.techStack + .split(",") + .map((entry) => entry.trim()) + .filter(Boolean), + repositoryUrl: form.repositoryUrl, + demoUrl: form.demoUrl, + }); + } + + if (isEditing && canEdit) { + return ( +
+
+
+

Overview

+

Refine your submission

+
+
+ + +
+
+ + {validationErrors.length > 0 && ( +
+ {validationErrors.map((error) => ( +

{error}

+ ))} +
+ )} + +
+ + +