- 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>
168 lines
5.9 KiB
TypeScript
168 lines
5.9 KiB
TypeScript
import type { ReactNode } from "react";
|
||
import Link from "next/link";
|
||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||
import { ONCEKI_YIL, SON_YIL } from "@/lib/veri-yillari";
|
||
|
||
/** Programatik tablo sayfaları için satır dilimi boyutu. */
|
||
export const SAYFA_BOYU = 50;
|
||
|
||
/**
|
||
* Düz listenin `sayfa` numaralı dilimi. Metadata (title/description) ile sayfa
|
||
* gövdesi aynı satırları anlatsın diye dilim hesabı tek yerde durur.
|
||
*/
|
||
export function sayfaDilimi<T>(liste: readonly T[], sayfa: number): T[] {
|
||
return liste.slice((sayfa - 1) * SAYFA_BOYU, sayfa * SAYFA_BOYU);
|
||
}
|
||
|
||
/**
|
||
* Aralıktaki sıraların hangi yıldan geldiği: hepsi son yıl / hepsi bir önceki
|
||
* yıl / karışık. `yalnizOnceki`: sırası son yılda olmayıp bir önceki yıldan
|
||
* gelen satır sayısı; `siraliSayi`: sırası olan toplam satır.
|
||
*/
|
||
export function siraYiliEtiketi(
|
||
yalnizOnceki: number,
|
||
siraliSayi: number,
|
||
): string {
|
||
if (yalnizOnceki === 0) return String(SON_YIL);
|
||
return yalnizOnceki === siraliSayi
|
||
? String(ONCEKI_YIL)
|
||
: `${ONCEKI_YIL}–${SON_YIL}`;
|
||
}
|
||
|
||
/**
|
||
* Üniversite ve bölüm tablo sayfalarının altındaki CtaSiraForm kartının köprü
|
||
* cümlesi — iki sayfa birebir aynı metni kullansın diye tek kaynak. Sunucu
|
||
* bileşenlerinden okunduğu için "use client" olan cta-sira-form.tsx'te
|
||
* duramaz (istemci modülünün sabitleri sunucuda değer olarak okunamaz).
|
||
* Vaat içermez: tablo geçmiş veridir, ürün sıralamaya yakın programları
|
||
* risk seviyesine göre ayırıp listeler.
|
||
*/
|
||
export const KATALOG_CTA_ACIKLAMA =
|
||
"Bu tablodaki tabanlar geçmiş yerleştirmelerin sonucu; bu yıl değişebilir. Sıralamanı gir, tabanı sıralamana yakın programları risk seviyesine göre ayrılmış hâlde listeleyelim. Ücretsiz, üyelik gerekmez.";
|
||
|
||
/**
|
||
* Statik path tabanlı sayfalama navigasyonu. 1. sayfa taban URL'in
|
||
* kendisidir (/bolum/x), sonrakiler /bolum/x/sayfa/2 ... — hepsi gerçek
|
||
* <Link>, Google her sayfayı ayrı statik URL olarak tarar.
|
||
*
|
||
* `onSayfa` verilirse Link yerine aynı görünümde butonlar render edilir —
|
||
* istemci tarafında sıralanmış listelerin sayfalaması URL değiştirmeden
|
||
* bu geri çağrıyla ilerler.
|
||
*/
|
||
export function Sayfalama({
|
||
tabanYol,
|
||
sayfa,
|
||
toplamSayfa,
|
||
sorgu,
|
||
onSayfa,
|
||
}: {
|
||
tabanYol: string;
|
||
sayfa: number;
|
||
toplamSayfa: number;
|
||
sorgu?: string;
|
||
onSayfa?: (n: number) => void;
|
||
}) {
|
||
if (toplamSayfa <= 1) return null;
|
||
const yol = (n: number) => {
|
||
const path = n === 1 ? tabanYol : `${tabanYol}/sayfa/${n}`;
|
||
return sorgu ? `${path}?${sorgu}` : path;
|
||
};
|
||
const gecis = (
|
||
n: number,
|
||
className: string,
|
||
icerik: ReactNode,
|
||
ozellikler?: { rel?: string; aktif?: boolean },
|
||
) =>
|
||
onSayfa ? (
|
||
<button
|
||
key={n}
|
||
type="button"
|
||
onClick={() => onSayfa(n)}
|
||
aria-current={ozellikler?.aktif ? "page" : undefined}
|
||
className={`${className} cursor-pointer`}
|
||
>
|
||
{icerik}
|
||
</button>
|
||
) : (
|
||
<Link
|
||
key={n}
|
||
href={yol(n)}
|
||
rel={ozellikler?.rel}
|
||
aria-current={ozellikler?.aktif ? "page" : undefined}
|
||
className={className}
|
||
>
|
||
{icerik}
|
||
</Link>
|
||
);
|
||
|
||
// 7'den çok sayfada pencere: 1 … (c-1) c (c+1) … son. Telefonda
|
||
// (sm altı) tek satır kalsın diye yalnız Önceki · aktif · Sonraki görünür;
|
||
// diğer numaralar HTML'de durur (tarayıcı botları tüm sayfaları görür).
|
||
const numaralar: (number | "...")[] = [];
|
||
if (toplamSayfa <= 7) {
|
||
for (let n = 1; n <= toplamSayfa; n++) numaralar.push(n);
|
||
} else {
|
||
numaralar.push(1);
|
||
if (sayfa > 3) numaralar.push("...");
|
||
for (
|
||
let n = Math.max(2, sayfa - 1);
|
||
n <= Math.min(toplamSayfa - 1, sayfa + 1);
|
||
n++
|
||
) {
|
||
numaralar.push(n);
|
||
}
|
||
if (sayfa < toplamSayfa - 2) numaralar.push("...");
|
||
numaralar.push(toplamSayfa);
|
||
}
|
||
|
||
return (
|
||
<nav
|
||
aria-label="Sayfalama"
|
||
className="mt-8 flex flex-wrap items-center justify-center gap-2"
|
||
>
|
||
{sayfa > 1
|
||
? gecis(
|
||
sayfa - 1,
|
||
"inline-flex h-11 items-center gap-1 rounded-full border border-slate-200 bg-white px-4 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]",
|
||
<>
|
||
<ChevronLeft className="size-4" aria-hidden />
|
||
Önceki
|
||
</>,
|
||
{ rel: "prev" },
|
||
)
|
||
: null}
|
||
{numaralar.map((n, i) =>
|
||
n === "..." ? (
|
||
<span
|
||
key={`bosluk-${i}`}
|
||
className="hidden px-1.5 text-sm text-slate-400 sm:inline"
|
||
aria-hidden
|
||
>
|
||
…
|
||
</span>
|
||
) : (
|
||
gecis(
|
||
n,
|
||
n === sayfa
|
||
? "inline-flex h-11 min-w-11 items-center justify-center rounded-full bg-primary px-2 text-sm font-semibold text-primary-foreground"
|
||
: "hidden h-11 min-w-11 items-center justify-center rounded-full border border-slate-200 bg-white px-2 text-sm font-semibold text-slate-700 sm:inline-flex 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]",
|
||
n,
|
||
{ aktif: n === sayfa },
|
||
)
|
||
),
|
||
)}
|
||
{sayfa < toplamSayfa
|
||
? gecis(
|
||
sayfa + 1,
|
||
"inline-flex h-11 items-center gap-1 rounded-full border border-slate-200 bg-white px-4 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]",
|
||
<>
|
||
Sonraki
|
||
<ChevronRight className="size-4" aria-hidden />
|
||
</>,
|
||
{ rel: "next" },
|
||
)
|
||
: null}
|
||
</nav>
|
||
);
|
||
}
|