Files
erika/app/form/form-filler-client.tsx
T

242 lines
8.0 KiB
TypeScript

"use client";
import React, { useState } from "react";
import { useRouter } from "next/navigation";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
import { toast } from "sonner";
import { submitForm } from "@/app/actions/submissions";
import { Loader2 } from "lucide-react";
interface Question {
id: string;
formId: string;
type: "text" | "textarea" | "radio" | "checkbox";
label: string;
imageUrl: string | null;
displayOrder: number;
required: boolean;
options: string[] | null;
}
interface FormFillerClientProps {
form: {
id: string;
title: string;
description: string | null;
};
questions: Question[];
initialAnswers?: Record<string, string>;
}
export default function FormFillerClient({ form, questions, initialAnswers = {} }: FormFillerClientProps) {
const router = useRouter();
// Pre-process checkbox answers since they are stored as comma-separated strings
const initialCheckboxes: Record<string, string[]> = {};
for (const q of questions) {
if (q.type === "checkbox" && initialAnswers[q.id]) {
initialCheckboxes[q.id] = initialAnswers[q.id].split(", ");
}
}
const [answers, setAnswers] = useState<Record<string, string>>(initialAnswers);
const [checkboxAnswers, setCheckboxAnswers] = useState<Record<string, string[]>>(initialCheckboxes);
const [submitting, setSubmitting] = useState(false);
const isUpdating = Object.keys(initialAnswers).length > 0;
const handleTextChange = (questionId: string, value: string) => {
setAnswers((prev) => ({ ...prev, [questionId]: value }));
};
const handleRadioChange = (questionId: string, value: string) => {
setAnswers((prev) => ({ ...prev, [questionId]: value }));
};
const handleCheckboxChange = (questionId: string, option: string, checked: boolean) => {
setCheckboxAnswers((prev) => {
const current = prev[questionId] || [];
const updated = checked
? [...current, option]
: current.filter((item) => item !== option);
return { ...prev, [questionId]: updated };
});
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Validate required questions
for (const q of questions) {
if (q.required) {
if (q.type === "checkbox") {
const selected = checkboxAnswers[q.id] || [];
if (selected.length === 0) {
toast.error(`"${q.label}" is required.`);
return;
}
} else {
const ans = answers[q.id] || "";
if (!ans.trim()) {
toast.error(`"${q.label}" is required.`);
return;
}
}
}
}
setSubmitting(true);
try {
const formattedAnswers = questions.map((q) => {
let value = "";
if (q.type === "checkbox") {
value = (checkboxAnswers[q.id] || []).join(", ");
} else {
value = answers[q.id] || "";
}
return {
questionId: q.id,
value,
};
});
const res = await submitForm(form.id, formattedAnswers);
if (res.ok) {
toast.success(isUpdating ? "Form updated successfully!" : "Form submitted successfully!");
// We don't reset answers if updating, so they can see their current state
if (!isUpdating) {
setAnswers({});
setCheckboxAnswers({});
}
}
} catch (err: any) {
console.error(err);
toast.error(err.message || "Failed to submit form.");
} finally {
setSubmitting(false);
}
};
return (
<form onSubmit={handleSubmit} className="space-y-6">
<Card className="border-t-8 border-t-primary shadow-md">
<CardHeader>
<CardTitle className="text-3xl font-bold">{form.title}</CardTitle>
{form.description && (
<CardDescription className="text-base mt-2 whitespace-pre-wrap">
{form.description}
</CardDescription>
)}
</CardHeader>
</Card>
<div className="space-y-4">
{questions.map((q) => (
<Card key={q.id} className="shadow-sm">
<CardHeader className="pb-3">
<div className="flex items-start justify-between gap-4">
<Label className="text-base font-semibold leading-normal">
{q.label}
{q.required && <span className="text-destructive ml-1">*</span>}
</Label>
</div>
{q.imageUrl && (
<div className="mt-3 relative max-w-full h-auto max-h-80 overflow-hidden rounded-md border">
<img
src={q.imageUrl}
alt={q.label}
className="object-contain max-h-80 w-auto"
/>
</div>
)}
</CardHeader>
<CardContent>
{q.type === "text" && (
<Input
type="text"
placeholder="Your answer"
required={q.required}
value={answers[q.id] || ""}
onChange={(e) => handleTextChange(q.id, e.target.value)}
className="w-full"
/>
)}
{q.type === "textarea" && (
<Textarea
placeholder="Your answer"
required={q.required}
value={answers[q.id] || ""}
onChange={(e) => handleTextChange(q.id, e.target.value)}
className="w-full min-h-24 resize-y"
/>
)}
{q.type === "radio" && q.options && (
<RadioGroup
required={q.required}
value={answers[q.id] || ""}
onValueChange={(val) => handleRadioChange(q.id, val)}
className="space-y-2"
>
{q.options.map((opt, idx) => (
<div key={idx} className="flex items-center space-x-2">
<RadioGroupItem value={opt} id={`${q.id}-${idx}`} />
<Label htmlFor={`${q.id}-${idx}`} className="font-normal cursor-pointer">
{opt}
</Label>
</div>
))}
</RadioGroup>
)}
{q.type === "checkbox" && q.options && (
<div className="space-y-2">
{q.options.map((opt, idx) => {
const isChecked = (checkboxAnswers[q.id] || []).includes(opt);
return (
<div key={idx} className="flex items-center space-x-2">
<Checkbox
id={`${q.id}-${idx}`}
checked={isChecked}
onCheckedChange={(checked) =>
handleCheckboxChange(q.id, opt, !!checked)
}
/>
<Label htmlFor={`${q.id}-${idx}`} className="font-normal cursor-pointer">
{opt}
</Label>
</div>
);
})}
</div>
)}
</CardContent>
</Card>
))}
</div>
<div className="flex justify-end items-center pt-4">
<Button type="submit" disabled={submitting} className="min-w-28">
{submitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Submitting
</>
) : isUpdating ? (
"Update Submission"
) : (
"Submit"
)}
</Button>
</div>
</form>
);
}