Enhance error handling and user experience in AI-related features
Some checks failed
Deploy / deploy (push) Failing after 38m47s
Some checks failed
Deploy / deploy (push) Failing after 38m47s
- Introduced a mechanism to save partially generated responses in case of errors, ensuring users do not lose their input. - Updated the RevizyonKutusu component to utilize a persistent request ID for retrying failed requests without double charging credits. - Improved the Sayfalama component to support client-side pagination without changing the URL, enhancing user navigation. - Added a timeout for AI generation requests to ensure credits are refunded in case of prolonged processing. - Refactored various components for better clarity and responsiveness, including updates to the BolumProgramListesi and ListePaneli components.
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useMemo, useRef, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { ChevronDown, ChevronUp } from "lucide-react";
|
||||
import type { Program } from "@/lib/db";
|
||||
import { trBaslikDuzeni } from "@/lib/slug";
|
||||
import { Sayfalama, SAYFA_BOYU } from "@/components/sayfalama";
|
||||
import {
|
||||
ProgramSiraGecmisi,
|
||||
ProgramTabanTrendi,
|
||||
@@ -82,12 +83,22 @@ function SiralamaDugmesi({
|
||||
export function BolumProgramListesi({
|
||||
programlar,
|
||||
bazAd,
|
||||
tabanYol,
|
||||
sayfa,
|
||||
}: {
|
||||
/** Bölümün TÜM programları — sıralama liste geneline uygulanır. */
|
||||
programlar: BolumProgrami[];
|
||||
bazAd: string;
|
||||
tabanYol: string;
|
||||
sayfa: number;
|
||||
}) {
|
||||
const [aktifAlan, setAktifAlan] = useState<SiralamaAlani | null>(null);
|
||||
const [yon, setYon] = useState<SiralamaYonu>("artan");
|
||||
// Sıralama aktifken URL değişmeden istemci tarafında gezilen sayfa
|
||||
const [istemciSayfa, setIstemciSayfa] = useState(1);
|
||||
const listeRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const toplamSayfa = Math.max(1, Math.ceil(programlar.length / SAYFA_BOYU));
|
||||
|
||||
const siraliProgramlar = useMemo(() => {
|
||||
if (!aktifAlan) return programlar;
|
||||
@@ -107,7 +118,18 @@ export function BolumProgramListesi({
|
||||
.map(({ program }) => program);
|
||||
}, [aktifAlan, programlar, yon]);
|
||||
|
||||
const gosterilenSayfa = aktifAlan ? istemciSayfa : sayfa;
|
||||
const gosterilenProgramlar = useMemo(
|
||||
() =>
|
||||
siraliProgramlar.slice(
|
||||
(gosterilenSayfa - 1) * SAYFA_BOYU,
|
||||
gosterilenSayfa * SAYFA_BOYU,
|
||||
),
|
||||
[siraliProgramlar, gosterilenSayfa],
|
||||
);
|
||||
|
||||
function sirala(alan: SiralamaAlani) {
|
||||
setIstemciSayfa(1);
|
||||
if (aktifAlan === alan) {
|
||||
setYon((mevcut) => (mevcut === "artan" ? "azalan" : "artan"));
|
||||
return;
|
||||
@@ -116,124 +138,140 @@ export function BolumProgramListesi({
|
||||
setYon("artan");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="overflow-hidden rounded-2xl border border-slate-200 bg-white">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Üniversite</TableHead>
|
||||
<TableHead className="hidden text-right md:table-cell">
|
||||
Son 5 yıl
|
||||
</TableHead>
|
||||
<TableHead className="hidden text-right lg:table-cell">
|
||||
<SiralamaDugmesi
|
||||
alan="puan2025"
|
||||
aktifAlan={aktifAlan}
|
||||
yon={yon}
|
||||
onSirala={sirala}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="hidden text-right sm:table-cell">
|
||||
<SiralamaDugmesi
|
||||
alan="kontenjan2025"
|
||||
aktifAlan={aktifAlan}
|
||||
yon={yon}
|
||||
onSirala={sirala}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="text-right">
|
||||
<SiralamaDugmesi
|
||||
alan="sira2025"
|
||||
aktifAlan={aktifAlan}
|
||||
yon={yon}
|
||||
onSirala={sirala}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="w-14 text-right">
|
||||
<span className="sr-only">Listeye ekle</span>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{siraliProgramlar.map((p) => {
|
||||
const uniAd = trBaslikDuzeni(p.universite);
|
||||
const varyant = p.isim.trim() !== bazAd ? p.isim.trim() : null;
|
||||
const devlet = p.unitur === "DEVLET";
|
||||
function istemciSayfaDegistir(n: number) {
|
||||
setIstemciSayfa(n);
|
||||
listeRef.current?.scrollIntoView({ block: "start" });
|
||||
}
|
||||
|
||||
return (
|
||||
<TableRow key={p.id}>
|
||||
<TableCell className="max-w-0 w-full">
|
||||
<span className="flex min-w-0 items-center gap-2.5">
|
||||
{p.universiteSlug ? (
|
||||
<UniLogo slug={p.universiteSlug} ad={uniAd} boy="sm" />
|
||||
) : null}
|
||||
<span className="min-w-0 flex-1">
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
ref={listeRef}
|
||||
className="scroll-mt-24 overflow-hidden rounded-2xl border border-slate-200 bg-white"
|
||||
>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Üniversite</TableHead>
|
||||
<TableHead className="hidden text-right md:table-cell">
|
||||
Son 5 yıl
|
||||
</TableHead>
|
||||
<TableHead className="hidden text-right lg:table-cell">
|
||||
<SiralamaDugmesi
|
||||
alan="puan2025"
|
||||
aktifAlan={aktifAlan}
|
||||
yon={yon}
|
||||
onSirala={sirala}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="hidden text-right sm:table-cell">
|
||||
<SiralamaDugmesi
|
||||
alan="kontenjan2025"
|
||||
aktifAlan={aktifAlan}
|
||||
yon={yon}
|
||||
onSirala={sirala}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="text-right">
|
||||
<SiralamaDugmesi
|
||||
alan="sira2025"
|
||||
aktifAlan={aktifAlan}
|
||||
yon={yon}
|
||||
onSirala={sirala}
|
||||
/>
|
||||
</TableHead>
|
||||
<TableHead className="w-14 text-right">
|
||||
<span className="sr-only">Listeye ekle</span>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{gosterilenProgramlar.map((p) => {
|
||||
const uniAd = trBaslikDuzeni(p.universite);
|
||||
const varyant = p.isim.trim() !== bazAd ? p.isim.trim() : null;
|
||||
const devlet = p.unitur === "DEVLET";
|
||||
|
||||
return (
|
||||
<TableRow key={p.id}>
|
||||
<TableCell className="max-w-0 w-full">
|
||||
<span className="flex min-w-0 items-center gap-2.5">
|
||||
{p.universiteSlug ? (
|
||||
<Link
|
||||
href={`/universite/${p.universiteSlug}`}
|
||||
className="block truncate text-sm font-semibold hover:text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{uniAd}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="block truncate text-sm font-semibold">
|
||||
{uniAd}
|
||||
</span>
|
||||
)}
|
||||
<span className="mt-0.5 flex min-w-0 items-center gap-1.5 text-xs text-slate-500">
|
||||
{p.unitur ? (
|
||||
<span
|
||||
className={`shrink-0 rounded px-1 py-px text-[10px] font-medium leading-4 ${
|
||||
devlet
|
||||
? "bg-slate-100 text-slate-600"
|
||||
: "bg-violet-50 text-violet-600"
|
||||
}`}
|
||||
<UniLogo slug={p.universiteSlug} ad={uniAd} boy="sm" />
|
||||
) : null}
|
||||
<span className="min-w-0 flex-1">
|
||||
{p.universiteSlug ? (
|
||||
<Link
|
||||
href={`/universite/${p.universiteSlug}`}
|
||||
className="block truncate text-sm font-semibold hover:text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{devlet ? "Devlet" : trBaslikDuzeni(p.unitur)}
|
||||
{uniAd}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="block truncate text-sm font-semibold">
|
||||
{uniAd}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="truncate">
|
||||
{p.il ? trBaslikDuzeni(p.il) : null}
|
||||
{varyant ? `${p.il ? " · " : ""}${varyant}` : ""}
|
||||
)}
|
||||
<span className="mt-0.5 flex min-w-0 items-center gap-1.5 text-xs text-slate-500">
|
||||
{p.unitur ? (
|
||||
<span
|
||||
className={`shrink-0 rounded px-1 py-px text-[10px] font-medium leading-4 ${
|
||||
devlet
|
||||
? "bg-slate-100 text-slate-600"
|
||||
: "bg-violet-50 text-violet-600"
|
||||
}`}
|
||||
>
|
||||
{devlet ? "Devlet" : trBaslikDuzeni(p.unitur)}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="truncate">
|
||||
{p.il ? trBaslikDuzeni(p.il) : null}
|
||||
{varyant ? `${p.il ? " · " : ""}${varyant}` : ""}
|
||||
</span>
|
||||
{p.sure ? (
|
||||
<span className="shrink-0 text-slate-400">
|
||||
· {p.sure} yıl
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
{p.sure ? (
|
||||
<span className="shrink-0 text-slate-400">
|
||||
· {p.sure} yıl
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="hidden text-right text-xs md:table-cell">
|
||||
<ProgramSiraGecmisi program={p} />
|
||||
</TableCell>
|
||||
<TableCell className="hidden text-right text-xs tabular-nums text-slate-500 lg:table-cell">
|
||||
{p.puan2025 != null
|
||||
? p.puan2025.toFixed(2).replace(".", ",")
|
||||
: "—"}
|
||||
</TableCell>
|
||||
<TableCell className="hidden text-right text-xs tabular-nums text-slate-500 sm:table-cell">
|
||||
{p.kontenjan2025 != null ? `${sayi(p.kontenjan2025)} kişi` : "—"}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-xs tabular-nums text-slate-500">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<ProgramTabanTrendi program={p} />
|
||||
{p.sira2025 != null ? `~${sayi(p.sira2025)}.` : "—"}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="pr-3 text-right">
|
||||
<ProgramEkleButonu
|
||||
program={p}
|
||||
kaynak="bolum"
|
||||
className="size-8 min-h-8 min-w-8"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden text-right text-xs md:table-cell">
|
||||
<ProgramSiraGecmisi program={p} />
|
||||
</TableCell>
|
||||
<TableCell className="hidden text-right text-xs tabular-nums text-slate-500 lg:table-cell">
|
||||
{p.puan2025 != null
|
||||
? p.puan2025.toFixed(2).replace(".", ",")
|
||||
: "—"}
|
||||
</TableCell>
|
||||
<TableCell className="hidden text-right text-xs tabular-nums text-slate-500 sm:table-cell">
|
||||
{p.kontenjan2025 != null ? `${sayi(p.kontenjan2025)} kişi` : "—"}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-xs tabular-nums text-slate-500">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<ProgramTabanTrendi program={p} />
|
||||
{p.sira2025 != null ? `~${sayi(p.sira2025)}.` : "—"}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="pr-3 text-right">
|
||||
<ProgramEkleButonu
|
||||
program={p}
|
||||
kaynak="bolum"
|
||||
className="size-8 min-h-8 min-w-8"
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
<Sayfalama
|
||||
tabanYol={tabanYol}
|
||||
sayfa={gosterilenSayfa}
|
||||
toplamSayfa={toplamSayfa}
|
||||
onSayfa={aktifAlan ? istemciSayfaDegistir : undefined}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ReactNode } from "react";
|
||||
import Link from "next/link";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
|
||||
@@ -8,23 +9,56 @@ 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 | "...")[] = [];
|
||||
@@ -49,16 +83,17 @@ export function Sayfalama({
|
||||
aria-label="Sayfalama"
|
||||
className="mt-6 flex flex-wrap items-center justify-center gap-1.5"
|
||||
>
|
||||
{sayfa > 1 ? (
|
||||
<Link
|
||||
href={yol(sayfa - 1)}
|
||||
rel="prev"
|
||||
className="inline-flex h-9 items-center gap-1 rounded-lg border border-slate-200 bg-white px-3 text-sm text-slate-700 hover:border-primary/40 hover:text-primary"
|
||||
>
|
||||
<ChevronLeft className="size-4" aria-hidden />
|
||||
Önceki
|
||||
</Link>
|
||||
) : null}
|
||||
{sayfa > 1
|
||||
? gecis(
|
||||
sayfa - 1,
|
||||
"inline-flex h-9 items-center gap-1 rounded-lg border border-slate-200 bg-white px-3 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
|
||||
@@ -69,30 +104,27 @@ export function Sayfalama({
|
||||
…
|
||||
</span>
|
||||
) : (
|
||||
<Link
|
||||
key={n}
|
||||
href={yol(n)}
|
||||
aria-current={n === sayfa ? "page" : undefined}
|
||||
className={
|
||||
n === sayfa
|
||||
? "inline-flex h-9 min-w-9 items-center justify-center rounded-lg bg-primary px-2 text-sm font-semibold text-primary-foreground"
|
||||
: "inline-flex h-9 min-w-9 items-center justify-center rounded-lg border border-slate-200 bg-white px-2 text-sm text-slate-700 hover:border-primary/40 hover:text-primary"
|
||||
}
|
||||
>
|
||||
{n}
|
||||
</Link>
|
||||
gecis(
|
||||
n,
|
||||
n === sayfa
|
||||
? "inline-flex h-9 min-w-9 items-center justify-center rounded-lg bg-primary px-2 text-sm font-semibold text-primary-foreground"
|
||||
: "inline-flex h-9 min-w-9 items-center justify-center rounded-lg 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 ? (
|
||||
<Link
|
||||
href={yol(sayfa + 1)}
|
||||
rel="next"
|
||||
className="inline-flex h-9 items-center gap-1 rounded-lg border border-slate-200 bg-white px-3 text-sm text-slate-700 hover:border-primary/40 hover:text-primary"
|
||||
>
|
||||
Sonraki
|
||||
<ChevronRight className="size-4" aria-hidden />
|
||||
</Link>
|
||||
) : null}
|
||||
{sayfa < toplamSayfa
|
||||
? gecis(
|
||||
sayfa + 1,
|
||||
"inline-flex h-9 items-center gap-1 rounded-lg border border-slate-200 bg-white px-3 text-sm text-slate-700 hover:border-primary/40 hover:text-primary",
|
||||
<>
|
||||
Sonraki
|
||||
<ChevronRight className="size-4" aria-hidden />
|
||||
</>,
|
||||
{ rel: "next" },
|
||||
)
|
||||
: null}
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user