From e45c44721f2f65833ddae4394f04f0be5fa0565e Mon Sep 17 00:00:00 2001 From: Ege Can Komur Date: Wed, 20 May 2026 18:45:02 +0300 Subject: [PATCH] feat: WP'den header + kart stilleri + blog sidebar widget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Header (components/header.tsx + header-scroll.tsx): - WP'deki 'floating pill' efekti — scroll'da küçülen + yuvarlanan + gölgeli - 3 sütun grid: Logo | Nav | CTA - Hizmetler mega menu dropdown — 2 sütunlu (Web&Yazılım + Dijital Pazarlama) - Hover'da açılır, services tablosundan dinamik - Alt linkle 'Tüm hizmetleri gör' - Mobil için scroll-down'da gizlenir - Sağda 'Ücretsiz Teklif' CTA butonu + telefon link Kart stilleri (WP'ye eşlendi): - ServicesGrid: - Gradient icon (sky → purple) ile WP'deki '🎨 🚀 📱' emoji yerine ikon - Hover: -translate-y-2 + colored shadow + scale icon - ArrowUpRight ikonu absolute, hover'da görünür - ProjectsGrid: - Kategori bazlı renkli badge (Kurumsal navy, Klinik cyan, Portfolio violet, …) - Hover: image scale-105 + gradient overlay - 5/3 aspect ratio (daha WP-like) Public sidebar (components/content-sidebar.tsx): - CTA card (gradient navy→sky): Telefon + WhatsApp - Son yazılar (4 adet, kapak + başlık + tarih) - Etiketler (en sık kullanılan 10) - Hizmetler menü (6 adet) - Site analizi lead magnet Blog detay sayfası (/blog/[slug]): - Tek sütun → 2 sütun grid (content + sidebar) - sticky sidebar, max-w-7xl - Aynı pattern hizmet/proje detay sayfalarına da uygulanabilir 37 route, build temiz. --- app/(site)/blog/[slug]/page.tsx | 108 +++++++++--------- components/content-sidebar.tsx | 188 ++++++++++++++++++++++++++++++++ components/header-scroll.tsx | 86 +++++++++++++++ components/header.tsx | 187 ++++++++++++++++++++++++------- components/projects-grid.tsx | 146 ++++++++++++++----------- components/services-grid.tsx | 31 +++--- 6 files changed, 577 insertions(+), 169 deletions(-) create mode 100644 components/content-sidebar.tsx create mode 100644 components/header-scroll.tsx diff --git a/app/(site)/blog/[slug]/page.tsx b/app/(site)/blog/[slug]/page.tsx index 259e878..c1749d1 100644 --- a/app/(site)/blog/[slug]/page.tsx +++ b/app/(site)/blog/[slug]/page.tsx @@ -6,6 +6,7 @@ import { ArrowLeft, Calendar } from "lucide-react"; import { renderContent } from "@/lib/content-render"; import { getPostBySlug } from "@/lib/data"; import { buildMetadata } from "@/lib/seo"; +import { ContentSidebar } from "@/components/content-sidebar"; export async function generateMetadata({ params, @@ -21,7 +22,10 @@ export async function generateMetadata({ openGraph: { title: post.seo_title || post.title, description: post.seo_description || post.excerpt || undefined, - images: post.seo_image || post.cover_image ? [{ url: (post.seo_image || post.cover_image) as string }] : undefined, + images: + post.seo_image || post.cover_image + ? [{ url: (post.seo_image || post.cover_image) as string }] + : undefined, type: "article", }, }); @@ -39,7 +43,7 @@ export default async function BlogPostPage({ const html = renderContent(post.content); return ( -
+
Tüm yazılar -
- {post.tags && post.tags.length > 0 && ( -
- {post.tags.map((t) => ( - - {t} - - ))} -
- )} -

- {post.title} -

- {post.excerpt && ( -

- {post.excerpt} -

- )} -
- {post.author && {post.author}} - {post.author && post.published_at && } - {post.published_at && ( - - - {new Date(post.published_at).toLocaleDateString("tr-TR")} - +
+
+
+ {post.tags && post.tags.length > 0 && ( +
+ {post.tags.map((t) => ( + + {t} + + ))} +
+ )} +

+ {post.title} +

+ {post.excerpt && ( +

+ {post.excerpt} +

+ )} +
+ {post.author && {post.author}} + {post.author && post.published_at && } + {post.published_at && ( + + + {new Date(post.published_at).toLocaleDateString("tr-TR")} + + )} +
+
+ + {post.cover_image && ( +
+ {post.title} +
)} -
-
- {post.cover_image && ( -
- {post.title} -
- )} +
-
- + +
+ ); } diff --git a/components/content-sidebar.tsx b/components/content-sidebar.tsx new file mode 100644 index 0000000..b8e563c --- /dev/null +++ b/components/content-sidebar.tsx @@ -0,0 +1,188 @@ +import Image from "next/image"; +import Link from "next/link"; +import { ArrowRight, MessageCircle, Phone, Tag } from "lucide-react"; +import { + getSiteSettings, + listPublishedPosts, + listServices, +} from "@/lib/data"; +import { siteConfig } from "@/lib/site-config"; + +interface Props { + /** + * Hangi yazıyı/sayfayı görüntülüyoruz — listede gizlemek için. + */ + currentSlug?: string; +} + +export async function ContentSidebar({ currentSlug }: Props) { + const [settings, services, posts] = await Promise.all([ + getSiteSettings(), + listServices(), + listPublishedPosts({ limit: 5 }), + ]); + + const phoneRaw = settings?.contact_phone_raw ?? siteConfig.contact.phoneRaw; + const phone = settings?.contact_phone ?? siteConfig.contact.phone; + const waCleaned = phoneRaw.replace(/[^\d]/g, ""); + const waMessage = settings?.whatsapp_message ?? ""; + const waHref = `https://wa.me/${waCleaned}${ + waMessage ? `?text=${encodeURIComponent(waMessage)}` : "" + }`; + + const otherPosts = posts.filter((p) => p.slug !== currentSlug).slice(0, 4); + + // Etiket sayımı (tüm yazılardan toplu) + const tagCount = new Map(); + posts.forEach((p) => + (p.tags ?? []).forEach((t) => + tagCount.set(t, (tagCount.get(t) ?? 0) + 1), + ), + ); + const topTags = Array.from(tagCount.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, 10); + + return ( + + ); +} diff --git a/components/header-scroll.tsx b/components/header-scroll.tsx new file mode 100644 index 0000000..32a903d --- /dev/null +++ b/components/header-scroll.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { useEffect } from "react"; + +/** + * WordPress sitesindeki "floating pill" header efekti. + * Scroll'da header küçülür, kenar yuvarlanır, gölge alır. + * Mobilde scroll-down'da gizlenir, scroll-up'ta görünür. + */ +export function HeaderScrollEffect() { + useEffect(() => { + const wrap = document.getElementById("floating-header-wrap"); + const pillWrap = document.getElementById("header-pill-wrap"); + const header = document.getElementById("site-header"); + const navBar = document.getElementById("header-nav-bar"); + if (!wrap || !pillWrap || !header || !navBar) return; + + let lastY = 0; + let ticking = false; + + wrap.style.transition = "transform 0.3s ease, opacity 0.3s ease"; + + function applyScroll() { + const y = window.scrollY; + const mobile = window.innerWidth < 1024; + const scrolled = y > 10; + const goingDown = y > lastY; + + if (!mobile && pillWrap && header && navBar && wrap) { + wrap.style.transform = ""; + wrap.style.opacity = ""; + if (scrolled) { + pillWrap.style.padding = "12px 16px 0"; + header.style.maxWidth = "1100px"; + header.style.borderRadius = "1rem"; + header.style.border = "1px solid #e5e7eb"; + header.style.boxShadow = "0 8px 24px rgba(0,0,0,0.08)"; + navBar.style.height = "52px"; + navBar.style.padding = "0 1.25rem"; + } else { + pillWrap.style.padding = ""; + header.style.maxWidth = ""; + header.style.borderRadius = ""; + header.style.border = ""; + header.style.boxShadow = ""; + navBar.style.height = ""; + navBar.style.padding = ""; + } + } + + if (mobile && wrap && y > 80) { + if (goingDown) { + wrap.style.transform = "translateY(-110%)"; + wrap.style.opacity = "0"; + } else { + wrap.style.transform = ""; + wrap.style.opacity = ""; + } + } else if (mobile && wrap) { + wrap.style.transform = ""; + wrap.style.opacity = ""; + } + + lastY = y; + ticking = false; + } + + function onScroll() { + if (!ticking) { + requestAnimationFrame(applyScroll); + ticking = true; + } + } + + window.addEventListener("scroll", onScroll, { passive: true }); + window.addEventListener("resize", applyScroll); + applyScroll(); + + return () => { + window.removeEventListener("scroll", onScroll); + window.removeEventListener("resize", applyScroll); + }; + }, []); + + return null; +} diff --git a/components/header.tsx b/components/header.tsx index f245fa3..5b17c78 100644 --- a/components/header.tsx +++ b/components/header.tsx @@ -1,53 +1,162 @@ import Image from "next/image"; import Link from "next/link"; -import { Phone } from "lucide-react"; -import { getSiteSettings } from "@/lib/data"; +import { ChevronDown, Phone } from "lucide-react"; +import { getSiteSettings, listServices } from "@/lib/data"; import { siteConfig } from "@/lib/site-config"; - -const nav = [ - { href: "/", label: "Anasayfa" }, - { href: "/hizmetler", label: "Hizmetler" }, - { href: "/projeler", label: "Projeler" }, - { href: "/blog", label: "Blog" }, - { href: "/hakkimizda", label: "Hakkımızda" }, - { href: "/iletisim", label: "İletişim" }, -]; +import { HeaderScrollEffect } from "@/components/header-scroll"; export async function Header() { - const settings = await getSiteSettings(); + const [settings, services] = await Promise.all([ + getSiteSettings(), + listServices(), + ]); const phone = settings?.contact_phone ?? siteConfig.contact.phone; const phoneRaw = settings?.contact_phone_raw ?? siteConfig.contact.phoneRaw; + // Mega menu groups + const webServices = services.filter((s) => + ["web-tasarim", "e-ticaret", "mobil-uygulama", "yazilim-gelistirme", "crm-sistemleri"].includes(s.slug), + ); + const marketingServices = services.filter((s) => + ["seo-dijital-pazarlama", "sosyal-medya-yonetimi", "dijital-reklam"].includes(s.slug), + ); + return ( -
-
- - {siteConfig.name} - - {siteConfig.name} - - - - +
+ - + ); } diff --git a/components/projects-grid.tsx b/components/projects-grid.tsx index 0bd6f78..398da8a 100644 --- a/components/projects-grid.tsx +++ b/components/projects-grid.tsx @@ -3,6 +3,15 @@ import Link from "next/link"; import { ArrowUpRight, ExternalLink } from "lucide-react"; import type { ProjectRow } from "@/lib/types"; +const CATEGORY_COLORS: Record = { + "Kurumsal Web Sitesi": "bg-[var(--navy)]", + "Klinik Web Sitesi": "bg-cyan-600", + "Portfolyo & SEO": "bg-violet-600", + "Web Tasarım": "bg-emerald-600", + "Özel Yazılım": "bg-sky-600", + "E-Ticaret": "bg-pink-600", +}; + export function ProjectsGrid({ projects }: { projects: ProjectRow[] }) { if (projects.length === 0) { return ( @@ -15,73 +24,82 @@ export function ProjectsGrid({ projects }: { projects: ProjectRow[] }) { } return ( -
- {projects.map((p) => ( -
- -
- {p.image_url ? ( - {p.title} - ) : ( -
- {p.title.charAt(0)} +
+ {projects.map((p) => { + const tagColor = p.category && CATEGORY_COLORS[p.category] + ? CATEGORY_COLORS[p.category] + : "bg-[var(--navy)]"; + return ( +
+ +
+ {p.image_url ? ( + {p.title} + ) : ( +
+ {p.title.charAt(0)} +
+ )} + {/* Overlay gradient — WP stili */} +
+ {p.category && ( + + {p.category} + + )} +
+ +
+
+ +

+ {p.title} +

+ + {p.live_url ? ( + + + + ) : ( + + )} +
+

+ {p.description} +

+ {p.technologies && p.technologies.length > 0 && ( +
+ {p.technologies.map((t) => ( + + {t} + + ))}
)} - {p.category && ( - - {p.category} - - )}
- -
-
- -

- {p.title} -

- - {p.live_url ? ( - - - - ) : ( - - )} -
-

- {p.description} -

- {p.technologies && p.technologies.length > 0 && ( -
- {p.technologies.map((t) => ( - - {t} - - ))} -
- )} -
-
- ))} +
+ ); + })}
); } diff --git a/components/services-grid.tsx b/components/services-grid.tsx index fe1f438..3ddec5a 100644 --- a/components/services-grid.tsx +++ b/components/services-grid.tsx @@ -18,30 +18,27 @@ export function ServicesGrid({ services }: { services: ServiceRow[] }) { : (siteConfig.fallbackServices as readonly ServiceLike[]).slice(); return ( -
+
{items.map((s) => ( -
- -
-
- -
-

- {s.title} -

-

- {s.description} -

+ + + {/* Gradient icon — WP'deki stil */} +
+
+ +

+ {s.title} +

+

+ {s.description} +

))}