first commit
This commit is contained in:
582
frontend/app/admin/settings/_components/setting-dialog.tsx
Normal file
582
frontend/app/admin/settings/_components/setting-dialog.tsx
Normal file
@@ -0,0 +1,582 @@
|
||||
"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 {
|
||||
Form,
|
||||
FormControl,
|
||||
FormField,
|
||||
FormItem,
|
||||
FormLabel,
|
||||
FormMessage,
|
||||
FormDescription,
|
||||
} from "@/components/ui/form"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" // Assume these exist or need verify
|
||||
import { Setting } from "@/types/setting"
|
||||
import { settingService } from "@/services/settingService"
|
||||
import { toast } from "sonner"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
|
||||
const formSchema = z.object({
|
||||
title: z.string().min(2, "Başlık en az 2 karakter olmalı"),
|
||||
slogan: z.string().optional(),
|
||||
url: z.string().url("Geçerli bir URL giriniz").optional().or(z.literal("")),
|
||||
email: z.string().email("Geçerli bir e-posta giriniz").optional().or(z.literal("")),
|
||||
phone: z.string().optional(),
|
||||
address: z.string().optional(),
|
||||
copyright: z.string().optional(),
|
||||
map_embed: z.string().optional(),
|
||||
meta_title: z.string().optional(),
|
||||
meta_description: z.string().optional(),
|
||||
|
||||
// Social
|
||||
facebook: z.string().url().optional().or(z.literal("")),
|
||||
x: z.string().url().optional().or(z.literal("")),
|
||||
instagram: z.string().url().optional().or(z.literal("")),
|
||||
whatsapp: z.string().optional(), // clean number usually
|
||||
linkedin: z.string().url().optional().or(z.literal("")),
|
||||
pinterest: z.string().url().optional().or(z.literal("")),
|
||||
|
||||
// Config
|
||||
is_active: z.boolean().default(false),
|
||||
|
||||
// Images W Logo
|
||||
w_logo: z.any().optional(),
|
||||
w_width: z.coerce.number().min(1).default(100),
|
||||
w_height: z.coerce.number().min(1).default(100),
|
||||
w_quality: z.coerce.number().min(1).max(100).default(85),
|
||||
w_format: z.string().default("avif"),
|
||||
|
||||
// Images B Logo
|
||||
b_logo: z.any().optional(),
|
||||
b_width: z.coerce.number().min(1).default(100),
|
||||
b_height: z.coerce.number().min(1).default(100),
|
||||
b_quality: z.coerce.number().min(1).max(100).default(85),
|
||||
b_format: z.string().default("avif"),
|
||||
})
|
||||
|
||||
interface SettingDialogProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
setting?: Setting | null
|
||||
onSuccess?: () => void
|
||||
}
|
||||
|
||||
export function SettingDialog({ open, onOpenChange, setting, onSuccess }: SettingDialogProps) {
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [wPreview, setWPreview] = useState<string | null>(null)
|
||||
const [bPreview, setBPreview] = 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: "",
|
||||
slogan: "",
|
||||
url: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
address: "",
|
||||
copyright: "",
|
||||
map_embed: "",
|
||||
meta_title: "",
|
||||
meta_description: "",
|
||||
facebook: "",
|
||||
x: "",
|
||||
instagram: "",
|
||||
whatsapp: "",
|
||||
linkedin: "",
|
||||
pinterest: "",
|
||||
is_active: false,
|
||||
w_width: 100,
|
||||
w_height: 100,
|
||||
w_quality: 85,
|
||||
w_format: "avif",
|
||||
b_width: 100,
|
||||
b_height: 100,
|
||||
b_quality: 85,
|
||||
b_format: "avif",
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (setting) {
|
||||
form.reset({
|
||||
title: setting.title,
|
||||
slogan: setting.slogan || "",
|
||||
url: setting.url || "",
|
||||
email: setting.email || "",
|
||||
phone: setting.phone || "",
|
||||
address: setting.address || "",
|
||||
copyright: setting.copyright || "",
|
||||
map_embed: setting.map_embed || "",
|
||||
meta_title: setting.meta_title || "",
|
||||
meta_description: setting.meta_description || "",
|
||||
facebook: setting.facebook || "",
|
||||
x: setting.x || "",
|
||||
instagram: setting.instagram || "",
|
||||
whatsapp: setting.whatsapp || "",
|
||||
linkedin: setting.linkedin || "",
|
||||
pinterest: setting.pinterest || "",
|
||||
is_active: !!setting.is_active,
|
||||
w_width: setting.w_width || 100,
|
||||
w_height: setting.w_height || 100,
|
||||
w_quality: setting.w_quality || 85,
|
||||
w_format: setting.w_format || "avif",
|
||||
b_width: setting.b_width || 100,
|
||||
b_height: setting.b_height || 100,
|
||||
b_quality: setting.b_quality || 85,
|
||||
b_format: setting.b_format || "avif",
|
||||
})
|
||||
|
||||
// Set previews
|
||||
// Assuming backend serves images from a static path, need env URL
|
||||
const apiUrl = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8080"
|
||||
if (setting.w_logo) {
|
||||
// Check if it's already a full URL or relative
|
||||
setWPreview(setting.w_logo.startsWith("http") ? setting.w_logo : `${apiUrl}${setting.w_logo}`)
|
||||
}
|
||||
if (setting.b_logo) {
|
||||
setBPreview(setting.b_logo.startsWith("http") ? setting.b_logo : `${apiUrl}${setting.b_logo}`)
|
||||
}
|
||||
|
||||
} else {
|
||||
form.reset({
|
||||
title: "",
|
||||
slogan: "",
|
||||
url: "",
|
||||
email: "",
|
||||
phone: "",
|
||||
address: "",
|
||||
copyright: "",
|
||||
map_embed: "",
|
||||
meta_title: "",
|
||||
meta_description: "",
|
||||
facebook: "",
|
||||
x: "",
|
||||
instagram: "",
|
||||
whatsapp: "",
|
||||
linkedin: "",
|
||||
pinterest: "",
|
||||
is_active: false,
|
||||
w_width: 100,
|
||||
w_height: 100,
|
||||
w_quality: 85,
|
||||
w_format: "avif",
|
||||
b_width: 100,
|
||||
b_height: 100,
|
||||
b_quality: 85,
|
||||
b_format: "avif",
|
||||
})
|
||||
setWPreview(null)
|
||||
setBPreview(null)
|
||||
}
|
||||
}, [setting, form, open])
|
||||
|
||||
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>, fieldName: "w_logo" | "b_logo") => {
|
||||
const file = e.target.files?.[0]
|
||||
if (file) {
|
||||
form.setValue(fieldName, file)
|
||||
const reader = new FileReader()
|
||||
reader.onloadend = () => {
|
||||
if (fieldName === "w_logo") setWPreview(reader.result as string)
|
||||
else setBPreview(reader.result as string)
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
}
|
||||
|
||||
const onSubmit = async (values: z.infer<typeof formSchema>) => {
|
||||
setLoading(true)
|
||||
const formData = new FormData()
|
||||
|
||||
// Append basic fields
|
||||
Object.entries(values).forEach(([key, value]) => {
|
||||
if (key !== "w_logo" && key !== "b_logo") {
|
||||
formData.append(key, String(value))
|
||||
}
|
||||
})
|
||||
|
||||
// Append images if they are files
|
||||
if (values.w_logo instanceof File) {
|
||||
formData.append("w_logo", values.w_logo)
|
||||
}
|
||||
if (values.b_logo instanceof File) {
|
||||
formData.append("b_logo", values.b_logo)
|
||||
}
|
||||
|
||||
try {
|
||||
if (setting) {
|
||||
await settingService.updateSetting(setting.ID, formData)
|
||||
toast.success("Ayar başarıyla güncellendi")
|
||||
} else {
|
||||
await settingService.createSetting(formData)
|
||||
toast.success("Ayar başarıyla oluşturuldu")
|
||||
}
|
||||
onOpenChange(false)
|
||||
if (onSuccess) onSuccess()
|
||||
} catch (error: unknown) {
|
||||
console.error("Setting 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-[800px] h-[90vh] overflow-y-auto flex flex-col">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{setting ? "Ayar Düzenle" : "Yeni Ayar Ekle"}</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 overflow-y-auto py-2">
|
||||
<Form {...form}>
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
||||
<Tabs defaultValue="general" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="general">Genel</TabsTrigger>
|
||||
<TabsTrigger value="contact">İletişim</TabsTrigger>
|
||||
<TabsTrigger value="social">Sosyal Medya</TabsTrigger>
|
||||
<TabsTrigger value="images">Görseller</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* GENERAL TAB */}
|
||||
<TabsContent value="general" className="space-y-4 pt-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Site Başlığı</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Site Başlığı" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="slogan"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Slogan</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="Slogan" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="meta_title"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Meta Başlığı</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="SEO için Başlık" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="meta_description"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Meta Açıklama</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="SEO için Açıklama" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<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 bg-destructive/10">
|
||||
<div className="space-y-0.5">
|
||||
<FormLabel className="font-bold text-destructive">Aktif Ayar</FormLabel>
|
||||
<FormDescription>
|
||||
Bu ayarı aktif yaparsanız, diğer tüm ayarlar otomatik olarak pasif duruma geçer.
|
||||
</FormDescription>
|
||||
</div>
|
||||
<FormControl>
|
||||
<Switch
|
||||
checked={field.value}
|
||||
onCheckedChange={field.onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{/* CONTACT TAB */}
|
||||
<TabsContent value="contact" className="space-y-4 pt-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="email"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>E-posta</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="ornek@site.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="phone"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Telefon</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="+90 555 ..." {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="url"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Site URL</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="https://site.com" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="address"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Adres</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea placeholder="Adres bilgisi" {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="map_embed"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Harita Embed Kodu (Iframe)</FormLabel>
|
||||
<FormControl>
|
||||
<Textarea placeholder='<iframe src="..." ...></iframe>' {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="copyright"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Telif Hakkı Metni</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder="© 2024 Tüm hakları saklıdır." {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{/* SOCIAL TAB */}
|
||||
<TabsContent value="social" className="space-y-4 pt-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField control={form.control} name="facebook" render={({ field }) => (
|
||||
<FormItem><FormLabel>Facebook</FormLabel><FormControl><Input placeholder="URL" {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="x" render={({ field }) => (
|
||||
<FormItem><FormLabel>X (Twitter)</FormLabel><FormControl><Input placeholder="URL" {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="instagram" render={({ field }) => (
|
||||
<FormItem><FormLabel>Instagram</FormLabel><FormControl><Input placeholder="URL" {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="linkedin" render={({ field }) => (
|
||||
<FormItem><FormLabel>LinkedIn</FormLabel><FormControl><Input placeholder="URL" {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="pinterest" render={({ field }) => (
|
||||
<FormItem><FormLabel>Pinterest</FormLabel><FormControl><Input placeholder="URL" {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
<FormField control={form.control} name="whatsapp" render={({ field }) => (
|
||||
<FormItem><FormLabel>Whatsapp</FormLabel><FormControl><Input placeholder="Numara" {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* IMAGES TAB */}
|
||||
<TabsContent value="images" className="space-y-4 pt-4">
|
||||
{/* White Logo Config */}
|
||||
<div className="border p-4 rounded-md space-y-4">
|
||||
<h3 className="font-semibold text-lg">Beyaz Yazılı Logo (w_logo)</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="w_width"
|
||||
render={({ field }) => (
|
||||
<FormItem><FormLabel>Genişlik</FormLabel><FormControl><Input type="number" {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="w_height"
|
||||
render={({ field }) => (
|
||||
<FormItem><FormLabel>Yükseklik</FormLabel><FormControl><Input type="number" {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="w_quality"
|
||||
render={({ field }) => (
|
||||
<FormItem><FormLabel>Kalite</FormLabel><FormControl><Input type="number" {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="w_format"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Format</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger><SelectValue placeholder="Format" /></SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="avif">AVIF</SelectItem>
|
||||
<SelectItem value="webp">WebP</SelectItem>
|
||||
<SelectItem value="png">PNG</SelectItem>
|
||||
<SelectItem value="jpeg">JPEG</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormItem>
|
||||
<FormLabel>Logo Dosyası</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="file" accept="image/*" onChange={(e) => handleImageChange(e, "w_logo")} />
|
||||
</FormControl>
|
||||
{wPreview && (
|
||||
<div className="mt-2 w-full h-20 bg-gray-100 rounded flex items-center justify-center overflow-hidden border">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={wPreview} alt="W Logo Preview" className="h-full object-contain" />
|
||||
</div>
|
||||
)}
|
||||
</FormItem>
|
||||
</div>
|
||||
|
||||
{/* Black Logo Config */}
|
||||
<div className="border p-4 rounded-md space-y-4">
|
||||
<h3 className="font-semibold text-lg">Siyah Yazılı Logo (b_logo)</h3>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="b_width"
|
||||
render={({ field }) => (
|
||||
<FormItem><FormLabel>Genişlik</FormLabel><FormControl><Input type="number" {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="b_height"
|
||||
render={({ field }) => (
|
||||
<FormItem><FormLabel>Yükseklik</FormLabel><FormControl><Input type="number" {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="b_quality"
|
||||
render={({ field }) => (
|
||||
<FormItem><FormLabel>Kalite</FormLabel><FormControl><Input type="number" {...field} /></FormControl><FormMessage /></FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="b_format"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>Format</FormLabel>
|
||||
<Select onValueChange={field.onChange} defaultValue={field.value}>
|
||||
<FormControl>
|
||||
<SelectTrigger><SelectValue placeholder="Format" /></SelectTrigger>
|
||||
</FormControl>
|
||||
<SelectContent>
|
||||
<SelectItem value="avif">AVIF</SelectItem>
|
||||
<SelectItem value="webp">WebP</SelectItem>
|
||||
<SelectItem value="png">PNG</SelectItem>
|
||||
<SelectItem value="jpeg">JPEG</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<FormItem>
|
||||
<FormLabel>Logo Dosyası</FormLabel>
|
||||
<FormControl>
|
||||
<Input type="file" accept="image/*" onChange={(e) => handleImageChange(e, "b_logo")} />
|
||||
</FormControl>
|
||||
{bPreview && (
|
||||
<div className="mt-2 w-full h-20 bg-gray-100 rounded flex items-center justify-center overflow-hidden border">
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img src={bPreview} alt="B Logo Preview" className="h-full object-contain" />
|
||||
</div>
|
||||
)}
|
||||
</FormItem>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<DialogFooter>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{loading ? "Kaydediliyor..." : "Kaydet"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</form>
|
||||
</Form>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
229
frontend/app/admin/settings/page.tsx
Normal file
229
frontend/app/admin/settings/page.tsx
Normal file
@@ -0,0 +1,229 @@
|
||||
"use client"
|
||||
|
||||
import { useEffect, useState, useCallback } from "react"
|
||||
import { useSession } from "next-auth/react"
|
||||
import { DataTable } from "@/components/ui/data-table"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { Plus, Edit, Trash, RotateCcw } from "lucide-react"
|
||||
import { settingService } from "@/services/settingService"
|
||||
import { Setting } from "@/types/setting"
|
||||
import { SettingDialog } from "./_components/setting-dialog"
|
||||
import { toast } from "sonner"
|
||||
import Swal from "sweetalert2"
|
||||
import withReactContent from "sweetalert2-react-content"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select"
|
||||
|
||||
const MySwal = withReactContent(Swal)
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { data: session } = useSession()
|
||||
const [settings, setSettings] = useState<Setting[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
const [perPage] = useState(10)
|
||||
const [search, setSearch] = useState("")
|
||||
const [statusFilter, setStatusFilter] = useState("with") // "with" | "active" | "only"
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
const [selectedSetting, setSelectedSetting] = useState<Setting | null>(null)
|
||||
|
||||
const fetchSettings = useCallback(async () => {
|
||||
try {
|
||||
// "active" -> "" (backend default?), "with" -> "with", "only" -> "only"
|
||||
const apiSoftFilter = statusFilter === "active" ? "" : statusFilter
|
||||
const res = await settingService.getSettings(page, perPage, search, apiSoftFilter)
|
||||
setSettings(res.items || [])
|
||||
setTotal(res.total)
|
||||
} catch (error) {
|
||||
toast.error("Ayarlar yüklenirken hata oluştu")
|
||||
console.error(error)
|
||||
}
|
||||
}, [page, perPage, search, statusFilter])
|
||||
|
||||
useEffect(() => {
|
||||
if (session) {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
fetchSettings()
|
||||
}
|
||||
}, [session, fetchSettings])
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
const result = await MySwal.fire({
|
||||
title: "Emin misiniz?",
|
||||
text: "Bu ayarı silmek istediğinize emin misiniz?",
|
||||
icon: "warning",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Evet, Sil",
|
||||
cancelButtonText: "İptal",
|
||||
customClass: {
|
||||
popup: "dark:bg-gray-800 dark:text-white",
|
||||
title: "dark:text-white",
|
||||
},
|
||||
})
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
await settingService.deleteSetting(id)
|
||||
toast.success("Ayar başarıyla silindi")
|
||||
fetchSettings()
|
||||
} catch (error) {
|
||||
toast.error("Silme işlemi başarısız oldu")
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleRestore = async (id: number) => {
|
||||
const result = await MySwal.fire({
|
||||
title: "Geri Yükle?",
|
||||
text: "Bu ayarı geri yüklemek istediğinize emin misiniz?",
|
||||
icon: "question",
|
||||
showCancelButton: true,
|
||||
confirmButtonText: "Evet, Geri Yükle",
|
||||
cancelButtonText: "İptal",
|
||||
customClass: {
|
||||
popup: "dark:bg-gray-800 dark:text-white",
|
||||
title: "dark:text-white",
|
||||
},
|
||||
})
|
||||
|
||||
if (result.isConfirmed) {
|
||||
try {
|
||||
await settingService.restoreSetting(id)
|
||||
toast.success("Ayar başarıyla geri yüklendi")
|
||||
fetchSettings()
|
||||
} catch (error) {
|
||||
toast.error("Geri yükleme başarısız oldu")
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
accessorKey: "title",
|
||||
header: "Başlık",
|
||||
},
|
||||
{
|
||||
accessorKey: "is_active",
|
||||
header: "Durum",
|
||||
cell: ({ row }: { row: { original: Setting } }) => (
|
||||
<span
|
||||
className={`px-2 py-1 rounded-full text-xs ${row.original.is_active ? "bg-green-100 text-green-800" : "bg-red-100 text-red-800"
|
||||
}`}
|
||||
>
|
||||
{row.original.is_active ? "Aktif" : "Pasif"}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "UpdatedAt",
|
||||
header: "Son Güncelleme",
|
||||
cell: ({ row }: { row: { original: Setting } }) => {
|
||||
return new Date(row.original.UpdatedAt).toLocaleDateString("tr-TR")
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "actions",
|
||||
cell: ({ row }: { row: { original: Setting } }) => {
|
||||
const isDeleted = !!row.original.DeletedAt
|
||||
return (
|
||||
<div className="flex gap-2 justify-end">
|
||||
{isDeleted ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleRestore(row.original.ID)}
|
||||
className="h-8 w-8 p-0 text-green-600 hover:text-green-700 hover:bg-green-50"
|
||||
title="Geri Yükle"
|
||||
>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
</Button>
|
||||
) : (
|
||||
<>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => {
|
||||
setSelectedSetting(row.original)
|
||||
setDialogOpen(true)
|
||||
}}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Edit className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(row.original.ID)}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
<Trash className="h-4 w-4" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="p-8">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Site Ayarları</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Genel site ayarlarını ve SEO yapılandırmalarını yönetin.
|
||||
</p>
|
||||
</div>
|
||||
<Button onClick={() => {
|
||||
setSelectedSetting(null)
|
||||
setDialogOpen(true)
|
||||
}}>
|
||||
<Plus className="mr-2 h-4 w-4" /> Yeni Ayar
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center py-4 gap-4">
|
||||
<Input
|
||||
placeholder="Başlık ara..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="max-w-sm"
|
||||
/>
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Durum" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="with">Tümü (Dahil)</SelectItem>
|
||||
<SelectItem value="active">Sadece Aktif</SelectItem>
|
||||
<SelectItem value="only">Sadece Silinenler</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<DataTable
|
||||
columns={columns}
|
||||
data={settings}
|
||||
pageCount={Math.ceil(total / perPage)}
|
||||
page={page}
|
||||
onPageChange={setPage}
|
||||
/>
|
||||
|
||||
<SettingDialog
|
||||
open={dialogOpen}
|
||||
onOpenChange={setDialogOpen}
|
||||
setting={selectedSetting}
|
||||
onSuccess={fetchSettings}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user