Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"use client"

import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import { Save } from "lucide-react"

import type { AddProductFormType } from "../../types"

import { AddProductSchema } from "../_schemas/add-product-schema"

import { ButtonLoading } from "@/components/ui/button"
import { Form } from "@/components/ui/form"
import { GeneralInformationCard } from "./general-information-card"
import { InventoryCard } from "./inventory-card"
import { MediaCard } from "./media-card"
import { PricingCard } from "./pricing-card"

export function AddProductForm() {
const form = useForm<AddProductFormType>({
resolver: zodResolver(AddProductSchema),
defaultValues: {
title: "",
sku: "",
description: "",
category: "",
price: undefined,
compareAtPrice: undefined,
stock: undefined,
status: "Active",
tags: [],
},
})

const { isSubmitting, isDirty } = form.formState
const isDisabled = isSubmitting || !isDirty

function onSubmit(data: AddProductFormType) {
// Demo only — no backend
console.log("Product data:", data)
}

return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid gap-4 md:grid-cols-2"
>
<div className="md:col-span-2">
<GeneralInformationCard form={form} />
</div>
<div className="md:col-span-2">
<MediaCard />
</div>
<PricingCard form={form} />
<InventoryCard form={form} />
<div className="md:col-span-2">
<ButtonLoading
isLoading={isSubmitting}
disabled={isDisabled}
className="w-full"
icon={Save}
>
Save Product
</ButtonLoading>
</div>
</form>
</Form>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"use client"

import type { UseFormReturn } from "react-hook-form"
import type { AddProductFormType } from "../../types"

import { categoryOptionsData, tagSuggestionsData } from "../_data/categories"

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { InputTagsWithSuggestions } from "@/components/ui/input-tags"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Textarea } from "@/components/ui/textarea"

interface GeneralInformationCardProps {
form: UseFormReturn<AddProductFormType>
}

export function GeneralInformationCard({ form }: GeneralInformationCardProps) {
return (
<Card>
<CardHeader>
<CardTitle>General Information</CardTitle>
</CardHeader>
<CardContent className="grid gap-4">
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input placeholder="Product name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="sku"
render={({ field }) => (
<FormItem>
<FormLabel>SKU</FormLabel>
<FormControl>
<Input placeholder="e.g. BH-001" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Textarea
placeholder="Product description"
className="resize-none"
rows={4}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="category"
render={({ field }) => (
<FormItem>
<FormLabel>Category</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a category" />
</SelectTrigger>
</FormControl>
<SelectContent>
{categoryOptionsData.map((cat) => (
<SelectItem key={cat} value={cat}>
{cat}
</SelectItem>
))}
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="tags"
render={({ field }) => (
<FormItem>
<FormLabel>Tags</FormLabel>
<FormControl>
<InputTagsWithSuggestions
suggestions={tagSuggestionsData}
tags={field.value ?? []}
onTagsChange={field.onChange}
placeholder="Add tags..."
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
</Card>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"use client"

import type { UseFormReturn } from "react-hook-form"
import type { AddProductFormType } from "../../types"

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"

interface InventoryCardProps {
form: UseFormReturn<AddProductFormType>
}

export function InventoryCard({ form }: InventoryCardProps) {
return (
<Card>
<CardHeader>
<CardTitle>Inventory</CardTitle>
</CardHeader>
<CardContent className="grid gap-4">
<FormField
control={form.control}
name="stock"
render={({ field }) => (
<FormItem>
<FormLabel>Stock Quantity</FormLabel>
<FormControl>
<Input type="number" placeholder="0" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="status"
render={({ field }) => (
<FormItem>
<FormLabel>Status</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select status" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="Active">Active</SelectItem>
<SelectItem value="Draft">Draft</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
</Card>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"use client"

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { FileDropzone } from "@/components/ui/file-dropzone"

export function MediaCard() {
return (
<Card>
<CardHeader>
<CardTitle>Media</CardTitle>
</CardHeader>
<CardContent>
<FileDropzone multiple accept={{ "image/*": [] }} />
</CardContent>
</Card>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"use client"

import type { UseFormReturn } from "react-hook-form"
import type { AddProductFormType } from "../../types"

import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"

interface PricingCardProps {
form: UseFormReturn<AddProductFormType>
}

export function PricingCard({ form }: PricingCardProps) {
return (
<Card>
<CardHeader>
<CardTitle>Pricing</CardTitle>
</CardHeader>
<CardContent className="grid gap-4">
<FormField
control={form.control}
name="price"
render={({ field }) => (
<FormItem>
<FormLabel>Price</FormLabel>
<FormControl>
<Input type="number" placeholder="0.00" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="compareAtPrice"
render={({ field }) => (
<FormItem>
<FormLabel>Compare-at Price</FormLabel>
<FormControl>
<Input type="number" placeholder="0.00" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</CardContent>
</Card>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export const categoryOptionsData = [
"Electronics",
"Wearables",
"Cameras",
"Phones",
"Audio",
"Accessories",
"Gaming",
"Storage",
"Networking",
"Peripherals",
]

export const tagSuggestionsData = [
"wireless",
"bluetooth",
"usb-c",
"portable",
"premium",
"budget",
"new-arrival",
"best-seller",
"limited-edition",
"waterproof",
]
Loading