refactor(lib): organize modules by feature
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
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: {},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user