Custom Pizza Form
The Custom Pizza Form allows administrators to create new pizzas and add them to the catalog. It features comprehensive validation using Zod, real-time preview, and a polished submission flow.
Validation is handled by Zod with strict rules to ensure data quality:
const pizzaSchema = z.object({
name: z.string().min(3, 'Name must be at least 3 characters'),
price: z.number().min(5, 'Price must be at least $5').max(50, 'Price cannot exceed $50'),
category: z.enum(['Vegetarian', 'Meat', 'Seafood', 'Spicy']),
ingredients: z.string().min(5, 'Please list at least a few ingredients'),
imageUrl: z.string().url('Please enter a valid image URL'),
isRecommended: z.boolean().optional(),
});
type PizzaFormValues = z.infer<typeof pizzaSchema>;
Validation Rules
| Field | Type | Validation | Error Message |
|---|
name | string | Min 3 characters | ”Name must be at least 3 characters” |
price | number | Min 5,Max50 | ”Price must be at least 5"/"Pricecannotexceed50” |
category | enum | Must be one of 4 options | Type-safe enum |
ingredients | string | Min 5 characters | ”Please list at least a few ingredients” |
imageUrl | string | Must be valid URL | ”Please enter a valid image URL” |
isRecommended | boolean | Optional | N/A |
The price range of 5−50 is intentionally set to match the filtering system’s max price, ensuring new pizzas are always visible to users.
The form uses React Hook Form with Zod resolver:
const {
register,
handleSubmit,
watch,
formState: { errors }
} = useForm<PizzaFormValues>({
resolver: zodResolver(pizzaSchema),
defaultValues: {
name: '',
category: 'Vegetarian',
isRecommended: false,
price: 15,
ingredients: '',
imageUrl: 'https://images.unsplash.com/photo-1513104890138-7c749659a591?q=80&w=2070&auto=format&fit=crop'
},
});
Default Values
- Name: Empty (user must provide)
- Category:
Vegetarian (safest default)
- Price: $15 (mid-range)
- Ingredients: Empty (user must provide)
- Image URL: High-quality Unsplash placeholder
- Recommended:
false
Providing sensible defaults (especially for the image URL) ensures users can quickly create a pizza and see a professional preview even before customizing all fields.
Pizza Name
<input
{...register('name')}
placeholder="e.g. Vulcano Pepperoni"
className={`block w-full px-5 py-4 bg-gray-50 border ${errors.name ? 'border-red-500' : 'border-gray-200'} rounded-3xl`}
/>
{errors.name && <p className="text-red-500">{errors.name.message}</p>}
Dynamic border color provides visual feedback when validation fails.
Price
<input
type="number"
step="0.01"
{...register('price', { valueAsNumber: true })}
/>
Key Details:
type="number" provides native number input
step="0.01" allows decimal prices ($14.99)
valueAsNumber: true ensures the value is a number, not a string
Category
<select {...register('category')}>
<option value="Vegetarian">Vegetarian</option>
<option value="Meat">Meat</option>
<option value="Seafood">Seafood</option>
<option value="Spicy">Spicy</option>
</select>
Type-safe category selection using the Zod enum.
Ingredients
<textarea
{...register('ingredients')}
placeholder="e.g. Tomato sauce, Mozzarella, Pepperoni, Spicy oil"
rows={3}
/>
The ingredients field accepts a comma-separated string. During submission, it’s parsed into an array:
ingredients: data.ingredients.split(',').map((i) => i.trim())
This converts "Tomato, Cheese, Basil" into ["Tomato", "Cheese", "Basil"].
Image URL
<input
{...register('imageUrl')}
placeholder="https://images.unsplash.com/..."
/>
Zod’s .url() validator ensures only valid URLs are accepted.
Recommended Flag
<input
type="checkbox"
id="isRecommended"
{...register('isRecommended')}
/>
<label htmlFor="isRecommended">
Mark as Recommended by the Chef
</label>
Optional boolean to highlight special pizzas in the catalog.
Real-Time Preview
The form includes a live preview card that updates as the user types:
const watchedValues = watch();
// Later in the UI:
<img src={watchedValues.imageUrl || 'default-image.jpg'} />
<h3>{watchedValues.name || 'Your Pizza Name'}</h3>
<p>{watchedValues.category} Category</p>
<div className="price">${watchedValues.price?.toFixed(2) || '0.00'}</div>
The watch() hook subscribes to all form fields, triggering a re-render whenever any field changes.
Preview Features
- Image: Displays the URL in real-time (with fallback)
- Name: Shows placeholder “Your Pizza Name” when empty
- Price: Formatted to 2 decimal places
- Category: Displays the selected category
- Ingredients: Shows first 3 ingredients as badges
{(watchedValues.ingredients || 'Tomato, Cheese').split(',').slice(0, 3).map((ing, i) => (
<span key={i}>{ing.trim()}</span>
))}
The submission flow includes loading states, simulated API delay, and success feedback:
const [isSubmitting, setIsSubmitting] = useState(false);
const [showSuccess, setShowSuccess] = useState(false);
const onSubmit = async (data: PizzaFormValues) => {
setIsSubmitting(true);
// Simulate API call
await new Promise((resolve) => setTimeout(resolve, 1500));
const newPizza = {
...data,
id: crypto.randomUUID(),
ingredients: data.ingredients.split(',').map((i) => i.trim()),
};
dispatch(addPizzaToCatalog(newPizza));
setIsSubmitting(false);
setShowSuccess(true);
setTimeout(() => {
navigate('/');
}, 2000);
};
Submission Steps
- Set Loading:
setIsSubmitting(true) disables the button
- Simulate API: 1.5s delay to mimic network request
- Generate ID: Uses
crypto.randomUUID() for unique IDs
- Parse Ingredients: Converts comma-separated string to array
- Dispatch Action: Adds pizza to Redux store and localStorage
- Show Success: Displays success overlay
- Navigate: Returns to dashboard after 2s
The simulated API delay provides realistic UX feedback. In production, replace with actual API call:const response = await fetch('/api/pizzas', {
method: 'POST',
body: JSON.stringify(newPizza),
});
Redux Integration
The addPizzaToCatalog action adds the pizza to both the in-memory state and localStorage:
addPizzaToCatalog: (state, action: PayloadAction<Pizza>) => {
state.pizzas.push(action.payload);
const saved = localStorage.getItem('custom_pizzas');
const customPizzas = saved ? JSON.parse(saved) : [];
localStorage.setItem('custom_pizzas', JSON.stringify([...customPizzas, action.payload]));
}
Custom pizzas are stored separately in custom_pizzas but merged with the default catalog on load:
pizzas: [...pizzasData, ...loadCustomPizzas()] as Pizza[]
UI States
Default State
Form is interactive, button is enabled:
<button type="submit" disabled={isSubmitting}>
CREATE PIZZA
</button>
Submitting State
Button shows loading spinner:
{isSubmitting && (
<div className="absolute inset-0 flex items-center justify-center">
<Loader2 className="animate-spin" size={24} />
</div>
)}
Success State
Overlay covers the form:
{showSuccess && (
<motion.div className="absolute inset-0 z-10 bg-white/95">
<CheckCircle2 size={80} className="text-green-500" />
<h2>Success!</h2>
<p>Your pizza has been added to the catalog.</p>
</motion.div>
)}
Error Handling
Validation errors are displayed inline below each field:
{errors.name && (
<p className="text-red-500 text-[10px] font-bold uppercase ml-2">
{errors.name.message}
</p>
)}
Field borders turn red when invalid:
border ${errors.name ? 'border-red-500' : 'border-gray-200'}
Accessibility
- Labels: Every input has a corresponding label
- Error Messages: Validation errors are announced to screen readers
- Focus States: Custom focus rings with
focus:ring-2 focus:ring-orange-500/20
- Disabled State: Submit button is properly disabled during submission
- Checkbox Label: Clicking the label toggles the checkbox
<label htmlFor="isRecommended" className="cursor-pointer">
Mark as Recommended by the Chef
</label>
Navigation
Back button returns to dashboard:
<button onClick={() => navigate('/')}>
<ArrowLeft size={16} />
Back to Dashboard
</button>
Animation
Framer Motion provides subtle animations:
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
>
{/* Form content */}
</motion.div>
Best Practices
Image URL Tips for Users:
- Use high-quality images (Unsplash, Pexels)
- Ensure images are landscape/square aspect ratio
- Test the URL in the preview before submitting
- Recommended resolution: 1920x1080 or higher
Testing Checklist
src/pages/AddPizza.tsx:11-18 - Zod validation schema
src/pages/AddPizza.tsx:28-43 - Form setup and defaults
src/pages/AddPizza.tsx:47-65 - Submission logic
src/store/pizzaSlice.ts:70-75 - addPizzaToCatalog action