diff --git a/app/admin/form/[id]/client.tsx b/app/admin/form/[id]/client.tsx
index 5672f37..5d36327 100644
--- a/app/admin/form/[id]/client.tsx
+++ b/app/admin/form/[id]/client.tsx
@@ -71,6 +71,11 @@ import {
FieldLegend,
FieldSet,
} from "@/components/ui/field";
+import {
+ getQuestionResponseSettings,
+ getQuestionResponseType,
+ type QuestionResponseType,
+} from "@/lib/question-response-type";
interface Question {
id: string;
@@ -241,6 +246,16 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
questionsAutoSave.schedule();
};
+ const handleResponseTypeChange = (
+ question: Question,
+ responseType: QuestionResponseType,
+ ) => {
+ handleUpdateQuestion(
+ question.id,
+ getQuestionResponseSettings(responseType, question.required),
+ );
+ };
+
const handleDeleteQuestion = async (id: string) => {
try {
await deleteQuestion(id, form.id);
@@ -504,7 +519,7 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
Question {index + 1} · {questionTypeLabel(q.type)}
- {q.required ? " · Required" : ""}
+ {q.required || q.imageAnswerMode === "required" ? " · Required" : ""}
@@ -639,29 +654,12 @@ export default function FormEditorClient({ form, initialQuestions }: FormEditorC
/>
)}
-
-
- handleUpdateQuestion(q.id, { required: checked })
- }
- />
-
-
-
- Answer image
+
+ Response type
+
+ “Image only” hides the normal answer field. “Answer + required image” requires both.
+
+ {(q.imageAnswerMode === "none" || q.imageAnswerMode === "optional") && (
+
+
+ handleUpdateQuestion(q.id, { required: checked })
+ }
+ />
+
+
+ Require answer
+
+
+ Users must complete the normal answer field.
+
+
+
+ )}
{(q.type === "radio" || q.type === "checkbox") && (
diff --git a/app/admin/form/[id]/result/[submissionId]/client.tsx b/app/admin/form/[id]/result/[submissionId]/client.tsx
index 53f427a..d93fe66 100644
--- a/app/admin/form/[id]/result/[submissionId]/client.tsx
+++ b/app/admin/form/[id]/result/[submissionId]/client.tsx
@@ -157,15 +157,11 @@ export default function SubmissionDetailClient({
- Admin note
-
- Private context for administrators. Respondents cannot see this note.
-
+ Note
- Note
- Autosaves after you stop typing. Only administrators can view
- this note.
+ Autosaves after you stop typing.
- {wasEdited && (
-
- Current Answer:
-
- )}
-
+
{answerValue}
diff --git a/app/admin/form/[id]/result/page.tsx b/app/admin/form/[id]/result/page.tsx
index 3112249..865778e 100644
--- a/app/admin/form/[id]/result/page.tsx
+++ b/app/admin/form/[id]/result/page.tsx
@@ -142,7 +142,7 @@ export default async function ResultsListPage({
Respondent
Submitted
Status
- Admin note
+ Note
Actions
@@ -237,7 +237,7 @@ export default async function ResultsListPage({
- Admin note
+ Note
{submission.adminNote ? (
diff --git a/app/form/[id]/not-found.tsx b/app/form/[id]/not-found.tsx
index ea301f2..4f1716a 100644
--- a/app/form/[id]/not-found.tsx
+++ b/app/form/[id]/not-found.tsx
@@ -42,7 +42,7 @@ export default function NotFound() {
404 ไม่พบหน้านี้
{message}
-
+
diff --git a/app/form/client.tsx b/app/form/client.tsx
index d02d62b..6c48c3e 100644
--- a/app/form/client.tsx
+++ b/app/form/client.tsx
@@ -55,6 +55,7 @@ import {
decodeCheckboxAnswer,
type FormAnswerValue,
} from "@/lib/form-answer-values";
+import { isImageOnlyResponse } from "@/lib/question-response-type";
export function FormLayoutClient({
userName,
@@ -148,14 +149,6 @@ const buildSchema = (questions: Question[]) => {
return z.object(shape);
};
-function isImageOnlyQuestion(question: Question) {
- return (
- question.imageAnswerMode === "required" &&
- !question.required &&
- (question.type === "text" || question.type === "textarea")
- );
-}
-
function prepareInitialState(
questions: Question[],
answers: Record
,
@@ -445,6 +438,7 @@ export function FormFillerClient({
const formData = new FormData();
formData.append("file", file);
setUploadingAnswerId(questionId);
+ const uploadToastId = toast.loading("กำลังอัปโหลดรูป");
try {
const response = await fetch("/api/upload", {
@@ -457,9 +451,11 @@ export function FormFillerClient({
}
setImageAnswers((current) => ({ ...current, [questionId]: data.id }));
setImageErrors((current) => ({ ...current, [questionId]: null }));
- toast.success("อัปโหลดรูปแล้ว");
+ toast.success("อัปโหลดรูปแล้ว", { id: uploadToastId });
} catch (error) {
- toast.error(getErrorMessage(error, "ไม่สามารถอัปโหลดรูปได้"));
+ toast.error(getErrorMessage(error, "ไม่สามารถอัปโหลดรูปได้"), {
+ id: uploadToastId,
+ });
} finally {
setUploadingAnswerId(null);
}
@@ -576,7 +572,7 @@ export function FormFillerClient({
{questions.length} คำถาม
{totalAnswers} คำตอบทั้งหมด
{queueNumber !== null && (
- ลำดับของคุณ #{queueNumber}
+ ลำดับของคุณ #{queueNumber + 1}
)}
@@ -597,9 +593,11 @@ export function FormFillerClient({
)}
- {questions.some((question) => question.required) && (
+ {questions.some(
+ (question) => question.required || question.imageAnswerMode === "required",
+ ) && (
- ช่องที่มีป้าย “จำเป็น” ต้องตอบให้ครบ
+ คำถามที่มี “จำเป็น” ต้องตอบให้ครบ
)}
{!isAcceptingSubmissions && (
@@ -665,10 +663,10 @@ export function FormFillerClient({
control={control}
name={q.id}
render={({ field, fieldState }) => (
-
+
- ข้อ {questionIndex + 1} จาก {questions.length}
+ ข้อที่ {questionIndex + 1}
- {q.required && (
+ {(q.required || q.imageAnswerMode === "required") && (
จำเป็น
)}
{q.imageUrl && (
@@ -721,7 +719,7 @@ export function FormFillerClient({
- {q.type === "text" && !isImageOnlyQuestion(q) && (
+ {q.type === "text" && !isImageOnlyResponse(q) && (
)}
- {q.type === "textarea" && !isImageOnlyQuestion(q) && (
+ {q.type === "textarea" && !isImageOnlyResponse(q) && (
)}
- {q.type === "radio" && q.options && (
+ {q.type === "radio" && q.options && !isImageOnlyResponse(q) && (
)}
- {q.type === "checkbox" && q.options && (
+ {q.type === "checkbox" && q.options && !isImageOnlyResponse(q) && (