feat: WP'den header + kart stilleri + blog sidebar widget

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.
This commit is contained in:
Ege Can Komur
2026-05-20 18:45:02 +03:00
parent deff889f0c
commit e45c44721f
6 changed files with 577 additions and 169 deletions
+59 -49
View File
@@ -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 (
<article className="mx-auto max-w-3xl px-6 py-20">
<div className="mx-auto max-w-7xl px-6 py-16">
<Link
href="/blog"
className="inline-flex items-center gap-1 text-sm text-[var(--muted)] hover:text-[var(--navy)]"
@@ -47,56 +51,62 @@ export default async function BlogPostPage({
<ArrowLeft className="size-3.5" /> Tüm yazılar
</Link>
<header className="mt-6 border-b border-[var(--border)] pb-8">
{post.tags && post.tags.length > 0 && (
<div className="flex flex-wrap gap-2">
{post.tags.map((t) => (
<span
key={t}
className="rounded-full bg-[var(--sky-50)] px-2.5 py-1 text-xs text-[var(--sky-600)]"
>
{t}
</span>
))}
</div>
)}
<h1 className="mt-4 text-3xl font-bold leading-tight tracking-tight text-[var(--navy)] sm:text-4xl">
{post.title}
</h1>
{post.excerpt && (
<p className="mt-4 text-lg leading-relaxed text-[var(--muted)]">
{post.excerpt}
</p>
)}
<div className="mt-6 flex items-center gap-3 text-xs text-[var(--muted)]">
{post.author && <span>{post.author}</span>}
{post.author && post.published_at && <span></span>}
{post.published_at && (
<span className="inline-flex items-center gap-1">
<Calendar className="size-3" />
{new Date(post.published_at).toLocaleDateString("tr-TR")}
</span>
<div className="mt-6 grid gap-12 lg:grid-cols-[1fr_320px]">
<article>
<header className="border-b border-[var(--border)] pb-8">
{post.tags && post.tags.length > 0 && (
<div className="flex flex-wrap gap-2">
{post.tags.map((t) => (
<span
key={t}
className="rounded-full bg-[var(--sky-50)] px-2.5 py-1 text-xs text-[var(--sky-600)]"
>
{t}
</span>
))}
</div>
)}
<h1 className="mt-4 text-3xl font-bold leading-tight tracking-tight text-[var(--navy)] sm:text-4xl">
{post.title}
</h1>
{post.excerpt && (
<p className="mt-4 text-lg leading-relaxed text-[var(--muted)]">
{post.excerpt}
</p>
)}
<div className="mt-6 flex items-center gap-3 text-xs text-[var(--muted)]">
{post.author && <span>{post.author}</span>}
{post.author && post.published_at && <span></span>}
{post.published_at && (
<span className="inline-flex items-center gap-1">
<Calendar className="size-3" />
{new Date(post.published_at).toLocaleDateString("tr-TR")}
</span>
)}
</div>
</header>
{post.cover_image && (
<div className="relative mt-8 aspect-video overflow-hidden rounded-2xl">
<Image
src={post.cover_image}
alt={post.title}
fill
sizes="(min-width: 1024px) 768px, 100vw"
className="object-cover"
priority
/>
</div>
)}
</div>
</header>
{post.cover_image && (
<div className="relative mt-8 aspect-video overflow-hidden rounded-2xl">
<Image
src={post.cover_image}
alt={post.title}
fill
sizes="(min-width: 1024px) 768px, 100vw"
className="object-cover"
priority
<div
className="prose prose-lg mt-10 max-w-none text-[var(--foreground)]"
dangerouslySetInnerHTML={{ __html: html }}
/>
</div>
)}
</article>
<div
className="prose prose-lg mt-10 max-w-none text-[var(--foreground)]"
dangerouslySetInnerHTML={{ __html: html }}
/>
</article>
<ContentSidebar currentSlug={slug} />
</div>
</div>
);
}
+188
View File
@@ -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<string, number>();
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 (
<aside className="space-y-6 lg:sticky lg:top-24 lg:self-start">
{/* CTA card */}
<div className="overflow-hidden rounded-2xl border border-[var(--border)] bg-gradient-to-br from-[var(--navy)] to-[var(--sky-600)] p-6 text-white">
<h3 className="text-base font-bold">Projeniz mi var?</h3>
<p className="mt-2 text-sm text-white/80">
Ücretsiz keşif görüşmesi için bizi arayın veya WhatsApp'tan yazın.
</p>
<div className="mt-4 space-y-2">
<a
href={`tel:${phoneRaw}`}
className="flex items-center justify-center gap-2 rounded-xl bg-white px-4 py-2.5 text-sm font-semibold text-[var(--navy)] transition hover:bg-blue-50"
>
<Phone className="size-3.5" />
{phone}
</a>
<a
href={waHref}
target="_blank"
rel="noopener noreferrer"
className="flex items-center justify-center gap-2 rounded-xl bg-[#25d366] px-4 py-2.5 text-sm font-semibold text-white transition hover:bg-[#1ebe5d]"
>
<MessageCircle className="size-3.5" />
WhatsApp'tan yaz
</a>
</div>
</div>
{/* Diğer yazılar */}
{otherPosts.length > 0 && (
<div className="rounded-2xl border border-[var(--border)] bg-white p-5">
<div className="flex items-center justify-between">
<h3 className="text-sm font-bold uppercase tracking-wider text-[var(--navy)]">
Son Yazılar
</h3>
<Link
href="/blog"
className="text-xs text-[var(--sky-600)] hover:text-[var(--navy)]"
>
Tümü
</Link>
</div>
<ul className="mt-4 space-y-3">
{otherPosts.map((p) => (
<li key={p.$id}>
<Link
href={`/blog/${p.slug}`}
className="group flex gap-3"
>
<div className="relative size-16 shrink-0 overflow-hidden rounded-lg bg-[var(--navy-50)]">
{p.cover_image ? (
<Image
src={p.cover_image}
alt={p.title}
fill
sizes="64px"
className="object-cover"
/>
) : (
<div className="flex h-full items-center justify-center text-lg font-bold text-[var(--navy)]/30">
{p.title.charAt(0)}
</div>
)}
</div>
<div className="min-w-0 flex-1">
<p className="line-clamp-2 text-sm font-medium leading-snug text-[var(--navy)] transition-colors group-hover:text-[var(--sky-600)]">
{p.title}
</p>
{p.published_at && (
<p className="mt-1 text-[11px] text-[var(--muted)]">
{new Date(p.published_at).toLocaleDateString("tr-TR")}
</p>
)}
</div>
</Link>
</li>
))}
</ul>
</div>
)}
{/* Etiketler */}
{topTags.length > 0 && (
<div className="rounded-2xl border border-[var(--border)] bg-white p-5">
<h3 className="flex items-center gap-1.5 text-sm font-bold uppercase tracking-wider text-[var(--navy)]">
<Tag className="size-3.5" />
Etiketler
</h3>
<div className="mt-3 flex flex-wrap gap-1.5">
{topTags.map(([tag]) => (
<span
key={tag}
className="rounded-md bg-[var(--navy-50)] px-2.5 py-1 text-xs font-medium text-[var(--navy-700)] hover:bg-[var(--sky-50)]"
>
{tag}
</span>
))}
</div>
</div>
)}
{/* Hizmetler */}
{services.length > 0 && (
<div className="rounded-2xl border border-[var(--border)] bg-white p-5">
<h3 className="text-sm font-bold uppercase tracking-wider text-[var(--navy)]">
Hizmetlerimiz
</h3>
<ul className="mt-3 space-y-1.5">
{services.slice(0, 6).map((s) => (
<li key={s.slug}>
<Link
href={`/hizmetler/${s.slug}`}
className="flex items-center justify-between rounded-lg px-2 py-1.5 text-sm text-[var(--foreground)] transition hover:bg-[var(--navy-50)] hover:text-[var(--navy)]"
>
<span>{s.title}</span>
<ArrowRight className="size-3 text-[var(--muted)]" />
</Link>
</li>
))}
</ul>
</div>
)}
{/* Site analizi lead magnet */}
<div className="rounded-2xl border border-[var(--sky)]/30 bg-[var(--sky-50)]/50 p-5">
<h3 className="text-sm font-bold text-[var(--navy)]">
Ücretsiz Site Analizi
</h3>
<p className="mt-2 text-xs leading-relaxed text-[var(--muted)]">
Sitenizin SEO, hız ve dönüşüm performansını ücretsiz değerlendirelim.
24 saat içinde rapor e-postanızda.
</p>
<Link
href="/site-analizi"
className="mt-3 inline-flex items-center gap-1 text-xs font-semibold text-[var(--sky-600)] hover:text-[var(--navy)]"
>
Hemen başla
<ArrowRight className="size-3" />
</Link>
</div>
</aside>
);
}
+86
View File
@@ -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;
}
+148 -39
View File
@@ -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 (
<header className="sticky top-0 z-40 border-b border-[var(--border)] bg-white/90 backdrop-blur">
<div className="mx-auto flex max-w-7xl items-center justify-between gap-6 px-6 py-3">
<Link href="/" className="flex items-center gap-3">
<Image src="/logo.png" alt={siteConfig.name} width={44} height={44} priority />
<span className="hidden text-base font-semibold tracking-tight text-[var(--navy)] sm:block">
{siteConfig.name}
</span>
</Link>
<nav className="hidden items-center gap-8 md:flex">
{nav.map((item) => (
<Link
key={item.href}
href={item.href}
className="text-sm font-medium text-[var(--muted)] transition hover:text-[var(--navy)]"
<>
<HeaderScrollEffect />
<div className="sticky top-0 z-50 w-full" id="floating-header-wrap">
<div id="header-pill-wrap" className="transition-all duration-300 ease-out">
<header
id="site-header"
className="mx-auto w-full border-b border-gray-100 bg-white/95 backdrop-blur-lg transition-all duration-300 ease-out"
>
<nav
id="header-nav-bar"
className="flex h-14 items-center justify-between px-6 transition-all duration-300 ease-out lg:grid lg:h-16 lg:grid-cols-[1fr_auto_1fr] lg:px-8"
>
{item.label}
</Link>
))}
</nav>
{/* Col 1 — Logo */}
<Link href="/" className="flex items-center gap-2.5">
<Image
src="/logo.png"
alt={siteConfig.name}
width={40}
height={40}
priority
className="h-9 w-9 object-contain"
/>
<span className="hidden text-sm font-semibold tracking-tight text-[var(--navy)] sm:block">
{siteConfig.name}
</span>
</Link>
<a
href={`tel:${phoneRaw}`}
className="hidden items-center gap-2 rounded-full bg-[var(--navy)] px-4 py-2 text-sm font-medium text-white shadow-sm transition hover:bg-[var(--navy-700)] sm:inline-flex"
>
<Phone className="size-4" />
{phone}
</a>
{/* Col 2 — Desktop nav */}
<div className="hidden items-center gap-0.5 lg:flex">
<Link
href="/"
className="inline-flex h-9 items-center justify-center whitespace-nowrap rounded-lg px-3.5 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900"
>
Anasayfa
</Link>
{/* Hizmetler mega menu */}
<div className="group relative">
<button
type="button"
className="inline-flex h-9 items-center justify-center gap-1 whitespace-nowrap rounded-lg px-3.5 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900"
>
Hizmetler
<ChevronDown className="size-3 transition-transform duration-200 group-hover:rotate-180" />
</button>
<div className="pointer-events-none invisible absolute left-1/2 top-full z-50 w-[480px] -translate-x-1/2 pt-2 opacity-0 transition-all duration-150 ease-out group-hover:pointer-events-auto group-hover:visible group-hover:opacity-100">
<div className="translate-y-1 rounded-2xl border border-gray-100 bg-white p-4 shadow-xl transition-transform duration-150 group-hover:translate-y-0">
<div className="grid grid-cols-2 gap-x-3">
<div>
<p className="px-3 pb-1.5 text-[10px] font-semibold uppercase tracking-wider text-gray-400">
Web & Yazılım
</p>
{webServices.map((s) => (
<Link
key={s.slug}
href={`/hizmetler/${s.slug}`}
className="flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm text-gray-700 transition-colors hover:bg-blue-50 hover:text-[var(--navy)]"
>
{s.title}
</Link>
))}
</div>
<div>
<p className="px-3 pb-1.5 text-[10px] font-semibold uppercase tracking-wider text-gray-400">
Dijital Pazarlama
</p>
{marketingServices.map((s) => (
<Link
key={s.slug}
href={`/hizmetler/${s.slug}`}
className="flex items-center gap-3 rounded-xl px-3 py-2.5 text-sm text-gray-700 transition-colors hover:bg-blue-50 hover:text-[var(--navy)]"
>
{s.title}
</Link>
))}
</div>
</div>
<div className="mt-3 border-t border-gray-100 pt-3">
<Link
href="/hizmetler"
className="block rounded-xl px-3 py-2 text-center text-xs font-semibold text-[var(--navy)] hover:bg-blue-50"
>
Tüm hizmetleri gör
</Link>
</div>
</div>
</div>
</div>
<Link
href="/projeler"
className="inline-flex h-9 items-center justify-center whitespace-nowrap rounded-lg px-3.5 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900"
>
Projeler
</Link>
<Link
href="/blog"
className="inline-flex h-9 items-center justify-center whitespace-nowrap rounded-lg px-3.5 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900"
>
Blog
</Link>
<Link
href="/hakkimizda"
className="inline-flex h-9 items-center justify-center whitespace-nowrap rounded-lg px-3.5 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900"
>
Hakkımızda
</Link>
<Link
href="/iletisim"
className="inline-flex h-9 items-center justify-center whitespace-nowrap rounded-lg px-3.5 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900"
>
İletişim
</Link>
</div>
{/* Col 3 — CTA */}
<div className="flex items-center justify-end gap-2">
<a
href={`tel:${phoneRaw}`}
className="hidden lg:inline-flex h-9 items-center gap-1.5 rounded-lg px-3 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-900"
>
<Phone className="size-3.5" />
<span className="hidden xl:inline">{phone}</span>
</a>
<Link
href="/iletisim"
className="inline-flex h-9 items-center justify-center rounded-lg bg-[var(--navy)] px-4 text-sm font-semibold text-white shadow-sm transition-colors hover:bg-[var(--navy-700)]"
>
Ücretsiz Teklif
</Link>
</div>
</nav>
</header>
</div>
</div>
</header>
</>
);
}
+82 -64
View File
@@ -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<string, string> = {
"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 (
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
{projects.map((p) => (
<article
key={p.$id}
className="group overflow-hidden rounded-2xl border border-[var(--border)] bg-white transition hover:shadow-xl"
>
<Link href={`/projeler/${p.slug}`} className="block">
<div className="relative aspect-video overflow-hidden bg-[var(--navy-50)]">
{p.image_url ? (
<Image
src={p.image_url}
alt={p.title}
fill
sizes="(min-width: 1024px) 33vw, (min-width: 768px) 50vw, 100vw"
className="object-cover transition group-hover:scale-105"
/>
) : (
<div className="flex h-full items-center justify-center text-[var(--navy)]/30">
<span className="text-5xl font-bold">{p.title.charAt(0)}</span>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{projects.map((p) => {
const tagColor = p.category && CATEGORY_COLORS[p.category]
? CATEGORY_COLORS[p.category]
: "bg-[var(--navy)]";
return (
<article
key={p.$id}
className="group overflow-hidden rounded-2xl border border-[var(--border)] bg-white transition-all duration-300 hover:-translate-y-1 hover:shadow-2xl hover:shadow-[var(--navy)]/10"
>
<Link href={`/projeler/${p.slug}`} className="block">
<div className="relative aspect-[5/3] overflow-hidden bg-[var(--navy-50)]">
{p.image_url ? (
<Image
src={p.image_url}
alt={p.title}
fill
sizes="(min-width: 1024px) 33vw, (min-width: 768px) 50vw, 100vw"
className="object-cover transition-transform duration-500 group-hover:scale-105"
/>
) : (
<div className="flex h-full items-center justify-center text-[var(--navy)]/30">
<span className="text-5xl font-bold">{p.title.charAt(0)}</span>
</div>
)}
{/* Overlay gradient — WP stili */}
<div className="absolute inset-0 bg-gradient-to-t from-black/65 via-transparent to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100" />
{p.category && (
<span
className={`absolute left-4 top-4 rounded-full ${tagColor} px-3 py-1 text-xs font-semibold text-white shadow-lg`}
>
{p.category}
</span>
)}
</div>
</Link>
<div className="p-6">
<div className="flex items-start justify-between gap-3">
<Link href={`/projeler/${p.slug}`} className="block">
<h3 className="text-lg font-bold text-[var(--navy)] transition-colors group-hover:text-[var(--sky-600)]">
{p.title}
</h3>
</Link>
{p.live_url ? (
<a
href={p.live_url}
target="_blank"
rel="noopener noreferrer"
aria-label="Projeyi canlı aç"
className="text-[var(--sky-600)] transition-colors hover:text-[var(--navy)]"
>
<ExternalLink className="size-4" />
</a>
) : (
<ArrowUpRight className="size-5 text-[var(--muted)] transition-colors group-hover:text-[var(--sky-600)]" />
)}
</div>
<p className="mt-2 text-sm leading-relaxed text-[var(--muted)] line-clamp-3">
{p.description}
</p>
{p.technologies && p.technologies.length > 0 && (
<div className="mt-4 flex flex-wrap gap-1.5">
{p.technologies.map((t) => (
<span
key={t}
className="rounded-md bg-[var(--navy-50)] px-2 py-0.5 text-xs font-medium text-[var(--navy-700)]"
>
{t}
</span>
))}
</div>
)}
{p.category && (
<span className="absolute left-4 top-4 rounded-full bg-white/95 px-3 py-1 text-xs font-medium text-[var(--navy)] shadow-sm">
{p.category}
</span>
)}
</div>
</Link>
<div className="p-6">
<div className="flex items-start justify-between gap-3">
<Link href={`/projeler/${p.slug}`} className="block">
<h3 className="text-lg font-semibold text-[var(--navy)] transition group-hover:text-[var(--sky-600)]">
{p.title}
</h3>
</Link>
{p.live_url ? (
<a
href={p.live_url}
target="_blank"
rel="noopener noreferrer"
aria-label="Projeyi canlı aç"
className="text-[var(--sky-600)] hover:text-[var(--navy)]"
>
<ExternalLink className="size-4" />
</a>
) : (
<ArrowUpRight className="size-5 text-[var(--muted)] transition group-hover:text-[var(--sky-600)]" />
)}
</div>
<p className="mt-2 text-sm leading-relaxed text-[var(--muted)] line-clamp-3">
{p.description}
</p>
{p.technologies && p.technologies.length > 0 && (
<div className="mt-4 flex flex-wrap gap-1.5">
{p.technologies.map((t) => (
<span
key={t}
className="rounded-md bg-[var(--navy-50)] px-2 py-0.5 text-xs text-[var(--navy-700)]"
>
{t}
</span>
))}
</div>
)}
</div>
</article>
))}
</article>
);
})}
</div>
);
}
+14 -17
View File
@@ -18,30 +18,27 @@ export function ServicesGrid({ services }: { services: ServiceRow[] }) {
: (siteConfig.fallbackServices as readonly ServiceLike[]).slice();
return (
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-4">
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
{items.map((s) => (
<Link
key={s.slug}
href={`/hizmetler/${s.slug}`}
id={s.slug}
className="group relative overflow-hidden rounded-2xl border border-[var(--border)] bg-white p-6 transition hover:border-[var(--sky)]/40 hover:shadow-lg hover:shadow-[var(--sky)]/10"
className="group relative overflow-hidden rounded-2xl border border-[var(--border)] bg-white p-8 transition-all duration-300 hover:-translate-y-2 hover:border-[var(--sky)]/40 hover:shadow-2xl hover:shadow-[var(--navy)]/10"
>
<div
className="absolute -right-12 -top-12 size-32 rounded-full bg-[var(--sky-50)] opacity-0 transition group-hover:opacity-100"
aria-hidden
/>
<ArrowUpRight className="absolute right-5 top-5 size-4 text-[var(--muted)] transition group-hover:text-[var(--sky-600)]" />
<div className="relative">
<div className="flex size-12 items-center justify-center rounded-xl bg-[var(--navy-50)] text-[var(--navy)]">
<Icon name={s.icon} className="size-6" />
</div>
<h3 className="mt-5 text-lg font-semibold text-[var(--navy)] transition group-hover:text-[var(--sky-600)]">
{s.title}
</h3>
<p className="mt-2 text-sm leading-relaxed text-[var(--muted)]">
{s.description}
</p>
<ArrowUpRight className="absolute right-6 top-6 size-4 text-[var(--muted)] opacity-0 transition-all duration-300 group-hover:translate-x-1 group-hover:-translate-y-1 group-hover:text-[var(--sky-600)] group-hover:opacity-100" />
{/* Gradient icon — WP'deki stil */}
<div className="flex size-14 items-center justify-center rounded-xl bg-gradient-to-br from-[var(--sky)] to-purple-500 text-white shadow-lg shadow-[var(--sky)]/30 transition-transform duration-300 group-hover:scale-110">
<Icon name={s.icon} className="size-6" />
</div>
<h3 className="mt-6 text-lg font-bold leading-tight text-[var(--navy)] transition-colors group-hover:text-[var(--sky-600)]">
{s.title}
</h3>
<p className="mt-3 text-sm leading-relaxed text-[var(--muted)]">
{s.description}
</p>
</Link>
))}
</div>