Files
padmaja/components/admin/ProductForm.tsx
2026-01-17 14:17:42 +05:30

420 lines
14 KiB
TypeScript

'use client'
import { useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Textarea } from '@/components/ui/textarea'
import { Switch } from '@/components/ui/switch'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { ArrowLeft, Save, Upload, X, ImageIcon } from 'lucide-react'
import { toast } from 'sonner'
import Link from 'next/link'
import Image from 'next/image'
import MediaSelector, { MediaFile } from '@/components/admin/MediaSelector'
interface Product {
id?: string
name: string
description: string
price: number
discount: number
images: string[]
stock: number
manageStock: boolean
sku: string
isActive: boolean
categoryId: string
}
interface Category {
id: string
name: string
}
interface ProductFormProps {
product?: Product
}
export function ProductForm({ product }: ProductFormProps) {
const router = useRouter()
const [loading, setLoading] = useState(false)
const [categoriesLoading, setCategoriesLoading] = useState(true)
const [categories, setCategories] = useState<Category[]>([])
const [imageUrl, setImageUrl] = useState('')
const [formData, setFormData] = useState<Product>({
name: '',
description: '',
price: 0,
discount: 0,
images: [],
stock: 0,
manageStock: true,
sku: '',
isActive: true,
categoryId: '',
...product
})
useEffect(() => {
fetchCategories()
}, [])
const fetchCategories = async () => {
try {
setCategoriesLoading(true)
const response = await fetch('/api/categories')
const data = await response.json()
// Handle the new API response structure
setCategories(data.categories || [])
} catch (error) {
console.error('Error fetching categories:', error)
setCategories([]) // Fallback to empty array
} finally {
setCategoriesLoading(false)
}
}
const handleInputChange = (field: keyof Product, value: any) => {
setFormData(prev => ({ ...prev, [field]: value }))
}
const handleAddImageUrl = () => {
if (!imageUrl.trim()) {
toast.error('Please enter a valid image URL')
return
}
// Basic URL validation
try {
new URL(imageUrl)
} catch {
toast.error('Please enter a valid URL')
return
}
setFormData(prev => ({
...prev,
images: [...prev.images, imageUrl.trim()]
}))
setImageUrl('')
toast.success('Image URL added')
}
const removeImage = (index: number) => {
setFormData(prev => ({
...prev,
images: prev.images.filter((_, i) => i !== index)
}))
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
try {
const url = product
? `/api/admin/products/${product.id}`
: '/api/admin/products'
const method = product ? 'PUT' : 'POST'
const response = await fetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
})
if (!response.ok) throw new Error('Failed to save product')
toast.success(`Product ${product ? 'updated' : 'created'} successfully`)
router.push('/admin/products')
} catch (error) {
toast.error(`Failed to ${product ? 'update' : 'create'} product`)
} finally {
setLoading(false)
}
}
return (
<form onSubmit={handleSubmit} className="space-y-6">
<div className="flex items-center justify-between">
<Button variant="ghost" asChild>
<Link href="/admin/products">
<ArrowLeft className="h-4 w-4 mr-2" />
Back to Products
</Link>
</Button>
<Button type="submit" disabled={loading}>
<Save className="h-4 w-4 mr-2" />
{loading ? 'Saving...' : 'Save Product'}
</Button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
<Card>
<CardHeader>
<CardTitle>Product Information</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="name">Product Name</Label>
<Input
id="name"
value={formData.name}
onChange={(e) => handleInputChange('name', e.target.value)}
required
/>
</div>
<div>
<Label htmlFor="description">Description</Label>
<Textarea
id="description"
value={formData.description}
onChange={(e) => handleInputChange('description', e.target.value)}
rows={4}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="sku">SKU</Label>
<Input
id="sku"
value={formData.sku}
onChange={(e) => handleInputChange('sku', e.target.value)}
required
/>
</div>
<div>
<Label htmlFor="category">Category</Label>
<Select
value={formData.categoryId}
onValueChange={(value) => handleInputChange('categoryId', value)}
disabled={categoriesLoading}
>
<SelectTrigger>
<SelectValue placeholder={
categoriesLoading
? "Loading categories..."
: categories.length === 0
? "No categories available"
: "Select category"
} />
</SelectTrigger>
<SelectContent>
{!categoriesLoading && categories.length > 0 ? (
categories.map((category) => (
<SelectItem key={category.id} value={category.id}>
{category.name}
</SelectItem>
))
) : null}
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Product Images</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-6">
{/* Media Selector Section */}
<div className="space-y-2">
<Label className="text-sm font-medium">Product Images</Label>
<MediaSelector
allowedTypes={['image/*']}
onSelect={(file: MediaFile) => {
const imageUrl = file.url
if (imageUrl && typeof imageUrl === 'string' && imageUrl.trim()) {
setFormData(prev => ({
...prev,
images: [...prev.images, imageUrl]
}))
toast.success('Image added successfully')
} else {
toast.error('Invalid file selected - no valid URL found')
}
}}
>
<div className="flex items-center space-x-2 px-4 py-2 border border-dashed border-gray-300 rounded-lg hover:border-gray-400 cursor-pointer">
<ImageIcon className="h-4 w-4" />
<span>Browse & Upload Images</span>
</div>
</MediaSelector>
<p className="text-xs text-gray-500">
Browse existing images or upload new ones. Supports JPG, PNG, WebP formats.
</p>
</div>
{/* URL Input Section (Optional) */}
<div className="space-y-2">
<Label className="text-sm font-medium">Add Image URL (Optional)</Label>
<div className="flex space-x-2">
<Input
placeholder="https://example.com/image.jpg"
value={imageUrl}
onChange={(e) => setImageUrl(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleAddImageUrl()}
/>
<Button
type="button"
variant="outline"
onClick={handleAddImageUrl}
disabled={!imageUrl.trim()}
>
Add URL
</Button>
</div>
<p className="text-xs text-gray-500">
Add images from external URLs (e.g., Unsplash, your CDN, etc.)
</p>
</div>
{/* Image Preview Grid */}
{formData.images.length > 0 && (
<div className="space-y-2">
<Label className="text-sm font-medium">Image Preview</Label>
<div className="grid grid-cols-3 gap-4">
{formData.images.map((image, index) => (
<div key={index} className="relative group">
<Image
src={image}
alt={`Product image ${index + 1}`}
width={150}
height={150}
className="rounded-lg object-cover w-full h-32 border"
onError={(e) => {
const target = e.target as HTMLImageElement;
target.src = '/placeholder-image.jpg'; // fallback image
}}
/>
<Button
type="button"
variant="destructive"
size="sm"
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity h-6 w-6 p-0"
onClick={() => removeImage(index)}
>
<X className="h-3 w-3" />
</Button>
<div className="absolute bottom-0 left-0 right-0 bg-black/50 text-white text-xs p-1 rounded-b-lg truncate">
{image.startsWith('/products/') ? 'Uploaded' : 'External URL'}
</div>
</div>
))}
</div>
</div>
)}
{formData.images.length === 0 && (
<div className="text-center py-8 border-2 border-dashed border-gray-200 rounded-lg">
<Upload className="h-8 w-8 mx-auto text-gray-400 mb-2" />
<p className="text-sm text-gray-500">No images added yet</p>
<p className="text-xs text-gray-400">Upload files or add URLs to get started</p>
</div>
)}
</div>
</CardContent>
</Card>
</div>
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Pricing & Inventory</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<Label htmlFor="price">Price ()</Label>
<Input
id="price"
type="number"
step="0.01"
value={formData.price}
onChange={(e) => handleInputChange('price', parseFloat(e.target.value) || 0)}
required
/>
</div>
<div>
<Label htmlFor="discount">Discount (%)</Label>
<Input
id="discount"
type="number"
min="0"
max="100"
value={formData.discount}
onChange={(e) => handleInputChange('discount', parseInt(e.target.value) || 0)}
/>
</div>
<div>
<div className="flex items-center justify-between mb-3">
<Label htmlFor="manageStock">Manage Stock</Label>
<Switch
id="manageStock"
checked={formData.manageStock}
onCheckedChange={(checked) => handleInputChange('manageStock', checked)}
/>
</div>
<p className="text-sm text-gray-500 mb-4">
Enable this to track inventory levels for this product
</p>
{formData.manageStock && (
<div>
<Label htmlFor="stock">Stock Quantity</Label>
<Input
id="stock"
type="number"
min="0"
value={formData.stock}
onChange={(e) => handleInputChange('stock', parseInt(e.target.value) || 0)}
required
/>
</div>
)}
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Status</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center justify-between">
<Label htmlFor="isActive">Active Status</Label>
<Switch
id="isActive"
checked={formData.isActive}
onCheckedChange={(checked) => handleInputChange('isActive', checked)}
/>
</div>
<p className="text-sm text-gray-500 mt-2">
{formData.isActive ? 'Product is visible to customers' : 'Product is hidden from customers'}
</p>
</CardContent>
</Card>
</div>
</div>
</form>
)
}