289 lines
9.5 KiB
TypeScript
289 lines
9.5 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, LogOut } from "lucide-react";
|
|
import { signOut } from "next-auth/react";
|
|
import Image from "next/image";
|
|
|
|
export function FormLayoutClient({
|
|
userName,
|
|
userImage,
|
|
}: {
|
|
userName: string;
|
|
userImage: string;
|
|
}) {
|
|
return (
|
|
<div className="flex items-center gap-3">
|
|
{userImage && (
|
|
<Image
|
|
src={userImage}
|
|
alt={userName}
|
|
width={28}
|
|
height={28}
|
|
className="rounded-full"
|
|
/>
|
|
)}
|
|
<span className="text-sm text-muted-foreground">{userName}</span>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => signOut({ callbackUrl: "/auth/signin" })}
|
|
>
|
|
<LogOut className="w-4 h-4" />
|
|
</Button>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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;
|
|
isOpen: boolean;
|
|
};
|
|
questions: Question[];
|
|
initialAnswers?: Record<string, string>;
|
|
}
|
|
|
|
export 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();
|
|
|
|
if (!form.isOpen) return;
|
|
|
|
// 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!");
|
|
if (!isUpdating) {
|
|
setAnswers({});
|
|
setCheckboxAnswers({});
|
|
}
|
|
}
|
|
} catch (err: any) {
|
|
console.error(err);
|
|
toast.error(err.message || "Failed to submit form.");
|
|
} finally {
|
|
setSubmitting(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="relative">
|
|
{!form.isOpen && (
|
|
<div className="absolute inset-0 z-50 flex items-center justify-center bg-background/50 backdrop-blur-sm rounded-xl">
|
|
<div className="bg-background border shadow-lg p-6 rounded-lg">
|
|
<h2 className="text-xl font-bold text-center">This form disable for now</h2>
|
|
</div>
|
|
</div>
|
|
)}
|
|
<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"
|
|
disabled={!form.isOpen}
|
|
/>
|
|
)}
|
|
|
|
{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"
|
|
disabled={!form.isOpen}
|
|
/>
|
|
)}
|
|
|
|
{q.type === "radio" && q.options && (
|
|
<RadioGroup
|
|
required={q.required}
|
|
value={answers[q.id] || ""}
|
|
onValueChange={(val) => handleRadioChange(q.id, val)}
|
|
className="space-y-2"
|
|
disabled={!form.isOpen}
|
|
>
|
|
{q.options.map((opt, idx) => (
|
|
<div key={idx} className="flex items-center space-x-2">
|
|
<RadioGroupItem value={opt} id={`${q.id}-${idx}`} disabled={!form.isOpen} />
|
|
<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)
|
|
}
|
|
disabled={!form.isOpen}
|
|
/>
|
|
<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 || !form.isOpen} className="min-w-28">
|
|
{submitting ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
Submitting
|
|
</>
|
|
) : isUpdating ? (
|
|
"Update Submission"
|
|
) : (
|
|
"Submit"
|
|
)}
|
|
</Button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
);
|
|
}
|