diff --git a/add_email_field.sql b/add_email_field.sql
new file mode 100644
index 0000000..e1d6d21
--- /dev/null
+++ b/add_email_field.sql
@@ -0,0 +1,3 @@
+INSERT INTO public.site_contents (key, value, type, section)
+VALUES ('contact_email', 'info@ruyadugun.com', 'text', 'contact')
+ON CONFLICT (key) DO NOTHING;
diff --git a/add_is_hero_column.sql b/add_is_hero_column.sql
new file mode 100644
index 0000000..40ed517
--- /dev/null
+++ b/add_is_hero_column.sql
@@ -0,0 +1 @@
+ALTER TABLE public.gallery ADD COLUMN IF NOT EXISTS is_hero BOOLEAN DEFAULT false;
diff --git a/add_map_field.sql b/add_map_field.sql
new file mode 100644
index 0000000..22fcca3
--- /dev/null
+++ b/add_map_field.sql
@@ -0,0 +1,3 @@
+INSERT INTO public.site_contents (key, value, type, section)
+VALUES ('contact_map_embed', '', 'html', 'contact')
+ON CONFLICT (key) DO NOTHING;
diff --git a/add_social_fields.sql b/add_social_fields.sql
new file mode 100644
index 0000000..d8a830e
--- /dev/null
+++ b/add_social_fields.sql
@@ -0,0 +1,5 @@
+INSERT INTO public.site_contents (key, value, type, section) VALUES
+('social_instagram', 'https://instagram.com/', 'text', 'contact'),
+('social_facebook', 'https://facebook.com/', 'text', 'contact'),
+('social_twitter', 'https://twitter.com/', 'text', 'contact')
+ON CONFLICT (key) DO NOTHING;
diff --git a/add_video_url_column.sql b/add_video_url_column.sql
new file mode 100644
index 0000000..7054b79
--- /dev/null
+++ b/add_video_url_column.sql
@@ -0,0 +1 @@
+ALTER TABLE public.gallery ADD COLUMN IF NOT EXISTS video_url TEXT;
diff --git a/cms_migration.sql b/cms_migration.sql
new file mode 100644
index 0000000..9161a44
--- /dev/null
+++ b/cms_migration.sql
@@ -0,0 +1,123 @@
+-- Enable UUID extension if not enabled
+CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
+
+-- 1. Site Contents Table (Genel İçerikler)
+CREATE TABLE IF NOT EXISTS public.site_contents (
+ key TEXT PRIMARY KEY,
+ value TEXT,
+ type TEXT DEFAULT 'text', -- 'text', 'image_url', 'html', 'json'
+ section TEXT, -- 'home', 'about', 'contact', 'footer'
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
+);
+
+-- RLS: Public read, Admin write
+ALTER TABLE public.site_contents ENABLE ROW LEVEL SECURITY;
+
+DROP POLICY IF EXISTS "Allow public read access on site_contents" ON public.site_contents;
+CREATE POLICY "Allow public read access on site_contents"
+ON public.site_contents FOR SELECT TO anon, authenticated
+USING (true);
+
+DROP POLICY IF EXISTS "Allow authenticated update on site_contents" ON public.site_contents;
+CREATE POLICY "Allow authenticated update on site_contents"
+ON public.site_contents FOR UPDATE TO authenticated
+USING (true)
+WITH CHECK (true);
+
+DROP POLICY IF EXISTS "Allow authenticated insert on site_contents" ON public.site_contents;
+CREATE POLICY "Allow authenticated insert on site_contents"
+ON public.site_contents FOR INSERT TO authenticated
+WITH CHECK (true);
+
+-- 2. Services Table (Hizmetler)
+CREATE TABLE IF NOT EXISTS public.services (
+ id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
+ title TEXT NOT NULL,
+ description TEXT,
+ image_url TEXT,
+ "order" INTEGER DEFAULT 0,
+ is_active BOOLEAN DEFAULT true,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
+);
+
+ALTER TABLE public.services ENABLE ROW LEVEL SECURITY;
+
+DROP POLICY IF EXISTS "Allow public read on services" ON public.services;
+CREATE POLICY "Allow public read on services"
+ON public.services FOR SELECT TO anon, authenticated
+USING (is_active = true OR auth.role() = 'authenticated');
+
+DROP POLICY IF EXISTS "Allow admin all on services" ON public.services;
+CREATE POLICY "Allow admin all on services"
+ON public.services FOR ALL TO authenticated
+USING (true)
+WITH CHECK (true);
+
+-- 3. Gallery Table (Galeri)
+CREATE TABLE IF NOT EXISTS public.gallery (
+ id UUID DEFAULT uuid_generate_v4() PRIMARY KEY,
+ image_url TEXT NOT NULL,
+ caption TEXT,
+ category TEXT DEFAULT 'Genel',
+ "order" INTEGER DEFAULT 0,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT timezone('utc'::text, now()) NOT NULL
+);
+
+ALTER TABLE public.gallery ENABLE ROW LEVEL SECURITY;
+
+DROP POLICY IF EXISTS "Allow public read on gallery" ON public.gallery;
+CREATE POLICY "Allow public read on gallery"
+ON public.gallery FOR SELECT TO anon, authenticated
+USING (true);
+
+DROP POLICY IF EXISTS "Allow admin all on gallery" ON public.gallery;
+CREATE POLICY "Allow admin all on gallery"
+ON public.gallery FOR ALL TO authenticated
+USING (true)
+WITH CHECK (true);
+
+
+-- 4. Storage Bucket for Public Site
+INSERT INTO storage.buckets (id, name, public)
+VALUES ('public-site', 'public-site', true)
+ON CONFLICT (id) DO NOTHING;
+
+-- Storage Policies (Renamed to avoid conflicts)
+-- Note: 'storage.objects' is a shared table, so policy names must be unique or scoped.
+
+-- Allow public read SPECIFIC to this bucket
+DROP POLICY IF EXISTS "Public Access public-site" ON storage.objects;
+CREATE POLICY "Public Access public-site"
+ON storage.objects FOR SELECT
+TO public
+USING ( bucket_id = 'public-site' );
+
+-- Allow authenticated upload/delete SPECIFIC to this bucket
+DROP POLICY IF EXISTS "Authenticated Insert public-site" ON storage.objects;
+CREATE POLICY "Authenticated Insert public-site"
+ON storage.objects FOR INSERT
+TO authenticated
+WITH CHECK ( bucket_id = 'public-site' );
+
+DROP POLICY IF EXISTS "Authenticated Update public-site" ON storage.objects;
+CREATE POLICY "Authenticated Update public-site"
+ON storage.objects FOR UPDATE
+TO authenticated
+USING ( bucket_id = 'public-site' );
+
+DROP POLICY IF EXISTS "Authenticated Delete public-site" ON storage.objects;
+CREATE POLICY "Authenticated Delete public-site"
+ON storage.objects FOR DELETE
+TO authenticated
+USING ( bucket_id = 'public-site' );
+
+-- Seed Data (Initial Content)
+INSERT INTO public.site_contents (key, value, type, section) VALUES
+('site_title', 'Rüya Düğün Salonu', 'text', 'general'),
+('hero_title', 'Hayallerinizdeki Düğün İçin', 'text', 'home'),
+('hero_subtitle', 'Unutulmaz anlar, profesyonel hizmet ve şık atmosfer.', 'text', 'home'),
+('contact_phone', '+90 555 123 45 67', 'text', 'contact'),
+('contact_address', 'Atatürk Mah. Karanfil Sok. No:5, İstanbul', 'text', 'contact'),
+('site_logo', '', 'image_url', 'general')
+ON CONFLICT (key) DO NOTHING;
diff --git a/next.config.ts b/next.config.ts
index 8d0a4a1..354a29b 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -10,6 +10,18 @@ const nextConfig: NextConfig = {
port: '',
pathname: '/storage/v1/object/public/**',
},
+ {
+ protocol: 'https',
+ hostname: 'img.youtube.com',
+ port: '',
+ pathname: '/**',
+ },
+ {
+ protocol: 'https',
+ hostname: 'images.unsplash.com',
+ port: '',
+ pathname: '/**',
+ },
],
},
};
diff --git a/package-lock.json b/package-lock.json
index f75e2e7..46d5311 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,6 +19,8 @@
"@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",
+ "@radix-ui/react-visually-hidden": "^1.2.4",
"@supabase/ssr": "^0.8.0",
"@supabase/supabase-js": "^2.86.0",
"class-variance-authority": "^0.7.1",
@@ -2563,6 +2565,29 @@
}
}
},
+ "node_modules/@radix-ui/react-select/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",
+ "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
+ "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-separator": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz",
@@ -2604,6 +2629,91 @@
}
}
},
+ "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-switch/node_modules/@radix-ui/react-context": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
+ "integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
+ "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-switch/node_modules/@radix-ui/react-primitive": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
+ "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-slot": "1.2.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-switch/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-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",
@@ -2759,12 +2869,12 @@
}
},
"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",
- "integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.4.tgz",
+ "integrity": "sha512-kaeiyGCe844dkb9AVF+rb4yTyb1LiLN/e3es3nLiRyN4dC8AduBYPMnnNlDjX2VDOcvDEiPnRNMJeWCfsX0txg==",
"license": "MIT",
"dependencies": {
- "@radix-ui/react-primitive": "2.1.3"
+ "@radix-ui/react-primitive": "2.1.4"
},
"peerDependencies": {
"@types/react": "*",
@@ -2781,47 +2891,6 @@
}
}
},
- "node_modules/@radix-ui/react-visually-hidden/node_modules/@radix-ui/react-primitive": {
- "version": "2.1.3",
- "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
- "integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
- "license": "MIT",
- "dependencies": {
- "@radix-ui/react-slot": "1.2.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-visually-hidden/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/rect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.1.tgz",
diff --git a/package.json b/package.json
index 8197ab2..3adbd17 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,8 @@
"@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",
+ "@radix-ui/react-visually-hidden": "^1.2.4",
"@supabase/ssr": "^0.8.0",
"@supabase/supabase-js": "^2.86.0",
"class-variance-authority": "^0.7.1",
@@ -51,4 +53,4 @@
"tw-animate-css": "^1.4.0",
"typescript": "^5"
}
-}
\ No newline at end of file
+}
diff --git a/src/app/(public)/galeri/page.tsx b/src/app/(public)/galeri/page.tsx
new file mode 100644
index 0000000..d814567
--- /dev/null
+++ b/src/app/(public)/galeri/page.tsx
@@ -0,0 +1,37 @@
+import { GalleryGrid } from "@/components/public/gallery-grid"
+import { createClient } from "@/lib/supabase/server"
+import { GalleryItem } from "@/types/cms"
+
+export const revalidate = 0 // Force dynamic behavior to ensure fresh data
+
+export default async function GalleryAllPage() {
+ const supabase = await createClient()
+
+ // Fetch ALL gallery items, ordered by 'order'
+ const { data: gallery } = await supabase
+ .from('gallery')
+ .select('*')
+ .order('order')
+
+ return (
+
+
+
+
Fotoğraf Galerisi
+
+
+ En özel anlarınıza şahitlik ettiğimiz muhteşem düğünlerden kareler.
+
+
+
+ {(!gallery || gallery.length === 0) ? (
+
+ Henüz galeriye fotoğraf eklenmemiş.
+
+ ) : (
+
+ )}
+
+
+ )
+}
diff --git a/src/app/(public)/layout.tsx b/src/app/(public)/layout.tsx
new file mode 100644
index 0000000..9449b48
--- /dev/null
+++ b/src/app/(public)/layout.tsx
@@ -0,0 +1,45 @@
+import { SiteHeader } from "@/components/public/site-header"
+import { SiteFooter } from "@/components/public/site-footer"
+
+interface PublicLayoutProps {
+ children: React.ReactNode
+}
+
+import { createClient } from "@/lib/supabase/server"
+
+export default async function PublicLayout({ children }: PublicLayoutProps) {
+ const supabase = await createClient()
+ const { data: contents } = await supabase
+ .from('site_contents')
+ .select('key, value')
+ .in('key', ['site_title', 'site_logo', 'contact_phone', 'contact_address', 'contact_email', 'hero_subtitle', 'social_instagram', 'social_facebook', 'social_twitter'])
+
+ const siteTitle = contents?.find(c => c.key === 'site_title')?.value || "Rüya Düğün Salonu"
+ const siteLogo = contents?.find(c => c.key === 'site_logo')?.value
+ const phone = contents?.find(c => c.key === 'contact_phone')?.value
+ const address = contents?.find(c => c.key === 'contact_address')?.value
+ const email = contents?.find(c => c.key === 'contact_email')?.value
+ const description = contents?.find(c => c.key === 'hero_subtitle')?.value
+
+ // Social Media
+ const instagram = contents?.find(c => c.key === 'social_instagram')?.value
+ const facebook = contents?.find(c => c.key === 'social_facebook')?.value
+ const twitter = contents?.find(c => c.key === 'social_twitter')?.value
+
+ return (
+
+
+
+ {children}
+
+
+
+ )
+}
diff --git a/src/app/(public)/page.tsx b/src/app/(public)/page.tsx
new file mode 100644
index 0000000..2ec8256
--- /dev/null
+++ b/src/app/(public)/page.tsx
@@ -0,0 +1,186 @@
+import { Button } from "@/components/ui/button"
+import { HeroCarousel } from "@/components/public/hero-carousel"
+import Link from "next/link"
+import { GalleryGrid } from "@/components/public/gallery-grid"
+import { createClient } from "@/lib/supabase/server"
+import Image from "next/image"
+import { Service, GalleryItem, SiteContent } from "@/types/cms"
+import { Card, CardContent } from "@/components/ui/card"
+import { MapPin, Phone, Mail } from "lucide-react"
+
+async function getPublicData() {
+ const supabase = await createClient()
+
+ const [
+ { data: contents },
+ { data: services },
+ { data: gallery }
+ ] = await Promise.all([
+ supabase.from('site_contents').select('*'),
+ supabase.from('services').select('*').eq('is_active', true).order('order'),
+ supabase.from('gallery').select('*').order('order').limit(8)
+ ])
+
+ // Helper to get content by key
+ const getContent = (key: string) => {
+ return (contents as SiteContent[])?.find(c => c.key === key)?.value || ''
+ }
+
+ const heroImages = gallery?.filter((item: GalleryItem) => item.is_hero) || []
+
+ return {
+ contents: contents as SiteContent[],
+ services: services as Service[],
+ gallery: (gallery as GalleryItem[])?.slice(0, 8),
+ heroImages: heroImages as GalleryItem[],
+ getContent
+ }
+}
+
+export default async function LandingPage() {
+ const { services, gallery, heroImages, getContent } = await getPublicData()
+
+ const heroTitle = getContent('hero_title')
+ const heroSubtitle = getContent('hero_subtitle')
+ const contactPhone = getContent('contact_phone')
+ const contactAddress = getContent('contact_address')
+ const contactEmail = getContent('contact_email')
+ const mapEmbed = getContent('contact_map_embed')
+ // Fallback image if not set? No, we use a placeholder or check if content exists.
+
+ return (
+
+ {/* HER SECTİON */}
+
+
+
+
+ {heroTitle || 'Hayallerinizdeki Düğün'}
+
+
+ {heroSubtitle || 'Unutulmaz anlar için...'}
+
+
+
+
+
+ {/* SERVICES SECTION */}
+
+
+
+
Hizmetlerimiz
+
+
+ Size özel sunduğumuz ayrıcalıklar ve profesyonel hizmetler.
+
+
+
+
+ {services?.map((service) => (
+
+
+ {service.image_url ? (
+
+ ) : (
+
+ Görsel Yok
+
+ )}
+
+
+
+ {service.title}
+
+
+ {service.description}
+
+
+
+ ))}
+
+ {(!services || services.length === 0) && (
+
Henüz hizmet eklenmemiş.
+ )}
+
+
+
+ {/* GALLERY PREVIEW SECTION */}
+
+
+
+
Galeri
+
+
En mutlu anlarınızdan kareler.
+
+
+
+
+
+
+
+
+
+
+
+ {/* CONTACT SECTION */}
+
+
+ )
+}
diff --git a/src/app/dashboard/cms/content/actions.ts b/src/app/dashboard/cms/content/actions.ts
new file mode 100644
index 0000000..70ea32d
--- /dev/null
+++ b/src/app/dashboard/cms/content/actions.ts
@@ -0,0 +1,34 @@
+'use server'
+
+import { createClient } from "@/lib/supabase/server"
+import { SiteContent } from "@/types/cms"
+import { revalidatePath } from "next/cache"
+
+export async function updateSiteContent(updates: SiteContent[]) {
+ const supabase = await createClient()
+
+ try {
+ // Upsert all updates
+ // Note: Supabase upsert accepts an array of objects
+ const { error } = await supabase
+ .from('site_contents')
+ .upsert(
+ updates.map(({ key, value, type, section }) => ({
+ key,
+ value,
+ type,
+ section,
+ updated_at: new Date().toISOString()
+ }))
+ )
+
+ if (error) throw error
+
+ revalidatePath('/dashboard/cms/content')
+ revalidatePath('/', 'layout') // Revalidate public root layout (header title)
+ return { success: true }
+ } catch (error) {
+ console.error('Content update error:', error)
+ return { success: false, error: 'İçerik güncellenirken bir hata oluştu.' }
+ }
+}
diff --git a/src/app/dashboard/cms/content/content-form.tsx b/src/app/dashboard/cms/content/content-form.tsx
new file mode 100644
index 0000000..852acef
--- /dev/null
+++ b/src/app/dashboard/cms/content/content-form.tsx
@@ -0,0 +1,129 @@
+'use client'
+
+import { useState } from "react"
+import { SiteContent } from "@/types/cms"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { Textarea } from "@/components/ui/textarea"
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from "@/components/ui/card"
+import { ImageUpload } from "@/components/ui/image-upload"
+import { updateSiteContent } from "./actions"
+import { toast } from "sonner"
+import { Save, Loader2 } from "lucide-react"
+import { cn } from "@/lib/utils"
+
+interface ContentFormProps {
+ initialContent: SiteContent[]
+}
+
+const SECTIONS = [
+ { id: 'general', label: 'Genel Ayarlar' },
+ { id: 'home', label: 'Anasayfa' },
+ { id: 'contact', label: 'İletişim' },
+]
+
+export function ContentForm({ initialContent }: ContentFormProps) {
+ const [contents, setContents] = useState(initialContent)
+ const [loading, setLoading] = useState(false)
+ const [activeSection, setActiveSection] = useState('general')
+
+ const handleChange = (key: string, value: string) => {
+ setContents(prev => prev.map(item =>
+ item.key === key ? { ...item, value } : item
+ ))
+ }
+
+ const onSubmit = async () => {
+ setLoading(true)
+ try {
+ const result = await updateSiteContent(contents)
+ if (result.success) {
+ toast.success("İçerikler başarıyla güncellendi")
+ } else {
+ toast.error(result.error)
+ }
+ } catch (error) {
+ toast.error("Bir hata oluştu")
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const filteredContent = contents.filter(item => item.section === activeSection)
+
+ return (
+
+ {/* Custom Tabs */}
+
+ {SECTIONS.map((section) => (
+
+ ))}
+
+
+
+
+ {SECTIONS.find(s => s.id === activeSection)?.label}
+
+ Bu bölümdeki içerikleri aşağıdan düzenleyebilirsiniz.
+
+
+
+ {filteredContent.length === 0 && (
+ Bu bölümde henüz ayar bulunmuyor.
+ )}
+ {filteredContent.map((item) => (
+
+
+
+ {item.type === 'image_url' ? (
+
handleChange(item.key, url)}
+ bucketName="public-site"
+ />
+ ) : item.type === 'long_text' || item.type === 'html' || item.key.includes('subtitle') || item.key.includes('address') ? (
+
+ ))}
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/app/dashboard/cms/content/page.tsx b/src/app/dashboard/cms/content/page.tsx
new file mode 100644
index 0000000..979e149
--- /dev/null
+++ b/src/app/dashboard/cms/content/page.tsx
@@ -0,0 +1,28 @@
+import { createClient } from "@/lib/supabase/server"
+import { ContentForm } from "./content-form"
+import { SiteContent } from "@/types/cms"
+
+export default async function ContentPage() {
+ const supabase = await createClient()
+
+ const { data: contents } = await supabase
+ .from('site_contents')
+ .select('*')
+ .order('key')
+
+ // CMS migration script creates default keys.
+ // If table is empty, we might want to seed it or just show empty.
+ // Assuming migration ran, we have data.
+
+ return (
+
+
+
Genel İçerik Yönetimi
+
+ Site başlığı, sloganlar, iletişim bilgileri ve logoları buradan yönetebilirsiniz.
+
+
+
+
+ )
+}
diff --git a/src/app/dashboard/cms/gallery/actions.ts b/src/app/dashboard/cms/gallery/actions.ts
new file mode 100644
index 0000000..ad5ca4b
--- /dev/null
+++ b/src/app/dashboard/cms/gallery/actions.ts
@@ -0,0 +1,93 @@
+'use server'
+
+import { createClient } from "@/lib/supabase/server"
+import { GalleryItem } from "@/types/cms"
+import { revalidatePath } from "next/cache"
+
+export async function getGalleryItems() {
+ const supabase = await createClient()
+ const { data } = await supabase
+ .from('gallery')
+ .select('*')
+ .order('order', { ascending: true })
+
+ return data as GalleryItem[] || []
+}
+
+export async function createGalleryItem(formData: FormData) {
+ const supabase = await createClient()
+
+ const image_url = formData.get('image_url') as string
+ const caption = formData.get('caption') as string
+ const category = formData.get('category') as string
+ const order = Number(formData.get('order') || 0)
+ const is_hero = formData.get('is_hero') === 'true'
+ const video_url = formData.get('video_url') as string
+
+ const { error } = await supabase
+ .from('gallery')
+ .insert({
+ image_url,
+ caption,
+ category,
+ order,
+ is_hero,
+ video_url
+ })
+
+ if (error) {
+ console.error('Create gallery item error:', error)
+ return { success: false, error: 'Fotoğraf eklenirken hata oluştu.' }
+ }
+
+ revalidatePath('/dashboard/cms/gallery')
+ return { success: true }
+}
+
+export async function updateGalleryItem(id: string, formData: FormData) {
+ const supabase = await createClient()
+
+ const image_url = formData.get('image_url') as string
+ const caption = formData.get('caption') as string
+ const category = formData.get('category') as string
+ const order = Number(formData.get('order') || 0)
+ const is_hero = formData.get('is_hero') === 'true'
+ const video_url = formData.get('video_url') as string
+
+ const { error } = await supabase
+ .from('gallery')
+ .update({
+ image_url,
+ caption,
+ category,
+ order,
+ is_hero,
+ video_url
+ })
+ .eq('id', id)
+
+ if (error) {
+ console.error('Update gallery item error:', error)
+ return { success: false, error: 'Fotoğraf güncellenirken hata oluştu.' }
+ }
+
+ revalidatePath('/dashboard/cms/gallery')
+ return { success: true }
+}
+
+export async function deleteGalleryItem(id: string) {
+ const supabase = await createClient()
+
+ const { error } = await supabase
+ .from('gallery')
+ .delete()
+ .eq('id', id)
+
+ if (error) {
+ console.error('Delete gallery item error:', error)
+ return { success: false, error: 'Fotoğraf silinirken hata oluştu.' }
+ }
+
+ revalidatePath('/dashboard/cms/gallery')
+ return { success: true }
+}
diff --git a/src/app/dashboard/cms/gallery/gallery-form.tsx b/src/app/dashboard/cms/gallery/gallery-form.tsx
new file mode 100644
index 0000000..ece7864
--- /dev/null
+++ b/src/app/dashboard/cms/gallery/gallery-form.tsx
@@ -0,0 +1,193 @@
+'use client'
+
+import { GalleryItem } from "@/types/cms"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { ImageUpload } from "@/components/ui/image-upload"
+import { useState } from "react"
+import { createGalleryItem, updateGalleryItem } from "./actions"
+import { toast } from "sonner"
+import { Switch } from "@/components/ui/switch"
+import { Loader2 } from "lucide-react"
+import { VideoUpload } from "@/components/ui/video-upload"
+import { cn } from "@/lib/utils"
+
+interface GalleryFormProps {
+ item?: GalleryItem
+ afterSave: () => void
+}
+
+export function GalleryForm({ item, afterSave }: GalleryFormProps) {
+ const [loading, setLoading] = useState(false)
+ const [imageUrl, setImageUrl] = useState(item?.image_url || "")
+ const [caption, setCaption] = useState(item?.caption || "")
+ const [category, setCategory] = useState(item?.category || "Genel")
+ const [order, setOrder] = useState(item?.order.toString() || "0")
+ const [isHero, setIsHero] = useState(item?.is_hero || false)
+ const [videoUrl, setVideoUrl] = useState(item?.video_url || "")
+ const [videoInputType, setVideoInputType] = useState<'url' | 'file'>('url')
+
+ const onSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setLoading(true)
+
+ const formData = new FormData()
+ formData.append('image_url', imageUrl)
+ formData.append('caption', caption)
+ formData.append('category', category)
+ formData.append('order', order)
+ formData.append('is_hero', String(isHero))
+ formData.append('video_url', videoUrl)
+
+ try {
+ const result = item
+ ? await updateGalleryItem(item.id, formData)
+ : await createGalleryItem(formData)
+
+ if (result.success) {
+ toast.success(item ? "Fotoğraf güncellendi" : "Fotoğraf eklendi")
+ afterSave()
+ } else {
+ toast.error(result.error)
+ }
+ } catch (error) {
+ toast.error("Bir hata oluştu")
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ // New Logic for Youtube Thumbnail
+ // Only works if imageUrl is empty to avoid overwriting user uploaded images
+ const getYoutubeId = (url: string) => {
+ const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/
+ const match = url.match(regExp)
+ return (match && match[2].length === 11) ? match[2] : null
+ }
+
+ const checkAndSetThumbnail = (url: string) => {
+ if (!url) return
+ const id = getYoutubeId(url)
+ if (id && !imageUrl) {
+ // maxresdefault might not exist for some videos, but is standard for most.
+ // hqdefault is safer but lower res.
+ setImageUrl(`https://img.youtube.com/vi/${id}/maxresdefault.jpg`)
+ toast.info("Youtube kapak fotoğrafı otomatik alındı")
+ }
+ }
+
+ // We can call this on change or effect. Using Effect is reactive.
+ // However, better to verify we don't overwrite if user manually set logic.
+ // The requirement is: "Video eklmesi yapıldığında otomatik youtube da olduğu gibi görseli gelsin"
+ // So if I paste a url, it should set the image.
+
+ const handleVideoUrlChange = (value: string) => {
+ setVideoUrl(value)
+ checkAndSetThumbnail(value)
+ }
+
+ return (
+
+ )
+}
diff --git a/src/app/dashboard/cms/gallery/gallery-list.tsx b/src/app/dashboard/cms/gallery/gallery-list.tsx
new file mode 100644
index 0000000..5a5efc1
--- /dev/null
+++ b/src/app/dashboard/cms/gallery/gallery-list.tsx
@@ -0,0 +1,123 @@
+'use client'
+
+import { GalleryItem } from "@/types/cms"
+import { Card, CardContent } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import { Plus, Pencil, Trash2 } from "lucide-react"
+import { useState } from "react"
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog"
+import { GalleryForm } from "./gallery-form"
+import { deleteGalleryItem } from "./actions"
+import { toast } from "sonner"
+import Image from "next/image"
+
+interface GalleryListProps {
+ initialItems: GalleryItem[]
+}
+
+export function GalleryList({ initialItems }: GalleryListProps) {
+ const [open, setOpen] = useState(false)
+ const [editingItem, setEditingItem] = useState(null)
+
+ const handleDelete = async (id: string) => {
+ if (!confirm('Bu fotoğrafı silmek istediğinize emin misiniz?')) return
+ const result = await deleteGalleryItem(id)
+ if (result.success) {
+ toast.success("Fotoğraf silindi")
+ } else {
+ toast.error(result.error)
+ }
+ }
+
+ return (
+
+
+
+
+
+
+ {initialItems.length === 0 && (
+
+ Henüz fotoğraf eklenmemiş.
+
+ )}
+ {initialItems.map((item) => {
+ let displayImage = item.image_url
+ if (!displayImage && item.video_url) {
+ const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/
+ const match = item.video_url.match(regExp)
+ const id = (match && match[2].length === 11) ? match[2] : null
+ if (id) displayImage = `https://img.youtube.com/vi/${id}/maxresdefault.jpg`
+ }
+
+ return (
+
+
+ {displayImage ? (
+
+ ) : (
+
+ Resim Yok
+
+ )}
+
+
+
{item.caption || '-'}
+
{item.category}
+
+
+
+
+
+
+
+
+ )
+ })}
+
+
+ )
+}
diff --git a/src/app/dashboard/cms/gallery/page.tsx b/src/app/dashboard/cms/gallery/page.tsx
new file mode 100644
index 0000000..17a357f
--- /dev/null
+++ b/src/app/dashboard/cms/gallery/page.tsx
@@ -0,0 +1,18 @@
+import { getGalleryItems } from "./actions"
+import { GalleryList } from "./gallery-list"
+
+export default async function GalleryPage() {
+ const items = await getGalleryItems()
+
+ return (
+
+
+
Galeri Yönetimi
+
+ Web sitesinde görünecek fotoğrafları buradan yönetebilirsiniz.
+
+
+
+
+ )
+}
diff --git a/src/app/dashboard/cms/layout.tsx b/src/app/dashboard/cms/layout.tsx
new file mode 100644
index 0000000..25485d7
--- /dev/null
+++ b/src/app/dashboard/cms/layout.tsx
@@ -0,0 +1,56 @@
+import { Separator } from "@/components/ui/separator"
+import { LayoutDashboard, FileText, Image, Briefcase } from "lucide-react"
+import Link from "next/link"
+
+const sidebarNavItems = [
+ {
+ title: "İçerik Yönetimi",
+ href: "/dashboard/cms/content",
+ icon: ,
+ },
+ {
+ title: "Hizmetler",
+ href: "/dashboard/cms/services",
+ icon: ,
+ },
+ {
+ title: "Galeri",
+ href: "/dashboard/cms/gallery",
+ icon: ,
+ },
+]
+
+interface CmsLayoutProps {
+ children: React.ReactNode
+}
+
+export default function CmsLayout({ children }: CmsLayoutProps) {
+ return (
+
+
+
İçerik Yönetim Sistemi (CMS)
+
+ Kurumsal web sitesi içeriklerini, görselleri ve hizmetleri buradan yönetebilirsiniz.
+
+
+
+
+
+
{children}
+
+
+ )
+}
diff --git a/src/app/dashboard/cms/page.tsx b/src/app/dashboard/cms/page.tsx
new file mode 100644
index 0000000..a849b1b
--- /dev/null
+++ b/src/app/dashboard/cms/page.tsx
@@ -0,0 +1,5 @@
+import { redirect } from "next/navigation"
+
+export default function CmsPage() {
+ redirect('/dashboard/cms/content')
+}
diff --git a/src/app/dashboard/cms/services/actions.ts b/src/app/dashboard/cms/services/actions.ts
new file mode 100644
index 0000000..17dfe1e
--- /dev/null
+++ b/src/app/dashboard/cms/services/actions.ts
@@ -0,0 +1,88 @@
+'use server'
+
+import { createClient } from "@/lib/supabase/server"
+import { Service } from "@/types/cms"
+import { revalidatePath } from "next/cache"
+
+export async function getServices() {
+ const supabase = await createClient()
+ const { data } = await supabase
+ .from('services')
+ .select('*')
+ .order('order', { ascending: true })
+
+ return data as Service[] || []
+}
+
+export async function createService(formData: FormData) {
+ const supabase = await createClient()
+
+ const title = formData.get('title') as string
+ const description = formData.get('description') as string
+ const image_url = formData.get('image_url') as string
+ const order = Number(formData.get('order') || 0)
+
+ const { error } = await supabase
+ .from('services')
+ .insert({
+ title,
+ description,
+ image_url,
+ order,
+ is_active: true
+ })
+
+ if (error) {
+ console.error('Create service error:', error)
+ return { success: false, error: 'Hizmet oluşturulurken hata oluştu.' }
+ }
+
+ revalidatePath('/dashboard/cms/services')
+ return { success: true }
+}
+
+export async function updateService(id: string, formData: FormData) {
+ const supabase = await createClient()
+
+ const title = formData.get('title') as string
+ const description = formData.get('description') as string
+ const image_url = formData.get('image_url') as string
+ const order = Number(formData.get('order') || 0)
+ const is_active = formData.get('is_active') === 'true'
+
+ const { error } = await supabase
+ .from('services')
+ .update({
+ title,
+ description,
+ image_url,
+ order,
+ is_active
+ })
+ .eq('id', id)
+
+ if (error) {
+ console.error('Update service error:', error)
+ return { success: false, error: 'Hizmet güncellenirken hata oluştu.' }
+ }
+
+ revalidatePath('/dashboard/cms/services')
+ return { success: true }
+}
+
+export async function deleteService(id: string) {
+ const supabase = await createClient()
+
+ const { error } = await supabase
+ .from('services')
+ .delete()
+ .eq('id', id)
+
+ if (error) {
+ console.error('Delete service error:', error)
+ return { success: false, error: 'Hizmet silinirken hata oluştu.' }
+ }
+
+ revalidatePath('/dashboard/cms/services')
+ return { success: true }
+}
diff --git a/src/app/dashboard/cms/services/page.tsx b/src/app/dashboard/cms/services/page.tsx
new file mode 100644
index 0000000..7493d4d
--- /dev/null
+++ b/src/app/dashboard/cms/services/page.tsx
@@ -0,0 +1,18 @@
+import { getServices } from "./actions"
+import { ServiceList } from "./service-list"
+
+export default async function ServicesPage() {
+ const services = await getServices()
+
+ return (
+
+
+
Hizmet Yönetimi
+
+ Salonda sunulan hizmetleri bu sayfadan ekleyip düzenleyebilirsiniz.
+
+
+
+
+ )
+}
diff --git a/src/app/dashboard/cms/services/service-form.tsx b/src/app/dashboard/cms/services/service-form.tsx
new file mode 100644
index 0000000..eb4c314
--- /dev/null
+++ b/src/app/dashboard/cms/services/service-form.tsx
@@ -0,0 +1,115 @@
+'use client'
+
+import { Service } from "@/types/cms"
+import { Button } from "@/components/ui/button"
+import { Input } from "@/components/ui/input"
+import { Label } from "@/components/ui/label"
+import { Textarea } from "@/components/ui/textarea"
+import { ImageUpload } from "@/components/ui/image-upload"
+import { useState } from "react"
+import { createService, updateService } from "./actions"
+import { toast } from "sonner"
+import { Switch } from "@/components/ui/switch"
+import { Loader2 } from "lucide-react"
+
+interface ServiceFormProps {
+ service?: Service
+ afterSave: () => void
+}
+
+export function ServiceForm({ service, afterSave }: ServiceFormProps) {
+ const [loading, setLoading] = useState(false)
+ const [title, setTitle] = useState(service?.title || "")
+ const [description, setDescription] = useState(service?.description || "")
+ const [imageUrl, setImageUrl] = useState(service?.image_url || "")
+ const [order, setOrder] = useState(service?.order.toString() || "0")
+ const [isActive, setIsActive] = useState(service?.is_active ?? true)
+
+ const onSubmit = async (e: React.FormEvent) => {
+ e.preventDefault()
+ setLoading(true)
+
+ const formData = new FormData()
+ formData.append('title', title)
+ formData.append('description', description)
+ formData.append('image_url', imageUrl)
+ formData.append('order', order)
+ formData.append('is_active', String(isActive))
+
+ try {
+ const result = service
+ ? await updateService(service.id, formData)
+ : await createService(formData)
+
+ if (result.success) {
+ toast.success(service ? "Hizmet güncellendi" : "Hizmet oluşturuldu")
+ afterSave()
+ } else {
+ toast.error(result.error)
+ }
+ } catch (error) {
+ toast.error("Bir hata oluştu")
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+ )
+}
diff --git a/src/app/dashboard/cms/services/service-list.tsx b/src/app/dashboard/cms/services/service-list.tsx
new file mode 100644
index 0000000..d509f82
--- /dev/null
+++ b/src/app/dashboard/cms/services/service-list.tsx
@@ -0,0 +1,118 @@
+'use client'
+
+import { Service } from "@/types/cms"
+import { Card, CardContent } from "@/components/ui/card"
+import { Button } from "@/components/ui/button"
+import { Plus, Pencil, Trash2, GripVertical } from "lucide-react"
+import { useState } from "react"
+import {
+ Dialog,
+ DialogContent,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog"
+import { ServiceForm } from "./service-form"
+import { deleteService } from "./actions"
+import { toast } from "sonner"
+import Image from "next/image"
+import { Badge } from "@/components/ui/badge"
+
+interface ServiceListProps {
+ initialServices: Service[]
+}
+
+export function ServiceList({ initialServices }: ServiceListProps) {
+ const [open, setOpen] = useState(false)
+ const [editingService, setEditingService] = useState(null)
+
+ const handleDelete = async (id: string) => {
+ if (!confirm('Bu hizmeti silmek istediğinize emin misiniz?')) return
+ const result = await deleteService(id)
+ if (result.success) {
+ toast.success("Hizmet silindi")
+ } else {
+ toast.error(result.error)
+ }
+ }
+
+ return (
+
+
+
+
+
+
+ {initialServices.length === 0 && (
+
+ Henüz hizmet eklenmemiş.
+
+ )}
+ {initialServices.map((service) => (
+
+
+
+ {service.image_url ? (
+
+ ) : (
+
+ Resim Yok
+
+ )}
+
+
+
+
{service.title}
+ {!service.is_active && Pasif}
+
+
+ {service.description}
+
+
+
+
+
+
+
+
+ ))}
+
+
+ )
+}
diff --git a/src/app/dashboard/settings/page.tsx b/src/app/dashboard/settings/page.tsx
index 1975c3c..c9127d3 100644
--- a/src/app/dashboard/settings/page.tsx
+++ b/src/app/dashboard/settings/page.tsx
@@ -1,6 +1,6 @@
import Link from "next/link"
import { Card, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
-import { Utensils, Users, Activity } from "lucide-react"
+import { Utensils, Users, Activity, Globe } from "lucide-react"
export default function SettingsPage() {
return (
@@ -45,6 +45,19 @@ export default function SettingsPage() {
+
+
+
+
+
+ Site Yönetimi
+
+
+ Kurumsal web sitesi içeriklerini, hizmetleri ve galeriyi yönetin.
+
+
+
+
)
diff --git a/src/app/page.tsx b/src/app/page.tsx
deleted file mode 100644
index 28c5ca1..0000000
--- a/src/app/page.tsx
+++ /dev/null
@@ -1,5 +0,0 @@
-import { redirect } from 'next/navigation'
-
-export default function Home() {
- redirect('/dashboard')
-}
diff --git a/src/components/public/gallery-grid.tsx b/src/components/public/gallery-grid.tsx
new file mode 100644
index 0000000..92be9a4
--- /dev/null
+++ b/src/components/public/gallery-grid.tsx
@@ -0,0 +1,107 @@
+"use client"
+
+import { GalleryItem } from "@/types/cms"
+import Image from "next/image"
+import { useState } from "react"
+import { Dialog, DialogContent, DialogTitle } from "@/components/ui/dialog"
+import { VisuallyHidden } from "@radix-ui/react-visually-hidden"
+import { Play } from "lucide-react"
+
+interface GalleryGridProps {
+ items: GalleryItem[]
+}
+
+export function GalleryGrid({ items }: GalleryGridProps) {
+ const [selectedVideo, setSelectedVideo] = useState(null)
+
+ const isYoutube = (url: string) => {
+ return url.includes('youtube.com') || url.includes('youtu.be')
+ }
+
+ const getYoutubeEmbedUrl = (url: string) => {
+ const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/
+ const match = url.match(regExp)
+ const id = (match && match[2].length === 11) ? match[2] : null
+ return id ? `https://www.youtube.com/embed/${id}?autoplay=1` : url
+ }
+
+ const getImageUrl = (item: GalleryItem) => {
+ if (item.image_url) return item.image_url
+ if (item.video_url) {
+ const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/
+ const match = item.video_url.match(regExp)
+ const id = (match && match[2].length === 11) ? match[2] : null
+ if (id) return `https://img.youtube.com/vi/${id}/maxresdefault.jpg`
+ }
+ return null
+ }
+
+ return (
+ <>
+
+ {items?.map((item) => {
+ const displayImage = getImageUrl(item)
+ return (
+
{
+ if (item.video_url) {
+ setSelectedVideo(item.video_url)
+ }
+ }}
+ >
+ {displayImage && (
+
+ )}
+
+ {item.video_url && (
+
+ )}
+
+ {item.category}
+
+ {item.video_url &&
İzle}
+
+
+ )
+ })}
+
+
+
+ >
+ )
+}
diff --git a/src/components/public/hero-carousel.tsx b/src/components/public/hero-carousel.tsx
new file mode 100644
index 0000000..0be7415
--- /dev/null
+++ b/src/components/public/hero-carousel.tsx
@@ -0,0 +1,95 @@
+"use client"
+
+import { useState, useEffect } from "react"
+import { GalleryItem } from "@/types/cms"
+import Image from "next/image"
+import { cn } from "@/lib/utils"
+
+interface HeroCarouselProps {
+ images: GalleryItem[]
+ children?: React.ReactNode
+}
+
+export function HeroCarousel({ images, children }: HeroCarouselProps) {
+ const [current, setCurrent] = useState(0)
+
+ useEffect(() => {
+ if (images.length <= 1) return
+
+ const timer = setInterval(() => {
+ setCurrent((prev) => (prev + 1) % images.length)
+ }, 5000)
+
+ return () => clearInterval(timer)
+ }, [images.length])
+
+ const getImageUrl = (item: GalleryItem) => {
+ if (item.image_url) return item.image_url
+ if (item.video_url) {
+ const regExp = /^.*(youtu.be\/|v\/|u\/\w\/|embed\/|watch\?v=|&v=)([^#&?]*).*/
+ const match = item.video_url.match(regExp)
+ const id = (match && match[2].length === 11) ? match[2] : null
+ if (id) return `https://img.youtube.com/vi/${id}/maxresdefault.jpg`
+ }
+ return null
+ }
+
+ // Fallback if no images
+ if (!images || images.length === 0) {
+ return (
+
+ )
+ }
+
+ return (
+ <>
+ {images.map((img, idx) => {
+ const displayImage = getImageUrl(img)
+ if (!displayImage) return null;
+
+ return (
+
+ )
+ })}
+
+ {/* Indicators */}
+ {images.length > 1 && (
+
+ {images.map((_, idx) => (
+
+ )}
+
+ {children}
+ >
+ )
+}
diff --git a/src/components/public/site-footer.tsx b/src/components/public/site-footer.tsx
new file mode 100644
index 0000000..3b53c91
--- /dev/null
+++ b/src/components/public/site-footer.tsx
@@ -0,0 +1,72 @@
+import Link from "next/link"
+import { Facebook, Instagram, Twitter } from "lucide-react"
+
+interface SiteFooterProps {
+ siteTitle: string
+ phone?: string
+ address?: string
+ email?: string
+ description?: string
+ socials?: {
+ instagram?: string
+ facebook?: string
+ twitter?: string
+ }
+}
+
+export function SiteFooter({ siteTitle, phone, address, email, description, socials }: SiteFooterProps) {
+ return (
+
+ )
+}
diff --git a/src/components/public/site-header.tsx b/src/components/public/site-header.tsx
new file mode 100644
index 0000000..9d6f054
--- /dev/null
+++ b/src/components/public/site-header.tsx
@@ -0,0 +1,52 @@
+import Link from "next/link"
+import { Button } from "@/components/ui/button"
+
+import Image from "next/image"
+
+interface SiteHeaderProps {
+ siteTitle?: string
+ siteLogo?: string
+}
+
+export function SiteHeader({ siteTitle = "Rüya Düğün Salonu", siteLogo }: SiteHeaderProps) {
+ return (
+
+
+
+ {siteLogo && (
+
+
+
+ )}
+
+ {siteTitle}
+
+
+
+
+
+ )
+}
diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx
new file mode 100644
index 0000000..5b1186a
--- /dev/null
+++ b/src/components/ui/switch.tsx
@@ -0,0 +1,29 @@
+"use client"
+
+import * as React from "react"
+import * as SwitchPrimitives from "@radix-ui/react-switch"
+
+import { cn } from "@/lib/utils"
+
+const Switch = React.forwardRef<
+ React.ElementRef,
+ React.ComponentPropsWithoutRef
+>(({ className, ...props }, ref) => (
+
+
+
+))
+Switch.displayName = SwitchPrimitives.Root.displayName
+
+export { Switch }
diff --git a/src/components/ui/video-upload.tsx b/src/components/ui/video-upload.tsx
new file mode 100644
index 0000000..d5966e6
--- /dev/null
+++ b/src/components/ui/video-upload.tsx
@@ -0,0 +1,104 @@
+"use client"
+
+import { ChangeEvent, useState } from "react"
+import { Button } from "@/components/ui/button"
+import { createClient } from "@/lib/supabase/client"
+import { Video, Loader2, X } from "lucide-react"
+import { toast } from "sonner"
+
+interface VideoUploadProps {
+ value: string
+ onChange: (url: string) => void
+ disabled?: boolean
+ bucketName?: string
+}
+
+export function VideoUpload({
+ value,
+ onChange,
+ disabled,
+ bucketName = "public-site"
+}: VideoUploadProps) {
+ const [isUploading, setIsUploading] = useState(false)
+ const supabase = createClient()
+
+ const onUpload = async (e: ChangeEvent) => {
+ try {
+ const file = e.target.files?.[0]
+ if (!file) return
+
+ // 50MB limit check (optional, but good practice)
+ if (file.size > 50 * 1024 * 1024) {
+ toast.error("Dosya boyutu 50MB'dan büyük olamaz")
+ return
+ }
+
+ setIsUploading(true)
+
+ const fileExt = file.name.split('.').pop()
+ const fileName = `videos/${Math.random()}.${fileExt}` // Put in videos subfolder
+
+ const { error: uploadError } = await supabase.storage
+ .from(bucketName)
+ .upload(fileName, file)
+
+ if (uploadError) {
+ throw uploadError
+ }
+
+ const { data } = supabase.storage
+ .from(bucketName)
+ .getPublicUrl(fileName)
+
+ onChange(data.publicUrl)
+ toast.success("Video yüklendi")
+ } catch (error) {
+ toast.error("Video yüklenirken hata oluştu")
+ console.error(error)
+ } finally {
+ setIsUploading(false)
+ }
+ }
+
+ if (value) {
+ return (
+
+
+ {value}
+
+
+ )
+ }
+
+ return (
+
+
+ {isUploading ? (
+
+ ) : (
+
+
+
Video Yükle (Maks 50MB)
+
+ )}
+
+ )
+}
diff --git a/src/types/cms.ts b/src/types/cms.ts
new file mode 100644
index 0000000..b1f1aa0
--- /dev/null
+++ b/src/types/cms.ts
@@ -0,0 +1,27 @@
+export interface SiteContent {
+ key: string
+ value: string
+ type: 'text' | 'image_url' | 'html' | 'json' | 'long_text'
+ section: string
+}
+
+export interface Service {
+ id: string
+ title: string
+ description: string | null
+ image_url: string | null
+ order: number
+ is_active: boolean
+ created_at: string
+}
+
+export interface GalleryItem {
+ id: string
+ image_url: string
+ caption: string | null
+ category: string
+ order: number
+ created_at: string
+ is_hero?: boolean
+ video_url?: string | null
+}