feat(ui): Arc bileşenleri, landing bölümleri ve bildirim yığını

Sonner/vaul yerine kendi bildirim yığını ve alt sayfa; çip grubu, kart
destesi, kaydırma eylemi, yazı dönüşümü, bölme tutamacı; landing'e program
duvarı, süzülen haplar ve veri akışı; yaylar tek dosyada (lib/hareket.ts).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-07 17:12:53 +03:00
parent bc920f6111
commit 3c995402c6
38 changed files with 5474 additions and 697 deletions

View File

@@ -1,16 +1,12 @@
import {
ArrowRight,
ArrowUpRight,
BarChart3,
Coins,
Compass,
FileText,
ListChecks,
MessageCircleQuestion,
RefreshCcw,
ShieldCheck,
TrendingUp,
Wallet,
} from "lucide-react";
import Link from "next/link";
import { Suspense } from "react";
@@ -22,9 +18,13 @@ import { Parallax } from "@/components/parallax";
import { Reveal } from "@/features/pazarlama/components/reveal";
import { SosyalKanitBandi } from "@/features/pazarlama/components/sosyal-kanit";
import { TurkeyPixelMap } from "@/features/pazarlama/components/turkey-pixel-map";
import { SuzulenHaplar } from "@/features/pazarlama/components/suzulen-haplar";
import { VeriAkisi } from "@/features/pazarlama/components/veri-akisi";
import { katalogHaplari } from "@/features/pazarlama/landing-verisi";
import {
PixelCorner,
PixelDivider,
PixelRakam,
PixelSahne,
SectionEyebrow,
} from "@/components/pixel-decor";
import { PikselCtaYuzeyi } from "@/components/piksel-cta-yuzeyi";
@@ -41,12 +41,6 @@ import {
AccordionTrigger,
} from "@/components/ui/accordion";
import { Button } from "@/components/ui/button";
import {
Card,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Table,
TableBody,
@@ -58,11 +52,9 @@ import {
import type { Metadata } from "next";
import {
JsonLd,
KATALOG_VERI_TARIHI,
faqPageJsonLd,
softwareApplicationJsonLd,
} from "@/lib/seo";
import { KATALOG_TARIH_METNI } from "@/features/katalog/components/veri-kaynagi";
import { SiteFooter } from "@/components/site-footer";
export const metadata: Metadata = {
@@ -76,20 +68,55 @@ const TOPUP_FIYATI = (URUNLER.topup.amountKurus / 100).toLocaleString("tr-TR", {
maximumFractionDigits: 0,
});
// Sorun kartlarının piksel sahneleri (DESIGN.md §4.31): hero haritasıyla aynı
// ızgara dili — `#` doku, `-` silik, `o` turuncu vurgu. Her biri kartın
// söylediğini çizer: binlerce programın içinde tam 24 seçim; her yıl başka
// boyda çubuklar ve yalnız sonuncusu işaretli; basamak basamak tırmanan fiyat.
const SAHNE_IHTIMAL = [
"o#o#######o##o##o#######",
"####o###o#########o###o#",
"######o####o###o########",
"#o##o###################",
"######o##o####o####o##o#",
"#o##########o####o######",
".###..o#......####.#..o.",
"..#...###...##......#...",
];
const SAHNE_YILLAR = [
"........................",
".................##.....",
".........##......##.....",
".##......##......##.....",
".##......##..##..##..oo.",
".##..##..##..##..##..##.",
".##..##..##..##..##..##.",
".##..##..##..##..##..##.",
];
const SAHNE_FIYAT = [
".....................ooo",
"..................###---",
"...............###------",
"............###---------",
".........###------------",
"......###---------------",
"...###------------------",
"###---------------------",
];
const problems = [
{
icon: ListChecks,
sahne: SAHNE_IHTIMAL,
title: "24 tercih, binlerce ihtimal",
description: `${PROGRAM_SAYISI_CUMLE} lisans ve önlisans programı arasından sana uyan 24 tanesini doğru sırayla dizmek, tablolarla boğuşarak günler alıyor.`,
},
{
icon: TrendingUp,
sahne: SAHNE_YILLAR,
title: "Geçen yılın puanı yetmiyor",
description:
"Taban sıralamaları her yıl kayıyor; tek yıla bakmak riski gizler.",
},
{
icon: Wallet,
sahne: SAHNE_FIYAT,
title: "Danışman lüks haline geldi",
description:
"Armut'ta tercih danışmanlığı ilanları ortalama 1.000–3.000 TL (Eylül 2026). Ücretsiz robotların çoğu ise yalnızca liste filtreliyor; listeyi kurup gerekçesini yazmıyor.",
@@ -98,26 +125,34 @@ const problems = [
const steps = [
{
icon: Compass,
step: "1",
step: 1,
title: "Sıralamanı ve hedeflerini anlat",
description:
"YKS başarı sıralamanı gir; sıralamana uygun ilgi alanı, şehir ve üniversite tercihlerini birkaç adımda işaretle.",
},
{
icon: BarChart3,
step: "2",
step: 2,
title: `Son ${VERI_YIL_SAYISI} yılın resmî verisi taransın`,
description:
"YÖK Atlas'ın taban sıralaması trendleri, kontenjan değişimleri ve doluluk oranları sıralamana göre elensin; yapay zekâ yalnızca bu havuzdan seçer, dışına çıkamaz.",
},
{
icon: ShieldCheck,
step: "3",
step: 3,
title: "Dengeli 24'lük listeni al",
description:
"Hayal, dengeli ve güvenli dilimlerine dağıtılmış, her satırı gerekçeli tercih listeni indir; aklına takılanı listen üzerinden yapay zekâya sor.",
},
] as const;
// "Nasıl çalışır"da akışın yanında duran üç sayı (DESIGN.md §4.28).
const veriSayilari = [
{ value: PROGRAM_SAYISI_METNI, label: "Taranan lisans ve önlisans programı" },
{
value: String(VERI_YIL_SAYISI),
birim: "yıl",
label: "Taban sıralaması trend verisi",
},
{ value: "%100", label: "Resmî YÖK Atlas kaynağı" },
];
const faqs = [
@@ -217,6 +252,10 @@ const paketOzellikleri = [
] as const;
export default function Home() {
// Katalog hapları gerçek veriden (landing-verisi.ts); veri okunamazsa
// bölüm çizilmez.
const haplar = katalogHaplari();
return (
<div
id="top"
@@ -257,27 +296,6 @@ export default function Home() {
>
<HeroForm />
</div>
<div className="mt-3 inline-flex items-center gap-2.5 rounded-lg border border-slate-200 bg-card px-3 py-2 text-left text-xs text-slate-700 shadow-xs">
<span>
<strong className="font-semibold text-slate-900">
Veri: YÖK Atlas
</strong>{" "}
· son güncelleme{" "}
<time dateTime={KATALOG_VERI_TARIHI}>
{KATALOG_TARIH_METNI}
</time>
</span>
</div>
{/* Markayı tek cümlede tanımlayan görünür metin: sitedeki SSS ve
/kosullar ifadelerinin toplamı, yeni vaat yok. Meta/OG'de zaten
var; yapay zekâ cevap motorları gövde metnini önceliklendirdiği
için burada da durur (docs/arastirma/2026-09-24-geo-seo). */}
<p className="mt-5 max-w-2xl text-sm leading-6 text-slate-600">
KolayTercih, YÖK Atlas&apos;ın resmî yerleştirme verisiyle
sıralamana uygun 24 tercihlik dengeli liste kuran, yapay zekâ
destekli bir karar destek aracıdır. ÖSYM ya da YÖK ile bağlantılı
değildir; yerleşme garantisi vermez.
</p>
</Parallax>
</section>
@@ -303,23 +321,24 @@ export default function Home() {
</p>
</Reveal>
</Parallax>
{/* Sahne kartı (DESIGN.md §4.31): üstte hero'nun piksel diliyle
kartın söylediğini çizen sahne, altında başlık ve metin. */}
<Parallax strength={7} className="mt-12 grid gap-6 md:grid-cols-3">
{problems.map((p, i) => (
<Reveal key={p.title} delay={i * 0.08}>
<Card className="relative h-full overflow-hidden border-slate-200/70 shadow-sm transition-all duration-300 hover:-translate-y-1 hover:border-orange-500 hover:shadow-lg hover:shadow-orange-500/5">
<PixelCorner className="absolute right-4 top-4" />
<CardHeader>
<div className="mb-2 flex size-11 items-center justify-center rounded-xl bg-primary/10">
<p.icon className="size-5 text-primary" aria-hidden />
</div>
<CardTitle className="font-heading text-xl">
<Reveal key={p.title} delay={i * 0.08} className="h-full">
<div className="flex h-full flex-col overflow-hidden rounded-2xl border border-slate-200/70 bg-card shadow-sm transition-[translate,border-color,box-shadow] duration-300 hover:-translate-y-1 hover:border-orange-500 hover:shadow-lg hover:shadow-orange-500/5">
<div className="border-b border-slate-100 bg-slate-50 px-6 py-8">
<PixelSahne izgara={p.sahne} />
</div>
<div className="p-6">
<h3 className="font-heading text-xl font-bold text-balance">
{p.title}
</CardTitle>
<CardDescription className="text-base leading-relaxed">
</h3>
<p className="mt-2 text-base leading-relaxed text-slate-600">
{p.description}
</CardDescription>
</CardHeader>
</Card>
</p>
</div>
</div>
</Reveal>
))}
</Parallax>
@@ -361,77 +380,82 @@ export default function Home() {
</p>
</Reveal>
</Parallax>
<Parallax strength={7} className="mt-12 grid gap-6 md:grid-cols-3">
{steps.map((s, i) => (
<Reveal key={s.step} delay={i * 0.08} className="h-full">
<div className="relative h-full rounded-2xl border border-slate-200/70 bg-card p-8 shadow-sm transition-all duration-300 hover:-translate-y-1 hover:border-orange-500 hover:shadow-lg hover:shadow-orange-500/5">
<span className="absolute -top-4 left-8 flex size-8 items-center justify-center rounded-full bg-orange-500 font-heading text-sm font-bold text-white">
{s.step}
</span>
<PixelCorner className="absolute right-4 top-4" />
<s.icon className="size-6 text-primary" aria-hidden />
<h3 className="mt-4 font-heading text-xl font-semibold">
{s.title}
</h3>
<p className="mt-2 leading-relaxed text-slate-600">
{s.description}
</p>
</div>
</Reveal>
))}
{/* Adımlar kartsız (DESIGN.md §4.32): numara hero'nun piksel
diliyle iri rakam, md'den itibaren rakamlar düz çizgiyle
birbirine bağlanır. */}
<Parallax strength={7} className="mt-12">
<ol className="grid gap-12 md:grid-cols-3 md:gap-8">
{steps.map((s, i) => (
<li key={s.step}>
<Reveal
delay={i * 0.08}
className="relative flex flex-col items-center text-center"
>
<PixelRakam rakam={s.step} className="h-24 w-auto" />
{i < steps.length - 1 ? (
<span
aria-hidden
className="absolute top-12 left-[calc(50%+4.5rem)] hidden h-0.5 w-[calc(100%-7rem)] -translate-y-1/2 rounded-full bg-slate-200 md:block"
/>
) : null}
<h3 className="mt-6 font-heading text-xl font-bold text-balance">
<span className="sr-only">{s.step}. adım: </span>
{s.title}
</h3>
<p className="mt-2 max-w-sm text-base leading-relaxed text-slate-600">
{s.description}
</p>
</Reveal>
</li>
))}
</ol>
</Parallax>
{/* Veriye dayalı güven + garanti vaadine karşı dürüst konumlandırma */}
<Parallax strength={10} className="mx-auto mt-12 w-full max-w-3xl">
{/* Veri akışı + veriye dayalı güven + dürüstlük notu (DESIGN.md
§4.28). Üçü de kartsız, sayfa zemininde açıkta durur: geniş
ekranda solda dürüstlük notu (sola yaslı), ortada animasyon,
sağda (YÖK Atlas girdilerinin yanında) üç sayı (sağa yaslı).
lg altında hepsi ortalı; sıra: animasyon → sayılar → not. */}
<Parallax strength={10} className="mt-20">
<Reveal
delay={0.1}
className="overflow-hidden rounded-2xl border border-slate-200/70 bg-card shadow-sm"
className="grid items-center gap-12 lg:grid-cols-[minmax(0,1fr)_22rem_minmax(0,1fr)] lg:gap-8 xl:grid-cols-[minmax(0,1fr)_24rem_minmax(0,1fr)] xl:gap-12"
>
<dl className="grid grid-cols-1 divide-y divide-slate-200 sm:grid-cols-3 sm:divide-x sm:divide-y-0">
{[
{
value: PROGRAM_SAYISI_METNI,
label: "Taranan lisans ve önlisans programı",
},
{
value: `${VERI_YIL_SAYISI} yıl`,
label: "Taban sıralaması trend verisi",
},
{ value: "%100", label: "Resmî YÖK Atlas kaynağı" },
].map((stat) => (
<VeriAkisi className="lg:order-2" />
<dl className="mx-auto grid w-full max-w-3xl grid-cols-1 gap-8 text-center sm:grid-cols-3 lg:order-3 lg:mx-0 lg:grid-cols-1 lg:gap-10 lg:text-right">
{veriSayilari.map((stat) => (
// Sayı kahramandır (DESIGN.md §1.1): iri sayı üstte, etiket
// altında; dt anlam için DOM'da önce, görsel sıra order ile.
<div
key={stat.label}
className="flex flex-col p-6 text-center"
>
<div key={stat.label} className="flex flex-col">
<dt className="order-2 mt-2 text-sm font-semibold text-slate-500">
{stat.label}
</dt>
<dd className="order-1 font-heading text-4xl font-bold leading-none tracking-tight text-primary">
<dd className="order-1 font-heading text-4xl font-bold leading-none tracking-tight text-primary sm:text-5xl">
{stat.value}
{stat.birim ? (
<span className="ml-1.5 font-sans text-base font-semibold tracking-normal text-slate-400">
{stat.birim}
</span>
) : null}
</dd>
</div>
))}
</dl>
<div className="flex gap-4 border-t border-slate-200 bg-primary/5 p-6 text-left sm:items-center sm:p-7">
<div className="flex size-11 shrink-0 items-center justify-center rounded-xl bg-primary text-white shadow-sm">
<ShieldCheck className="size-5" aria-hidden />
</div>
<div>
<p className="text-sm font-semibold text-primary">
Dürüstlük notu
</p>
<p className="mt-1 font-heading text-lg font-bold text-slate-900 sm:text-xl">
Yerleşme garantisi vermiyoruz — çünkü kimse veremez.
</p>
<p className="mt-1.5 text-sm leading-6 text-slate-600">
Taban sıralamaları her yıl değişir. Biz sonuç vaadi satmak
yerine riski satır satır gösterir, daha güvenli bir liste
kurmana yardımcı oluruz.
</p>
</div>
<div className="mx-auto flex w-full max-w-xl flex-col items-center text-center lg:order-1 lg:mx-0 lg:max-w-xs lg:items-start lg:text-left">
<SectionEyebrow>Dürüstlük notu</SectionEyebrow>
<h3 className="mt-4 font-heading text-2xl font-bold text-balance text-slate-900">
Yerleşme garantisi vermiyoruz —{" "}
<span className="font-bricolage font-bold tracking-tighter text-primary italic">
çünkü kimse veremez.
</span>
</h3>
<p className="mt-3 text-base leading-relaxed text-slate-600">
Taban sıralamaları her yıl değişir. Biz sonuç vaadi satmak
yerine riski satır satır gösterir, daha güvenli bir liste
kurmana yardımcı oluruz.
</p>
</div>
</Reveal>
</Parallax>
@@ -668,6 +692,47 @@ export default function Home() {
</div>
</section>
{/* Katalog: süzülen haplar (DESIGN.md §4.30) */}
{haplar.length >= 6 ? (
<>
<div aria-hidden className="mx-auto w-56">
<Parallax strength={26}>
<PixelDivider seed={41} />
</Parallax>
</div>
<section id="katalog" className="scroll-mt-24 py-20 sm:py-24">
<div className="mx-auto max-w-6xl px-4">
<Parallax strength={16}>
<Reveal className="flex flex-col items-center gap-4 text-center">
<SectionEyebrow>Katalog ücretsiz</SectionEyebrow>
<h2 className="font-heading text-3xl font-bold text-balance sm:text-4xl">
Önce bir bakmak ister misin?
</h2>
<p className="max-w-xl text-lg leading-relaxed text-slate-600">
Taban sıralamaları ve kontenjanlar herkese açık. Üye
olmadan bölüm bölüm, şehir şehir gez.
</p>
</Reveal>
</Parallax>
<SuzulenHaplar haplar={haplar} className="mt-6" />
<div className="mt-4 flex justify-center">
<Button
asChild
size="lg"
variant="outline"
className="cursor-pointer px-6 transition-[background-color,border-color,color,transform] duration-150 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500 active:scale-[0.97]"
>
<Link href="/bolumler">
Tüm bölümleri gör
<ArrowRight className="size-4" aria-hidden />
</Link>
</Button>
</div>
</div>
</section>
</>
) : null}
{/* Fiyat → SSS geçişi */}
<div aria-hidden className="mx-auto w-56">
<Parallax strength={26}>