feat(v2): 9–10 Ekim çalışma ağacı — deneyim dili 1. dalga, PWA manifest, revizyon niyet modeli, listem/tadımlık bileşen denetimi

9–10 Ekim oturumlarının commit'lenmemiş işi tek parça alındı
(raporlar: docs/ekip-raporlari/2026-10-09, 2026-10-10):
- Landing deneyim dili 1. dalga: sayfa geçişi (ViewTransition), piksel morf/şerit,
  sorun sahneleri, adım akışı, sayan sayı, mobil menü, adım göstergesi, logoya tepki;
  DESIGN.md §11 ve AGENTS.md kuralları, tasarım denetimine yeni kurallar
- Tema: tema ikonu, D kısayolu, tema geçişi
- PWA: manifest.ts ve ikonlar
- Danışman: revizyon niyet modeli (OPENROUTER_NIYET_MODEL), sohbet geçmişi, soru API'si
- /listem ve /sonuc tadımlığı bileşen denetimi düzeltmeleri
- eslint: .venv-tts/ yok sayılır

tsc, eslint ve tasarım denetimi sıfır hata.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-10-11 00:34:29 +03:00
parent da0e9dff7a
commit aec8a2ad76
80 changed files with 4134 additions and 654 deletions

View File

@@ -20,6 +20,7 @@ import { kuralOzeti } from "@/lib/revizyon-kurali";
import {
anketCevabiDogrula,
anketCevabiMetni,
type AnketCevaplari,
type RevizyonOzeti,
type SohbetMeta,
} from "@/lib/ai/sohbet-meta";
@@ -138,6 +139,7 @@ export async function POST(request: NextRequest) {
// üretilir; istemcinin yazdığı metin kullanılmaz.
const sonAnket = sonMesaj?.role === "assistant" ? sonMesaj.meta?.anket : undefined;
let kullaniciMeta: SohbetMeta | undefined;
let anketCevaplari: AnketCevaplari | null = null;
if (anketCevabiHam) {
if (!sonAnket || sonAnket.id !== anketCevabiHam.anketId) {
return Response.json({ error: "Bu form artık geçerli değil; isteğini yazarak söyleyebilirsin." }, { status: 409 });
@@ -148,6 +150,7 @@ export async function POST(request: NextRequest) {
}
mesaj = anketCevabiMetni(sonAnket, cevaplar);
kullaniciMeta = { anketCevabi: { anketId: sonAnket.id, cevaplar } };
anketCevaplari = cevaplar;
}
const bedelsiz = Boolean(anketCevabiHam) || onayMesajiMi(mesaj, sonAsistan);
@@ -294,7 +297,7 @@ export async function POST(request: NextRequest) {
let islem: KoddanIslem | undefined;
if (paketli && revizyonNiyetiVar(mesaj)) {
const t0 = Date.now();
const niyet = await revizyonNiyetiCikar({ mesaj, params: raporParams, oncekiForm: anketCevabiHam ? sonAnket : null });
const niyet = await revizyonNiyetiCikar({ mesaj, params: raporParams, oncekiForm: anketCevabiHam ? sonAnket : null, anketCevabi: anketCevaplari });
console.info("[soru] revizyon niyeti:", niyet.niyet, "kural" in niyet ? "kural" : "form" in niyet ? `form (eksik: ${niyet.eksik.join(", ") || "—"})` : "neden" in niyet ? niyet.neden : "", `${Date.now() - t0} ms`);
if (niyet.niyet === "revizyon" && "kural" in niyet) {
const kural = niyet.kural;

View File

@@ -74,6 +74,11 @@
--chart-4: oklch(0.371 0 0);
--chart-5: oklch(0.269 0 0);
--radius: 0.625rem;
/* Hareket eğrileri (DESIGN.md §6); JS karşılığı src/lib/hareket.ts EGRI */
--egri-giris: cubic-bezier(0.16, 1, 0.3, 1);
--egri-standart: cubic-bezier(0.22, 1, 0.36, 1);
--egri-guclu-cikis: cubic-bezier(0.23, 1, 0.32, 1);
--egri-yerlesme: cubic-bezier(0.32, 0.72, 0, 1);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
@@ -484,3 +489,297 @@
animation: none;
}
}
/* Tema geçişi (DESIGN.md §2.3, lib/tema-gecisi.ts): yeni tema header'daki
logodan yumuşak bir halka gibi açılır. Eski görüntü yerinde durur; yeni
görüntü radyal degrade maskesiyle üstünde belirir. Maskenin kenarı keskin
değil: yarıçapın %55'ine kadar tam opak, oradan kenara doğru tüy gibi
söner (--kt-tema-cekirdek; JS'teki CEKIRDEK ile aynı olmalı) — ışık
yayılır gibi, sert çizgi yok. Yarıçap (--kt-tema-r) 0 → --kt-tema-son
yumuşak kalkıp yumuşak durur; kısa ve hafif (600 ms) — asıl gösteri,
geçiş bitince oynayan logo dönüşümüdür. Merkez ve son yarıçap JS'ten gelir.
Hareket azaltılmışsa JS geçişi hiç başlatmaz, tema düz değişir. */
@property --kt-tema-r {
syntax: "<length>";
inherits: true;
initial-value: 0px;
}
.kt-tema-gecis {
view-transition-name: root;
--kt-tema-cekirdek: 0.55;
}
.kt-tema-gecis::view-transition-old(root),
.kt-tema-gecis::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
}
.kt-tema-gecis::view-transition-old(root) {
z-index: 1;
}
.kt-tema-gecis::view-transition-new(root) {
z-index: 2;
mask-image: radial-gradient(
circle at var(--kt-tema-x) var(--kt-tema-y),
#000 calc(var(--kt-tema-r) * var(--kt-tema-cekirdek)),
transparent var(--kt-tema-r)
);
animation: kt-tema-halka 600ms cubic-bezier(0.4, 0, 0.2, 1) both;
}
@keyframes kt-tema-halka {
from {
--kt-tema-r: 0px;
}
to {
--kt-tema-r: var(--kt-tema-son);
}
}
/* Sayfa geçişi (DESIGN.md §6, components/sayfa-gecisi.tsx): rota değişince
eski içerik hızla söner, yenisi hafifçe yükselerek belirir — çıkış girişten
hızlı. Üst çubuk kendi grubunda ve animasyonsuz: içerik değişir, çubuk
yerinde kalır (uygulama hissi, §11.4). Tema geçişinde (`.kt-tema-gecis`)
çubuk adını bırakır ki halka onu da kaplasın. */
.kt-ust-cubuk {
view-transition-name: kt-ust-cubuk;
}
.kt-tema-gecis .kt-ust-cubuk {
view-transition-name: none;
}
::view-transition-group(kt-ust-cubuk) {
animation: none;
z-index: 100;
}
::view-transition-old(kt-ust-cubuk) {
display: none;
}
::view-transition-new(kt-ust-cubuk) {
animation: none;
}
::view-transition-old(.kt-sayfa-cik) {
animation: 120ms var(--egri-standart) both kt-sayfa-sol reverse;
}
::view-transition-new(.kt-sayfa-gir) {
animation:
200ms var(--egri-standart) 80ms both kt-sayfa-sol,
280ms var(--egri-giris) both kt-sayfa-yuksel;
}
@keyframes kt-sayfa-sol {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes kt-sayfa-yuksel {
from {
translate: 0 8px;
}
to {
translate: 0 0;
}
}
@media (prefers-reduced-motion: reduce) {
::view-transition-old(.kt-sayfa-cik),
::view-transition-new(.kt-sayfa-gir) {
animation: none;
}
}
/* Ana sayfa ilk ekranının girişi (DESIGN.md §6 "sayfa başına tek orkestre
giriş", §11.2): önce harita belirir, sonra başlık, sonra form netleşerek
gelir — sırayla, üst üste binmeden. `--sira` ile kademelenir. Başlık LCP
öğesi olduğundan hiçbir kare tam saydam başlamaz (opaklık 0,35'ten). */
.kt-hero-gir {
animation: kt-hero-gir 0.7s var(--egri-giris) both;
animation-delay: calc(var(--sira, 0) * 110ms + 80ms);
}
.kt-hero-harita {
animation: kt-hero-harita 1.1s var(--egri-standart) both;
}
@keyframes kt-hero-gir {
from {
opacity: 0.35;
filter: blur(10px);
translate: 0 12px;
}
to {
opacity: 1;
filter: blur(0);
translate: 0 0;
}
}
@keyframes kt-hero-harita {
from {
opacity: 0;
scale: 0.985;
}
to {
opacity: 1;
scale: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.kt-hero-gir,
.kt-hero-harita {
animation: kt-unblur-sade 0.25s ease-out both;
}
}
/* "Nasıl çalışır" adımları arasındaki çizgi (DESIGN.md §4.32): adımın rakamı
biçimine dönüşüp geri toplanınca (adim-akisi.tsx `data-dolu` yazar) soldan
maviyle dolar. JS'siz istemcide dolu durur. */
@media (scripting: enabled) {
.kt-cizgi-dol {
transform: scaleX(0);
transform-origin: left;
transition: transform 0.6s var(--egri-guclu-cikis);
}
.kt-cizgi-dol[data-dolu] {
transform: scaleX(1);
}
@media (prefers-reduced-motion: reduce) {
.kt-cizgi-dol {
transform: none;
transition: none;
}
}
}
/* Piksel morf (components/piksel-morf.tsx, DESIGN.md §4.38): her piksel bir
<g>; yeri `translate` ile verilir ve kare değişince yeni hücresine süzülür.
`gizli` küçülüp söner. Turuncu vurgu, doku karesinin üstündeki ikinci
karedir: çabuk ısınır, yavaş soğur (ısı fırçasıyla aynı asimetri).
Sekme yok (§6); kayma süresi sahneye göre `--morf-sure` ile ayarlanır. */
.kt-morf-px {
transform-box: fill-box;
transform-origin: center;
transition:
translate var(--morf-sure, 0.55s) var(--egri-standart) var(--g, 0ms),
opacity 0.3s var(--egri-standart) var(--g, 0ms),
scale 0.3s var(--egri-standart) var(--g, 0ms);
}
.kt-morf-px[data-ton="gizli"] {
opacity: 0;
scale: 0.6;
}
.kt-morf-vurgu {
opacity: 0;
fill-opacity: 0.7;
transition: opacity 1.2s var(--egri-standart);
}
.kt-morf-taban {
transition: opacity 0.6s var(--egri-standart);
}
.kt-morf-px[data-ton="vurgu"] > .kt-morf-taban {
opacity: 0;
transition: opacity 0.25s var(--egri-standart) var(--g, 0ms);
}
.kt-morf-px[data-ton="vurgu"] > .kt-morf-vurgu {
opacity: 1;
animation:
kt-morf-isin 0.25s var(--egri-standart) var(--g, 0ms) backwards,
kt-morf-nabiz 3.6s ease-in-out var(--n, 0s) infinite;
}
@keyframes kt-morf-isin {
from {
opacity: 0;
}
}
@keyframes kt-morf-nabiz {
50% {
fill-opacity: 0.35;
}
}
@media (prefers-reduced-motion: reduce) {
.kt-morf-px,
.kt-morf-taban,
.kt-morf-vurgu {
transition: none;
}
.kt-morf-px[data-ton="vurgu"] > .kt-morf-vurgu {
animation: none;
}
}
/* Kendini çizen tik (landing-parcalari.tsx, DESIGN.md §5.2.3): karşılaştırma
tablosu ekrana girince (Reveal → .kt-reveal-in) önce halka, sonra işaret
çizilir; satır gecikmesi `--tik-g` ile gelir. `pathLength="1"` bekler. */
@media (scripting: enabled) and (prefers-reduced-motion: no-preference) {
.kt-reveal .kt-tik-ciz {
stroke-dasharray: 1;
stroke-dashoffset: 1;
opacity: 0;
transition:
stroke-dashoffset 0.5s var(--egri-standart) calc(var(--tik-g, 0ms) + 250ms),
opacity 0.01s linear calc(var(--tik-g, 0ms) + 250ms);
}
.kt-reveal .kt-tik-isaret {
transition-duration: 0.3s, 0.01s;
transition-delay:
calc(var(--tik-g, 0ms) + 600ms),
calc(var(--tik-g, 0ms) + 600ms);
}
.kt-reveal.kt-reveal-in .kt-tik-ciz {
stroke-dashoffset: 0;
opacity: 1;
}
}
/* Hero haritasının serpintisi (turkey-pixel-map.tsx, DESIGN.md §11.1 V2):
piksel bir süre yerinde durur, kopup `--d` birim (10 = bir hücre) düşerken
söner, sonra yerinde yeniden belirir. Süre ve faz satır içi. */
@keyframes kt-serpinti {
0%,
42% {
translate: 0 0;
opacity: 1;
animation-timing-function: ease-in;
}
76% {
translate: 0 calc(var(--d, 20) * 1px);
opacity: 0;
animation-timing-function: step-end;
}
80% {
translate: 0 0;
opacity: 0;
animation-timing-function: ease-out;
}
100% {
translate: 0 0;
opacity: 1;
}
}
.kt-serpinti {
animation: kt-serpinti 6s infinite;
}
@media (prefers-reduced-motion: reduce) {
.kt-serpinti {
animation: none;
}
}
/* Piksel şeridi (components/piksel-serit.tsx, DESIGN.md §4.38): kaydırmaya
bağlı sütun. Okuma çizgisini geçen piksel `data-yandi` alır ve turuncuya
ısınır; geri kaydırınca yavaş soğur. Şeridin ucu (`data-uc`) tam turuncu. */
.kt-serit-px {
fill: currentColor;
fill-opacity: var(--op, 0.2);
transition:
fill 0.9s var(--egri-standart),
fill-opacity 0.9s var(--egri-standart);
}
.kt-serit-px[data-yandi] {
fill: var(--color-orange-500);
fill-opacity: 0.62;
transition-duration: 0.16s;
}
.kt-serit-px[data-uc] {
fill-opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
.kt-serit-px {
transition: none;
}
}

View File

@@ -3,9 +3,11 @@ import Script from "next/script";
import { Cal_Sans, Geist_Mono, Bricolage_Grotesque } from "next/font/google";
import localFont from "next/font/local";
import { BildirimYigini } from "@/components/ui/bildirim";
import { SayfaGecisi } from "@/components/sayfa-gecisi";
import { SiteHeader } from "@/components/site-header";
import { SiteTopBanner } from "@/components/site-top-banner";
import { ListeCekmecesiLazy } from "@/features/liste/components/liste-cekmecesi-lazy";
import { UcusKatmani } from "@/features/liste/components/ucus-katmani";
import { KaydetBannerLazy } from "@/features/liste/components/kaydet-banner-lazy";
import { TercihProfiliKapisiLazy } from "@/features/liste/components/tercih-profili-kapisi-lazy";
import { HeaderGate } from "@/components/header-gate";
@@ -117,12 +119,14 @@ export default function RootLayout({
<div
id="icerik"
tabIndex={-1}
className="flex min-h-0 flex-1 flex-col pb-28 outline-none print:pb-0"
className="flex min-h-0 flex-1 flex-col pb-28 outline-none has-[[data-site-footer]]:pb-0 print:pb-0"
>
{children}
<SayfaGecisi>{children}</SayfaGecisi>
</div>
{/* Manuel 24'lük liste çekmecesi + "+" uçuş katmanı (navbar'dan açılır) */}
{/* Manuel 24'lük liste çekmecesi (navbar'dan açılır) */}
<ListeCekmecesiLazy />
{/* "+" ile eklemede rozete uçan piksel kare (DESIGN.md §4.37) */}
<UcusKatmani />
{/* Girişsiz + 3 seçimli kullanıcıya "listen yalnızca bu tarayıcıda" banner'ı */}
<KaydetBannerLazy />
{/* İlk "+" için sıra + sihirbaz kapısı */}

26
src/app/manifest.ts Normal file
View File

@@ -0,0 +1,26 @@
import type { MetadataRoute } from "next";
// Ana ekrana eklenebilir kabuk (DESIGN.md §11.4): telefonda "Ana ekrana ekle"
// ile site tarayıcı çubuğu olmadan, uygulama gibi açılır. Yalnız kurulum
// kabuğudur; service worker ve çevrimdışı çalışma yok. Renkler gövde zemini
// (bg-slate-50); koyu temada adres çubuğu rengini tema-saglayici.tsx sürer.
export default function manifest(): MetadataRoute.Manifest {
return {
name: "KolayTercih — YKS Tercih Danışmanı",
short_name: "KolayTercih",
description:
"YKS sıralamana göre gerçek YÖK Atlas verisiyle dengeli 24 tercihlik liste.",
lang: "tr",
start_url: "/",
scope: "/",
display: "standalone",
orientation: "portrait",
background_color: "#f8fafc",
theme_color: "#f8fafc",
icons: [
{ src: "/pwa-ikon.svg", sizes: "any", type: "image/svg+xml", purpose: "any" },
{ src: "/pwa-ikon-maskable.svg", sizes: "any", type: "image/svg+xml", purpose: "maskable" },
{ src: "/apple-icon.png", sizes: "180x180", type: "image/png", purpose: "any" },
],
};
}

View File

@@ -10,23 +10,22 @@ import {
} from "lucide-react";
import Link from "next/link";
import { Suspense } from "react";
import { AdimAkisi } from "@/features/pazarlama/components/adim-akisi";
import { BolumGecisi } from "@/features/pazarlama/components/bolum-gecisi";
import { ComparisonCell } from "@/features/pazarlama/components/landing-parcalari";
import { HeroBaslik } from "@/features/pazarlama/components/hero-baslik";
import { KapanisCta } from "@/features/pazarlama/components/hero-focus-button";
import { HeroForm } from "@/features/sihirbaz/components/hero-form";
import { Parallax } from "@/components/parallax";
import { Reveal } from "@/features/pazarlama/components/reveal";
import { SayanSayi } from "@/features/pazarlama/components/sayan-sayi";
import { SorunSahnesi } from "@/features/pazarlama/components/sorun-sahneleri";
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 {
PixelDivider,
PixelRakam,
PixelSahne,
SectionEyebrow,
} from "@/components/pixel-decor";
import { SectionEyebrow } from "@/components/pixel-decor";
import { PikselCtaYuzeyi } from "@/components/piksel-cta-yuzeyi";
import { URUNLER } from "@/lib/credits";
import {
@@ -68,78 +67,57 @@ 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",
"..................###---",
"...............###------",
"............###---------",
".........###------------",
"......###---------------",
"...###------------------",
"###---------------------",
];
// Bölüm ritmi (DESIGN.md §5.1; Bilal, 11 Eki 2026: "section'lar arası es yok,
// her şey çok sıkışık"). Bölümler arasında piksel şeridi durur (§4.38) ve
// çevresinde boşluk ister: şerit üst bölümün içeriğinden uzak, alt bölümün
// rozetine yakındır — bir bölümü bitirip sıradakine iner. Alt boşluk bu yüzden
// üsttekinden bir kademe büyük. Şerit geçiş çizgisinin iki yanına telefonda
// 56, sm'den itibaren 75 px taşar; kalan nefes telefonda 56/40, sm'de 85/53,
// lg'de 101/69 px (üst bölüme/alt bölüme).
const BOLUM_BOSLUGU = "pt-24 pb-28 sm:pt-32 sm:pb-40 lg:pt-36 lg:pb-44";
// Sorun kartlarının piksel sahneleri (DESIGN.md §4.31, §4.38): hero haritasıyla
// aynı ızgara dili. Her biri kartın söylediğini hareketle çizer: binlerce
// programın içinde ısınıp sönen tam 24 seçim; her yıl bir sola kayan, sağdan
// başka boyda gelen çubuklar; basamak basamak tırmanan fiyat. Izgaralar
// `sorun-sahneleri.tsx`te.
const problems = [
{
sahne: SAHNE_IHTIMAL,
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.`,
},
{
sahne: SAHNE_YILLAR,
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.",
},
{
sahne: SAHNE_FIYAT,
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.",
},
];
] as const;
const steps = [
{
step: 1,
title: "Sıralamanı ve hedeflerini anlat",
description:
no: 1,
baslik: "Sıralamanı ve hedeflerini anlat",
aciklama:
"YKS başarı sıralamanı gir; sıralamana uygun ilgi alanı, şehir ve üniversite tercihlerini birkaç adımda işaretle.",
},
{
step: 2,
title: `Son ${VERI_YIL_SAYISI} yılın resmî verisi taransın`,
description:
no: 2,
baslik: `Son ${VERI_YIL_SAYISI} yılın resmî verisi taransın`,
aciklama:
"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.",
},
{
step: 3,
title: "Dengeli 24'lük listeni al",
description:
no: 3,
baslik: "Dengeli 24'lük listeni al",
aciklama:
"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;
@@ -270,19 +248,23 @@ export default function Home() {
aria-hidden
// SVG altta 9 hücrelik serpinti payı taşıdığı için silüet merkezi
// yüksekliğin %37,1'inde; -1/2 yerine bu oranla hizalanır
className="pointer-events-none absolute left-1/2 top-[43%] -z-10 hidden -translate-x-1/2 -translate-y-[37.14%] sm:block"
// Telefonda harita gizlenmez (DESIGN.md §11.2-9): ekrandan taşacak
// kadar iri durur, başlığın üstündeki boşluğu doldurur ve serpinti
// başlığa doğru dökülür.
className="kt-hero-harita pointer-events-none absolute left-1/2 top-[27%] -z-10 -translate-x-1/2 -translate-y-[37.14%] sm:top-[43%]"
>
{/* Arka katman: harita scroll'un gerisinde kalır, derinlik verir */}
<Parallax fromStart strength={-52}>
{/* Harita orta ekranlarda nefes payı bırakır, ekranla birlikte
ölçeklenir ve çok geniş ekranda 1665px tavanına oturur;
mobilde tümüyle gizli. */}
<TurkeyPixelMap className="w-[min(1665px,82vw)] max-w-none text-primary" />
telefonda ekranın 1,7 katı (kenarlardan taşar, kırpılır). */}
<TurkeyPixelMap className="w-[170vw] max-w-none text-primary sm:w-[min(1665px,82vw)]" />
</Parallax>
</div>
<div
aria-hidden
className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(ellipse_58%_52%_at_50%_42%,color-mix(in_oklab,var(--color-slate-50)_90%,transparent)_25%,transparent_75%)]"
// Okunurluk perdesi: telefonda başlık daha aşağıda ve dar, perde ona göre
className="pointer-events-none absolute inset-0 -z-10 bg-[radial-gradient(ellipse_95%_38%_at_50%_62%,color-mix(in_oklab,var(--color-slate-50)_92%,transparent)_35%,transparent_80%)] sm:bg-[radial-gradient(ellipse_58%_52%_at_50%_42%,color-mix(in_oklab,var(--color-slate-50)_90%,transparent)_25%,transparent_75%)]"
/>
<Parallax
fromStart
@@ -292,22 +274,21 @@ export default function Home() {
<HeroBaslik />
<div
id="hero-form"
className="mt-6 flex w-full scroll-mt-28 justify-center"
className="kt-hero-gir mt-6 flex w-full scroll-mt-28 justify-center"
style={{ "--sira": 2 } as React.CSSProperties}
>
<HeroForm />
</div>
</Parallax>
</section>
{/* Hero → Problem geçişi: border yerine piksel ayraç */}
<div aria-hidden className="mx-auto w-56">
<Parallax strength={26}>
<PixelDivider seed={11} />
</Parallax>
</div>
{/* Hero → Problem geçişi: border ya da ayraç yerine piksel şeridi.
Haritanın serpintisinden başlar, kaydırdıkça turuncuya ısınarak
bölümden bölüme iner (DESIGN.md §4.38). */}
<BolumGecisi tohum={11} ust={6} ustTelefon={6} ustKisaTelefon={3} />
{/* Problem */}
<section className="py-20 sm:py-24">
<section className={BOLUM_BOSLUGU}>
<div className="mx-auto max-w-6xl px-4">
{/* Başlık gövdeden hafif hızlı akar — katman derinliği */}
<Parallax strength={16}>
@@ -322,13 +303,18 @@ export default function Home() {
</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">
kartın söylediğini çizen sahne, altında başlık ve metin.
Sahneler sırayla oynar; karta gelince o sahne hemen oynar
(`data-sahne-karti`, sorun-sahneleri.tsx). */}
<Parallax strength={7} className="mt-12 grid gap-6 sm:mt-16 md:grid-cols-3">
{problems.map((p, i) => (
<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
data-sahne-karti=""
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 motion-reduce:hover:translate-y-0 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} />
<SorunSahnesi tur={p.sahne} sira={i} />
</div>
<div className="p-6">
<h3 className="font-heading text-xl font-bold text-balance">
@@ -346,14 +332,10 @@ export default function Home() {
</section>
{/* Problem → Nasıl çalışır geçişi */}
<div aria-hidden className="mx-auto w-56">
<Parallax strength={26}>
<PixelDivider seed={17} />
</Parallax>
</div>
<BolumGecisi tohum={17} />
{/* How it works */}
<section id="nasil-calisir" className="scroll-mt-24 py-20 sm:py-24">
<section id="nasil-calisir" className={`scroll-mt-24 ${BOLUM_BOSLUGU}`}>
<div className="mx-auto max-w-6xl px-4">
<Parallax strength={16}>
<Reveal className="flex flex-col items-center gap-4 text-center">
@@ -362,7 +344,7 @@ export default function Home() {
{/* Kuralların şeffaf anlatımına köprü: /meraklisina */}
<Link
href="/meraklisina"
className="group inline-flex items-center gap-1.5 rounded-full bg-orange-500 px-3.5 py-1.5 font-bricolage text-xs font-semibold text-white transition-colors duration-200 hover:bg-orange-600"
className="group inline-flex min-h-8 items-center gap-1 rounded-full px-2 text-sm font-semibold text-slate-600 transition-colors duration-200 hover:text-orange-500"
>
Meraklısına
<ArrowUpRight
@@ -381,34 +363,10 @@ export default function Home() {
</Reveal>
</Parallax>
{/* 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>
diliyle iri rakam; her rakam sırayla adımının biçimine dönüşüp
geri toplanır, md'den itibaren aradaki düz çizgi dolar. */}
<Parallax strength={7} className="mt-12 sm:mt-16">
<AdimAkisi adimlar={steps} yilSayisi={VERI_YIL_SAYISI} />
</Parallax>
{/* Veri akışı + veriye dayalı güven + dürüstlük notu (DESIGN.md
@@ -416,10 +374,10 @@ export default function Home() {
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">
<Parallax strength={10} className="mt-24 sm:mt-32">
<Reveal
delay={0.1}
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"
className="grid items-center gap-16 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"
>
<VeriAkisi className="lg:order-2" />
@@ -432,7 +390,7 @@ export default function Home() {
{stat.label}
</dt>
<dd className="order-1 font-heading text-4xl font-bold leading-none tracking-tight text-primary sm:text-5xl">
{stat.value}
<SayanSayi deger={stat.value} />
{stat.birim ? (
<span className="ml-1.5 font-sans text-base font-semibold tracking-normal text-slate-400">
{stat.birim}
@@ -463,14 +421,10 @@ export default function Home() {
</section>
{/* Nasıl çalışır → Karşılaştırma geçişi */}
<div aria-hidden className="mx-auto w-56">
<Parallax strength={26}>
<PixelDivider seed={23} />
</Parallax>
</div>
<BolumGecisi tohum={23} />
{/* Comparison */}
<section id="karsilastirma" className="scroll-mt-24 py-20 sm:py-24">
<section id="karsilastirma" className={`scroll-mt-24 ${BOLUM_BOSLUGU}`}>
<div className="mx-auto max-w-4xl px-4">
<Parallax strength={16}>
<Reveal className="flex flex-col items-center gap-4 text-center">
@@ -488,7 +442,7 @@ export default function Home() {
üstte yapışık durur (site başlığının hemen altında: telefonda 96 px,
sm'den itibaren 128 px — ölçüldü).
lg'de klasik dört sütunlu tablo. */}
<Parallax strength={7} className="mt-12">
<Parallax strength={7} className="mt-12 sm:mt-16">
<Reveal delay={0.08}>
<div className="rounded-2xl border border-slate-200/70 bg-card shadow-sm lg:hidden">
<div
@@ -502,8 +456,12 @@ export default function Home() {
</span>
</div>
<ul className="divide-y divide-slate-100">
{comparison.map((row) => (
<li key={row.feature} className="px-3 py-5">
{comparison.map((row, i) => (
<li
key={row.feature}
className="px-3 py-5"
style={{ "--tik-g": `${i * 70}ms` } as React.CSSProperties}
>
<p className="px-1 text-center text-base font-semibold leading-snug text-slate-900">
{row.feature}
</p>
@@ -551,10 +509,11 @@ export default function Home() {
</TableRow>
</TableHeader>
<TableBody>
{comparison.map((row) => (
{comparison.map((row, i) => (
<TableRow
key={row.feature}
className="hover:bg-transparent"
style={{ "--tik-g": `${i * 70}ms` } as React.CSSProperties}
>
<TableCell className="whitespace-normal py-4 pl-5 text-base font-semibold text-slate-900">
{row.feature}
@@ -582,14 +541,10 @@ export default function Home() {
</section>
{/* Karşılaştırma → Fiyat geçişi */}
<div aria-hidden className="mx-auto w-56">
<Parallax strength={26}>
<PixelDivider seed={29} />
</Parallax>
</div>
<BolumGecisi tohum={29} />
{/* Fiyat */}
<section id="fiyat" className="scroll-mt-24 py-20 sm:py-24">
<section id="fiyat" className={`scroll-mt-24 ${BOLUM_BOSLUGU}`}>
<div className="mx-auto max-w-4xl px-4">
<Parallax strength={16}>
<Reveal className="flex flex-col items-center gap-4 text-center">
@@ -610,7 +565,7 @@ export default function Home() {
<Parallax
strength={7}
className="mt-12 grid gap-6 sm:grid-cols-[1fr_auto]"
className="mt-12 grid gap-6 sm:mt-16 sm:grid-cols-[1fr_auto]"
>
<Reveal
delay={0.08}
@@ -648,10 +603,7 @@ export default function Home() {
<SosyalKanitBandi />
</Suspense>
<Button
asChild
className="h-12 w-full cursor-pointer bg-orange-500 px-8 text-white transition-colors duration-200 hover:bg-orange-600"
>
<Button asChild size="lg" variant="cta" className="w-full">
<Link href="/paket?kaynak=fiyat_paket">
{PAKET_FIYATI} TL — Paketi incele
<ArrowRight className="size-4" aria-hidden />
@@ -677,11 +629,7 @@ export default function Home() {
+{URUNLER.topup.credits} soru hakkı, {TOPUP_FIYATI} TL. Pakete
ek olarak, istediğin zaman.
</p>
<Button
asChild
variant="outline"
className="mt-4 h-11 w-full cursor-pointer"
>
<Button asChild variant="ikincil" className="mt-4 h-11 w-full">
<Link href="/paket?kaynak=fiyat_kredi">
+{URUNLER.topup.credits} kredi — {TOPUP_FIYATI} TL
<ArrowRight className="size-4" aria-hidden />
@@ -695,12 +643,8 @@ export default function Home() {
{/* 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">
<BolumGecisi tohum={41} />
<section id="katalog" className={`scroll-mt-24 ${BOLUM_BOSLUGU}`}>
<div className="mx-auto max-w-6xl px-4">
<Parallax strength={16}>
<Reveal className="flex flex-col items-center gap-4 text-center">
@@ -719,8 +663,7 @@ export default function Home() {
<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]"
variant="ikincil"
>
<Link href="/bolumler">
Tüm bölümleri gör
@@ -734,14 +677,10 @@ export default function Home() {
) : null}
{/* Fiyat → SSS geçişi */}
<div aria-hidden className="mx-auto w-56">
<Parallax strength={26}>
<PixelDivider seed={31} />
</Parallax>
</div>
<BolumGecisi tohum={31} />
{/* FAQ */}
<section id="sss" className="scroll-mt-24 py-20 sm:py-24">
<section id="sss" className={`scroll-mt-24 ${BOLUM_BOSLUGU}`}>
<div className="mx-auto max-w-3xl px-4">
<Parallax strength={16}>
<Reveal className="flex flex-col items-center gap-4 text-center">
@@ -756,7 +695,7 @@ export default function Home() {
<Accordion
type="single"
collapsible
className="mt-10 rounded-2xl border border-slate-200/70 bg-card px-6 shadow-sm"
className="mt-12 rounded-2xl border border-slate-200/70 bg-card px-6 shadow-sm sm:mt-16"
>
{faqs.map((faq) => (
<AccordionItem
@@ -779,7 +718,7 @@ export default function Home() {
</section>
{/* Final CTA — tam genişlik bant yerine yüzen, köşeleri yumuşak panel */}
<section className="px-4 pb-24 pt-6 sm:px-6">
<section className="px-4 pb-24 pt-6 sm:px-6 sm:pb-32">
<Parallax strength={12} className="mx-auto max-w-6xl">
<Reveal>
{/* Kapanış yüzeyi (DESIGN.md §10.8): koyu kart + 24'lük