From 259534fa9cb0613a0c36929236e50af25340098e Mon Sep 17 00:00:00 2001 From: bilalgursen Date: Thu, 24 Sep 2026 14:56:11 +0300 Subject: [PATCH] =?UTF-8?q?feat(katalog+tasarim):=20=C5=9Fehir=20adlar?= =?UTF-8?q?=C4=B1=20her=20yerde=20link;=20piksel=20y=C3=BCzeylere=20beyaz?= =?UTF-8?q?=20ton?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ilSayfaHref: üniversite sayfası il hapı, /universiteler listesi, bölüm / sonuç / listem / tadımlık satırlarındaki il metası şehir sayfasına gider. - Şehir kümeleri tüm (üniversite, il) çiftlerinden: DB'deki her il değerinin sayfası var (ODTÜ Ankara + KKTC); dev'de eski globalThis önbelleğine dayanıklı. - PikselZemin ton="acik", CtaBandi/CtaSiraForm ton="beyaz": /universiteler altı ve /meraklisina kapanışı beyaz, diğer bantlar mavi (dönüşümlü). - ilUlkeHaritasi: il değerinden ülke haritası + şehir konumu. Co-Authored-By: Claude Fable 5.1 --- src/components/cta-bandi.tsx | 18 ++++++- src/components/piksel-zemin.tsx | 9 +++- .../components/bolum-program-listesi.tsx | 12 +++-- .../katalog/components/universite-icerik.tsx | 12 +++-- .../katalog/components/universite-listesi.tsx | 42 ++++++++++----- .../liste/components/program-tablosu.tsx | 12 ++++- .../pazarlama/components/meraklisina-demo.tsx | 2 +- .../rapor/components/rapor-listesi.tsx | 12 ++++- .../rapor/components/tadimlik-satiri.tsx | 12 ++++- .../sihirbaz/components/cta-sira-form.tsx | 18 +++++-- src/lib/harita-ulkeler.ts | 33 ++++++++++++ src/lib/katalog.ts | 52 +++++++++++++------ src/lib/slug.ts | 8 +++ 13 files changed, 193 insertions(+), 49 deletions(-) diff --git a/src/components/cta-bandi.tsx b/src/components/cta-bandi.tsx index 3751e71..affd356 100644 --- a/src/components/cta-bandi.tsx +++ b/src/components/cta-bandi.tsx @@ -11,6 +11,11 @@ import { cn } from "@/lib/utils"; * * - `buyuk` (varsayılan): sayfa kapanışı / panel — ortalı, iri boşluk. * - `kompakt`: çekmece altı gibi dar yerler — sola yaslı, alçak. + * - `ton="beyaz"`: aynı bant beyaz zeminde — slate kenarlık, koyu yazı, + * slate pikseller. Aynı sayfada üst üste iki mavi bant olmasın diye + * (Bilal, 24 Eyl 2026: "her kısımda mavi arka kullanmışsın, çeşitlendir"). + * Çağıran paragraf renklerini tona göre verir (`text-white/90` ↔ + * `text-slate-600`). * * İçerik (başlık, paragraf, buton) çağıranındır; yalnız kabuk burada durur. * Bilal'in kararı (24 Eyl 2026): koyu slate-950 yüzey yerine bu band. @@ -20,6 +25,7 @@ export function CtaBandi({ className, seed = 8, boyut = "buyuk", + ton = "mavi", etiket = "section", ref, ...props @@ -27,6 +33,7 @@ export function CtaBandi({ /** Piksel dokusunun deseni; aynı sayfadaki iki band farklı olsun */ seed?: number; boyut?: "buyuk" | "kompakt"; + ton?: "mavi" | "beyaz"; /** Çekmece altı gibi başlıksız yerlerde `div` */ etiket?: "section" | "div"; ref?: Ref; @@ -38,13 +45,20 @@ export function CtaBandi({ } className={cn( - "relative isolate overflow-hidden rounded-2xl bg-primary text-white", + "relative isolate overflow-hidden rounded-2xl", + ton === "beyaz" + ? "border border-slate-200 bg-white text-slate-900" + : "bg-primary text-white", buyuk ? "px-5 py-8 text-center sm:p-12" : "px-5 py-4 text-left sm:py-5", className, )} {...props} > - + {children} ); diff --git a/src/components/piksel-zemin.tsx b/src/components/piksel-zemin.tsx index d9788cf..d979ca5 100644 --- a/src/components/piksel-zemin.tsx +++ b/src/components/piksel-zemin.tsx @@ -21,8 +21,14 @@ export function PikselZemin({ yogunluk, kenar, haric, + ton = "koyu", className, }: { + /** + * `koyu`: renkli (mavi) zemin, kareler beyaz. `acik`: beyaz zemin, kareler + * slate (Bilal, 24 Eyl 2026: "beyaz arka planda piksel de güzel görünüyor"). + */ + ton?: "koyu" | "acik"; /** Aynı ekrandaki iki yüzey farklı desen alsın */ seed?: number; /** Doluluk çarpanı (1 varsayılan) */ @@ -38,7 +44,8 @@ export function PikselZemin({
diff --git a/src/features/katalog/components/bolum-program-listesi.tsx b/src/features/katalog/components/bolum-program-listesi.tsx index 7420e18..311bb9a 100644 --- a/src/features/katalog/components/bolum-program-listesi.tsx +++ b/src/features/katalog/components/bolum-program-listesi.tsx @@ -4,7 +4,8 @@ import { startTransition, useMemo, useRef, useState } from "react"; import Link from "next/link"; import { ChevronDown, ChevronUp } from "lucide-react"; import type { Program } from "@/types/yokatlas"; -import { trBaslikDuzeni } from "@/lib/slug"; +import { ilSayfaHref, trBaslikDuzeni } from "@/lib/slug"; +import { cn } from "@/lib/utils"; import { UNITUR_ETIKET } from "@/lib/unitur"; import { VERI_YIL_SAYISI } from "@/lib/veri-yillari"; import { Sayfalama, SAYFA_BOYU } from "./sayfalama"; @@ -23,6 +24,7 @@ import { SATIR_BUTON_KABI, SATIR_CIP, SATIR_GRUP, + SATIR_IKINCIL_LINK, SATIR_META, SATIR_MOBIL_SAYI, } from "@/components/liste-satiri"; @@ -247,9 +249,13 @@ export function BolumProgramListesi({ ) : null} {p.il ? ( - + {trBaslikDuzeni(p.il)} - + ) : null} {p.sure ? ( {p.sure} yıl diff --git a/src/features/katalog/components/universite-icerik.tsx b/src/features/katalog/components/universite-icerik.tsx index af54056..948c0d8 100644 --- a/src/features/katalog/components/universite-icerik.tsx +++ b/src/features/katalog/components/universite-icerik.tsx @@ -18,7 +18,7 @@ import { getUniversiteBySlug, bolumSlugFromIsim, } from "../katalog-queries"; -import { trBaslikDuzeni } from "@/lib/slug"; +import { ilSayfaHref, trBaslikDuzeni } from "@/lib/slug"; import { UNITUR_ETIKET, uniturDevletMi } from "@/lib/unitur"; import { JsonLd, @@ -579,10 +579,16 @@ export function UniversiteIcerik({ ) : null} {uni.il ? ( - + // İl hapı şehir sayfasına gider (Bilal, 24 Eyl 2026: "şehirlerin + // geçtiği her yer tıklanabilir"); hover §2.1 tek turuncu. + {trBaslikDuzeni(uni.il)} - + ) : null}

) : null} diff --git a/src/features/katalog/components/universite-listesi.tsx b/src/features/katalog/components/universite-listesi.tsx index ff3412e..8faa447 100644 --- a/src/features/katalog/components/universite-listesi.tsx +++ b/src/features/katalog/components/universite-listesi.tsx @@ -4,7 +4,7 @@ import { getAllUniversiteler, type UniOzet, } from "../katalog-queries"; -import { trBaslikDuzeni } from "@/lib/slug"; +import { ilSayfaHref, trBaslikDuzeni } from "@/lib/slug"; import { SehirCipleri, oneCikanIller } from "./sehir-cipleri"; import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form"; import { PagePixelDivider } from "@/components/pixel-decor"; @@ -15,25 +15,41 @@ import { SON_YIL, TERCIH_YILI } from "@/lib/veri-yillari"; const sayi = (n: number) => n.toLocaleString("tr-TR"); export function UniLinkleri({ uniler }: { uniler: UniOzet[] }) { + // Ad ve il ayrı linkler (iç içe olmaz): logo + ad üniversiteye, alt + // satırdaki şehir adı şehir sayfasına (Bilal, 24 Eyl 2026: "şehirlerin + // geçtiği her yer tıklanabilir"). Hover §2.1 tek turuncu. return (
    {uniler.map((u) => ( -
  • +
  • - - - {u.ad} - - - {u.il ? `${trBaslikDuzeni(u.il)} · ` : ""} - {sayi(u.programSayisi)} program - - + + {u.ad} + + + {u.il ? ( + <> + + {trBaslikDuzeni(u.il)} + + {" · "} + + ) : null} + {sayi(u.programSayisi)} program +
  • ))}
@@ -146,7 +162,7 @@ export function UniversiteKatalogu() {
- +
); diff --git a/src/features/liste/components/program-tablosu.tsx b/src/features/liste/components/program-tablosu.tsx index 88e249f..9398c4e 100644 --- a/src/features/liste/components/program-tablosu.tsx +++ b/src/features/liste/components/program-tablosu.tsx @@ -21,7 +21,8 @@ import type { RankResultsListe, } from "@/types/yokatlas"; import { DILIM_ETIKET, riskHesapla } from "@/lib/risk"; -import { bolumSayfaSlug, trBaslikDuzeni, uniSayfaSlug } from "@/lib/slug"; +import { bolumSayfaSlug, ilSayfaHref, trBaslikDuzeni, uniSayfaSlug } from "@/lib/slug"; +import { cn } from "@/lib/utils"; import { VERI_YIL_SAYISI } from "@/lib/veri-yillari"; import { ProgramSiraGecmisi, @@ -374,7 +375,14 @@ function ProgramSatiri({ {devlet ? "Devlet" : "Vakıf"} {p.il ? ( - {trBaslikDuzeni(p.il)} + e.stopPropagation()} + > + {trBaslikDuzeni(p.il)} + ) : null} {p.sure ? ( {p.sure} yıl diff --git a/src/features/pazarlama/components/meraklisina-demo.tsx b/src/features/pazarlama/components/meraklisina-demo.tsx index 63b2c9a..e5e25b4 100644 --- a/src/features/pazarlama/components/meraklisina-demo.tsx +++ b/src/features/pazarlama/components/meraklisina-demo.tsx @@ -239,7 +239,7 @@ export function DemoKapanisCta() { // §10.8; Bilal, 24 Eyl 2026: "burda piksel dokuları yok"). Tek başlık, tek // turuncu buton; sayfadaki diğer bantlardan farklı seed. return ( - +

{profil ? "Kuralları öğrendin — sayılar zaten senin sıralamanla hesaplandı." diff --git a/src/features/rapor/components/rapor-listesi.tsx b/src/features/rapor/components/rapor-listesi.tsx index ecfea38..0464cc1 100644 --- a/src/features/rapor/components/rapor-listesi.tsx +++ b/src/features/rapor/components/rapor-listesi.tsx @@ -26,7 +26,8 @@ import { TableHeader, TableRow, } from "@/components/ui/table"; -import { bolumSayfaSlug, trBaslikDuzeni, uniSayfaSlug } from "@/lib/slug"; +import { bolumSayfaSlug, ilSayfaHref, trBaslikDuzeni, uniSayfaSlug } from "@/lib/slug"; +import { cn } from "@/lib/utils"; import { UNITUR_ETIKET } from "@/lib/unitur"; import { RiskEtiketi } from "@/components/risk-etiketi"; import { @@ -266,7 +267,14 @@ export function RaporListesi({ {devlet ? "Devlet" : "Vakıf"} ) : null} {p?.il ? ( - {trBaslikDuzeni(p.il)} + e.stopPropagation()} + > + {trBaslikDuzeni(p.il)} + ) : null} diff --git a/src/features/rapor/components/tadimlik-satiri.tsx b/src/features/rapor/components/tadimlik-satiri.tsx index 73f2c93..27cb874 100644 --- a/src/features/rapor/components/tadimlik-satiri.tsx +++ b/src/features/rapor/components/tadimlik-satiri.tsx @@ -24,12 +24,14 @@ import { import { useGorunumOlayi } from "@/components/use-gorunum-olayi"; import { olay, siraKovasi } from "@/lib/analitik"; import { dilimdenRisk, riskHesapla } from "@/lib/risk"; -import { trBaslikDuzeni, turkishSlugify } from "@/lib/slug"; +import { ilSayfaHref, trBaslikDuzeni, turkishSlugify } from "@/lib/slug"; +import { cn } from "@/lib/utils"; import { SATIR_ALT_AD, SATIR_AD_DUZ, SATIR_CIP, SATIR_ALT_GRUP, + SATIR_IKINCIL_LINK, SATIR_META, } from "@/components/liste-satiri"; import { listemUretHref, PROFIL_DEGISTI_EVENT } from "@/features/sihirbaz/sihirbaz-sabitler"; @@ -166,7 +168,13 @@ export function TadimlikSatiri({ {devlet ? "Devlet" : "Vakıf"} ) : null} {p.il ? ( - {trBaslikDuzeni(p.il)} + + {trBaslikDuzeni(p.il)} + ) : null} diff --git a/src/features/sihirbaz/components/cta-sira-form.tsx b/src/features/sihirbaz/components/cta-sira-form.tsx index b854bb9..e85a1c4 100644 --- a/src/features/sihirbaz/components/cta-sira-form.tsx +++ b/src/features/sihirbaz/components/cta-sira-form.tsx @@ -101,6 +101,9 @@ function KayitliSira({ * tabloya bağlayan köprü cümlesini buradan verir. Başlık ve açıklamalar * yerleşme vaadi içermez: ürün sıralamaya yakın programları listeler. * + * `ton="beyaz"`: kart varyantının beyaz zeminli bandı (aynı sayfada iki mavi + * bant üst üste gelmesin diye; DESIGN.md §10.8). + * * `varyant="kompakt"`: katalog sayfalarında başlığın hemen altına, tablonun * ÜSTÜNE konan tek satırlık sürüm. Sıralama alanı çekmecede değil doğrudan * sayfadadır (tek sayfada çıkan ziyaretçi alttaki karta hiç inmiyor); gönderince @@ -113,10 +116,13 @@ export function CtaSiraForm({ aciklama, varyant = "kart", konum = "alt", + ton = "mavi", }: { baslik?: string; aciklama?: string; varyant?: "kart" | "kompakt"; + /** Yalnız kart varyantı: bandın zemini */ + ton?: "mavi" | "beyaz"; /** Yalnız ölçüm içindir: formun sayfadaki yeri (tablonun üstü / altı). */ konum?: "ust" | "alt"; }) { @@ -427,16 +433,18 @@ export function CtaSiraForm({ } // Kart varyantı sayfa sonundaki CTA bandıdır (DESIGN.md §10.8, CtaBandi). + const beyaz = ton === "beyaz"; + const paragrafSinifi = beyaz ? "text-slate-600" : "text-white/90"; 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.

@@ -456,11 +464,11 @@ export function CtaSiraForm({ 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."}

diff --git a/src/lib/harita-ulkeler.ts b/src/lib/harita-ulkeler.ts index 687d2b6..d9ecdd9 100644 --- a/src/lib/harita-ulkeler.ts +++ b/src/lib/harita-ulkeler.ts @@ -180,6 +180,39 @@ export function yurtdisiUniKonum( return cozum && "anahtar" in cozum ? sehirKonumu(cozum.anahtar) : null; } +/** + * Yalnızca il değerinden ülke haritası (şehir sayfası). "KIBRIS" genel + * değeri ülke seviyesinde kalır; "ÜSKÜP-MAKEDONYA" gibi ülke ekli değerler + * ilk parçasıyla çözülür. Türkiye ili ya da bilinmeyen değer → null. + */ +export function ilUlkeHaritasi(il: string): { + ulke: UlkeKodu; + ulkeAd: string; + harita: UlkeHarita; + sehir: YurtdisiKonum | null; +} | null { + const anahtar = + il in SEHIRLER + ? (il as SehirAnahtari) + : (() => { + const ilk = il.split("-")[0].trim(); + return ilk in SEHIRLER ? (ilk as SehirAnahtari) : null; + })(); + const ulke: UlkeKodu | null = anahtar + ? SEHIRLER[anahtar].ulke + : il === "KIBRIS" + ? "kktc" + : null; + if (!ulke) return null; + const harita = ULKE_HARITALARI[ulke]; + return { + ulke, + ulkeAd: harita.ad, + harita, + sehir: anahtar ? sehirKonumu(anahtar) : null, + }; +} + /** Yalnızca il değerinden (kilitli satırlar) yurtdışı şehir konumu. */ export function yurtdisiIlKonum(il: string | null): YurtdisiKonum | null { return il && il in SEHIRLER ? sehirKonumu(il as SehirAnahtari) : null; diff --git a/src/lib/katalog.ts b/src/lib/katalog.ts index 00871d8..0ed6656 100644 --- a/src/lib/katalog.ts +++ b/src/lib/katalog.ts @@ -64,6 +64,8 @@ const globalKatalog = globalThis as unknown as { uniOzetler?: UniOzet[]; iller?: IlOzet[]; ilMap?: Map; + /** slug → (il → o ildeki program sayısı); getAllUniversiteler doldurur */ + uniIlAdetleri?: Map>; }; function bolumMapGetir(): Map { @@ -335,6 +337,7 @@ export function getAllUniversiteler(): UniOzet[] { } sayac.set(row.il, (sayac.get(row.il) ?? 0) + row.adet); } + globalKatalog.uniIlAdetleri = ilAdetleri; for (const [slug, sayac] of ilAdetleri) { let enIyi: string | null = null; let enCok = 0; @@ -378,7 +381,9 @@ export type IlOzet = { slug: string; /** Görünen ad: İstanbul, Bakü-Azerbaycan */ ad: string; - /** Bu ilde yer alan üniversiteler (UniOzet.il esas; ad sırasıyla) */ + /** DB biçiminde ham il ("İSTANBUL"); harita anahtarı */ + il: string; + /** Bu ilde programı olan üniversiteler (ad sırasıyla) */ uniler: UniOzet[]; programSayisi: number; }; @@ -395,25 +400,42 @@ function ilGorunenAd(il: string): string { /** * Üniversiteleri iline göre kümeler; sıralama üniversite sayısına göre - * azalan, eşitlikte ad. Her üniversite tek ilde sayılır (UniOzet.il: en çok - * programının bulunduğu il). İli olmayan (NULL/boş) üniversite hiçbir ile - * girmez. KKTC ve yurt dışı şehirler de birer ildir (LEFKOŞA, BAKÜ-…). + * azalan, eşitlikte ad. Bir üniversite **programı olan her ilde** sayılır + * (ODTÜ hem Ankara'da hem KKTC'de; öğrenci "KKTC'de okuyabileceğim + * üniversiteler" diye bakar) — böylece DB'deki her il değerinin sayfası + * vardır ve program satırlarındaki her şehir adı güvenle link olur + * (`ilSayfaHref`). `programSayisi` o ildeki program sayısıdır. */ export function getAllIller(): IlOzet[] { if (!globalKatalog.iller) { + // Dev'de globalThis önbelleği HMR'ı aşar: uniOzetler eski modül koduyla + // kurulduysa il sayacı yoktur; özetler yeniden kurulur. + if (!globalKatalog.uniIlAdetleri) globalKatalog.uniOzetler = undefined; + const uniler = getAllUniversiteler(); + const ilAdetleri = globalKatalog.uniIlAdetleri!; const bySlug = new Map(); - for (const u of getAllUniversiteler()) { - const il = u.il?.trim(); - if (!il) continue; - const slug = turkishSlugify(il); - if (!slug) continue; - let kayit = bySlug.get(slug); - if (!kayit) { - kayit = { slug, ad: ilGorunenAd(il), uniler: [], programSayisi: 0 }; - bySlug.set(slug, kayit); + for (const u of uniler) { + const sayac = ilAdetleri.get(u.slug); + if (!sayac) continue; + for (const [ham, adet] of sayac) { + const il = ham.trim(); + if (!il) continue; + const slug = turkishSlugify(il); + if (!slug) continue; + let kayit = bySlug.get(slug); + if (!kayit) { + kayit = { + slug, + ad: ilGorunenAd(il), + il, + uniler: [], + programSayisi: 0, + }; + bySlug.set(slug, kayit); + } + kayit.uniler.push(u); + kayit.programSayisi += adet; } - kayit.uniler.push(u); - kayit.programSayisi += u.programSayisi; } globalKatalog.ilMap = bySlug; globalKatalog.iller = [...bySlug.values()].sort( diff --git a/src/lib/slug.ts b/src/lib/slug.ts index 6988a89..3c3cc28 100644 --- a/src/lib/slug.ts +++ b/src/lib/slug.ts @@ -52,6 +52,14 @@ export function bolumSayfaSlug(isim: string): string { return turkishSlugify(bolumBazAdi(isim)); } +/** + * /universiteler/[il] şehir sayfası adresi. Şehir sayfaları DB'deki her il + * değeri için üretildiğinden (katalog.ts getAllIller) adres her zaman vardır. + */ +export function ilSayfaHref(il: string): string { + return `/universiteler/${turkishSlugify(il)}`; +} + /** ALL-CAPS veritabanı adlarını tr-TR başlık düzenine çevirir (İSTANBUL → İstanbul). */ export function trBaslikDuzeni(s: string): string { return s