güncelleme

This commit is contained in:
2026-01-29 16:49:51 +03:00
parent 10bfa3089e
commit 54113726f4
38 changed files with 1469 additions and 642 deletions

View File

@@ -23,9 +23,15 @@ import {
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Switch } from "@/components/ui/switch"
import { useRouter } from "next/navigation"
import { toast } from "sonner"
import { Loader2 } from "lucide-react"
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"),
@@ -33,11 +39,12 @@ const productSchema = z.object({
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>
import { createProduct, updateProduct } from "@/app/(dashboard)/dashboard/products/actions"
// Define the shape of data coming from Supabase
interface Product {
@@ -48,6 +55,10 @@ interface Product {
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 {
@@ -57,6 +68,13 @@ interface ProductFormProps {
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>,
@@ -66,15 +84,93 @@ export function ProductForm({ initialData }: ProductFormProps) {
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)
@@ -104,21 +200,45 @@ export function ProductForm({ initialData }: ProductFormProps) {
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8 w-full max-w-2xl">
<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>
)}
/>
<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"
@@ -143,39 +263,70 @@ export function ProductForm({ initialData }: ProductFormProps) {
</FormItem>
)}
/>
<FormField
control={form.control}
name="price"
render={({ field }) => (
<FormItem>
<FormLabel>Fiyat ()</FormLabel>
<FormControl>
<Input type="number" placeholder="0.00" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="image_url"
name="price"
render={({ field }) => (
<FormItem>
<FormLabel>Görsel URL (Opsiyonel)</FormLabel>
<FormLabel>Fiyat ()</FormLabel>
<FormControl>
<Input placeholder="https://..." {...field} />
<Input type="number" placeholder="0.00" {...field} />
</FormControl>
<FormDescription>
Ürün görseli için şimdilik dış bağlantı kullanın.
</FormDescription>
<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"
@@ -190,8 +341,8 @@ export function ProductForm({ initialData }: ProductFormProps) {
)}
/>
<Button type="submit" disabled={loading}>
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
<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>