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

@@ -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>
);
}