feat : update
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
import { db } from "@/db";
|
||||
import { Metadata } from "next";
|
||||
|
||||
export async function generateMetadata(
|
||||
props: { params: Promise<{ id: string }> }
|
||||
): Promise<Metadata> {
|
||||
const params = await props.params;
|
||||
|
||||
const form = await db.query.forms.findFirst({
|
||||
where: (f, { eq }) => eq(f.id, params.id),
|
||||
});
|
||||
|
||||
if (!form) {
|
||||
return {
|
||||
title: "Not Found",
|
||||
description: "Form not found",
|
||||
};
|
||||
}
|
||||
|
||||
const cleanDescription = form.description ? form.description.replace(/<[^>]*>?/gm, '') : "แบบฟอร์ม";
|
||||
|
||||
return {
|
||||
title: form.title,
|
||||
description: cleanDescription,
|
||||
openGraph: {
|
||||
title: form.title,
|
||||
description: cleanDescription,
|
||||
},
|
||||
twitter: {
|
||||
title: form.title,
|
||||
description: cleanDescription,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default function FormIdLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
return children;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useState } from "react";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import Background from "@/public/background/404.png";
|
||||
import Profile from "@/public/404-image.jpg";
|
||||
import { Activity, Home } from "lucide-react";
|
||||
import { config } from "@/lib/config";
|
||||
|
||||
const MESSAGES = [
|
||||
"ฟอร์มนี้อาจจะถูก Erika กินไปแล้ว...",
|
||||
"หรือว่าฟอร์มนี้โดน Sukuna เล่นไปแล้ว...",
|
||||
"ฟอร์มนี้อาจจะหมดอายุหรือโดนลบไปแล้ว...",
|
||||
"บางทีคนสร้างอาจจะลืมเปิดฟอร์มนะ...",
|
||||
"ฟอร์มหายไปไหนน้า... ลองเช็คลิงก์ดูอีกทีสิ",
|
||||
];
|
||||
|
||||
export default function NotFound() {
|
||||
const [goingHome, goHome] = useState(false);
|
||||
const [message] = useState(() => MESSAGES[Math.floor(Math.random() * MESSAGES.length)]);
|
||||
|
||||
return (
|
||||
<div className="relative flex min-h-screen w-full flex-col items-center justify-center gap-2">
|
||||
<Image
|
||||
src={Background}
|
||||
alt="404 Background"
|
||||
fill
|
||||
priority
|
||||
className="z-[-1] object-cover opacity-20 grayscale"
|
||||
/>
|
||||
<div className="border bg-card p-4 pb-12 shadow-lg max-sm:landscape:hidden">
|
||||
<Image
|
||||
src={Profile}
|
||||
alt="404"
|
||||
width={256}
|
||||
height={256}
|
||||
className="vintage-filter size-64 bg-muted"
|
||||
/>
|
||||
</div>
|
||||
<h2 className="text-2xl font-semibold">404 ไม่พบหน้านี้</h2>
|
||||
<span>{message}</span>
|
||||
<div className="flex gap-2">
|
||||
<Link href="/form">
|
||||
<Button
|
||||
onClick={() => {
|
||||
goHome(true);
|
||||
}}
|
||||
disabled={goingHome}
|
||||
variant="destructive"
|
||||
>
|
||||
{goingHome ? <Spinner /> : <Home />}
|
||||
กลับหน้าเลือก
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href={config.site.statusUrl}>
|
||||
<Button variant="link" className="text-secondary-foreground">
|
||||
<Activity />
|
||||
สถานะเว็บไซต์
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
import { db } from "@/db";
|
||||
import { FormFillerClient } from "../client";
|
||||
import { LoginDialog } from "@/components/login-dialog";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "@/lib/auth/auth-options";
|
||||
import { notFound } from "next/navigation";
|
||||
import { canAccessForm, getSessionDiscordId } from "@/lib/auth/auth";
|
||||
import { RealtimeRefresh } from "@/components/realtime-refresh";
|
||||
import {
|
||||
decodeCheckboxAnswer,
|
||||
type FormAnswerValue,
|
||||
} from "@/lib/form/form-answer-values";
|
||||
import { getFormSubmissionStats } from "@/lib/form/form-submission-stats";
|
||||
import { isFormAcceptingSubmissions } from "@/lib/form/form-availability";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function FormsSubmitPage(
|
||||
props: {
|
||||
params: Promise<{ id: string }>;
|
||||
searchParams: Promise<{ submission?: string | string[] }>;
|
||||
}
|
||||
) {
|
||||
const [params, query] = await Promise.all([props.params, props.searchParams]);
|
||||
const session = await getServerSession(authOptions);
|
||||
const discordId = getSessionDiscordId(session);
|
||||
|
||||
const form = await db.query.forms.findFirst({
|
||||
where: (f, { eq }) => eq(f.id, params.id),
|
||||
});
|
||||
|
||||
if (!form) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
if (!discordId) {
|
||||
return <LoginDialog />;
|
||||
}
|
||||
|
||||
if (!(await canAccessForm(form, discordId))) {
|
||||
return notFound();
|
||||
}
|
||||
|
||||
const questionsList = await db.query.questions.findMany({
|
||||
where: (q, { eq }) => eq(q.formId, form.id),
|
||||
orderBy: (q, { asc }) => [asc(q.displayOrder)],
|
||||
});
|
||||
|
||||
const [ownSubmissions, submissionStats] = await Promise.all([
|
||||
db.query.submissions.findMany({
|
||||
where: (s, { eq, and }) =>
|
||||
and(eq(s.formId, form.id), eq(s.userDiscordId, discordId)),
|
||||
orderBy: (s, { desc }) => [desc(s.submittedAt)],
|
||||
columns: {
|
||||
id: true,
|
||||
submittedAt: true,
|
||||
},
|
||||
}),
|
||||
getFormSubmissionStats([form.id], discordId),
|
||||
]);
|
||||
const stats = submissionStats.get(form.id) ?? {
|
||||
totalAnswers: 0,
|
||||
queueNumber: null,
|
||||
};
|
||||
|
||||
const requestedSubmissionId = Array.isArray(query.submission)
|
||||
? query.submission[0]
|
||||
: query.submission;
|
||||
const existingSubmission = form.allowMultipleSubmissions
|
||||
? ownSubmissions.find((submission) => submission.id === requestedSubmissionId) ?? null
|
||||
: ownSubmissions[0] ?? null;
|
||||
|
||||
const answersBySubmissionId: Record<
|
||||
string,
|
||||
Record<string, { value: FormAnswerValue; imageCdnId: string | null }>
|
||||
> = {};
|
||||
if (ownSubmissions.length > 0) {
|
||||
const answersList = await db.query.answers.findMany({
|
||||
where: (a, { inArray }) => inArray(a.submissionId, ownSubmissions.map((submission) => submission.id)),
|
||||
});
|
||||
const questionTypes = new Map(questionsList.map((question) => [question.id, question.type]));
|
||||
for (const answer of answersList) {
|
||||
answersBySubmissionId[answer.submissionId] ??= {};
|
||||
answersBySubmissionId[answer.submissionId][answer.questionId] = {
|
||||
value:
|
||||
questionTypes.get(answer.questionId) === "checkbox"
|
||||
? decodeCheckboxAnswer(answer.value)
|
||||
: answer.value,
|
||||
imageCdnId: answer.imageCdnId ?? null,
|
||||
};
|
||||
}
|
||||
}
|
||||
const initialAnswers = existingSubmission
|
||||
? answersBySubmissionId[existingSubmission.id] ?? {}
|
||||
: {};
|
||||
|
||||
return (
|
||||
<>
|
||||
<RealtimeRefresh topic="forms" formId={form.id} />
|
||||
<div className="mx-auto max-w-2xl">
|
||||
<FormFillerClient
|
||||
form={{
|
||||
...form,
|
||||
isOpen: isFormAcceptingSubmissions(form),
|
||||
closesAt: form.closesAt?.toISOString() ?? null,
|
||||
}}
|
||||
questions={questionsList}
|
||||
initialAnswers={initialAnswers}
|
||||
hasSubmission={Boolean(existingSubmission)}
|
||||
selectedSubmissionId={existingSubmission?.id ?? null}
|
||||
ownSubmissions={ownSubmissions.map((submission) => ({
|
||||
id: submission.id,
|
||||
submittedAt: submission.submittedAt.toISOString(),
|
||||
answers: answersBySubmissionId[submission.id] ?? {},
|
||||
}))}
|
||||
totalAnswers={stats.totalAnswers}
|
||||
queueNumber={stats.queueNumber}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user