Files
parakasa/components/dashboard/product-form.tsx
2026-01-29 16:49:51 +03:00

352 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client"
import { useState } from "react"
import { useForm, type Resolver } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { useRouter } from "next/navigation"
import { toast } from "sonner"
import { Loader2, X, UploadCloud } from "lucide-react"
import imageCompression from 'browser-image-compression'
import { createClient } from "@/lib/supabase-browser"
import Image from "next/image"
import { createProduct, updateProduct } from "@/app/(dashboard)/dashboard/products/actions"
const productSchema = z.object({
name: z.string().min(2, "Ürün adı en az 2 karakter olmalıdır"),
category: z.string().min(1, "Kategori seçiniz"),
description: z.string().optional(),
price: z.coerce.number().min(0, "Fiyat 0'dan küçük olamaz"),
image_url: z.string().optional(),
is_active: z.boolean().default(true),
images: z.array(z.string()).optional()
})
type ProductFormValues = z.infer<typeof productSchema>
// Define the shape of data coming from Supabase
interface Product {
id: number
name: string
category: string
description: string | null
price: number
image_url: string | null
created_at: string
is_active?: boolean
// images? we might need to fetch them separately if they are in another table,
// but for now let's assume update passes them if fetched, or we can handle it later.
// Ideally the server component fetches relation.
}
interface ProductFormProps {
initialData?: Product
}
export function ProductForm({ initialData }: ProductFormProps) {
const router = useRouter()
const [loading, setLoading] = useState(false)
const [uploading, setUploading] = useState(false)
const [previewImages, setPreviewImages] = useState<string[]>(
initialData?.image_url ? [initialData.image_url] : []
)
// Note: initialData probably only has single image_url field unless we updated the fetch query.
// For MVP phase 1, we just sync with image_url or expect 'images' prop if we extended it.
// I will add a local state for images.
const form = useForm<ProductFormValues>({
resolver: zodResolver(productSchema) as Resolver<ProductFormValues>,
defaultValues: initialData ? {
name: initialData.name,
category: initialData.category,
description: initialData.description || "",
price: initialData.price,
image_url: initialData.image_url || "",
is_active: initialData.is_active ?? true,
images: initialData.image_url ? [initialData.image_url] : []
} : {
name: "",
category: "",
description: "",
price: 0,
image_url: "",
is_active: true,
images: []
},
})
const handleImageUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files
if (!files || files.length === 0) return
setUploading(true)
const supabase = createClient()
const uploadedUrls: string[] = [...form.getValues("images") || []]
try {
for (let i = 0; i < files.length; i++) {
const file = files[i]
// Compression
const options = {
maxSizeMB: 1, // Max 1MB
maxWidthOrHeight: 1920,
useWebWorker: true
}
let compressedFile = file
try {
compressedFile = await imageCompression(file, options)
} catch (error) {
console.error("Compression error:", error)
// Fallback to original
}
// Upload
const fileExt = file.name.split('.').pop()
const fileName = `${Math.random().toString(36).substring(2)}_${Date.now()}.${fileExt}`
const filePath = `products/${fileName}`
const { error: uploadError } = await supabase.storage
.from('products') // Assuming 'products' bucket exists
.upload(filePath, compressedFile)
if (uploadError) {
console.error(uploadError)
toast.error(`Resim yüklenemedi: ${file.name}`)
continue
}
// Get URL
const { data } = supabase.storage.from('products').getPublicUrl(filePath)
uploadedUrls.push(data.publicUrl)
}
// Update form
form.setValue("images", uploadedUrls)
// Set first image as main
if (uploadedUrls.length > 0) {
form.setValue("image_url", uploadedUrls[0])
}
setPreviewImages(uploadedUrls)
} catch {
toast.error("Yükleme sırasında hata oluştu")
} finally {
setUploading(false)
}
}
const removeImage = (index: number) => {
const currentImages = [...form.getValues("images") || []]
currentImages.splice(index, 1)
form.setValue("images", currentImages)
if (currentImages.length > 0) {
form.setValue("image_url", currentImages[0])
} else {
form.setValue("image_url", "")
}
setPreviewImages(currentImages)
}
async function onSubmit(data: ProductFormValues) {
try {
setLoading(true)
let result
if (initialData) {
result = await updateProduct(initialData.id, data)
} else {
result = await createProduct(data)
}
if (!result.success) {
toast.error(result.error || "Bir hata oluştu")
return
}
toast.success(initialData ? "Ürün güncellendi" : "Ürün başarıyla oluşturuldu")
router.push("/dashboard/products")
router.refresh()
} catch {
toast.error("Bir aksilik oldu")
} finally {
setLoading(false)
}
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8 w-full max-w-2xl">
<div className="flex items-center justify-between">
<FormField
control={form.control}
name="is_active"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-4 shadow-sm w-full max-w-xs">
<div className="space-y-0.5">
<FormLabel className="text-base">Aktif Durum</FormLabel>
<FormDescription>
Ürün sitede görüntülensin mi?
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Ürün Adı</FormLabel>
<FormControl>
<Input placeholder="Çelik Kasa Model X" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="category"
render={({ field }) => (
<FormItem>
<FormLabel>Kategori</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Kategori seçin" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="ev">Ev Tipi</SelectItem>
<SelectItem value="ofis">Ofis Tipi</SelectItem>
<SelectItem value="otel">Otel Kasası</SelectItem>
<SelectItem value="ozel">Özel Üretim</SelectItem>
<SelectItem value="diger">Diğer</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="price"
render={({ field }) => (
<FormItem>
<FormLabel>Fiyat ()</FormLabel>
<FormControl>
<Input type="number" placeholder="0.00" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="space-y-4">
<FormLabel>Ürün Görselleri</FormLabel>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{previewImages.map((url, i) => (
<div key={i} className="relative aspect-square border rounded-md overflow-hidden group">
<Image
src={url}
alt="Preview"
fill
className="object-cover"
/>
<button
type="button"
onClick={() => removeImage(i)}
className="absolute top-1 right-1 bg-red-500 text-white p-1 rounded-full opacity-0 group-hover:opacity-100 transition-opacity"
>
<X className="w-4 h-4" />
</button>
</div>
))}
<label className="flex flex-col items-center justify-center w-full aspect-square border-2 border-dashed rounded-lg cursor-pointer hover:bg-muted/50 transition-colors">
<div className="flex flex-col items-center justify-center pt-5 pb-6">
{uploading ? (
<Loader2 className="w-8 h-8 text-gray-500 animate-spin" />
) : (
<UploadCloud className="w-8 h-8 text-gray-500" />
)}
<p className="mb-2 text-sm text-gray-500">Resim Yükle</p>
</div>
<input
type="file"
className="hidden"
multiple
accept="image/*"
onChange={handleImageUpload}
disabled={uploading}
/>
</label>
</div>
<FormDescription>
Birden fazla resim seçebilirsiniz. Resimler otomatik olarak sıkıştırılacaktır.
</FormDescription>
</div>
{/* Hidden input for main image url fallback if needed */}
<input type="hidden" {...form.register("image_url")} />
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>ıklama</FormLabel>
<FormControl>
<Textarea placeholder="Ürün özellikleri..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={loading || uploading}>
{(loading || uploading) && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{initialData ? "Güncelle" : "Oluştur"}
</Button>
</form>
</Form>
)
}