diff --git a/src/app/(frontend)/surveys/[id]/page.tsx b/src/app/(frontend)/surveys/[id]/page.tsx index 4c208fa..13a0108 100644 --- a/src/app/(frontend)/surveys/[id]/page.tsx +++ b/src/app/(frontend)/surveys/[id]/page.tsx @@ -3,7 +3,7 @@ import { headers as nextHeaders } from "next/headers"; import { getPayload } from "payload"; import Link from "next/link"; import { notFound } from "next/navigation"; -import { ClipboardCheck, Lock, TriangleAlert } from "lucide-react"; +import { ArrowLeftIcon, ClipboardCheck, Lock, TriangleAlert } from "lucide-react"; import { SurveyForm } from "@/components/frontend/surveys/SurveyForm"; import { isSurveyOpen, surveyAudienceMatches } from "@/lib/surveys/evaluate"; import { Card, CardContent } from "@/components/ui/card"; @@ -16,7 +16,7 @@ export const metadata = { function NoticeCard({ icon, title, body }: { icon: React.ReactNode; title: string; body: string }) { return ( - + {icon}
@@ -64,26 +64,6 @@ export default async function SurveyDetailPage({ }), ]); - if (submissionCount.totalDocs > 0) { - return ( - } - title="Already submitted" - body="You have already responded to this survey." - /> - ); - } - - if (!isSurveyOpen(form, Date.now())) { - return ( - } - title="Survey closed" - body="This survey is not open right now." - /> - ); - } - const heldQualifications = (profileRes.docs[0]?.progression?.qualifications ?? []) .map((q) => (typeof q === "object" && q !== null && "name" in q ? (q.name as string) : typeof q === "string" ? q : null)) .filter((n): n is string => n !== null); @@ -98,39 +78,70 @@ export default async function SurveyDetailPage({ qualificationNames: heldQualifications, }); - if (!eligible) { - return ( + let content: React.ReactNode; + if (submissionCount.totalDocs > 0) { + content = ( + } + title="Already submitted" + body="You have already responded to this survey." + /> + ); + } else if (!isSurveyOpen(form, Date.now())) { + content = ( + } + title="Survey closed" + body="This survey is not open right now." + /> + ); + } else if (!eligible) { + content = ( } title="Not eligible" body="This survey is not available to your account." /> ); + } else { + content = ( + + + + + + ); } return ( -
-
-

- - Surveys - - {" / "} - Survey -

-

{form.title}

- {form.activeUntil && ( -

- Open until {new Date(form.activeUntil).toLocaleString()} -

- )} +
+
+ + + Surveys + +
+
+ +

{form.title}

+
+ {form.activeUntil && ( +

+ Open until {new Date(form.activeUntil).toLocaleString()} +

+ )} +
- + + {content}
); } diff --git a/src/app/(frontend)/surveys/page.tsx b/src/app/(frontend)/surveys/page.tsx index 4f9723e..aff9c72 100644 --- a/src/app/(frontend)/surveys/page.tsx +++ b/src/app/(frontend)/surveys/page.tsx @@ -3,22 +3,18 @@ import { headers as nextHeaders } from "next/headers"; import { getPayload } from "payload"; import { ClipboardCheck } from "lucide-react"; import { SurveyList, type SurveyListItem } from "@/components/frontend/surveys/SurveyList"; -import { isSurveyOpen, surveyAudienceMatches } from "@/lib/surveys/evaluate"; +import { getPendingSurveys } from "@/lib/surveys/pending"; import type { User } from "@/payload-types"; export const metadata = { title: "Surveys: Polaris Task Force", }; -function audienceContext(user: User) { - const roleIds = (user.roleDocs ?? []) - .map((r) => (typeof r === "object" ? r.id : r)) - .filter((id): id is number => typeof id === "number"); - - return { userId: user.id, roleIds }; -} - -export default async function SurveysPage() { +export default async function SurveysPage({ + searchParams, +}: { + searchParams: Promise<{ open?: string }>; +}) { const headers = await nextHeaders(); const payload = await getPayload({ config }); const { user } = await payload.auth({ headers, canSetHeaders: false }); @@ -27,47 +23,31 @@ export default async function SurveysPage() { // the authed render path's types. if (!user || user.collection !== "users") return null; - const [formsResult, profileRes] = await Promise.all([ - payload.find({ - collection: "forms", - sort: "-createdAt", - limit: 100, - depth: 1, - }), - payload.find({ - collection: "profiles", - where: { user: { equals: user.id } }, - limit: 1, - depth: 1, - }), - ]); + const [pendingForms, params] = await Promise.all([getPendingSurveys(payload, user), searchParams]); + const openParam = Number(params.open); + const initialSurveyId = Number.isInteger(openParam) ? openParam : null; - const heldQualifications = (profileRes.docs[0]?.progression?.qualifications ?? []) - .map((q) => (typeof q === "object" && q !== null && "name" in q ? (q.name as string) : typeof q === "string" ? q : null)) - .filter((n): n is string => n !== null); - - const ctx = { ...audienceContext(user), qualificationNames: heldQualifications }; - const now = Date.now(); - - const items: SurveyListItem[] = formsResult.docs - .filter((form) => isSurveyOpen(form, now) && surveyAudienceMatches(form, ctx)) - .map((form) => ({ - id: form.id, - title: form.title, - openUntil: form.activeUntil ?? null, - questionCount: form.fields?.filter((f) => f.blockType !== "message").length ?? 0, - })); + const items: SurveyListItem[] = pendingForms.map((form) => ({ + id: form.id, + title: form.title, + openUntil: form.activeUntil ?? null, + questionCount: form.fields?.filter((f) => f.blockType !== "message").length ?? 0, + fields: form.fields ?? [], + submitLabel: form.submitButtonLabel || "Submit", + confirmationMessage: form.confirmationMessage ?? null, + })); return ( -
-
- -
-

Surveys

-

Open surveys for you to take.

+
+
+
+ +

Surveys

+

Open surveys for you to take.

- + +
); } diff --git a/src/components/frontend/surveys/SurveyForm.tsx b/src/components/frontend/surveys/SurveyForm.tsx index 9f75af4..938855e 100644 --- a/src/components/frontend/surveys/SurveyForm.tsx +++ b/src/components/frontend/surveys/SurveyForm.tsx @@ -44,11 +44,13 @@ export function SurveyForm({ fields, submitLabel, confirmationMessage, + onSuccess, }: { formId: number; fields: SurveyField[]; submitLabel: string; confirmationMessage: ConfirmationMessage; + onSuccess?: () => void; }) { const [values, setValues] = useState>(() => initialValues(fields)); const [status, setStatus] = useState<"idle" | "submitting" | "success">("idle"); @@ -92,6 +94,7 @@ export function SurveyForm({ throw new Error(message || `Submission failed (${response.status}).`); } setStatus("success"); + onSuccess?.(); } catch (e) { setStatus("idle"); setError(e instanceof Error ? e.message : "Submission failed."); diff --git a/src/components/frontend/surveys/SurveyList.tsx b/src/components/frontend/surveys/SurveyList.tsx index 87a25b0..957dc78 100644 --- a/src/components/frontend/surveys/SurveyList.tsx +++ b/src/components/frontend/surveys/SurveyList.tsx @@ -1,17 +1,63 @@ -import Link from "next/link"; -import { ClipboardCheck, Clock, ListChecks } from "lucide-react"; -import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; +"use client"; + +import { useState } from "react"; +import { + ChevronRight, + ClipboardCheck, + Clock, + ListChecks, +} from "lucide-react"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import type { Form } from "@/payload-types"; +import { SurveyForm } from "@/components/frontend/surveys/SurveyForm"; export interface SurveyListItem { id: number; title: string; openUntil: string | null; questionCount: number; + fields: NonNullable; + submitLabel: string; + confirmationMessage: Form["confirmationMessage"]; } -export function SurveyList({ items }: { items: SurveyListItem[] }) { - if (items.length === 0) { +export function SurveyList({ + items, + initialSurveyId = null, +}: { + items: SurveyListItem[]; + initialSurveyId?: number | null; +}) { + const [active, setActive] = useState( + () => items.find((item) => item.id === initialSurveyId) ?? null, + ); + const [succeeded, setSucceeded] = useState(false); + const [completedIds, setCompletedIds] = useState>(() => new Set()); + + const visible = items.filter((item) => !completedIds.has(item.id)); + + const close = () => { + if (active && succeeded) { + setCompletedIds((prev) => new Set(prev).add(active.id)); + } + setSucceeded(false); + setActive(null); + }; + + if (visible.length === 0 && !active) { return (
@@ -21,14 +67,28 @@ export function SurveyList({ items }: { items: SurveyListItem[] }) { } return ( -
- {items.map((item) => ( - - + <> +
+ {visible.map((item) => ( + { + setSucceeded(false); + setActive(item); + }} + onKeyDown={(e) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + setSucceeded(false); + setActive(item); + } + }} + className="hover:border-sidebar-border hover:bg-sidebar-accent/40 h-full cursor-pointer transition-colors" + > - - {item.title} - + {item.title} @@ -37,19 +97,42 @@ export function SurveyList({ items }: { items: SurveyListItem[] }) { {item.openUntil && ( - until {new Date(item.openUntil).toLocaleDateString()} + closes {new Date(item.openUntil).toLocaleDateString()} )} - - - Open - + + Take survey + - - ))} -
+ ))} +
+ + !open && close()}> + {active && ( + + + {active.title} + {active.openUntil && ( + + Open until {new Date(active.openUntil).toLocaleString()} + + )} + +
+ setSucceeded(true)} + /> +
+
+ )} +
+ ); }