Files
goGin/frontend/app/admin/heroes/_components/hero-dialog.tsx
Beyhan Oğur 2a5b661443 first commit
2026-04-26 21:46:42 +03:00

385 lines
16 KiB
TypeScript
Raw Permalink 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, useEffect } from "react"
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import * as z from "zod"
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog"
import { Button } from "@/components/ui/button"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
FormDescription,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Switch } from "@/components/ui/switch"
import { Hero } from "@/types/hero"
import { heroService } from "@/services/heroService"
import { toast } from "sonner"
// Zod Schema
const formSchema = z.object({
title: z.string().min(2, "Başlık en az 2 karakter olmalıdır"),
text1: z.string().optional(),
text2: z.string().optional(),
text4: z.string().optional(),
text5: z.string().optional(),
color: z.string().optional(),
is_active: z.boolean(),
width: z.coerce.number().min(1, "Genişlik 0'dan büyük olmalıdır"),
height: z.coerce.number().min(1, "Yükseklik 0'dan büyük olmalıdır"),
quality: z.coerce.number().min(1).max(100).default(85),
format: z.string().optional().default("avif"),
image: z.any().optional(),
})
interface HeroDialogProps {
open: boolean
onOpenChange: (open: boolean) => void
hero?: Hero | null
onSuccess?: () => void
}
export function HeroDialog({ open, onOpenChange, hero, onSuccess }: HeroDialogProps) {
const [loading, setLoading] = useState(false)
const [preview, setPreview] = useState<string | null>(null)
const form = useForm<z.infer<typeof formSchema>>({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
resolver: zodResolver(formSchema) as any,
defaultValues: {
title: "",
text1: "",
text2: "",
text4: "",
text5: "",
color: "#000000",
is_active: true,
width: 0,
height: 0,
quality: 85,
format: "webp",
},
})
useEffect(() => {
if (hero) {
form.reset({
title: hero.title,
text1: hero.text1 || "",
text2: hero.text2 || "",
text4: hero.text4 || "",
text5: hero.text5 || "",
color: hero.color || "#000000",
is_active: !!hero.is_active,
width: hero.width || 0,
height: hero.height || 0,
quality: hero.quality || 85,
format: hero.format || "avif",
})
// Existing image preview
// Backend returns relative path usually, ensure full URL if needed or use as is
// Assuming backend/frontend serve static files correctly
setPreview(hero.image ? `${process.env.NEXT_PUBLIC_API_URL}${hero.image}` : null)
} else {
form.reset({
title: "",
text1: "",
text2: "",
text4: "",
text5: "",
color: "#000000",
is_active: true,
width: 0,
height: 0,
quality: 85,
format: "avif",
})
setPreview(null)
}
}, [hero, form, open])
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0]
if (file) {
form.setValue("image", file)
const reader = new FileReader()
reader.onloadend = () => {
setPreview(reader.result as string)
}
reader.readAsDataURL(file)
}
}
const onSubmit = async (values: z.infer<typeof formSchema>) => {
setLoading(true)
const formData = new FormData()
formData.append("title", values.title)
if (values.text1) formData.append("text1", values.text1)
if (values.text2) formData.append("text2", values.text2)
if (values.text4) formData.append("text4", values.text4)
if (values.text5) formData.append("text5", values.text5)
if (values.color) formData.append("color", values.color)
formData.append("is_active", String(values.is_active))
// New fields
formData.append("width", String(values.width))
formData.append("height", String(values.height))
formData.append("quality", String(values.quality))
if (values.format) formData.append("format", values.format)
if (values.image instanceof File) {
formData.append("image", values.image)
}
try {
if (hero) {
await heroService.updateHero(hero.ID, formData)
toast.success("Hero başarıyla güncellendi")
} else {
await heroService.createHero(formData)
toast.success("Hero başarıyla oluşturuldu")
}
onOpenChange(false)
if (onSuccess) onSuccess()
} catch (error: unknown) {
console.error("Hero save error:", error)
toast.error((error as Error).message || "Bir hata oluştu")
} finally {
setLoading(false)
}
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>{hero ? "Hero Düzenle" : "Yeni Hero Ekle"}</DialogTitle>
</DialogHeader>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="title"
render={({ field }) => (
<FormItem>
<FormLabel>Başlık</FormLabel>
<FormControl>
<Input placeholder="Başlık" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="text1"
render={({ field }) => (
<FormItem>
<FormLabel>Text 1</FormLabel>
<FormControl>
<Input placeholder="Text 1" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="text2"
render={({ field }) => (
<FormItem>
<FormLabel>Text 2</FormLabel>
<FormControl>
<Input placeholder="Text 2" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="text4"
render={({ field }) => (
<FormItem>
<FormLabel>Text 4</FormLabel>
<FormControl>
<Input placeholder="Text 4" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="text5"
render={({ field }) => (
<FormItem>
<FormLabel>Text 5</FormLabel>
<FormControl>
<Input placeholder="Text 5" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="width"
render={({ field }) => (
<FormItem>
<FormLabel>Genişlik (px)</FormLabel>
<FormControl>
<Input type="number" placeholder="0" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="height"
render={({ field }) => (
<FormItem>
<FormLabel>Yükseklik (px)</FormLabel>
<FormControl>
<Input type="number" placeholder="0" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="grid grid-cols-2 gap-4">
<FormField
control={form.control}
name="quality"
render={({ field }) => (
<FormItem>
<FormLabel>Kalite (1-100)</FormLabel>
<FormControl>
<Input type="number" min="1" max="100" placeholder="80" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="format"
render={({ field }) => (
<FormItem>
<FormLabel>Format</FormLabel>
<Select onValueChange={field.onChange} defaultValue={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Format Seçin" />
</SelectTrigger>
</FormControl>
<SelectContent>
<SelectItem value="avif">AVIF (Önerilen)</SelectItem>
<SelectItem value="webp">WebP</SelectItem>
<SelectItem value="jpeg">JPEG</SelectItem>
<SelectItem value="png">PNG</SelectItem>
</SelectContent>
</Select>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
control={form.control}
name="color"
render={({ field }) => (
<FormItem>
<FormLabel>Renk (Hex)</FormLabel>
<div className="flex gap-2">
<FormControl>
<Input type="color" className="w-12 h-10 p-1" {...field} />
</FormControl>
<FormControl>
<Input placeholder="#000000" {...field} />
</FormControl>
</div>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="is_active"
render={({ field }) => (
<FormItem className="flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm">
<div className="space-y-0.5">
<FormLabel>Aktif</FormLabel>
<FormDescription>
Bu hero banner sitede görüntülensin mi?
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
<FormItem>
<FormLabel>Görsel</FormLabel>
<FormControl>
<Input
type="file"
accept="image/*"
onChange={handleImageChange}
/>
</FormControl>
{preview && (
<div className="mt-2 relative w-full h-40 border rounded-md overflow-hidden">
{/* Note: Using standard img for now to avoid Next.js Image Config issues with localhost */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={preview}
alt="Preview"
className="object-cover w-full h-full"
/>
</div>
)}
</FormItem>
<DialogFooter>
<Button type="submit" disabled={loading}>
{loading ? "Kaydediliyor..." : "Kaydet"}
</Button>
</DialogFooter>
</form>
</Form>
</DialogContent>
</Dialog>
)
}