Files
erika/lib/form/form-client.ts
T

148 lines
4.1 KiB
TypeScript

import { z } from "zod";
import type { FormAnswerValue } from "@/lib/form/form-answer-values";
import { decodeCheckboxAnswer } from "@/lib/form/form-answer-values";
export type Question = {
id: string;
formId: string;
type: "text" | "textarea" | "radio" | "checkbox";
label: string;
imageUrl: string | null;
displayOrder: number;
required: boolean;
options: string[] | null;
allowOther: boolean;
imageAnswerMode: "none" | "optional" | "required";
};
export type AnswerDraft = {
value: FormAnswerValue;
imageCdnId: string | null;
};
export type FormFillerClientProps = {
form: {
id: string;
title: string;
description: string | null;
isOpen: boolean;
closesAt: string | null;
allowMultipleSubmissions: boolean;
};
questions: Question[];
initialAnswers?: Record<string, AnswerDraft>;
hasSubmission?: boolean;
selectedSubmissionId?: string | null;
ownSubmissions?: {
id: string;
submittedAt: string;
answers: Record<string, AnswerDraft>;
}[];
totalAnswers: number;
queueNumber: number | null;
preview?: boolean;
};
export type FormValues = Record<string, string | string[] | undefined>;
export type StoredDraft = {
version: 1;
answers: FormValues;
otherValues: Record<string, string>;
imageAnswers?: Record<string, string | null>;
};
export function buildFormSchema(questions: Question[]) {
const shape: Record<string, z.ZodTypeAny> = {};
questions.forEach((question) => {
if (question.type === "checkbox") {
shape[question.id] = question.required
? z.array(z.string()).min(1, "จำเป็นต้องตอบข้อนี้")
: z.array(z.string()).optional();
} else {
shape[question.id] = question.required
? z.string().trim().min(1, "จำเป็นต้องตอบข้อนี้")
: z.string().optional();
}
});
return z.object(shape);
}
export function prepareInitialState(
questions: Question[],
answers: Record<string, AnswerDraft | undefined>,
savedOtherValues: Record<string, string> = {}
) {
const values: FormValues = {};
const imageAnswers: Record<string, string | null> = {};
const otherValues = { ...savedOtherValues };
questions.forEach((question) => {
const answer = answers[question.id]?.value;
imageAnswers[question.id] = answers[question.id]?.imageCdnId ?? null;
if (question.type === "checkbox") {
let selected = decodeCheckboxAnswer(answer);
if (question.allowOther && question.options) {
const customAnswers = selected.filter(
(value) => !question.options?.includes(value) && value !== "__OTHER__"
);
if (customAnswers.length > 0) {
otherValues[question.id] = customAnswers.join(", ");
}
if (selected.includes("__OTHER__") || customAnswers.length > 0) {
selected = selected.filter((value) => question.options?.includes(value));
selected.push("__OTHER__");
}
}
values[question.id] = selected;
return;
}
if (question.type === "radio") {
let selected = typeof answer === "string" ? answer : "";
if (
question.allowOther &&
question.options &&
selected &&
!question.options.includes(selected)
) {
if (selected !== "__OTHER__") {
otherValues[question.id] = selected;
}
selected = "__OTHER__";
}
values[question.id] = selected;
return;
}
values[question.id] = typeof answer === "string" ? answer : "";
});
return { values, otherValues, imageAnswers };
}
export function parseStoredDraft(raw: string) {
const parsed: unknown = JSON.parse(raw);
if (
parsed &&
typeof parsed === "object" &&
"version" in parsed &&
parsed.version === 1 &&
"answers" in parsed
) {
const draft = parsed as StoredDraft;
return {
answers: draft.answers ?? {},
otherValues: draft.otherValues ?? {},
imageAnswers: draft.imageAnswers ?? {},
};
}
return {
answers: (parsed as FormValues) ?? {},
otherValues: {},
imageAnswers: {},
};
}