diff --git a/PLANLAMA.md b/PLANLAMA.md
new file mode 100644
index 0000000..3c80e55
--- /dev/null
+++ b/PLANLAMA.md
@@ -0,0 +1,23 @@
+# ParaKasa Geliştirme Planı (Revize Edildi)
+
+**Vizyon:** Proje, tek bir çatı altında iki farklı uygulama gibi çalışacaktır.
+
+## 1. Web Sitesi (Public)
+- Herkese açık kurumsal web sitesi.
+- Ürünler, kategoriler ve iletişim bilgileri sergilenecek.
+- Veriler Yönetim Panelinden (CMS) çekilecek.
+
+## 2. Yönetim Paneli (Private)
+- Sadece firma yetkililerinin giriş yapabileceği kapalı devre sistem.
+- **Fonksiyonlar:**
+ - **CMS:** Web sitesinin içeriğini (ürünler, slider, metinler) yönetme.
+ - **ERP (İç Yönetim):** Stok takibi, gelir/gider hesapları, sipariş yönetimi.
+- **Durum:** Bu bölümün tasarımı ve akışı tamamen baştan kurgulanacak. Şimdilik mevcut haliyle donduruldu.
+
+---
+
+## Mevcut Durum (Tamamlananlar)
+- [x] Kullanıcı Yönetimi (Admin Ekle/Sil).
+- [x] Temel Site Ayarları (Başlık, İletişim).
+- [x] Ürün Yönetimi (Temel CRUD).
+- [x] Kategori Yönetimi (Arayüz hazır, veritabanı bekleniyor).
diff --git a/app/(dashboard)/dashboard/categories/[id]/page.tsx b/app/(dashboard)/dashboard/categories/[id]/page.tsx
new file mode 100644
index 0000000..99bd894
--- /dev/null
+++ b/app/(dashboard)/dashboard/categories/[id]/page.tsx
@@ -0,0 +1,22 @@
+import { createClient } from "@/lib/supabase-server"
+import { CategoryForm } from "@/components/dashboard/category-form"
+import { notFound } from "next/navigation"
+
+export default async function EditCategoryPage({ params }: { params: { id: string } }) {
+ const supabase = createClient()
+ const { data: category } = await supabase
+ .from('categories')
+ .select('*')
+ .eq('id', params.id)
+ .single()
+
+ if (!category) {
+ notFound()
+ }
+
+ return (
+
+
+
+ )
+}
diff --git a/app/(dashboard)/dashboard/categories/actions.ts b/app/(dashboard)/dashboard/categories/actions.ts
new file mode 100644
index 0000000..c4d9451
--- /dev/null
+++ b/app/(dashboard)/dashboard/categories/actions.ts
@@ -0,0 +1,107 @@
+"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
+ }
+ }
+)
+
+export async function createCategory(data: { name: string, description?: string, image_url?: string }) {
+ const supabase = createClient()
+
+ // Check admin
+ const { data: { user } } = await supabase.auth.getUser()
+ if (!user) return { error: "Oturum açmanız gerekiyor." }
+
+ // Check if current user is admin
+ const { data: profile } = await supabase.from('profiles').select('role').eq('id', user.id).single()
+ if (profile?.role !== 'admin') return { error: "Yetkisiz işlem." }
+
+ // Generate slug from name
+ const slug = data.name.toLowerCase()
+ .replace(/ğ/g, 'g')
+ .replace(/ü/g, 'u')
+ .replace(/ş/g, 's')
+ .replace(/ı/g, 'i')
+ .replace(/ö/g, 'o')
+ .replace(/ç/g, 'c')
+ .replace(/[^a-z0-9]/g, '-')
+ .replace(/-+/g, '-')
+ .replace(/^-|-$/g, '')
+
+ const { error } = await supabaseAdmin.from('categories').insert({
+ name: data.name,
+ slug: slug,
+ description: data.description,
+ image_url: data.image_url
+ })
+
+ if (error) return { error: "Kategori oluşturulamadı: " + error.message }
+
+ revalidatePath("/dashboard/categories")
+ return { success: true }
+}
+
+export async function updateCategory(id: string, data: { name: string, description?: string, image_url?: string }) {
+ const supabase = createClient()
+
+ // Check admin
+ const { data: { user } } = await supabase.auth.getUser()
+ if (!user) return { error: "Oturum açmanız gerekiyor." }
+
+ // Check if current user is admin
+ const { data: profile } = await supabase.from('profiles').select('role').eq('id', user.id).single()
+ if (profile?.role !== 'admin') return { error: "Yetkisiz işlem." }
+
+ const slug = data.name.toLowerCase()
+ .replace(/ğ/g, 'g')
+ .replace(/ü/g, 'u')
+ .replace(/ş/g, 's')
+ .replace(/ı/g, 'i')
+ .replace(/ö/g, 'o')
+ .replace(/ç/g, 'c')
+ .replace(/[^a-z0-9]/g, '-')
+ .replace(/-+/g, '-')
+ .replace(/^-|-$/g, '')
+
+ const { error } = await supabaseAdmin.from('categories').update({
+ name: data.name,
+ slug: slug,
+ description: data.description,
+ image_url: data.image_url
+ }).eq('id', id)
+
+ if (error) return { error: "Kategori güncellenemedi: " + error.message }
+
+ revalidatePath("/dashboard/categories")
+ return { success: true }
+}
+
+export async function deleteCategory(id: string) {
+ const supabase = createClient()
+
+ // Check admin
+ const { data: { user } } = await supabase.auth.getUser()
+ if (!user) return { error: "Oturum açmanız gerekiyor." }
+
+ // Check if current user is admin
+ const { data: profile } = await supabase.from('profiles').select('role').eq('id', user.id).single()
+ if (profile?.role !== 'admin') return { error: "Yetkisiz işlem." }
+
+ const { error } = await supabaseAdmin.from('categories').delete().eq('id', id)
+
+ if (error) return { error: "Kategori silinemedi: " + error.message }
+
+ revalidatePath("/dashboard/categories")
+ return { success: true }
+}
diff --git a/app/(dashboard)/dashboard/categories/new/page.tsx b/app/(dashboard)/dashboard/categories/new/page.tsx
new file mode 100644
index 0000000..b484e34
--- /dev/null
+++ b/app/(dashboard)/dashboard/categories/new/page.tsx
@@ -0,0 +1,9 @@
+import { CategoryForm } from "@/components/dashboard/category-form"
+
+export default function NewCategoryPage() {
+ return (
+
+
+
+ )
+}
diff --git a/app/(dashboard)/dashboard/categories/page.tsx b/app/(dashboard)/dashboard/categories/page.tsx
new file mode 100644
index 0000000..3e7bfed
--- /dev/null
+++ b/app/(dashboard)/dashboard/categories/page.tsx
@@ -0,0 +1,78 @@
+import { createClient } from "@/lib/supabase-server"
+import { format } from "date-fns"
+import { tr } from "date-fns/locale"
+import Link from "next/link"
+import { Plus } from "lucide-react"
+import { Button } from "@/components/ui/button"
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table"
+
+export default async function CategoriesPage() {
+ const supabase = createClient()
+ const { data: categories } = await supabase
+ .from('categories')
+ .select('*')
+ .order('created_at', { ascending: false })
+
+ return (
+
+
+
+
Kategoriler ({categories?.length || 0})
+
+ Sitenizdeki ürün kategorilerini yönetin.
+
+
+
+
+ Yeni Ekle
+
+
+
+
+
+
+
+
+ Ad
+ Slug
+ Oluşturulma Tarihi
+ İşlemler
+
+
+
+ {categories?.map((category) => (
+
+ {category.name}
+ {category.slug}
+
+ {format(new Date(category.created_at), "d MMMM yyyy", { locale: tr })}
+
+
+
+
+ Düzenle
+
+
+
+
+ ))}
+ {(!categories || categories.length === 0) && (
+
+
+ Kategori bulunamadı.
+
+
+ )}
+
+
+
+
+ )
+}
diff --git a/app/(dashboard)/dashboard/page.tsx b/app/(dashboard)/dashboard/page.tsx
index 83e7937..e4deb2a 100644
--- a/app/(dashboard)/dashboard/page.tsx
+++ b/app/(dashboard)/dashboard/page.tsx
@@ -1,126 +1,126 @@
-
+import { createClient } from "@/lib/supabase-server"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
-import { DollarSign, ShoppingCart, Users, CreditCard } from "lucide-react"
+import { DollarSign, ShoppingCart, Users, CreditCard, Package } from "lucide-react"
+import Link from "next/link"
+import { Button } from "@/components/ui/button"
+
+export default async function DashboardPage() {
+ const supabase = createClient()
+
+ // Fetch real data
+ const { data: products } = await supabase
+ .from("products")
+ .select("*")
+ .order("created_at", { ascending: false })
+
+ const totalProducts = products?.length || 0
+ const totalValue = products?.reduce((acc, product) => acc + (Number(product.price) || 0), 0) || 0
+ const recentProducts = products?.slice(0, 5) || []
+
+ // Calculate unique categories
+ const categories = new Set(products?.map(p => p.category)).size
-export default function DashboardPage() {
return (
-
+
Genel Bakış
+
+
+ Ürün Ekle
+
+
- {/* Stats Grid */}
- Toplam Gelir
+ Toplam Ürün Değeri
- ₺45,231.89
- +20.1% geçen aya göre
+ ₺{totalValue.toLocaleString('tr-TR', { minimumFractionDigits: 2 })}
+ Stoktaki toplam varlık
- Abonelikler
+ Toplam Ürün
+
+
+
+ {totalProducts}
+ Kayıtlı ürün sayısı
+
+
+
+
+ Kategoriler
+
+
+
+ {categories}
+ Aktif kategori
+
+
+
+
+ Son Güncelleme
- +2350
- +180.1% geçen aya göre
-
-
-
-
- Satışlar
-
-
-
- +12,234
- +19% geçen aya göre
-
-
-
-
- Aktif Şimdi
-
-
-
- +573
- +201 son bir saatte
+ Şimdi
+ Canlı veri akışı
- {/* Recent Sales / Activity */}
- Son Hareketler
+ Son Eklenen Ürünler
- Bu ay 265+ satış yaptınız.
+ En son eklenen {recentProducts.length} ürün.
- {/* Mock List */}
-
-
-
OM
+ {recentProducts.map((product) => (
+
+
+ {product.name.substring(0, 2).toUpperCase()}
+
+
+
{product.name}
+
{product.category}
+
+
₺{Number(product.price).toLocaleString('tr-TR')}
-
-
Ozan Mehmet
-
ozan@email.com
-
-
+₺1,999.00
-
-
-
- AÖ
-
-
-
Ayşe Özdemir
-
ayse@email.com
-
-
+₺39.00
-
-
-
- MK
-
-
-
Mehmet Kaya
-
mehmet@email.com
-
-
+₺299.00
-
+ ))}
+ {recentProducts.length === 0 && (
+
Henüz ürün yok.
+ )}
- {/* Recent Products or Other Info */}
+ {/* Placeholder for future features or quick actions */}
- Son Eklenen Ürünler
+ Hızlı İşlemler
- Stoğa yeni giren ürünler.
+ Yönetim paneli kısayolları.
-
-
- Çelik Kasa EV-100
- Stokta
-
-
- Ofis Tipi XYZ
- Azaldı
-
-
- Otel Kasası H-20
- Stokta
-
+
+
+
+ Yeni Ürün Ekle
+
+
+
+ Siparişleri Yönet (Yakında)
+
@@ -128,3 +128,23 @@ export default function DashboardPage() {
)
}
+
+function PlusIcon(props: any) {
+ return (
+
+
+
+
+ )
+}
diff --git a/app/(dashboard)/dashboard/products/[productId]/page.tsx b/app/(dashboard)/dashboard/products/[productId]/page.tsx
new file mode 100644
index 0000000..c92c6bb
--- /dev/null
+++ b/app/(dashboard)/dashboard/products/[productId]/page.tsx
@@ -0,0 +1,33 @@
+import { createClient } from "@/lib/supabase-server"
+import { ProductForm } from "@/components/dashboard/product-form"
+import { notFound } from "next/navigation"
+
+interface ProductEditPageProps {
+ params: {
+ productId: string
+ }
+}
+
+export default async function ProductEditPage({ params }: ProductEditPageProps) {
+ const supabase = createClient()
+ const { data: product } = await supabase
+ .from("products")
+ .select("*")
+ .eq("id", params.productId)
+ .single()
+
+ if (!product) {
+ notFound()
+ }
+
+ return (
+
+ )
+}
diff --git a/app/(dashboard)/dashboard/products/actions.ts b/app/(dashboard)/dashboard/products/actions.ts
new file mode 100644
index 0000000..71acb65
--- /dev/null
+++ b/app/(dashboard)/dashboard/products/actions.ts
@@ -0,0 +1,51 @@
+"use server"
+
+import { createClient } from "@/lib/supabase-server"
+import { revalidatePath } from "next/cache"
+
+export async function createProduct(data: any) {
+ const supabase = createClient()
+
+ // Validate data manually or use Zod schema here again securely
+ // For simplicity, we assume data is coming from the strongly typed Client Form
+ // In production, ALWAYS validate server-side strictly.
+
+ try {
+ const { error } = await supabase.from("products").insert({
+ name: data.name,
+ category: data.category,
+ description: data.description,
+ price: data.price,
+ image_url: data.image_url,
+ })
+
+ if (error) throw error
+
+ revalidatePath("/dashboard/products")
+ return { success: true }
+ } catch (error: any) {
+ return { success: false, error: error.message }
+ }
+}
+
+export async function updateProduct(id: number, data: any) {
+ const supabase = createClient()
+
+ try {
+ const { error } = await supabase.from("products").update({
+ name: data.name,
+ category: data.category,
+ description: data.description,
+ price: data.price,
+ image_url: data.image_url,
+ }).eq("id", id)
+
+ if (error) throw error
+
+ revalidatePath("/dashboard/products")
+ revalidatePath(`/dashboard/products/${id}`)
+ return { success: true }
+ } catch (error: any) {
+ return { success: false, error: error.message }
+ }
+}
diff --git a/app/(dashboard)/dashboard/products/new/page.tsx b/app/(dashboard)/dashboard/products/new/page.tsx
new file mode 100644
index 0000000..b2d4312
--- /dev/null
+++ b/app/(dashboard)/dashboard/products/new/page.tsx
@@ -0,0 +1,14 @@
+import { ProductForm } from "@/components/dashboard/product-form"
+
+export default function NewProductPage() {
+ return (
+
+ )
+}
diff --git a/app/(dashboard)/dashboard/products/page.tsx b/app/(dashboard)/dashboard/products/page.tsx
new file mode 100644
index 0000000..c786446
--- /dev/null
+++ b/app/(dashboard)/dashboard/products/page.tsx
@@ -0,0 +1,75 @@
+import { createClient } from "@/lib/supabase-server"
+import { Button } from "@/components/ui/button"
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table"
+import { Plus } from "lucide-react"
+import Link from "next/link"
+import { Badge } from "@/components/ui/badge"
+
+export default async function ProductsPage() {
+ const supabase = createClient()
+ const { data: products } = await supabase
+ .from("products")
+ .select("*")
+ .order("created_at", { ascending: false })
+
+ return (
+
+
+
+
+
+
+
+ Ad
+ Kategori
+ Fiyat
+ İşlemler
+
+
+
+ {products?.length === 0 ? (
+
+
+ Henüz ürün eklenmemiş.
+
+
+ ) : (
+ products?.map((product) => (
+
+ {product.name}
+
+
+ {product.category}
+
+
+ ₺{product.price}
+
+
+ Düzenle
+
+
+
+ ))
+ )}
+
+
+
+
+ )
+}
diff --git a/app/(dashboard)/dashboard/profile/page.tsx b/app/(dashboard)/dashboard/profile/page.tsx
new file mode 100644
index 0000000..57526dd
--- /dev/null
+++ b/app/(dashboard)/dashboard/profile/page.tsx
@@ -0,0 +1,50 @@
+import { createClient } from "@/lib/supabase-server"
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+
+export default async function ProfilePage() {
+ const supabase = createClient()
+ const { data: { user } } = await supabase.auth.getUser()
+
+ return (
+
+
+
Profil
+
+
+
+
+
+ Genel Bilgiler
+
+ Kişisel profil bilgileriniz.
+
+
+
+
+
+
+ PK
+
+
Fotoğraf Değiştir
+
+
+
+
E-posta
+
+
E-posta adresi değiştirilemez.
+
+
+
+ Rol
+
+
+
+
+
+
+ )
+}
diff --git a/app/(dashboard)/dashboard/settings/actions.ts b/app/(dashboard)/dashboard/settings/actions.ts
new file mode 100644
index 0000000..f26c8da
--- /dev/null
+++ b/app/(dashboard)/dashboard/settings/actions.ts
@@ -0,0 +1,42 @@
+"use server"
+
+import { createClient } from "@/lib/supabase-server"
+import { revalidatePath } from "next/cache"
+
+export async function updateSiteSettings(data: {
+ site_title: string
+ site_description: string
+ contact_email: string
+ contact_phone: string
+ currency: string
+}) {
+ const supabase = createClient()
+
+ // Check admin is already handled by RLS on database level, but we can double check here
+ const { data: { user } } = await supabase.auth.getUser()
+ if (!user) return { error: "Oturum açmanız gerekiyor." }
+
+ // We update the single row where id is likely 1 or just the first row
+ // Since we initialized it with one row, we can just update match on something true or fetch id first.
+ // Easier: Update all rows (there should only be one) or fetch the specific ID first.
+
+ // Let's first get the ID just to be precise
+ const { data: existing } = await supabase.from('site_settings').select('id').single()
+
+ if (!existing) {
+ return { error: "Ayarlar bulunamadı." }
+ }
+
+ const { error } = await supabase
+ .from('site_settings')
+ .update(data)
+ .eq('id', existing.id)
+
+ if (error) {
+ return { error: "Ayarlar güncellenemedi: " + error.message }
+ }
+
+ revalidatePath("/dashboard/settings")
+ revalidatePath("/") // Revalidate home as it might use these settings
+ return { success: true }
+}
diff --git a/app/(dashboard)/dashboard/settings/page.tsx b/app/(dashboard)/dashboard/settings/page.tsx
new file mode 100644
index 0000000..5cced11
--- /dev/null
+++ b/app/(dashboard)/dashboard/settings/page.tsx
@@ -0,0 +1,45 @@
+import { createClient } from "@/lib/supabase-server"
+import { SiteSettingsForm } from "@/components/dashboard/site-settings-form"
+import { AppearanceForm } from "@/components/dashboard/appearance-form"
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
+import { Label } from "@/components/ui/label"
+import { Switch } from "@/components/ui/switch"
+import { Button } from "@/components/ui/button"
+
+export default async function SettingsPage() {
+ const supabase = createClient()
+
+ // Fetch site settings
+ const { data: settings } = await supabase
+ .from('site_settings')
+ .select('*')
+ .single()
+
+ return (
+
+
Ayarlar
+
+ {/* Site General Settings */}
+
+
+
+
+
+
+
+
+
+ Hesap Güvenliği
+
+ Şifre ve oturum yönetimi.
+
+
+
+ Şifre Değiştir
+ Hesabı Sil
+
+
+
+
+ )
+}
diff --git a/app/(dashboard)/dashboard/users/[userId]/page.tsx b/app/(dashboard)/dashboard/users/[userId]/page.tsx
new file mode 100644
index 0000000..545a572
--- /dev/null
+++ b/app/(dashboard)/dashboard/users/[userId]/page.tsx
@@ -0,0 +1,69 @@
+import { createClient } from "@/lib/supabase-server"
+import { UserForm } from "@/components/dashboard/user-form"
+import { notFound } from "next/navigation"
+
+export default async function EditUserPage({ params }: { params: { userId: string } }) {
+ const supabase = createClient()
+
+ // Fetch profile
+ const { data: profile } = await supabase
+ .from('profiles')
+ .select('*')
+ .eq('id', params.userId)
+ .single()
+
+ if (!profile) {
+ notFound()
+ }
+
+ // We also need the email, which is in auth.users, but we can't select from there easily with RLS/Client if not admin API
+ // However, our logged in user IS admin, but RLS on auth.users is usually strict.
+ // Let's see if we can get it via RPC or if the profile should store email (bad practice duplication, but helpful).
+ // Actually, `supabaseAdmin` in a server action can get it, but here we are in a Page (Server Component).
+ // We can use `supabaseAdmin` here too if we create a utility for it or just import createClient from supabase-js with admin key.
+
+ // WORKAROUND: For now, let's assume we might need a server function to fetch full user details including email
+ // OR we just update the profile part. But the user wants to update email probably.
+ // Let's write a small server action/function to fetch this data securely to render the form.
+
+ // Better: Helper function to get user details
+ const userDetails = await getUserDetails(params.userId)
+
+ return (
+
+
+
Kullanıcı Düzenle
+
+
+
+ )
+}
+
+// Helper to get admin-level data for the form
+import { createClient as createSupabaseClient } from "@supabase/supabase-js"
+
+async function getUserDetails(userId: string) {
+ const supabaseAdmin = createSupabaseClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.SUPABASE_SERVICE_ROLE_KEY!,
+ { auth: { autoRefreshToken: false, persistSession: false } }
+ )
+
+ const { data: { user }, error } = await supabaseAdmin.auth.admin.getUserById(userId)
+ const { data: profile } = await supabaseAdmin.from('profiles').select('*').eq('id', userId).single()
+
+ if (error || !user || !profile) return undefined
+
+ // Split full name
+ const parts = (profile.full_name || "").split(' ')
+ const firstName = parts[0] || ""
+ const lastName = parts.slice(1).join(' ') || ""
+
+ return {
+ id: userId,
+ firstName,
+ lastName,
+ email: user.email || "",
+ role: profile.role as "admin" | "user"
+ }
+}
diff --git a/app/(dashboard)/dashboard/users/actions.ts b/app/(dashboard)/dashboard/users/actions.ts
new file mode 100644
index 0000000..1d49868
--- /dev/null
+++ b/app/(dashboard)/dashboard/users/actions.ts
@@ -0,0 +1,133 @@
+"use server"
+
+import { createClient } from "@/lib/supabase-server"
+import { createClient as createSupabaseClient } from "@supabase/supabase-js"
+import { revalidatePath } from "next/cache"
+import { redirect } from "next/navigation"
+
+// WARNING: specialized client for admin actions only
+// This requires SUPABASE_SERVICE_ROLE_KEY to be set in .env.local
+const supabaseAdmin = createSupabaseClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.SUPABASE_SERVICE_ROLE_KEY!,
+ {
+ auth: {
+ autoRefreshToken: false,
+ persistSession: false
+ }
+ }
+)
+
+export async function createUser(firstName: string, lastName: string, email: string, password: string, role: 'admin' | 'user') {
+ const supabase = createClient()
+
+ // 1. Check if current user is admin
+ const { data: { user: currentUser } } = await supabase.auth.getUser()
+ if (!currentUser) return { error: "Oturum açmanız gerekiyor." }
+
+ const { data: profile } = await supabase
+ .from('profiles')
+ .select('role')
+ .eq('id', currentUser.id)
+ .single()
+
+ if (!profile || profile.role !== 'admin') {
+ return { error: "Yetkisiz işlem. Sadece yöneticiler kullanıcı oluşturabilir." }
+ }
+
+ // 2. Create user using Admin client
+ const { data: newUser, error: createError } = await supabaseAdmin.auth.admin.createUser({
+ email,
+ password,
+ email_confirm: true, // Auto confirm
+ user_metadata: {
+ full_name: `${firstName} ${lastName}`.trim()
+ }
+ })
+
+ if (createError) {
+ return { error: createError.message }
+ }
+
+ if (!newUser.user) {
+ return { error: "Kullanıcı oluşturulamadı." }
+ }
+
+ // 3. Create profile entry (if not handled by trigger, but we'll do it manually to be safe/explicit about role)
+ const { error: profileError } = await supabaseAdmin
+ .from('profiles')
+ .insert({
+ id: newUser.user.id,
+ full_name: `${firstName} ${lastName}`.trim(),
+ role: role
+ })
+
+ if (profileError) {
+ // Optional: delete auth user if profile creation fails?
+ // For now just return error
+ return { error: "Kullanıcı oluşturuldu ancak profil kaydedilemedi: " + profileError.message }
+ }
+
+ revalidatePath("/dashboard/users")
+ return { success: true }
+}
+
+export async function deleteUser(userId: string) {
+ const supabase = createClient()
+
+ // Check admin
+ const { data: { user: currentUser } } = await supabase.auth.getUser()
+ if (!currentUser) return { error: "Oturum açmanız gerekiyor." }
+
+ const { data: profile } = await supabase.from('profiles').select('role').eq('id', currentUser.id).single()
+ if (profile?.role !== 'admin') return { error: "Yetkisiz işlem." }
+
+ // Delete user
+ const { error } = await supabaseAdmin.auth.admin.deleteUser(userId)
+
+ if (error) return { error: error.message }
+
+ revalidatePath("/dashboard/users")
+ return { success: true }
+}
+
+export async function updateUser(userId: string, data: { firstName: string, lastName: string, email: string, password?: string, role: 'admin' | 'user' }) {
+ const supabase = createClient()
+
+ // Check admin
+ const { data: { user: currentUser } } = await supabase.auth.getUser()
+ if (!currentUser) return { error: "Oturum açmanız gerekiyor." }
+
+ // Check if current user is admin
+ const { data: profile } = await supabase.from('profiles').select('role').eq('id', currentUser.id).single()
+ if (profile?.role !== 'admin') return { error: "Yetkisiz işlem." }
+
+ // 1. Update Profile (Role and Name)
+ const { error: profileError } = await supabaseAdmin
+ .from('profiles')
+ .update({
+ full_name: `${data.firstName} ${data.lastName}`.trim(),
+ role: data.role
+ })
+ .eq('id', userId)
+
+ if (profileError) return { error: "Profil güncellenemedi: " + profileError.message }
+
+ // 2. Update Auth (Email and Password)
+ const authUpdates: any = {
+ email: data.email,
+ user_metadata: {
+ full_name: `${data.firstName} ${data.lastName}`.trim()
+ }
+ }
+ if (data.password && data.password.length >= 6) {
+ authUpdates.password = data.password
+ }
+
+ const { error: authError } = await supabaseAdmin.auth.admin.updateUserById(userId, authUpdates)
+
+ if (authError) return { error: "Kullanıcı giriş bilgileri güncellenemedi: " + authError.message }
+
+ revalidatePath("/dashboard/users")
+ return { success: true }
+}
diff --git a/app/(dashboard)/dashboard/users/new/page.tsx b/app/(dashboard)/dashboard/users/new/page.tsx
new file mode 100644
index 0000000..3650b55
--- /dev/null
+++ b/app/(dashboard)/dashboard/users/new/page.tsx
@@ -0,0 +1,13 @@
+
+import { UserForm } from "@/components/dashboard/user-form"
+
+export default function NewUserPage() {
+ return (
+
+
+
Yeni Kullanıcı Ekle
+
+
+
+ )
+}
diff --git a/app/(dashboard)/dashboard/users/page.tsx b/app/(dashboard)/dashboard/users/page.tsx
new file mode 100644
index 0000000..0290841
--- /dev/null
+++ b/app/(dashboard)/dashboard/users/page.tsx
@@ -0,0 +1,87 @@
+import { createClient } from "@/lib/supabase-server"
+import { Button } from "@/components/ui/button"
+import Link from "next/link"
+import { Plus } from "lucide-react"
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table"
+import { Badge } from "@/components/ui/badge"
+
+export default async function UsersPage() {
+ const supabase = createClient()
+ const { data: { user } } = await supabase.auth.getUser()
+
+ // Protected Route Check (Simple)
+ const { data: currentUserProfile } = await supabase
+ .from('profiles')
+ .select('role')
+ .eq('id', user?.id)
+ .single()
+
+ // Only verify if we have profiles, if not (first run), maybe allow?
+ // But for safety, blocking non-admins.
+ if (currentUserProfile?.role !== 'admin') {
+ return (
+
+
Yetkisiz Erişim
+
Bu sayfayı görüntüleme yetkiniz yok.
+
+ )
+ }
+
+ const { data: profiles } = await supabase
+ .from("profiles")
+ .select("*")
+ .order("created_at", { ascending: false })
+
+ return (
+
+
+
Kullanıcı Yönetimi
+
+
+
+ Yeni Kullanıcı
+
+
+
+
+
+
+
+
+
+ Ad Soyad
+ Rol
+ Kayıt Tarihi
+ İşlemler
+
+
+
+ {profiles?.map((profile) => (
+
+ {profile.full_name}
+
+
+ {profile.role === 'admin' ? 'Yönetici' : 'Kullanıcı'}
+
+
+ {new Date(profile.created_at).toLocaleDateString('tr-TR')}
+
+
+ Düzenle
+
+
+
+ ))}
+
+
+
+
+ )
+}
diff --git a/app/(public)/login/page.tsx b/app/(public)/login/page.tsx
index 0d037ea..bc053b9 100644
--- a/app/(public)/login/page.tsx
+++ b/app/(public)/login/page.tsx
@@ -3,7 +3,7 @@
import { useState } from "react"
import { useRouter } from "next/navigation"
import Link from "next/link"
-import { supabase } from "@/lib/supabase"
+import { createClient } from "@/lib/supabase-browser"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
@@ -11,6 +11,7 @@ import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle }
import { AlertCircle, Loader2 } from "lucide-react"
export default function LoginPage() {
+ const supabase = createClient()
const router = useRouter()
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
@@ -33,7 +34,7 @@ export default function LoginPage() {
return
}
- router.push("/")
+ router.push("/dashboard")
router.refresh()
} catch (err: any) {
setError("Bir hata oluştu. Lütfen tekrar deneyin.")
diff --git a/app/layout.tsx b/app/layout.tsx
index bc34f6f..2d099e5 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -6,10 +6,20 @@ import "./globals.css";
const inter = Inter({ subsets: ["latin"] });
const outfit = Outfit({ subsets: ["latin"], variable: "--font-outfit" });
-export const metadata: Metadata = {
- title: "ParaKasa - Premium Çelik Kasalar",
- description: "Eviniz ve iş yeriniz için en yüksek güvenlikli çelik kasa ve para sayma çözümleri.",
-};
+import { getSiteSettings } from "@/lib/site-settings";
+
+export async function generateMetadata() {
+ const settings = await getSiteSettings();
+
+ return {
+ title: settings?.site_title || "ParaKasa - Premium Çelik Kasalar",
+ description: settings?.site_description || "Eviniz ve iş yeriniz için en yüksek güvenlikli çelik kasa ve para sayma çözümleri.",
+ };
+}
+
+import { ThemeProvider } from "@/components/theme-provider"
+
+// ... imports
export default function RootLayout({
children,
@@ -17,12 +27,19 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
-
+
- {children}
-
+
+ {children}
+
+
);
diff --git a/components/dashboard/appearance-form.tsx b/components/dashboard/appearance-form.tsx
new file mode 100644
index 0000000..5020a9f
--- /dev/null
+++ b/components/dashboard/appearance-form.tsx
@@ -0,0 +1,57 @@
+"use client"
+
+import { useTheme } from "next-themes"
+import { Card, CardContent, CardTitle, CardHeader } from "@/components/ui/card"
+import { Label } from "@/components/ui/label"
+import { Switch } from "@/components/ui/switch"
+import { useEffect, useState } from "react"
+
+export function AppearanceForm() {
+ const { theme, setTheme } = useTheme()
+ const [mounted, setMounted] = useState(false)
+
+ // Avoid hydration mismatch
+ useEffect(() => {
+ setMounted(true)
+ }, [])
+
+ if (!mounted) {
+ return (
+
+
+ Görünüm
+
+
+
+
+ Karanlık Mod
+ Koyu temayı etkinleştir.
+
+
+
+
+
+ )
+ }
+
+ return (
+
+
+ Görünüm
+
+
+
+
+ Karanlık Mod
+ Koyu temayı etkinleştir.
+
+ setTheme(checked ? 'dark' : 'light')}
+ />
+
+
+
+ )
+}
diff --git a/components/dashboard/category-form.tsx b/components/dashboard/category-form.tsx
new file mode 100644
index 0000000..4bc5b76
--- /dev/null
+++ b/components/dashboard/category-form.tsx
@@ -0,0 +1,186 @@
+"use client"
+
+import { useState } from "react"
+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 {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
+import { toast } from "sonner"
+import { useRouter } from "next/navigation"
+import { createCategory, updateCategory, deleteCategory } from "@/app/(dashboard)/dashboard/categories/actions"
+import { Trash } from "lucide-react"
+import { AlertModal } from "@/components/modals/alert-modal"
+
+const formSchema = z.object({
+ name: z.string().min(2, "Kategori adı en az 2 karakter olmalıdır."),
+ description: z.string().optional(),
+ image_url: z.string().optional(),
+})
+
+type CategoryFormValues = z.infer
+
+interface CategoryFormProps {
+ initialData?: {
+ id: string
+ name: string
+ description?: string
+ image_url?: string
+ } | null
+}
+
+export function CategoryForm({ initialData }: CategoryFormProps) {
+ const router = useRouter()
+ const [open, setOpen] = useState(false)
+ const [loading, setLoading] = useState(false)
+
+ const title = initialData ? "Kategoriyi Düzenle" : "Yeni Kategori"
+ const description = initialData ? "Kategori detaylarını düzenleyin." : "Yeni bir kategori ekleyin."
+ const toastMessage = initialData ? "Kategori güncellendi." : "Kategori oluşturuldu."
+ const action = initialData ? "Kaydet" : "Oluştur"
+
+ const form = useForm({
+ resolver: zodResolver(formSchema),
+ defaultValues: initialData || {
+ name: "",
+ description: "",
+ image_url: "",
+ },
+ })
+
+ const onSubmit = async (data: CategoryFormValues) => {
+ setLoading(true)
+ try {
+ if (initialData) {
+ const result = await updateCategory(initialData.id, data)
+ if ((result as any).error) {
+ toast.error((result as any).error)
+ } else {
+ toast.success(toastMessage)
+ router.push(`/dashboard/categories`)
+ router.refresh()
+ }
+ } else {
+ const result = await createCategory(data)
+ if ((result as any).error) {
+ toast.error((result as any).error)
+ } else {
+ toast.success(toastMessage)
+ router.push(`/dashboard/categories`)
+ router.refresh()
+ }
+ }
+ } catch (error) {
+ toast.error("Bir hata oluştu.")
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const onDelete = async () => {
+ setLoading(true)
+ try {
+ const result = await deleteCategory(initialData!.id)
+ if ((result as any).error) {
+ toast.error((result as any).error)
+ } else {
+ toast.success("Kategori silindi.")
+ router.push(`/dashboard/categories`)
+ router.refresh()
+ }
+ } catch (error) {
+ toast.error("Silme işlemi başarısız.")
+ } finally {
+ setLoading(false)
+ setOpen(false)
+ }
+ }
+
+ return (
+ <>
+ setOpen(false)}
+ onConfirm={onDelete}
+ loading={loading}
+ />
+
+
+
{title}
+
{description}
+
+ {initialData && (
+
setOpen(true)}
+ disabled={loading}
+ >
+
+
+ )}
+
+
+ >
+ )
+}
diff --git a/components/dashboard/product-form.tsx b/components/dashboard/product-form.tsx
new file mode 100644
index 0000000..4d0112b
--- /dev/null
+++ b/components/dashboard/product-form.tsx
@@ -0,0 +1,200 @@
+"use client"
+
+import { useState } from "react"
+import { useForm } 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 { useRouter } from "next/navigation"
+import { toast } from "sonner"
+import { Loader2 } from "lucide-react"
+
+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(),
+})
+
+type ProductFormValues = z.infer
+
+import { createProduct, updateProduct } from "@/app/(dashboard)/dashboard/products/actions"
+
+// 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
+}
+
+interface ProductFormProps {
+ initialData?: Product
+}
+
+export function ProductForm({ initialData }: ProductFormProps) {
+ const router = useRouter()
+ const [loading, setLoading] = useState(false)
+
+ const form = useForm({
+ resolver: zodResolver(productSchema) as any,
+ defaultValues: initialData ? {
+ name: initialData.name,
+ category: initialData.category,
+ description: initialData.description || "",
+ price: initialData.price,
+ image_url: initialData.image_url || "",
+ } : {
+ name: "",
+ category: "",
+ description: "",
+ price: 0,
+ image_url: "",
+ },
+ })
+
+ 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 (error) {
+ toast.error("Bir aksilik oldu")
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+
+ (
+
+ Ürün Adı
+
+
+
+
+
+ )}
+ />
+
+
+ (
+
+ Kategori
+
+
+
+
+
+
+
+ Ev Tipi
+ Ofis Tipi
+ Otel Kasası
+ Özel Üretim
+ Diğer
+
+
+
+
+ )}
+ />
+
+ (
+
+ Fiyat (₺)
+
+
+
+
+
+ )}
+ />
+
+
+ (
+
+ Görsel URL (Opsiyonel)
+
+
+
+
+ Ürün görseli için şimdilik dış bağlantı kullanın.
+
+
+
+ )}
+ />
+
+ (
+
+ Açıklama
+
+
+
+
+
+ )}
+ />
+
+
+ {loading && }
+ {initialData ? "Güncelle" : "Oluştur"}
+
+
+
+ )
+}
diff --git a/components/dashboard/sidebar.tsx b/components/dashboard/sidebar.tsx
index e5c7dcc..f583aca 100644
--- a/components/dashboard/sidebar.tsx
+++ b/components/dashboard/sidebar.tsx
@@ -3,7 +3,7 @@
import Link from "next/link"
import { usePathname } from "next/navigation"
import { cn } from "@/lib/utils"
-import { LayoutDashboard, Package, ShoppingCart, Users, Settings } from "lucide-react"
+import { LayoutDashboard, Package, ShoppingCart, Users, Settings, Globe, Tags } from "lucide-react"
const sidebarItems = [
{
@@ -21,6 +21,11 @@ const sidebarItems = [
href: "/dashboard/orders",
icon: ShoppingCart,
},
+ {
+ title: "Kategoriler",
+ href: "/dashboard/categories",
+ icon: Tags,
+ },
{
title: "Kullanıcılar",
href: "/dashboard/users",
@@ -31,6 +36,11 @@ const sidebarItems = [
href: "/dashboard/settings",
icon: Settings,
},
+ {
+ title: "Siteye Dön",
+ href: "/",
+ icon: Globe,
+ },
]
interface SidebarProps extends React.HTMLAttributes { }
diff --git a/components/dashboard/site-settings-form.tsx b/components/dashboard/site-settings-form.tsx
new file mode 100644
index 0000000..da8036b
--- /dev/null
+++ b/components/dashboard/site-settings-form.tsx
@@ -0,0 +1,164 @@
+"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 {
+ Form,
+ FormControl,
+ FormDescription,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
+import { toast } from "sonner"
+import { Loader2 } from "lucide-react"
+import { updateSiteSettings } from "@/app/(dashboard)/dashboard/settings/actions"
+
+const settingsSchema = z.object({
+ site_title: z.string().min(2, "Site başlığı en az 2 karakter olmalıdır."),
+ site_description: z.string(),
+ contact_email: z.literal("").or(z.string().email("Geçerli bir e-posta adresi giriniz.")),
+ contact_phone: z.string(),
+ currency: z.string(),
+})
+
+type SettingsFormValues = z.infer
+
+interface SiteSettingsFormProps {
+ initialData: any
+}
+
+export function SiteSettingsForm({ initialData }: SiteSettingsFormProps) {
+ const router = useRouter()
+ const [loading, setLoading] = useState(false)
+
+ const form = useForm({
+ resolver: zodResolver(settingsSchema),
+ defaultValues: {
+ site_title: initialData?.site_title || "ParaKasa",
+ site_description: initialData?.site_description || "",
+ contact_email: initialData?.contact_email || "",
+ contact_phone: initialData?.contact_phone || "",
+ currency: initialData?.currency || "TRY",
+ },
+ })
+
+ const onSubmit = async (data: SettingsFormValues) => {
+ setLoading(true)
+ try {
+ // @ts-ignore
+ const result = await updateSiteSettings(data)
+
+ if (result.error) {
+ toast.error(result.error)
+ return
+ }
+
+ toast.success("Site ayarları güncellendi.")
+ router.refresh()
+ } catch (error) {
+ toast.error("Bir sorun oluştu.")
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+
+ Genel Ayarlar
+ Web sitesinin genel yapılandırma ayarları.
+
+
+
+
+ (
+
+ Site Başlığı
+
+
+
+ Tarayıcı sekmesinde görünen ad.
+
+
+ )}
+ />
+
+ (
+
+ Site Açıklaması
+
+
+
+
+
+ )}
+ />
+
+
+ (
+
+ İletişim E-posta
+
+
+
+
+
+ )}
+ />
+ (
+
+ İletişim Telefon
+
+
+
+
+
+ )}
+ />
+
+
+ (
+
+ Para Birimi
+
+
+
+
+
+ )}
+ />
+
+
+ {loading && }
+ Ayarları Kaydet
+
+
+
+
+
+ )
+}
diff --git a/components/dashboard/user-form.tsx b/components/dashboard/user-form.tsx
new file mode 100644
index 0000000..30f36a1
--- /dev/null
+++ b/components/dashboard/user-form.tsx
@@ -0,0 +1,201 @@
+"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 {
+ Form,
+ FormControl,
+ FormField,
+ FormItem,
+ FormLabel,
+ FormMessage,
+} from "@/components/ui/form"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import { Card, CardContent } from "@/components/ui/card"
+import { toast } from "sonner"
+import { Loader2 } from "lucide-react"
+import { createUser, updateUser } from "@/app/(dashboard)/dashboard/users/actions"
+
+const userSchema = z.object({
+ firstName: z.string().min(2, "Ad en az 2 karakter olmalıdır."),
+ lastName: z.string().min(2, "Soyad en az 2 karakter olmalıdır."),
+ email: z.string().email("Geçerli bir e-posta adresi giriniz."),
+ password: z.string().optional(), // Password is optional on edit
+ role: z.enum(["admin", "user"]),
+}).refine((data) => {
+ // If we are creating a NEW user (no ID passed in props effectively, but schema doesn't know props),
+ // we generally want password required. But here we'll handle it in the component logic or strictly separate schemas.
+ // For simplicity, we make password optional in Zod but check it in onSubmit if it's a create action.
+ return true
+})
+
+type UserFormValues = z.infer
+
+interface UserFormProps {
+ initialData?: {
+ id: string
+ firstName: string
+ lastName: string
+ email: string
+ role: "admin" | "user"
+ }
+}
+
+export function UserForm({ initialData }: UserFormProps) {
+ const router = useRouter()
+ const [loading, setLoading] = useState(false)
+
+ const form = useForm({
+ resolver: zodResolver(userSchema),
+ defaultValues: initialData ? {
+ firstName: initialData.firstName,
+ lastName: initialData.lastName,
+ email: initialData.email,
+ password: "", // Empty password means no change
+ role: initialData.role,
+ } : {
+ firstName: "",
+ lastName: "",
+ email: "",
+ password: "",
+ role: "user",
+ },
+ })
+
+ const onSubmit = async (data: UserFormValues) => {
+ setLoading(true)
+ try {
+ let result;
+ if (initialData) {
+ // Update
+ result = await updateUser(initialData.id, data)
+ } else {
+ // Create
+ if (!data.password || data.password.length < 6) {
+ toast.error("Yeni kullanıcı için şifre gereklidir (min 6 karakter).")
+ setLoading(false)
+ return
+ }
+ result = await createUser(data.firstName, data.lastName, data.email, data.password, data.role)
+ }
+
+ if (result.error) {
+ toast.error(result.error)
+ return
+ }
+
+ toast.success(initialData ? "Kullanıcı güncellendi." : "Kullanıcı oluşturuldu.")
+ router.push("/dashboard/users")
+ router.refresh()
+ } catch (error) {
+ toast.error("Bir sorun oluştu.")
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+
+
+
+
+ (
+
+ Ad
+
+
+
+
+
+ )}
+ />
+ (
+
+ Soyad
+
+
+
+
+
+ )}
+ />
+
+
+ (
+
+ E-posta
+
+
+
+
+
+ )}
+ />
+
+ (
+
+ Şifre
+
+
+
+
+
+ )}
+ />
+
+ (
+
+ Rol
+
+
+
+
+
+
+
+ Kullanıcı
+ Yönetici
+
+
+
+
+ )}
+ />
+
+
+ {loading && }
+ Kullanıcı Oluştur
+
+
+
+
+
+ )
+}
diff --git a/components/dashboard/user-nav.tsx b/components/dashboard/user-nav.tsx
index a01b5b5..55b738c 100644
--- a/components/dashboard/user-nav.tsx
+++ b/components/dashboard/user-nav.tsx
@@ -6,6 +6,7 @@ import {
AvatarImage,
} from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
+import Link from "next/link"
import {
DropdownMenu,
DropdownMenuContent,
@@ -49,12 +50,21 @@ export function UserNav() {
-
- Profil
-
-
- Ayarlar
-
+
+
+ Profil
+
+
+
+
+ Kullanıcılar
+
+
+
+
+ Ayarlar
+
+
diff --git a/components/modals/alert-modal.tsx b/components/modals/alert-modal.tsx
new file mode 100644
index 0000000..48102d5
--- /dev/null
+++ b/components/modals/alert-modal.tsx
@@ -0,0 +1,57 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+
+interface AlertModalProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onConfirm: () => void;
+ loading: boolean;
+}
+
+export const AlertModal: React.FC = ({
+ isOpen,
+ onClose,
+ onConfirm,
+ loading,
+}) => {
+ const [isMounted, setIsMounted] = useState(false);
+
+ useEffect(() => {
+ setIsMounted(true);
+ }, []);
+
+ if (!isMounted) {
+ return null;
+ }
+
+ return (
+
+
+
+ Emin misiniz?
+
+ Bu işlem geri alınamaz. Bu kategoriyi kalıcı olarak silmek istediğinizden emin misiniz?
+
+
+
+
+ İptal
+
+
+ Devam Et
+
+
+
+
+ );
+};
diff --git a/components/theme-provider.tsx b/components/theme-provider.tsx
new file mode 100644
index 0000000..3da6178
--- /dev/null
+++ b/components/theme-provider.tsx
@@ -0,0 +1,8 @@
+"use client"
+
+import * as React from "react"
+import { ThemeProvider as NextThemesProvider, type ThemeProviderProps } from "next-themes"
+
+export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
+ return {children}
+}
diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx
new file mode 100644
index 0000000..71e428b
--- /dev/null
+++ b/components/ui/avatar.tsx
@@ -0,0 +1,53 @@
+"use client"
+
+import * as React from "react"
+import * as AvatarPrimitive from "@radix-ui/react-avatar"
+
+import { cn } from "@/lib/utils"
+
+function Avatar({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AvatarImage({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function AvatarFallback({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export { Avatar, AvatarImage, AvatarFallback }
diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx
new file mode 100644
index 0000000..fd3a406
--- /dev/null
+++ b/components/ui/badge.tsx
@@ -0,0 +1,46 @@
+import * as React from "react"
+import { Slot } from "@radix-ui/react-slot"
+import { cva, type VariantProps } from "class-variance-authority"
+
+import { cn } from "@/lib/utils"
+
+const badgeVariants = cva(
+ "inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
+ {
+ variants: {
+ variant: {
+ default:
+ "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
+ secondary:
+ "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
+ destructive:
+ "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+ outline:
+ "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
+ },
+ },
+ defaultVariants: {
+ variant: "default",
+ },
+ }
+)
+
+function Badge({
+ className,
+ variant,
+ asChild = false,
+ ...props
+}: React.ComponentProps<"span"> &
+ VariantProps & { asChild?: boolean }) {
+ const Comp = asChild ? Slot : "span"
+
+ return (
+
+ )
+}
+
+export { Badge, badgeVariants }
diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx
new file mode 100644
index 0000000..a6f1cfb
--- /dev/null
+++ b/components/ui/dialog.tsx
@@ -0,0 +1,143 @@
+"use client"
+
+import * as React from "react"
+import * as DialogPrimitive from "@radix-ui/react-dialog"
+import { XIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Dialog({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogTrigger({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogPortal({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogClose({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DialogOverlay({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DialogContent({
+ className,
+ children,
+ showCloseButton = true,
+ ...props
+}: React.ComponentProps & {
+ showCloseButton?: boolean
+}) {
+ return (
+
+
+
+ {children}
+ {showCloseButton && (
+
+
+ Close
+
+ )}
+
+
+ )
+}
+
+function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
+ return (
+
+ )
+}
+
+function DialogTitle({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DialogDescription({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ Dialog,
+ DialogClose,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogOverlay,
+ DialogPortal,
+ DialogTitle,
+ DialogTrigger,
+}
diff --git a/components/ui/dropdown-menu.tsx b/components/ui/dropdown-menu.tsx
new file mode 100644
index 0000000..bbe6fb0
--- /dev/null
+++ b/components/ui/dropdown-menu.tsx
@@ -0,0 +1,257 @@
+"use client"
+
+import * as React from "react"
+import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
+import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function DropdownMenu({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DropdownMenuPortal({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuTrigger({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuContent({
+ className,
+ sideOffset = 4,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function DropdownMenuGroup({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuItem({
+ className,
+ inset,
+ variant = "default",
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+ variant?: "default" | "destructive"
+}) {
+ return (
+
+ )
+}
+
+function DropdownMenuCheckboxItem({
+ className,
+ children,
+ checked,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
+
+function DropdownMenuRadioGroup({
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuRadioItem({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
+
+function DropdownMenuLabel({
+ className,
+ inset,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+}) {
+ return (
+
+ )
+}
+
+function DropdownMenuSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function DropdownMenuShortcut({
+ className,
+ ...props
+}: React.ComponentProps<"span">) {
+ return (
+
+ )
+}
+
+function DropdownMenuSub({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function DropdownMenuSubTrigger({
+ className,
+ inset,
+ children,
+ ...props
+}: React.ComponentProps & {
+ inset?: boolean
+}) {
+ return (
+
+ {children}
+
+
+ )
+}
+
+function DropdownMenuSubContent({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+export {
+ DropdownMenu,
+ DropdownMenuPortal,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuGroup,
+ DropdownMenuLabel,
+ DropdownMenuItem,
+ DropdownMenuCheckboxItem,
+ DropdownMenuRadioGroup,
+ DropdownMenuRadioItem,
+ DropdownMenuSeparator,
+ DropdownMenuShortcut,
+ DropdownMenuSub,
+ DropdownMenuSubTrigger,
+ DropdownMenuSubContent,
+}
diff --git a/components/ui/select.tsx b/components/ui/select.tsx
new file mode 100644
index 0000000..88302a8
--- /dev/null
+++ b/components/ui/select.tsx
@@ -0,0 +1,190 @@
+"use client"
+
+import * as React from "react"
+import * as SelectPrimitive from "@radix-ui/react-select"
+import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
+
+import { cn } from "@/lib/utils"
+
+function Select({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SelectGroup({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SelectValue({
+ ...props
+}: React.ComponentProps) {
+ return
+}
+
+function SelectTrigger({
+ className,
+ size = "default",
+ children,
+ ...props
+}: React.ComponentProps & {
+ size?: "sm" | "default"
+}) {
+ return (
+
+ {children}
+
+
+
+
+ )
+}
+
+function SelectContent({
+ className,
+ children,
+ position = "item-aligned",
+ align = "center",
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+ {children}
+
+
+
+
+ )
+}
+
+function SelectLabel({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SelectItem({
+ className,
+ children,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+
+
+
+ {children}
+
+ )
+}
+
+function SelectSeparator({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+ )
+}
+
+function SelectScrollUpButton({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+function SelectScrollDownButton({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+export {
+ Select,
+ SelectContent,
+ SelectGroup,
+ SelectItem,
+ SelectLabel,
+ SelectScrollDownButton,
+ SelectScrollUpButton,
+ SelectSeparator,
+ SelectTrigger,
+ SelectValue,
+}
diff --git a/components/ui/switch.tsx b/components/ui/switch.tsx
new file mode 100644
index 0000000..d8551a4
--- /dev/null
+++ b/components/ui/switch.tsx
@@ -0,0 +1,31 @@
+"use client"
+
+import * as React from "react"
+import * as SwitchPrimitive from "@radix-ui/react-switch"
+
+import { cn } from "@/lib/utils"
+
+function Switch({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+export { Switch }
diff --git a/lib/site-settings.ts b/lib/site-settings.ts
new file mode 100644
index 0000000..46585da
--- /dev/null
+++ b/lib/site-settings.ts
@@ -0,0 +1,8 @@
+import { createClient } from "@/lib/supabase-server"
+import { cache } from "react"
+
+export const getSiteSettings = cache(async () => {
+ const supabase = createClient()
+ const { data } = await supabase.from('site_settings').select('*').single()
+ return data
+})
diff --git a/lib/supabase-browser.ts b/lib/supabase-browser.ts
new file mode 100644
index 0000000..2719869
--- /dev/null
+++ b/lib/supabase-browser.ts
@@ -0,0 +1,8 @@
+import { createBrowserClient } from '@supabase/ssr'
+
+export function createClient() {
+ return createBrowserClient(
+ process.env.NEXT_PUBLIC_SUPABASE_URL!,
+ process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
+ )
+}
diff --git a/make_admin.sql b/make_admin.sql
new file mode 100644
index 0000000..b7a94b1
--- /dev/null
+++ b/make_admin.sql
@@ -0,0 +1,8 @@
+
+-- Insert profile for the existing user and make them admin
+insert into public.profiles (id, role, full_name)
+select id, 'admin', 'Sistem Yöneticisi'
+from auth.users
+where email = 'kenankaraerr@hotmail.com'
+on conflict (id) do update
+set role = 'admin';
diff --git a/next.config.mjs b/next.config.mjs
index 4678774..b71f58b 100644
--- a/next.config.mjs
+++ b/next.config.mjs
@@ -1,4 +1,13 @@
/** @type {import('next').NextConfig} */
-const nextConfig = {};
+const nextConfig = {
+ reactStrictMode: true,
+ webpack: (config) => {
+ // Suppress cache serialization warnings
+ config.infrastructureLogging = {
+ level: 'error',
+ };
+ return config;
+ },
+};
export default nextConfig;
diff --git a/package-lock.json b/package-lock.json
index 5eeac68..b1a5904 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,18 +9,24 @@
"version": "0.1.0",
"dependencies": {
"@hookform/resolvers": "^5.2.2",
+ "@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-dialog": "^1.1.15",
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-navigation-menu": "^1.2.14",
+ "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
+ "@radix-ui/react-switch": "^1.2.6",
"@supabase/ssr": "^0.8.0",
"@supabase/supabase-js": "^2.89.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
"lucide-react": "^0.378.0",
"next": "14.2.16",
+ "next-themes": "^0.4.6",
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.70.0",
@@ -152,6 +158,44 @@
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
+ "node_modules/@floating-ui/core": {
+ "version": "1.7.3",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz",
+ "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.7.4",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz",
+ "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.7.3",
+ "@floating-ui/utils": "^0.2.10"
+ }
+ },
+ "node_modules/@floating-ui/react-dom": {
+ "version": "2.1.6",
+ "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz",
+ "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/dom": "^1.7.4"
+ },
+ "peerDependencies": {
+ "react": ">=16.8.0",
+ "react-dom": ">=16.8.0"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.10",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
+ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
+ "license": "MIT"
+ },
"node_modules/@hookform/resolvers": {
"version": "5.2.2",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz",
@@ -520,12 +564,106 @@
"node": ">=14"
}
},
+ "node_modules/@radix-ui/number": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.1.tgz",
+ "integrity": "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==",
+ "license": "MIT"
+ },
"node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
+ "node_modules/@radix-ui/react-arrow": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
+ "integrity": "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-avatar": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.11.tgz",
+ "integrity": "sha512-0Qk603AHGV28BOBO34p7IgD5m+V5Sg/YovfayABkoDDBM5d3NCx0Mp4gGrjzLGes1jV5eNOE1r3itqOR33VC6Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.3",
+ "@radix-ui/react-primitive": "2.1.4",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-is-hydrated": "0.1.0",
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-context": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.3.tgz",
+ "integrity": "sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.4.tgz",
+ "integrity": "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.4"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-collection": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
@@ -696,6 +834,35 @@
}
}
},
+ "node_modules/@radix-ui/react-dropdown-menu": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.16.tgz",
+ "integrity": "sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-menu": "2.1.16",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-focus-guards": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.3.tgz",
@@ -800,6 +967,64 @@
}
}
},
+ "node_modules/@radix-ui/react-menu": {
+ "version": "2.1.16",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.16.tgz",
+ "integrity": "sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-presence": "1.1.5",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-roving-focus": "1.1.11",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-navigation-menu": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz",
@@ -836,6 +1061,38 @@
}
}
},
+ "node_modules/@radix-ui/react-popper": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
+ "integrity": "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/react-dom": "^2.0.0",
+ "@radix-ui/react-arrow": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-rect": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1",
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-portal": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.9.tgz",
@@ -925,6 +1182,98 @@
}
}
},
+ "node_modules/@radix-ui/react-roving-focus": {
+ "version": "1.1.11",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.11.tgz",
+ "integrity": "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select": {
+ "version": "2.2.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.2.6.tgz",
+ "integrity": "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/number": "1.1.1",
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-collection": "1.1.7",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-direction": "1.1.1",
+ "@radix-ui/react-dismissable-layer": "1.1.11",
+ "@radix-ui/react-focus-guards": "1.1.3",
+ "@radix-ui/react-focus-scope": "1.1.7",
+ "@radix-ui/react-id": "1.1.1",
+ "@radix-ui/react-popper": "1.2.8",
+ "@radix-ui/react-portal": "1.1.9",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-slot": "1.2.3",
+ "@radix-ui/react-use-callback-ref": "1.1.1",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-layout-effect": "1.1.1",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-visually-hidden": "1.2.3",
+ "aria-hidden": "^1.2.4",
+ "react-remove-scroll": "^2.6.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
+ "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-compose-refs": "1.1.2"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-separator": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz",
@@ -989,6 +1338,35 @@
}
}
},
+ "node_modules/@radix-ui/react-switch": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz",
+ "integrity": "sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.3",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
@@ -1059,6 +1437,24 @@
}
}
},
+ "node_modules/@radix-ui/react-use-is-hydrated": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-is-hydrated/-/react-use-is-hydrated-0.1.0.tgz",
+ "integrity": "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.5.0"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
@@ -1089,6 +1485,42 @@
}
}
},
+ "node_modules/@radix-ui/react-use-rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.1.tgz",
+ "integrity": "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/rect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@radix-ui/react-use-size": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.1.tgz",
+ "integrity": "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-use-layout-effect": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-visually-hidden": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
@@ -1112,6 +1544,12 @@
}
}
},
+ "node_modules/@radix-ui/rect": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
+ "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
+ "license": "MIT"
+ },
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@@ -2645,6 +3083,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/date-fns": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
+ "integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/kossnocorp"
+ }
+ },
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
@@ -4929,6 +5377,16 @@
}
}
},
+ "node_modules/next-themes": {
+ "version": "0.4.6",
+ "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
+ "integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
+ }
+ },
"node_modules/next/node_modules/postcss": {
"version": "8.4.31",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz",
@@ -6867,6 +7325,15 @@
}
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
diff --git a/package.json b/package.json
index 52f8d22..50c1bdd 100644
--- a/package.json
+++ b/package.json
@@ -10,18 +10,24 @@
},
"dependencies": {
"@hookform/resolvers": "^5.2.2",
+ "@radix-ui/react-avatar": "^1.1.11",
"@radix-ui/react-dialog": "^1.1.15",
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-navigation-menu": "^1.2.14",
+ "@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
+ "@radix-ui/react-switch": "^1.2.6",
"@supabase/ssr": "^0.8.0",
"@supabase/supabase-js": "^2.89.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
+ "date-fns": "^4.1.0",
"embla-carousel-react": "^8.6.0",
"lucide-react": "^0.378.0",
"next": "14.2.16",
+ "next-themes": "^0.4.6",
"react": "^18",
"react-dom": "^18",
"react-hook-form": "^7.70.0",
diff --git a/scripts/test-supabase.js b/scripts/test-supabase.js
new file mode 100644
index 0000000..4360e74
--- /dev/null
+++ b/scripts/test-supabase.js
@@ -0,0 +1,50 @@
+const fs = require('fs');
+const path = require('path');
+const { createClient } = require('@supabase/supabase-js');
+
+const envPath = path.resolve(__dirname, '../.env.local');
+
+console.log('Reading env from:', envPath);
+
+try {
+ const envContent = fs.readFileSync(envPath, 'utf8');
+ const envVars = {};
+ envContent.split('\n').forEach(line => {
+ const parts = line.split('=');
+ if (parts.length >= 2) {
+ const key = parts[0].trim();
+ const value = parts.slice(1).join('=').trim().replace(/"/g, '');
+ envVars[key] = value;
+ }
+ });
+
+ const supabaseUrl = envVars['NEXT_PUBLIC_SUPABASE_URL'];
+ const supabaseKey = envVars['NEXT_PUBLIC_SUPABASE_ANON_KEY'];
+
+ if (!supabaseUrl || !supabaseKey) {
+ console.error('❌ Missing Supabase Environment Variables in .env.local');
+ console.log('URL:', supabaseUrl ? 'Set' : 'Missing');
+ console.log('Key:', supabaseKey ? 'Set' : 'Missing');
+ process.exit(1);
+ }
+
+ console.log('Checking connection to:', supabaseUrl);
+ const supabase = createClient(supabaseUrl, supabaseKey);
+
+ (async () => {
+ try {
+ const { error } = await supabase.auth.getSession();
+ if (error) {
+ console.error('❌ Connection Failed:', error.message);
+ process.exit(1);
+ } else {
+ console.log('✅ Connection Successful! Supabase is reachable and keys are valid.');
+ }
+ } catch (err) {
+ console.error('❌ Unexpected Error:', err);
+ }
+ })();
+
+} catch (err) {
+ console.error('❌ Could not read .env.local:', err.message);
+}
diff --git a/supabase_schema_additions.sql b/supabase_schema_additions.sql
new file mode 100644
index 0000000..43e935b
--- /dev/null
+++ b/supabase_schema_additions.sql
@@ -0,0 +1,58 @@
+
+-- Create profiles table
+create table if not exists profiles (
+ id uuid references auth.users on delete cascade primary key,
+ role text not null default 'user' check (role in ('admin', 'user')),
+ full_name text,
+ created_at timestamp with time zone default timezone('utc'::text, now()) not null
+);
+
+-- Enable RLS for profiles
+alter table profiles enable row level security;
+
+-- Policies for profiles
+create policy "Public profiles are viewable by everyone."
+ on profiles for select
+ using ( true );
+
+create policy "Users can insert their own profile."
+ on profiles for insert
+ with check ( auth.uid() = id );
+
+create policy "Users can update own profile."
+ on profiles for update
+ using ( auth.uid() = id );
+
+-- Create site_settings table
+create table if not exists site_settings (
+ id bigint primary key generated always as identity,
+ site_title text not null default 'ParaKasa',
+ site_description text,
+ contact_email text,
+ contact_phone text,
+ logo_url text,
+ currency text default 'TRY',
+ updated_at timestamp with time zone default timezone('utc'::text, now()) not null
+);
+
+-- Enable RLS for site_settings
+alter table site_settings enable row level security;
+
+-- Policies for site_settings
+create policy "Site settings are viewable by everyone."
+ on site_settings for select
+ using ( true );
+
+create policy "Only admins can update site settings."
+ on site_settings for update
+ using (
+ exists (
+ select 1 from profiles
+ where profiles.id = auth.uid() and profiles.role = 'admin'
+ )
+ );
+
+-- Initialize default site settings if empty
+insert into site_settings (site_title, contact_email)
+select 'ParaKasa', 'info@parakasa.com'
+where not exists (select 1 from site_settings);
diff --git a/supabase_schema_categories.sql b/supabase_schema_categories.sql
new file mode 100644
index 0000000..3fb6f78
--- /dev/null
+++ b/supabase_schema_categories.sql
@@ -0,0 +1,44 @@
+-- Create categories table
+create table if not exists categories (
+ id uuid default gen_random_uuid() primary key,
+ name text not null,
+ slug text not null unique,
+ description text,
+ image_url text,
+ created_at timestamp with time zone default timezone('utc'::text, now()) not null
+);
+
+-- Enable RLS
+alter table categories enable row level security;
+
+-- Policies
+create policy "Public categories are viewable by everyone."
+ on categories for select
+ using ( true );
+
+create policy "Admins can insert categories."
+ on categories for insert
+ with check (
+ exists (
+ select 1 from profiles
+ where profiles.id = auth.uid() and profiles.role = 'admin'
+ )
+ );
+
+create policy "Admins can update categories."
+ on categories for update
+ using (
+ exists (
+ select 1 from profiles
+ where profiles.id = auth.uid() and profiles.role = 'admin'
+ )
+ );
+
+create policy "Admins can delete categories."
+ on categories for delete
+ using (
+ exists (
+ select 1 from profiles
+ where profiles.id = auth.uid() and profiles.role = 'admin'
+ )
+ );
diff --git a/supabase_schema_products_category_fk.sql b/supabase_schema_products_category_fk.sql
new file mode 100644
index 0000000..9d4cc5b
--- /dev/null
+++ b/supabase_schema_products_category_fk.sql
@@ -0,0 +1,6 @@
+-- Add category_id to products table
+alter table products
+add column if not exists category_id uuid references categories(id) on delete set null;
+
+-- Optional: Create an index for better performance
+create index if not exists idx_products_category_id on products(category_id);