diff --git a/app/routes.ts b/app/routes.ts
index 59661c4..e6e8c85 100644
--- a/app/routes.ts
+++ b/app/routes.ts
@@ -7,6 +7,7 @@ export default [
route("forms", "routes/forms.tsx"),
route("forms/new", "routes/create-form.tsx"),
route("form/:id", "routes/form.tsx"),
+ route("form/:id/submit", "routes/submit-form.tsx"),
route("form/:id/edit", "routes/edit-form.tsx"),
route("*", "routes/not-found.tsx"),
] satisfies RouteConfig;
diff --git a/app/routes/form.tsx b/app/routes/form.tsx
index 2cecbb0..48b8058 100644
--- a/app/routes/form.tsx
+++ b/app/routes/form.tsx
@@ -175,11 +175,13 @@ export default function FormPreviewPage() {
- This is a read-only preview. Form submission is disabled.
+ This is a read-only preview. Click below to fill out this form.
-
- Submit (Preview Mode)
-
+
+
+ Fill Out Form
+
+
diff --git a/app/routes/submit-form.tsx b/app/routes/submit-form.tsx
new file mode 100644
index 0000000..6d8f026
--- /dev/null
+++ b/app/routes/submit-form.tsx
@@ -0,0 +1,440 @@
+import { useState, useEffect } from "react"
+import { useParams, Link } from "react-router"
+import {
+ ArrowLeft,
+ CheckCircle2,
+ FileText,
+ Star,
+ ChevronDown,
+ Calendar,
+ Send,
+} from "lucide-react"
+import { Navbar } from "@/components/shared/navbar"
+import { Footer } from "@/components/shared/footer"
+import { FormButton } from "@/components/shared/form-button"
+import { getFormById, submitFormResponse } from "@/lib/api"
+import type { FormDetail, Question } from "@/lib/types"
+
+export default function SubmitFormPage() {
+ const { id } = useParams()
+ const [form, setForm] = useState
(null)
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [answers, setAnswers] = useState>({})
+ const [submitting, setSubmitting] = useState(false)
+ const [submitted, setSubmitted] = useState(false)
+ const [validationErrors, setValidationErrors] = useState>({})
+
+ useEffect(() => {
+ if (!id) return
+
+ async function fetchForm() {
+ try {
+ const data = await getFormById(id!)
+ setForm(data)
+ // Initialize empty answers
+ const initial: Record = {}
+ data.questions.forEach((q) => {
+ initial[q.id] = ""
+ })
+ setAnswers(initial)
+ } catch (err) {
+ if (err instanceof Response && err.status === 404) {
+ setError("Form not found")
+ } else {
+ setError("Failed to load form. Please try again.")
+ }
+ } finally {
+ setLoading(false)
+ }
+ }
+ fetchForm()
+ }, [id])
+
+ function updateAnswer(questionId: string, value: string) {
+ setAnswers((prev) => ({ ...prev, [questionId]: value }))
+ // Clear validation error on change
+ if (validationErrors[questionId]) {
+ setValidationErrors((prev) => {
+ const next = { ...prev }
+ delete next[questionId]
+ return next
+ })
+ }
+ }
+
+ function validate(): boolean {
+ if (!form) return false
+ const errors: Record = {}
+
+ form.questions.forEach((q) => {
+ if (q.required && !answers[q.id]?.trim()) {
+ errors[q.id] = "This question is required"
+ }
+ })
+
+ setValidationErrors(errors)
+ return Object.keys(errors).length === 0
+ }
+
+ async function handleSubmit(e: React.FormEvent) {
+ e.preventDefault()
+ if (!id || !form) return
+
+ if (!validate()) {
+ // Scroll to first error
+ const firstErrorId = form.questions.find((q) => validationErrors[q.id] || (q.required && !answers[q.id]?.trim()))?.id
+ if (firstErrorId) {
+ document.getElementById(`question-${firstErrorId}`)?.scrollIntoView({ behavior: "smooth", block: "center" })
+ }
+ return
+ }
+
+ setSubmitting(true)
+ setError(null)
+ try {
+ await submitFormResponse(id, {
+ answers: form.questions
+ .filter((q) => answers[q.id]?.trim())
+ .map((q) => ({
+ question_id: q.id,
+ answer: answers[q.id].trim(),
+ })),
+ })
+ setSubmitted(true)
+ } catch (err) {
+ setError(err instanceof Error ? err.message : "Failed to submit response")
+ } finally {
+ setSubmitting(false)
+ }
+ }
+
+ if (loading) {
+ return (
+
+ )
+ }
+
+ if (error && !form) {
+ return (
+
+
+
+
+
{error}
+
+
+ Back to forms
+
+
+
+
+
+ )
+ }
+
+ if (submitted) {
+ return (
+
+
+
+
+
+
+
+
+ Response Submitted!
+
+
+ Thank you for filling out {form?.title}.
+ Your response has been recorded.
+
+
+
+
{
+ setSubmitted(false)
+ // Reset answers
+ if (form) {
+ const initial: Record = {}
+ form.questions.forEach((q) => { initial[q.id] = "" })
+ setAnswers(initial)
+ }
+ }}
+ >
+ Submit another response
+
+
+
+
+
+ Back to forms
+
+
+
+
+
+
+
+ )
+ }
+
+ if (!form) return null
+
+ return (
+
+
+
+
+
+
+
+ Back to form preview
+
+
+
+
+
+
+ {form.title}
+
+
+ {form.description}
+
+
+
+ {form.questions.length} questions
+ * Required
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+
+
+
+
+
+
+ )
+}
+
+// ─── Question field component ────────────────────────────────────────────────
+
+interface QuestionFieldProps {
+ question: Question
+ index: number
+ value: string
+ onChange: (value: string) => void
+ error?: string
+}
+
+function QuestionField({ question, index, value, onChange, error }: QuestionFieldProps) {
+ return (
+
+
+
+ {index + 1}
+
+
+
+ {question.title}
+ {question.required && (
+ *
+ )}
+
+
+ {question.type.replace("_", " ")}
+
+
+
+
+
+
+ )
+}
diff --git a/lib/api.ts b/lib/api.ts
index c7915f9..2f2368e 100644
--- a/lib/api.ts
+++ b/lib/api.ts
@@ -6,7 +6,7 @@ import {
isTokenExpired,
decodeJWT,
} from "@/lib/auth"
-import type { FormSummary, FormDetail, CreateFormPayload, UpdateFormPayload } from "@/lib/types"
+import type { FormSummary, FormDetail, CreateFormPayload, UpdateFormPayload, SubmitFormPayload } from "@/lib/types"
export async function refreshAccessToken(): Promise {
if (typeof window === "undefined") return null
@@ -210,6 +210,19 @@ export async function updateForm(id: string, payload: UpdateFormPayload): Promis
return res.json()
}
+export async function submitFormResponse(id: string, payload: SubmitFormPayload): Promise {
+ const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/form/${id}/response`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(payload),
+ })
+
+ if (!res.ok) {
+ const data = await res.json().catch(() => null)
+ throw new Error(data?.message ?? "Failed to submit form response")
+ }
+}
+
export async function deleteForm(id: string): Promise {
const res = await fetchWithAuth(`${import.meta.env.VITE_API_BASE_URL}/api/form/${id}`, {
method: "DELETE",
diff --git a/lib/types.ts b/lib/types.ts
index db6b9eb..ee6e7b1 100644
--- a/lib/types.ts
+++ b/lib/types.ts
@@ -66,3 +66,12 @@ export interface UpdateFormPayload {
description: string
questions: CreateQuestion[]
}
+
+export interface SubmitFormAnswer {
+ question_id: string
+ answer: string
+}
+
+export interface SubmitFormPayload {
+ answers: SubmitFormAnswer[]
+}