From 3a257e4758c491c5249485d519141b3456357582 Mon Sep 17 00:00:00 2001 From: gunshiz Date: Mon, 29 Jun 2026 18:41:53 +0700 Subject: [PATCH] refactor: migrate administrative form management to a centralized modular routing structure --- app/actions/submissions.ts | 6 +- .../admin => admin/form/[id]}/client.tsx | 24 +- .../form/[id]}/extra/client.tsx | 0 .../admin => admin/form/[id]}/extra/page.tsx | 23 +- .../admin => admin/form/[id]}/layout.tsx | 16 +- app/{form/admin => admin/form/[id]}/page.tsx | 24 +- .../[id]/result/[submissionId]}/client.tsx | 0 .../form/[id]/result/[submissionId]}/page.tsx | 22 +- .../admin => admin/form/[id]}/result/page.tsx | 26 +- app/admin/form/page.tsx | 95 +++ app/form/[id]/page.tsx | 57 ++ app/form/client.tsx | 8 +- app/form/page.tsx | 80 ++- app/minecraft/client.tsx | 12 - app/minecraft/layout.tsx | 32 - app/minecraft/page.tsx | 56 -- bun.lock | 3 + components/delete-buttons.tsx | 4 +- db/schema/form.ts | 21 +- drizzle.config.ts | 2 +- drizzle/0007_quiet_shooting_star.sql | 49 ++ drizzle/0008_broken_valeria_richards.sql | 5 + drizzle/0009_slippery_nomad.sql | 45 ++ drizzle/meta/0007_snapshot.json | 549 ++++++++++++++++++ drizzle/meta/0008_snapshot.json | 265 +++++++++ drizzle/meta/0009_snapshot.json | 549 ++++++++++++++++++ drizzle/meta/_journal.json | 21 + package.json | 1 + 28 files changed, 1774 insertions(+), 221 deletions(-) rename app/{form/admin => admin/form/[id]}/client.tsx (97%) rename app/{form/admin => admin/form/[id]}/extra/client.tsx (100%) rename app/{form/admin => admin/form/[id]}/extra/page.tsx (61%) rename app/{form/admin => admin/form/[id]}/layout.tsx (79%) rename app/{form/admin => admin/form/[id]}/page.tsx (64%) rename app/{form/admin/result/[id] => admin/form/[id]/result/[submissionId]}/client.tsx (100%) rename app/{form/admin/result/[id] => admin/form/[id]/result/[submissionId]}/page.tsx (81%) rename app/{form/admin => admin/form/[id]}/result/page.tsx (78%) create mode 100644 app/admin/form/page.tsx create mode 100644 app/form/[id]/page.tsx delete mode 100644 app/minecraft/client.tsx delete mode 100644 app/minecraft/layout.tsx delete mode 100644 app/minecraft/page.tsx create mode 100644 drizzle/0007_quiet_shooting_star.sql create mode 100644 drizzle/0008_broken_valeria_richards.sql create mode 100644 drizzle/0009_slippery_nomad.sql create mode 100644 drizzle/meta/0007_snapshot.json create mode 100644 drizzle/meta/0008_snapshot.json create mode 100644 drizzle/meta/0009_snapshot.json diff --git a/app/actions/submissions.ts b/app/actions/submissions.ts index aea7c2a..35c6993 100644 --- a/app/actions/submissions.ts +++ b/app/actions/submissions.ts @@ -145,12 +145,12 @@ export async function deleteSubmission(id: string) { return { ok: true }; } -export async function deleteAllSubmissions() { +export async function deleteAllSubmissions(formId: string) { const session = await getServerSession(authOptions); if (!session?.user) throw new Error("Not authenticated"); - await db.delete(submissions); - revalidatePath("/form/admin/result"); + await db.delete(submissions).where(eq(submissions.formId, formId)); + revalidatePath(`/admin/form/${formId}/result`); return { ok: true }; } diff --git a/app/form/admin/client.tsx b/app/admin/form/[id]/client.tsx similarity index 97% rename from app/form/admin/client.tsx rename to app/admin/form/[id]/client.tsx index 5d89a8c..0d14c72 100644 --- a/app/form/admin/client.tsx +++ b/app/admin/form/[id]/client.tsx @@ -609,14 +609,14 @@ function AnswerPreview({ question }: { question: Question }) { ); } -export function AdminSidebar() { +export function AdminSidebar({ formId }: { formId: string }) { const pathname = usePathname(); return ( - Form admin + Form Admin @@ -626,26 +626,26 @@ export function AdminSidebar() { - - + + - Dashboard + Editor - - + + - Response + Responses - - + + Extra @@ -660,9 +660,9 @@ export function AdminSidebar() { - + - Go public + All Forms diff --git a/app/form/admin/extra/client.tsx b/app/admin/form/[id]/extra/client.tsx similarity index 100% rename from app/form/admin/extra/client.tsx rename to app/admin/form/[id]/extra/client.tsx diff --git a/app/form/admin/extra/page.tsx b/app/admin/form/[id]/extra/page.tsx similarity index 61% rename from app/form/admin/extra/page.tsx rename to app/admin/form/[id]/extra/page.tsx index bce0cb2..b9086ff 100644 --- a/app/form/admin/extra/page.tsx +++ b/app/admin/form/[id]/extra/page.tsx @@ -1,20 +1,21 @@ import { db } from "@/db"; -import { forms } from "@/db/schema"; +import { notFound } from "next/navigation"; import FormExtraClient from "./client"; export const dynamic = "force-dynamic"; -export default async function AdminExtraPage() { - let form = await db.query.forms.findFirst(); +export default async function AdminExtraPage( + props: { + params: Promise<{ id: string }>; + } +) { + const params = await props.params; + const form = await db.query.forms.findFirst({ + where: (f, { eq }) => eq(f.id, params.id), + }); + if (!form) { - const [newForm] = await db - .insert(forms) - .values({ - title: "Erika Custom Form", - description: "Welcome! Please fill out the form below.", - }) - .returning(); - form = newForm; + return notFound(); } return ( diff --git a/app/form/admin/layout.tsx b/app/admin/form/[id]/layout.tsx similarity index 79% rename from app/form/admin/layout.tsx rename to app/admin/form/[id]/layout.tsx index b53b6da..c77c551 100644 --- a/app/form/admin/layout.tsx +++ b/app/admin/form/[id]/layout.tsx @@ -12,11 +12,13 @@ export const metadata: Metadata = { }, } -export default async function AdminLayout({ - children, -}: { - children: React.ReactNode; -}) { +export default async function AdminFormLayout( + props: { + children: React.ReactNode; + params: Promise<{ id: string }>; + } +) { + const params = await props.params; const session = await getServerSession(authOptions); const adminIds = (process.env.ADMIN_DISCORD_IDS ?? "") .split(",") @@ -31,9 +33,9 @@ export default async function AdminLayout({ return (
- +
- {children} + {props.children}
diff --git a/app/form/admin/page.tsx b/app/admin/form/[id]/page.tsx similarity index 64% rename from app/form/admin/page.tsx rename to app/admin/form/[id]/page.tsx index 214ef53..6763b02 100644 --- a/app/form/admin/page.tsx +++ b/app/admin/form/[id]/page.tsx @@ -1,20 +1,22 @@ import { db } from "@/db"; import { forms } from "@/db/schema"; import FormEditorClient from "./client"; +import { notFound } from "next/navigation"; export const dynamic = "force-dynamic"; -export default async function AdminDashboardPage() { - let form = await db.query.forms.findFirst(); +export default async function AdminDashboardPage( + props: { + params: Promise<{ id: string }>; + } +) { + const params = await props.params; + const form = await db.query.forms.findFirst({ + where: (f, { eq }) => eq(f.id, params.id), + }); + if (!form) { - const [newForm] = await db - .insert(forms) - .values({ - title: "Erika Custom Form", - description: "Welcome! Please fill out the form below.", - }) - .returning(); - form = newForm; + return notFound(); } const questionsList = await db.query.questions.findMany({ @@ -26,7 +28,7 @@ export default async function AdminDashboardPage() {
-

Admin Dashboard

+

Edit Form

Customize form settings, add/edit questions, or upload images.

diff --git a/app/form/admin/result/[id]/client.tsx b/app/admin/form/[id]/result/[submissionId]/client.tsx similarity index 100% rename from app/form/admin/result/[id]/client.tsx rename to app/admin/form/[id]/result/[submissionId]/client.tsx diff --git a/app/form/admin/result/[id]/page.tsx b/app/admin/form/[id]/result/[submissionId]/page.tsx similarity index 81% rename from app/form/admin/result/[id]/page.tsx rename to app/admin/form/[id]/result/[submissionId]/page.tsx index b17d609..78713c2 100644 --- a/app/form/admin/result/[id]/page.tsx +++ b/app/admin/form/[id]/result/[submissionId]/page.tsx @@ -9,14 +9,15 @@ import { DeleteSubmissionButton } from "@/components/delete-buttons"; interface SubmissionDetailPageProps { params: Promise<{ id: string; + submissionId: string; }>; } export default async function SubmissionDetailPage({ params }: SubmissionDetailPageProps) { - const { id } = await params; + const { id: formId, submissionId } = await params; const submission = await db.query.submissions.findFirst({ - where: (s, { eq }) => eq(s.id, id), + where: (s, { eq }) => eq(s.id, submissionId), }); if (!submission) { @@ -37,18 +38,19 @@ export default async function SubmissionDetailPage({ params }: SubmissionDetailP answersMap[ans.questionId] = ans.value; } const submissionsList = await db.query.submissions.findMany({ + where: (s, { eq }) => eq(s.formId, formId), orderBy: (s, { desc }) => [desc(s.submittedAt)], }); - const currentIndex = submissionsList.findIndex((s) => s.id === id); + const currentIndex = submissionsList.findIndex((s) => s.id === submissionId); const prevId = currentIndex > 0 ? submissionsList[currentIndex - 1].id : null; const nextId = currentIndex >= 0 && currentIndex < submissionsList.length - 1 ? submissionsList[currentIndex + 1].id : null; return ( -
+
- + @@ -57,16 +59,16 @@ export default async function SubmissionDetailPage({ params }: SubmissionDetailP
{prevId && ( - + )} {nextId && ( - + )} - +
@@ -78,7 +80,7 @@ export default async function SubmissionDetailPage({ params }: SubmissionDetailP
{prevId ? ( - + @@ -87,7 +89,7 @@ export default async function SubmissionDetailPage({ params }: SubmissionDetailP
)} {nextId && ( - + diff --git a/app/form/admin/result/page.tsx b/app/admin/form/[id]/result/page.tsx similarity index 78% rename from app/form/admin/result/page.tsx rename to app/admin/form/[id]/result/page.tsx index c603bb8..620b731 100644 --- a/app/form/admin/result/page.tsx +++ b/app/admin/form/[id]/result/page.tsx @@ -3,18 +3,32 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"; import { Button } from "@/components/ui/button"; import Link from "next/link"; -import { Users } from "lucide-react"; +import { Users, ArrowLeft } from "lucide-react"; import { DeleteSubmissionButton, DeleteAllSubmissionsButton } from "@/components/delete-buttons"; export const dynamic = "force-dynamic"; -export default async function ResultsListPage() { +export default async function ResultsListPage( + props: { + params: Promise<{ id: string }>; + } +) { + const params = await props.params; const submissionsList = await db.query.submissions.findMany({ + where: (s, { eq }) => eq(s.formId, params.id), orderBy: (s, { desc }) => [desc(s.submittedAt)], }); return ( -
+
+
+ +

Form Submissions

@@ -22,7 +36,7 @@ export default async function ResultsListPage() { Review all submissions and individual user answers.

- {submissionsList.length > 0 && } + {submissionsList.length > 0 && }
{submissionsList.length === 0 ? ( @@ -60,13 +74,13 @@ export default async function ResultsListPage() { return ( - + {userLabel} - + {new Date(sub.submittedAt).toLocaleString()} diff --git a/app/admin/form/page.tsx b/app/admin/form/page.tsx new file mode 100644 index 0000000..1c77dfc --- /dev/null +++ b/app/admin/form/page.tsx @@ -0,0 +1,95 @@ +import { db } from "@/db"; +import { forms } from "@/db/schema"; +import Link from "next/link"; +import { Button } from "@/components/ui/button"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { Badge } from "@/components/ui/badge"; +import { Plus } from "lucide-react"; +import { redirect } from "next/navigation"; + +export const dynamic = "force-dynamic"; + +export default async function AdminFormsPage() { + const formsList = await db.query.forms.findMany({ + orderBy: (f, { desc }) => [desc(f.createdAt)], + }); + + return ( +
+
+
+

Forms

+

+ Manage your custom forms and view submissions. +

+
+
{ + "use server"; + const [newForm] = await db.insert(forms).values({ + title: "New Form", + description: "", + }).returning(); + redirect(`/admin/form/${newForm.id}`); + }}> + +
+
+ +
+ + + + Title + Status + Created + Actions + + + + {formsList.length === 0 ? ( + + + No forms found. Create one to get started. + + + ) : ( + formsList.map((form) => ( + + {form.title} + + {form.isOpen ? ( + Open + ) : ( + Closed + )} + + + {form.createdAt ? new Date(form.createdAt).toLocaleDateString() : 'N/A'} + + + + + + + )) + )} + +
+
+
+ ); +} diff --git a/app/form/[id]/page.tsx b/app/form/[id]/page.tsx new file mode 100644 index 0000000..79fa53b --- /dev/null +++ b/app/form/[id]/page.tsx @@ -0,0 +1,57 @@ +import { db } from "@/db"; +import { FormFillerClient } from "../client"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/app/api/auth/[...nextauth]/route"; +import { notFound } from "next/navigation"; + +export const dynamic = "force-dynamic"; + +export default async function FormsSubmitPage( + props: { + params: Promise<{ id: string }>; + } +) { + const params = await props.params; + const session = await getServerSession(authOptions); + const discordId = (session?.user as { discordId?: string })?.discordId; + + const form = await db.query.forms.findFirst({ + where: (f, { eq }) => eq(f.id, params.id), + }); + + if (!form) { + return notFound(); + } + + const questionsList = await db.query.questions.findMany({ + where: (q, { eq }) => eq(q.formId, form.id), + orderBy: (q, { asc }) => [asc(q.displayOrder)], + }); + + let initialAnswers: Record = {}; + if (discordId) { + const existingSubmission = await db.query.submissions.findFirst({ + where: (s, { eq, and }) => + and(eq(s.formId, form.id), eq(s.userDiscordId, discordId)), + }); + + if (existingSubmission) { + const answersList = await db.query.answers.findMany({ + where: (a, { eq }) => eq(a.submissionId, existingSubmission.id), + }); + for (const a of answersList) { + initialAnswers[a.questionId] = a.value; + } + } + } + + return ( +
+ +
+ ); +} diff --git a/app/form/client.tsx b/app/form/client.tsx index c7fe5af..f133e39 100644 --- a/app/form/client.tsx +++ b/app/form/client.tsx @@ -40,15 +40,19 @@ export function FormLayoutClient({ userName: string; userImage: string; }) { + const [imgError, setImgError] = useState(false); + return (
- {userImage && ( + {userImage && !imgError && ( {userName} setImgError(true)} + unoptimized /> )} {userName} diff --git a/app/form/page.tsx b/app/form/page.tsx index ac55959..4016b0b 100644 --- a/app/form/page.tsx +++ b/app/form/page.tsx @@ -1,57 +1,43 @@ import { db } from "@/db"; -import { forms } from "@/db/schema"; -import { FormFillerClient } from "./client"; -import { getServerSession } from "next-auth"; -import { authOptions } from "@/app/api/auth/[...nextauth]/route"; +import Link from "next/link"; +import { + Card, + CardHeader, + CardTitle, + CardDescription, +} from "@/components/ui/card"; export const dynamic = "force-dynamic"; -export default async function FormsSubmitPage() { - const session = await getServerSession(authOptions); - const discordId = (session?.user as { discordId?: string })?.discordId; - - // Automatically retrieve the first form, or create a default one - let form = await db.query.forms.findFirst(); - if (!form) { - const [newForm] = await db - .insert(forms) - .values({ - title: "Erika Custom Form", - description: "Welcome! Please fill out the form below.", - }) - .returning(); - form = newForm; - } - - const questionsList = await db.query.questions.findMany({ - where: (q, { eq }) => eq(q.formId, form.id), - orderBy: (q, { asc }) => [asc(q.displayOrder)], +export default async function FormsListingPage() { + const formsList = await db.query.forms.findMany({ + where: (f, { eq }) => eq(f.isOpen, true), + orderBy: (f, { desc }) => [desc(f.createdAt)], }); - let initialAnswers: Record = {}; - if (discordId) { - const existingSubmission = await db.query.submissions.findFirst({ - where: (s, { eq, and }) => - and(eq(s.formId, form.id), eq(s.userDiscordId, discordId)), - }); - - if (existingSubmission) { - const answersList = await db.query.answers.findMany({ - where: (a, { eq }) => eq(a.submissionId, existingSubmission.id), - }); - for (const a of answersList) { - initialAnswers[a.questionId] = a.value; - } - } - } - return ( -
- +
+

Available Forms

+ {formsList.length === 0 ? ( +

There are currently no open forms available.

+ ) : ( +
+ {formsList.map((form) => ( + + + + {form.title} + {form.description && ( + + {form.description} + + )} + + + + ))} +
+ )}
); } diff --git a/app/minecraft/client.tsx b/app/minecraft/client.tsx deleted file mode 100644 index 20093a0..0000000 --- a/app/minecraft/client.tsx +++ /dev/null @@ -1,12 +0,0 @@ -"use client"; - -export function MapIframe() { - return ( -