diff --git a/.gitignore b/.gitignore index 019d9c0..ec22680 100644 --- a/.gitignore +++ b/.gitignore @@ -46,3 +46,7 @@ drizzle/* !drizzle/0012_clammy_the_stranger.sql !drizzle/0013_regular_unus.sql !drizzle/0014_lethal_pepper_potts.sql +!drizzle/0015_form_closes_at.sql +!drizzle/meta/ +drizzle/meta/* +!drizzle/meta/0015_snapshot.json diff --git a/app/actions/submissions.ts b/app/actions/submissions.ts index 0ceee68..17c2008 100644 --- a/app/actions/submissions.ts +++ b/app/actions/submissions.ts @@ -14,6 +14,7 @@ import { type StoredAnswerSnapshot, } from "@/lib/submission-intake"; import { submissionWebhookMessage } from "@/lib/submission-webhook"; +import { isFormAcceptingSubmissions } from "@/lib/form-availability"; function getSubmissionResultLink(formId: string, submissionId: string) { const baseUrl = process.env.BASE_URL || process.env.NEXTAUTH_URL; @@ -32,7 +33,9 @@ export async function submitForm( const discordId = await requireDiscordId(); const userName = session.user.name ?? null; const form = await requireFormAccess(formId, discordId); - if (!form.isOpen) throw new Error("This form is disabled for now"); + if (!isFormAcceptingSubmissions(form)) { + throw new Error("This form is closed for submissions"); + } const questionsList = await db.query.questions.findMany({ where: (question, { eq }) => eq(question.formId, formId), diff --git a/app/admin/form/[id]/client.tsx b/app/admin/form/[id]/client.tsx index de395ac..5672f37 100644 --- a/app/admin/form/[id]/client.tsx +++ b/app/admin/form/[id]/client.tsx @@ -20,6 +20,8 @@ import { } from "@/app/actions/questions"; import { Plus, Trash, ArrowUp, ArrowDown, Loader2, Image as ImageIcon, X, Check, GripVertical, AlertCircle } from "lucide-react"; import Image from 'next/image'; +import { format, parseISO } from "date-fns"; +import { DateTimePicker } from "@/components/date-time-picker"; import { RichTextEditor } from "@/components/rich-text-editor"; import { getErrorMessage } from "@/lib/utils"; import { @@ -66,6 +68,8 @@ import { FieldDescription, FieldGroup, FieldLabel, + FieldLegend, + FieldSet, } from "@/components/ui/field"; interface Question { @@ -87,16 +91,28 @@ interface FormEditorClientProps { title: string; description: string | null; isOpen: boolean; + closesAt: string | null; allowMultipleSubmissions: boolean; }; initialQuestions: Question[]; } +function toDateTimeLocalValue(value: string | null) { + if (!value) return ""; + + const date = new Date(value); + const localDate = new Date(date.getTime() - date.getTimezoneOffset() * 60_000); + return localDate.toISOString().slice(0, 16); +} + export default function FormEditorClient({ form, initialQuestions }: FormEditorClientProps) { const router = useRouter(); const [formTitle, setFormTitle] = useState(form.title); const [formDescription, setFormDescription] = useState(form.description || ""); const [formIsOpen, setFormIsOpen] = useState(form.isOpen); + const [closesAt, setClosesAt] = useState( + toDateTimeLocalValue(form.closesAt) + ); const [allowMultipleSubmissions, setAllowMultipleSubmissions] = useState( form.allowMultipleSubmissions ); @@ -108,6 +124,7 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC const formTitleRef = useRef(formTitle); const formDescriptionRef = useRef(formDescription); const formIsOpenRef = useRef(formIsOpen); + const closesAtRef = useRef(closesAt); const allowMultipleSubmissionsRef = useRef(allowMultipleSubmissions); const questionsRef = useRef(questions); @@ -115,9 +132,10 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC formTitleRef.current = formTitle; formDescriptionRef.current = formDescription; formIsOpenRef.current = formIsOpen; + closesAtRef.current = closesAt; allowMultipleSubmissionsRef.current = allowMultipleSubmissions; questionsRef.current = questions; - }, [allowMultipleSubmissions, formDescription, formIsOpen, formTitle, questions]); + }, [allowMultipleSubmissions, closesAt, formDescription, formIsOpen, formTitle, questions]); const saveForm = useCallback(async () => { const title = formTitleRef.current; @@ -128,6 +146,9 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC title: title.trim(), description: formDescriptionRef.current, isOpen: formIsOpenRef.current, + closesAt: closesAtRef.current + ? new Date(closesAtRef.current).toISOString() + : null, allowMultipleSubmissions: allowMultipleSubmissionsRef.current, }); router.refresh(); @@ -169,6 +190,14 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC formAutoSave.schedule(); }; + const handleClosesAtChange = (val: string) => { + closesAtRef.current = val; + setClosesAt(val); + formAutoSave.schedule(); + }; + + const selectedCloseDateTime = closesAt ? parseISO(closesAt) : undefined; + const handleAllowMultipleSubmissionsChange = (val: boolean) => { allowMultipleSubmissionsRef.current = val; setAllowMultipleSubmissions(val); @@ -389,6 +418,27 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC Accepting Submissions +
{preview ? "ตัวอย่างสำหรับผู้ดูแลระบบ — ไม่มีการส่งคำตอบ" - : !form.isOpen + : !isAcceptingSubmissions ? "ปิดรับคำตอบแล้ว" : draftSaved ? "บันทึกฉบับร่างไว้ในอุปกรณ์นี้แล้ว" @@ -1061,7 +1096,7 @@ export function FormFillerClient({ )} - {form.isOpen && ( + {isAcceptingSubmissions && (