feat(tasarim): bölüm sayfası "şişko ve sade" dile göre
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<string, string> = {
|
||||
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<string>();
|
||||
const vakifUni = new Set<string>();
|
||||
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 (
|
||||
<>
|
||||
<main className="mx-auto w-full max-w-5xl px-4 py-10 sm:py-14">
|
||||
<JsonLd
|
||||
data={breadcrumbJsonLd([
|
||||
{ name: "Ana Sayfa", path: "/" },
|
||||
{ name: "Bölümler", path: "/bolumler" },
|
||||
{ name: bolum.ad, path: `/bolum/${slug}` },
|
||||
...(ilkSayfa ? [] : [{ name: `Sayfa ${sayfa}`, path: buYol }]),
|
||||
])}
|
||||
/>
|
||||
<main className="mx-auto w-full max-w-5xl px-4 py-12 sm:py-16">
|
||||
<JsonLd
|
||||
data={breadcrumbJsonLd([
|
||||
{ name: "Ana Sayfa", path: "/" },
|
||||
{ name: "Bölümler", path: "/bolumler" },
|
||||
{ name: bolum.ad, path: `/bolum/${slug}` },
|
||||
...(ilkSayfa ? [] : [{ name: `Sayfa ${sayfa}`, path: buYol }]),
|
||||
])}
|
||||
/>
|
||||
|
||||
<nav
|
||||
aria-label="İçerik yolu"
|
||||
className="mb-5 flex flex-wrap items-center gap-1.5 text-sm text-muted-foreground"
|
||||
>
|
||||
<Link
|
||||
href="/"
|
||||
className="transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
<nav
|
||||
aria-label="İçerik yolu"
|
||||
className="flex flex-wrap items-center justify-center gap-1.5 text-sm text-slate-500 lg:justify-start"
|
||||
>
|
||||
Ana sayfa
|
||||
</Link>
|
||||
<ChevronRight className="size-3.5" aria-hidden />
|
||||
<Link
|
||||
href="/bolumler"
|
||||
className="transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
Bölümler
|
||||
</Link>
|
||||
<ChevronRight className="size-3.5" aria-hidden />
|
||||
{ilkSayfa ? (
|
||||
<span className="text-foreground">{bolum.ad}</span>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
href={`/bolum/${slug}`}
|
||||
className="transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{bolum.ad}
|
||||
</Link>
|
||||
<ChevronRight className="size-3.5" aria-hidden />
|
||||
<span className="text-foreground">Sayfa {sayfa}</span>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
<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="/bolumler"
|
||||
className="transition-colors hover:text-orange-500 hover:underline"
|
||||
>
|
||||
Bölümler
|
||||
</Link>
|
||||
<ChevronRight className="size-3.5 shrink-0" aria-hidden />
|
||||
{ilkSayfa ? (
|
||||
<span className="text-slate-700">{bolum.ad}</span>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
href={`/bolum/${slug}`}
|
||||
className="transition-colors hover:text-orange-500 hover:underline"
|
||||
>
|
||||
{bolum.ad}
|
||||
</Link>
|
||||
<ChevronRight className="size-3.5 shrink-0" aria-hidden />
|
||||
<span className="text-slate-700">Sayfa {sayfa}</span>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
{/* Başlık üst çubuğun altına girince üst çubukta bölüm adı belirir */}
|
||||
<NavBaslik baslikId="bolum-baslik" ad={bolum.ad} />
|
||||
<header className="border-b pb-7 sm:pb-9">
|
||||
{/* 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). */}
|
||||
<NavBaslik baslikId="bolum-baslik" ad={bolum.ad} />
|
||||
<h1
|
||||
id="bolum-baslik"
|
||||
className="max-w-3xl font-heading text-3xl font-bold tracking-tight text-foreground sm:text-5xl">
|
||||
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 ? (
|
||||
<>
|
||||
{" "}
|
||||
<span className="mt-2 block text-lg font-semibold tracking-normal text-muted-foreground sm:text-2xl">
|
||||
<span className="mt-2 block text-lg font-semibold text-slate-500 sm:text-2xl">
|
||||
{aralik.charAt(0).toLocaleUpperCase("tr-TR") + aralik.slice(1)}
|
||||
</span>
|
||||
</>
|
||||
) : null}
|
||||
</h1>
|
||||
|
||||
{duzeyler.length > 0 || puanTurleri.length > 0 ? (
|
||||
<p className="mt-5 flex flex-wrap items-center justify-center gap-2 lg:justify-start">
|
||||
{duzeyler.map((d) => (
|
||||
<span key={d} className={hapSinifi}>
|
||||
<GraduationCap className="size-4 text-primary" aria-hidden />
|
||||
{d}
|
||||
</span>
|
||||
))}
|
||||
{puanTurleri.map((t) => (
|
||||
<span key={t} className={hapSinifi}>
|
||||
<Calculator className="size-4 text-primary" aria-hidden />
|
||||
{t}
|
||||
</span>
|
||||
))}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{ilkSayfa ? (
|
||||
<p className="mt-3 max-w-2xl text-sm leading-6 text-muted-foreground sm:text-base">
|
||||
{sayi(tumProgramlar.length)} üniversite programının {SON_YIL} taban
|
||||
puanlarını, başarı sıralamalarını ve kontenjanlarını incele.
|
||||
<p className="mx-auto mt-6 max-w-3xl text-center text-base leading-relaxed text-slate-600 sm:text-lg lg:mx-0 lg:text-left">
|
||||
{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.{" "}
|
||||
<em>
|
||||
{TERCIH_YILI} tabanları, yerleştirme sonuçları açıklandığında
|
||||
netleşir.
|
||||
</em>
|
||||
</p>
|
||||
) : (
|
||||
<p className="mt-3 max-w-2xl text-sm leading-6 text-muted-foreground sm:text-base">
|
||||
<p className="mx-auto mt-6 max-w-3xl text-center text-base leading-relaxed text-slate-600 sm:text-lg lg:mx-0 lg:text-left">
|
||||
{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şı{" "}
|
||||
<Link
|
||||
href={`/bolum/${slug}`}
|
||||
className="font-medium text-primary hover:underline"
|
||||
className="font-medium text-primary transition-colors hover:text-orange-500 hover:underline"
|
||||
>
|
||||
ilk sayfada
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
)}
|
||||
</header>
|
||||
|
||||
{/* 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. */}
|
||||
<div className="mt-6">
|
||||
<CtaSiraForm varyant="kompakt" konum="ust" baslik={ctaBaslik} />
|
||||
</div>
|
||||
|
||||
<section aria-labelledby="universite-programlari" className="mt-8">
|
||||
<div className="mb-4 flex items-end justify-between gap-4">
|
||||
<h2
|
||||
id="universite-programlari"
|
||||
className="font-heading text-xl font-bold text-foreground sm:text-2xl"
|
||||
>
|
||||
Üniversite programları
|
||||
</h2>
|
||||
<span className="shrink-0 text-sm text-muted-foreground">
|
||||
{sayi(tumProgramlar.length)} program
|
||||
</span>
|
||||
{/* 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. */}
|
||||
<div className="mt-8">
|
||||
<CtaSiraForm varyant="kompakt" konum="ust" baslik={ctaBaslik} />
|
||||
</div>
|
||||
|
||||
<BolumProgramListesi
|
||||
programlar={tumProgramlar.map((program) => ({
|
||||
...program,
|
||||
universiteSlug: uniSlugFromAd(program.universite),
|
||||
}))}
|
||||
bazAd={bolum.ad}
|
||||
tabanYol={`/bolum/${slug}`}
|
||||
sayfa={sayfa}
|
||||
/>
|
||||
<VeriKaynagi className="mt-6" />
|
||||
</section>
|
||||
{/* Sayfada en çok dört özet sayı (DESIGN.md §1.1, §4.8). Derin
|
||||
sayfalarda tekrar edilmez — üniversite sayfasıyla aynı. */}
|
||||
{ilkSayfa ? (
|
||||
<dl className="mt-8 grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||
<OzetKart
|
||||
ikon={LayoutList}
|
||||
etiket="Program"
|
||||
deger={sayi(stats.toplam)}
|
||||
alt={`${sayi(stats.ilSayisi)} ilde`}
|
||||
/>
|
||||
<OzetKart
|
||||
ikon={Landmark}
|
||||
etiket="Devlet"
|
||||
deger={sayi(stats.devlet)}
|
||||
alt={
|
||||
devletUni.size > 0
|
||||
? `${sayi(devletUni.size)} üniversitede`
|
||||
: "bu bölümde yok"
|
||||
}
|
||||
/>
|
||||
<OzetKart
|
||||
ikon={Building2}
|
||||
etiket="Vakıf"
|
||||
deger={sayi(vakifProgram)}
|
||||
alt={
|
||||
vakifUni.size > 0
|
||||
? `${sayi(vakifUni.size)} üniversitede`
|
||||
: "bu bölümde yok"
|
||||
}
|
||||
/>
|
||||
<OzetKart
|
||||
ikon={Target}
|
||||
etiket="En iyi sıralama"
|
||||
{...(stats.minSira != null
|
||||
? kartSayisi(stats.minSira)
|
||||
: { deger: "—" })}
|
||||
alt={`${SON_YIL} taban sırası`}
|
||||
/>
|
||||
</dl>
|
||||
) : null}
|
||||
|
||||
{/* Üniversite sayfasıyla aynı konum ve aynı kart: liste + sayfalama altı */}
|
||||
<div className="mt-10">
|
||||
<CtaSiraForm
|
||||
konum="alt"
|
||||
baslik={ctaBaslik}
|
||||
aciklama={KATALOG_CTA_ACIKLAMA}
|
||||
/>
|
||||
</div>
|
||||
<PagePixelDivider seed={83} className="mx-auto mt-12 lg:mx-0" />
|
||||
|
||||
<section aria-labelledby="universite-programlari" className="mt-10">
|
||||
<div className="text-center lg:text-left">
|
||||
<h2
|
||||
id="universite-programlari"
|
||||
className="font-heading text-2xl font-bold text-slate-900 sm:text-3xl"
|
||||
>
|
||||
Üniversite programları
|
||||
</h2>
|
||||
<p className="mt-2 text-base text-slate-500">
|
||||
{sayi(tumProgramlar.length)} program, taban sırasına göre
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<BolumProgramListesi
|
||||
programlar={tumProgramlar.map((program) => ({
|
||||
...program,
|
||||
universiteSlug: uniSlugFromAd(program.universite),
|
||||
}))}
|
||||
bazAd={bolum.ad}
|
||||
tabanYol={`/bolum/${slug}`}
|
||||
sayfa={sayfa}
|
||||
/>
|
||||
</div>
|
||||
<VeriKaynagi className="mt-6" />
|
||||
</section>
|
||||
|
||||
{/* Üniversite sayfasıyla aynı konum ve aynı kart: liste + sayfalama altı */}
|
||||
<div className="mt-10">
|
||||
<CtaSiraForm
|
||||
konum="alt"
|
||||
baslik={ctaBaslik}
|
||||
aciklama={KATALOG_CTA_ACIKLAMA}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
<SiteFooter />
|
||||
</>
|
||||
@@ -313,16 +444,20 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
|
||||
|
||||
export function BolumIcerikSkeleton() {
|
||||
return (
|
||||
<main className="mx-auto w-full max-w-5xl px-4 py-10 sm:py-14">
|
||||
<Skeleton className="h-5 w-56" />
|
||||
<div className="border-b pb-7 sm:pb-9">
|
||||
<Skeleton className="mt-5 h-10 w-2/3 max-w-xl" />
|
||||
<Skeleton className="mt-3 h-5 w-full max-w-2xl" />
|
||||
<main className="mx-auto w-full max-w-5xl px-4 py-12 sm:py-16">
|
||||
<Skeleton className="mx-auto h-5 w-56 lg:mx-0" />
|
||||
<Skeleton className="mx-auto mt-6 h-10 w-2/3 max-w-xl lg:mx-0" />
|
||||
<Skeleton className="mx-auto mt-5 h-10 w-48 rounded-full lg:mx-0" />
|
||||
<Skeleton className="mx-auto mt-6 h-5 w-full max-w-3xl lg:mx-0" />
|
||||
<div className="mt-8 grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||
{Array.from({ length: 4 }, (_, i) => (
|
||||
<Skeleton key={i} className="h-28 w-full rounded-2xl" />
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-8 space-y-3">
|
||||
<div className="mt-16 space-y-3">
|
||||
<Skeleton className="h-7 w-64" />
|
||||
{Array.from({ length: 5 }, (_, i) => (
|
||||
<Skeleton key={i} className="h-12 w-full" />
|
||||
<Skeleton key={i} className="h-20 w-full rounded-2xl" />
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user