From 08f8a55b6cb52c343f8c32a8ed57828bb547d362 Mon Sep 17 00:00:00 2001 From: Kenan KARAER Date: Mon, 8 Dec 2025 10:25:51 +0300 Subject: [PATCH] feat: Complete Corporate Site Deployment Checkpoint --- add_email_field.sql | 3 + add_is_hero_column.sql | 1 + add_map_field.sql | 3 + add_social_fields.sql | 5 + add_video_url_column.sql | 1 + cms_migration.sql | 123 +++++++++++ next.config.ts | 12 ++ package-lock.json | 159 +++++++++++---- package.json | 4 +- src/app/(public)/galeri/page.tsx | 37 ++++ src/app/(public)/layout.tsx | 45 ++++ src/app/(public)/page.tsx | 186 +++++++++++++++++ src/app/dashboard/cms/content/actions.ts | 34 +++ .../dashboard/cms/content/content-form.tsx | 129 ++++++++++++ src/app/dashboard/cms/content/page.tsx | 28 +++ src/app/dashboard/cms/gallery/actions.ts | 93 +++++++++ .../dashboard/cms/gallery/gallery-form.tsx | 193 ++++++++++++++++++ .../dashboard/cms/gallery/gallery-list.tsx | 123 +++++++++++ src/app/dashboard/cms/gallery/page.tsx | 18 ++ src/app/dashboard/cms/layout.tsx | 56 +++++ src/app/dashboard/cms/page.tsx | 5 + src/app/dashboard/cms/services/actions.ts | 88 ++++++++ src/app/dashboard/cms/services/page.tsx | 18 ++ .../dashboard/cms/services/service-form.tsx | 115 +++++++++++ .../dashboard/cms/services/service-list.tsx | 118 +++++++++++ src/app/dashboard/settings/page.tsx | 15 +- src/app/page.tsx | 5 - src/components/public/gallery-grid.tsx | 107 ++++++++++ src/components/public/hero-carousel.tsx | 95 +++++++++ src/components/public/site-footer.tsx | 72 +++++++ src/components/public/site-header.tsx | 52 +++++ src/components/ui/switch.tsx | 29 +++ src/components/ui/video-upload.tsx | 104 ++++++++++ src/types/cms.ts | 27 +++ 34 files changed, 2051 insertions(+), 52 deletions(-) create mode 100644 add_email_field.sql create mode 100644 add_is_hero_column.sql create mode 100644 add_map_field.sql create mode 100644 add_social_fields.sql create mode 100644 add_video_url_column.sql create mode 100644 cms_migration.sql create mode 100644 src/app/(public)/galeri/page.tsx create mode 100644 src/app/(public)/layout.tsx create mode 100644 src/app/(public)/page.tsx create mode 100644 src/app/dashboard/cms/content/actions.ts create mode 100644 src/app/dashboard/cms/content/content-form.tsx create mode 100644 src/app/dashboard/cms/content/page.tsx create mode 100644 src/app/dashboard/cms/gallery/actions.ts create mode 100644 src/app/dashboard/cms/gallery/gallery-form.tsx create mode 100644 src/app/dashboard/cms/gallery/gallery-list.tsx create mode 100644 src/app/dashboard/cms/gallery/page.tsx create mode 100644 src/app/dashboard/cms/layout.tsx create mode 100644 src/app/dashboard/cms/page.tsx create mode 100644 src/app/dashboard/cms/services/actions.ts create mode 100644 src/app/dashboard/cms/services/page.tsx create mode 100644 src/app/dashboard/cms/services/service-form.tsx create mode 100644 src/app/dashboard/cms/services/service-list.tsx delete mode 100644 src/app/page.tsx create mode 100644 src/components/public/gallery-grid.tsx create mode 100644 src/components/public/hero-carousel.tsx create mode 100644 src/components/public/site-footer.tsx create mode 100644 src/components/public/site-header.tsx create mode 100644 src/components/ui/switch.tsx create mode 100644 src/components/ui/video-upload.tsx create mode 100644 src/types/cms.ts 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 ? ( + {service.title} + ) : ( +
+ Görsel Yok +
+ )} +
+ +

+ {service.title} +

+

+ {service.description} +

+
+
+ ))} +
+ {(!services || services.length === 0) && ( +

Henüz hizmet eklenmemiş.

+ )} +
+
+ + {/* GALLERY PREVIEW SECTION */} + + + {/* CONTACT SECTION */} +
+
+
+
+
+

Bizimle İletişime Geçin

+

+ Düğününüzü planlamak veya salonumuzu görmek için bizi arayın veya ziyaret edin. + Kahve eşliğinde hayallerinizi konuşalım. +

+
+ +
+
+ +
+

Adres

+

{contactAddress || 'Adres bilgisi girilmemiş.'}

+
+
+
+ +
+

Telefon

+

{contactPhone || 'Telefon bilgisi girilmemiş.'}

+
+
+
+ +
+

E-Posta

+

{contactEmail || 'E-Posta bilgisi girilmemiş.'}

+
+
+
+
+ + {/* MAP */} +
+ {mapEmbed ? ( +
+ ) : ( +
+ Harita bilgisi eklenmemiş. +
+ )} +
+
+
+
+
+ ) +} 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') ? ( +