From 70f61a76b05c659b967b8824760f6f94bbcd5924 Mon Sep 17 00:00:00 2001 From: Kenan KARAER Date: Tue, 13 Jan 2026 23:36:40 +0300 Subject: [PATCH] =?UTF-8?q?=C4=B0mage=20upload,s=C4=B1k=C4=B1=C5=9Ft=C4=B1?= =?UTF-8?q?rma?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/sliders/[id]/page.tsx | 34 +++ app/(dashboard)/dashboard/sliders/actions.ts | 124 ++++++++++ .../dashboard/sliders/new/page.tsx | 14 ++ app/(dashboard)/dashboard/sliders/page.tsx | 85 +++++++ components/dashboard/slider-form.tsx | 219 ++++++++++++++++++ components/ui/checkbox.tsx | 32 +++ components/ui/image-upload.tsx | 142 ++++++++++++ next.config.mjs | 8 + package-lock.json | 47 ++++ package.json | 2 + supabase_schema_sliders.sql | 88 +++++++ 11 files changed, 795 insertions(+) create mode 100644 app/(dashboard)/dashboard/sliders/[id]/page.tsx create mode 100644 app/(dashboard)/dashboard/sliders/actions.ts create mode 100644 app/(dashboard)/dashboard/sliders/new/page.tsx create mode 100644 app/(dashboard)/dashboard/sliders/page.tsx create mode 100644 components/dashboard/slider-form.tsx create mode 100644 components/ui/checkbox.tsx create mode 100644 components/ui/image-upload.tsx create mode 100644 supabase_schema_sliders.sql diff --git a/app/(dashboard)/dashboard/sliders/[id]/page.tsx b/app/(dashboard)/dashboard/sliders/[id]/page.tsx new file mode 100644 index 0000000..46308ef --- /dev/null +++ b/app/(dashboard)/dashboard/sliders/[id]/page.tsx @@ -0,0 +1,34 @@ +import { SliderForm } from "@/components/dashboard/slider-form" +import { createClient } from "@/lib/supabase-server" +import { notFound } from "next/navigation" + +interface EditSliderPageProps { + params: { + id: string + } +} + +export default async function EditSliderPage({ params }: EditSliderPageProps) { + const supabase = createClient() + + const { data: slider } = await supabase + .from('sliders') + .select('*') + .eq('id', params.id) + .single() + + if (!slider) { + notFound() + } + + return ( +
+
+

Slider Düzenle

+
+
+ +
+
+ ) +} diff --git a/app/(dashboard)/dashboard/sliders/actions.ts b/app/(dashboard)/dashboard/sliders/actions.ts new file mode 100644 index 0000000..02b0430 --- /dev/null +++ b/app/(dashboard)/dashboard/sliders/actions.ts @@ -0,0 +1,124 @@ +"use server" + +import { createClient } from "@/lib/supabase-server" +import { createClient as createSupabaseClient } from "@supabase/supabase-js" +import { revalidatePath } from "next/cache" + +// Admin client for privileged operations +const supabaseAdmin = createSupabaseClient( + process.env.NEXT_PUBLIC_SUPABASE_URL!, + process.env.SUPABASE_SERVICE_ROLE_KEY!, + { + auth: { + autoRefreshToken: false, + persistSession: false + } + } +) + +async function assertAdmin() { + const supabase = createClient() + const { data: { user } } = await supabase.auth.getUser() + if (!user) throw new Error("Oturum açmanız gerekiyor.") + + const { data: profile } = await supabase.from('profiles').select('role').eq('id', user.id).single() + if (profile?.role !== 'admin') throw new Error("Yetkisiz işlem.") + + return user +} + +export async function getSliders() { + const supabase = createClient() + // Everyone can read, so normal client is fine + const { data, error } = await supabase + .from('sliders') + .select('*') + .order('order', { ascending: true }) + .order('created_at', { ascending: false }) + + if (error) return { error: error.message } + return { data } +} + +export async function createSlider(data: { + title: string + description?: string + image_url: string + link?: string + order?: number + is_active?: boolean +}) { + try { + await assertAdmin() + + const { error } = await supabaseAdmin.from('sliders').insert({ + title: data.title, + description: data.description, + image_url: data.image_url, + link: data.link, + order: data.order || 0, + is_active: data.is_active ?? true + }) + + if (error) throw error + + revalidatePath("/dashboard/sliders") + revalidatePath("/") // Homepage cache update + return { success: true } + } catch (error) { + return { error: (error as Error).message } + } +} + +export async function updateSlider(id: string, data: { + title: string + description?: string + image_url: string + link?: string + order?: number + is_active?: boolean +}) { + try { + await assertAdmin() + + const { error } = await supabaseAdmin + .from('sliders') + .update({ + title: data.title, + description: data.description, + image_url: data.image_url, + link: data.link, + order: data.order, + is_active: data.is_active, + // updated_at trigger usually handles time, but we don't have it in schema yet, so maybe add later + }) + .eq('id', id) + + if (error) throw error + + revalidatePath("/dashboard/sliders") + revalidatePath("/") + return { success: true } + } catch (error) { + return { error: (error as Error).message } + } +} + +export async function deleteSlider(id: string) { + try { + await assertAdmin() + + const { error } = await supabaseAdmin + .from('sliders') + .delete() + .eq('id', id) + + if (error) throw error + + revalidatePath("/dashboard/sliders") + revalidatePath("/") + return { success: true } + } catch (error) { + return { error: (error as Error).message } + } +} diff --git a/app/(dashboard)/dashboard/sliders/new/page.tsx b/app/(dashboard)/dashboard/sliders/new/page.tsx new file mode 100644 index 0000000..0bd6959 --- /dev/null +++ b/app/(dashboard)/dashboard/sliders/new/page.tsx @@ -0,0 +1,14 @@ +import { SliderForm } from "@/components/dashboard/slider-form" + +export default function NewSliderPage() { + return ( +
+
+

Yeni Slider Oluştur

+
+
+ +
+
+ ) +} diff --git a/app/(dashboard)/dashboard/sliders/page.tsx b/app/(dashboard)/dashboard/sliders/page.tsx new file mode 100644 index 0000000..334e316 --- /dev/null +++ b/app/(dashboard)/dashboard/sliders/page.tsx @@ -0,0 +1,85 @@ +import Link from "next/link" +import { Button } from "@/components/ui/button" +import { Plus, Pencil, Trash2, GripVertical } from "lucide-react" +import { getSliders, deleteSlider } from "./actions" +import { Card, CardContent } from "@/components/ui/card" +import Image from "next/image" +import { Badge } from "@/components/ui/badge" + +export default async function SlidersPage() { + const { data: sliders, error } = await getSliders() + + if (error) { + return
Hata: {error}
+ } + + return ( +
+
+

Slider Yönetimi

+
+ + + +
+
+ +
+ {sliders?.length === 0 ? ( + + +

Henüz hiç slider eklenmemiş.

+
+
+ ) : ( + sliders?.map((slider) => ( + +
+
+ +
+ +
+ {slider.title} +
+ +
+
+

{slider.title}

+ {!slider.is_active && ( + Pasif + )} + Sıra: {slider.order} +
+

+ {slider.description || "Açıklama yok"} +

+
+ +
+ + + + {/* Delete functionality usually needs a client component or form action, + for simplicity here we will just link to edit, + or we can add a delete button with server action in a separate client component if needed. + Ideally, list items should be client components to handle delete easily. + */} +
+
+
+ )) + )} +
+
+ ) +} diff --git a/components/dashboard/slider-form.tsx b/components/dashboard/slider-form.tsx new file mode 100644 index 0000000..655259a --- /dev/null +++ b/components/dashboard/slider-form.tsx @@ -0,0 +1,219 @@ +"use client" + +import { useState } from "react" +import { useRouter } from "next/navigation" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import * as z from "zod" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { Textarea } from "@/components/ui/textarea" +import { Checkbox } from "@/components/ui/checkbox" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { toast } from "sonner" +import { Loader2 } from "lucide-react" +import { ImageUpload } from "@/components/ui/image-upload" +import { createSlider, updateSlider } from "@/app/(dashboard)/dashboard/sliders/actions" + +const sliderSchema = z.object({ + title: z.string().min(2, "Başlık en az 2 karakter olmalıdır"), + description: z.string().optional(), + image_url: z.string().min(1, "Görsel yüklemek zorunludur"), + link: z.string().optional(), + order: z.coerce.number().default(0), + is_active: z.boolean().default(true), +}) + +type SliderFormValues = z.infer + +interface Slider { + id: string + title: string + description: string | null + image_url: string + link: string | null + order: number | null + is_active: boolean | null +} + +interface SliderFormProps { + initialData?: Slider +} + +export function SliderForm({ initialData }: SliderFormProps) { + const router = useRouter() + const [loading, setLoading] = useState(false) + + const form = useForm({ + resolver: zodResolver(sliderSchema), + defaultValues: initialData ? { + title: initialData.title, + description: initialData.description || "", + image_url: initialData.image_url, + link: initialData.link || "", + order: initialData.order || 0, + is_active: initialData.is_active ?? true, + } : { + title: "", + description: "", + image_url: "", + link: "", + order: 0, + is_active: true, + }, + }) + + async function onSubmit(data: SliderFormValues) { + setLoading(true) + try { + let result + if (initialData) { + result = await updateSlider(initialData.id, data) + } else { + result = await createSlider(data) + } + + if (result.error) { + toast.error(result.error) + return + } + + toast.success(initialData ? "Slider güncellendi" : "Slider oluşturuldu") + router.push("/dashboard/sliders") + router.refresh() + } catch { + toast.error("Bir sorun oluştu.") + } finally { + setLoading(false) + } + } + + return ( + + + {initialData ? "Slider Düzenle" : "Yeni Slider Ekle"} + + +
+ + + ( + + Görsel + + field.onChange("")} + disabled={loading} + /> + + + + )} + /> + +
+ ( + + Başlık + + + + + + )} + /> + + ( + + Sıralama + + + + Düşük numara önce gösterilir (0, 1, 2...) + + + )} + /> +
+ + ( + + Açıklama + +