refactor: katalog feature'ı — sayfalar senkron kompozisyona geçti (Faz 2)

- 8 katalog bileşeni src/features/katalog/components/ altına taşındı;
  katalog-queries.ts (server-only) lib/katalog'un feature kapısı oldu
- /bolumler ve /universiteler'in sayfa içi fetch+inline bileşenleri
  BolumKatalogu/UniversiteKatalogu feature bileşenlerine çıkarıldı
- 6 sayfa async'ten senkron'a: params.then() + sayfada Suspense,
  skeleton'lar feature bileşeninin yanında (BolumIcerikSkeleton vb.)
- generateStaticParams artık *-queries yerine bileşen dosyasının
  helper'larını kullanıyor (bolumStaticSlugs, uniSayfaStaticParams)
- Davranış değişikliği yok; route durumları aynı (◐), 1917 statik sayfa

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-08-08 01:14:00 +03:00
parent 820eea7eca
commit 8d26e375ff
19 changed files with 425 additions and 258 deletions

View File

@@ -0,0 +1,130 @@
import type { ReactNode } from "react";
import Link from "next/link";
import { ChevronLeft, ChevronRight } from "lucide-react";
/** Programatik tablo sayfaları için satır dilimi boyutu. */
export const SAYFA_BOYU = 50;
/**
* 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
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-6 flex flex-wrap items-center justify-center gap-1.5"
>
{sayfa > 1
? gecis(
sayfa - 1,
"inline-flex h-9 items-center gap-1 rounded-full border border-slate-200 bg-white px-3.5 text-sm text-slate-700 hover:border-primary/40 hover:text-primary",
<>
<ChevronLeft className="size-4" aria-hidden />
Önceki
</>,
{ rel: "prev" },
)
: null}
{numaralar.map((n, i) =>
n === "..." ? (
<span
key={`bosluk-${i}`}
className="px-1.5 text-sm text-slate-400"
aria-hidden
>
…
</span>
) : (
gecis(
n,
n === sayfa
? "inline-flex h-9 min-w-9 items-center justify-center rounded-full bg-primary px-2 text-sm font-semibold text-primary-foreground"
: "inline-flex h-9 min-w-9 items-center justify-center rounded-full border border-slate-200 bg-white px-2 text-sm text-slate-700 hover:border-primary/40 hover:text-primary",
n,
{ aktif: n === sayfa },
)
),
)}
{sayfa < toplamSayfa
? gecis(
sayfa + 1,
"inline-flex h-9 items-center gap-1 rounded-full border border-slate-200 bg-white px-3.5 text-sm text-slate-700 hover:border-primary/40 hover:text-primary",
<>
Sonraki
<ChevronRight className="size-4" aria-hidden />
</>,
{ rel: "next" },
)
: null}
</nav>
);
}