first commit
This commit is contained in:
243
components/admin/CategoryFormDialog.tsx
Normal file
243
components/admin/CategoryFormDialog.tsx
Normal file
@@ -0,0 +1,243 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
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 {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Save, Upload, X, Plus, Edit } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import Image from 'next/image'
|
||||
|
||||
interface Category {
|
||||
id?: string
|
||||
name: string
|
||||
description: string
|
||||
image: string | null
|
||||
isActive: boolean
|
||||
}
|
||||
|
||||
interface CategoryFormDialogProps {
|
||||
category?: Category
|
||||
onSuccess?: () => void
|
||||
trigger?: React.ReactNode
|
||||
mode?: 'create' | 'edit'
|
||||
}
|
||||
|
||||
export function CategoryFormDialog({
|
||||
category,
|
||||
onSuccess,
|
||||
trigger,
|
||||
mode = category ? 'edit' : 'create'
|
||||
}: CategoryFormDialogProps) {
|
||||
const [open, setOpen] = useState(false)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [formData, setFormData] = useState<Category>({
|
||||
name: '',
|
||||
description: '',
|
||||
image: null,
|
||||
isActive: true,
|
||||
...category
|
||||
})
|
||||
|
||||
const handleInputChange = (field: keyof Category, value: any) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }))
|
||||
}
|
||||
|
||||
const handleImageUpload = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
// Mock image upload - replace with actual upload logic
|
||||
const imageUrl = URL.createObjectURL(file)
|
||||
setFormData(prev => ({ ...prev, image: imageUrl }))
|
||||
}
|
||||
|
||||
const removeImage = () => {
|
||||
setFormData(prev => ({ ...prev, image: null }))
|
||||
}
|
||||
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
name: '',
|
||||
description: '',
|
||||
image: null,
|
||||
isActive: true,
|
||||
...category
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setLoading(true)
|
||||
|
||||
try {
|
||||
const url = mode === 'edit' && category?.id
|
||||
? `/api/admin/categories/${category.id}`
|
||||
: '/api/admin/categories'
|
||||
|
||||
const method = mode === 'edit' ? 'PATCH' : 'POST'
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(formData)
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json()
|
||||
throw new Error(errorData.error || 'Failed to save category')
|
||||
}
|
||||
|
||||
toast.success(`Category ${mode === 'edit' ? 'updated' : 'created'} successfully`)
|
||||
setOpen(false)
|
||||
resetForm()
|
||||
onSuccess?.()
|
||||
} catch (error: any) {
|
||||
toast.error(error.message || `Failed to ${mode === 'edit' ? 'update' : 'create'} category`)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const defaultTrigger = mode === 'edit' ? (
|
||||
<Button variant="ghost" size="sm">
|
||||
<Edit className="h-4 w-4 mr-2" />
|
||||
Edit
|
||||
</Button>
|
||||
) : (
|
||||
<Button>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Category
|
||||
</Button>
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger asChild>
|
||||
{trigger || defaultTrigger}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{mode === 'edit' ? 'Edit Category' : 'Create New Category'}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Category Information</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="name">Category 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}
|
||||
placeholder="Describe this category..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<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">
|
||||
{formData.isActive ? 'Category is visible to customers' : 'Category is hidden from customers'}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Category Image</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<Label htmlFor="image" className="cursor-pointer">
|
||||
<div className="flex items-center space-x-2 px-4 py-2 border border-dashed border-gray-300 rounded-lg hover:border-gray-400">
|
||||
<Upload className="h-4 w-4" />
|
||||
<span>Upload Image</span>
|
||||
</div>
|
||||
</Label>
|
||||
<Input
|
||||
id="image"
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleImageUpload}
|
||||
className="hidden"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{formData.image && (
|
||||
<div className="relative group">
|
||||
<Image
|
||||
src={formData.image}
|
||||
alt="Category image"
|
||||
width={200}
|
||||
height={200}
|
||||
className="rounded-lg object-cover w-full h-48"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
onClick={removeImage}
|
||||
>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-3">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setOpen(false)}
|
||||
disabled={loading}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
{loading ? 'Saving...' : mode === 'edit' ? 'Update Category' : 'Create Category'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user