feat(katalog): şehre göre üniversiteler — /universiteler/[il] sayfaları ve şehir çipleri

- Veri: getAllIller/getIlBySlug (üniversite en çok programının olduğu ile
  sayılır; ili boş olan hiçbir şehre girmez; tireli yurt dışı adlar baş harfli).
- /universiteler: "Şehre göre" çip satırı (2+ üniversiteli 28 şehir) +
  "Tüm şehirler" A–Z üç sütunlu liste (#tum-sehirler).
- /universiteler/[il]: 92 statik şehir sayfası — katalog detay düzeni, il ve
  üniversite sayısı hapları, kompakt sıra satırı, devlet/vakıf/KKTC-yurt dışı
  öbekleri, diğer şehir çipleri, CTA bandı; metadata + breadcrumb JSON-LD.
- SehirCipleri: sihirbaz il çipiyle aynı kalıp, aktif şehir mavi dolu.
- Sitemap'e şehir sayfaları eklendi.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-24 14:19:09 +03:00
parent f9b6f0c235
commit d8a28b7c3a
7 changed files with 462 additions and 3 deletions

View File

@@ -0,0 +1,80 @@
import Link from "next/link";
import { cn } from "@/lib/utils";
import type { IlOzet } from "../katalog-queries";
const sayi = (n: number) => n.toLocaleString("tr-TR");
/**
* Şehir çipi (DESIGN.md §4.17): sihirbazın il çipiyle aynı kalıp — 40 px,
* tam yuvarlak, hover §2.1 tek turuncu; seçili/aktif hâl mavi dolu.
* Etkileşimli çip olduğu için rozet (§4.2) kuralının dışındadır.
*/
const CIP_TEMEL =
"inline-flex h-10 cursor-pointer items-center gap-1.5 rounded-full border px-3.5 text-sm font-semibold transition-[background-color,border-color,color,transform] duration-150 active:scale-[0.97]";
const CIP_PASIF =
"border-slate-200 bg-white text-slate-700 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500";
const CIP_AKTIF = "border-primary bg-primary text-white";
/**
* Şehre göre üniversite gezinme çipleri. `iller` verilen sırayla dizilir
* (çağıran genelde üniversite sayısına göre azalan verir). `aktifSlug`
* bulunulan şehir sayfasıdır: mavi dolu ve `aria-current="page"`.
* `tumuHref` verilirse sona "Tüm şehirler" çipi eklenir (A–Z listesine).
*/
export function SehirCipleri({
iller,
aktifSlug,
tumuHref,
tumuEtiket,
className,
}: {
iller: IlOzet[];
aktifSlug?: string;
tumuHref?: string;
tumuEtiket?: string;
className?: string;
}) {
return (
<ul
className={cn(
"flex flex-wrap items-center justify-center gap-2 lg:justify-start",
className,
)}
>
{iller.map((il) => {
const aktif = il.slug === aktifSlug;
return (
<li key={il.slug}>
<Link
href={`/universiteler/${il.slug}`}
aria-current={aktif ? "page" : undefined}
className={cn(CIP_TEMEL, aktif ? CIP_AKTIF : CIP_PASIF)}
>
{il.ad}
<span
className={cn(
"font-normal tabular-nums",
aktif ? "text-white/80" : "text-slate-400",
)}
>
{sayi(il.uniler.length)}
</span>
</Link>
</li>
);
})}
{tumuHref ? (
<li>
<Link href={tumuHref} className={cn(CIP_TEMEL, CIP_PASIF)}>
{tumuEtiket ?? "Tüm şehirler"}
</Link>
</li>
) : null}
</ul>
);
}
/** Çip satırında gösterilecek şehirler: en az iki üniversitesi olanlar. */
export function oneCikanIller(iller: IlOzet[]): IlOzet[] {
return iller.filter((il) => il.uniler.length >= 2);
}