The Form Management Dashboard is your control center for viewing, publishing, and managing all forms you’ve created. Every form is stored securely in PostgreSQL and accessible only to its creator.
Dashboard Overview
The dashboard displays all your forms with key metrics and quick actions.
What You See
Each form card displays:
Form title - The AI-generated or custom title
Response count - Total number of submissions received
Creation date - When the form was generated
Delete button - Remove the form permanently
// app/dashboard/page.tsx:87-106
{ forms . map (( form ) => (
< div
key = {form. id }
onClick = {() => router.push( `/forms/ ${ form . id } /responses` )}
className= "p-4 border rounded-lg hover:bg-gray-50 cursor-pointer"
>
<h2 className= "text-xl font-semibold" >{form.title}</h2>
<p className= "text-sm text-gray-600" >
{form._count.responses} responses • Created{ " " }
{ new Date ( form . createdAt ). toLocaleDateString ()}
</ p >
< button
onClick = {(e) => handleDelete (form.id, e )}
disabled = { deletingFormId === form . id }
className = "mt-2 text-sm text-red-500 hover:underline"
>
{ deletingFormId === form . id ? "Deleting..." : "Delete" }
</ button >
</ div >
))}
Data Fetching
Forms are fetched from the /api/getAllForms endpoint on page load:
Component mounts
The useEffect hook triggers on component mount.
API request
Axios fetches all forms for the authenticated user. // app/dashboard/page.tsx:25-28
const fetchForms = async () => {
const response = await axios . get ( "/api/getAllForms" );
setForms ( response . data . forms );
setIsLoading ( false );
};
State update
Forms are stored in React state and rendered as cards.
The dashboard uses optimistic UI updates - when you delete a form, it’s immediately removed from the view while the API request processes in the background.
Forms must be published before they can receive responses. This gives you control over when forms go live.
Default state when a form is first generated.
Cannot receive public submissions
Shareable link is not active
Shows “Publish Form” button
// app/forms/[formId]/page.tsx:233-247
< div >
< p className = "text-sm text-gray-600 mb-3" >
This form is not published yet . Publish it to get a shareable link .
</ p >
< button
type = "button"
onClick = { handlePublishToggle }
disabled = { isToggling }
className = "w-full px-4 py-2 bg-green-600 text-white rounded hover:bg-green-700 disabled:opacity-50"
>
{ isToggling ? "Publishing..." : "Publish Form" }
</ button >
</ div >
Active state - form is live and accepting responses.
Public URL is active: /f/{slug}
Shows shareable link with copy button
Shows “Unpublish Form” button
// app/forms/[formId]/page.tsx:204-231
< div >
< p className = "text-sm text-gray-600 mb-2" > Share this form :</ p >
< div className = "flex gap-2 mb-3" >
< input
type = "text"
value = { ` ${ window . location . origin } /f/ ${ formData . slug } ` }
readOnly
className = "flex-1 px-3 py-2 border rounded bg-white"
/>
< button
type = "button"
onClick = { handleCopyLink }
className = "px-4 py-2 bg-black text-white rounded hover:bg-gray-800"
>
{ isCopied ? "Copied!" : "Copy Link" }
</ button >
</ div >
< button
type = "button"
onClick = { handlePublishToggle }
disabled = { isToggling }
className = "w-full px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700 disabled:opacity-50"
>
{ isToggling ? "Unpublishing..." : "Unpublish Form" }
</ button >
</ div >
Publishing Flow
Click Publish/Unpublish button
User clicks the toggle button in the form preview.
API request
A PATCH request is sent to /api/forms/{formId} to toggle the isPublished field. // app/forms/[formId]/page.tsx:156-166
const handlePublishToggle = async () => {
setIsToggling ( true );
try {
const response = await axios . patch ( `/api/forms/ ${ formId } ` );
setFormData ( response . data . data );
} catch ( error ) {
console . error ( "Failed to toggle publish:" , error );
} finally {
setIsToggling ( false );
}
};
Database update
The form’s isPublished field is toggled in PostgreSQL via Prisma.
UI update
The component state is updated to reflect the new publish status, showing/hiding the shareable link.
Unpublishing a form immediately prevents new submissions, but existing responses are preserved. The public URL will return an error if accessed.
Each form has a preview page at /forms/{formId} where you can:
See exactly how the form will appear to respondents
All fields are disabled (preview mode)
Toggle publish status
Copy the shareable link (when published)
// app/forms/[formId]/page.tsx:179-187
< div className = "mb-4 p-3 bg-blue-50 border border-blue-200 rounded-lg" >
< p className = "text-sm text-blue-800" >
👁️ < strong > Preview Mode </ strong > - This is how your form will look .
Fields are disabled .
</ p >
</ div >
The preview uses the same rendering logic as the public form, ensuring WYSIWYG (What You See Is What You Get) accuracy.
Deleting a form permanently removes it and all associated responses.
Deletion Flow
Click Delete button
User clicks the red “Delete” button on a form card.
Confirmation dialog
Browser confirmation dialog prevents accidental deletion. // app/dashboard/page.tsx:47-53
if (
! window . confirm (
"Are you sure you want to delete this form? This action cannot be undone." ,
)
) {
return ;
}
API request
DELETE request sent to /api/forms/{formId}. // app/dashboard/page.tsx:54-58
setDeletingFormId ( formId );
const response = await axios . delete ( `/api/forms/ ${ formId } ` );
if ( response . data . success ) {
setForms (( prevForms ) => prevForms . filter (( form ) => form . id !== formId ));
}
Database cascade
Prisma deletes the form and all related responses (cascade delete).
UI update
The form card is immediately removed from the dashboard via optimistic update.
Deletion is permanent and cannot be undone. All form data and responses are permanently removed from the database.
Database Schema
Forms are stored in PostgreSQL using Prisma ORM:
model Forms {
id String @id @default ( cuid ())
userId String // Clerk user ID
title String
slug String @unique
fields Json // Array of field objects
isPublished Boolean @default ( false )
createdAt DateTime @default ( now ())
responses FormsResponses []
}
Key Fields
Primary key using CUID (Collision-resistant Unique Identifier) for secure, unpredictable IDs.
Links the form to its creator via Clerk authentication. Used for authorization checks.
Unique, URL-friendly identifier used in public form URLs (/f/{slug}). Auto-generated on creation.
JSON column storing the complete form schema as an array of field objects. Allows flexible field types without schema migrations.
Controls whether the form is accessible via public URL. Defaults to false for safety.
responses (FormsResponses[])
Relation to the responses table. Enables cascade deletion when a form is removed.
Error Handling
The dashboard handles various error states:
// app/dashboard/page.tsx:30-39
catch ( error ) {
if ( axios . isAxiosError ( error )) {
setError ( error . response ?. data ?. error || "Failed to fetch forms" );
} else if ( error instanceof Error ) {
setError ( error . message );
} else {
setError ( "An unexpected error occurred" );
}
}
Loading State
Error State
Empty State
if ( isLoading ) {
return < div className = "p-8" > Loading ...</ div > ;
}
if ( error ) {
return < div className = "p-8 text-red-500" > Error : { error } </ div > ;
}
{ forms . length === 0 ? (
< p > No forms yet . Create one !</ p >
) : (
// Render form cards
)}
Navigation
Clicking a form card navigates to its responses page:
// app/dashboard/page.tsx:90
onClick = {() => router.push( `/forms/ ${ form . id } /responses` )}
The delete button uses e.stopPropagation() to prevent triggering navigation when clicking delete.