From 5b51fd16e05fbb3cae6408afb2915bb837539237 Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Thu, 24 Sep 2026 00:39:21 +0300 Subject: [PATCH 01/23] =?UTF-8?q?feat(tasarim):=20b=C3=B6l=C3=BCm=20sayfas?= =?UTF-8?q?=C4=B1=20"=C5=9Fi=C5=9Fko=20ve=20sade"=20dile=20g=C3=B6re?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Başlık bloğu lg altında ortalı, kimlik hapları (düzey + puan türü), dört özet kartı (Program, Devlet, Vakıf, En iyi sıralama), piksel ayraç - OzetKart src/components/ozet-kart.tsx'e çıkarıldı; üniversite ve bölüm sayfası oradan kullanıyor. 6-7 haneli sayı birimle kısalıyor (ölçüldü) - Program listesi §4.15: ad sarar, varyant ayrı satırda, 44 px butonlar, telefonda taban sıra adın altında, butonlar alt alta - Sayfalama 44 px, telefonda tek satır; hover'lar tek turuncu - Denetim: kanonik liste satırı adının mavi hover'ı artık uyarı vermiyor Co-Authored-By: Claude Opus 5.5 --- docs/tasarim/DESIGN.md | 28 +- scripts/tasarim-denetim.ts | 15 +- src/components/ozet-kart.tsx | 58 ++++ .../katalog/components/bolum-icerik.tsx | 327 +++++++++++++----- .../katalog/components/bolum-listesi.tsx | 2 +- .../components/bolum-program-listesi.tsx | 96 ++--- .../katalog/components/program-netleri.tsx | 2 +- src/features/katalog/components/sayfalama.tsx | 16 +- .../katalog/components/universite-icerik.tsx | 58 +--- .../components/universite-program-tablosu.tsx | 4 +- .../liste/components/program-ekle-butonu.tsx | 2 +- 11 files changed, 394 insertions(+), 214 deletions(-) create mode 100644 src/components/ozet-kart.tsx diff --git a/docs/tasarim/DESIGN.md b/docs/tasarim/DESIGN.md index 25187ed..adfed78 100644 --- a/docs/tasarim/DESIGN.md +++ b/docs/tasarim/DESIGN.md @@ -222,7 +222,7 @@ shadcn `Input`, `Select`, `Textarea`, `Slider`. Etiket her alanda; hata mesajı ### 4.7 İkonlar Yalnızca `lucide-react`. Boyut: `size-4` buton/satır içi (varsayılan), `size-5` ikon kutusu, `size-3.5` küçük çip, `size-6` üstü yalnızca boş durum. Dekoratif ikon `aria-hidden`; tek başına ikon buton `aria-label` ya da `sr-only` metin taşır. Emoji ikon olarak kullanılmaz. -### 4.8 Özet kartı — `OzetKart`, `universite-icerik.tsx` +### 4.8 Özet kartı — `OzetKart`, `src/components/ozet-kart.tsx` Apple Fitness özet kartının sitenin ölçüleriyle hâli. Sayfada **en çok dört** tane (harita yanındaki 2×2). @@ -239,7 +239,11 @@ yanındaki 2×2). ikon kutusu `flex size-8 shrink-0 items-center justify-center rounded-xl bg-primary/10 sm:size-10`. - `dl` anlamı için `dt` (başlık) DOM'da önce gelir, görsel sıra `order-*` ile kurulur. - Telefonda kart iç genişliği ~131 px: en uzun sayı ("13.889", 36 px'te ~90 px) + 32 px ikon - sığar. Daha uzun sayı gelecekse önce ölç (§9.2), sonra yaz. + sığar. Daha uzun sayı gelecekse önce ölç (§9.2), sonra yaz. Ölçülen sınır: 36 px'te + "1.618.988" 123 px — ikonun altına girer. 6–7 haneli sayı birimle kısalır: 245.300 → + `245` + birim `bin`, 1.618.988 → `1,62` + birim `milyon` (bölüm sayfasındaki `kartSayisi`). +- Kullanıldığı yerler: üniversite ve bölüm sayfası (bölümde dört kart: Program, Devlet, + Vakıf, En iyi sıralama; `grid grid-cols-2 gap-4 lg:grid-cols-4`, haritası olmadığı için). - Tıklanabilir kart (ör. Türkiye sırası → sıralama sayfası): kartın tamamı link olur (`after:absolute after:inset-0` gerilmiş link), iç kaynak linki `relative z-10` ile üstte kalır; hover §2.1. @@ -341,7 +345,14 @@ opaklık. adın altındaki satıra iner: `mt-1.5 flex items-center gap-1.5 text-sm text-slate-500 sm:hidden` içinde değer `font-semibold text-slate-900`. - Tür çipi: `rounded-full bg-slate-100 px-2.5 py-0.5 text-xs font-semibold text-slate-700`. -- Satır butonları varsayılan `size-11` (44 px); `size-8` gibi küçültme verilmez. +- Satır butonları varsayılan `size-11` (44 px); `size-8` gibi küçültme verilmez. Telefonda + iki buton alt alta durur (`inline-flex flex-col items-center gap-2 sm:flex-row`, sütun + `w-14 sm:w-28`): yan yana 96 px yiyip adı 157 px'e sıkıştırıyordu, alt alta 273 px kalıyor + (24 Eyl 2026 ölçümü). +- Satırda logo varsa (bölüm sayfası) `UniLogo boy="sm"` adın solunda, `items-start`. +- Program varyantı ("… (İngilizce) (Burslu)") adın altında ayrı satır + `mt-0.5 block text-sm leading-snug text-slate-600`; tür çipine yapıştırılıp kesilmez. +- Tür çipi devlet/vakıf için de aynı slate çip; vakıf violet değildir (§2.2). - Tablo başlığı `h-12 text-sm font-semibold text-slate-500`; hücre dolgusu `py-4 pl-4 sm:pl-5`. ## 5. Yerleşim @@ -378,6 +389,11 @@ kartlarla aynı hizada: yorar), form alanları, uzun rehber metni. - Alt sayfa (§4.10) içeriği her ekranda ortalıdır. +### 5.2.1 Sayfalama — `Sayfalama`, `sayfalama.tsx` +Haplar `h-11` (44 px), hover §2.1. Telefonda (sm altı) tek satır kalsın diye yalnız Önceki · +aktif sayfa · Sonraki görünür; diğer numaralar `hidden sm:inline-flex` ile HTML'de durur +(tarayıcı botları tüm sayfaları görür). 375 px'te 7 sayfalık şerit iki satıra kırılıyordu. + ### 5.3 Verimli ve havadar - İlişkili öğeler yan yana dizilir (iki sütun) — tek sütunda alt alta uzayan panel yapılmaz. - Yan yana dizmek "hepsini tek şeride sıkıştırmak" değildir: 23 Eyl'de denenen yatay "bilgi @@ -470,7 +486,11 @@ Bu dilde sorunların çoğu koddan görünmez (taşma, sığma, hizalanma). Tasa ### 9.4 Site taraması için denetim uyarıları `pnpm tasarim:denetim` bu dilin eski izlerini **uyarı** olarak listeler (lint'i kırmaz, dönüşüm listesidir): `minik-yazi` (10–11 px), `buyuk-harf-etiket` (uppercase + tracking-wide), -`mavi-hover` (§2.1; liste satırı adı istisna), `iri-sayi-tabular` (Cal Sans + tabular-nums). +`mavi-hover` (§2.1), `iri-sayi-tabular` (Cal Sans + tabular-nums). +`mavi-hover` §4.15 kanonik liste satırı adını (`text-base font-semibold leading-snug` + +`hover:text-primary hover:underline` aynı satırda) kendiliğinden atlar — istisna uyarı +vermez (Bilal'in kararı, 24 Eyl 2026). Eski `truncate text-sm` adlar yine uyarır; önce §4.15'e +çevrilir. Bir ekranı dönüştüren ajan o ekrandaki uyarıları sıfırlar ya da neden kaldığını yazar. ### 9.5 Bitirmeden diff --git a/scripts/tasarim-denetim.ts b/scripts/tasarim-denetim.ts index 810ead0..96dbabb 100644 --- a/scripts/tasarim-denetim.ts +++ b/scripts/tasarim-denetim.ts @@ -191,7 +191,20 @@ const KURALLAR: Kural[] = [ ad: "mavi-hover", seviye: "uyari", mesaj: "Hover rengi tek turuncu (orange-500); liste satırındaki ad linkiyse istisna, mavi kalır (DESIGN.md §2.1)", - bul: regexKurali(/\b(?:group-)?hover:(?:text|border|shadow)-primary\b/), + // §2.1 istisnası: §4.15 kanonik liste satırı adı (sarar, text-base + // font-semibold leading-snug + hover:text-primary hover:underline) mavi + // kalır. Eski truncate/text-sm adlar istisnaya girmez — önce §4.15'e + // çevrilir. (Bilal'in kararı, 24 Eyl 2026: istisna uyarı vermesin.) + bul: (icerik) => + regexKurali(/\b(?:group-)?hover:(?:text|border|shadow)-primary\b/)(icerik).filter((e) => { + const bas = icerik.lastIndexOf("\n", e.index) + 1; + const son = icerik.indexOf("\n", e.index); + const satir = icerik.slice(bas, son === -1 ? undefined : son); + return !( + /\btext-base font-semibold leading-snug\b/.test(satir) && + /\bhover:text-primary hover:underline\b/.test(satir) + ); + }), }, { ad: "iri-sayi-tabular", diff --git a/src/components/ozet-kart.tsx b/src/components/ozet-kart.tsx new file mode 100644 index 0000000..4a3ec4d --- /dev/null +++ b/src/components/ozet-kart.tsx @@ -0,0 +1,58 @@ +import type { ReactNode } from "react"; +import type { LucideIcon } from "lucide-react"; + +/** + * Özet kartı (DESIGN.md §4.8 — Apple Fitness özet kartlarının düzeni, + * sitenin ölçüleriyle): kartın en baskın öğesi iri sayı; ikonu sağında, + * küçük ikon kutusunda (§4.4 ikon kutusunun küçük hali). Altında başlık ve + * tek satır açıklama. Telefonda kart iç genişliği ~131 px: en uzun sayı + * ("13.889", text-4xl ≈ 90 px) + size-8 ikon kutusu sığar; sm'den itibaren + * text-5xl. Üniversite ve bölüm sayfalarının tek kaynağı. `ek` verilirse + * açıklamanın altında durur (ör. Türkiye sırasındaki piksel şeridi). dl + * anlamı için dt DOM'da önce gelir, görsel sıra `order` ile kurulur; çağıran + * kartları bir `
` içine koyar. + */ +export function OzetKart({ + ikon: Ikon, + etiket, + deger, + birim, + alt, + ek, + className, +}: { + ikon: LucideIcon; + etiket: string; + deger: ReactNode; + birim?: string; + alt: ReactNode; + ek?: ReactNode; + className?: string; +}) { + return ( +
+
+ {etiket} +
+
+ + + {deger} + + {birim ? ( + + {birim} + + ) : null} + + + + +
+
{alt}
+ {ek ?
{ek}
: null} +
+ ); +} diff --git a/src/features/katalog/components/bolum-icerik.tsx b/src/features/katalog/components/bolum-icerik.tsx index 1459913..4da8337 100644 --- a/src/features/katalog/components/bolum-icerik.tsx +++ b/src/features/katalog/components/bolum-icerik.tsx @@ -1,7 +1,15 @@ import type { Metadata } from "next"; import Link from "next/link"; import { notFound } from "next/navigation"; -import { ChevronRight } from "lucide-react"; +import { + Building2, + Calculator, + ChevronRight, + GraduationCap, + Landmark, + LayoutList, + Target, +} from "lucide-react"; import { getAllBolumler, getAllBolumSlugs, @@ -23,6 +31,8 @@ import { import { BolumProgramListesi } from "./bolum-program-listesi"; import { VeriKaynagi } from "./veri-kaynagi"; import { NavBaslik } from "./nav-baslik"; +import { OzetKart } from "@/components/ozet-kart"; +import { PagePixelDivider } from "@/components/pixel-decor"; import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form"; import { SiteFooter } from "@/components/site-footer"; import { Skeleton } from "@/components/ui/skeleton"; @@ -35,6 +45,32 @@ import { const sayi = (n: number) => n.toLocaleString("tr-TR"); +/** + * Özet kartında iri sayı telefonda ~94 px'e sığmalı (DESIGN.md §4.8): + * 36 px Cal Sans'ta "1.618.988" 123 px ölçüldü, ikonun altına giriyor. 6 ve + * 7 haneli sıralar birimle kısalır: 245.300 → "245" + "bin", + * 1.618.988 → "1,62" + "milyon". + */ +function kartSayisi(n: number): { deger: string; birim?: string } { + // 999.500+ "1.000 bin" olmasın diye milyona geçer + if (n >= 999_500) { + return { + deger: (n / 1e6).toLocaleString("tr-TR", { maximumFractionDigits: 2 }), + birim: "milyon", + }; + } + if (n >= 100_000) return { deger: sayi(Math.round(n / 1000)), birim: "bin" }; + return { deger: sayi(n) }; +} + +/** Lisans puan türünün kimlik hapındaki adı (DB: SAYISAL, EŞİT AĞIRLIK…). */ +const PUAN_TURU_HAP: Record = { + SAYISAL: "Sayısal puan", + "EŞİT AĞIRLIK": "Eşit ağırlık puanı", + SÖZEL: "Sözel puan", + DİL: "Dil puanı", +}; + export function bolumToplamSayfa(programSayisi: number): number { return Math.max(1, Math.ceil(programSayisi / SAYFA_BOYU)); } @@ -179,76 +215,126 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) { // adı geçer (ziyaretçi Google'dan doğrudan /sayfa/N'ye iniyor). const ctaBaslik = `Sıralaman ${bolum.ad} tabanlarının neresinde?`; + // Kimlik hapları (DESIGN.md §4.12): düzey + lisans puan türleri. TYT + // puanı önlisansın kendisi olduğundan ayrı hap açmaz. + const puanTurleri = [ + ...new Set(bolum.lisansProgramlari.map((p) => p.tur)), + ].flatMap((t) => (PUAN_TURU_HAP[t] ? [PUAN_TURU_HAP[t]] : [])); + const duzeyler = [ + ...(bolum.lisansProgramlari.length > 0 ? ["Lisans"] : []), + ...(bolum.onlisansProgramlari.length > 0 ? ["Önlisans"] : []), + ]; + // Özet kartları: devlet ve vakıf ayrı sayılır; KKTC ve yurt dışı + // programları toplamda var ama iki karta da girmez (vakıf sayılmaz). + const devletUni = new Set(); + const vakifUni = new Set(); + let vakifProgram = 0; + for (const p of tumProgramlar) { + if (p.unitur === "DEVLET") devletUni.add(p.universite); + else if (p.unitur === "VAKIF" || p.unitur === "VAKIF MYO") { + vakifProgram += 1; + vakifUni.add(p.universite); + } + } + const { stats } = bolum; + const hapSinifi = + "inline-flex h-10 items-center gap-2 rounded-full border border-slate-200 bg-white px-4 text-sm font-semibold text-slate-800"; + return ( <> -
- +
+ - + + Ana Sayfa + + + + Bölümler + + + {ilkSayfa ? ( + {bolum.ad} + ) : ( + <> + + {bolum.ad} + + + Sayfa {sayfa} + + )} + - {/* Başlık üst çubuğun altına girince üst çubukta bölüm adı belirir */} - -
+ {/* Başlık üst çubuğun altına girince üst çubukta bölüm adı belirir + (logosu yok — DESIGN.md §4.14). Geniş ekran (lg) altında başlık + bloğu ortalı (§5.2). */} +

+ className="mt-6 text-center font-heading text-3xl font-bold leading-tight text-slate-900 sm:text-5xl lg:text-left" + > {bolum.ad} {ilkSayfa ? "" : ` — Sayfa ${sayfa}`} {!ilkSayfa && aralik ? ( <> {" "} - + {aralik.charAt(0).toLocaleUpperCase("tr-TR") + aralik.slice(1)} ) : null}

+ + {duzeyler.length > 0 || puanTurleri.length > 0 ? ( +

+ {duzeyler.map((d) => ( + + + {d} + + ))} + {puanTurleri.map((t) => ( + + + {t} + + ))} +

+ ) : null} + {ilkSayfa ? ( -

- {sayi(tumProgramlar.length)} üniversite programının {SON_YIL} taban - puanlarını, başarı sıralamalarını ve kontenjanlarını incele. +

+ {bolum.ad} bölümünü açan {sayi(tumProgramlar.length)} üniversite + programının {SON_YIL} taban puanlarını, başarı sıralamalarını ve + kontenjanlarını incele.{" "} + + {TERCIH_YILI} tabanları, yerleştirme sonuçları açıklandığında + netleşir. +

) : ( -

+

{bolum.ad} taban puanları tablosunun {sayfa}. sayfası ( {sayi(tumProgramlar.length)} programın{" "} {sayi((sayfa - 1) * SAYFA_BOYU + 1)}– @@ -257,54 +343,99 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) { {aralik ? `: ${aralik}` : ""}. Listenin başı{" "} ilk sayfada .

)} -
- {/* Tek sayfada çıkan ziyaretçi alttaki karta inmiyor: kompakt sıra girişi - başlık bloğunun hemen altında, listenin üstünde. Alttaki kart kalır. */} -
- -
- -
-
-

- Üniversite programları -

- - {sayi(tumProgramlar.length)} program - + {/* Tek sayfada çıkan ziyaretçi alttaki karta inmiyor: kompakt sıra + girişi özet paragrafın hemen altında, listenin üstünde. Alttaki + kart kalır. */} +
+
- ({ - ...program, - universiteSlug: uniSlugFromAd(program.universite), - }))} - bazAd={bolum.ad} - tabanYol={`/bolum/${slug}`} - sayfa={sayfa} - /> - -
+ {/* Sayfada en çok dört özet sayı (DESIGN.md §1.1, §4.8). Derin + sayfalarda tekrar edilmez — üniversite sayfasıyla aynı. */} + {ilkSayfa ? ( +
+ + 0 + ? `${sayi(devletUni.size)} üniversitede` + : "bu bölümde yok" + } + /> + 0 + ? `${sayi(vakifUni.size)} üniversitede` + : "bu bölümde yok" + } + /> + +
+ ) : null} - {/* Üniversite sayfasıyla aynı konum ve aynı kart: liste + sayfalama altı */} -
- -
+ + +
+
+

+ Üniversite programları +

+

+ {sayi(tumProgramlar.length)} program, taban sırasına göre +

+
+ +
+ ({ + ...program, + universiteSlug: uniSlugFromAd(program.universite), + }))} + bazAd={bolum.ad} + tabanYol={`/bolum/${slug}`} + sayfa={sayfa} + /> +
+ +
+ + {/* Üniversite sayfasıyla aynı konum ve aynı kart: liste + sayfalama altı */} +
+ +
@@ -313,16 +444,20 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) { export function BolumIcerikSkeleton() { return ( -
- -
- - +
+ + + + +
+ {Array.from({ length: 4 }, (_, i) => ( + + ))}
-
+
{Array.from({ length: 5 }, (_, i) => ( - + ))}
diff --git a/src/features/katalog/components/bolum-listesi.tsx b/src/features/katalog/components/bolum-listesi.tsx index bb6af6f..3096042 100644 --- a/src/features/katalog/components/bolum-listesi.tsx +++ b/src/features/katalog/components/bolum-listesi.tsx @@ -15,7 +15,7 @@ function BolumLinkleri({ bolumler }: { bolumler: BolumOzet[] }) {
  • {b.ad} diff --git a/src/features/katalog/components/bolum-program-listesi.tsx b/src/features/katalog/components/bolum-program-listesi.tsx index 76a0d0c..b4e2310 100644 --- a/src/features/katalog/components/bolum-program-listesi.tsx +++ b/src/features/katalog/components/bolum-program-listesi.tsx @@ -58,7 +58,7 @@ function SiralamaDugmesi({ aktif && yon === "artan" ? "azalan" : "artan" } sırala`} aria-pressed={aktif} - className="inline-flex min-h-11 cursor-pointer items-center gap-1 rounded-full px-1.5 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" + className="inline-flex min-h-11 cursor-pointer items-center gap-1 rounded-full px-1.5 text-sm font-semibold text-slate-500 transition-colors hover:text-orange-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" > {etiket} @@ -66,7 +66,7 @@ function SiralamaDugmesi({ className={`size-3 ${ aktif && yon === "artan" ? "text-primary" - : "text-muted-foreground/40" + : "text-slate-300" }`} strokeWidth={aktif && yon === "artan" ? 3 : 2} /> @@ -74,7 +74,7 @@ function SiralamaDugmesi({ className={`size-3 ${ aktif && yon === "azalan" ? "text-primary" - : "text-muted-foreground/40" + : "text-slate-300" }`} strokeWidth={aktif && yon === "azalan" ? 3 : 2} /> @@ -154,14 +154,21 @@ export function BolumProgramListesi({ ref={listeRef} className="scroll-mt-24 overflow-hidden rounded-2xl border border-slate-200 bg-white" > + {/* Şişko ve sade (DESIGN.md §4.15): üniversite adı kesilmez, sarar; + satır butonları 44 px. 375 px'te yalnız ad + butonlar sütun + olarak kalır; taban sıra adın altındaki satıra iner. Sıralama + düğmeleri sütun başlıklarıyla birlikte sm'den itibaren görünür — + telefonda liste zaten taban sırasına göre dizili. */} - - Üniversite - + + + Üniversite + + Son {VERI_YIL_SAYISI} yıl - + - + - + - + Netler ve listeye ekle @@ -194,12 +201,13 @@ export function BolumProgramListesi({ {gosterilenProgramlar.map((p) => { const uniAd = trBaslikDuzeni(p.universite); const varyant = p.isim.trim() !== bazAd ? p.isim.trim() : null; - const devlet = p.unitur === "DEVLET"; + const tabanMetni = + p.siraSon != null ? `~${sayi(p.siraSon)}.` : "—"; return ( - - + + {p.universiteSlug ? ( ) : null} @@ -207,71 +215,71 @@ export function BolumProgramListesi({ {p.universiteSlug ? ( {uniAd} ) : ( - + {uniAd} )} - + {varyant ? ( + + {varyant} + + ) : null} + {p.unitur ? ( - + {UNITUR_ETIKET[p.unitur]?.cip ?? trBaslikDuzeni(p.unitur)} ) : null} - - {p.il ? trBaslikDuzeni(p.il) : null} - {varyant ? `${p.il ? " · " : ""}${varyant}` : ""} - - {p.sure ? ( - - · {p.sure} yıl + {p.il ? ( + + {trBaslikDuzeni(p.il)} ) : null} + {p.sure ? ( + {p.sure} yıl + ) : null} + + + Taban sıra + + {tabanMetni} + + - + - + {p.puanSon != null ? p.puanSon.toFixed(2).replace(".", ",") : "—"} - + {p.kontenjanSon != null ? `${sayi(p.kontenjanSon)} kişi` : "—"} - - + + - {p.siraSon != null ? `~${sayi(p.siraSon)}.` : "—"} + {tabanMetni} - - + + - + diff --git a/src/features/katalog/components/program-netleri.tsx b/src/features/katalog/components/program-netleri.tsx index 02570ad..e2e08fd 100644 --- a/src/features/katalog/components/program-netleri.tsx +++ b/src/features/katalog/components/program-netleri.tsx @@ -96,7 +96,7 @@ export function ProgramNetleriButonu({ type="button" title="Son yerleşenin netleri" onClick={(e) => e.stopPropagation()} - className={`inline-flex size-11 min-h-11 min-w-11 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-white text-slate-500 transition-[background-color,border-color,color,transform] duration-150 hover:border-primary hover:bg-primary/5 hover:text-primary active:scale-[0.92] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${className}`} + className={`inline-flex size-11 min-h-11 min-w-11 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-white text-slate-500 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.92] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${className}`} > {isim} — son yerleşenin netleri diff --git a/src/features/katalog/components/sayfalama.tsx b/src/features/katalog/components/sayfalama.tsx index bf2c546..8b2a033 100644 --- a/src/features/katalog/components/sayfalama.tsx +++ b/src/features/katalog/components/sayfalama.tsx @@ -95,7 +95,9 @@ export function Sayfalama({ ); - // 7'den çok sayfada pencere: 1 … (c-1) c (c+1) … son + // 7'den çok sayfada pencere: 1 … (c-1) c (c+1) … son. Telefonda + // (sm altı) tek satır kalsın diye yalnız Önceki · aktif · Sonraki görünür; + // diğer numaralar HTML'de durur (tarayıcı botları tüm sayfaları görür). const numaralar: (number | "...")[] = []; if (toplamSayfa <= 7) { for (let n = 1; n <= toplamSayfa; n++) numaralar.push(n); @@ -116,12 +118,12 @@ export function Sayfalama({ return (
    - + Üniversite - + Son {VERI_YIL_SAYISI} yıl @@ -192,7 +205,7 @@ export function BolumProgramListesi({ onSirala={sirala} /> - + Netler ve listeye ekle @@ -206,7 +219,7 @@ export function BolumProgramListesi({ return ( - + {p.universiteSlug ? ( @@ -215,23 +228,23 @@ export function BolumProgramListesi({ {p.universiteSlug ? ( {uniAd} ) : ( - + {uniAd} )} {varyant ? ( - + {varyant} ) : null} - + {p.unitur ? ( - + {UNITUR_ETIKET[p.unitur]?.cip ?? trBaslikDuzeni(p.unitur)} @@ -245,7 +258,7 @@ export function BolumProgramListesi({ {p.sure} yıl ) : null} - + Taban sıra {tabanMetni} @@ -272,8 +285,8 @@ export function BolumProgramListesi({ {tabanMetni} - - + + - + Program - + Son {VERI_YIL_SAYISI} yıl - + {SON_YIL} puan - + Kontenjan - + Taban sıra - + Netler ve listeye ekle @@ -76,28 +88,28 @@ export function UniversiteProgramTablosu({ const tabanMetni = taban != null ? `~${sayi(taban)}.` : "—"; return ( - + {p.bolumSlug ? ( {p.isim.trim()} ) : ( - + {p.isim.trim()} )} - - + + {TUR_ETIKET[p.tur] ?? p.tur} {p.sure ? ( {p.sure} yıl ) : null} - + Taban sıra {tabanMetni} @@ -124,8 +136,8 @@ export function UniversiteProgramTablosu({ {tabanMetni} - - + + + {DILIMLER.map((d) => { const aktif = aktifDilim === d.key; return ( {aktif ? ( ) : null} - - - {d.etiket} - + + + + {d.etiket} + + {sonuclar.toplam[d.key].toLocaleString("tr-TR")} @@ -236,7 +257,7 @@ export function ProgramTablosu({ const toplam = sonuclar.toplam[d.key]; return ( -
    +
    {satirlar.length === 0 ? (

    Bu dilimde filtrene uyan program bulunamadı. @@ -244,17 +265,31 @@ export function ProgramTablosu({ ) : (

    - - Risk - Program - + + + Risk + + + Program + + Son {VERI_YIL_SAYISI} yıl - + Kontenjan - Taban sıra - + + Taban sıra + + Netler ve listeye ekle @@ -273,17 +308,19 @@ export function ProgramTablosu({ )} {satirlar.length < toplam ? ( -
    - - + + + {satirlar.length.toLocaleString("tr-TR")} /{" "} {toplam.toLocaleString("tr-TR")} program @@ -297,6 +334,27 @@ export function ProgramTablosu({ ); } +// Risk etiketinin metin rengi (§4.5): nokta + renkli metin, renk tek başına değil. +const RISK_METIN: Record = { + guvenli: "text-emerald-600", + "az-riskli": "text-amber-600", + riskli: "text-red-600", +}; + +function RiskEtiketi({ risk }: { risk: RiskSeviyesi }) { + return ( + + + {RISK_ETIKET[risk]} + + ); +} + function ProgramSatiri({ program: p, adaySira, @@ -309,65 +367,74 @@ function ProgramSatiri({ const taban = programEtkinSira(p); const risk = riskHesapla(taban, adaySira); const devlet = p.unitur === "DEVLET"; + const tabanMetni = taban != null ? `~${taban.toLocaleString("tr-TR")}.` : "—"; + const bosKaldi = + p.kontenjanSon != null && + p.yerlesenSon != null && + p.yerlesenSon < p.kontenjanSon; + // Şişko ve sade (DESIGN.md §4.15): program adı kesilmez, üniversite adı + // altında ayrı satır; telefonda risk ve taban sıra adın altına iner, + // butonlar alt alta 44 px. return ( - - {risk ? ( - - - {RISK_ETIKET[risk]} - - ) : null} + + {risk ? : null} - - + + e.stopPropagation()} > {p.isim} - - + e.stopPropagation()} > - {devlet ? "Devlet" : "Vakıf"} - - - e.stopPropagation()} - > - {p.universite} - - {p.il ? ` · ${p.il.toLocaleLowerCase("tr-TR")}` : ""} - - {p.sure ? ( - · {p.sure} yıl + {trBaslikDuzeni(p.universite)} + + + + {devlet ? "Devlet" : "Vakıf"} + {p.il ? ( + {trBaslikDuzeni(p.il)} ) : null} + {p.sure ? ( + {p.sure} yıl + ) : null} + {bosKaldi ? ( + + Boş kaldı + + ) : null} + + + {risk ? : null} + + Taban sıra + + {tabanMetni} + + + - + {p.kontenjanSon != null ? ( - - {p.yerlesenSon != null && p.yerlesenSon < p.kontenjanSon ? ( - - boş kaldı + + {p.kontenjanSon.toLocaleString("tr-TR")} kişi + {bosKaldi ? ( + + Boş kaldı ) : null} - {p.kontenjanSon.toLocaleString("tr-TR")} kişi ) : ( "—" )} - - + + - {taban != null ? `~${taban.toLocaleString("tr-TR")}.` : "—"} + {tabanMetni} - - + + diff --git a/src/features/liste/components/tercih-haritasi.tsx b/src/features/liste/components/tercih-haritasi.tsx index 0af9813..187b3e5 100644 --- a/src/features/liste/components/tercih-haritasi.tsx +++ b/src/features/liste/components/tercih-haritasi.tsx @@ -513,10 +513,10 @@ export function TercihHaritasi({ - ) : null} - - - Kapat - -
    - + {/* Alt sayfa başlığı (DESIGN.md §4.10): ortalı başlık + bağlam; + çarpı §4.3 kanonik sınıf setinde, temizle düğmesi aynı ölçüde + solda. */} + + {liste.length > 0 ? ( + + ) : null} + + + Kapat + + + Kendi Listem + +

    + {liste.length} / {MANUEL_LISTE_MAX} tercih +

    -
      - {( - [ - ["guvenli", "yeşil güvenli"], - ["az-riskli", "turuncu az riskli"], - ["riskli", "kırmızı riskli"], - ] as const - ).map(([seviye, aciklama]) => ( -
    • - - {aciklama} -
    • - ))} -
    {liste.length === 0 ? (
    @@ -196,10 +181,10 @@ export function ListeCekmecesi() { strokeWidth={1.25} aria-hidden /> -

    +

    Henüz program seçmedin

    -

    +

    Sonuç sayfasından program ekleyerek listeni doldurmaya başla.

    @@ -228,29 +213,32 @@ export function ListeCekmecesi() { ) : null}
    - + {/* C2 kalıcı satır: girişsiz kullanıcıya listenin uçucu olduğu hatırlatılır; giriş doğal "kaydet" gerekçesi olarak sunulur. */} {girisli === false ? ( -
    -

    +

    +

    {/* O anki işletim sisteminin logosu — "bu tarayıcıda"yı somutlaştırır */} - + Listen yalnızca bu tarayıcıda. - {" "} - Giriş yap; her cihazdan eriş{" "} - + + + {" "} + Giriş yap; her cihazdan eriş{" "} + +

    Giriş yap, kaydet - +
    ) : null} {/* Ana sayfadaki final CTA panelinin kompakt hâli: koyu primary zemin + kenarlara doğru yoğunlaşan piksel dokusu (PixelField) */} -
    + {/* Telefonda alt alta (başlık tek satır, buton tam genişlik): + yan yana dizince başlık üç satıra kırılıp listenin yerini + yiyordu (24 Eyl 2026, 375 px ölçümü). */} +
    -

    +

    {riskEksik ? "Risk analizi sıralamanı bekliyor" : "Risk analizini biz yaptık"}

    -

    +

    {riskEksik ? "Riskleri gösterelim; istersen 24'lük listeni de kur (3 kredi)." : "Her seçimin yerleşme riski hazır — son kararı sen ver."} @@ -287,7 +278,7 @@ export function ListeCekmecesi() { {riskEksik ? ( - + {sira} - e.stopPropagation()} // kaydırmayı tetiklemesin - className="block truncate text-sm font-semibold hover:text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring" + className={SATIR_AD} > {tercih.isim} - + + e.stopPropagation()} // kaydırmayı tetiklemesin title={`${tercih.universite} taban puanları sayfası`} - className="hover:text-primary hover:underline" + className={`min-w-0 ${SATIR_IKINCIL_LINK}`} > - {tercih.universite} + {trBaslikDuzeni(tercih.universite)} - {tercih.il ? ` · ${tercih.il.toLocaleLowerCase("tr-TR")}` : ""} + {tercih.risk || tercih.efektifSira != null ? ( + + {tercih.risk ? ( + + + {RISK_ETIKET[tercih.risk]} + + ) : null} + {tercih.efektifSira != null ? ( + + Taban sıra + + ~{tercih.efektifSira.toLocaleString("tr-TR")}. + + + ) : null} + + ) : null} - {tercih.efektifSira != null ? ( - - ~{tercih.efektifSira.toLocaleString("tr-TR")}. - - ) : null} - {tercih.risk ? ( - - - {RISK_ETIKET[tercih.risk]} - - ) : null} diff --git a/src/features/liste/components/listem-butonu.tsx b/src/features/liste/components/listem-butonu.tsx index e11c60a..ebf079c 100644 --- a/src/features/liste/components/listem-butonu.tsx +++ b/src/features/liste/components/listem-butonu.tsx @@ -46,7 +46,7 @@ export function ListemButonu() { initial={azMotion ? false : { scale: 0.6 }} animate={{ scale: 1 }} transition={{ type: "spring", duration: 0.4, bounce: 0.45 }} - className="flex size-5 items-center justify-center rounded-full bg-orange-500 font-heading text-[11px] font-bold tabular-nums text-white" + className="flex h-6 min-w-6 items-center justify-center rounded-full bg-orange-500 px-1.5 font-heading text-xs font-bold text-white" > {liste.length} From 309fb73a34ca79d0b1b44e3c2b27a689c5de7795 Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Thu, 24 Sep 2026 00:57:27 +0300 Subject: [PATCH 04/23] =?UTF-8?q?feat(tasarim):=20rapor=20listesi=20ve=20t?= =?UTF-8?q?ad=C4=B1ml=C4=B1k=20sat=C4=B1r=C4=B1=20"=C5=9Fi=C5=9Fko=20ve=20?= =?UTF-8?q?sade"=20dile=20g=C3=B6re?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Risk etiketi src/components/risk-etiketi.tsx'e çıkarıldı; /sonuc, çekmece ve rapor listesi oradan kullanıyor - Rapor satırı §4.15: ad sarar, üniversite adı ayrı satırda, telefonda risk + taban sıra adın altında, logo gizli (satır 214 → 152 px), aç/kapa düğmesi 44 px, hover tek turuncu - Açılan ayrıntı: 16 px metin, 14 px koyu etiket, rounded-xl iç kutular - Tadımlık satırı aynı satır düzenine geçti - Tercih değişti penceresi, liste gövdesi ve yazdırma sayfasında 10-11 px / büyük harfli etiketler kalktı Co-Authored-By: Claude Opus 5.5 --- docs/tasarim/DESIGN.md | 7 +- src/components/risk-etiketi.tsx | 32 +++ .../liste/components/liste-cekmecesi.tsx | 27 +-- .../liste/components/program-tablosu.tsx | 40 +--- .../rapor/components/listem-govde.tsx | 2 +- .../rapor/components/rapor-listesi.tsx | 216 ++++++++++-------- .../rapor/components/rapor-yazdir.tsx | 2 +- .../rapor/components/tadimlik-satiri.tsx | 44 ++-- .../components/tercih-degisti-modali.tsx | 2 +- 9 files changed, 186 insertions(+), 186 deletions(-) create mode 100644 src/components/risk-etiketi.tsx diff --git a/docs/tasarim/DESIGN.md b/docs/tasarim/DESIGN.md index e77bd02..ec8c9e5 100644 --- a/docs/tasarim/DESIGN.md +++ b/docs/tasarim/DESIGN.md @@ -213,7 +213,8 @@ Alt sayfada (`Drawer`) aynı sınıf seti `DrawerClose` üstünde kullanılır ( - İkon kutusu: `flex size-11 items-center justify-center rounded-xl bg-primary/10` içinde `size-5 text-primary` ikon. Kart içinde sayının yanında küçük hâli: `size-8 … sm:size-10` + `size-4 sm:size-5` ikon (§4.8). - Gölge: `shadow-sm` varsayılan; hover'da `shadow-lg shadow-orange-500/5`. Başka gölge yok. -### 4.5 Risk çipi / etiketi +### 4.5 Risk çipi / etiketi — `RiskEtiketi`, `src/components/risk-etiketi.tsx` +Tablolar, çekmece ve rapor listesi `` kullanır; elle yazılmaz. Nokta + metin: `` yanında `text-xs font-semibold text-emerald-600`; metin `RISK_ETIKET`'ten. Renk tek başına asla. ### 4.6 Form @@ -360,6 +361,10 @@ tablosu bu sabitleri kullanır, sınıfı elle yazmaz. iki buton alt alta durur (`inline-flex flex-col items-center gap-2 sm:flex-row`, sütun `w-14 sm:w-28`): yan yana 96 px yiyip adı 157 px'e sıkıştırıyordu, alt alta 273 px kalıyor (24 Eyl 2026 ölçümü). +- Rapor listesinde (`rapor-listesi.tsx`) telefonda logo gizlenir (`hidden sm:block`): solda + sıra numarası zaten satırı çapalıyor, logo adı üç satıra sıkıştırıyordu (satır 214 → + 152 px). Açılan ayrıntı iç kutuları `rounded-xl`, etiket `text-sm font-semibold + text-slate-900`, metin `text-base`. - Satırda logo varsa (bölüm sayfası) `UniLogo boy="sm"` adın solunda, `items-start`. - Program varyantı ("… (İngilizce) (Burslu)") adın altında ayrı satır `mt-0.5 block text-sm leading-snug text-slate-600`; tür çipine yapıştırılıp kesilmez. diff --git a/src/components/risk-etiketi.tsx b/src/components/risk-etiketi.tsx new file mode 100644 index 0000000..0398422 --- /dev/null +++ b/src/components/risk-etiketi.tsx @@ -0,0 +1,32 @@ +import { RISK_ETIKET, type RiskSeviyesi } from "@/lib/risk"; + +/** + * Risk etiketi (DESIGN.md §4.5) — tek kaynak: renkli nokta + aynı renkte + * metin. Renk hiçbir yerde tek başına durmaz; metin RISK_ETIKET'ten gelir. + * /sonuc tablosu, liste çekmecesi ve rapor listesi buradan kullanır. + */ +export const RISK_NOKTA: Record = { + guvenli: "bg-emerald-500", + "az-riskli": "bg-amber-500", + riskli: "bg-red-500", +}; + +const RISK_METIN: Record = { + guvenli: "text-emerald-600", + "az-riskli": "text-amber-600", + riskli: "text-red-600", +}; + +export function RiskEtiketi({ risk }: { risk: RiskSeviyesi }) { + return ( + + + {RISK_ETIKET[risk]} + + ); +} diff --git a/src/features/liste/components/liste-cekmecesi.tsx b/src/features/liste/components/liste-cekmecesi.tsx index 7f22d20..83240b6 100644 --- a/src/features/liste/components/liste-cekmecesi.tsx +++ b/src/features/liste/components/liste-cekmecesi.tsx @@ -16,6 +16,7 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; import { PixelField } from "@/components/pixel-decor"; import { UniLogo } from "@/components/uni-logo"; +import { RiskEtiketi } from "@/components/risk-etiketi"; import { Button } from "@/components/ui/button"; import { Drawer, @@ -42,7 +43,6 @@ import { useTransform, type PanInfo, } from "motion/react"; -import { RISK_ETIKET, type RiskSeviyesi } from "@/lib/risk"; import { bolumBazAdi, trBaslikDuzeni, turkishSlugify } from "@/lib/slug"; import { SATIR_AD, SATIR_IKINCIL_LINK } from "@/components/liste-satiri"; import { olay } from "@/lib/analitik"; @@ -78,19 +78,6 @@ function uniHref(ad: string): string { return `/universite/${turkishSlugify(ad.replace(/\s*\([^)]*\)\s*$/, "").trim())}`; } -// Risk renk dili ürünün geri kalanıyla birebir aynı (bkz. rapor-listesi) -const RISK_NOKTA: Record = { - guvenli: "bg-emerald-500", - "az-riskli": "bg-amber-500", - riskli: "bg-red-500", -}; -// §4.5: nokta + renkli metin; renk tek başına anlam taşımaz -const RISK_METIN: Record = { - guvenli: "text-emerald-600", - "az-riskli": "text-amber-600", - riskli: "text-red-600", -}; - export function ListeCekmecesi() { const router = useRouter(); const acik = useCekmeceAcik(); @@ -407,17 +394,7 @@ function CekmeceSatiri({ {tercih.risk || tercih.efektifSira != null ? ( - {tercih.risk ? ( - - - {RISK_ETIKET[tercih.risk]} - - ) : null} + {tercih.risk ? : null} {tercih.efektifSira != null ? ( Taban sıra diff --git a/src/features/liste/components/program-tablosu.tsx b/src/features/liste/components/program-tablosu.tsx index 73b90e0..3dc2fd0 100644 --- a/src/features/liste/components/program-tablosu.tsx +++ b/src/features/liste/components/program-tablosu.tsx @@ -20,12 +20,7 @@ import type { PuanTuruKey, RankResultsListe, } from "@/types/yokatlas"; -import { - DILIM_ETIKET, - RISK_ETIKET, - riskHesapla, - type RiskSeviyesi, -} from "@/lib/risk"; +import { DILIM_ETIKET, riskHesapla } from "@/lib/risk"; import { bolumSayfaSlug, trBaslikDuzeni, uniSayfaSlug } from "@/lib/slug"; import { VERI_YIL_SAYISI } from "@/lib/veri-yillari"; import { @@ -46,6 +41,7 @@ import { ProgramEkleButonu } from "@/features/liste/components/program-ekle-buto import { preloadTercihHaritasi } from "@/features/liste/components/tercih-haritasi-lazy"; import { ProgramNetleriButonu } from "@/features/katalog/components/program-netleri"; import { UniLogo } from "@/components/uni-logo"; +import { RiskEtiketi } from "@/components/risk-etiketi"; import { Button } from "@/components/ui/button"; import { SATIR_AD, @@ -92,15 +88,6 @@ const DILIMLER: { }, ]; -// Risk renk dili: yeşil = güvenli, sarı = az riskli, kırmızı = riskli. Nokta -// hiçbir yerde tek başına durmaz; yanında RISK_ETIKET'ten gelen ad her ekran -// genişliğinde yazılı durur (renk tek başına anlam taşımaz). -const RISK_NOKTA: Record = { - guvenli: "bg-emerald-500", - "az-riskli": "bg-amber-500", - riskli: "bg-red-500", -}; - const BOS_EKSTRA: Record = { hayal: [], dengeli: [], @@ -334,27 +321,6 @@ export function ProgramTablosu({ ); } -// Risk etiketinin metin rengi (§4.5): nokta + renkli metin, renk tek başına değil. -const RISK_METIN: Record = { - guvenli: "text-emerald-600", - "az-riskli": "text-amber-600", - riskli: "text-red-600", -}; - -function RiskEtiketi({ risk }: { risk: RiskSeviyesi }) { - return ( - - - {RISK_ETIKET[risk]} - - ); -} - function ProgramSatiri({ program: p, adaySira, @@ -381,7 +347,7 @@ function ProgramSatiri({ {risk ? : null} - + diff --git a/src/features/rapor/components/listem-govde.tsx b/src/features/rapor/components/listem-govde.tsx index 28feb08..e5b7de8 100644 --- a/src/features/rapor/components/listem-govde.tsx +++ b/src/features/rapor/components/listem-govde.tsx @@ -372,7 +372,7 @@ export function ListemGovde({ -

    +
    iyzico güvenli ödeme diff --git a/src/features/rapor/components/rapor-listesi.tsx b/src/features/rapor/components/rapor-listesi.tsx index 16db174..9e23d2e 100644 --- a/src/features/rapor/components/rapor-listesi.tsx +++ b/src/features/rapor/components/rapor-listesi.tsx @@ -26,12 +26,26 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { bolumSayfaSlug, uniSayfaSlug } from "@/lib/slug"; +import { bolumSayfaSlug, trBaslikDuzeni, uniSayfaSlug } from "@/lib/slug"; +import { UNITUR_ETIKET } from "@/lib/unitur"; +import { RiskEtiketi } from "@/components/risk-etiketi"; +import { + SATIR_AD, + SATIR_AD_DUZ, + SATIR_AD_HUCRE, + SATIR_ALT_AD, + SATIR_BASLIK, + SATIR_BUTON_BASLIK, + SATIR_BUTON_HUCRE, + SATIR_CIP, + SATIR_IKINCIL_LINK, + SATIR_META, + SATIR_MOBIL_SAYI, +} from "@/components/liste-satiri"; import { VERI_YIL_SAYISI } from "@/lib/veri-yillari"; import type { RaporSonuc } from "@/features/rapor/types/rapor"; import { DILIM_ETIKET, - RISK_ETIKET, dilimdenFarkliRiskEtiketi, dilimdenRisk, riskHesapla, @@ -39,13 +53,11 @@ import { } from "@/lib/risk"; import type { Dilim } from "@/lib/rapor-havuzu"; -// Risk renk dili: yeşil = güvenli, sarı = az riskli, kırmızı = riskli. -// Nokta hiçbir yerde tek başına durmaz; yanındaki etiket her ekran -// genişliğinde yazılı durur (renk tek başına anlam taşımaz). -const RISK_STIL: Record = { - riskli: { nokta: "bg-red-500", icon: Rocket }, - "az-riskli": { nokta: "bg-amber-500", icon: Scale }, - guvenli: { nokta: "bg-emerald-500", icon: ShieldCheck }, +// Detay başlığındaki dilim ikonu; risk etiketi RiskEtiketi'nden (§4.5). +const RISK_STIL: Record = { + riskli: { icon: Rocket }, + "az-riskli": { icon: Scale }, + guvenli: { icon: ShieldCheck }, }; // Sütun sayısı: # · Program · Son N yıl · Risk · Taban sıra · detay oku @@ -78,26 +90,28 @@ export function RaporSatirDetayi({ const farkliRisk = dilimdenFarkliRiskEtiketi(dilim, risk); return ( <> -
    - - +
    + + {DILIM_ETIKET[dilim as Dilim] ?? dilim} {farkliRisk ? ` · ${farkliRisk}` : null} - {unitur ? · {unitur} : null} + {unitur ? ( + · {UNITUR_ETIKET[unitur]?.cip ?? unitur} + ) : null}
    -
    -
    -
    +
    +
    +
    Neden listede?
    -
    {gerekce}
    +
    {gerekce}
    -
    -
    +
    +
    Risk ve Yapay Zeka yorumu
    -
    +
    {riskNotu} {trendOzeti}
    @@ -181,11 +195,19 @@ export function RaporListesi({ const risk = (adaySira != null ? riskHesapla(p?.efektifSira, adaySira) : null) ?? dilimdenRisk(t.dilim); - const stil = RISK_STIL[risk]; const seri = raporSiraSerisi(p?.siraGecmisi, p?.ilkYil); const taban = p?.efektifSira ?? programEtkinSira(seri); const acikMi = acik.has(t.sira); const devlet = p?.unitur === "DEVLET"; + const tabanMetni = taban != null ? `~${taban.toLocaleString("tr-TR")}.` : "—"; + const riskHucresi = analizKilitli ? ( + + + Pakette + + ) : ( + + ); return ( @@ -197,84 +219,77 @@ export function RaporListesi({ onClick={() => tiklandi(t.sira)} className="kt-cv-satir cursor-pointer" > - - + + {t.sira} - - - {p?.universite ? : null} + + + {/* Telefonda logo yok: sıra numarası satırı zaten çapalıyor, + logo adı üç satıra sıkıştırıyordu (375 px ölçümü). */} + {p?.universite ? ( + + + + ) : null} {p ? ( e.stopPropagation()} > {p.isim} ) : ( - - {t.programId} - + {t.programId} )} - - {p?.unitur ? ( - + e.stopPropagation()} > - {devlet ? "Devlet" : "Vakıf"} - + {trBaslikDuzeni(p.universite)} + + + ) : null} + + {p?.unitur ? ( + {devlet ? "Devlet" : "Vakıf"} ) : null} - - {p?.universite ? ( - e.stopPropagation()} - > - {p.universite} - - ) : null} - {p?.il ? ` · ${p.il.toLocaleLowerCase("tr-TR")}` : ""} + {p?.il ? ( + {trBaslikDuzeni(p.il)} + ) : null} + + + {riskHucresi} + + Taban sıra + + {tabanMetni} + + - + - - {analizKilitli ? ( - - - Pakette - - ) : ( - - - {RISK_ETIKET[risk]} - - )} - - - + {riskHucresi} + + - {taban != null ? `~${taban.toLocaleString("tr-TR")}.` : "—"} + {tabanMetni} - + ) : null} @@ -349,13 +365,13 @@ export function RaporListesi({ return (
    {kilitIdx < rapor.tercihler.length ? ( -

    +

    Programlar ve ham veriler ücretsiz

    ) : null} {rapor.kapsam?.uyarlandi ? ( -

    +

    Tercihlerine göre {rapor.kapsam.toplam} uygun program bulundu; listen{" "} {rapor.kapsam.dagilim.hayal} hayal · {rapor.kapsam.dagilim.dengeli}{" "} dengeli · {rapor.kapsam.dagilim.garanti} güvenli olarak kuruldu. Daha @@ -364,27 +380,31 @@ export function RaporListesi({ ) : null} {tekIl ? ( -

    +

    Listenin {rapor.tercihler.length} tercihi de tek şehirde ( - {tekIl.toLocaleLowerCase("tr-TR")}). Boşta kalma riskini azaltmak için + {trBaslikDuzeni(tekIl)}). Boşta kalma riskini azaltmak için birkaç farklı şehir eklemeyi düşünebilirsin.

    ) : null} -
    +
    - - # - Program - + + + # + + Program + Son {yilSayisi} yıl - - Yapay Zeka Risk + + Yapay Zeka riski - Taban sıra - + + Taban sıra + + Yapay Zeka değerlendirmesi diff --git a/src/features/rapor/components/rapor-yazdir.tsx b/src/features/rapor/components/rapor-yazdir.tsx index 10fc8c0..df81b29 100644 --- a/src/features/rapor/components/rapor-yazdir.tsx +++ b/src/features/rapor/components/rapor-yazdir.tsx @@ -122,7 +122,7 @@ export async function RaporYazdir() {
    -
    +

    Bu rapor KolayTercih tarafından resmî YÖK Atlas verisi ({veriAraligi}{" "} taban sıralamaları, kontenjan ve yerleşme istatistikleri) kullanılarak diff --git a/src/features/rapor/components/tadimlik-satiri.tsx b/src/features/rapor/components/tadimlik-satiri.tsx index 1f7e8cf..3e0d7a7 100644 --- a/src/features/rapor/components/tadimlik-satiri.tsx +++ b/src/features/rapor/components/tadimlik-satiri.tsx @@ -24,7 +24,13 @@ import { import { useGorunumOlayi } from "@/components/use-gorunum-olayi"; import { olay, siraKovasi } from "@/lib/analitik"; import { dilimdenRisk, riskHesapla } from "@/lib/risk"; -import { turkishSlugify } from "@/lib/slug"; +import { trBaslikDuzeni, turkishSlugify } from "@/lib/slug"; +import { + SATIR_ALT_AD, + SATIR_AD_DUZ, + SATIR_CIP, + SATIR_META, +} from "@/components/liste-satiri"; import { listemUretHref, PROFIL_DEGISTI_EVENT } from "@/features/sihirbaz/sihirbaz-sabitler"; import { tercihProfiliOku } from "@/features/sihirbaz/sihirbaz-profil"; import type { TadimlikSatir, TadimlikTip } from "@/lib/tadimlik-havuzu"; @@ -147,40 +153,34 @@ export function TadimlikSatiri({

  • -
    + {/* Rapor listesinin satırıyla aynı düzen (§4.15): ad sarar, + üniversite adı altında, taban sıra sağda iri. */} +
    - - {p.isim} + {p.isim} + + {trBaslikDuzeni(p.universite)} - + {p.unitur ? ( - - {devlet ? "Devlet" : "Vakıf"} - + {devlet ? "Devlet" : "Vakıf"} + ) : null} + {p.il ? ( + {trBaslikDuzeni(p.il)} ) : null} - - {p.universite} - {p.il ? ` · ${p.il.toLocaleLowerCase("tr-TR")}` : ""} - - + - + {taban != null ? `~${taban.toLocaleString("tr-TR")}.` : "—"}
    -
    +
    @@ -293,7 +298,7 @@ export function SihirbazAdimlar({ } >
    -

    +

    Hangi alanlara ilgi duyuyorsun?

    @@ -307,20 +312,20 @@ export function SihirbazAdimlar({ onClick={() => listeDegistir(kategoriler, setKategoriler, k.ad)} > {k.ad} - {k.adet} + {k.adet} ))}

    -

    +

    Nerede okumak istersin?

    En fazla 5 il seçebilirsin. İstersen boş bırak, fark etmez.

    -
    +
    {ilArama ? ( @@ -350,7 +355,7 @@ export function SihirbazAdimlar({ yalnızca görünümü daraltır, seçimleri etkilemez ve kırpmayı devre dışı bırakır. */}
    @@ -362,14 +367,14 @@ export function SihirbazAdimlar({ onClick={() => listeDegistir(iller, setIller, i.il, 5)} > {trBaslikDuzeni(i.il)} - {i.adet} + {i.adet} ))} {ilListesiKirpik ? ( @@ -381,7 +386,7 @@ export function SihirbazAdimlar({ @@ -398,11 +403,11 @@ export function SihirbazAdimlar({ {gorunenDisIller.length > 0 ? (
    -

    +

    KKTC & Yurt Dışı

    @@ -414,7 +419,7 @@ export function SihirbazAdimlar({ onClick={() => listeDegistir(iller, setIller, i.il, 5)} > {trBaslikDuzeni(i.il)} - {i.adet} + {i.adet} ))}
    @@ -423,7 +428,7 @@ export function SihirbazAdimlar({ {iller.length > 0 ? (
    - + Seçtiklerin: {iller.map((il) => ( @@ -431,10 +436,10 @@ export function SihirbazAdimlar({ key={il} type="button" onClick={() => listeDegistir(iller, setIller, il)} - className="inline-flex cursor-pointer items-center gap-1 rounded-full bg-orange-500 px-3 py-1.5 text-xs font-medium text-white transition-colors duration-200 hover:bg-orange-600" + className="inline-flex h-10 cursor-pointer items-center gap-1.5 rounded-full bg-primary px-3.5 text-sm font-semibold text-white transition-[background-color,transform] duration-150 hover:bg-primary/90 active:scale-[0.97]" > {trBaslikDuzeni(il)} - + ))}
    @@ -449,9 +454,9 @@ export function SihirbazAdimlar({ />
    -
    +
    -

    +

    Üniversite tipi & öncelikler

    @@ -459,7 +464,7 @@ export function SihirbazAdimlar({

    -

    +

    Üniversite tipi

    {UNIVERSITE_TIPI_ETIKET[u.grup]} - {u.adet} + {u.adet} ))}
    -

    +

    Senin için önemli olan

    -
    + {/* Telefonda iki sütun, uzun ad iki satıra sarar: tek sütunda + yedi çip 44 px'le pencereyi 812 px'in dışına itiyordu. */} +
    {ONCELIKLER.map((o) => ( listeDegistir(oncelikler, setOncelikler, o)} > @@ -504,7 +512,7 @@ export function SihirbazAdimlar({ ))}
    -

    +

    Öncelikler yapay zekânın gerekçelerine ve program seçimine yön verir; listeyi daraltmaz.

    @@ -512,13 +520,13 @@ export function SihirbazAdimlar({
    -
    +
    @@ -564,7 +564,7 @@ export function KatalogArama({ rehberler }: { rehberler: RehberAramaKaydi[] }) { function BaslangicIcerigi({ onSec }: { onSec: () => void }) { return (
    -

    +

    Keşfet

    @@ -677,11 +677,11 @@ function KesfetLinki({
    - + {kisaBaslik} {baslik} - + {aciklama} @@ -700,7 +700,7 @@ function SonucGrubu({ }) { return (
    -

    +

    {ikon} {baslik}

    diff --git a/src/features/pazarlama/components/sosyal-kanit-govde.tsx b/src/features/pazarlama/components/sosyal-kanit-govde.tsx index 5739a0d..d42f2f6 100644 --- a/src/features/pazarlama/components/sosyal-kanit-govde.tsx +++ b/src/features/pazarlama/components/sosyal-kanit-govde.tsx @@ -127,7 +127,7 @@ export function SosyalKanitGovde({ ilk }: { ilk: SosyalKanitVerisi }) { className="rounded-xl border border-slate-200/70 bg-slate-50/80 px-4 py-3.5" >

    - + {sayi(gosterilenListe)} From 3500bb3745ae8e73f252a3f9e5e20034fae313e8 Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Thu, 24 Sep 2026 01:07:19 +0300 Subject: [PATCH 07/23] =?UTF-8?q?feat(tasarim):=20kalan=20k=C3=BC=C3=A7?= =?UTF-8?q?=C3=BCk=20ekranlar=20(rehber,=20s=C4=B1ralama,=20=C3=BCniversit?= =?UTF-8?q?e=20listesi)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Türkiye sıralaması satırı §4.15: ad sarar, 16 px; satır hover'ı turuncu; Cal Sans sırada tabular-nums kalktı - Rehber yol satırı 14 px ve turuncu hover; üniversite dizini hover - Logo monogramı 10 → 12 px; arama kartındaki elle hex → slate-950 - Denetim: satırın tamamı link olduğunda group-hover mavi ad çifti de istisna sayılıyor Co-Authored-By: Claude Opus 5.5 --- scripts/tasarim-denetim.ts | 5 +++-- src/app/rehber/[slug]/page.tsx | 14 ++++++++++---- src/components/arama-funnel-karti.tsx | 2 +- src/components/uni-logo.tsx | 2 +- .../katalog/components/turkiye-siralamasi.tsx | 8 ++++---- .../katalog/components/universite-listesi.tsx | 2 +- 6 files changed, 20 insertions(+), 13 deletions(-) diff --git a/scripts/tasarim-denetim.ts b/scripts/tasarim-denetim.ts index 96dbabb..8737bc9 100644 --- a/scripts/tasarim-denetim.ts +++ b/scripts/tasarim-denetim.ts @@ -194,7 +194,8 @@ const KURALLAR: Kural[] = [ // §2.1 istisnası: §4.15 kanonik liste satırı adı (sarar, text-base // font-semibold leading-snug + hover:text-primary hover:underline) mavi // kalır. Eski truncate/text-sm adlar istisnaya girmez — önce §4.15'e - // çevrilir. (Bilal'in kararı, 24 Eyl 2026: istisna uyarı vermesin.) + // çevrilir. Satırın tamamı link olduğunda group-hover çifti de sayılır. + // (Bilal'in kararı, 24 Eyl 2026: istisna uyarı vermesin.) bul: (icerik) => regexKurali(/\b(?:group-)?hover:(?:text|border|shadow)-primary\b/)(icerik).filter((e) => { const bas = icerik.lastIndexOf("\n", e.index) + 1; @@ -202,7 +203,7 @@ const KURALLAR: Kural[] = [ const satir = icerik.slice(bas, son === -1 ? undefined : son); return !( /\btext-base font-semibold leading-snug\b/.test(satir) && - /\bhover:text-primary hover:underline\b/.test(satir) + /\b(?:group-)?hover:text-primary (?:group-)?hover:underline\b/.test(satir) ); }), }, diff --git a/src/app/rehber/[slug]/page.tsx b/src/app/rehber/[slug]/page.tsx index eb6ec53..f287e7c 100644 --- a/src/app/rehber/[slug]/page.tsx +++ b/src/app/rehber/[slug]/page.tsx @@ -29,13 +29,19 @@ export default function RehberYaziPage({

    diff --git a/src/components/arama-funnel-karti.tsx b/src/components/arama-funnel-karti.tsx index 9979195..7fac0fe 100644 --- a/src/components/arama-funnel-karti.tsx +++ b/src/components/arama-funnel-karti.tsx @@ -102,7 +102,7 @@ export function AramaFunnelKarti({ onSec }: { onSec: () => void }) { return (
    diff --git a/src/components/uni-logo.tsx b/src/components/uni-logo.tsx index 453163f..01819e8 100644 --- a/src/components/uni-logo.tsx +++ b/src/components/uni-logo.tsx @@ -17,7 +17,7 @@ const BOYUT: Record<"xs" | "sm" | "md" | "lg", string> = { }; const MONOGRAM_METIN: Record<"xs" | "sm" | "md" | "lg", string> = { - xs: "text-[10px]", + xs: "text-xs", sm: "text-xs", md: "text-sm", lg: "text-lg sm:text-xl", diff --git a/src/features/katalog/components/turkiye-siralamasi.tsx b/src/features/katalog/components/turkiye-siralamasi.tsx index be25374..3967a8d 100644 --- a/src/features/katalog/components/turkiye-siralamasi.tsx +++ b/src/features/katalog/components/turkiye-siralamasi.tsx @@ -73,18 +73,18 @@ export function TurkiyeSiralamasi() {
  • - + {u.sira} - + {ad} {alt ? ( - {alt} + {alt} ) : null} diff --git a/src/features/katalog/components/universite-listesi.tsx b/src/features/katalog/components/universite-listesi.tsx index 13c521f..70dd8a4 100644 --- a/src/features/katalog/components/universite-listesi.tsx +++ b/src/features/katalog/components/universite-listesi.tsx @@ -16,7 +16,7 @@ function UniLinkleri({ uniler }: { uniler: UniOzet[] }) {
  • From 5a2fd285555eeaa43da4df54e7aa09e85d0e900a Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Thu, 24 Sep 2026 01:07:58 +0300 Subject: [PATCH 08/23] =?UTF-8?q?docs(tasarim):=20taramada=20bilerek=20b?= =?UTF-8?q?=C4=B1rak=C4=B1lan=20uyar=C4=B1lar=C4=B1n=20gerek=C3=A7esi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/tasarim/DESIGN.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/tasarim/DESIGN.md b/docs/tasarim/DESIGN.md index ec8c9e5..3b82d79 100644 --- a/docs/tasarim/DESIGN.md +++ b/docs/tasarim/DESIGN.md @@ -514,6 +514,13 @@ vermez (Bilal'in kararı, 24 Eyl 2026). Eski `truncate text-sm` adlar yine uyar çevrilir. Bir ekranı dönüştüren ajan o ekrandaki uyarıları sıfırlar ya da neden kaldığını yazar. +**Bilerek bırakılan uyarılar (24 Eyl 2026 taraması sonunda 12):** +- `meraklisina-demo.tsx` (8 × minik-yazi): SVG `viewBox` 640 birim; 375 px'te 0,54 ölçekle + çiziliyor, 11 birimlik yazı ekranda 5,9 px (ölçüldü). 12'ye çıkarmak yalnız 6,5 px eder ve + etiketler çakışır — çözüm telefona ayrı (dikey) çizim; ayrı iş. +- `secimlerim-paneli.tsx` (4): hiçbir yerde kullanılmıyor (ölü kod); dönüştürülmedi, + silinmesi ayrı iş. + ### 9.5 Bitirmeden `pnpm tasarim:denetim` sıfır hata (uyarılar raporda listelenir) + 375 px'te tek bakış (taşma, üst üste binme, dokunma hedefi, çekmece tek ekran) + §9.2 ölçümleri. From d82d59df109edcc7d992044395bf97e76ef4889b Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Thu, 24 Sep 2026 01:19:43 +0300 Subject: [PATCH 09/23] =?UTF-8?q?docs(tasarim):=20samimi=20ve=20oyunsu=20a?= =?UTF-8?q?ray=C3=BCz=20ara=C5=9Ft=C4=B1rmas=C4=B1=20(Duolingo,=20Apple,?= =?UTF-8?q?=20NN/g,=20WCAG)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../2026-09-25-samimi-arayuz-arastirmasi.md | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 docs/tasarim/2026-09-25-samimi-arayuz-arastirmasi.md diff --git a/docs/tasarim/2026-09-25-samimi-arayuz-arastirmasi.md b/docs/tasarim/2026-09-25-samimi-arayuz-arastirmasi.md new file mode 100644 index 0000000..c620c50 --- /dev/null +++ b/docs/tasarim/2026-09-25-samimi-arayuz-arastirmasi.md @@ -0,0 +1,110 @@ +# Samimi ve oyunsu arayüz: araştırma ve öneriler + +> 25 Eylül 2026. Bilal'in isteği: "Duolingo gibi basit ve oyunsu, Apple gibi irdeleyici gözle +> kurulmuş, samimi bir arayüz." Kaynaklar sonda. Uygulaması `tasarim/ogrenci-samimi` dalında; +> kurallar `DESIGN.md` §10'a işlendi. + +## Tek cümle + +Bizim öğrencimiz için "samimi" neşe değil, **güven** demek: kaygılı öğrenciye her ekranda +nerede olduğunu, sıradaki tek adımı ve kendi yaptığı küçük ilerlemeyi göstermek. Duolingo'nun +ilerleme ve küçük kutlama dilini alıyoruz. Seri, puan ve kayıp korkusunu almıyoruz. + +## Neden böyle: öğrencimiz kim? + +Aynı klasördeki öğrenci senaryoları (`docs/arastirma/2026-09-24-ogrenci-senaryolari/`) üç +duyguyu tekrar tekrar gösteriyor: + +- **Yanlış yapma korkusu.** Can, geçen yılın tabanına güvenip listeyi yanlış kurmaktan korkuyor. +- **Mahcubiyet.** Ayşe Hanım "sistemi bilmiyorum" demekten, Yusuf "ailem para gönderemiyor" + demekten çekiniyor. +- **Dağınık bilgi.** Bilgi var ama dağınık ve zamansız. Kimse "şimdi şunu yap" demiyor. + +Duolingo günlük bir alışkanlık ürünü. Tercih ise birkaç haftalık, tek seferlik ve yüksek riskli +bir karar. Bu yüzden oyunlaştırmanın yalnız bir kısmı bize uyuyor. + +## Duolingo'dan alınacaklar ve alınmayacaklar + +| Alıyoruz | Almıyoruz, çünkü | +|---|---| +| **Görünür ilerleme.** "Listen 7/24", "Adım 2/3: Şehir" | **Seri (streak).** Günlük alışkanlık için. Tercih günlük iş değil. | +| **Küçük kilometre taşı kutlaması.** İlk program eklendi, liste 24/24 doldu. Seri ekranındaki kutlama animasyonu yeni kullanıcının 7. günde kalma oranını %1,7 artırmış. | **Kayıp korkusu.** Duolingo uzun seride bunu bilerek kullanıyor, ama kırılınca motivasyonun çöktüğünü kendisi yazıyor. Kaygılı öğrenciye uygun değil. | +| **Başta küçük adımı büyük hissettirmek.** 2'den 3'e çıkan seri %50 büyür. Bizde ilk eklenen program. | **Puan, rozet koleksiyonu, liderlik tablosu.** Tek seferlik kararda toplanacak bir şey yok. Başkasıyla yarış kaygıyı artırır. | +| **Beyaz boşlukla ayırmak, kutuyla değil.** Duolingo 2026 sekme yenilemesi. | **Karakter ve maskot.** Yeni çizim işi. Piksel imzamız zaten bizim karakterimiz. | +| **Tutarlılık amaca hizmet eder.** Her başlığa süs koymak tutarlı ama zorlama. | **Şaka ve "Hay aksi!" tonu.** Apple: samimiyetsiz duruyor. Kaygılı kullanıcıya hiç. | + +## Apple'dan alınacaklar (irdeleyici göz) + +- **Sesimiz sabit, tonumuz duruma göre.** Liste tamamlanınca hafif bir tebrik. Hata ya da + ödemede düz ve ciddi. Açıkta kalan öğrenciye kutlama yok. NN/g'nin deyişiyle "neşe + dayatılamaz". +- **Ekranın amacı ilk satırda.** En önemli bilgi önce. Birden çok fikir varsa ekranı böl. +- **Düğmede fiil.** "Listemi aç", "Sıralamamı gir". "Hadi bakalım!" gibi şirin etiket yok. + Çok adımlı akışta tutarlı dil: "Devam" → "Devam" → son adımda ne olacağını söyleyen fiil. +- **"Biz" az.** Hata mesajında "biz" yok. "Bir sorun oluştu" değil, ne olduğu ve ne + yapılacağı yazılır. +- **Boş ekranda net sonraki adım** ve tek düğme. +- **Geri bildirim önemiyle orantılı.** Sıradan eklemede sessiz onay (düğme ✓ olur, sayaç + artar). Önemli anda (ilk program, liste doldu) görünür onay. Uyarı penceresi yalnız geri + dönüşsüz kayıpta. +- **Dürüst ilerleme.** Sahte yüzde yok. Beklerken ne yapıldığı kısa ve doğru yazılır. +- **İpucu yerinde.** Baştan eğitim turu yerine, öğrenci bir şeye ilk dokunduğunda. + +## İlkeler + +1. **Her ekranın tek cümlelik cevabı var.** `/sonuc`'a gelen öğrenci önce kendi sonucunu + görür: "85.000. sıradasın · Sayısal. 4.760 program açık." +2. **Tercih yolu her yerde aynı dille.** Üç adım: Sıralamanı gir → Programlarını seç → + Listeni kur. Öğrenci hangi adımda olduğunu her ekranda görür (hedef eğimi ve Zeigarnik + etkisi: yarım işi tamamlama isteği). +3. **İlerleme dürüst ve somut.** Sayı ile yazılır ("7/24"), yüzde ya da sahte hız yok. +4. **Kutlama küçük, yerinde ve hak edilmiş.** Yalnız öğrencinin kendi başardığı adımda. + Yerleşme gibi sonuçlarda kutlama yok. "Hareketi azalt" açıksa animasyon yok, metin var. +5. **Metin sen diliyle, sakin, vaatsiz.** Fiil düğmeler. Jargon ilk geçtiği yerde açıklanır. + Hata mesajında suçlama ve ünlem yok. +6. **Boş durum = ne olduğu + tek düğme.** +7. **Anlamsal yapı herkes için.** Sayfa başına tek `h1`, bölümler `h2`. Odak almayan durum + değişiklikleri (`Listene eklendi`, `Adım 2/3`) ekran okuyucuya duyurulur (WCAG 4.1.3). + Açılır satırda `aria-expanded` ve `aria-controls` (APG). Dokunma hedefi 44 px (WCAG 2.5.5). + +**Yapmadıklarımız (aldatıcı desenler):** sahte aciliyet ve geri sayım baskısı, sahte kıtlık, +utandırarak onay ("Hayır, geleceğimi umursamıyorum"), ısrarlı açılır pencere, "kredi" ile +gerçek TL'yi gizlemek. Kredi geçen her yerde TL karşılığı da görünür. + +## Ekran ekran öneriler (öğrencinin kullandığı yerler) + +| Ekran | Bugün | Öneri | +|---|---|---| +| `/sonuc` | Sayfada `h1` yok. İlk ekran yapay zekâ örneği, öğrencinin kendi sonucu değil. | Üstte sonuç başlığı: `h1` + sıra + açık program sayısı (hayal/dengeli/güvenli ayrı ayrı). Altında üç adımlı tercih yolu. Tadımlık kartı yerinde kalır, bir blok aşağı iner. | +| Liste çekmecesi | "5 / 24 tercih" ve çubuk var. Doluluğun anlamı yok. | Risk dağılımı metinle ("2 riskli · 2 az riskli · 1 güvenli"). 24/24'te sakin bir tebrik satırı. | +| + ile ekleme | Profilliyken hiç geri bildirim yok. Profilsizde uzun bir açıklama. | İlk eklemede tek sefer "İlk programın listende · 1/24". Sonrakiler sessiz (düğme ✓, sayaç artar). | +| Sihirbaz | "Adım 1/3" ve üç nokta. Adımların adı yok. | Adımların adı: Alan · Şehir · Öncelik. Adım değişince ekran okuyucuya duyurulur. | +| Sıralama yok sayfası | "Geçerli bir sıralama gerekli" (robotik). İkon düğmenin başında. | "Önce sıralamanı gir" ve bunun ne işe yaradığını söyleyen tek cümle. CTA kurala uygun (ok sonda). | +| Liste hazırlanırken | Aşama mesajları var (iyi). "Sayfayı kapatma" uyarısı 12 px gri. | Önemli uyarı okunur boyda. Başlık iri. | +| Rapor satırı aç/kapa | `aria-expanded` var, `aria-controls` yok. | Açılan bölgeye kimlik ver ve bağla. | + +## Bilal'e kalan kararlar + +- **`/sonuc` sırası.** Tadımlık 22 Eylül'de dönüşüm için bilerek ilk ekrana alındı. Sonuç + başlığı onu yaklaşık 170 px aşağı itiyor (375 px'te hâlâ ilk ekranda başlıyor). Ölçümde + "tadımlık görüntülenme" düşerse başlık küçültülür, kaldırılmaz. +- **Kutlama hareketi.** Şimdilik yalnız metin, hafif bir büyüme ve renk. Konfeti ya da ses + yok. Daha "Duolingo" isteniyorsa bir sonraki adım piksel imzasıyla kısa bir "ısınma" + (hero'daki pikseller gibi) olabilir. + +## Kaynaklar + +1. Duolingo, "Elevating craft: How we refreshed our core tabs", 4 Şub 2026. https://blog.duolingo.com/core-tabs-redesign/ +2. Duolingo, "The habit-building research behind your Duolingo streak", 31 Oca 2022. https://blog.duolingo.com/how-duolingo-streak-builds-habit/ +3. Duolingo, "Start earning brand-new Achievements", 11 Ara 2023. https://blog.duolingo.com/achievement-badges/ +4. NN/g, Tanner Kohler, "Autonomy, Relatedness, and Competence in UX Design", 9 Oca 2022. https://www.nngroup.com/articles/autonomy-relatedness-competence/ +5. NN/g, Therese Fessenden, "Three Pillars of User Delight", 27 Kas 2022. https://www.nngroup.com/articles/pillars-user-delight/ +6. NN/g, Jakob Nielsen, "Progressive Disclosure", 2006. https://www.nngroup.com/articles/progressive-disclosure/ +7. NN/g, Kate Kaplan, "Designing Empty States in Complex Applications", 19 Eyl 2021. https://www.nngroup.com/articles/empty-state-interface-design/ +8. Apple HIG, Writing (güncelleme 16 Ara 2025). https://developer.apple.com/design/human-interface-guidelines/writing +9. Apple HIG, Onboarding (10 Haz 2024). https://developer.apple.com/design/human-interface-guidelines/onboarding +10. Apple HIG, Feedback. https://developer.apple.com/design/human-interface-guidelines/feedback +11. Apple HIG, Progress indicators. https://developer.apple.com/design/human-interface-guidelines/progress-indicators +12. Laws of UX (Jon Yablonski): Goal-Gradient Effect, Peak-End Rule, Hick's Law, Zeigarnik Effect. https://lawsofux.com +13. W3C WCAG 2.2: 2.5.8 Target Size (Minimum), 4.1.3 Status Messages; WAI-ARIA APG Disclosure Pattern. https://www.w3.org/WAI/ +14. Harry Brignull, Deceptive Patterns: Types. https://www.deceptive.design/types From f4df52000b576749dd2622278c7ce2ce33d72b28 Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Thu, 24 Sep 2026 01:23:20 +0300 Subject: [PATCH 10/23] =?UTF-8?q?feat(ux):=20/sonuc'a=20tek=20c=C3=BCmleli?= =?UTF-8?q?k=20cevap,=20liste=20durumu=20ve=20kilometre=20ta=C5=9Flar?= =?UTF-8?q?=C4=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Sayfanın h1'i yoktu: "Sıralamanla açılan programlar" + "85.000. sıradasın (Sayısal). 4.760 program sana açık" başlığı en üstte; tadımlık altında (375 px'te hâlâ ilk ekranda, 474 px) - Başlık altında liste durumu: boşsa + ipucu, doluysa "Listende 3/24 program · 21 yer kaldı" düğmesi çekmeceyi açar - + ile ilk programda ve 24/24'te tek seferlik sakin onay; liste doluyken ne yapılacağını söyleyen mesaj - Bildirimler 16 px başlık, okunur açıklama, sitenin köşesiyle - Başlık ve tablo aynı sorguyu React cache ile paylaşıyor Co-Authored-By: Claude Opus 5.5 --- src/app/sonuc/page.tsx | 12 +++- src/components/ui/sonner.tsx | 7 ++- .../liste/components/liste-durumu-satiri.tsx | 55 ++++++++++++++++ .../liste/components/manuel-harita.tsx | 14 +++-- .../liste/components/program-ekle-butonu.tsx | 26 +++++++- .../components/sonuc-program-tablosu.tsx | 63 ++++++++++++++++++- .../rapor/components/tadimlik-satiri.tsx | 11 ++-- 7 files changed, 172 insertions(+), 16 deletions(-) create mode 100644 src/features/liste/components/liste-durumu-satiri.tsx diff --git a/src/app/sonuc/page.tsx b/src/app/sonuc/page.tsx index 2cc4b0b..10a57a8 100644 --- a/src/app/sonuc/page.tsx +++ b/src/app/sonuc/page.tsx @@ -7,6 +7,7 @@ import { PROFIL_CEREZ_ADI, } from "@/features/sihirbaz/sihirbaz-sabitler"; import { + SonucBasligi, SonucProgramTablosu, SonucProgramTablosuSkeleton, } from "@/features/liste/components/sonuc-program-tablosu"; @@ -133,7 +134,16 @@ export default function SonucPage({ boyada haritanın tam ölçülü kutusunu çizer, chunk inince kutunun yalnızca İÇİ dolar. Haritanın yerini bir daha aşağı almadan önce bu mekanizmayı kontrol et. */} - + {/* Sayfanın tek cümlelik cevabı ve h1'i: öğrenci önce kendi + sonucunu görür (DESIGN.md §10). Harita ve tadımlık altında. */} + + + {/* Girişsiz tadımlık: gerçek rapor satırı formatında tek örnek. Havuzda içerik yoksa bileşen hiç render edilmez. Listesi diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx index ce25f68..1c51819 100644 --- a/src/components/ui/sonner.tsx +++ b/src/components/ui/sonner.tsx @@ -37,8 +37,13 @@ const Toaster = ({ ...props }: ToasterProps) => { } as React.CSSProperties } toastOptions={{ + // Şişko ve sade (DESIGN.md §10): başlık 16 px, açıklama okunur + // koyulukta; kutu sitenin kart köşesiyle aynı. classNames: { - toast: "cn-toast", + toast: "cn-toast !rounded-2xl !border-slate-200 !p-4 !shadow-lg", + title: "!text-base !font-semibold !text-slate-900", + description: "!text-sm !leading-relaxed !text-slate-600", + icon: "!text-primary", }, }} {...props} diff --git a/src/features/liste/components/liste-durumu-satiri.tsx b/src/features/liste/components/liste-durumu-satiri.tsx new file mode 100644 index 0000000..51c71b4 --- /dev/null +++ b/src/features/liste/components/liste-durumu-satiri.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { ArrowRight, Plus } from "lucide-react"; +import { + MANUEL_LISTE_MAX, + cekmeceAc, + useListeHidreMi, + useManuelListe, +} from "../liste-store"; + +/** + * /sonuc başlığının altındaki "listen nerede?" satırı (DESIGN.md §10): + * öğrenci kendi kurduğu listenin durumunu sayıyla görür (hedef eğimi: + * "5/24"), tek dokunuşla çekmeceyi açar. Liste boşsa ne yapılacağını söyler. + * Liste localStorage'da yaşadığı için hidrasyondan önce yer tutucu çizilir, + * satır zıplamasın. + */ +export function ListeDurumuSatiri() { + const hidre = useListeHidreMi(); + const liste = useManuelListe(); + + if (!hidre) return
    ; + + if (liste.length === 0) { + return ( +

    + + + Artı düğmesi + + ile beğendiğin programı listene ekle; sırasını sonra sen belirlersin. +

    + ); + } + + const kalan = MANUEL_LISTE_MAX - liste.length; + return ( +
    + +
    + ); +} diff --git a/src/features/liste/components/manuel-harita.tsx b/src/features/liste/components/manuel-harita.tsx index 007a867..8b2f810 100644 --- a/src/features/liste/components/manuel-harita.tsx +++ b/src/features/liste/components/manuel-harita.tsx @@ -20,7 +20,13 @@ const RISK_AGIRLIK: Record = { riskli: 2, }; -export function ManuelHarita({ yerAyir = false }: { yerAyir?: boolean }) { +export function ManuelHarita({ + yerAyir = false, + className, +}: { + yerAyir?: boolean; + className?: string; +}) { const liste = useManuelListe(); const hidre = useListeHidreMi(); const [seciliIl, setSeciliIl] = useState(null); @@ -85,10 +91,10 @@ export function ManuelHarita({ yerAyir = false }: { yerAyir?: boolean }) { // durumunda boş harita ekranda kalmaz. if (liste.length === 0 && (hidre || !yerAyir)) return null; - // Margin yok: harita /sonuc'ta main'in İLK çocuğudur, üst boşluğunu - // main'in py-12'sinden alır (bkz. sonuc/page.tsx). + // Üst boşluk çağırandan gelir: /sonuc'ta harita sonuç başlığının altında + // durur (bkz. sonuc/page.tsx). Harita çizilmezse boşluk da oluşmaz. return ( -
    +
    n.toLocaleString("tr-TR"); + +// Başlık ve tablo aynı isteği okur; cache tek istek içinde bir kez sorgular. +// Anahtar ilkel olsun diye iller birleştirilmiş metin olarak geçer. +const sonuclariOku = cache( + (sira: number, tur: PuanTuruKey, illerAnahtari: string, tip?: UniturGrubu) => + siraylaProgramAra(sira, tur, { + iller: illerAnahtari ? illerAnahtari.split("|") : [], + uniturGrubu: tip, + }), +); + +/** + * /sonuc'un tek cümlelik cevabı (DESIGN.md §10): sayfanın h1'i, öğrencinin + * sırası ve kaç programın açık olduğu. Tadımlık ve tablo bunun altında. + * Geniş ekran altında ortalı (§5.2). + */ +export function SonucBasligi({ + sira, + tur, + iller, + uniturGrubu, +}: { + sira: number; + tur: PuanTuruKey; + iller?: string[]; + uniturGrubu?: UniturGrubu; +}) { + const { toplam } = sonuclariOku(sira, tur, (iller ?? []).join("|"), uniturGrubu); + const acik = toplam.hayal + toplam.dengeli + toplam.garanti; + const filtre = [ + ...(iller ?? []).map(trBaslikDuzeni), + ...(uniturGrubu === "devlet" + ? ["yalnız devlet"] + : uniturGrubu === "vakif" + ? ["yalnız vakıf"] + : []), + ]; + return ( +
    +

    + Sıralamanla açılan programlar +

    +

    + {sayi(sira)}.{" "} + sıradasın ({PUAN_TURU_ETIKET[tur] ?? tur}).{" "} + {filtre.length > 0 ? `Seçimlerine göre (${filtre.join(", ")}) ` : ""} + + {sayi(acik)} program + {" "} + sana açık; aşağıda riskine göre ayrıldı. +

    + +
    + ); +} + // /sonuc'un ham tablosunun sunucu sarmalayıcısı: okuma burada yaşar, sayfa // yalnızca kompoze eder. export function SonucProgramTablosu({ @@ -16,7 +77,7 @@ export function SonucProgramTablosu({ iller?: string[]; uniturGrubu?: UniturGrubu; }) { - const sonuclar = siraylaProgramAra(sira, tur, { iller, uniturGrubu }); + const sonuclar = sonuclariOku(sira, tur, (iller ?? []).join("|"), uniturGrubu); return ( + // bu bileşen hiç dönmez. +
    Yapay zekâdan örnek satır

    From e29a0ceb9a9ade7566e73c41ac5f2c2895b765da Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Thu, 24 Sep 2026 01:25:16 +0300 Subject: [PATCH 11/23] =?UTF-8?q?feat(ux):=20liste=20=C3=A7ekmecesinde=20d?= =?UTF-8?q?enge=20ve=20tamamlanma?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Listenin risk dağılımı metinle ("Az riskli 20 · Güvenli 4") - 24/24'te "Listen tamam" satırı; durum satırı ekran okuyucuya duyurulur - Riskleri bilinen 6+ tercihli listede güvenli yoksa tek cümlelik ipucu Co-Authored-By: Claude Opus 5.5 --- .../liste/components/liste-cekmecesi.tsx | 44 ++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/src/features/liste/components/liste-cekmecesi.tsx b/src/features/liste/components/liste-cekmecesi.tsx index 83240b6..df77faf 100644 --- a/src/features/liste/components/liste-cekmecesi.tsx +++ b/src/features/liste/components/liste-cekmecesi.tsx @@ -28,6 +28,7 @@ import { } from "@/components/ui/drawer"; import { ArrowRight, + Check, GripVertical, ListChecks, Plus, @@ -89,6 +90,13 @@ export function ListeCekmecesi() { const [oturdu, setOturdu] = useState(false); const doluluk = Math.round((liste.length / MANUEL_LISTE_MAX) * 100); + // Risk dağılımı: riskliden güvenliye, sıfır olanlar yazılmaz + const riskSayilari = (["riskli", "az-riskli", "guvenli"] as const) + .map((r) => [r, liste.filter((t) => t.risk === r).length] as const) + .filter(([, adet]) => adet > 0); + const riskBilinen = liste.filter((t) => t.risk != null).length; + const guvenliYok = + riskBilinen >= 6 && !liste.some((t) => t.risk === "guvenli"); // Profilsiz + ile eklenenlerin riski henüz hesaplanmamıştır; alt kart // olmayan bir analizi "hazır" diye duyurmasın. Profil varken null risk, // programın taban sırası olmadığındandır — sıra istemek yanlış olur. @@ -139,8 +147,18 @@ export function ListeCekmecesi() { Kendi Listem -

    - {liste.length} / {MANUEL_LISTE_MAX} tercih + {/* Durum ekran okuyucuya da duyurulur (WCAG 4.1.3) */} +

    + {liste.length === MANUEL_LISTE_MAX ? ( + + + Listen tamam · {liste.length} / {MANUEL_LISTE_MAX} + + ) : ( + <> + {liste.length} / {MANUEL_LISTE_MAX} tercih + + )}

    + {/* Listenin dengesi metinle (renk tek başına değil, §2.2) */} + {riskSayilari.length > 0 ? ( +

    + {riskSayilari.map(([seviye, adet]) => ( + + + + {adet} + + + ))} +

    + ) : null} + {/* Denge ipucu: 24'lük iskeletin mantığı (alt sıralarda güvenli + programlar). Yalnız riskleri bilinen ve en az 6 tercihli + listede; vaat değil, gerekçe. */} + {guvenliYok ? ( +

    + Güvenli program yok; alta birkaç tane eklemek açıkta kalma riskini + azaltır. +

    + ) : null}
    Date: Thu, 24 Sep 2026 01:27:11 +0300 Subject: [PATCH 12/23] =?UTF-8?q?feat(ux):=20sihirbaz=20ad=C4=B1mlar=C4=B1?= =?UTF-8?q?=20adland=C4=B1;=20Emil=20Kowalski=20ilkeleriyle=20hareket=20in?= =?UTF-8?q?celtmesi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adım göstergesi: Alan · Şehir · Öncelik; dolum transform ile soldan, güçlü ease-out 300 ms; adım değişimi ekran okuyucuya duyurulur - Çekmecede "Listen tamam" satırı @starting-style ile küçük giriş (scale 0.95 + opaklık); hareketi azalt'ta yalnız opaklık - Dilim sekmesindeki kayan hapta sekme kaldırıldı (bounce 0, 0.35 sn) Co-Authored-By: Claude Opus 5.5 --- .../liste/components/liste-cekmecesi.tsx | 4 +- .../liste/components/program-tablosu.tsx | 2 +- .../sihirbaz/components/sihirbaz-adimlar.tsx | 52 +++++++++++++------ 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/features/liste/components/liste-cekmecesi.tsx b/src/features/liste/components/liste-cekmecesi.tsx index df77faf..4332389 100644 --- a/src/features/liste/components/liste-cekmecesi.tsx +++ b/src/features/liste/components/liste-cekmecesi.tsx @@ -150,7 +150,9 @@ export function ListeCekmecesi() { {/* Durum ekran okuyucuya da duyurulur (WCAG 4.1.3) */}

    {liste.length === MANUEL_LISTE_MAX ? ( - + // Nadir, hak edilmiş an: küçük giriş (Emil: scale 0.95 + opaklık, + // güçlü ease-out, <300 ms). Hareketi azalt'ta yalnız opaklık. + Listen tamam · {liste.length} / {MANUEL_LISTE_MAX} diff --git a/src/features/liste/components/program-tablosu.tsx b/src/features/liste/components/program-tablosu.tsx index 3dc2fd0..d384bc2 100644 --- a/src/features/liste/components/program-tablosu.tsx +++ b/src/features/liste/components/program-tablosu.tsx @@ -215,7 +215,7 @@ export function ProgramTablosu({ transition={ hareketAzalt ? { duration: 0 } - : { type: "spring", duration: 0.4, bounce: 0.15 } + : { type: "spring", duration: 0.35, bounce: 0 } } aria-hidden /> diff --git a/src/features/sihirbaz/components/sihirbaz-adimlar.tsx b/src/features/sihirbaz/components/sihirbaz-adimlar.tsx index 0880084..d1220cd 100644 --- a/src/features/sihirbaz/components/sihirbaz-adimlar.tsx +++ b/src/features/sihirbaz/components/sihirbaz-adimlar.tsx @@ -56,6 +56,8 @@ export function Cip({ ); } +const ADIM_ADLARI = ["Alan", "Şehir", "Öncelik"] as const; + // Haritada karşılığı olan iller (KKTC/yurtdışı kampüsler çip olarak kalır) const HARITA_ILLERI = new Set(cities.map((c) => normalizeIlAdi(c.name))); @@ -237,22 +239,40 @@ export function SihirbazAdimlar({ return (

    -
    - {[0, 1, 2].map((i) => ( - - ))} - - Adım {adim + 1}/3 - + {/* Adım göstergesi (DESIGN.md §10): üç parçalı çubuk + adların + kendisi; öğrenci nerede olduğunu ve ne kaldığını görür. Değişim + ekran okuyucuya duyurulur (WCAG 4.1.3). */} +
    +
      + {ADIM_ADLARI.map((ad, i) => ( +
    1. + {/* Dolum transform ile (yalnız transform/opaklık canlandırılır); + soldan dolar, güçlü ease-out 300 ms. */} + + + + + {ad} + +
    2. + ))} +
    +

    + Adım {adim + 1} / 3: {ADIM_ADLARI[adim]} +

    Date: Thu, 24 Sep 2026 01:29:51 +0300 Subject: [PATCH 13/23] =?UTF-8?q?feat(ux):=20listeden=20silme=20geri=20al?= =?UTF-8?q?=C4=B1nabilir;=20better-ui=20skill'i=20eklendi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Çekmecede "Listeyi temizle" ve satır çıkarma (düğme ve kaydırma) anında siliyordu, geri dönüş yoktu. Artık 8 sn "Geri al" bildirimi; çıkanlar eski sıralarına döner, arada eklenenler korunur - Çekmece kaydırması sayfaya taşmıyor (overscroll-contain) - .claude/skills/better-ui: Jakub Krehel'in UI cilası skill'i (ui-skills.com); başına DESIGN.md önceliği ve çakışan iki değer notu eklendi Co-Authored-By: Claude Opus 5.5 --- .claude/skills/better-ui/SKILL.md | 106 ++++++++++++++++++ .../liste/components/liste-cekmecesi.tsx | 47 +++++++- 2 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 .claude/skills/better-ui/SKILL.md diff --git a/.claude/skills/better-ui/SKILL.md b/.claude/skills/better-ui/SKILL.md new file mode 100644 index 0000000..55d8fda --- /dev/null +++ b/.claude/skills/better-ui/SKILL.md @@ -0,0 +1,106 @@ +--- +name: better-ui +description: Design engineering principles for making interfaces feel polished. Use when building UI components, reviewing frontend code, implementing animations, hover states, shadows, borders, micro-interactions, enter/exit animations, or any visual detail work. Triggers on UI polish, design details, "make it feel better", "feels off", stagger animations, border radius, optical alignment, image outlines, box shadows. +--- + +> **KolayTercih notu (25 Eyl 2026, Bilal'in isteğiyle eklendi).** Kaynak: Jakub Krehel, +> https://github.com/jakubkrehel/skills (ui-skills.com üzerinden okundu, elle kopyalandı). +> Bu projede `docs/tasarim/DESIGN.md` önceliklidir. Çakışan iki nokta: +> basınca küçülme bizde `scale-[0.97]` (geniş satır `0.99`, çarpı `0.96`), buradaki `0.96` +> değil; kart ve satırlarda yapıyı `border-slate-200` verir, gölge yalnız §4.4'teki kadar. +> Görsel ayar yaparken önce DESIGN.md, sonra bu dosya. + +# UI polish + +Polish comes from a pile of small details that compound. This skill is the reference for which are worth having and what values they take. + +When reviewing, slow the interface down. What feels off at 10% speed is what is subtly wrong at full speed. + +Keep the project's component library, tokens and density, and match its motion language except where a rule below prescribes an exact interaction. + +Every duration, curve, scale and blur below is a specific value, not a range to approximate. cubic-bezier(0.2, 0, 0, 1) is not cubic-bezier(0.4, 0, 0.2, 1), and 0.96 is not 0.95. Use what is written. + +## Concentric border radius + +Outer radius = inner radius + padding. Mismatched radii on nested elements is the most common thing that makes an interface feel off. + +## Optical over geometric alignment + +When geometric centering looks off, align optically. Buttons with icons, play triangles and asymmetric icons all need a manual nudge. + +## Shadows for elevation, borders for structure + +Where a border exists only to create depth, prefer layered transparent box-shadow values. Keep borders that communicate structure or state: dividers, separators and selected or focus states. + +## Interruptible animations + +Use CSS transitions for interactive state changes, because they can be interrupted mid-animation. Reserve keyframes for staged sequences that run once. + +## Split and stagger enter animations + +For an infrequent staged entrance where sequence communicates hierarchy, break the content into semantic chunks and stagger them by ~100ms. Animating one container gets you less for the same cost. Leave high-frequency interactions unstaggered. + +## Subtle exit animations + +Use a small fixed translateY rather than full height. Exits should be softer than enters. Use ease-out for both directions. + +## Contextual icon animations + +Animate icons with opacity, scale and blur rather than toggling visibility. Use exactly these values: scale 0.25 to 1, opacity 0 to 1, blur 4px to 0px. + +With a motion library (motion or framer-motion in package.json), match that package's import path. Use transition: { type: "spring", duration: 0.3, bounce: 0 }. Bounce is always 0. + +Without one, keep both icons in the DOM with one absolutely positioned, and cross-fade with cubic-bezier(0.2, 0, 0, 1). + +## Image outlines + +Give images a 1px outline at low opacity for consistent depth. Pure black in light mode (oklch(0 0 0 / 0.1)), pure white in dark (oklch(1 0 0 / 0.1)). Never a near-black like slate or zinc and never a tinted neutral. + +## Scale on press + +A scale on click gives a button tactile feedback (upstream value 0.96; this project uses 0.97, see note above). Add a static prop to switch it off where motion would distract. + +## Skip animation on page load + +Use initial={false} on AnimatePresence to keep enter animations off the first render. Check that it leaves intentional page entrances intact. + +## Suppress transitions on theme switch + +A theme flip changes color, background, border and shadow on nearly every element at once. Inject *,*::before,*::after{transition:none !important}, force a reflow, then remove it on the next frame. + +## Transition only what changes + +Always name the exact properties: transition-property: scale, opacity. Tailwind's transition-transform covers transform, translate, scale, rotate. + +## Use will-change sparingly + +Only for transform, opacity and filter. Never will-change: all. Add it when you see first-frame stutter, not before. + +## Match icon stroke to text weight + +1.5px stroke beside regular (400) text, 2px beside semibold (600). One stroke weight per icon set and one icon library per surface. + +## One SVG, recolored per state + +Icons use currentColor and take hover, selected and disabled states from CSS color and opacity. Outline is the default variant; fill marks the active state. + +## Motion restraint + +Give high-frequency interactions instant feedback, or a transition of 150ms or less on opacity and color. Every animated state change also needs a static cue: color, an icon, or a label. Motion is never the only feedback channel. + +## Before you finish + +| Mistake | Fix | +|---|---| +| Icons look off-center | Nudge optically with padding, or fix the SVG | +| Jarring staged entrance or exit | Stagger infrequent entrances; keep exits subtle | +| Theme toggle crossfades the whole page | Disable transitions for the swap, force a reflow, restore on the next frame | +| transition: all on elements | Specify exact properties | +| First-frame animation stutter | Add will-change: transform (sparingly) | +| Hairline icon beside bold text | Match the stroke width to the text weight | + +## Reporting + +Severity. HIGH breaks an interaction, makes motion unusable, or leaves a state change visible only while the animation runs. MEDIUM is a visible inconsistency in surfaces, icons, or motion. LOW is isolated polish. + +Format: group findings under the principle each violates, ordered by severity, one row per root cause: Severity | Location | Before | After | Why. End with Block when any HIGH remains, Approve otherwise. Report every check you could not run as Not verified. diff --git a/src/features/liste/components/liste-cekmecesi.tsx b/src/features/liste/components/liste-cekmecesi.tsx index 4332389..ab1960e 100644 --- a/src/features/liste/components/liste-cekmecesi.tsx +++ b/src/features/liste/components/liste-cekmecesi.tsx @@ -12,6 +12,7 @@ // sadeleşir; X butonu her durumda kalır. import { useState } from "react"; +import { toast } from "sonner"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { PixelField } from "@/components/pixel-decor"; @@ -60,6 +61,7 @@ import { cekmeceAc, cekmeceKapat, cikar, + listeOku, temizle, useCekmeceAcik, useManuelListe, @@ -79,6 +81,36 @@ function uniHref(ad: string): string { return `/universite/${turkishSlugify(ad.replace(/\s*\([^)]*\)\s*$/, "").trim())}`; } +/** + * Geri alınabilir silme (Vercel arayüz ilkeleri: yıkıcı işlem ya onay ya + * geri al penceresi ister; Apple: beklenen işlemde uyarı penceresi açma). + * Çıkan tercihler eski sıralarına geri konur; arada eklenenler kaybolmaz. + */ +function geriAlinabilirCikar(ids: string[], baslik: string) { + const onceki = listeOku(); + const cikanlar = onceki + .map((t, sira) => ({ t, sira })) + .filter(({ t }) => ids.includes(t.id)); + if (cikanlar.length === 0) return; + if (ids.length === onceki.length) temizle(); + else for (const id of ids) cikar(id); + toast(baslik, { + duration: 8000, + action: { + label: "Geri al", + onClick: () => { + const simdi = listeOku().filter( + (t) => !cikanlar.some((c) => c.t.id === t.id), + ); + for (const { t, sira } of cikanlar) { + simdi.splice(Math.min(sira, simdi.length), 0, t); + } + yenidenSirala(simdi.slice(0, MANUEL_LISTE_MAX)); + }, + }, + }); +} + export function ListeCekmecesi() { const router = useRouter(); const acik = useCekmeceAcik(); @@ -132,7 +164,12 @@ export function ListeCekmecesi() { {liste.length > 0 ? ( {profil ? (

    @@ -358,7 +358,7 @@ function SunucuYokEkrani() { disabled={deniyor} className="mt-6 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600" > - {deniyor ? "Kontrol ediliyor" : "Listemi aç"} + {deniyor ? "Kontrol ediliyor…" : "Listemi aç"}

    @@ -401,7 +401,7 @@ function UretimGovdesi({ uzunSurdu }: { uzunSurdu: boolean }) { return (

    -

    +

    Listen hazırlanıyor

    {uzunSurdu ? ( @@ -426,8 +426,8 @@ function UretimGovdesi({ uzunSurdu }: { uzunSurdu: boolean }) { ) : ( -

    - Bu işlem yarım dakika kadar sürebilir, sayfayı kapatma. +

    + Yarım dakika kadar sürebilir; bu sayfayı kapatma.

    )}
    diff --git a/src/features/rapor/components/rapor-listesi.tsx b/src/features/rapor/components/rapor-listesi.tsx index 9e23d2e..3eddb7d 100644 --- a/src/features/rapor/components/rapor-listesi.tsx +++ b/src/features/rapor/components/rapor-listesi.tsx @@ -293,6 +293,7 @@ export function RaporListesi({

  • ); } return (
    -

    - Geçerli bir sıralama gerekli +

    + Önce sıralamanı gir

    -

    - Listeni kurabilmek için YKS başarı sıralamanı gir. Kaydettiğin sıra tüm - uygulamada kullanılır. +

    + YKS başarı sıralamanı girince hangi programların sana açık olduğunu + hemen görürsün. Bir kez girmen yeter; her sayfada kullanılır.

    @@ -56,12 +58,18 @@ export function SiraGerekli() { const { href } = sonucaGitIste(); if (href) router.push(href); }} - className="cursor-pointer bg-orange-500 text-white hover:bg-orange-600" + size="lg" + className="cursor-pointer bg-orange-500 text-white transition-[background-color,transform] duration-200 hover:bg-orange-600 active:scale-[0.97]" > - Sıralamamı gir + - diff --git a/src/features/rapor/components/liste-uretici.tsx b/src/features/rapor/components/liste-uretici.tsx index b673d7a..8f62125 100644 --- a/src/features/rapor/components/liste-uretici.tsx +++ b/src/features/rapor/components/liste-uretici.tsx @@ -434,7 +434,7 @@ function UretimGovdesi({ uzunSurdu }: { uzunSurdu: boolean }) { {/* Nihai sayfanın (listem-govde) grid'iyle aynı düzen: liste solda, danışman sağda — üretim biter bitmez ekran aynı yerlere oturur. */} -
    +
    {/* Gelmekte olan tercih listesinin iskeleti */}
    diff --git a/src/features/rapor/components/listem-govde.tsx b/src/features/rapor/components/listem-govde.tsx index e5b7de8..f2a6076 100644 --- a/src/features/rapor/components/listem-govde.tsx +++ b/src/features/rapor/components/listem-govde.tsx @@ -39,6 +39,11 @@ import { } from "@/features/liste/hooks/use-tercih-profili"; import { UNIVERSITE_TIPI_ETIKET } from "@/features/sihirbaz/sihirbaz-sabitler"; import { PUAN_TURLERI } from "@/types/yokatlas"; +import { DILIM_ETIKET } from "@/lib/risk"; + +// Başlık satırındaki ikincil düğmeler: 44 px, hover tek turuncu (§4.1, §2.1) +const IKINCIL_BUTON = + "h-11 cursor-pointer px-4 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]"; export function ListemGovde({ rapor, @@ -94,6 +99,10 @@ export function ListemGovde({ ); const acikSatir = kilitliBaslangic ?? rapor.tercihler.length; + const dilimOzeti = useMemo(() => { + const say = (d: string) => rapor.tercihler.filter((t) => t.dilim === d).length; + return `${rapor.tercihler.length} tercih: ${say("hayal")} ${DILIM_ETIKET.hayal.toLocaleLowerCase("tr-TR")} · ${say("dengeli")} ${DILIM_ETIKET.dengeli.toLocaleLowerCase("tr-TR")} · ${say("garanti")} ${DILIM_ETIKET.garanti.toLocaleLowerCase("tr-TR")}`; + }, [rapor]); // Profil client'ta (localStorage) çözülür: ilk render'da null döner, yani // SSR ile aynı "Tercihlerimi düzenle" hâli basılır — hidrasyon uyuşmazlığı @@ -148,7 +157,7 @@ export function ListemGovde({ ); return ( -
    +
    {/* Harita: listenin coğrafyası — ile dokun, üniversiteleri gör */}
    -

    - {hasPaket - ? "Yapay Zeka tercih listen" - : "Veriyi ücretsiz incele. Kararı Yapay Zeka ile netleştir."} -

    + {/* Sayfa nerede olduğunu söyler (DESIGN.md §10.1): başlık yer, + altındaki satır listenin sayılarla özeti. */} +
    +

    + Tercih listen +

    +

    + {dilimOzeti} + {hasPaket ? null : ( + + Veriyi ücretsiz incele, kararı Yapay Zeka ile netleştir. + + )} +

    +
    {/* Tercih düzenleme her kullanıcıya açık: kapı (layout'ta monte TercihProfiliKapisi) profili günceller, /listem'e döner. @@ -200,9 +219,8 @@ export function ListemGovde({ {farkVar ? ( diff --git a/src/features/pazarlama/components/landing-parcalari.tsx b/src/features/pazarlama/components/landing-parcalari.tsx index f49b28e..66e7412 100644 --- a/src/features/pazarlama/components/landing-parcalari.tsx +++ b/src/features/pazarlama/components/landing-parcalari.tsx @@ -2,13 +2,26 @@ import { CheckCircle2, X } from "lucide-react"; // Ana sayfadaki karşılaştırma tablosunun hücresi: metin değeri düz yazılır, // boolean değer Var/Yok ikonuna çevrilir. -export function ComparisonCell({ value }: { value: boolean | string }) { +export function ComparisonCell({ + value, + vurgu = false, +}: { + value: boolean | string; + /** KolayTercih sütunu: metin koyu ve kalın */ + vurgu?: boolean; +}) { if (typeof value === "string") { - return {value}; + return ( + + {value} + + ); } return value ? ( - + ) : ( - + ); } From 71e63891313d547ec0a1184fd070b29acc7571cb Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Thu, 24 Sep 2026 02:12:45 +0300 Subject: [PATCH 22/23] =?UTF-8?q?feat(tasarim):=20funnel=20CTA'lar=C4=B1?= =?UTF-8?q?=20arama=20kart=C4=B1=20dilinde,=2024'l=C3=BCk=20piksel=20liste?= =?UTF-8?q?=20kreatifi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - PikselCtaYuzeyi: koyu slate-950 yüzey, yazılıp silinen satırlar, tek turuncu CTA (arama penceresindeki kartın dili); ana sayfa kapanışı ve /sonuc'taki iki panelde köşeye kümelenen PixelField'in yerine - PikselListe: 24 kare = 24 tercih; kendini kurar (5 hayal · 13 dengeli · 6 güvenli), imlece tepki verir, CTA'ya gelince tamamı dolar, dokunmada nabız; hareketi azalt'ta durağan; renk hep metinle - Liste çekmecesi: ilerleme çubuğunun yerinde öğrencinin kendi listesi pikselde (her kare o tercihin riski); başlık yan yana dizildi, liste alanı 150 → 234 px (375 px ölçümü) - Funnel metinleri src/lib/funnel-metinleri.ts'e taşındı Co-Authored-By: Claude Opus 5.5 --- docs/tasarim/DESIGN.md | 18 ++ src/app/globals.css | 18 ++ src/app/page.tsx | 32 +-- src/components/arama-funnel-karti.tsx | 34 +--- src/components/piksel-cta-yuzeyi.tsx | 77 +++++++ src/components/piksel-liste.tsx | 191 ++++++++++++++++++ .../liste/components/liste-cekmecesi.tsx | 118 ++++++----- .../components/sihirbaz-cagri-karti.tsx | 91 ++++----- src/lib/funnel-metinleri.ts | 36 ++++ 9 files changed, 451 insertions(+), 164 deletions(-) create mode 100644 src/components/piksel-cta-yuzeyi.tsx create mode 100644 src/components/piksel-liste.tsx create mode 100644 src/lib/funnel-metinleri.ts diff --git a/docs/tasarim/DESIGN.md b/docs/tasarim/DESIGN.md index dc0a159..e4d1585 100644 --- a/docs/tasarim/DESIGN.md +++ b/docs/tasarim/DESIGN.md @@ -589,3 +589,21 @@ Araştırma ve kaynaklar: `docs/tasarim/2026-09-25-samimi-arayuz-arastirmasi.md` Seri (streak), kayıp korkusu, puan/XP, rozet koleksiyonu, liderlik tablosu, maskot, sahte aciliyet/kıtlık, utandırarak onay. Tercih günlük alışkanlık değil, tek seferlik kaygılı bir karar. + +### 10.8 Funnel CTA yüzeyi ve 24'lük piksel liste +Bilal'in isteği (25 Eyl 2026): "CTA alanlarını arama içindeki alandan ilham, piksel detaylı yap; +pikseller bir kümede kümeleşmesin, interaktif kreatif olsun." +- **Yüzey — `PikselCtaYuzeyi`, `src/components/piksel-cta-yuzeyi.tsx`:** arama penceresindeki + kartın dili: `rounded-3xl border border-blue-400/20 bg-slate-950 text-white`, altta yazılıp + silinen satırlar (`FUNNEL_METINLERI`, `src/lib/funnel-metinleri.ts`), tek turuncu CTA. + Kullanıldığı yerler: ana sayfa kapanış CTA'sı, `/sonuc`'taki iki sihirbaz paneli. Eski + `bg-primary` + köşeye kümelenmiş `PixelField` bu yüzeylerde kullanılmaz. +- **Kreatif — `PikselListe`, `src/components/piksel-liste.tsx`:** 24 kare = 24 tercih, hero'nun + nokta ölçüsüyle (hücre 10, nokta 7.4, rx 2). Örnek modda liste kendini kurar (5 hayal · 13 + dengeli · 6 güvenli, 110 ms arayla), yalnız ekrandayken. İmleç yakın kareleri büyütür; kart + içindeki düğmeye imleç/odak gelince 24 kare birden dolar; dokunmada basılan kare ve komşuları + nabız atar (`kt-piksel-nabiz`). Hareketi azalt: dolu, durağan. Renk her zaman + `PikselListeAciklama` metniyle. +- **Gerçek liste:** çekmecede ilerleme çubuğunun yerinde öğrencinin kendi listesi (`gercek`, + 12×2): her kare o tercihin riskiyle, riski bilinmeyen beyaz; metin karşılığı yanındaki risk + dağılımında. diff --git a/src/app/globals.css b/src/app/globals.css index 6cee40b..2ba1976 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -303,3 +303,21 @@ mask-image: none; } } + +/* 24'lük piksel listede dokunma nabzı (bkz. components/piksel-liste.tsx): + basılan kare ve komşuları kısa bir büyüyüp söner. Tek seferlik, 420 ms. */ +@keyframes kt-piksel-nabiz { + 0% { + transform: scale(1); + } + 35% { + transform: scale(1.3); + } + 100% { + transform: scale(1); + } +} + +.kt-piksel-nabiz { + animation: kt-piksel-nabiz 0.42s cubic-bezier(0.23, 1, 0.32, 1); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 882eb68..0439e78 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -25,9 +25,9 @@ import { TurkeyPixelMap } from "@/features/pazarlama/components/turkey-pixel-map import { PixelCorner, PixelDivider, - PixelField, SectionEyebrow, } from "@/components/pixel-decor"; +import { PikselCtaYuzeyi } from "@/components/piksel-cta-yuzeyi"; import { MAX_REVIZYON, URUNLER } from "@/lib/credits"; import { VERI_YIL_SAYISI } from "@/lib/veri-kapsami"; import { @@ -681,28 +681,14 @@ export default function Home() {
    -
    - {/* Hero'nun piksel dokusu koyu zeminde: ortası boş, kenarlara doğru yoğun */} - {/* Piksel zemin panel içinde geride kalır; taşma payı için dikeyde büyütülür */} -
    - - - -
    -
    - -

    - Tercih listen, pişmanlık listesi olmasın -

    - -
    -
    + {/* Funnel CTA yüzeyi (DESIGN.md §10.8): köşede kümelenen + piksel zemini yerine 24'lük listenin interaktif kreatifi */} + +

    + Tercih listen, pişmanlık listesi olmasın +

    + +
    diff --git a/src/components/arama-funnel-karti.tsx b/src/components/arama-funnel-karti.tsx index 7fac0fe..2ca1dae 100644 --- a/src/components/arama-funnel-karti.tsx +++ b/src/components/arama-funnel-karti.tsx @@ -9,6 +9,7 @@ import { ListChecks, } from "lucide-react"; import { Button } from "@/components/ui/button"; +import { FUNNEL_METINLERI } from "@/lib/funnel-metinleri"; import { TypingAnimation } from "@/components/ui/typing-animation"; import { SIHIRBAZ_AC_EVENT, sonucHref } from "@/features/sihirbaz/sihirbaz-sabitler"; import { @@ -16,39 +17,6 @@ import { useTercihProfili, } from "@/features/liste/hooks/use-tercih-profili"; -const FUNNEL_METINLERI = [ - "Sıralamana en uygun listeyi birlikte kuralım.", - "Geleceğin 24 satırlık bir plana sığar.", - "Gerçek YÖK Atlas verisiyle güvenle seç.", - "Her tercihinin bir nedeni olsun.", - "Riskleri biz hesaplayalım, kararı sen ver.", - "Sana uygun bölümler bir adım uzağında.", - "Dengeli bir liste, rahat bir tercih dönemi.", - "Tercihlerini şansa bırakma.", - "Doğru strateji, doğru yerleşme.", - "Hayalindeki bölüm için doğru plan.", - "Verilerle konuşan bir tercih listesi.", - "Üç adımda sana özel 24 tercih.", - "Tercih dönemini stressiz geçir.", - "Geçen yılın verisi, senin avantajın.", - "Güvenli ve iddialı tercihleri dengele.", - "Sıralaman ne söylüyor? Birlikte bakalım.", - "Her satırı senin için hesaplıyoruz.", - "Akıllı tercih, içi rahat bir bekleyiş.", - "Yerleşme riskini görerek seç.", - "Sana özel liste birkaç dakikada hazır.", - "İlgi alanların ve şehrin, tek listede.", - "Boş kalan tercih, kaçan fırsattır.", - "Uzman işi bir liste, senin onayınla.", - "Kararını veriye dayandır.", - "Doğru tercih, doğru bilgiyle başlar.", - "Listeni bugün kur, için rahat olsun.", - "Yerleşme riskin renklerle önünde.", - "En önemli 24 kararını birlikte verelim.", - "Senin sıralaman, sana özel plan.", - "İyi bir liste, iyi bir başlangıçtır.", -]; - /** Fisher-Yates ile kopya üzerinde karıştırır; her açılışta farklı sıra. */ function karistir(dizi: readonly T[]): T[] { const kopya = [...dizi]; diff --git a/src/components/piksel-cta-yuzeyi.tsx b/src/components/piksel-cta-yuzeyi.tsx new file mode 100644 index 0000000..c9d75ad --- /dev/null +++ b/src/components/piksel-cta-yuzeyi.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { useState, type ReactNode } from "react"; +import { cn } from "@/lib/utils"; +import { TypingAnimation } from "@/components/ui/typing-animation"; +import { FUNNEL_METINLERI } from "@/lib/funnel-metinleri"; +import { PikselListe, PikselListeAciklama } from "@/components/piksel-liste"; + +/** Listeyi `ofset` kadar döndürür: aynı sayfadaki yüzeyler farklı satırla + * başlar. Rastgele değil, deterministik — sunucu ve istemci aynı çizer. */ +function dondur(dizi: readonly T[], ofset: number): T[] { + const n = ofset % dizi.length; + return [...dizi.slice(n), ...dizi.slice(0, n)]; +} + +/** + * Funnel CTA yüzeyi (DESIGN.md §10.8) — arama penceresindeki kartın dili: + * koyu slate-950 zemin, ince mavi kenar, yazılıp silinen kısa satırlar, + * turuncu CTA. Pikseller bir köşede kümelenmez; yüzeyin kreatifi 24'lük + * listenin kendisidir (PikselListe). Kartın içindeki bir düğmeye ya da + * linke imleç/odak gelince 24 kare birden dolar — "listen böyle hazır olur". + */ +export function PikselCtaYuzeyi({ + children, + className, + metinOfseti = 0, + kayanMetin = true, +}: { + children: ReactNode; + className?: string; + /** Aynı sayfadaki iki yüzey aynı satırla başlamasın */ + metinOfseti?: number; + kayanMetin?: boolean; +}) { + const [tamamla, setTamamla] = useState(false); + const [metinler] = useState(() => dondur(FUNNEL_METINLERI, metinOfseti)); + + const hedefMi = (t: EventTarget | null) => + t instanceof Element && t.closest("a, button") != null; + + return ( +
    setTamamla(hedefMi(e.target))} + onPointerLeave={() => setTamamla(false)} + onFocusCapture={(e) => setTamamla(hedefMi(e.target))} + onBlurCapture={() => setTamamla(false)} + className={cn( + "relative isolate overflow-hidden rounded-3xl border border-blue-400/20 bg-slate-950 text-white shadow-xl shadow-slate-900/10", + className, + )} + > +
    +
    + {children} + {kayanMetin ? ( + + ) : null} +
    +
    + + +
    +
    +
    + ); +} diff --git a/src/components/piksel-liste.tsx b/src/components/piksel-liste.tsx new file mode 100644 index 0000000..33ab0a9 --- /dev/null +++ b/src/components/piksel-liste.tsx @@ -0,0 +1,191 @@ +"use client"; + +import { + useCallback, + useEffect, + useRef, + useState, + type PointerEvent as ReactPointerEvent, + type ReactNode, +} from "react"; +import { useReducedMotion } from "motion/react"; +import { cn } from "@/lib/utils"; +import { DILIM_ETIKET } from "@/lib/risk"; + +/** + * 24'lük tercih listesinin piksel hâli — CTA yüzeylerinin interaktif + * kreatifi (Bilal'in isteği, 25 Eyl 2026: "pikseller bir köşede + * kümelenmesin, interaktif kreatif olsun"). Her kare bir tercih; hero'daki + * dot-matrix ölçüsüyle (hücre 10, nokta 7.4, rx 2). + * + * Davranış (DESIGN.md §10.8): + * - Örnek modda liste kendini kurar: kareler sırayla dolar (5 hayal · + * 13 dengeli · 6 güvenli), bir süre durur, sakin söner, yeniden başlar. + * Yalnız ekrandayken çalışır. + * - İmleç ızgaranın üstündeyken yakındaki kareler hafifçe büyür. + * - `tamamla` true ise (ör. CTA düğmesine gelince) 24 kare birden dolar. + * - Dokunmada kareye basınca o kare ve komşuları kısa bir nabız atar. + * - Hareketi azalt: dolu ve durağan ızgara. + * - `gercek` verilirse döngü yok; kareler öğrencinin kendi listesidir + * (her kare o tercihin riskiyle). + * Renk tek başına anlam taşımaz: çağıran `PikselListeAciklama` ile metni + * yanına koyar. + */ + +export type PikselKare = "hayal" | "dengeli" | "guvenli" | "belirsiz" | "bos"; + +const SUTUN = 6; +const SATIR = 4; +const TOPLAM = SUTUN * SATIR; +const CELL = 10; +const DOT = 7.4; +const INSET = (CELL - DOT) / 2; + +const ORNEK: PikselKare[] = Array.from({ length: TOPLAM }, (_, i) => + i < 5 ? "hayal" : i < 18 ? "dengeli" : "guvenli", +); + +const RENK: Record = { + hayal: "fill-red-500/85", + dengeli: "fill-amber-400/85", + guvenli: "fill-emerald-500/85", + belirsiz: "fill-white/55", + bos: "fill-white/15", +}; + +const ADIM_MS = 110; +const BEKLE_MS = 2600; + +export function PikselListe({ + tamamla = false, + gercek, + sutun = SUTUN, + className, +}: { + tamamla?: boolean; + /** Öğrencinin gerçek listesi (sırasıyla); verilirse örnek döngü kapanır */ + gercek?: PikselKare[]; + sutun?: number; + className?: string; +}) { + const hareketAzalt = useReducedMotion(); + const satir = Math.ceil(TOPLAM / sutun); + const svgRef = useRef(null); + const [dolu, setDolu] = useState(hareketAzalt || gercek ? TOPLAM : 0); + const [gorunur, setGorunur] = useState(false); + const [imlec, setImlec] = useState<{ x: number; y: number } | null>(null); + const [nabiz, setNabiz] = useState<{ i: number; t: number } | null>(null); + + // Yalnız ekrandayken çalış (IntersectionObserver, DESIGN.md §6) + useEffect(() => { + const el = svgRef.current; + if (!el) return; + const io = new IntersectionObserver(([e]) => setGorunur(e.isIntersecting), { + rootMargin: "0px 0px -10% 0px", + }); + io.observe(el); + return () => io.disconnect(); + }, []); + + // Örnek döngü: 0 → 24 sırayla, bekle, sıfırla + useEffect(() => { + if (gercek || hareketAzalt || !gorunur || tamamla) return; + const id = window.setTimeout( + () => setDolu((d) => (d >= TOPLAM ? 0 : d + 1)), + dolu >= TOPLAM ? BEKLE_MS : dolu === 0 ? 500 : ADIM_MS, + ); + return () => window.clearTimeout(id); + }, [dolu, gorunur, gercek, hareketAzalt, tamamla]); + + const kareler: PikselKare[] = gercek + ? Array.from({ length: TOPLAM }, (_, i) => gercek[i] ?? "bos") + : ORNEK.map((k, i) => (tamamla || i < dolu ? k : "bos")); + + const imlecGuncelle = useCallback((e: ReactPointerEvent) => { + if (e.pointerType !== "mouse") return; + const svg = svgRef.current; + if (!svg) return; + const r = svg.getBoundingClientRect(); + setImlec({ + x: ((e.clientX - r.left) / r.width) * sutun * CELL, + y: ((e.clientY - r.top) / r.height) * satir * CELL, + }); + }, [sutun, satir]); + + return ( + + ); +} + +/** Renklerin metin karşılığı (§2.2: renk tek başına anlam taşımaz). */ +export function PikselListeAciklama({ + className, + sayilar = { hayal: 5, dengeli: 13, guvenli: 6 }, +}: { + className?: string; + sayilar?: { hayal: number; dengeli: number; guvenli: number }; +}) { + const oge = (renk: string, sayi: number, etiket: string): ReactNode => ( + + + {sayi} {etiket.toLocaleLowerCase("tr-TR")} + + ); + return ( +

    + {oge("bg-red-500", sayilar.hayal, DILIM_ETIKET.hayal)} + {oge("bg-amber-400", sayilar.dengeli, DILIM_ETIKET.dengeli)} + {oge("bg-emerald-500", sayilar.guvenli, DILIM_ETIKET.garanti)} +

    + ); +} diff --git a/src/features/liste/components/liste-cekmecesi.tsx b/src/features/liste/components/liste-cekmecesi.tsx index dd066b1..c2d6ea5 100644 --- a/src/features/liste/components/liste-cekmecesi.tsx +++ b/src/features/liste/components/liste-cekmecesi.tsx @@ -15,7 +15,7 @@ import { useState } from "react"; import { toast } from "sonner"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { PixelField } from "@/components/pixel-decor"; +import { PikselListe } from "@/components/piksel-liste"; import { UniLogo } from "@/components/uni-logo"; import { RiskEtiketi } from "@/components/risk-etiketi"; import { Button } from "@/components/ui/button"; @@ -122,7 +122,6 @@ export function ListeCekmecesi() { // drawer içinde erken ölçüm satırları kaydırıyordu const [oturdu, setOturdu] = useState(false); - const doluluk = Math.round((liste.length / MANUEL_LISTE_MAX) * 100); // Risk dağılımı: riskliden güvenliye, sıfır olanlar yazılmaz const riskSayilari = (["riskli", "az-riskli", "guvenli"] as const) .map((r) => [r, liste.filter((t) => t.risk === r).length] as const) @@ -185,55 +184,66 @@ export function ListeCekmecesi() { Kendi Listem - {/* Durum ekran okuyucuya da duyurulur (WCAG 4.1.3) */} -

    - {liste.length === MANUEL_LISTE_MAX ? ( - // Nadir, hak edilmiş an: küçük giriş (Emil: scale 0.95 + opaklık, - // güçlü ease-out, <300 ms). Hareketi azalt'ta yalnız opaklık. - - - Listen tamam · {liste.length} / {MANUEL_LISTE_MAX} - - ) : ( - <> - {liste.length} / {MANUEL_LISTE_MAX} tercih - - )} -

    -
    - -
    - {/* Listenin dengesi metinle (renk tek başına değil, §2.2) */} - {riskSayilari.length > 0 ? ( -

    - {riskSayilari.map(([seviye, adet]) => ( - - - - {adet} + {/* Durum satırı: solda listenin piksel hâli (ilerleme çubuğunun + yerine), sağda sayı ve risk dağılımı — dikine uzamasın diye + yan yana (DESIGN.md §5.3). Durum ekran okuyucuya duyurulur. */} +

    +
    + + t.risk === "riskli" + ? "hayal" + : t.risk === "az-riskli" + ? "dengeli" + : t.risk === "guvenli" + ? "guvenli" + : "belirsiz", + )} + sutun={12} + /> +
    +
    +

    + {liste.length === MANUEL_LISTE_MAX ? ( + // Nadir, hak edilmiş an: küçük giriş (Emil: scale 0.95 + opaklık, + // güçlü ease-out, <300 ms). Hareketi azalt'ta yalnız opaklık. + + + Listen tamam · {liste.length} / {MANUEL_LISTE_MAX} - - ))} -

    - ) : null} + ) : ( + <> + {liste.length} / {MANUEL_LISTE_MAX} tercih + + )} +

    + {riskSayilari.length > 0 ? ( +

    + {riskSayilari.map(([seviye, adet]) => ( + + + + {adet} + + + ))} +

    + ) : null} +
    +
    {/* Denge ipucu: 24'lük iskeletin mantığı (alt sıralarda güvenli programlar). Yalnız riskleri bilinen ve en az 6 tercihli listede; vaat değil, gerekçe. */} {guvenliYok ? (

    - Güvenli program yok; alta birkaç tane eklemek açıkta kalma riskini - azaltır. + Güvenli yok; alta birkaç tane eklemek riski azaltır.

    ) : null} @@ -325,18 +335,14 @@ export function ListeCekmecesi() { {/* Telefonda alt alta (başlık tek satır, buton tam genişlik): yan yana dizince başlık üç satıra kırılıp listenin yerini yiyordu (24 Eyl 2026, 375 px ölçümü). */} -
    -
    - -
    -
    + {/* Funnel yüzeyi (DESIGN.md §10.8): arama penceresindeki kartın + koyu zemini; listenin piksel hâli başlıkta. */} +
    +

    {riskEksik ? "Risk analizi sıralamanı bekliyor" : "Risk analizini biz yaptık"}

    -

    +

    {riskEksik ? "Riskleri gösterelim; istersen 24'lük listeni de kur (3 kredi)." : "Her seçimin yerleşme riski hazır — son kararı sen ver."} @@ -345,7 +351,7 @@ export function ListeCekmecesi() { {riskEksik ? (

    ) : girisBekliyor ? ( -
    -
    - -
    -

    - Seçimlerin hazır — Yapay Zeka listen bir adım uzakta -

    -

    - Yukarıdaki tablo her zaman ücretsiz. 5 deneme kredinle 24 - tercihlik Yapay Zeka listeni de kurabilirsin: ücretsiz giriş yap, - seçimlerin kayıtlı — listen otomatik kurulsun (üretim 3 - kredi, kalan 2 kredinle danışmana soru sorarsın). -

    - -

    - Kredi kartı gerekmez · Google ya da e-posta ile 30 saniyede -

    -
    + + olay("giris_cta_tiklandi", { kaynak: "hazir_panel" }) + } + > + 5 deneme kredinle Yapay Zeka listeni kur — giriş yap + + + +

    + Kredi kartı gerekmez · Google ya da e-posta ile 30 saniyede +

    + +
    ) : ( -
    -
    - -
    -

    + +

    24 tercihi tek tek elemek yerine, yapay zekâ kursun

    -

    +

    Tablodan elle eklemek her zaman serbest. Ama üç adımda ilgi - alanlarını ve şehirlerini söylersen, yapay zekâ hayal, dengeli - ve güvenli dilimlerine dağıtılmış 24 tercihi — her satırı - yerleşme riski ve gerekçesiyle — senin yerine dizer. + alanlarını ve şehirlerini söylersen, yapay zekâ hayal, dengeli ve + güvenli dilimlerine dağıtılmış 24 tercihi — her satırı yerleşme + riski ve gerekçesiyle — senin yerine dizer.

    -

    + )} {monteEdildi ? ( diff --git a/src/lib/funnel-metinleri.ts b/src/lib/funnel-metinleri.ts new file mode 100644 index 0000000..d296c39 --- /dev/null +++ b/src/lib/funnel-metinleri.ts @@ -0,0 +1,36 @@ +/** + * CTA yüzeylerinde yazılıp silinen kısa satırlar (arama penceresindeki + * kart ve PikselCtaYuzeyi). "use client" yok: sunucu da okuyabilsin (§9.3). + */ +export const FUNNEL_METINLERI: readonly string[] = [ + "Sıralamana en uygun listeyi birlikte kuralım.", + "Geleceğin 24 satırlık bir plana sığar.", + "Gerçek YÖK Atlas verisiyle güvenle seç.", + "Her tercihinin bir nedeni olsun.", + "Riskleri biz hesaplayalım, kararı sen ver.", + "Sana uygun bölümler bir adım uzağında.", + "Dengeli bir liste, rahat bir tercih dönemi.", + "Tercihlerini şansa bırakma.", + "Doğru strateji, doğru yerleşme.", + "Hayalindeki bölüm için doğru plan.", + "Verilerle konuşan bir tercih listesi.", + "Üç adımda sana özel 24 tercih.", + "Tercih dönemini stressiz geçir.", + "Geçen yılın verisi, senin avantajın.", + "Güvenli ve iddialı tercihleri dengele.", + "Sıralaman ne söylüyor? Birlikte bakalım.", + "Her satırı senin için hesaplıyoruz.", + "Akıllı tercih, içi rahat bir bekleyiş.", + "Yerleşme riskini görerek seç.", + "Sana özel liste birkaç dakikada hazır.", + "İlgi alanların ve şehrin, tek listede.", + "Boş kalan tercih, kaçan fırsattır.", + "Uzman işi bir liste, senin onayınla.", + "Kararını veriye dayandır.", + "Doğru tercih, doğru bilgiyle başlar.", + "Listeni bugün kur, için rahat olsun.", + "Yerleşme riskin renklerle önünde.", + "En önemli 24 kararını birlikte verelim.", + "Senin sıralaman, sana özel plan.", + "İyi bir liste, iyi bir başlangıçtır.", +]; From 4d3bebab76d67a5b9616e0114963a026ba4b2961 Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Thu, 24 Sep 2026 02:14:04 +0300 Subject: [PATCH 23/23] =?UTF-8?q?feat(tasarim):=20katalog=20sayfas=C4=B1?= =?UTF-8?q?=20alt=20CTA=20kart=C4=B1=20funnel=20y=C3=BCzeyinde=20(piksel?= =?UTF-8?q?=20liste)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- docs/tasarim/DESIGN.md | 4 +- .../sihirbaz/components/cta-sira-form.tsx | 95 ++++++++++++------- 2 files changed, 64 insertions(+), 35 deletions(-) diff --git a/docs/tasarim/DESIGN.md b/docs/tasarim/DESIGN.md index e4d1585..3268a68 100644 --- a/docs/tasarim/DESIGN.md +++ b/docs/tasarim/DESIGN.md @@ -596,7 +596,9 @@ pikseller bir kümede kümeleşmesin, interaktif kreatif olsun." - **Yüzey — `PikselCtaYuzeyi`, `src/components/piksel-cta-yuzeyi.tsx`:** arama penceresindeki kartın dili: `rounded-3xl border border-blue-400/20 bg-slate-950 text-white`, altta yazılıp silinen satırlar (`FUNNEL_METINLERI`, `src/lib/funnel-metinleri.ts`), tek turuncu CTA. - Kullanıldığı yerler: ana sayfa kapanış CTA'sı, `/sonuc`'taki iki sihirbaz paneli. Eski + Kullanıldığı yerler: ana sayfa kapanış CTA'sı, `/sonuc`'taki iki sihirbaz paneli, katalog + sayfalarının altındaki `CtaSiraForm` kart varyantı (üstteki kompakt satır form olduğu için + açık zeminde kalır). Eski `bg-primary` + köşeye kümelenmiş `PixelField` bu yüzeylerde kullanılmaz. - **Kreatif — `PikselListe`, `src/components/piksel-liste.tsx`:** 24 kare = 24 tercih, hero'nun nokta ölçüsüyle (hücre 10, nokta 7.4, rx 2). Örnek modda liste kendini kurar (5 hayal · 13 diff --git a/src/features/sihirbaz/components/cta-sira-form.tsx b/src/features/sihirbaz/components/cta-sira-form.tsx index 7de60aa..e07561d 100644 --- a/src/features/sihirbaz/components/cta-sira-form.tsx +++ b/src/features/sihirbaz/components/cta-sira-form.tsx @@ -29,6 +29,7 @@ import { import { authClient } from "@/lib/auth-client"; import { olay, siraKovasi } from "@/lib/analitik"; import { cn } from "@/lib/utils"; +import { PikselCtaYuzeyi } from "@/components/piksel-cta-yuzeyi"; import { onbellekliJsonGetir } from "@/lib/istek-cache"; import { useTercihProfili } from "@/features/liste/hooks/use-tercih-profili"; import type { SihirbazFacetleri } from "@/types/yokatlas"; @@ -61,13 +62,35 @@ function turEtiketi(tur: string) { } /** "Sayısal · 100.000. sıra" — kayıtlı profilin kısa özeti. */ -function KayitliSira({ sira, tur }: { sira: number; tur: string }) { +function KayitliSira({ + sira, + tur, + koyu = false, +}: { + sira: number; + tur: string; + /** Koyu funnel yüzeyinde (PikselCtaYuzeyi) */ + koyu?: boolean; +}) { return ( - - + + Sıralaman kayıtlı:{" "} - + {sira.toLocaleString("tr-TR")}. {" "} sıra · {turEtiketi(tur)} @@ -424,54 +447,58 @@ export function CtaSiraForm({ ); } + // Kart varyantı sayfa sonundaki funnel CTA'sıdır: arama kartının koyu + // yüzeyi + 24'lük piksel liste (DESIGN.md §10.8). if (profil) { return ( -
    -

    + +

    {baslik ?? "Sıralamana yakın programlar hangileri? Saniyede gör."}

    - +
    -

    +

    Gerçek YÖK Atlas verisiyle hayal, dengeli ve güvenli dilimlerindeki programların hazır.

    - - -

    + ); } return ( -
    -

    - {baslik ?? "Sıralamana yakın programlar hangileri? Saniyede gör."} -

    -

    - {aciklama ?? - "YKS başarı sıralamanı gir; gerçek YÖK Atlas verisiyle sıralamana uygun hayal, dengeli ve güvenli dilimlerindeki programları anında listele. Ücretsiz, üyelik gerekmez."} -

    - - +
    + +

    + {baslik ?? "Sıralamana yakın programlar hangileri? Saniyede gör."} +

    +

    + {aciklama ?? + "YKS başarı sıralamanı gir; gerçek YÖK Atlas verisiyle sıralamana uygun hayal, dengeli ve güvenli dilimlerindeki programları anında listele. Ücretsiz, üyelik gerekmez."} +

    + +
    {cekmece} -
    +
    ); }