Production-ready forms that cover the two main conversion funnels in ignitionstack.pro (leads and admin CRUD). Every form is fully typed, uses the shared UI primitives, and is wired to the appropriate Server Actions for persistence and telemetry.
Displays form-level error messages.
import { FormError } from '@/components/ui/form-error'
<FormError error={error} />
// With custom styling
<FormError
error="Something went wrong"
className="mb-4"
/>Displays field-level validation errors.
import { FieldError } from '@/components/ui/form-error'
<FieldError error={emailError} />Button with loading state for form submissions.
import { SubmitButton } from '@/components/ui/submit-button'
<form action={createPost}>
{/* form fields */}
<SubmitButton>Create Post</SubmitButton>
</form>
// With custom loading text
<SubmitButton loadingText="Creating...">
Create Post
</SubmitButton>
// Disabled state
<SubmitButton disabled={!isValid}>
Submit
</SubmitButton>import {
CreateButton,
UpdateButton,
DeleteButton
} from '@/components/ui/submit-button'
// Create action
<CreateButton>Add Product</CreateButton>
// Update action
<UpdateButton>Save Changes</UpdateButton>
// Delete action (with confirmation styling)
<DeleteButton>Remove Item</DeleteButton>Field-level state and validation hook.
'use client'
import { useFormField } from '@/hooks'
export default function MyForm() {
const email = useFormField('', { validator: 'email' })
const name = useFormField('', { required: true })
return (
<form>
<input
{...email.inputProps}
placeholder="Email"
/>
{email.error && <span>{email.error}</span>}
<input
{...name.inputProps}
placeholder="Name"
/>
{name.error && <span>{name.error}</span>}
</form>
)
}Manage async operations with loading and error states.
'use client'
import { useAsyncAction } from '@/hooks'
import { createPost } from '@/server/actions/posts'
export default function CreatePostForm() {
const { execute, isLoading, error } = useAsyncAction(createPost)
const handleSubmit = async (formData: FormData) => {
const result = await execute(formData)
if (result.success) {
// Handle success
}
}
return (
<form action={handleSubmit}>
{error && <FormError error={error} />}
<SubmitButton disabled={isLoading}>
{isLoading ? 'Creating...' : 'Create Post'}
</SubmitButton>
</form>
)
}Optimistic updates with automatic rollback.
'use client'
import { useOptimisticAction } from '@/hooks'
import { toggleLike } from '@/server/actions/likes'
export default function LikeButton({ post, initialLiked }) {
const { execute, optimisticValue } = useOptimisticAction(
toggleLike,
initialLiked,
(current) => !current // Optimistic update
)
return (
<button onClick={() => execute(post.id)}>
{optimisticValue ? 'Unlike' : 'Like'}
</button>
)
}Complete contact form with validation and analytics.
import ContactForm from '@/components/contact/contact-form'
// In your page
<ContactForm />'use client'
import { useFormField, useAsyncAction } from '@/hooks'
import { sendContactEmail } from '@/server/actions/contact'
import { FormError, SubmitButton } from '@/components/ui'
import TextInput from '@/components/ui/text-input'
import TextArea from '@/components/ui/text-area'
export default function ContactForm() {
const name = useFormField('', { required: true })
const email = useFormField('', { validator: 'email' })
const message = useFormField('', { required: true, minLength: 10 })
const { execute, isLoading, error, success } = useAsyncAction(
sendContactEmail
)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!name.isValid || !email.isValid || !message.isValid) {
return
}
const formData = new FormData()
formData.append('name', name.value)
formData.append('email', email.value)
formData.append('message', message.value)
const result = await execute(formData)
if (result.success) {
name.reset()
email.reset()
message.reset()
}
}
if (success) {
return (
<div className="text-center p-8">
<h3>Thank you!</h3>
<p>We'll get back to you soon.</p>
</div>
)
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<FormError error={error} />
<div>
<TextInput
{...name.inputProps}
placeholder="Your name"
/>
<FieldError error={name.error} />
</div>
<div>
<TextInput
{...email.inputProps}
type="email"
placeholder="your@email.com"
/>
<FieldError error={email.error} />
</div>
<div>
<TextArea
{...message.inputProps}
placeholder="Your message..."
rows={5}
/>
<FieldError error={message.error} />
</div>
<SubmitButton disabled={isLoading}>
{isLoading ? 'Sending...' : 'Send Message'}
</SubmitButton>
</form>
)
}// server/actions/products.ts
'use server'
import { createAdminClient } from '@/lib/supabase/server'
import { ActionResult } from '@/lib/action-result'
import { withAdminAuth } from '@/lib/admin-action-wrapper'
export const createProduct = withAdminAuth(async (
formData: FormData
): Promise<ActionResult<Product>> => {
const supabase = createAdminClient()
const { data, error } = await supabase
.from('products')
.insert({
name: formData.get('name') as string,
price: Number(formData.get('price')),
description: formData.get('description') as string,
})
.select()
.single()
if (error) {
return { success: false, error: error.message }
}
return { success: true, data }
})'use client'
import { useFormField, useAsyncAction } from '@/hooks'
import { createProduct } from '@/server/actions/products'
import { CreateButton, FormError, FieldError } from '@/components/ui'
export default function CreateProductForm() {
const name = useFormField('', { required: true })
const price = useFormField('', { required: true })
const description = useFormField('')
const { execute, isLoading, error } = useAsyncAction(createProduct)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
const formData = new FormData()
formData.append('name', name.value)
formData.append('price', price.value)
formData.append('description', description.value)
const result = await execute(formData)
if (result.success) {
// Redirect or show success
}
}
return (
<form onSubmit={handleSubmit}>
<FormError error={error} />
{/* Form fields */}
<CreateButton disabled={isLoading}>
Create Product
</CreateButton>
</form>
)
}For complex validation, use Zod schemas:
import { z } from 'zod'
const ProductSchema = z.object({
name: z.string().min(1, 'Name is required').max(100),
price: z.number().positive('Price must be positive'),
description: z.string().max(1000).optional(),
})
// In server action
export async function createProduct(formData: FormData) {
const result = ProductSchema.safeParse({
name: formData.get('name'),
price: Number(formData.get('price')),
description: formData.get('description'),
})
if (!result.success) {
return {
success: false,
error: result.error.errors[0].message
}
}
// Continue with validated data
const { name, price, description } = result.data
}Zod validation runs on the server, providing an additional security layer beyond client-side validation.
type="email", type="tel", etc.