feat(katalog+tasarim): devlet/vakıf tür sayfaları, tür çipleri link; kayıtlı-sıra satırı şişko ve sade
All checks were successful
Deploy / deploy (push) Successful in 5m1s

- /universiteler/devlet ve /universiteler/vakif: şehir sayfası iskeleti,
  haritada bu türden üniversitesi olan her il turuncu (yakınlaşma yok), üç
  özet kartı, şehir çipleri (sayfa içi çapa), şehir şehir §4.15 satırları,
  öteki türe geçiş hapı; metadata + sitemap.
- TurCipi tek kaynak: devlet/vakıf çipi tür sayfasına link (sonuç, listem,
  tadımlık, bölüm, şehir/tür satırları); üniversite sayfası tür hapı ve
  /universiteler başlıkları da link. UniSatirlari ortak dosyada: ad, tür ve
  il ayrı linkler.
- Kompakt profilli satır (DESIGN.md §10.9): iri kupa kutusu + text-3xl/4xl
  sıra, text-xl/2xl başlık, 48 px buton; telefonda alt alta, lg'de tek satır.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-24 15:53:54 +03:00
parent a0e94bc024
commit a43c045828
17 changed files with 606 additions and 109 deletions

View File

@@ -352,8 +352,8 @@ aşağı kaymış, kare boyları farklı — ne tepeler ne bitişler hizalı.**
Başlığın altında kurumun kimliği; navbar haplarının ölçüsünde:
`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`
+ başta `size-4 text-primary` ikon (`Landmark` devlet, `Building2` vakıf, `MapPin` il). Tür
renkle değil ikonla ayrılır. Rozet (§4.2) değildir. Tür hapı etkileşimsizdir; **il hapı şehir
sayfasına link**tir (Bilal, 24 Eyl 2026: "şehirlerin geçtiği her yer tıklanabilir") — aynı
renkle değil ikonla ayrılır. Rozet (§4.2) değildir. **Devlet/vakıf hapı tür sayfasına**
(§4.18), **il hapı şehir sayfasına link**tir (Bilal, 24 Eyl 2026: "şehirlerin geçtiği her yer tıklanabilir") — aynı
sınıf + `hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500 active:scale-[0.97]`.
Kural genel: bir şehir adı nerede geçerse geçsin (`/universiteler` listesi, bölüm/sonuç/listem/
tadımlık satırlarındaki tür-il-süre metası) `ilSayfaHref(il)` ile `/universiteler/[il]`'e
@@ -493,6 +493,22 @@ bulunulan şehir `border-primary bg-primary text-white` + `aria-current="page"`.
üst satır). Kareler büyür, il kadraja gelir; ısı fırçası viewBox'ı
canlı okur. Hareketi azalt açıkken kutuya doğrudan atlanır. Sayfa başına tek yakınlaşma.
### 4.18 Tür sayfaları — `/universiteler/devlet`, `/universiteler/vakif` (`tur-icerik.tsx`)
Bilal (24 Eyl 2026): "vakıf ve devlet de tıklanabilir olmalı, özel sayfalar yapabilirsin,
kreatif". Şehir sayfasının iskeleti (§4.17) ile: yol satırı, `SehirYuzeyi` başlık yüzeyi,
kimlik hapları (tür + öteki türe geçiş hapı), özet paragraf, kompakt `CtaSiraForm`, üç
`OzetKart` (üniversite / program / şehir; `grid grid-cols-2 gap-4 lg:grid-cols-3`, üçüncü
telefonda tam genişlik), "Şehre göre" çipleri (sayfa içi `#sehir-…` çapaları), şehir şehir
§4.15 `UniSatirlari`, altta mavi CTA bandı.
- **Kreatif:** başlık yüzeyindeki dot-matrix Türkiye'de bu türden üniversitesi olan **her il**
turuncu yanar (`TurkiyePikselHaritasi iller=[…]`), yakınlaşma yok — "devlet üniversiteleri
Türkiye'nin neresinde" tek bakışta. Vakıfta harita seyrek yanar; bu bilginin kendisidir.
- Tür çipi tek kaynak `TurCipi` (`src/components/tur-cipi.tsx`): devlet/vakıf (ve Vakıf MYO)
tür sayfasına link, KKTC/yurt dışı düz çip; `/sonuc`, `/listem`, tadımlık, bölüm sayfası ve
şehir/tür satırları bunu kullanır. Satırın tamamı link olan yerde kullanılmaz.
- `/universiteler` dizinindeki "Devlet Üniversiteleri" / "Vakıf Üniversiteleri" başlıkları da
tür sayfasına link.
## 5. Yerleşim
### 5.1 Genel
@@ -812,3 +828,14 @@ yok** (Bilal, 24 Eyl 2026: kaldırıldı); `FUNNEL_METINLERI` yalnız arama penc
- Çekmece başlığında denenen "gerçek liste" pikseli (12×2, her kare tercihin riski) **kaldırıldı**:
başlığı çok dolu gösteriyordu (Bilal, 24 Eyl 2026). Başlık sayı + ilerleme çubuğu + risk
dağılımı metniyle kalır.
### 10.9 Kayıtlı sıra satırı (katalog üstü, profilli) — `cta-sira-form.tsx` kompakt
Bilal (24 Eyl 2026): "bu kısım UI kötü, Apple görünümü yok, yazılar küçük". §1.1 "sayı
kahramandır" uygulanır: mavi bantta (§10.8 kompakt kabuk + `PikselZemin`) **iri kupa kutusu**
(`flex size-14 items-center justify-center rounded-2xl bg-white/15` + `Trophy size-7`), yanında
sıra `font-heading text-3xl font-bold leading-none tracking-tight sm:text-4xl` (Cal Sans'ta
tabular-nums yok, §3) ve
altında `text-sm font-semibold text-white/80` "sıra · Sayısal"; başlık `font-heading text-xl
font-bold leading-tight text-balance sm:text-2xl`; buton 48 px turuncu, ok sonda. Telefonda
başlık → sayı → buton alt alta ortalı; lg'de tek satır (sayı · başlık · buton). Yükseklik her
iki hâlde formu aşmaz (CLS).

View File

@@ -23,6 +23,18 @@ const staticEntries: MetadataRoute.Sitemap = [
priority: 0.8,
changeFrequency: "weekly",
},
{
url: `${SITE_URL}/universiteler/devlet`,
lastModified: KATALOG_VERI_TARIHI,
priority: 0.7,
changeFrequency: "monthly",
},
{
url: `${SITE_URL}/universiteler/vakif`,
lastModified: KATALOG_VERI_TARIHI,
priority: 0.7,
changeFrequency: "monthly",
},
{
url: `${SITE_URL}/universite-siralamasi`,
priority: 0.7,

View File

@@ -0,0 +1,22 @@
import { Suspense } from "react";
import type { Metadata } from "next";
import { SiteFooter } from "@/components/site-footer";
import {
TurIcerik,
TurIcerikSkeleton,
turSayfaMetadata,
} from "@/features/katalog/components/tur-icerik";
// Statik segment: /universiteler/[il] dinamik rotasından önce eşleşir.
export const metadata: Metadata = turSayfaMetadata("devlet");
export default function DevletPage() {
return (
<>
<Suspense fallback={<TurIcerikSkeleton />}>
<TurIcerik tur="devlet" />
</Suspense>
<SiteFooter />
</>
);
}

View File

@@ -0,0 +1,22 @@
import { Suspense } from "react";
import type { Metadata } from "next";
import { SiteFooter } from "@/components/site-footer";
import {
TurIcerik,
TurIcerikSkeleton,
turSayfaMetadata,
} from "@/features/katalog/components/tur-icerik";
// Statik segment: /universiteler/[il] dinamik rotasından önce eşleşir.
export const metadata: Metadata = turSayfaMetadata("vakif");
export default function VakifPage() {
return (
<>
<Suspense fallback={<TurIcerikSkeleton />}>
<TurIcerik tur="vakif" />
</Suspense>
<SiteFooter />
</>
);
}

View File

@@ -0,0 +1,41 @@
import Link from "next/link";
import { SATIR_CIP } from "@/components/liste-satiri";
import { UNITUR_ETIKET, turSayfaHref } from "@/lib/unitur";
import { cn } from "@/lib/utils";
/**
* Liste satırlarındaki tür çipi (DESIGN.md §4.15) — tek kaynak. Devlet ve
* vakıf tür sayfasına link (Bilal, 24 Eyl 2026: "vakıf ve devlet de
* tıklanabilir olmalı"); KKTC/yurt dışı gibi tür sayfası olmayanlar düz çip.
* Hover §2.1 tek turuncu. Satırın tamamı link olan yerde kullanılmaz
* (iç içe <a>).
*/
export function TurCipi({
unitur,
className,
onClick,
}: {
unitur: string | null | undefined;
className?: string;
/** Tıklanabilir satırın içinde olayın satıra yayılmasını kesmek için */
onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;
}) {
if (!unitur) return null;
const etiket = UNITUR_ETIKET[unitur]?.cip ?? unitur;
const href = turSayfaHref(unitur);
if (!href) return <span className={cn(SATIR_CIP, className)}>{etiket}</span>;
return (
<Link
href={href}
title={`Tüm ${etiket.toLocaleLowerCase("tr-TR")} üniversiteleri`}
onClick={onClick}
className={cn(
SATIR_CIP,
"transition-colors hover:bg-orange-50/60 hover:text-orange-500",
className,
)}
>
{etiket}
</Link>
);
}

View File

@@ -5,8 +5,8 @@ import Link from "next/link";
import { ChevronDown, ChevronUp } from "lucide-react";
import type { Program } from "@/types/yokatlas";
import { ilSayfaHref, trBaslikDuzeni } from "@/lib/slug";
import { TurCipi } from "@/components/tur-cipi";
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";
import {
@@ -22,7 +22,6 @@ import {
SATIR_BUTON_BASLIK,
SATIR_BUTON_HUCRE,
SATIR_BUTON_KABI,
SATIR_CIP,
SATIR_GRUP,
SATIR_IKINCIL_LINK,
SATIR_META,
@@ -242,12 +241,7 @@ export function BolumProgramListesi({
</span>
)}
<span className={SATIR_META}>
{p.unitur ? (
<span className={SATIR_CIP}>
{UNITUR_ETIKET[p.unitur]?.cip ??
trBaslikDuzeni(p.unitur)}
</span>
) : null}
<TurCipi unitur={p.unitur} />
{p.il ? (
<Link
href={ilSayfaHref(p.il)}

View File

@@ -2,16 +2,6 @@ import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import { Building2, ChevronRight, Landmark, MapPin } from "lucide-react";
import {
SATIR_AD_DUZ,
SATIR_CIP,
SATIR_GRUP,
SATIR_META,
} from "@/components/liste-satiri";
import { UniLogo } from "@/components/uni-logo";
import { trBaslikDuzeni } from "@/lib/slug";
import { UNITUR_ETIKET } from "@/lib/unitur";
import { cn } from "@/lib/utils";
import {
getAllIller,
getAllIlSlugs,
@@ -32,6 +22,7 @@ import { NavBaslik } from "./nav-baslik";
import { SehirCipleri, oneCikanIller } from "./sehir-cipleri";
import { SehirPikselHaritasi } from "./sehir-piksel-haritasi";
import { SehirYuzeyi } from "./sehir-yuzeyi";
import { UniSatirlari } from "./uni-satirlari";
const sayi = (n: number) => n.toLocaleString("tr-TR");
@@ -55,57 +46,6 @@ export function ilSayfaMetadata(slug: string): Metadata {
});
}
/**
* Şehir sayfasının üniversite satırları — DESIGN.md §4.15 liste satırı
* (Bilal, 24 Eyl 2026: "il kısmında bulunan üniler küçük gözüküyor"): 48 px
* logo, `text-base font-semibold` ad, altında tür çipi + program sayısı,
* satırın tamamı üniversite sayfasına link (44 px'ten iri dokunma hedefi).
* Ad hover'ı §2.1 istisnasıyla mavi, satır zemini turuncuya döner, sağdaki
* ok kayar. Üniversitenin ana ili bu sayfadan farklıysa (ODTÜ KKTC sayfasında)
* il de metada yazar.
*/
function UniSatirlari({ uniler, buIl }: { uniler: UniOzet[]; buIl: string }) {
return (
<ol className="mt-4 divide-y divide-slate-100 overflow-hidden rounded-xl border border-slate-200 bg-white text-left">
{uniler.map((u) => {
const tur = u.unitur ? UNITUR_ETIKET[u.unitur]?.cip : null;
const digerIl = u.il && u.il !== buIl ? trBaslikDuzeni(u.il) : null;
return (
<li key={u.slug}>
<Link
href={`/universite/${u.slug}`}
className="group flex items-start gap-3 px-3 py-4 transition-[background-color,transform] duration-150 hover:bg-orange-50/60 active:scale-[0.99] sm:items-center sm:gap-4 sm:px-5 lg:py-3"
>
<UniLogo slug={u.slug} ad={u.ad} boy="sm" />
<span className="min-w-0 flex-1">
<span className={SATIR_GRUP}>
<span
className={cn(
SATIR_AD_DUZ,
"transition-colors group-hover:text-primary group-hover:underline",
)}
>
{u.ad}
</span>
<span className={SATIR_META}>
{tur ? <span className={SATIR_CIP}>{tur}</span> : null}
{digerIl ? <span className="shrink-0">{digerIl}</span> : null}
<span className="shrink-0">{sayi(u.programSayisi)} program</span>
</span>
</span>
</span>
<ChevronRight
className="mt-3 size-4 shrink-0 text-slate-400 transition-[color,transform] duration-150 group-hover:translate-x-0.5 group-hover:text-orange-500 sm:mt-0"
aria-hidden
/>
</Link>
</li>
);
})}
</ol>
);
}
function UniGrubu({
baslik,
uniler,
@@ -124,7 +64,7 @@ function UniGrubu({
{sayi(uniler.length)}
</span>
</h2>
<UniSatirlari uniler={uniler} buIl={buIl} />
<UniSatirlari uniler={uniler} gizliIl={buIl} />
</section>
);
}

View File

@@ -237,14 +237,23 @@ function PikselSvg({
const TUM_IL_HUCRELERI = Object.values(IL_HUCRELERI).flat();
/** Türkiye; `il` (DB biçimi, ör. "İZMİR") turuncu karelerle yanar. */
/**
* Türkiye; `il` (DB biçimi, ör. "İZMİR") turuncu karelerle yanar. `iller`
* verilirse hepsi yanar (tür sayfası: devlet üniversitesi olan her il) ve
* yakınlaşma yapılmaz — ülkenin tamamı görünür.
*/
export function TurkiyePikselHaritasi({
il,
iller,
...props
}: HaritaProps & { il: string }) {
const harita = markup(`tr:${il}`, () =>
markupUret(TUM_IL_HUCRELERI, IL_HUCRELERI[il] ?? [], null),
);
}: HaritaProps & { il?: string; iller?: readonly string[] }) {
const liste = iller ?? (il ? [il] : []);
const anahtar = `tr:${liste.join("|")}`;
const harita = markup(anahtar, () => {
const vurgu = liste.flatMap((ad) => IL_HUCRELERI[ad] ?? []);
const h = markupUret(TUM_IL_HUCRELERI, vurgu, null);
return iller ? { ...h, sinir: null } : h;
});
return <PikselSvg harita={harita} {...props} />;
}

View File

@@ -0,0 +1,279 @@
import type { Metadata } from "next";
import Link from "next/link";
import {
Building2,
ChevronRight,
Landmark,
LayoutList,
MapPin,
} from "lucide-react";
import { getAllUniversiteler, type UniOzet } from "../katalog-queries";
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
import { PagePixelDivider } from "@/components/pixel-decor";
import { OzetKart } from "@/components/ozet-kart";
import { Skeleton } from "@/components/ui/skeleton";
import {
JsonLd,
breadcrumbJsonLd,
sayfaMetadata,
ustSegmentOgGorseli,
} from "@/lib/seo";
import { ilSayfaHref, trBaslikDuzeni, turkishSlugify } from "@/lib/slug";
import { TUR_SAYFALARI, type TurSlug } from "@/lib/unitur";
import { BASLIK_YILI, SON_YIL, TERCIH_YILI } from "@/lib/veri-yillari";
import { NavBaslik } from "./nav-baslik";
import { TurkiyePikselHaritasi } from "./sehir-piksel-haritasi";
import { SehirYuzeyi } from "./sehir-yuzeyi";
import { UniSatirlari } from "./uni-satirlari";
const sayi = (n: number) => n.toLocaleString("tr-TR");
function turUnileri(tur: TurSlug): UniOzet[] {
const set = new Set<string>(TUR_SAYFALARI[tur].uniturler);
return getAllUniversiteler().filter((u) => u.unitur && set.has(u.unitur));
}
export function turSayfaMetadata(tur: TurSlug): Metadata {
const t = TUR_SAYFALARI[tur];
const uniler = turUnileri(tur);
const program = uniler.reduce((a, u) => a + u.programSayisi, 0);
return sayfaMetadata({
title: `${t.ad} Taban Puanları ve Sıralamaları ${BASLIK_YILI}`,
description: `Türkiye'deki ${uniler.length} ${t.tekil}nin ${SON_YIL} YKS yerleştirme verileri: ${sayi(program)} programın taban puanları, başarı sıralamaları ve kontenjanları, şehir şehir. ${TERCIH_YILI} tercih rehberi.`,
path: `/universiteler/${t.slug}`,
openGraph: {
images: ustSegmentOgGorseli(
"/universiteler",
"Üniversitelerin taban puanları ve sıralamaları — KolayTercih",
),
},
});
}
/**
* /universiteler/devlet ve /universiteler/vakif (DESIGN.md §4.18). Şehir
* sayfasıyla aynı iskelet; kreatif fark: başlık yüzeyindeki dot-matrix
* Türkiye'de bu türden üniversitesi olan **her il** turuncu yanar, yakınlaşma
* yok — "devlet üniversiteleri Türkiye'nin neresinde" tek bakışta. Altında
* üç özet kartı, şehir çipleri (bu türün il başına sayısı) ve şehir şehir
* §4.15 liste satırları.
*/
export function TurIcerik({ tur }: { tur: TurSlug }) {
const t = TUR_SAYFALARI[tur];
const uniler = turUnileri(tur);
const program = uniler.reduce((a, u) => a + u.programSayisi, 0);
// Şehir öbekleri: üniversitenin ana ili (UniOzet.il); ili boş olan sona.
const ilOzet = new Map<string, { il: string; ad: string; uniler: UniOzet[] }>();
const ilsiz: UniOzet[] = [];
for (const u of uniler) {
if (!u.il) {
ilsiz.push(u);
continue;
}
let k = ilOzet.get(u.il);
if (!k) {
k = { il: u.il, ad: trBaslikDuzeni(u.il), uniler: [] };
ilOzet.set(u.il, k);
}
k.uniler.push(u);
}
const sehirler = [...ilOzet.values()].sort((a, b) =>
a.ad.localeCompare(b.ad, "tr-TR"),
);
const cipSehirler = [...sehirler].sort(
(a, b) =>
b.uniler.length - a.uniler.length || a.ad.localeCompare(b.ad, "tr-TR"),
);
const Ikon = tur === "devlet" ? Landmark : Building2;
const DigerIkon = tur === "devlet" ? Building2 : Landmark;
const ctaBaslik = `Sıralaman ${t.tekil} tabanlarının neresinde?`;
const digerTur = tur === "devlet" ? TUR_SAYFALARI.vakif : TUR_SAYFALARI.devlet;
return (
<main className="mx-auto w-full max-w-5xl px-4 py-12 sm:py-16">
<JsonLd
data={breadcrumbJsonLd([
{ name: "Ana Sayfa", path: "/" },
{ name: "Üniversiteler", path: "/universiteler" },
{ name: t.ad, path: `/universiteler/${t.slug}` },
])}
/>
<nav
aria-label="İçerik yolu"
className="flex flex-wrap items-center justify-center gap-1.5 text-sm text-slate-500 lg:justify-start"
>
<Link
href="/"
className="transition-colors hover:text-orange-500 hover:underline"
>
Ana Sayfa
</Link>
<ChevronRight className="size-3.5 shrink-0" aria-hidden />
<Link
href="/universiteler"
className="transition-colors hover:text-orange-500 hover:underline"
>
Üniversiteler
</Link>
<ChevronRight className="size-3.5 shrink-0" aria-hidden />
<span className="text-slate-700">{t.ad}</span>
</nav>
<NavBaslik baslikId="tur-baslik" ad={t.ad} />
<SehirYuzeyi
harita={
<TurkiyePikselHaritasi
iller={sehirler.map((s) => s.il)}
etiket={`${t.ad} Türkiye haritasında: ${sehirler.length} il`}
doldur
/>
}
metin={
<div className="text-center lg:text-left">
<h1
id="tur-baslik"
className="font-heading text-3xl font-bold leading-tight text-slate-900 sm:text-5xl"
>
{t.ad} Taban Puanları {BASLIK_YILI}
</h1>
{/* Kimlik hapları (§4.12): tür + öteki türe geçiş */}
<p className="mt-5 flex flex-wrap items-center justify-center gap-2 lg:justify-start">
<span className="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">
<Ikon className="size-4 text-primary" aria-hidden />
{tur === "devlet" ? "Devlet" : "Vakıf"}
</span>
<Link
href={`/universiteler/${digerTur.slug}`}
className="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 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]"
>
<DigerIkon className="size-4 text-primary" aria-hidden />
{digerTur.ad}
</Link>
</p>
<p className="mx-auto mt-6 max-w-2xl text-base leading-relaxed text-slate-600 sm:text-lg lg:mx-0">
Türkiye&apos;de {SON_YIL} YKS yerleştirmesinde öğrenci alan{" "}
<strong>
{sayi(uniler.length)} {t.tekil}
</strong>
, <strong>{sayi(sehirler.length)} şehirde</strong> toplam{" "}
<strong>{sayi(program)} program</strong> açtı. Haritada turuncu
yanan her ilde en az bir {t.tekil} var. Üniversiteye tıklayınca
programlarının taban puanlarını, başarı sıralamalarını ve
kontenjanlarını fakülte fakülte görürsün; {TERCIH_YILI} tercihleri
için rehber niteliğindedir.
</p>
</div>
}
/>
<div className="mt-8">
<CtaSiraForm varyant="kompakt" konum="ust" baslik={ctaBaslik} />
</div>
<dl className="mt-8 grid grid-cols-2 gap-4 lg:grid-cols-3">
<OzetKart
ikon={Ikon}
etiket="Üniversite"
deger={sayi(uniler.length)}
alt={`${SON_YIL}'de öğrenci alan`}
/>
<OzetKart
ikon={LayoutList}
etiket="Program"
deger={sayi(program)}
alt="lisans ve önlisans"
/>
<OzetKart
ikon={MapPin}
etiket="Şehir"
deger={sayi(sehirler.length)}
alt="en az bir kampüs"
className="col-span-2 lg:col-span-1"
/>
</dl>
<PagePixelDivider seed={73} className="mx-auto mt-8 lg:mx-0" />
<section
className="mt-8 text-center lg:text-left"
aria-labelledby="tur-sehirler"
>
<h2
id="tur-sehirler"
className="font-heading text-2xl font-bold text-slate-900"
>
Şehre göre
<span className="ml-2 text-sm font-normal text-slate-400">
{sayi(sehirler.length)} şehir
</span>
</h2>
<ul className="mt-4 flex flex-wrap items-center justify-center gap-2 lg:justify-start">
{cipSehirler.map((s) => (
<li key={s.il}>
<a
href={`#sehir-${turkishSlugify(s.il)}`}
className="inline-flex h-10 cursor-pointer items-center gap-1.5 rounded-full border border-slate-200 bg-white px-3.5 text-sm font-semibold text-slate-700 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]"
>
{s.ad}
<span className="font-normal tabular-nums text-slate-400">
{sayi(s.uniler.length)}
</span>
</a>
</li>
))}
</ul>
</section>
{sehirler.map((s) => (
<section
key={s.il}
id={`sehir-${turkishSlugify(s.il)}`}
className="mt-10 scroll-mt-24 text-center lg:text-left"
>
<h2 className="font-heading text-2xl font-bold text-slate-900">
<Link
href={ilSayfaHref(s.il)}
className="transition-colors hover:text-orange-500 hover:underline"
>
{s.ad}
</Link>
<span className="ml-2 text-sm font-normal text-slate-400">
{sayi(s.uniler.length)}
</span>
</h2>
<UniSatirlari uniler={s.uniler} gizliIl={s.il} />
</section>
))}
{ilsiz.length > 0 ? (
<section className="mt-10 text-center lg:text-left">
<h2 className="font-heading text-2xl font-bold text-slate-900">
Diğer
</h2>
<UniSatirlari uniler={ilsiz} />
</section>
) : null}
<div className="mt-12">
<CtaSiraForm
baslik={ctaBaslik}
aciklama={`YKS başarı sıralamanı gir; ${t.tekil} programları dahil sıralamana uygun hayal, dengeli ve güvenli dilimlerindeki programları anında listele. Ücretsiz, üyelik gerekmez.`}
/>
</div>
</main>
);
}
export function TurIcerikSkeleton() {
return (
<main className="mx-auto w-full max-w-5xl px-4 py-12 sm:py-16">
<Skeleton className="mx-auto h-4 w-64 lg:mx-0" />
<Skeleton className="mt-6 h-72 w-full rounded-3xl" />
<div className="mt-8 grid grid-cols-2 gap-4 lg:grid-cols-3">
<Skeleton className="h-28 rounded-2xl" />
<Skeleton className="h-28 rounded-2xl" />
<Skeleton className="col-span-2 h-28 rounded-2xl lg:col-span-1" />
</div>
</main>
);
}

View File

@@ -0,0 +1,67 @@
import Link from "next/link";
import {
SATIR_AD,
SATIR_GRUP,
SATIR_IKINCIL_LINK,
SATIR_META,
} from "@/components/liste-satiri";
import { TurCipi } from "@/components/tur-cipi";
import { UniLogo } from "@/components/uni-logo";
import { ilSayfaHref, trBaslikDuzeni } from "@/lib/slug";
import type { UniOzet } from "../katalog-queries";
const sayi = (n: number) => n.toLocaleString("tr-TR");
/**
* Şehir ve tür sayfalarının üniversite satırları — DESIGN.md §4.15 liste
* satırı (Bilal, 24 Eyl 2026: "il kısmında üniler küçük gözüküyor"): 48 px
* logo, `SATIR_AD` ad (hover mavi, §2.1 istisnası), altında tür çipi
* (tür sayfasına link) + il (şehir sayfasına link) + program sayısı. Satırın
* tamamı link değildir: tür ve il ayrı hedeflere gider (iç içe <a> olmaz).
* `gizliIl`: bulunulan şehir sayfasında aynı il yazılmaz.
*/
export function UniSatirlari({
uniler,
gizliIl,
}: {
uniler: UniOzet[];
gizliIl?: string | null;
}) {
return (
<ol className="mt-4 divide-y divide-slate-100 overflow-hidden rounded-xl border border-slate-200 bg-white text-left">
{uniler.map((u) => {
const il = u.il && u.il !== gizliIl ? u.il : null;
return (
<li
key={u.slug}
className="flex items-start gap-3 px-3 py-4 sm:items-center sm:gap-4 sm:px-5 lg:py-3"
>
<Link href={`/universite/${u.slug}`} tabIndex={-1} aria-hidden>
<UniLogo slug={u.slug} ad={u.ad} boy="sm" />
</Link>
<span className="min-w-0 flex-1">
<span className={SATIR_GRUP}>
<Link href={`/universite/${u.slug}`} className={SATIR_AD}>
{u.ad}
</Link>
<span className={SATIR_META}>
<TurCipi unitur={u.unitur} />
{il ? (
<Link
href={ilSayfaHref(il)}
className={`shrink-0 ${SATIR_IKINCIL_LINK}`}
title={`${trBaslikDuzeni(il)} üniversiteleri`}
>
{trBaslikDuzeni(il)}
</Link>
) : null}
<span className="shrink-0">{sayi(u.programSayisi)} program</span>
</span>
</span>
</span>
</li>
);
})}
</ol>
);
}

View File

@@ -19,7 +19,7 @@ import {
bolumSlugFromIsim,
} from "../katalog-queries";
import { ilSayfaHref, trBaslikDuzeni } from "@/lib/slug";
import { UNITUR_ETIKET, uniturDevletMi } from "@/lib/unitur";
import { UNITUR_ETIKET, turSayfaHref, uniturDevletMi } from "@/lib/unitur";
import {
JsonLd,
breadcrumbJsonLd,
@@ -569,14 +569,31 @@ export function UniversiteIcerik({
{turEtiketi || uni.il ? (
<p className="mt-5 flex flex-wrap items-center justify-center gap-2 lg:justify-start">
{turEtiketi ? (
<span className="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">
{uniturDevletMi(uni.unitur) ? (
// Devlet/vakıf hapı tür sayfasına gider (DESIGN.md §4.12); KKTC
// ve yurt dışında tür sayfası yok, hap düz kalır.
(() => {
const href = turSayfaHref(uni.unitur);
const ikon = uniturDevletMi(uni.unitur) ? (
<Landmark className="size-4 text-primary" aria-hidden />
) : (
<Building2 className="size-4 text-primary" aria-hidden />
)}
{turEtiketi.cip}
</span>
);
return href ? (
<Link
href={href}
title={`Tüm ${turEtiketi.cip.toLocaleLowerCase("tr-TR")} üniversiteleri`}
className="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 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]"
>
{ikon}
{turEtiketi.cip}
</Link>
) : (
<span className="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">
{ikon}
{turEtiketi.cip}
</span>
);
})()
) : null}
{uni.il ? (
// İl hapı şehir sayfasına gider (Bilal, 24 Eyl 2026: "şehirlerin

View File

@@ -107,7 +107,12 @@ export function UniversiteKatalogu() {
<section className="mt-8">
<h2 className="font-heading text-2xl font-bold text-slate-900">
Devlet Üniversiteleri
<Link
href="/universiteler/devlet"
className="transition-colors hover:text-orange-500 hover:underline"
>
Devlet Üniversiteleri
</Link>
<span className="ml-2 text-sm font-normal text-slate-400">
{sayi(devlet.length)}
</span>
@@ -117,7 +122,12 @@ export function UniversiteKatalogu() {
<section className="mt-10">
<h2 className="font-heading text-2xl font-bold text-slate-900">
Vakıf Üniversiteleri
<Link
href="/universiteler/vakif"
className="transition-colors hover:text-orange-500 hover:underline"
>
Vakıf Üniversiteleri
</Link>
<span className="ml-2 text-sm font-normal text-slate-400">
{sayi(vakif.length)}
</span>

View File

@@ -22,6 +22,7 @@ import type {
} from "@/types/yokatlas";
import { DILIM_ETIKET, riskHesapla } from "@/lib/risk";
import { bolumSayfaSlug, ilSayfaHref, trBaslikDuzeni, uniSayfaSlug } from "@/lib/slug";
import { TurCipi } from "@/components/tur-cipi";
import { cn } from "@/lib/utils";
import { VERI_YIL_SAYISI } from "@/lib/veri-yillari";
import {
@@ -52,7 +53,6 @@ import {
SATIR_BUTON_BASLIK,
SATIR_BUTON_HUCRE,
SATIR_BUTON_KABI,
SATIR_CIP,
SATIR_ALT_GRUP,
SATIR_IKINCIL_LINK,
SATIR_META,
@@ -334,7 +334,6 @@ 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 &&
@@ -373,7 +372,10 @@ function ProgramSatiri({
</Link>
</span>
<span className={SATIR_META}>
<span className={SATIR_CIP}>{devlet ? "Devlet" : "Vakıf"}</span>
<TurCipi
unitur={p.unitur}
onClick={(e) => e.stopPropagation()}
/>
{p.il ? (
<Link
href={ilSayfaHref(p.il)}

View File

@@ -27,6 +27,7 @@ import {
TableRow,
} from "@/components/ui/table";
import { bolumSayfaSlug, ilSayfaHref, trBaslikDuzeni, uniSayfaSlug } from "@/lib/slug";
import { TurCipi } from "@/components/tur-cipi";
import { cn } from "@/lib/utils";
import { UNITUR_ETIKET } from "@/lib/unitur";
import { RiskEtiketi } from "@/components/risk-etiketi";
@@ -38,7 +39,6 @@ import {
SATIR_BASLIK,
SATIR_BUTON_BASLIK,
SATIR_BUTON_HUCRE,
SATIR_CIP,
SATIR_DUGME_BOYU,
SATIR_ALT_GRUP,
SATIR_IKINCIL_LINK,
@@ -201,7 +201,6 @@ export function RaporListesi({
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 ? (
<span className="inline-flex items-center gap-1.5 text-xs font-semibold text-slate-500">
@@ -263,9 +262,10 @@ export function RaporListesi({
</span>
) : null}
<span className={SATIR_META}>
{p?.unitur ? (
<span className={SATIR_CIP}>{devlet ? "Devlet" : "Vakıf"}</span>
) : null}
<TurCipi
unitur={p?.unitur}
onClick={(e) => e.stopPropagation()}
/>
{p?.il ? (
<Link
href={ilSayfaHref(p.il)}

View File

@@ -26,10 +26,10 @@ import { olay, siraKovasi } from "@/lib/analitik";
import { dilimdenRisk, riskHesapla } from "@/lib/risk";
import { ilSayfaHref, trBaslikDuzeni, turkishSlugify } from "@/lib/slug";
import { cn } from "@/lib/utils";
import { TurCipi } from "@/components/tur-cipi";
import {
SATIR_ALT_AD,
SATIR_AD_DUZ,
SATIR_CIP,
SATIR_ALT_GRUP,
SATIR_IKINCIL_LINK,
SATIR_META,
@@ -123,7 +123,6 @@ export function TadimlikSatiri({
const risk = riskHesapla(p.efektifSira, sira) ?? dilimdenRisk(satir.dilim);
const seri = raporSiraSerisi(p.siraGecmisi, p.ilkYil);
const taban = p.efektifSira ?? programEtkinSira(seri);
const devlet = p.unitur === "DEVLET";
// Giriş dönüşü kullanıcıyı /sonuc'a geri bırakmaz: callback doğrudan
// üretimin koştuğu /listem'e iner, liste ek tık olmadan orada başlar.
const girisUrl = `/giris?callback=${encodeURIComponent(listemUretHref("tadimlik"))}`;
@@ -164,9 +163,7 @@ export function TadimlikSatiri({
{trBaslikDuzeni(p.universite)}
</span>
<span className={SATIR_META}>
{p.unitur ? (
<span className={SATIR_CIP}>{devlet ? "Devlet" : "Vakıf"}</span>
) : null}
<TurCipi unitur={p.unitur} />
{p.il ? (
<Link
href={ilSayfaHref(p.il)}

View File

@@ -408,17 +408,39 @@ export function CtaSiraForm({
<div className="mt-3">{siraFormu}</div>
</div>
{profil ? (
<div className="col-start-1 row-start-1 flex flex-col items-center justify-center gap-3 text-center sm:flex-row sm:justify-between sm:gap-6 sm:text-left">
<div className="flex min-w-0 flex-col items-center gap-1 sm:items-start">
<p
id={baslikId}
className="font-heading text-base font-bold leading-6 text-white"
>
{kompaktBaslik}
</p>
<KayitliSira sira={profil.sira} tur={profil.tur} beyaz />
</div>
<Button asChild size="lg" className={cn(PROFILLI_CTA_SINIF, "w-full sm:w-auto")}>
// Şişko ve sade (DESIGN.md §1.1, §10.9): sayı kahramandır — iri
// kupa kutusu + text-3xl/4xl sıra, başlık text-xl/2xl, 48 px buton.
// Telefonda başlık → sayı → buton alt alta; lg'de tek satır
// (sayı · başlık · buton). Yükseklik formunkini aşmaz (CLS notu).
<div className="col-start-1 row-start-1 flex flex-col items-center justify-center gap-4 text-center lg:flex-row lg:gap-6 lg:text-left">
<p
id={baslikId}
className="order-1 min-w-0 flex-1 font-heading text-xl font-bold leading-tight text-balance text-white sm:text-2xl lg:order-2"
>
{kompaktBaslik}
</p>
<p className="order-2 flex items-center gap-3 lg:order-1 lg:shrink-0">
<span className="flex size-14 shrink-0 items-center justify-center rounded-2xl bg-white/15">
<Trophy className="size-7 text-white" aria-hidden />
</span>
<span className="flex flex-col items-start text-left">
<span className="sr-only">Sıralaman kayıtlı:</span>
<span className="font-heading text-3xl font-bold leading-none tracking-tight text-white sm:text-4xl">
{profil.sira.toLocaleString("tr-TR")}.
</span>
<span className="mt-1 text-sm font-semibold text-white/80">
sıra · {turEtiketi(profil.tur)}
</span>
</span>
</p>
<Button
asChild
size="lg"
className={cn(
PROFILLI_CTA_SINIF,
"order-3 w-full active:scale-[0.97] sm:w-auto lg:shrink-0",
)}
>
<Link href={sonucHref()}>
Sıralamana uygun programları gör
<ArrowRight className="size-4" aria-hidden />

View File

@@ -35,3 +35,39 @@ export const UNITUR_ETIKET: Record<string, { cip: string; cumle: string }> = {
*/
export const uniturDevletMi = (unitur: string | null | undefined) =>
unitur === "DEVLET";
/**
* Tür sayfaları (/universiteler/devlet, /universiteler/vakif; DESIGN.md
* §4.18). Bilal, 24 Eyl 2026: "vakıf ve devlet de tıklanabilir olmalı".
* Vakıf MYO'lar vakıf sayfasında sayılır; KKTC ve yurt dışının tür sayfası
* yoktur (şehir sayfalarında zaten ayrı öbek).
*/
export const TUR_SAYFALARI = {
devlet: {
slug: "devlet",
ad: "Devlet Üniversiteleri",
tekil: "devlet üniversitesi",
uniturler: ["DEVLET"],
},
vakif: {
slug: "vakif",
ad: "Vakıf Üniversiteleri",
tekil: "vakıf üniversitesi",
uniturler: ["VAKIF", "VAKIF MYO"],
},
} as const;
export type TurSlug = keyof typeof TUR_SAYFALARI;
export const turSlugMu = (s: string): s is TurSlug => s in TUR_SAYFALARI;
/** Ham unitur'dan tür sayfası adresi; tür sayfası olmayanlarda null. */
export function turSayfaHref(unitur: string | null | undefined): string | null {
if (!unitur) return null;
for (const t of Object.values(TUR_SAYFALARI)) {
if ((t.uniturler as readonly string[]).includes(unitur)) {
return `/universiteler/${t.slug}`;
}
}
return null;
}