Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1813b96f82 | |||
| f88b76546c | |||
| fdfa556d42 | |||
| d5344443e9 | |||
| 8b4129c233 | |||
| 69f0c857ec | |||
| 9d74cceb69 | |||
| e45c44721f | |||
| deff889f0c | |||
| 4d5186ff0c |
@@ -3,9 +3,10 @@ import Link from "next/link";
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { ArrowLeft, Calendar } from "lucide-react";
|
import { ArrowLeft, Calendar } from "lucide-react";
|
||||||
import { marked } from "marked";
|
import { renderContent } from "@/lib/content-render";
|
||||||
import { getPostBySlug } from "@/lib/data";
|
import { getPostBySlug } from "@/lib/data";
|
||||||
import { buildMetadata } from "@/lib/seo";
|
import { buildMetadata } from "@/lib/seo";
|
||||||
|
import { ContentSidebar } from "@/components/content-sidebar";
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
params,
|
params,
|
||||||
@@ -21,7 +22,10 @@ export async function generateMetadata({
|
|||||||
openGraph: {
|
openGraph: {
|
||||||
title: post.seo_title || post.title,
|
title: post.seo_title || post.title,
|
||||||
description: post.seo_description || post.excerpt || undefined,
|
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",
|
type: "article",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -36,10 +40,10 @@ export default async function BlogPostPage({
|
|||||||
const post = await getPostBySlug(slug);
|
const post = await getPostBySlug(slug);
|
||||||
if (!post || post.status !== "published") notFound();
|
if (!post || post.status !== "published") notFound();
|
||||||
|
|
||||||
const html = post.content ? marked.parse(post.content, { async: false }) as string : "";
|
const html = renderContent(post.content);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article className="mx-auto max-w-3xl px-6 py-20">
|
<div className="mx-auto max-w-7xl px-6 py-16">
|
||||||
<Link
|
<Link
|
||||||
href="/blog"
|
href="/blog"
|
||||||
className="inline-flex items-center gap-1 text-sm text-[var(--muted)] hover:text-[var(--navy)]"
|
className="inline-flex items-center gap-1 text-sm text-[var(--muted)] hover:text-[var(--navy)]"
|
||||||
@@ -47,7 +51,9 @@ export default async function BlogPostPage({
|
|||||||
<ArrowLeft className="size-3.5" /> Tüm yazılar
|
<ArrowLeft className="size-3.5" /> Tüm yazılar
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<header className="mt-6 border-b border-[var(--border)] pb-8">
|
<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 && (
|
{post.tags && post.tags.length > 0 && (
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{post.tags.map((t) => (
|
{post.tags.map((t) => (
|
||||||
@@ -98,5 +104,9 @@ export default async function BlogPostPage({
|
|||||||
dangerouslySetInnerHTML={{ __html: html }}
|
dangerouslySetInnerHTML={{ __html: html }}
|
||||||
/>
|
/>
|
||||||
</article>
|
</article>
|
||||||
|
|
||||||
|
<ContentSidebar currentSlug={slug} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+161
-13
@@ -1,21 +1,49 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { ArrowRight, Calendar } from "lucide-react";
|
import {
|
||||||
|
ArrowRight,
|
||||||
|
Calendar,
|
||||||
|
Sparkles,
|
||||||
|
MessageCircle,
|
||||||
|
Phone,
|
||||||
|
FileText,
|
||||||
|
} from "lucide-react";
|
||||||
import { SectionTitle } from "@/components/section-title";
|
import { SectionTitle } from "@/components/section-title";
|
||||||
import { listPublishedPosts } from "@/lib/data";
|
import { Icon } from "@/components/icon";
|
||||||
|
import {
|
||||||
|
getSiteSettings,
|
||||||
|
listPublishedPosts,
|
||||||
|
listServices,
|
||||||
|
} from "@/lib/data";
|
||||||
import { buildMetadata } from "@/lib/seo";
|
import { buildMetadata } from "@/lib/seo";
|
||||||
|
import { siteConfig } from "@/lib/site-config";
|
||||||
|
|
||||||
export async function generateMetadata(): Promise<Metadata> {
|
export async function generateMetadata(): Promise<Metadata> {
|
||||||
return buildMetadata("/blog", {
|
return buildMetadata("/blog", {
|
||||||
title: "Blog",
|
title: "Blog",
|
||||||
description: "Yazılım, web tasarım, SEO ve dijital pazarlama üzerine yazılar.",
|
description:
|
||||||
|
"Yazılım, web tasarım, SEO ve dijital pazarlama üzerine yazılar.",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function BlogIndex() {
|
export default async function BlogIndex() {
|
||||||
const posts = await listPublishedPosts();
|
const [posts, services, settings] = await Promise.all([
|
||||||
|
listPublishedPosts(),
|
||||||
|
listServices(),
|
||||||
|
getSiteSettings(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const phoneRaw = settings?.contact_phone_raw ?? siteConfig.contact.phoneRaw;
|
||||||
|
const phone = settings?.contact_phone ?? siteConfig.contact.phone;
|
||||||
|
const wa = phoneRaw.replace(/[^\d]/g, "");
|
||||||
|
const waMessage = settings?.whatsapp_message ?? "";
|
||||||
|
const waHref = `https://wa.me/${wa}${
|
||||||
|
waMessage ? `?text=${encodeURIComponent(waMessage)}` : ""
|
||||||
|
}`;
|
||||||
|
|
||||||
|
// Empty state — site daha yeni, içerik yok
|
||||||
|
if (posts.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto max-w-7xl px-6 py-20">
|
<div className="mx-auto max-w-7xl px-6 py-20">
|
||||||
<SectionTitle
|
<SectionTitle
|
||||||
@@ -24,15 +52,137 @@ export default async function BlogIndex() {
|
|||||||
description="Sektörden notlar, vaka çalışmaları ve teknik rehberler."
|
description="Sektörden notlar, vaka çalışmaları ve teknik rehberler."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="mt-14">
|
{/* Coming soon hero */}
|
||||||
{posts.length === 0 ? (
|
<div className="relative mt-14 overflow-hidden rounded-3xl bg-gradient-to-br from-[var(--navy)] via-[var(--sky-600)] to-[var(--sky)] p-12 text-center text-white shadow-xl shadow-[var(--navy)]/15">
|
||||||
<div className="rounded-2xl border border-dashed border-[var(--border)] bg-[var(--navy-50)]/40 p-12 text-center">
|
<div
|
||||||
<p className="text-sm text-[var(--muted)]">
|
className="absolute inset-0 opacity-10"
|
||||||
Henüz yayınlanmış yazı yok.
|
style={{
|
||||||
|
backgroundImage:
|
||||||
|
"radial-gradient(circle at 1px 1px, white 1px, transparent 0)",
|
||||||
|
backgroundSize: "24px 24px",
|
||||||
|
}}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<div className="relative">
|
||||||
|
<span className="inline-flex items-center gap-2 rounded-full border border-white/30 bg-white/10 px-4 py-1.5 text-xs font-medium backdrop-blur">
|
||||||
|
<Sparkles className="size-3.5" />
|
||||||
|
Yakında
|
||||||
|
</span>
|
||||||
|
<h2 className="mt-5 text-3xl font-bold sm:text-4xl">
|
||||||
|
Blog yazılarımızı hazırlıyoruz
|
||||||
|
</h2>
|
||||||
|
<p className="mx-auto mt-4 max-w-xl text-base text-white/80">
|
||||||
|
Sektörden vaka çalışmaları, teknik rehberler ve sahada öğrendiklerimizi
|
||||||
|
kısa sürede burada paylaşmaya başlayacağız.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-8 flex flex-wrap items-center justify-center gap-3">
|
||||||
|
<Link
|
||||||
|
href="/iletisim"
|
||||||
|
className="inline-flex items-center gap-2 rounded-xl bg-white px-5 py-3 text-sm font-semibold text-[var(--navy)] transition hover:-translate-y-0.5 hover:bg-blue-50"
|
||||||
|
>
|
||||||
|
Ücretsiz keşif görüşmesi
|
||||||
|
<ArrowRight className="size-4" />
|
||||||
|
</Link>
|
||||||
|
<a
|
||||||
|
href={waHref}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center gap-2 rounded-xl bg-[#25d366] px-5 py-3 text-sm font-semibold text-white transition hover:-translate-y-0.5 hover:bg-[#1ebe5d]"
|
||||||
|
>
|
||||||
|
<MessageCircle className="size-4" />
|
||||||
|
WhatsApp
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={`tel:${phoneRaw}`}
|
||||||
|
className="inline-flex items-center gap-2 rounded-xl border border-white/30 bg-white/5 px-5 py-3 text-sm font-semibold text-white backdrop-blur transition hover:border-white/60"
|
||||||
|
>
|
||||||
|
<Phone className="size-4" />
|
||||||
|
{phone}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bu arada → Hizmetler grid */}
|
||||||
|
{services.length > 0 && (
|
||||||
|
<section className="mt-16">
|
||||||
|
<div className="flex items-end justify-between">
|
||||||
|
<SectionTitle
|
||||||
|
align="left"
|
||||||
|
eyebrow="Bu arada"
|
||||||
|
title="Hizmetlerimize göz atın"
|
||||||
|
description="Blog yazılarımızı beklerken sunduğumuz çözümleri keşfedin."
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
href="/hizmetler"
|
||||||
|
className="hidden text-sm font-medium text-[var(--sky-600)] hover:text-[var(--navy)] sm:inline-flex"
|
||||||
|
>
|
||||||
|
Tümünü gör →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-10 grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{services.slice(0, 6).map((s) => (
|
||||||
|
<Link
|
||||||
|
key={s.slug}
|
||||||
|
href={`/hizmetler/${s.slug}`}
|
||||||
|
className="group relative overflow-hidden rounded-2xl border border-[var(--border)] bg-white p-6 transition-all duration-300 hover:-translate-y-1 hover:border-[var(--sky)]/40 hover:shadow-xl hover:shadow-[var(--navy)]/10"
|
||||||
|
>
|
||||||
|
<div className="flex size-12 items-center justify-center rounded-xl bg-gradient-to-br from-[var(--sky)] to-purple-500 text-white shadow-lg">
|
||||||
|
<Icon name={s.icon} className="size-5" />
|
||||||
|
</div>
|
||||||
|
<h3 className="mt-5 text-base font-bold text-[var(--navy)] group-hover:text-[var(--sky-600)]">
|
||||||
|
{s.title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 text-sm leading-relaxed text-[var(--muted)] line-clamp-2">
|
||||||
|
{s.description}
|
||||||
|
</p>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Lead magnet CTA */}
|
||||||
|
<section className="mt-16 rounded-2xl border border-dashed border-[var(--sky)]/40 bg-[var(--sky-50)]/40 p-8 sm:p-10">
|
||||||
|
<div className="flex flex-col items-start gap-6 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-[0.18em] text-[var(--sky-600)]">
|
||||||
|
<FileText className="mr-1 inline size-3.5" />
|
||||||
|
Ücretsiz Rapor
|
||||||
|
</p>
|
||||||
|
<h3 className="mt-2 text-2xl font-bold text-[var(--navy)]">
|
||||||
|
Site analiz raporunuzu alın
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 max-w-xl text-sm text-[var(--muted)]">
|
||||||
|
Mevcut sitenizin SEO, hız, mobil ve dönüşüm performansını ücretsiz
|
||||||
|
değerlendirelim. 24 saat içinde detaylı rapor e-postanızda.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
<Link
|
||||||
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
href="/site-analizi"
|
||||||
|
className="inline-flex items-center gap-2 rounded-xl bg-[var(--navy)] px-6 py-3 text-sm font-semibold text-white shadow-lg shadow-[var(--navy)]/20 transition hover:-translate-y-0.5 hover:bg-[var(--navy-700)]"
|
||||||
|
>
|
||||||
|
Ücretsiz raporumu istiyorum
|
||||||
|
<ArrowRight className="size-4" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Normal grid — yazı varsa
|
||||||
|
return (
|
||||||
|
<div className="mx-auto max-w-7xl px-6 py-20">
|
||||||
|
<SectionTitle
|
||||||
|
eyebrow="Blog"
|
||||||
|
title="Yazılım, tasarım ve büyüme üzerine"
|
||||||
|
description="Sektörden notlar, vaka çalışmaları ve teknik rehberler."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-14 grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{posts.map((p) => (
|
{posts.map((p) => (
|
||||||
<article
|
<article
|
||||||
key={p.$id}
|
key={p.$id}
|
||||||
@@ -77,8 +227,6 @@ export default async function BlogIndex() {
|
|||||||
</article>
|
</article>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,14 +81,14 @@ export default async function AboutPage() {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{team.length > 0 && (
|
{team.length > 0 && (
|
||||||
<section className="border-y border-[var(--border)] bg-[var(--navy-50)]/40 py-20">
|
<section className="border-y border-[var(--border)] bg-gray-50 py-20">
|
||||||
<div className="mx-auto max-w-7xl px-6">
|
<div className="mx-auto max-w-7xl px-6">
|
||||||
<SectionTitle
|
<SectionTitle
|
||||||
eyebrow="Ekip"
|
eyebrow="Ekibimiz"
|
||||||
title="İşi yapan insanları tanıyın"
|
title="Projenizde Kimlerle Çalışırsınız?"
|
||||||
description="Sizin projenizde birebir çalışacak ekip — geliştirici, tasarımcı ve proje yöneticileri."
|
description="Sizin projenizde birebir çalışacak kurucular — teknik altyapı ve ürün geliştirmenin arkasındaki isimler."
|
||||||
/>
|
/>
|
||||||
<div className="mt-12">
|
<div className="mt-14">
|
||||||
<TeamGrid members={team} />
|
<TeamGrid members={team} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { ArrowLeft, ArrowRight, CheckCircle2 } from "lucide-react";
|
import { CheckCircle2 } from "lucide-react";
|
||||||
import { marked } from "marked";
|
import { renderContent } from "@/lib/content-render";
|
||||||
import { getServiceBySlug, listProjects } from "@/lib/data";
|
import { getServiceBySlug, getSiteSettings, listProjects } from "@/lib/data";
|
||||||
import { buildMetadata } from "@/lib/seo";
|
import { buildMetadata } from "@/lib/seo";
|
||||||
import { Icon } from "@/components/icon";
|
|
||||||
import { ProjectsGrid } from "@/components/projects-grid";
|
import { ProjectsGrid } from "@/components/projects-grid";
|
||||||
import { SectionTitle } from "@/components/section-title";
|
import { SectionTitle } from "@/components/section-title";
|
||||||
import { FaqList } from "@/components/faq-list";
|
import { FaqList } from "@/components/faq-list";
|
||||||
|
import { ServiceHero } from "@/components/service-hero";
|
||||||
|
import { ServiceSidebar } from "@/components/service-sidebar";
|
||||||
import type { FaqItem } from "@/lib/types";
|
import type { FaqItem } from "@/lib/types";
|
||||||
|
|
||||||
export async function generateMetadata({
|
export async function generateMetadata({
|
||||||
@@ -50,64 +49,19 @@ export default async function ServiceDetailPage({
|
|||||||
const service = await getServiceBySlug(slug);
|
const service = await getServiceBySlug(slug);
|
||||||
if (!service) notFound();
|
if (!service) notFound();
|
||||||
|
|
||||||
const [relatedProjects] = await Promise.all([
|
const [relatedProjects, settings] = await Promise.all([
|
||||||
listProjects({ serviceSlug: slug, limit: 6 }),
|
listProjects({ serviceSlug: slug, limit: 6 }),
|
||||||
|
getSiteSettings(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const faqItems = parseFaq(service.faq);
|
const faqItems = parseFaq(service.faq);
|
||||||
const html = service.content
|
const html = renderContent(service.content);
|
||||||
? (marked.parse(service.content, { async: false }) as string)
|
|
||||||
: "";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<section className="relative overflow-hidden border-b border-[var(--border)]">
|
<ServiceHero service={service} settings={settings} />
|
||||||
<div className="absolute inset-0 hero-grid opacity-50" aria-hidden />
|
|
||||||
<div className="relative mx-auto max-w-7xl px-6 py-20">
|
|
||||||
<Link
|
|
||||||
href="/hizmetler"
|
|
||||||
className="inline-flex items-center gap-1 text-sm text-[var(--muted)] hover:text-[var(--navy)]"
|
|
||||||
>
|
|
||||||
<ArrowLeft className="size-3.5" /> Tüm hizmetler
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<div className="mt-6 grid items-start gap-10 md:grid-cols-[1fr_auto]">
|
<div className="mx-auto grid max-w-7xl gap-12 px-6 py-16 lg:grid-cols-[1.5fr_1fr]">
|
||||||
<div>
|
|
||||||
<div className="flex size-14 items-center justify-center rounded-2xl bg-[var(--navy-50)] text-[var(--navy)]">
|
|
||||||
<Icon name={service.icon} className="size-7" />
|
|
||||||
</div>
|
|
||||||
<h1 className="mt-5 text-4xl font-bold tracking-tight text-[var(--navy)] sm:text-5xl">
|
|
||||||
{service.title}
|
|
||||||
</h1>
|
|
||||||
<p className="mt-4 max-w-2xl text-lg leading-relaxed text-[var(--muted)]">
|
|
||||||
{service.description}
|
|
||||||
</p>
|
|
||||||
<Link
|
|
||||||
href="/iletisim"
|
|
||||||
className="mt-8 inline-flex items-center gap-2 rounded-full bg-[var(--navy)] px-6 py-3 text-sm font-medium text-white transition hover:bg-[var(--navy-700)]"
|
|
||||||
>
|
|
||||||
Bu hizmet için teklif al
|
|
||||||
<ArrowRight className="size-4" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{service.hero_image && (
|
|
||||||
<div className="relative hidden aspect-square w-64 overflow-hidden rounded-2xl md:block lg:w-80">
|
|
||||||
<Image
|
|
||||||
src={service.hero_image}
|
|
||||||
alt={service.title}
|
|
||||||
fill
|
|
||||||
sizes="320px"
|
|
||||||
className="object-cover"
|
|
||||||
priority
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div className="mx-auto grid max-w-7xl gap-12 px-6 py-16 lg:grid-cols-[2fr_1fr]">
|
|
||||||
<div>
|
<div>
|
||||||
{service.features && service.features.length > 0 && (
|
{service.features && service.features.length > 0 && (
|
||||||
<section className="mb-12">
|
<section className="mb-12">
|
||||||
@@ -147,34 +101,7 @@ export default async function ServiceDetailPage({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside className="space-y-4 lg:sticky lg:top-24 lg:self-start">
|
<ServiceSidebar currentSlug={slug} />
|
||||||
<div className="rounded-2xl border border-[var(--border)] bg-[var(--navy)] p-6 text-white">
|
|
||||||
<h3 className="text-base font-semibold">Bir proje mi var?</h3>
|
|
||||||
<p className="mt-2 text-sm text-white/70">
|
|
||||||
İhtiyacınızı anlatın, size en uygun çözümü hep birlikte planlayalım.
|
|
||||||
</p>
|
|
||||||
<Link
|
|
||||||
href="/iletisim"
|
|
||||||
className="mt-4 inline-flex items-center gap-2 rounded-full bg-white px-4 py-2 text-sm font-medium text-[var(--navy)] transition hover:bg-[var(--sky-50)]"
|
|
||||||
>
|
|
||||||
Bize ulaşın
|
|
||||||
<ArrowRight className="size-3.5" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="rounded-2xl border border-[var(--border)] bg-white p-6">
|
|
||||||
<h3 className="text-sm font-semibold uppercase tracking-wider text-[var(--muted)]">
|
|
||||||
Diğer hizmetler
|
|
||||||
</h3>
|
|
||||||
<Link
|
|
||||||
href="/hizmetler"
|
|
||||||
className="mt-3 inline-flex items-center gap-1 text-sm text-[var(--sky-600)] hover:text-[var(--navy)]"
|
|
||||||
>
|
|
||||||
Tümünü gör
|
|
||||||
<ArrowRight className="size-3.5" />
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{relatedProjects.length > 0 && (
|
{relatedProjects.length > 0 && (
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Link from "next/link";
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { ArrowLeft, Building2, Calendar, Clock, ExternalLink, Tag } from "lucide-react";
|
import { ArrowLeft, Building2, Calendar, Clock, ExternalLink, Tag } from "lucide-react";
|
||||||
import { marked } from "marked";
|
import { renderContent } from "@/lib/content-render";
|
||||||
import { getProjectBySlug, listProjects } from "@/lib/data";
|
import { getProjectBySlug, listProjects } from "@/lib/data";
|
||||||
import { buildMetadata } from "@/lib/seo";
|
import { buildMetadata } from "@/lib/seo";
|
||||||
import { Gallery } from "@/components/gallery";
|
import { Gallery } from "@/components/gallery";
|
||||||
@@ -54,9 +54,7 @@ export default async function ProjectDetailPage({
|
|||||||
const project = await getProjectBySlug(slug);
|
const project = await getProjectBySlug(slug);
|
||||||
if (!project) notFound();
|
if (!project) notFound();
|
||||||
|
|
||||||
const html = project.content
|
const html = renderContent(project.content);
|
||||||
? (marked.parse(project.content, { async: false }) as string)
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const metrics = parseMetrics(project.metrics);
|
const metrics = parseMetrics(project.metrics);
|
||||||
|
|
||||||
@@ -85,7 +83,11 @@ export default async function ProjectDetailPage({
|
|||||||
<ArrowLeft className="size-3.5" /> Tüm projeler
|
<ArrowLeft className="size-3.5" /> Tüm projeler
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="mt-6 grid items-start gap-10 lg:grid-cols-[1.4fr_1fr]">
|
<div
|
||||||
|
className={`mt-6 grid items-start gap-10 ${
|
||||||
|
meta.length >= 2 ? "lg:grid-cols-[1.4fr_1fr]" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
{project.category && (
|
{project.category && (
|
||||||
<span className="inline-flex rounded-full bg-[var(--sky-50)] px-3 py-1 text-xs font-medium text-[var(--sky-600)]">
|
<span className="inline-flex rounded-full bg-[var(--sky-50)] px-3 py-1 text-xs font-medium text-[var(--sky-600)]">
|
||||||
@@ -130,7 +132,8 @@ export default async function ProjectDetailPage({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{meta.length > 0 && (
|
{/* Meta tablo sadece 2+ alan dolu ise gösterilir — yarı boş card görünmesin */}
|
||||||
|
{meta.length >= 2 && (
|
||||||
<dl className="grid grid-cols-2 gap-4 rounded-2xl border border-[var(--border)] bg-white p-6">
|
<dl className="grid grid-cols-2 gap-4 rounded-2xl border border-[var(--border)] bg-white p-6">
|
||||||
{meta.map((m) => (
|
{meta.map((m) => (
|
||||||
<div key={m.label}>
|
<div key={m.label}>
|
||||||
@@ -147,6 +150,22 @@ export default async function ProjectDetailPage({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Tek-iki meta varsa daha kompakt inline strip olarak gösterilir */}
|
||||||
|
{meta.length > 0 && meta.length < 2 && (
|
||||||
|
<div className="mt-6 flex flex-wrap gap-3">
|
||||||
|
{meta.map((m) => (
|
||||||
|
<span
|
||||||
|
key={m.label}
|
||||||
|
className="inline-flex items-center gap-1.5 rounded-full border border-[var(--border)] bg-white px-3 py-1.5 text-xs text-[var(--muted)]"
|
||||||
|
>
|
||||||
|
{m.icon}
|
||||||
|
<span className="font-medium text-[var(--navy)]">{m.label}:</span>{" "}
|
||||||
|
{m.value}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{project.image_url && (
|
{project.image_url && (
|
||||||
<div className="relative mt-10 aspect-video overflow-hidden rounded-2xl">
|
<div className="relative mt-10 aspect-video overflow-hidden rounded-2xl">
|
||||||
<Image
|
<Image
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Link from "next/link";
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { ArrowRight, ArrowLeft, CheckCircle2 } from "lucide-react";
|
import { ArrowRight, ArrowLeft, CheckCircle2 } from "lucide-react";
|
||||||
import { marked } from "marked";
|
import { renderContent } from "@/lib/content-render";
|
||||||
import {
|
import {
|
||||||
getIndustryBySlug,
|
getIndustryBySlug,
|
||||||
listProjects,
|
listProjects,
|
||||||
@@ -65,9 +65,7 @@ export default async function IndustryPage({
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
const faqItems = parseFaq(industry.faq);
|
const faqItems = parseFaq(industry.faq);
|
||||||
const html = industry.content
|
const html = renderContent(industry.content);
|
||||||
? (marked.parse(industry.content, { async: false }) as string)
|
|
||||||
: "";
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
Textarea,
|
Textarea,
|
||||||
} from "@/components/admin/form";
|
} from "@/components/admin/form";
|
||||||
import { MediaPicker } from "@/components/admin/media-picker";
|
import { MediaPicker } from "@/components/admin/media-picker";
|
||||||
|
import { RichEditor } from "@/components/admin/rich-editor";
|
||||||
import { saveBlogPost } from "@/lib/admin-actions";
|
import { saveBlogPost } from "@/lib/admin-actions";
|
||||||
import type { BlogPostRow } from "@/lib/types";
|
import type { BlogPostRow } from "@/lib/types";
|
||||||
import { Save } from "lucide-react";
|
import { Save } from "lucide-react";
|
||||||
@@ -75,13 +76,19 @@ export function BlogForm({ post }: { post?: BlogPostRow }) {
|
|||||||
rows={3}
|
rows={3}
|
||||||
placeholder="Liste/kart görünümünde gösterilecek kısa özet"
|
placeholder="Liste/kart görünümünde gösterilecek kısa özet"
|
||||||
/>
|
/>
|
||||||
<Textarea
|
<div>
|
||||||
label="İçerik (Markdown)"
|
<span className="text-sm font-medium text-[var(--navy)]">
|
||||||
|
İçerik
|
||||||
|
</span>
|
||||||
|
<div className="mt-1.5">
|
||||||
|
<RichEditor
|
||||||
name="content"
|
name="content"
|
||||||
defaultValue={post?.content}
|
defaultValue={post?.content}
|
||||||
rows={14}
|
placeholder="Yazıya başlayın… `/` ile başlık, görsel, liste ekleyin"
|
||||||
placeholder={"# Başlık\n\nMarkdown desteklenir…"}
|
minHeight={500}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<MediaPicker
|
<MediaPicker
|
||||||
label="Kapak görseli"
|
label="Kapak görseli"
|
||||||
name="cover_image"
|
name="cover_image"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
Textarea,
|
Textarea,
|
||||||
} from "@/components/admin/form";
|
} from "@/components/admin/form";
|
||||||
import { MediaPicker } from "@/components/admin/media-picker";
|
import { MediaPicker } from "@/components/admin/media-picker";
|
||||||
|
import { RichEditor } from "@/components/admin/rich-editor";
|
||||||
import { saveService } from "@/lib/admin-actions";
|
import { saveService } from "@/lib/admin-actions";
|
||||||
import type { FaqItem, ServiceRow } from "@/lib/types";
|
import type { FaqItem, ServiceRow } from "@/lib/types";
|
||||||
|
|
||||||
@@ -90,14 +91,21 @@ export function ServiceForm({ service }: { service?: ServiceRow }) {
|
|||||||
help="Listede ve anasayfa kartında gösterilir."
|
help="Listede ve anasayfa kartında gösterilir."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Textarea
|
<div>
|
||||||
label="Detay içerik (Markdown)"
|
<span className="text-sm font-medium text-[var(--navy)]">
|
||||||
|
Detay içerik
|
||||||
|
</span>
|
||||||
|
<div className="mt-1.5">
|
||||||
|
<RichEditor
|
||||||
name="content"
|
name="content"
|
||||||
defaultValue={service?.content}
|
defaultValue={service?.content}
|
||||||
rows={10}
|
placeholder="Hizmetin detaylarını anlatın… `/` ile blok ekleyin"
|
||||||
placeholder={"## Yaklaşım\n\nMarkdown desteklenir…"}
|
|
||||||
help="Hizmet detay sayfasında ana içerik olarak gösterilir."
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-xs text-[var(--muted)]">
|
||||||
|
Hizmet detay sayfasında ana içerik olarak gösterilir.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Textarea
|
<Textarea
|
||||||
label="Özellikler"
|
label="Özellikler"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
Textarea,
|
Textarea,
|
||||||
} from "@/components/admin/form";
|
} from "@/components/admin/form";
|
||||||
import { MediaPicker } from "@/components/admin/media-picker";
|
import { MediaPicker } from "@/components/admin/media-picker";
|
||||||
|
import { RichEditor } from "@/components/admin/rich-editor";
|
||||||
import { saveProject } from "@/lib/admin-actions";
|
import { saveProject } from "@/lib/admin-actions";
|
||||||
import { listServices } from "@/lib/data";
|
import { listServices } from "@/lib/data";
|
||||||
import type { ProjectRow } from "@/lib/types";
|
import type { ProjectRow } from "@/lib/types";
|
||||||
@@ -95,16 +96,21 @@ export async function ProjectForm({ project }: { project?: ProjectRow }) {
|
|||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Textarea
|
<div>
|
||||||
label="Vaka çalışması içeriği (Markdown)"
|
<span className="text-sm font-medium text-[var(--navy)]">
|
||||||
|
Vaka çalışması içeriği
|
||||||
|
</span>
|
||||||
|
<div className="mt-1.5">
|
||||||
|
<RichEditor
|
||||||
name="content"
|
name="content"
|
||||||
defaultValue={project?.content}
|
defaultValue={project?.content}
|
||||||
rows={12}
|
placeholder="Müşteri / Problem / Çözüm / Sonuç…"
|
||||||
placeholder={
|
|
||||||
"## Müşteri\n\n## Problem\n\n## Çözüm\n\n## Sonuç"
|
|
||||||
}
|
|
||||||
help="Proje detay sayfasında uzun anlatım olarak gösterilir."
|
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-xs text-[var(--muted)]">
|
||||||
|
Proje detay sayfasında uzun anlatım olarak gösterilir.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<MediaPicker
|
<MediaPicker
|
||||||
label="Galeri görselleri"
|
label="Galeri görselleri"
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
Textarea,
|
Textarea,
|
||||||
} from "@/components/admin/form";
|
} from "@/components/admin/form";
|
||||||
import { MediaPicker } from "@/components/admin/media-picker";
|
import { MediaPicker } from "@/components/admin/media-picker";
|
||||||
|
import { RichEditor } from "@/components/admin/rich-editor";
|
||||||
import { saveIndustry } from "@/lib/admin-actions";
|
import { saveIndustry } from "@/lib/admin-actions";
|
||||||
import type { FaqItem, IndustryRow } from "@/lib/types";
|
import type { FaqItem, IndustryRow } from "@/lib/types";
|
||||||
|
|
||||||
@@ -75,13 +76,18 @@ export function IndustryForm({ row }: { row?: IndustryRow }) {
|
|||||||
placeholder="Avukatlar için KVKK uyumlu, randevu sistemli, SEO odaklı modern web siteleri."
|
placeholder="Avukatlar için KVKK uyumlu, randevu sistemli, SEO odaklı modern web siteleri."
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Textarea
|
<div>
|
||||||
label="İçerik (Markdown)"
|
<span className="text-sm font-medium text-[var(--navy)]">
|
||||||
|
İçerik
|
||||||
|
</span>
|
||||||
|
<div className="mt-1.5">
|
||||||
|
<RichEditor
|
||||||
name="content"
|
name="content"
|
||||||
rows={10}
|
|
||||||
defaultValue={row?.content}
|
defaultValue={row?.content}
|
||||||
placeholder="## Sektör özellikleri\n\nAvukatlar için..."
|
placeholder="Sektörünüz için içerik yazın…"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Textarea
|
<Textarea
|
||||||
label="Özellikler"
|
label="Özellikler"
|
||||||
|
|||||||
+43
-13
@@ -2,15 +2,23 @@
|
|||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--background: #ffffff;
|
||||||
--foreground: #0a0f1c;
|
--foreground: #0f172a;
|
||||||
--navy: #0f2c5c;
|
|
||||||
--navy-700: #15407f;
|
/* Kovak brand palette (WP'den aktarıldı) */
|
||||||
--navy-50: #eef3fb;
|
--navy: #043e8c; /* primary deep navy — CTA */
|
||||||
--sky: #4da3c7;
|
--navy-700: #032d66;
|
||||||
--sky-600: #2f87ad;
|
--navy-50: #eff6ff;
|
||||||
--sky-50: #ecf6fb;
|
--sky: #3b82f6; /* primary bright blue — accent */
|
||||||
--muted: #5b6577;
|
--sky-600: #2563eb;
|
||||||
--border: #e5e9f0;
|
--sky-50: #dbeafe;
|
||||||
|
|
||||||
|
/* Dark hero gradient stops */
|
||||||
|
--hero-dark-1: #0f172a;
|
||||||
|
--hero-dark-2: #1e293b;
|
||||||
|
--hero-dark-3: #334155;
|
||||||
|
|
||||||
|
--muted: #64748b;
|
||||||
|
--border: #e2e8f0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
@@ -24,30 +32,52 @@
|
|||||||
--color-sky-brand-50: var(--sky-50);
|
--color-sky-brand-50: var(--sky-50);
|
||||||
--color-muted-foreground: var(--muted);
|
--color-muted-foreground: var(--muted);
|
||||||
--color-border-soft: var(--border);
|
--color-border-soft: var(--border);
|
||||||
--font-sans: var(--font-geist-sans);
|
--font-sans: var(--font-poppins);
|
||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: var(--font-geist-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
font-family: var(--font-sans), Arial, Helvetica, sans-serif;
|
font-family: var(--font-poppins), Arial, Helvetica, sans-serif;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar { width: 5px; }
|
||||||
|
::-webkit-scrollbar-track { background: #f1f5f9; }
|
||||||
|
::-webkit-scrollbar-thumb { background: var(--navy); border-radius: 3px; }
|
||||||
|
|
||||||
.hero-grid {
|
.hero-grid {
|
||||||
background-image:
|
background-image:
|
||||||
radial-gradient(circle at 1px 1px, rgba(15, 44, 92, 0.08) 1px, transparent 0);
|
radial-gradient(circle at 1px 1px, rgba(4, 62, 140, 0.08) 1px, transparent 0);
|
||||||
background-size: 24px 24px;
|
background-size: 24px 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gradient-text {
|
.gradient-text {
|
||||||
background: linear-gradient(90deg, var(--navy) 0%, var(--sky) 100%);
|
background: linear-gradient(135deg, var(--navy) 0%, var(--sky) 100%);
|
||||||
-webkit-background-clip: text;
|
-webkit-background-clip: text;
|
||||||
background-clip: text;
|
background-clip: text;
|
||||||
color: transparent;
|
color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hero-dark {
|
||||||
|
background: linear-gradient(135deg, var(--hero-dark-1) 0%, var(--hero-dark-2) 50%, var(--hero-dark-3) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero-glow::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: -50%;
|
||||||
|
background: radial-gradient(circle, rgba(59, 130, 246, 0.1) 0%, transparent 70%);
|
||||||
|
animation: hero-pulse 4s ease-in-out infinite;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes hero-pulse {
|
||||||
|
0%, 100% { transform: scale(1); opacity: 0.5; }
|
||||||
|
50% { transform: scale(1.1); opacity: 0.8; }
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes float-slow {
|
@keyframes float-slow {
|
||||||
0%, 100% { transform: translateY(0); }
|
0%, 100% { transform: translateY(0); }
|
||||||
50% { transform: translateY(-12px); }
|
50% { transform: translateY(-12px); }
|
||||||
|
|||||||
+5
-6
@@ -6,9 +6,10 @@ import { ConsentInit } from "@/components/consent-init";
|
|||||||
import { CookieBanner } from "@/components/cookie-banner";
|
import { CookieBanner } from "@/components/cookie-banner";
|
||||||
import { getSeoSettings } from "@/lib/data";
|
import { getSeoSettings } from "@/lib/data";
|
||||||
|
|
||||||
const geistSans = Geist({
|
const sans = Geist({
|
||||||
variable: "--font-geist-sans",
|
variable: "--font-poppins",
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
|
display: "swap",
|
||||||
});
|
});
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
const geistMono = Geist_Mono({
|
||||||
@@ -46,12 +47,10 @@ export default async function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang="tr"
|
lang="tr"
|
||||||
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
className={`${sans.variable} ${geistMono.variable} h-full antialiased`}
|
||||||
>
|
>
|
||||||
<head>
|
|
||||||
<ConsentInit gtmId={gtmId} />
|
|
||||||
</head>
|
|
||||||
<body className="min-h-full flex flex-col bg-white text-[var(--foreground)]">
|
<body className="min-h-full flex flex-col bg-white text-[var(--foreground)]">
|
||||||
|
<ConsentInit gtmId={gtmId} />
|
||||||
{children}
|
{children}
|
||||||
<CookieBanner />
|
<CookieBanner />
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -0,0 +1,574 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useEditor, EditorContent } from "@tiptap/react";
|
||||||
|
import StarterKit from "@tiptap/starter-kit";
|
||||||
|
import Link from "@tiptap/extension-link";
|
||||||
|
import Image from "@tiptap/extension-image";
|
||||||
|
import Placeholder from "@tiptap/extension-placeholder";
|
||||||
|
import Underline from "@tiptap/extension-underline";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import {
|
||||||
|
Bold,
|
||||||
|
Italic,
|
||||||
|
Underline as UnderlineIcon,
|
||||||
|
Heading1,
|
||||||
|
Heading2,
|
||||||
|
Heading3,
|
||||||
|
List,
|
||||||
|
ListOrdered,
|
||||||
|
Quote,
|
||||||
|
Code,
|
||||||
|
Link as LinkIcon,
|
||||||
|
Image as ImageIcon,
|
||||||
|
Minus,
|
||||||
|
Undo,
|
||||||
|
Redo,
|
||||||
|
type LucideIcon,
|
||||||
|
} from "lucide-react";
|
||||||
|
|
||||||
|
interface BlockCommand {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
command: (e: ReturnType<typeof useEditor> & object) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SLASH_BLOCKS: BlockCommand[] = [
|
||||||
|
{
|
||||||
|
id: "h1",
|
||||||
|
title: "Başlık 1",
|
||||||
|
description: "Büyük başlık",
|
||||||
|
icon: Heading1,
|
||||||
|
command: (e) => e.chain().focus().deleteRange({ from: 0, to: 0 }).toggleHeading({ level: 1 }).run(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "h2",
|
||||||
|
title: "Başlık 2",
|
||||||
|
description: "Orta başlık",
|
||||||
|
icon: Heading2,
|
||||||
|
command: (e) => e.chain().focus().toggleHeading({ level: 2 }).run(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "h3",
|
||||||
|
title: "Başlık 3",
|
||||||
|
description: "Küçük başlık",
|
||||||
|
icon: Heading3,
|
||||||
|
command: (e) => e.chain().focus().toggleHeading({ level: 3 }).run(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ul",
|
||||||
|
title: "Madde listesi",
|
||||||
|
description: "Bullet list",
|
||||||
|
icon: List,
|
||||||
|
command: (e) => e.chain().focus().toggleBulletList().run(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "ol",
|
||||||
|
title: "Numaralı liste",
|
||||||
|
description: "Ordered list",
|
||||||
|
icon: ListOrdered,
|
||||||
|
command: (e) => e.chain().focus().toggleOrderedList().run(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "quote",
|
||||||
|
title: "Alıntı",
|
||||||
|
description: "Blockquote",
|
||||||
|
icon: Quote,
|
||||||
|
command: (e) => e.chain().focus().toggleBlockquote().run(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "code",
|
||||||
|
title: "Kod bloğu",
|
||||||
|
description: "Code block",
|
||||||
|
icon: Code,
|
||||||
|
command: (e) => e.chain().focus().toggleCodeBlock().run(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "hr",
|
||||||
|
title: "Ayırıcı",
|
||||||
|
description: "Yatay çizgi",
|
||||||
|
icon: Minus,
|
||||||
|
command: (e) => e.chain().focus().setHorizontalRule().run(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function RichEditor({
|
||||||
|
name,
|
||||||
|
defaultValue,
|
||||||
|
placeholder = "İçeriği yazmaya başlayın… `/` ile blok seç",
|
||||||
|
minHeight = 400,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
defaultValue?: string | null;
|
||||||
|
placeholder?: string;
|
||||||
|
minHeight?: number;
|
||||||
|
}) {
|
||||||
|
const [html, setHtml] = useState<string>(defaultValue ?? "");
|
||||||
|
const [showSlash, setShowSlash] = useState(false);
|
||||||
|
const [slashQuery, setSlashQuery] = useState("");
|
||||||
|
const [slashIndex, setSlashIndex] = useState(0);
|
||||||
|
const [imageModal, setImageModal] = useState(false);
|
||||||
|
|
||||||
|
const editor = useEditor({
|
||||||
|
extensions: [
|
||||||
|
StarterKit.configure({
|
||||||
|
heading: { levels: [1, 2, 3] },
|
||||||
|
}),
|
||||||
|
Underline,
|
||||||
|
Link.configure({
|
||||||
|
openOnClick: false,
|
||||||
|
HTMLAttributes: { class: "text-[var(--sky-600)] underline" },
|
||||||
|
}),
|
||||||
|
Image.configure({
|
||||||
|
HTMLAttributes: { class: "rounded-xl my-4" },
|
||||||
|
}),
|
||||||
|
Placeholder.configure({ placeholder }),
|
||||||
|
],
|
||||||
|
content: defaultValue || "",
|
||||||
|
editorProps: {
|
||||||
|
attributes: {
|
||||||
|
class:
|
||||||
|
"prose prose-base max-w-none focus:outline-none px-6 py-6 min-h-[400px]",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onUpdate: ({ editor }) => {
|
||||||
|
setHtml(editor.getHTML());
|
||||||
|
},
|
||||||
|
immediatelyRender: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Slash menu detection
|
||||||
|
useEffect(() => {
|
||||||
|
if (!editor) return;
|
||||||
|
const onUpdate = () => {
|
||||||
|
const { $from } = editor.state.selection;
|
||||||
|
const lineText = $from.parent.textContent;
|
||||||
|
const beforeCursor = lineText.slice(0, $from.parentOffset);
|
||||||
|
const match = beforeCursor.match(/\/(\w*)$/);
|
||||||
|
if (match) {
|
||||||
|
setShowSlash(true);
|
||||||
|
setSlashQuery(match[1].toLowerCase());
|
||||||
|
setSlashIndex(0);
|
||||||
|
} else {
|
||||||
|
setShowSlash(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
editor.on("update", onUpdate);
|
||||||
|
editor.on("selectionUpdate", onUpdate);
|
||||||
|
return () => {
|
||||||
|
editor.off("update", onUpdate);
|
||||||
|
editor.off("selectionUpdate", onUpdate);
|
||||||
|
};
|
||||||
|
}, [editor]);
|
||||||
|
|
||||||
|
const filteredBlocks = SLASH_BLOCKS.filter(
|
||||||
|
(b) =>
|
||||||
|
!slashQuery ||
|
||||||
|
b.title.toLowerCase().includes(slashQuery) ||
|
||||||
|
b.id.includes(slashQuery),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Slash menu keyboard
|
||||||
|
useEffect(() => {
|
||||||
|
if (!showSlash || !editor) return;
|
||||||
|
const handler = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape") {
|
||||||
|
setShowSlash(false);
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === "ArrowDown") {
|
||||||
|
setSlashIndex((i) => Math.min(i + 1, filteredBlocks.length - 1));
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === "ArrowUp") {
|
||||||
|
setSlashIndex((i) => Math.max(i - 1, 0));
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (e.key === "Enter") {
|
||||||
|
const block = filteredBlocks[slashIndex];
|
||||||
|
if (block) {
|
||||||
|
e.preventDefault();
|
||||||
|
// Delete the slash + query first
|
||||||
|
const { $from } = editor.state.selection;
|
||||||
|
const from = $from.pos - (slashQuery.length + 1);
|
||||||
|
editor.chain().focus().deleteRange({ from, to: $from.pos }).run();
|
||||||
|
block.command(
|
||||||
|
editor as unknown as ReturnType<typeof useEditor> & object,
|
||||||
|
);
|
||||||
|
setShowSlash(false);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", handler, true);
|
||||||
|
return () => window.removeEventListener("keydown", handler, true);
|
||||||
|
}, [showSlash, slashIndex, filteredBlocks, editor, slashQuery]);
|
||||||
|
|
||||||
|
if (!editor) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<input type="hidden" name={name} value={html} />
|
||||||
|
|
||||||
|
<div className="overflow-hidden rounded-2xl border border-[var(--border)] bg-white">
|
||||||
|
{/* Top toolbar — always visible, fixed */}
|
||||||
|
<div className="flex flex-wrap items-center gap-0.5 border-b border-[var(--border)] bg-[var(--navy-50)]/40 p-2">
|
||||||
|
<ToolbarButton
|
||||||
|
active={editor.isActive("bold")}
|
||||||
|
onClick={() => editor.chain().focus().toggleBold().run()}
|
||||||
|
icon={Bold}
|
||||||
|
label="Kalın (⌘B)"
|
||||||
|
/>
|
||||||
|
<ToolbarButton
|
||||||
|
active={editor.isActive("italic")}
|
||||||
|
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||||
|
icon={Italic}
|
||||||
|
label="İtalik (⌘I)"
|
||||||
|
/>
|
||||||
|
<ToolbarButton
|
||||||
|
active={editor.isActive("underline")}
|
||||||
|
onClick={() => editor.chain().focus().toggleUnderline().run()}
|
||||||
|
icon={UnderlineIcon}
|
||||||
|
label="Altı çizili"
|
||||||
|
/>
|
||||||
|
<Divider />
|
||||||
|
<ToolbarButton
|
||||||
|
active={editor.isActive("heading", { level: 1 })}
|
||||||
|
onClick={() =>
|
||||||
|
editor.chain().focus().toggleHeading({ level: 1 }).run()
|
||||||
|
}
|
||||||
|
icon={Heading1}
|
||||||
|
label="Başlık 1"
|
||||||
|
/>
|
||||||
|
<ToolbarButton
|
||||||
|
active={editor.isActive("heading", { level: 2 })}
|
||||||
|
onClick={() =>
|
||||||
|
editor.chain().focus().toggleHeading({ level: 2 }).run()
|
||||||
|
}
|
||||||
|
icon={Heading2}
|
||||||
|
label="Başlık 2"
|
||||||
|
/>
|
||||||
|
<ToolbarButton
|
||||||
|
active={editor.isActive("heading", { level: 3 })}
|
||||||
|
onClick={() =>
|
||||||
|
editor.chain().focus().toggleHeading({ level: 3 }).run()
|
||||||
|
}
|
||||||
|
icon={Heading3}
|
||||||
|
label="Başlık 3"
|
||||||
|
/>
|
||||||
|
<Divider />
|
||||||
|
<ToolbarButton
|
||||||
|
active={editor.isActive("bulletList")}
|
||||||
|
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
||||||
|
icon={List}
|
||||||
|
label="Madde listesi"
|
||||||
|
/>
|
||||||
|
<ToolbarButton
|
||||||
|
active={editor.isActive("orderedList")}
|
||||||
|
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||||
|
icon={ListOrdered}
|
||||||
|
label="Numaralı liste"
|
||||||
|
/>
|
||||||
|
<ToolbarButton
|
||||||
|
active={editor.isActive("blockquote")}
|
||||||
|
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
||||||
|
icon={Quote}
|
||||||
|
label="Alıntı"
|
||||||
|
/>
|
||||||
|
<ToolbarButton
|
||||||
|
active={editor.isActive("codeBlock")}
|
||||||
|
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
|
||||||
|
icon={Code}
|
||||||
|
label="Kod"
|
||||||
|
/>
|
||||||
|
<Divider />
|
||||||
|
<ToolbarButton
|
||||||
|
onClick={() => {
|
||||||
|
const previousUrl = editor.getAttributes("link").href;
|
||||||
|
const url = window.prompt("URL", previousUrl ?? "https://");
|
||||||
|
if (url === null) return;
|
||||||
|
if (url === "") {
|
||||||
|
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.extendMarkRange("link")
|
||||||
|
.setLink({ href: url })
|
||||||
|
.run();
|
||||||
|
}}
|
||||||
|
active={editor.isActive("link")}
|
||||||
|
icon={LinkIcon}
|
||||||
|
label="Bağlantı"
|
||||||
|
/>
|
||||||
|
<ToolbarButton
|
||||||
|
onClick={() => setImageModal(true)}
|
||||||
|
icon={ImageIcon}
|
||||||
|
label="Görsel ekle"
|
||||||
|
/>
|
||||||
|
<Divider />
|
||||||
|
<ToolbarButton
|
||||||
|
onClick={() => editor.chain().focus().undo().run()}
|
||||||
|
icon={Undo}
|
||||||
|
label="Geri al"
|
||||||
|
/>
|
||||||
|
<ToolbarButton
|
||||||
|
onClick={() => editor.chain().focus().redo().run()}
|
||||||
|
icon={Redo}
|
||||||
|
label="İleri al"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="relative">
|
||||||
|
<EditorContent editor={editor} style={{ minHeight }} />
|
||||||
|
|
||||||
|
{showSlash && filteredBlocks.length > 0 && (
|
||||||
|
<div className="absolute left-6 top-12 z-10 w-72 rounded-xl border border-[var(--border)] bg-white p-1 shadow-xl">
|
||||||
|
<p className="px-3 py-1.5 text-[10px] font-semibold uppercase tracking-wider text-[var(--muted)]">
|
||||||
|
Blok seç
|
||||||
|
</p>
|
||||||
|
{filteredBlocks.map((b, i) => {
|
||||||
|
const Icon = b.icon;
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={b.id}
|
||||||
|
type="button"
|
||||||
|
onMouseEnter={() => setSlashIndex(i)}
|
||||||
|
onClick={() => {
|
||||||
|
const { $from } = editor.state.selection;
|
||||||
|
const from = $from.pos - (slashQuery.length + 1);
|
||||||
|
editor
|
||||||
|
.chain()
|
||||||
|
.focus()
|
||||||
|
.deleteRange({ from, to: $from.pos })
|
||||||
|
.run();
|
||||||
|
b.command(
|
||||||
|
editor as unknown as ReturnType<typeof useEditor> &
|
||||||
|
object,
|
||||||
|
);
|
||||||
|
setShowSlash(false);
|
||||||
|
}}
|
||||||
|
className={`flex w-full items-center gap-3 rounded-lg px-3 py-2 text-left text-sm ${
|
||||||
|
i === slashIndex
|
||||||
|
? "bg-[var(--navy-50)]"
|
||||||
|
: "hover:bg-[var(--navy-50)]/60"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex size-8 items-center justify-center rounded-md bg-white text-[var(--navy)]">
|
||||||
|
<Icon className="size-4" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-[var(--navy)]">{b.title}</p>
|
||||||
|
<p className="text-[11px] text-[var(--muted)]">
|
||||||
|
{b.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Footer info */}
|
||||||
|
<p className="mt-2 text-xs text-[var(--muted)]">
|
||||||
|
‹/› ile blok ekle • Seçili metin için araç çubuğu otomatik açılır
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{imageModal && (
|
||||||
|
<ImagePickerModal
|
||||||
|
onClose={() => setImageModal(false)}
|
||||||
|
onPick={(url) => {
|
||||||
|
editor.chain().focus().setImage({ src: url }).run();
|
||||||
|
setImageModal(false);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToolbarButton({
|
||||||
|
icon: Icon,
|
||||||
|
label,
|
||||||
|
active,
|
||||||
|
onClick,
|
||||||
|
}: {
|
||||||
|
icon: LucideIcon;
|
||||||
|
label: string;
|
||||||
|
active?: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClick}
|
||||||
|
title={label}
|
||||||
|
aria-label={label}
|
||||||
|
className={`flex size-8 items-center justify-center rounded-md transition ${
|
||||||
|
active
|
||||||
|
? "bg-[var(--navy)] text-white"
|
||||||
|
: "text-[var(--muted)] hover:bg-white hover:text-[var(--navy)]"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Icon className="size-4" />
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function Divider() {
|
||||||
|
return <div className="mx-1 h-5 w-px bg-[var(--border)]" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Image picker — uses media library API ───────────────
|
||||||
|
|
||||||
|
interface MediaFile {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ImagePickerModal({
|
||||||
|
onClose,
|
||||||
|
onPick,
|
||||||
|
}: {
|
||||||
|
onClose: () => void;
|
||||||
|
onPick: (url: string) => void;
|
||||||
|
}) {
|
||||||
|
const [files, setFiles] = useState<MediaFile[] | null>(null);
|
||||||
|
const [uploading, setUploading] = useState(false);
|
||||||
|
const [progress, setProgress] = useState(0);
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/api/admin/media/list")
|
||||||
|
.then((r) => r.json())
|
||||||
|
.then((d) => setFiles(d.files || []))
|
||||||
|
.catch(() => setFiles([]));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function handleFile(file: File) {
|
||||||
|
setUploading(true);
|
||||||
|
setProgress(0);
|
||||||
|
const fd = new FormData();
|
||||||
|
fd.append("file", file);
|
||||||
|
const xhr = new XMLHttpRequest();
|
||||||
|
xhr.open("POST", "/api/admin/media/upload");
|
||||||
|
xhr.upload.addEventListener("progress", (e) => {
|
||||||
|
if (e.lengthComputable) setProgress(Math.round((e.loaded / e.total) * 100));
|
||||||
|
});
|
||||||
|
xhr.onload = () => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(xhr.responseText);
|
||||||
|
if (data.url) {
|
||||||
|
onPick(data.url);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
setUploading(false);
|
||||||
|
};
|
||||||
|
xhr.onerror = () => {
|
||||||
|
setUploading(false);
|
||||||
|
};
|
||||||
|
xhr.send(fd);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="w-full max-w-3xl rounded-2xl bg-white p-5 shadow-2xl"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<h3 className="text-base font-semibold text-[var(--navy)]">
|
||||||
|
Görsel ekle
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div className="mt-4 flex gap-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => inputRef.current?.click()}
|
||||||
|
disabled={uploading}
|
||||||
|
className="rounded-full bg-[var(--navy)] px-4 py-2 text-xs font-medium text-white hover:bg-[var(--navy-700)] disabled:opacity-60"
|
||||||
|
>
|
||||||
|
{uploading ? `Yükleniyor… ${progress}%` : "Yeni görsel yükle"}
|
||||||
|
</button>
|
||||||
|
<input
|
||||||
|
ref={inputRef}
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
onChange={(e) => {
|
||||||
|
const f = e.target.files?.[0];
|
||||||
|
if (f) handleFile(f);
|
||||||
|
e.target.value = "";
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{uploading && (
|
||||||
|
<div className="mt-3 h-1.5 overflow-hidden rounded-full bg-[var(--navy-50)]">
|
||||||
|
<div
|
||||||
|
className="h-full bg-[var(--sky)] transition-all"
|
||||||
|
style={{ width: `${progress}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="mt-5 max-h-[60vh] overflow-y-auto">
|
||||||
|
<p className="mb-2 text-xs font-semibold uppercase tracking-wider text-[var(--muted)]">
|
||||||
|
Kütüphane
|
||||||
|
</p>
|
||||||
|
{!files ? (
|
||||||
|
<p className="py-8 text-center text-sm text-[var(--muted)]">
|
||||||
|
Yükleniyor…
|
||||||
|
</p>
|
||||||
|
) : files.length === 0 ? (
|
||||||
|
<p className="py-8 text-center text-sm text-[var(--muted)]">
|
||||||
|
Kütüphane boş. Yeni görsel yükleyin.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-3 gap-2 sm:grid-cols-4 lg:grid-cols-5">
|
||||||
|
{files.map((f) => (
|
||||||
|
<button
|
||||||
|
key={f.id}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onPick(f.url)}
|
||||||
|
className="aspect-square overflow-hidden rounded-lg border border-[var(--border)] transition hover:border-[var(--sky)]"
|
||||||
|
>
|
||||||
|
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||||
|
<img
|
||||||
|
src={f.url}
|
||||||
|
alt={f.name}
|
||||||
|
className="size-full object-cover"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-4 text-right">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onClose}
|
||||||
|
className="rounded-full border border-[var(--border)] bg-white px-4 py-2 text-xs font-medium text-[var(--muted)] hover:text-[var(--navy)]"
|
||||||
|
>
|
||||||
|
Kapat
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
+11
-30
@@ -1,42 +1,23 @@
|
|||||||
import Script from "next/script";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Google Consent Mode v2 — defaults set to "denied" before any tag loads.
|
* Google Consent Mode v2 — default consent state denied'da.
|
||||||
* After the user makes a choice, CookieBanner calls gtag('consent','update', ...).
|
* Consent default ayarı `/public/consent-default.js`'den senkron yüklenir
|
||||||
|
* (script src ile — React'ın inline script warning'inden kaçınmak için).
|
||||||
*
|
*
|
||||||
* GTM/GA inject sadece site_settings.gtm_id doluysa yapılır.
|
* GTM script'i sadece gtm_id varsa yüklenir, aynı src pattern'i kullanır.
|
||||||
*/
|
*/
|
||||||
export function ConsentInit({ gtmId }: { gtmId?: string | null }) {
|
export function ConsentInit({ gtmId }: { gtmId?: string | null }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Script id="consent-default" strategy="beforeInteractive">
|
{/* Default consent — synchronous, runs before any other JS */}
|
||||||
{`
|
<script src="/consent-default.js" />
|
||||||
window.dataLayer = window.dataLayer || [];
|
|
||||||
function gtag(){dataLayer.push(arguments);}
|
|
||||||
window.gtag = gtag;
|
|
||||||
gtag('consent', 'default', {
|
|
||||||
ad_storage: 'denied',
|
|
||||||
ad_user_data: 'denied',
|
|
||||||
ad_personalization: 'denied',
|
|
||||||
analytics_storage: 'denied',
|
|
||||||
functionality_storage: 'granted',
|
|
||||||
personalization_storage: 'denied',
|
|
||||||
security_storage: 'granted',
|
|
||||||
wait_for_update: 500,
|
|
||||||
});
|
|
||||||
`}
|
|
||||||
</Script>
|
|
||||||
|
|
||||||
|
{/* GTM */}
|
||||||
{gtmId && (
|
{gtmId && (
|
||||||
<>
|
<>
|
||||||
<Script id="gtm-script" strategy="afterInteractive">
|
<script
|
||||||
{`
|
async
|
||||||
(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': new Date().getTime(),event:'gtm.js'});
|
src={`https://www.googletagmanager.com/gtm.js?id=${gtmId}`}
|
||||||
var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';
|
/>
|
||||||
j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
|
||||||
})(window,document,'script','dataLayer','${gtmId}');
|
|
||||||
`}
|
|
||||||
</Script>
|
|
||||||
<noscript>
|
<noscript>
|
||||||
<iframe
|
<iframe
|
||||||
src={`https://www.googletagmanager.com/ns.html?id=${gtmId}`}
|
src={`https://www.googletagmanager.com/ns.html?id=${gtmId}`}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
"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 = "";
|
||||||
|
// Pill mode toggle: data-pill-hide gizlenir, data-pill-show görünür
|
||||||
|
const hidables = document.querySelectorAll<HTMLElement>(
|
||||||
|
'[data-pill-hide="true"]',
|
||||||
|
);
|
||||||
|
const showables = document.querySelectorAll<HTMLElement>(
|
||||||
|
'[data-pill-show="true"]',
|
||||||
|
);
|
||||||
|
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";
|
||||||
|
hidables.forEach((el) => {
|
||||||
|
el.style.display = "none";
|
||||||
|
});
|
||||||
|
showables.forEach((el) => {
|
||||||
|
el.style.display = "inline-flex";
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
pillWrap.style.padding = "";
|
||||||
|
header.style.maxWidth = "";
|
||||||
|
header.style.borderRadius = "";
|
||||||
|
header.style.border = "";
|
||||||
|
header.style.boxShadow = "";
|
||||||
|
navBar.style.height = "";
|
||||||
|
navBar.style.padding = "";
|
||||||
|
hidables.forEach((el) => {
|
||||||
|
el.style.display = "";
|
||||||
|
});
|
||||||
|
showables.forEach((el) => {
|
||||||
|
el.style.display = "none";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
+150
-27
@@ -1,53 +1,176 @@
|
|||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Phone } from "lucide-react";
|
import { ChevronDown, Phone } from "lucide-react";
|
||||||
import { getSiteSettings } from "@/lib/data";
|
import { getSiteSettings, listServices } from "@/lib/data";
|
||||||
import { siteConfig } from "@/lib/site-config";
|
import { siteConfig } from "@/lib/site-config";
|
||||||
|
import { HeaderScrollEffect } from "@/components/header-scroll";
|
||||||
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" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export async function Header() {
|
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 phone = settings?.contact_phone ?? siteConfig.contact.phone;
|
||||||
const phoneRaw = settings?.contact_phone_raw ?? siteConfig.contact.phoneRaw;
|
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 (
|
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">
|
<HeaderScrollEffect />
|
||||||
<Link href="/" className="flex items-center gap-3">
|
<div className="sticky top-0 z-50 w-full" id="floating-header-wrap">
|
||||||
<Image src="/logo.png" alt={siteConfig.name} width={44} height={44} priority />
|
<div id="header-pill-wrap" className="transition-all duration-300 ease-out">
|
||||||
<span className="hidden text-base font-semibold tracking-tight text-[var(--navy)] sm:block">
|
<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"
|
||||||
|
>
|
||||||
|
{/* 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}
|
{siteConfig.name}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<nav className="hidden items-center gap-8 md:flex">
|
{/* Col 2 — Desktop nav */}
|
||||||
{nav.map((item) => (
|
<div className="hidden items-center gap-0.5 lg:flex">
|
||||||
<Link
|
<Link
|
||||||
key={item.href}
|
href="/"
|
||||||
href={item.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"
|
||||||
className="text-sm font-medium text-[var(--muted)] transition hover:text-[var(--navy)]"
|
|
||||||
>
|
>
|
||||||
{item.label}
|
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>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</nav>
|
</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">
|
||||||
|
{/* Phone — full mode (XL) */}
|
||||||
<a
|
<a
|
||||||
href={`tel:${phoneRaw}`}
|
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"
|
data-pill-hide="true"
|
||||||
|
className="hidden 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 xl:inline-flex"
|
||||||
|
aria-label={phone}
|
||||||
>
|
>
|
||||||
<Phone className="size-4" />
|
<Phone className="size-3.5" />
|
||||||
{phone}
|
<span>{phone}</span>
|
||||||
</a>
|
</a>
|
||||||
|
{/* "Ara" — pill mode'da görünür, kompakt */}
|
||||||
|
<a
|
||||||
|
href={`tel:${phoneRaw}`}
|
||||||
|
data-pill-show="true"
|
||||||
|
className="hidden h-9 items-center gap-1.5 rounded-lg border border-gray-200 px-3 text-sm font-medium text-gray-700 transition-colors hover:border-[var(--navy)] hover:text-[var(--navy)]"
|
||||||
|
aria-label={`${phone} - Ara`}
|
||||||
|
style={{ display: "none" }}
|
||||||
|
>
|
||||||
|
<Phone className="size-3.5" />
|
||||||
|
<span>Ara</span>
|
||||||
|
</a>
|
||||||
|
<Link
|
||||||
|
href="/iletisim"
|
||||||
|
className="inline-flex h-9 items-center justify-center whitespace-nowrap 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>
|
</div>
|
||||||
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+57
-44
@@ -1,11 +1,11 @@
|
|||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ArrowRight, Sparkles } from "lucide-react";
|
import { ArrowRight, Sparkles } from "lucide-react";
|
||||||
import type { SiteSettingsRow, StatItem } from "@/lib/types";
|
import type { SiteSettingsRow, StatItem } from "@/lib/types";
|
||||||
|
|
||||||
const DEFAULT_STATS: StatItem[] = [
|
const DEFAULT_STATS: StatItem[] = [
|
||||||
{ value: "50+", label: "Tamamlanan proje" },
|
{ value: "150+", label: "Tamamlanan proje" },
|
||||||
{ value: "10+", label: "Yıllık deneyim" },
|
{ value: "50+", label: "Aktif müşteri" },
|
||||||
|
{ value: "100%", label: "Memnuniyet" },
|
||||||
{ value: "24/7", label: "Teknik destek" },
|
{ value: "24/7", label: "Teknik destek" },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -24,84 +24,97 @@ function parseStats(items?: string[] | null): StatItem[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Hero({ settings }: { settings?: SiteSettingsRow | null }) {
|
export function Hero({ settings }: { settings?: SiteSettingsRow | null }) {
|
||||||
const badge = settings?.hero_badge ?? "Kocaeli'nin teknoloji ajansı";
|
const badge =
|
||||||
|
settings?.hero_badge ?? "Kocaeli Web Tasarım & Yazılım Ajansı";
|
||||||
const title =
|
const title =
|
||||||
settings?.hero_title ?? "Fikirden ürüne tek bir partner ile yola çıkın";
|
settings?.hero_title ??
|
||||||
|
"Kocaeli Web Tasarım ve Yazılım Ajansı";
|
||||||
const subtitle =
|
const subtitle =
|
||||||
settings?.hero_subtitle ??
|
settings?.hero_subtitle ??
|
||||||
"Web, mobil ve CRM çözümlerinde uçtan uca geliştirme. Markanıza özel tasarım, ölçeklenebilir altyapı ve uzun vadeli destek.";
|
"Kocaeli ve İzmit'te profesyonel web tasarım, SEO optimizasyonu ve özel yazılım çözümleri. 2015'ten bu yana işletmelere dijital dönüşümde rehberlik ediyoruz.";
|
||||||
const primaryLabel =
|
const primaryLabel = settings?.hero_cta_primary_label ?? "Ücretsiz Teklif Al";
|
||||||
settings?.hero_cta_primary_label ?? "Proje görüşmesi başlat";
|
|
||||||
const primaryHref = settings?.hero_cta_primary_href ?? "/iletisim";
|
const primaryHref = settings?.hero_cta_primary_href ?? "/iletisim";
|
||||||
const secondaryLabel =
|
const secondaryLabel =
|
||||||
settings?.hero_cta_secondary_label ?? "Hizmetlerimizi inceleyin";
|
settings?.hero_cta_secondary_label ?? "Hizmetlerimizi İnceleyin";
|
||||||
const secondaryHref = settings?.hero_cta_secondary_href ?? "/hizmetler";
|
const secondaryHref = settings?.hero_cta_secondary_href ?? "/hizmetler";
|
||||||
const stats = parseStats(settings?.hero_stats);
|
const stats = parseStats(settings?.hero_stats);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="relative overflow-hidden">
|
<section className="hero-dark hero-glow relative overflow-hidden">
|
||||||
<div className="absolute inset-0 hero-grid opacity-60" aria-hidden />
|
{/* Grid pattern overlay */}
|
||||||
<div className="absolute -right-32 top-1/2 -z-0 size-[520px] -translate-y-1/2 rounded-full bg-gradient-to-br from-[var(--sky)]/30 to-[var(--navy)]/0 blur-3xl" aria-hidden />
|
<div
|
||||||
|
className="absolute inset-0 opacity-[0.04]"
|
||||||
|
style={{
|
||||||
|
backgroundImage:
|
||||||
|
"radial-gradient(circle at 1px 1px, white 1px, transparent 0)",
|
||||||
|
backgroundSize: "32px 32px",
|
||||||
|
}}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="relative mx-auto grid max-w-7xl items-center gap-12 px-6 py-24 md:grid-cols-2 md:py-32">
|
<div className="relative mx-auto max-w-5xl px-6 py-24 text-center md:py-32">
|
||||||
<div>
|
<span className="inline-flex items-center gap-2 rounded-full border border-blue-400/30 bg-[#043e8c]/30 px-4 py-1.5 text-sm font-medium text-blue-300">
|
||||||
<span className="inline-flex items-center gap-2 rounded-full border border-[var(--sky)]/30 bg-[var(--sky-50)] px-3 py-1 text-xs font-medium text-[var(--sky-600)]">
|
<Sparkles className="size-4" />
|
||||||
<Sparkles className="size-3.5" />
|
|
||||||
{badge}
|
{badge}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<h1 className="mt-6 text-4xl font-bold leading-tight tracking-tight text-[var(--navy)] sm:text-5xl md:text-6xl">
|
<h1 className="mt-6 text-4xl font-extrabold leading-[1.1] tracking-tight text-white sm:text-5xl md:text-6xl">
|
||||||
{title}
|
{title.split(" ").map((word, i, arr) => {
|
||||||
|
// Highlight key words in blue (Web Tasarım, Yazılım)
|
||||||
|
const highlight =
|
||||||
|
word.toLowerCase().includes("web") ||
|
||||||
|
word.toLowerCase().includes("tasarım") ||
|
||||||
|
word.toLowerCase().includes("yazılım") ||
|
||||||
|
word.toLowerCase().includes("ajansı");
|
||||||
|
return (
|
||||||
|
<span key={i}>
|
||||||
|
<span className={highlight ? "text-blue-400" : undefined}>
|
||||||
|
{word}
|
||||||
|
</span>
|
||||||
|
{i < arr.length - 1 && " "}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<p className="mt-6 max-w-xl text-lg leading-relaxed text-[var(--muted)]">
|
<p className="mx-auto mt-6 max-w-2xl text-base leading-relaxed text-slate-300 sm:text-lg">
|
||||||
{subtitle}
|
{subtitle}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
|
<div className="mt-10 flex flex-col justify-center gap-3 sm:flex-row">
|
||||||
<Link
|
<Link
|
||||||
href={primaryHref}
|
href={primaryHref}
|
||||||
className="inline-flex items-center justify-center gap-2 rounded-full bg-[var(--navy)] px-6 py-3 text-sm font-medium text-white transition hover:bg-[var(--navy-700)]"
|
className="inline-flex items-center justify-center gap-2 rounded-xl bg-[var(--sky)] px-7 py-3.5 text-sm font-semibold text-white shadow-lg shadow-[var(--sky)]/30 transition hover:-translate-y-0.5 hover:bg-[var(--sky-600)] hover:shadow-xl hover:shadow-[var(--sky)]/40"
|
||||||
>
|
>
|
||||||
{primaryLabel}
|
{primaryLabel}
|
||||||
<ArrowRight className="size-4" />
|
<ArrowRight className="size-4" />
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href={secondaryHref}
|
href={secondaryHref}
|
||||||
className="inline-flex items-center justify-center gap-2 rounded-full border border-[var(--border)] bg-white px-6 py-3 text-sm font-medium text-[var(--navy)] transition hover:border-[var(--navy)]"
|
className="inline-flex items-center justify-center gap-2 rounded-xl border border-white/20 bg-white/5 px-7 py-3.5 text-sm font-semibold text-white backdrop-blur transition hover:border-white/40 hover:bg-white/10"
|
||||||
>
|
>
|
||||||
{secondaryLabel}
|
{secondaryLabel}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Stats strip — pinned bottom of hero */}
|
||||||
{stats.length > 0 && (
|
{stats.length > 0 && (
|
||||||
<dl className="mt-12 grid max-w-md grid-cols-3 gap-6">
|
<div className="relative border-t border-white/10 bg-black/20 backdrop-blur">
|
||||||
|
<div className="mx-auto grid max-w-6xl grid-cols-2 gap-4 px-6 py-8 sm:grid-cols-4">
|
||||||
{stats.map((stat) => (
|
{stats.map((stat) => (
|
||||||
<div key={stat.label}>
|
<div key={stat.label} className="text-center">
|
||||||
<dt className="text-2xl font-bold text-[var(--navy)]">
|
<p className="text-3xl font-bold text-[var(--sky)] sm:text-4xl">
|
||||||
{stat.value}
|
{stat.value}
|
||||||
</dt>
|
</p>
|
||||||
<dd className="mt-1 text-xs text-[var(--muted)]">{stat.label}</dd>
|
<p className="mt-1 text-xs text-slate-400 sm:text-sm">
|
||||||
|
{stat.label}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</dl>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="relative flex justify-center">
|
|
||||||
<div className="absolute inset-8 -z-10 rounded-full bg-gradient-to-br from-[var(--sky-50)] to-white blur-2xl" aria-hidden />
|
|
||||||
<div className="animate-float">
|
|
||||||
<Image
|
|
||||||
src="/logo.png"
|
|
||||||
alt="Kovak Yazılım"
|
|
||||||
width={420}
|
|
||||||
height={420}
|
|
||||||
priority
|
|
||||||
className="size-[320px] object-contain drop-shadow-[0_30px_50px_rgba(15,44,92,0.25)] md:size-[420px]"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,15 @@ import Link from "next/link";
|
|||||||
import { ArrowUpRight, ExternalLink } from "lucide-react";
|
import { ArrowUpRight, ExternalLink } from "lucide-react";
|
||||||
import type { ProjectRow } from "@/lib/types";
|
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[] }) {
|
export function ProjectsGrid({ projects }: { projects: ProjectRow[] }) {
|
||||||
if (projects.length === 0) {
|
if (projects.length === 0) {
|
||||||
return (
|
return (
|
||||||
@@ -15,29 +24,37 @@ export function ProjectsGrid({ projects }: { projects: ProjectRow[] }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
{projects.map((p) => (
|
{projects.map((p) => {
|
||||||
|
const tagColor = p.category && CATEGORY_COLORS[p.category]
|
||||||
|
? CATEGORY_COLORS[p.category]
|
||||||
|
: "bg-[var(--navy)]";
|
||||||
|
return (
|
||||||
<article
|
<article
|
||||||
key={p.$id}
|
key={p.$id}
|
||||||
className="group overflow-hidden rounded-2xl border border-[var(--border)] bg-white transition hover:shadow-xl"
|
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">
|
<Link href={`/projeler/${p.slug}`} className="block">
|
||||||
<div className="relative aspect-video overflow-hidden bg-[var(--navy-50)]">
|
<div className="relative aspect-[5/3] overflow-hidden bg-[var(--navy-50)]">
|
||||||
{p.image_url ? (
|
{p.image_url ? (
|
||||||
<Image
|
<Image
|
||||||
src={p.image_url}
|
src={p.image_url}
|
||||||
alt={p.title}
|
alt={p.title}
|
||||||
fill
|
fill
|
||||||
sizes="(min-width: 1024px) 33vw, (min-width: 768px) 50vw, 100vw"
|
sizes="(min-width: 1024px) 33vw, (min-width: 768px) 50vw, 100vw"
|
||||||
className="object-cover transition group-hover:scale-105"
|
className="object-cover transition-transform duration-500 group-hover:scale-105"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full items-center justify-center text-[var(--navy)]/30">
|
<div className="flex h-full items-center justify-center text-[var(--navy)]/30">
|
||||||
<span className="text-5xl font-bold">{p.title.charAt(0)}</span>
|
<span className="text-5xl font-bold">{p.title.charAt(0)}</span>
|
||||||
</div>
|
</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 && (
|
{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">
|
<span
|
||||||
|
className={`absolute left-4 top-4 rounded-full ${tagColor} px-3 py-1 text-xs font-semibold text-white shadow-lg`}
|
||||||
|
>
|
||||||
{p.category}
|
{p.category}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -46,7 +63,7 @@ export function ProjectsGrid({ projects }: { projects: ProjectRow[] }) {
|
|||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
<div className="flex items-start justify-between gap-3">
|
<div className="flex items-start justify-between gap-3">
|
||||||
<Link href={`/projeler/${p.slug}`} className="block">
|
<Link href={`/projeler/${p.slug}`} className="block">
|
||||||
<h3 className="text-lg font-semibold text-[var(--navy)] transition group-hover:text-[var(--sky-600)]">
|
<h3 className="text-lg font-bold text-[var(--navy)] transition-colors group-hover:text-[var(--sky-600)]">
|
||||||
{p.title}
|
{p.title}
|
||||||
</h3>
|
</h3>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -56,12 +73,12 @@ export function ProjectsGrid({ projects }: { projects: ProjectRow[] }) {
|
|||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
aria-label="Projeyi canlı aç"
|
aria-label="Projeyi canlı aç"
|
||||||
className="text-[var(--sky-600)] hover:text-[var(--navy)]"
|
className="text-[var(--sky-600)] transition-colors hover:text-[var(--navy)]"
|
||||||
>
|
>
|
||||||
<ExternalLink className="size-4" />
|
<ExternalLink className="size-4" />
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
<ArrowUpRight className="size-5 text-[var(--muted)] transition group-hover:text-[var(--sky-600)]" />
|
<ArrowUpRight className="size-5 text-[var(--muted)] transition-colors group-hover:text-[var(--sky-600)]" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-sm leading-relaxed text-[var(--muted)] line-clamp-3">
|
<p className="mt-2 text-sm leading-relaxed text-[var(--muted)] line-clamp-3">
|
||||||
@@ -72,7 +89,7 @@ export function ProjectsGrid({ projects }: { projects: ProjectRow[] }) {
|
|||||||
{p.technologies.map((t) => (
|
{p.technologies.map((t) => (
|
||||||
<span
|
<span
|
||||||
key={t}
|
key={t}
|
||||||
className="rounded-md bg-[var(--navy-50)] px-2 py-0.5 text-xs text-[var(--navy-700)]"
|
className="rounded-md bg-[var(--navy-50)] px-2 py-0.5 text-xs font-medium text-[var(--navy-700)]"
|
||||||
>
|
>
|
||||||
{t}
|
{t}
|
||||||
</span>
|
</span>
|
||||||
@@ -81,7 +98,8 @@ export function ProjectsGrid({ projects }: { projects: ProjectRow[] }) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,209 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { ArrowLeft, ArrowRight, MessageCircle, Phone, Sparkles, CheckCircle2 } from "lucide-react";
|
||||||
|
import { Icon } from "@/components/icon";
|
||||||
|
import type { ServiceRow, SiteSettingsRow } from "@/lib/types";
|
||||||
|
import { siteConfig } from "@/lib/site-config";
|
||||||
|
|
||||||
|
const QUICK_TRUST = [
|
||||||
|
"2-3 hafta teslim",
|
||||||
|
"1 yıl ücretsiz destek",
|
||||||
|
"İlk taslak ücretsiz",
|
||||||
|
"Yerel ekip — Kocaeli",
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ServiceHero({
|
||||||
|
service,
|
||||||
|
settings,
|
||||||
|
}: {
|
||||||
|
service: ServiceRow;
|
||||||
|
settings?: SiteSettingsRow | null;
|
||||||
|
}) {
|
||||||
|
const phoneRaw = settings?.contact_phone_raw ?? siteConfig.contact.phoneRaw;
|
||||||
|
const phone = settings?.contact_phone ?? siteConfig.contact.phone;
|
||||||
|
const wa = phoneRaw.replace(/[^\d]/g, "");
|
||||||
|
const waMessage = settings?.whatsapp_message ?? `Merhaba, ${service.title} hizmeti hakkında bilgi almak istiyorum.`;
|
||||||
|
const waHref = `https://wa.me/${wa}?text=${encodeURIComponent(waMessage)}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="relative overflow-hidden border-b border-[var(--border)] bg-gradient-to-br from-[var(--navy-50)]/60 via-white to-[var(--sky-50)]/40">
|
||||||
|
{/* Subtle grid + glow */}
|
||||||
|
<div className="absolute inset-0 hero-grid opacity-50" aria-hidden />
|
||||||
|
<div
|
||||||
|
className="absolute -right-32 top-1/2 size-[520px] -translate-y-1/2 rounded-full bg-gradient-to-br from-[var(--sky)]/15 to-transparent blur-3xl"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="relative mx-auto max-w-7xl px-6 py-16 lg:py-20">
|
||||||
|
<Link
|
||||||
|
href="/hizmetler"
|
||||||
|
className="inline-flex items-center gap-1 text-sm text-[var(--muted)] hover:text-[var(--navy)]"
|
||||||
|
>
|
||||||
|
<ArrowLeft className="size-3.5" /> Tüm hizmetler
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="mt-8 grid items-start gap-12 lg:grid-cols-[1.3fr_1fr]">
|
||||||
|
{/* Left — content */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="relative">
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 -z-10 rounded-2xl bg-gradient-to-br from-[var(--sky)] to-purple-500 blur-md opacity-50"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<div className="flex size-16 items-center justify-center rounded-2xl bg-gradient-to-br from-[var(--sky)] to-purple-500 text-white shadow-lg">
|
||||||
|
<Icon name={service.icon} className="size-8" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<span className="inline-flex items-center gap-2 rounded-full border border-[var(--sky)]/30 bg-white px-3 py-1 text-xs font-medium text-[var(--sky-600)]">
|
||||||
|
<Sparkles className="size-3.5" />
|
||||||
|
Profesyonel hizmet
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1 className="mt-6 text-4xl font-extrabold leading-[1.1] tracking-tight text-[var(--navy)] sm:text-5xl lg:text-6xl">
|
||||||
|
<span className="gradient-text">{service.title}</span>
|
||||||
|
</h1>
|
||||||
|
<p className="mt-5 max-w-xl text-lg leading-relaxed text-[var(--muted)]">
|
||||||
|
{service.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Quick trust strip */}
|
||||||
|
<ul className="mt-8 grid max-w-xl grid-cols-2 gap-2">
|
||||||
|
{QUICK_TRUST.map((it) => (
|
||||||
|
<li
|
||||||
|
key={it}
|
||||||
|
className="flex items-center gap-2 text-sm text-[var(--foreground)]"
|
||||||
|
>
|
||||||
|
<CheckCircle2 className="size-4 shrink-0 text-[var(--sky-600)]" />
|
||||||
|
{it}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="mt-8 flex flex-col gap-3 sm:flex-row">
|
||||||
|
<Link
|
||||||
|
href="/iletisim"
|
||||||
|
className="inline-flex items-center justify-center gap-2 rounded-xl bg-[var(--navy)] px-6 py-3.5 text-sm font-semibold text-white shadow-lg shadow-[var(--navy)]/20 transition hover:-translate-y-0.5 hover:bg-[var(--navy-700)]"
|
||||||
|
>
|
||||||
|
Ücretsiz teklif al
|
||||||
|
<ArrowRight className="size-4" />
|
||||||
|
</Link>
|
||||||
|
<a
|
||||||
|
href={waHref}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="inline-flex items-center justify-center gap-2 rounded-xl bg-[#25d366] px-6 py-3.5 text-sm font-semibold text-white shadow-lg shadow-[#25d366]/20 transition hover:-translate-y-0.5 hover:bg-[#1ebe5d]"
|
||||||
|
>
|
||||||
|
<MessageCircle className="size-4" />
|
||||||
|
WhatsApp'tan yaz
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={`tel:${phoneRaw}`}
|
||||||
|
className="inline-flex items-center justify-center gap-2 rounded-xl border border-[var(--border)] bg-white px-6 py-3.5 text-sm font-semibold text-[var(--navy)] transition hover:border-[var(--navy)]"
|
||||||
|
>
|
||||||
|
<Phone className="size-4" />
|
||||||
|
{phone}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Right — hero card */}
|
||||||
|
<div className="relative">
|
||||||
|
{service.hero_image ? (
|
||||||
|
<div className="relative aspect-[4/5] overflow-hidden rounded-3xl shadow-2xl shadow-[var(--navy)]/10">
|
||||||
|
<Image
|
||||||
|
src={service.hero_image}
|
||||||
|
alt={service.title}
|
||||||
|
fill
|
||||||
|
sizes="(min-width: 1024px) 480px, 100vw"
|
||||||
|
className="object-cover"
|
||||||
|
priority
|
||||||
|
/>
|
||||||
|
{/* Floating badge */}
|
||||||
|
<div className="absolute bottom-4 left-4 right-4 rounded-xl bg-white/95 p-4 backdrop-blur shadow-lg">
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-wider text-[var(--sky-600)]">
|
||||||
|
Şimdi başla
|
||||||
|
</p>
|
||||||
|
<p className="mt-1 text-sm font-bold text-[var(--navy)]">
|
||||||
|
İlk tasarım taslağı ücretsiz
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<DecorativeServiceCard service={service} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function DecorativeServiceCard({ service }: { service: ServiceRow }) {
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
{/* Outer gradient frame */}
|
||||||
|
<div className="relative overflow-hidden rounded-3xl bg-gradient-to-br from-[var(--navy)] via-[var(--sky-600)] to-[var(--sky)] p-px shadow-2xl shadow-[var(--navy)]/20">
|
||||||
|
<div className="relative rounded-3xl bg-[#0f172a] p-8">
|
||||||
|
{/* Animated dots */}
|
||||||
|
<div
|
||||||
|
className="absolute inset-0 opacity-20"
|
||||||
|
style={{
|
||||||
|
backgroundImage:
|
||||||
|
"radial-gradient(circle at 1px 1px, white 1px, transparent 0)",
|
||||||
|
backgroundSize: "24px 24px",
|
||||||
|
}}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Glow */}
|
||||||
|
<div className="absolute -right-20 -top-20 size-64 rounded-full bg-[var(--sky)]/30 blur-3xl" aria-hidden />
|
||||||
|
|
||||||
|
{/* Card content */}
|
||||||
|
<div className="relative">
|
||||||
|
<div className="flex size-20 items-center justify-center rounded-2xl bg-white/10 backdrop-blur ring-1 ring-white/20">
|
||||||
|
<Icon name={service.icon} className="size-10 text-[var(--sky)]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8 space-y-2 text-white">
|
||||||
|
<p className="text-[11px] font-mono uppercase tracking-[0.2em] text-[var(--sky)]">
|
||||||
|
kovak.yazilim
|
||||||
|
</p>
|
||||||
|
<p className="text-2xl font-bold leading-tight">
|
||||||
|
{service.title}
|
||||||
|
</p>
|
||||||
|
<p className="text-sm leading-relaxed text-white/60">
|
||||||
|
Sektörünüze özel, profesyonel çözüm.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Bottom badges */}
|
||||||
|
<div className="mt-8 flex flex-wrap gap-2">
|
||||||
|
<span className="rounded-full bg-white/10 px-3 py-1 text-[10px] font-medium text-white/80 ring-1 ring-white/10">
|
||||||
|
⚡ Hızlı
|
||||||
|
</span>
|
||||||
|
<span className="rounded-full bg-white/10 px-3 py-1 text-[10px] font-medium text-white/80 ring-1 ring-white/10">
|
||||||
|
🛡️ Garantili
|
||||||
|
</span>
|
||||||
|
<span className="rounded-full bg-white/10 px-3 py-1 text-[10px] font-medium text-white/80 ring-1 ring-white/10">
|
||||||
|
📞 7/24 Destek
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Floating accent */}
|
||||||
|
<div className="absolute -right-4 -top-4 rounded-2xl bg-white p-4 shadow-xl ring-1 ring-[var(--border)]">
|
||||||
|
<p className="text-xs font-medium text-[var(--muted)]">Memnuniyet</p>
|
||||||
|
<p className="text-2xl font-bold text-[var(--navy)]">100%</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="absolute -bottom-4 -left-4 rounded-2xl bg-white p-4 shadow-xl ring-1 ring-[var(--border)]">
|
||||||
|
<p className="text-xs font-medium text-[var(--muted)]">Proje</p>
|
||||||
|
<p className="text-2xl font-bold text-[var(--navy)]">150+</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { ArrowRight, MessageCircle, Phone, ShieldCheck } from "lucide-react";
|
||||||
|
import { Icon } from "@/components/icon";
|
||||||
|
import {
|
||||||
|
getSiteSettings,
|
||||||
|
listServices,
|
||||||
|
} from "@/lib/data";
|
||||||
|
import { siteConfig } from "@/lib/site-config";
|
||||||
|
import { QuickLeadForm } from "@/components/quick-lead-form";
|
||||||
|
|
||||||
|
export async function ServiceSidebar({
|
||||||
|
currentSlug,
|
||||||
|
}: {
|
||||||
|
currentSlug: string;
|
||||||
|
}) {
|
||||||
|
const [settings, services] = await Promise.all([
|
||||||
|
getSiteSettings(),
|
||||||
|
listServices(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const otherServices = services.filter((s) => s.slug !== currentSlug).slice(0, 6);
|
||||||
|
|
||||||
|
const phoneRaw = settings?.contact_phone_raw ?? siteConfig.contact.phoneRaw;
|
||||||
|
const phone = settings?.contact_phone ?? siteConfig.contact.phone;
|
||||||
|
const wa = phoneRaw.replace(/[^\d]/g, "");
|
||||||
|
const waMessage = settings?.whatsapp_message ?? "";
|
||||||
|
const waHref = `https://wa.me/${wa}${
|
||||||
|
waMessage ? `?text=${encodeURIComponent(waMessage)}` : ""
|
||||||
|
}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside className="space-y-5 lg:sticky lg:top-24 lg:self-start">
|
||||||
|
{/* Quick lead form */}
|
||||||
|
<QuickLeadForm
|
||||||
|
title="Bu hizmet için teklif"
|
||||||
|
description="Adınızı ve telefonunuzu bırakın, 24 saat içinde sizi arayalım."
|
||||||
|
buttonLabel="Beni arayın"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* 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">Hızlı iletişim</h3>
|
||||||
|
<p className="mt-1 text-sm text-white/80">
|
||||||
|
Telefon veya WhatsApp ile dakikalar içinde konuşalım.
|
||||||
|
</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>
|
||||||
|
|
||||||
|
{/* Guarantee mini */}
|
||||||
|
<div className="rounded-2xl border border-[var(--sky)]/30 bg-[var(--sky-50)]/50 p-5">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<ShieldCheck className="size-5 text-[var(--sky-600)]" />
|
||||||
|
<h3 className="text-sm font-bold text-[var(--navy)]">
|
||||||
|
Risk almazsınız
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<ul className="mt-3 space-y-1.5 text-xs text-[var(--foreground)]">
|
||||||
|
<li className="flex gap-1.5">
|
||||||
|
<span className="text-[var(--sky-600)]">✓</span>
|
||||||
|
İlk tasarım taslağı ücretsiz
|
||||||
|
</li>
|
||||||
|
<li className="flex gap-1.5">
|
||||||
|
<span className="text-[var(--sky-600)]">✓</span>
|
||||||
|
1 yıl ücretsiz teknik destek
|
||||||
|
</li>
|
||||||
|
<li className="flex gap-1.5">
|
||||||
|
<span className="text-[var(--sky-600)]">✓</span>
|
||||||
|
Kaynak kodlar size ait
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Diğer hizmetler — full list */}
|
||||||
|
{otherServices.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)]">
|
||||||
|
Diğer hizmetler
|
||||||
|
</h3>
|
||||||
|
<Link
|
||||||
|
href="/hizmetler"
|
||||||
|
className="text-xs text-[var(--sky-600)] hover:text-[var(--navy)]"
|
||||||
|
>
|
||||||
|
Tümü →
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<ul className="mt-4 space-y-1">
|
||||||
|
{otherServices.map((s) => (
|
||||||
|
<li key={s.slug}>
|
||||||
|
<Link
|
||||||
|
href={`/hizmetler/${s.slug}`}
|
||||||
|
className="group flex items-center gap-3 rounded-lg px-2 py-2 text-sm transition hover:bg-[var(--navy-50)]"
|
||||||
|
>
|
||||||
|
<div className="flex size-8 shrink-0 items-center justify-center rounded-lg bg-[var(--navy-50)] text-[var(--navy)] transition group-hover:bg-gradient-to-br group-hover:from-[var(--sky)] group-hover:to-purple-500 group-hover:text-white">
|
||||||
|
<Icon name={s.icon} className="size-4" />
|
||||||
|
</div>
|
||||||
|
<span className="flex-1 font-medium text-[var(--foreground)] group-hover:text-[var(--navy)]">
|
||||||
|
{s.title}
|
||||||
|
</span>
|
||||||
|
<ArrowRight className="size-3 text-[var(--muted)] opacity-0 transition group-hover:opacity-100" />
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Site analizi lead magnet */}
|
||||||
|
<div className="rounded-2xl border border-dashed border-[var(--border)] bg-white p-5">
|
||||||
|
<p className="text-xs font-semibold uppercase tracking-wider text-[var(--sky-600)]">
|
||||||
|
Ücretsiz fırsat
|
||||||
|
</p>
|
||||||
|
<h3 className="mt-1 text-sm font-bold text-[var(--navy)]">
|
||||||
|
Site analizi raporu
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2 text-xs leading-relaxed text-[var(--muted)]">
|
||||||
|
Mevcut sitenizin SEO, hız ve dönüşüm performansını ücretsiz değerlendirelim.
|
||||||
|
</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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -18,30 +18,27 @@ export function ServicesGrid({ services }: { services: ServiceRow[] }) {
|
|||||||
: (siteConfig.fallbackServices as readonly ServiceLike[]).slice();
|
: (siteConfig.fallbackServices as readonly ServiceLike[]).slice();
|
||||||
|
|
||||||
return (
|
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) => (
|
{items.map((s) => (
|
||||||
<Link
|
<Link
|
||||||
key={s.slug}
|
key={s.slug}
|
||||||
href={`/hizmetler/${s.slug}`}
|
href={`/hizmetler/${s.slug}`}
|
||||||
id={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
|
<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" />
|
||||||
className="absolute -right-12 -top-12 size-32 rounded-full bg-[var(--sky-50)] opacity-0 transition group-hover:opacity-100"
|
|
||||||
aria-hidden
|
{/* 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">
|
||||||
<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" />
|
<Icon name={s.icon} className="size-6" />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="mt-5 text-lg font-semibold text-[var(--navy)] transition group-hover:text-[var(--sky-600)]">
|
|
||||||
|
<h3 className="mt-6 text-lg font-bold leading-tight text-[var(--navy)] transition-colors group-hover:text-[var(--sky-600)]">
|
||||||
{s.title}
|
{s.title}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="mt-2 text-sm leading-relaxed text-[var(--muted)]">
|
<p className="mt-3 text-sm leading-relaxed text-[var(--muted)]">
|
||||||
{s.description}
|
{s.description}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+54
-15
@@ -2,51 +2,90 @@ import Image from "next/image";
|
|||||||
import { LinkedinIcon } from "@/components/social-icons";
|
import { LinkedinIcon } from "@/components/social-icons";
|
||||||
import type { TeamMemberRow } from "@/lib/types";
|
import type { TeamMemberRow } from "@/lib/types";
|
||||||
|
|
||||||
|
const GRADIENTS = [
|
||||||
|
"from-[var(--navy)] to-blue-400",
|
||||||
|
"from-blue-400 to-cyan-400",
|
||||||
|
"from-violet-500 to-purple-500",
|
||||||
|
"from-sky-500 to-emerald-400",
|
||||||
|
];
|
||||||
|
|
||||||
|
function initials(name: string): string {
|
||||||
|
return name
|
||||||
|
.split(" ")
|
||||||
|
.map((s) => s[0])
|
||||||
|
.filter(Boolean)
|
||||||
|
.slice(0, 2)
|
||||||
|
.join("")
|
||||||
|
.toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
export function TeamGrid({ members }: { members: TeamMemberRow[] }) {
|
export function TeamGrid({ members }: { members: TeamMemberRow[] }) {
|
||||||
if (members.length === 0) return null;
|
if (members.length === 0) return null;
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="mx-auto grid max-w-3xl gap-8 sm:grid-cols-2">
|
||||||
{members.map((m) => (
|
{members.map((m, i) => (
|
||||||
<article
|
<article
|
||||||
key={m.$id}
|
key={m.$id}
|
||||||
className="overflow-hidden rounded-2xl border border-[var(--border)] bg-white transition hover:shadow-md"
|
className="group rounded-3xl border border-[var(--border)] bg-white p-8 transition-all duration-300 hover:-translate-y-1 hover:shadow-xl hover:shadow-[var(--navy)]/10"
|
||||||
>
|
>
|
||||||
<div className="relative aspect-square overflow-hidden bg-gradient-to-br from-[var(--sky-50)] to-[var(--navy-50)]">
|
{/* Avatar — Foto veya gradient initial */}
|
||||||
|
<div className="mx-auto mb-6 size-20">
|
||||||
{m.photo_url ? (
|
{m.photo_url ? (
|
||||||
|
<div className="relative size-20 overflow-hidden rounded-2xl">
|
||||||
<Image
|
<Image
|
||||||
src={m.photo_url}
|
src={m.photo_url}
|
||||||
alt={m.name}
|
alt={m.name}
|
||||||
fill
|
fill
|
||||||
sizes="(min-width: 1024px) 33vw, (min-width: 768px) 50vw, 100vw"
|
sizes="80px"
|
||||||
className="object-cover"
|
className="object-cover"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex h-full items-center justify-center text-6xl font-bold text-[var(--navy)]/30">
|
<div
|
||||||
{m.name.charAt(0)}
|
className={`flex size-20 items-center justify-center rounded-2xl bg-gradient-to-br ${
|
||||||
|
GRADIENTS[i % GRADIENTS.length]
|
||||||
|
} text-2xl font-bold text-white shadow-lg`}
|
||||||
|
>
|
||||||
|
{initials(m.name)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="p-5">
|
|
||||||
<h3 className="text-base font-semibold text-[var(--navy)]">
|
<div className="text-center">
|
||||||
{m.name}
|
<h3 className="text-xl font-bold text-[var(--navy)]">{m.name}</h3>
|
||||||
</h3>
|
|
||||||
{m.role && (
|
{m.role && (
|
||||||
<p className="text-xs text-[var(--sky-600)]">{m.role}</p>
|
<div className="mt-1 text-sm font-medium text-[var(--sky-600)]">
|
||||||
|
{m.role}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{m.bio && (
|
{m.bio && (
|
||||||
<p className="mt-3 text-sm leading-relaxed text-[var(--muted)]">
|
<p className="mt-4 text-sm leading-relaxed text-[var(--muted)]">
|
||||||
{m.bio}
|
{m.bio}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{m.skills && m.skills.length > 0 && (
|
||||||
|
<div className="mt-5 flex flex-wrap items-center justify-center gap-1.5">
|
||||||
|
{m.skills.map((s) => (
|
||||||
|
<span
|
||||||
|
key={s}
|
||||||
|
className="rounded-full bg-[var(--sky-50)] px-3 py-1 text-xs font-medium text-[var(--sky-600)]"
|
||||||
|
>
|
||||||
|
{s}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{m.linkedin_url && (
|
{m.linkedin_url && (
|
||||||
<a
|
<a
|
||||||
href={m.linkedin_url}
|
href={m.linkedin_url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="mt-3 inline-flex items-center gap-1 text-xs font-medium text-[var(--sky-600)] hover:text-[var(--navy)]"
|
className="mt-5 inline-flex items-center gap-1.5 rounded-full border border-[var(--border)] bg-white px-3 py-1.5 text-xs font-medium text-[var(--muted)] transition hover:border-[var(--sky)] hover:text-[var(--navy)]"
|
||||||
>
|
>
|
||||||
<LinkedinIcon className="size-3.5" />
|
<LinkedinIcon className="size-3.5" />
|
||||||
LinkedIn'de bağlan
|
LinkedIn
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -527,6 +527,7 @@ export async function saveTeamMember(formData: FormData) {
|
|||||||
photo_url: str(formData.get("photo_url")),
|
photo_url: str(formData.get("photo_url")),
|
||||||
linkedin_url: str(formData.get("linkedin_url")),
|
linkedin_url: str(formData.get("linkedin_url")),
|
||||||
order: num(formData.get("order")) ?? 0,
|
order: num(formData.get("order")) ?? 0,
|
||||||
|
skills: strArr(formData.get("skills")),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (id) {
|
if (id) {
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { marked } from "marked";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Akıllı içerik render — RichEditor HTML üretir, eski içerikler markdown olabilir.
|
||||||
|
* - HTML işareti (`<p>`, `<h1>` vs ile başlıyor) varsa direkt döner
|
||||||
|
* - Aksi halde markdown olarak parse eder
|
||||||
|
*/
|
||||||
|
export function renderContent(content?: string | null): string {
|
||||||
|
if (!content) return "";
|
||||||
|
const trimmed = content.trim();
|
||||||
|
if (!trimmed) return "";
|
||||||
|
// HTML: ilk karakter '<' ise ve içinde HTML tag varsa
|
||||||
|
if (trimmed.startsWith("<") && /<\w+[^>]*>/.test(trimmed)) {
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
return marked.parse(trimmed, { async: false }) as string;
|
||||||
|
}
|
||||||
@@ -157,6 +157,7 @@ export interface TeamMemberRow extends AwRow {
|
|||||||
photo_url?: string | null;
|
photo_url?: string | null;
|
||||||
linkedin_url?: string | null;
|
linkedin_url?: string | null;
|
||||||
order?: number | null;
|
order?: number | null;
|
||||||
|
skills?: string[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IndustryRow extends AwRow {
|
export interface IndustryRow extends AwRow {
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ const nextConfig: NextConfig = {
|
|||||||
{ protocol: "https", hostname: "db.kovaksoft.com" },
|
{ protocol: "https", hostname: "db.kovaksoft.com" },
|
||||||
{ protocol: "https", hostname: "kovakyazilim.com" },
|
{ protocol: "https", hostname: "kovakyazilim.com" },
|
||||||
{ protocol: "https", hostname: "**.kovaksoft.com" },
|
{ protocol: "https", hostname: "**.kovaksoft.com" },
|
||||||
|
{ protocol: "https", hostname: "images.pexels.com" },
|
||||||
|
{ protocol: "https", hostname: "images.unsplash.com" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Generated
+679
-3
@@ -8,6 +8,13 @@
|
|||||||
"name": "kovak-yazilim",
|
"name": "kovak-yazilim",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tiptap/extension-image": "^3.23.5",
|
||||||
|
"@tiptap/extension-link": "^3.23.5",
|
||||||
|
"@tiptap/extension-placeholder": "^3.23.5",
|
||||||
|
"@tiptap/extension-text-align": "^3.23.5",
|
||||||
|
"@tiptap/extension-underline": "^3.23.5",
|
||||||
|
"@tiptap/react": "^3.23.5",
|
||||||
|
"@tiptap/starter-kit": "^3.23.5",
|
||||||
"lucide-react": "^1.16.0",
|
"lucide-react": "^1.16.0",
|
||||||
"marked": "^18.0.4",
|
"marked": "^18.0.4",
|
||||||
"next": "16.2.6",
|
"next": "16.2.6",
|
||||||
@@ -46,6 +53,34 @@
|
|||||||
"tslib": "^2.4.0"
|
"tslib": "^2.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@floating-ui/core": {
|
||||||
|
"version": "1.7.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.5.tgz",
|
||||||
|
"integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/utils": "^0.2.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/dom": {
|
||||||
|
"version": "1.7.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.6.tgz",
|
||||||
|
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.7.5",
|
||||||
|
"@floating-ui/utils": "^0.2.11"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/utils": {
|
||||||
|
"version": "0.2.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.11.tgz",
|
||||||
|
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
"node_modules/@img/colour": {
|
"node_modules/@img/colour": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
|
||||||
@@ -1048,6 +1083,473 @@
|
|||||||
"tailwindcss": "4.3.0"
|
"tailwindcss": "4.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tiptap/core": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-657Xqcgf1IYWLkAmRDJKNSGdoS1AHJEgK6zHWHFJERQGIqHnwC7Fz7nvWs/NQhQVBkclQd0ERRdTCZ3XwRc1+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/pm": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-blockquote": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-blockquote/-/extension-blockquote-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-PBQRoGfSWfIY7HmGbS5PTHEBQl5nKbild5J5phPLFF+O3aOBQ0d49AC9cxbaou/6FRCtq6g4Uqse9rRTKJRM0w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bold": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bold/-/extension-bold-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-DZsDCCf53fA9HmsFzfUHl5jLOwDYf+XzfP+QJjJ4cK23SsxDirameTjgnwi4l1EgEPLWunMZQjU+wHmh7vvX6Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bubble-menu": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bubble-menu/-/extension-bubble-menu-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-otcGwyVO6OfxdDPnbooZxYGrb+6q5WYmS+g2V+XGGNRn5oJgyY5pW0dqELIUJ66dosIIXXPyw2XqBDpMMY2kyQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.23.5",
|
||||||
|
"@tiptap/pm": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-bullet-list": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-bullet-list/-/extension-bullet-list-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-o0bzZbFvOPhPX6+RAhIFPKMIN3jIenY6Ib3FJ6ZqxTdVcjuV2mIXUmJU0uV2BwKtz73GmKSRKRKia6KJ0ml8qA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-code": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-code/-/extension-code-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-NOJUD2Z0hrtBWnovXiiH1XtOjEQePOfIG3bNJgXSs1bWxPVhqp6KjVd8mUJNra974hxbml3tC97sL9QqjpAWFg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-code-block": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-code-block/-/extension-code-block-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-P2XH8WPM4UahavcWoQgAwNAKQCbF/JWi6ZqgsQmVBfAqQ3mf8gMxB7HnciMq1DlyI9EfjXoJH11yUqldF/6AaQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.23.5",
|
||||||
|
"@tiptap/pm": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-document": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-document/-/extension-document-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-Y7uPjEM1xIK4Spcdk/kp/vZ/Az3cEaglTCk6uHrWvNFVglEoGehNb6IQbQFZW0wjE19YoMIiLBLtG6V9dqrpBw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-dropcursor": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-dropcursor/-/extension-dropcursor-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-l72R798Q69D6f89Vp9xreoRnPcpK0LHPKLZIc6pvqBC2iOjx5wLKtW0uP1uqVWdQtvF5AUYBRNIGAQ5Gel9XEg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extensions": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-floating-menu": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-floating-menu/-/extension-floating-menu-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-kP0bZKH/lxNogfvoIy/YJZ5gkty0OwqFVtQUwoc85vXYUfvy5Jh1VdO053tCE1iDzmvOITUpcb+MdWryP8dBxA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0",
|
||||||
|
"@tiptap/core": "3.23.5",
|
||||||
|
"@tiptap/pm": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-gapcursor": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-gapcursor/-/extension-gapcursor-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-x9XlYG26TowX0Ly1w0ZV2D8qliyQy9fTmMY4suI6B/6o6m/sXHGTAJMmJqwP66sZKF6cMLU3HECumhtyQxPT2g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extensions": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-hard-break": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-j/BDBMOA1mA+RhCx622SRPBhpp2XWNFYz9asbg8T3yk8v9WI3Vjo6IDlfTp6fwsR2LGE7Pek3R0xDAjW6yVG3g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-heading": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-heading/-/extension-heading-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-tFI+iYk34geacVOGqYgyoC8siQjdGn605XaYSZcGRFF8NY+HrGlLkQi2QRRCeLaUhxoctONmWc8USn3H5U7wLQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-horizontal-rule": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-9XkRYc4XE0stERZB3y8bsJd32Jw9UZfMwZXo1GLNYRHFr7dmhSGUj0IzgofqOVmLDcOMW6XcCk54TBYw6BCrWA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.23.5",
|
||||||
|
"@tiptap/pm": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-image": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-image/-/extension-image-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-v6u9zbJSKLjml6DDn1/1WOOIzVxz3K5Idl1EgUl+IpJH7kR1HLRJ3TaSgF7z2RRQmqyHlmtdCzdaKoe0jCIyqQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-italic": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-italic/-/extension-italic-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-XjRSPr6j4mz+8O5j5KNfxVb+1fGNt0wr+js6MLxxGdU7M+PoDPdVY6fARbmBazv4ERlZ5PNS9m35Vo5xDjDfrg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-link": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-link/-/extension-link-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-FEI58NAPnauBbs4nw1dkgRyEhcWnure0vIlStfQoQGXxj3xSRvxKH2lOkz54fGzuzRJAoudyLU65HW6D7kc+8Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"linkifyjs": "^4.3.3"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.23.5",
|
||||||
|
"@tiptap/pm": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-nzZXpVwnyKwTj4TVyPyu1bCUFjJCsaXnhAthmvJDnX3RBtemNG9Ka07xGR2NIspzumSbQSMFtDxjmxv3W5dEtg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.23.5",
|
||||||
|
"@tiptap/pm": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list-item": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-item/-/extension-list-item-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-l7Hb4rfNIkO6JrNJYkdXap6QYXCz4XeeFmI1bfQgEiwPGs+RAn/+0cOdg7q+6MmtZFac5uSXV0PftPk6A0GsEA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-list-keymap": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list-keymap/-/extension-list-keymap-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-Hz8jRA51VSiHezEkwqwaMYbTEYcR/5Aq3UgCgDlNPlE6k1OZrvRtV/4s3AOO0RRgzyVLKv7yv7KuOJN/OLGErw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-ordered-list": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-ordered-list/-/extension-ordered-list-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-qQeU71ij0cAAD9bbGqot5T5bpR3dysgQ+W67quRs6VDyusU89EYaJHKn/qWU6a1XOEQ4sL+5GNw52FYQVHUxbA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extension-list": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-paragraph": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-paragraph/-/extension-paragraph-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-LtgMcR1rvWnZDtphFJ/LBltlC0+6HGA07k7vhy+U7P/zIg/V3Fb4RD6YDuAo0cPfBsLm8p1WYJV92WpAsGgtlg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-placeholder": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-placeholder/-/extension-placeholder-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-B2snUujc6fb/16p8jSQCN4+mto7RlHKLm8quBTUWXksY8D82u/cxjUdmRQ7ueq7vsbRsA+WoJTrKEjJ8RQOpjw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/extensions": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-strike": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-strike/-/extension-strike-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-PMB9lpQGOJGuRTIS9rBw8UZtHQwmsiJbWKjcBr5z20MluaJQ3ZCHFhDYG6ncIDRz+0ny4ZvoJ7cKGpI+NTvXMA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-text": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-text/-/extension-text-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-GLa+AaA2NC5XYRZad/Qq/oH5Pa95s+uA17J7+RCkF8j1RNREUBkYQ5CD5MT8kT+D3DHgU8MRyYdTd28I46HBDQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-text-align": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-eOeXrbpPWc6gfXli2aXYg9t61HhkvEkdxQgpEpZPFhrT4pPQcIqTlihswByC+cPb8B5ynrc/iamiY9cRSU1qvw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extension-underline": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-fyxthzE6CNCi9a9OVAwXs1sSyJ7jlrzT3aP2KhYLQCsJABHaPJgJA7k52/CRuKqCW3WbxU1ULH9LGuGtBbhEyw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/extensions": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-ROcdNPV+buzldEFKvD3o29P7H7zpAf2lnLfndO2LHSToWyHw4hlzVPCeAU8uAvhl/jyfeUoFLrBwxphMX/KG6A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.23.5",
|
||||||
|
"@tiptap/pm": "3.23.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/pm": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-9tgLdpTvNN0/fLP4RcNzbyQ0qjg9J2ahaFbQzgV5uvd+QMy8Xkg2IqKKnOoJJUAV3FDjGq3Yx0WrV2BGro9pfw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-changeset": "^2.3.0",
|
||||||
|
"prosemirror-commands": "^1.6.2",
|
||||||
|
"prosemirror-dropcursor": "^1.8.1",
|
||||||
|
"prosemirror-gapcursor": "^1.3.2",
|
||||||
|
"prosemirror-history": "^1.4.1",
|
||||||
|
"prosemirror-keymap": "^1.2.2",
|
||||||
|
"prosemirror-model": "^1.24.1",
|
||||||
|
"prosemirror-schema-list": "^1.5.0",
|
||||||
|
"prosemirror-state": "^1.4.3",
|
||||||
|
"prosemirror-tables": "^1.6.4",
|
||||||
|
"prosemirror-transform": "^1.10.2",
|
||||||
|
"prosemirror-view": "^1.38.1"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/react": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/react/-/react-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-aEdKfJxoa6tCEV4FrnBqMQoUPwGcTWLaDzmP4fL1gR7E40rYDTiYNKoF1Ob+UimUpguAP6Emv1WlJa5oyI8FSw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
|
"fast-equals": "^5.3.3",
|
||||||
|
"use-sync-external-store": "^1.4.0"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@tiptap/extension-bubble-menu": "^3.23.5",
|
||||||
|
"@tiptap/extension-floating-menu": "^3.23.5"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@tiptap/core": "3.23.5",
|
||||||
|
"@tiptap/pm": "3.23.5",
|
||||||
|
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"@types/react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@tiptap/starter-kit": {
|
||||||
|
"version": "3.23.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tiptap/starter-kit/-/starter-kit-3.23.5.tgz",
|
||||||
|
"integrity": "sha512-ac0edQ1a1nYkNAzOgdqIBKGdrOlNQpPP9wGAG3Q9EgTq4+C4/EftJZZJmUn3KzaSOUv4cLEDo0z0jurJvZPkaw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@tiptap/core": "^3.23.5",
|
||||||
|
"@tiptap/extension-blockquote": "^3.23.5",
|
||||||
|
"@tiptap/extension-bold": "^3.23.5",
|
||||||
|
"@tiptap/extension-bullet-list": "^3.23.5",
|
||||||
|
"@tiptap/extension-code": "^3.23.5",
|
||||||
|
"@tiptap/extension-code-block": "^3.23.5",
|
||||||
|
"@tiptap/extension-document": "^3.23.5",
|
||||||
|
"@tiptap/extension-dropcursor": "^3.23.5",
|
||||||
|
"@tiptap/extension-gapcursor": "^3.23.5",
|
||||||
|
"@tiptap/extension-hard-break": "^3.23.5",
|
||||||
|
"@tiptap/extension-heading": "^3.23.5",
|
||||||
|
"@tiptap/extension-horizontal-rule": "^3.23.5",
|
||||||
|
"@tiptap/extension-italic": "^3.23.5",
|
||||||
|
"@tiptap/extension-link": "^3.23.5",
|
||||||
|
"@tiptap/extension-list": "^3.23.5",
|
||||||
|
"@tiptap/extension-list-item": "^3.23.5",
|
||||||
|
"@tiptap/extension-list-keymap": "^3.23.5",
|
||||||
|
"@tiptap/extension-ordered-list": "^3.23.5",
|
||||||
|
"@tiptap/extension-paragraph": "^3.23.5",
|
||||||
|
"@tiptap/extension-strike": "^3.23.5",
|
||||||
|
"@tiptap/extension-text": "^3.23.5",
|
||||||
|
"@tiptap/extension-underline": "^3.23.5",
|
||||||
|
"@tiptap/extensions": "^3.23.5",
|
||||||
|
"@tiptap/pm": "^3.23.5"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.19.41",
|
"version": "20.19.41",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz",
|
||||||
@@ -1062,7 +1564,6 @@
|
|||||||
"version": "19.2.15",
|
"version": "19.2.15",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz",
|
||||||
"integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==",
|
"integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
@@ -1072,12 +1573,17 @@
|
|||||||
"version": "19.2.3",
|
"version": "19.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
||||||
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@types/react": "^19.2.0"
|
"@types/react": "^19.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/use-sync-external-store": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/baseline-browser-mapping": {
|
"node_modules/baseline-browser-mapping": {
|
||||||
"version": "2.10.31",
|
"version": "2.10.31",
|
||||||
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz",
|
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.31.tgz",
|
||||||
@@ -1120,7 +1626,6 @@
|
|||||||
"version": "3.2.3",
|
"version": "3.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
@@ -1147,6 +1652,15 @@
|
|||||||
"node": ">=10.13.0"
|
"node": ">=10.13.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-equals": {
|
||||||
|
"version": "5.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz",
|
||||||
|
"integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/graceful-fs": {
|
"node_modules/graceful-fs": {
|
||||||
"version": "4.2.11",
|
"version": "4.2.11",
|
||||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
|
||||||
@@ -1437,6 +1951,12 @@
|
|||||||
"url": "https://opencollective.com/parcel"
|
"url": "https://opencollective.com/parcel"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/linkifyjs": {
|
||||||
|
"version": "4.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.3.3.tgz",
|
||||||
|
"integrity": "sha512-P8aEP5U/D1/IlTY2OeYsErdwh9bGuLE30NcXtKEjgdHcahveQoQwM2yZNsioQHsWFz0P7KKudisbrzCgR0sDHg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lucide-react": {
|
"node_modules/lucide-react": {
|
||||||
"version": "1.16.0",
|
"version": "1.16.0",
|
||||||
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.16.0.tgz",
|
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.16.0.tgz",
|
||||||
@@ -1567,6 +2087,12 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/orderedmap": {
|
||||||
|
"version": "2.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/orderedmap/-/orderedmap-2.1.1.tgz",
|
||||||
|
"integrity": "sha512-TvAWxi0nDe1j/rtMcWcIj94+Ffe6n7zhow33h40SKxmsmozs6dz/e+EajymfoFcHd7sxNn8yHM8839uixMOV6g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -1602,6 +2128,135 @@
|
|||||||
"node": "^10 || ^12 || >=14"
|
"node": "^10 || ^12 || >=14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/prosemirror-changeset": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-changeset/-/prosemirror-changeset-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-96WBLhOaYhJ+kPhLg3uW359Tz6I/MfcrQfL4EGv4SrcqKEMC1gmoGrXHecPE8eOwTVCJ4IwgfzM8fFad25wNfw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-transform": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-commands": {
|
||||||
|
"version": "1.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-commands/-/prosemirror-commands-1.7.1.tgz",
|
||||||
|
"integrity": "sha512-rT7qZnQtx5c0/y/KlYaGvtG411S97UaL6gdp6RIZ23DLHanMYLyfGBV5DtSnZdthQql7W+lEVbpSfwtO8T+L2w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.10.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-dropcursor": {
|
||||||
|
"version": "1.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-dropcursor/-/prosemirror-dropcursor-1.8.2.tgz",
|
||||||
|
"integrity": "sha512-CCk6Gyx9+Tt2sbYk5NK0nB1ukHi2ryaRgadV/LvyNuO3ena1payM2z6Cg0vO1ebK8cxbzo41ku2DE5Axj1Zuiw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.1.0",
|
||||||
|
"prosemirror-view": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-gapcursor": {
|
||||||
|
"version": "1.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-gapcursor/-/prosemirror-gapcursor-1.4.1.tgz",
|
||||||
|
"integrity": "sha512-pMdYaEnjNMSwl11yjEGtgTmLkR08m/Vl+Jj443167p9eB3HVQKhYCc4gmHVDsLPODfZfjr/MmirsdyZziXbQKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-keymap": "^1.0.0",
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-view": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-history": {
|
||||||
|
"version": "1.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-history/-/prosemirror-history-1.5.0.tgz",
|
||||||
|
"integrity": "sha512-zlzTiH01eKA55UAf1MEjtssJeHnGxO0j4K4Dpx+gnmX9n+SHNlDqI2oO1Kv1iPN5B1dm5fsljCfqKF9nFL6HRg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.2.2",
|
||||||
|
"prosemirror-transform": "^1.0.0",
|
||||||
|
"prosemirror-view": "^1.31.0",
|
||||||
|
"rope-sequence": "^1.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-keymap": {
|
||||||
|
"version": "1.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-keymap/-/prosemirror-keymap-1.2.3.tgz",
|
||||||
|
"integrity": "sha512-4HucRlpiLd1IPQQXNqeo81BGtkY8Ai5smHhKW9jjPKRc2wQIxksg7Hl1tTI2IfT2B/LgX6bfYvXxEpJl7aKYKw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"w3c-keyname": "^2.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-model": {
|
||||||
|
"version": "1.25.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.7.tgz",
|
||||||
|
"integrity": "sha512-A79aN8QEFUwI6cax8Yq4Rpcx1TJZ3Kagn+ii7qLo4/V8H3mMiHrhFyhTyHHvpSnOgMPpWiDGSwM3etwrxE50ug==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"orderedmap": "^2.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-schema-list": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-schema-list/-/prosemirror-schema-list-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-927lFx/uwyQaGwJxLWCZRkjXG0p48KpMj6ueoYiu4JX05GGuGcgzAy62dfiV8eFZftgyBUvLx76RsMe20fJl+Q==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.7.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-state": {
|
||||||
|
"version": "1.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
|
||||||
|
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.0.0",
|
||||||
|
"prosemirror-view": "^1.27.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-tables": {
|
||||||
|
"version": "1.8.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-tables/-/prosemirror-tables-1.8.5.tgz",
|
||||||
|
"integrity": "sha512-V/0cDCsHKHe/tfWkeCmthNUcEp1IVO3p6vwN8XtwE9PZQLAZJigbw3QoraAdfJPir4NKJtNvOB8oYGKRl+t0Dw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-keymap": "^1.2.3",
|
||||||
|
"prosemirror-model": "^1.25.4",
|
||||||
|
"prosemirror-state": "^1.4.4",
|
||||||
|
"prosemirror-transform": "^1.10.5",
|
||||||
|
"prosemirror-view": "^1.41.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-transform": {
|
||||||
|
"version": "1.12.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-transform/-/prosemirror-transform-1.12.0.tgz",
|
||||||
|
"integrity": "sha512-GxboyN4AMIsoHNtz5uf2r2Ru551i5hWeCMD6E2Ib4Eogqoub0NflniaBPVQ4MrGE5yZ8JV9tUHg9qcZTTrcN4w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.21.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/prosemirror-view": {
|
||||||
|
"version": "1.41.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.8.tgz",
|
||||||
|
"integrity": "sha512-TnKDdohEatgyZNGCDWIdccOHXhYloJwbwU+phw/a23KBvJIR9lWQWW7WHHK3vBdOLDNuF7TaX98GObUZOWkOnA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prosemirror-model": "^1.20.0",
|
||||||
|
"prosemirror-state": "^1.0.0",
|
||||||
|
"prosemirror-transform": "^1.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react": {
|
"node_modules/react": {
|
||||||
"version": "19.2.4",
|
"version": "19.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||||
@@ -1623,6 +2278,12 @@
|
|||||||
"react": "^19.2.4"
|
"react": "^19.2.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/rope-sequence": {
|
||||||
|
"version": "1.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/rope-sequence/-/rope-sequence-1.3.4.tgz",
|
||||||
|
"integrity": "sha512-UT5EDe2cu2E/6O4igUr5PSFs23nvvukicWHx6GnOPlHAiiYbzNuCRQCuiUdHJQcqKalLKlrYJnjY0ySGsXNQXQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/scheduler": {
|
"node_modules/scheduler": {
|
||||||
"version": "0.27.0",
|
"version": "0.27.0",
|
||||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
||||||
@@ -1766,6 +2427,21 @@
|
|||||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/use-sync-external-store": {
|
||||||
|
"version": "1.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
|
||||||
|
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/w3c-keyname": {
|
||||||
|
"version": "2.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||||
|
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||||
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,13 @@
|
|||||||
"start": "next start"
|
"start": "next start"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@tiptap/extension-image": "^3.23.5",
|
||||||
|
"@tiptap/extension-link": "^3.23.5",
|
||||||
|
"@tiptap/extension-placeholder": "^3.23.5",
|
||||||
|
"@tiptap/extension-text-align": "^3.23.5",
|
||||||
|
"@tiptap/extension-underline": "^3.23.5",
|
||||||
|
"@tiptap/react": "^3.23.5",
|
||||||
|
"@tiptap/starter-kit": "^3.23.5",
|
||||||
"lucide-react": "^1.16.0",
|
"lucide-react": "^1.16.0",
|
||||||
"marked": "^18.0.4",
|
"marked": "^18.0.4",
|
||||||
"next": "16.2.6",
|
"next": "16.2.6",
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
// Google Consent Mode v2 — set defaults to "denied" before any analytics tag loads.
|
||||||
|
// CookieBanner sonradan gtag('consent','update',…) ile günceller.
|
||||||
|
window.dataLayer = window.dataLayer || [];
|
||||||
|
function gtag() { dataLayer.push(arguments); }
|
||||||
|
window.gtag = gtag;
|
||||||
|
gtag('consent', 'default', {
|
||||||
|
ad_storage: 'denied',
|
||||||
|
ad_user_data: 'denied',
|
||||||
|
ad_personalization: 'denied',
|
||||||
|
analytics_storage: 'denied',
|
||||||
|
functionality_storage: 'granted',
|
||||||
|
personalization_storage: 'denied',
|
||||||
|
security_storage: 'granted',
|
||||||
|
wait_for_update: 500,
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user