- {u ? (
+ {/* Top-up hasPaket vermez: paketsiz biri alırsa parasını verip
+ yine maskeli liste görür. Satın alma yalnızca paketliye
+ açık; paketsize durum dürüstçe açıklanır. */}
+ {u?.hasPaket ? (
+ ) : u ? (
+
+ Ek kredi, kilitli analizleri açmaz — o yüzden önce paket
+ gerekir. Paket zaten {URUNLER.paket.credits} kredi içerir;
+ bitince buradan yükleyebilirsin.
+
) : (
;
if (durum === "kredi-yetersiz") {
+ // Paketli kullanıcıya paket satma: onun ürünü top-up. Liste ve sohbet
+ // geçmişi kalıcı; kredi yükleyince kaldığı yerden devam eder.
return (
@@ -163,15 +165,19 @@ export function ListePaneli({
Kredin bu liste için yetmiyor
- Liste üretimi 3 kredi. Tercih Dönemi Paketi'yle 60 kredi, tam
- liste, PDF ve revizyon hakları birlikte gelir.
+ {hasPaket
+ ? "Liste üretimi 3 kredi. +30 kredi yüklediğinde listen ve sohbet geçmişin duruyor, kaldığın yerden devam edersin."
+ : "Liste üretimi 3 kredi. Tercih Dönemi Paketi'yle 60 kredi, tam liste, PDF ve revizyon hakları birlikte gelir."}
);
diff --git a/src/app/sonuc/page.tsx b/src/app/sonuc/page.tsx
index c1b1299..bca5d15 100644
--- a/src/app/sonuc/page.tsx
+++ b/src/app/sonuc/page.tsx
@@ -18,6 +18,11 @@ import { ManuelHarita } from "@/components/manuel-liste/manuel-harita";
import { SiteFooter } from "@/components/site-footer";
import { SiraGerekli } from "./sira-gerekli";
+// listeOlustur aksiyonu 2 AI denemesi yapabiliyor (deneme başına 50 sn abort
+// sınırı, bkz. lib/ai/cagri.ts); platform limiti AI zaman aşımından SONRA
+// dolmalı ki kredi iadesi her zaman çalışabilsin.
+export const maxDuration = 120;
+
export const metadata: Metadata = {
title: "Tercih Listeni Kur",
// Param uzayı sonsuz + içerik sıralamaya göre kişisel: indexlenmez ama
diff --git a/src/app/sonuc/sihirbaz-cagri-karti.tsx b/src/app/sonuc/sihirbaz-cagri-karti.tsx
index dd28acb..c662270 100644
--- a/src/app/sonuc/sihirbaz-cagri-karti.tsx
+++ b/src/app/sonuc/sihirbaz-cagri-karti.tsx
@@ -105,6 +105,8 @@ export function SihirbazBolumu({
// Aynı üretim için sabit requestId (retry çifte harcama yapmaz)
const requestIdRef = useRef(null);
+ // Revizyon için aynı desen (bkz. listem/revizyon-kutusu.tsx)
+ const revizyonIdRef = useRef(null);
const otomatikBasladi = useRef(false);
const bolumRef = useRef(null);
// Funnel CTA'ları (banner olayı dahil) tıklama anındaki bekleyen seçimi
@@ -228,9 +230,17 @@ export function SihirbazBolumu({
// Yapay Zeka çıktısı bu sayfada gösterilmez: üretim animasyonu açık kalır,
// liste (paketsizse paywall'lı hâliyle) /listem'de karşılar.
router.push("/listem");
+ // Header'daki kredi pill'i layout'ta server-render edilir; push layout'u
+ // yeniden çizmediği için düşen bakiye refresh ile güncellenir.
+ router.refresh();
return;
}
- // Hata: aynı requestId'yi koru ki tekrar denemede çifte harcama olmasın
+ // Sunucudan cevap geldiyse harcama ya hiç olmadı ya iade edildi;
+ // sonraki deneme taze requestId ile başlar (aynı id ledger'daki UNIQUE'e
+ // takılıp DUPLICATE çıkmazına giriyordu). requestId yalnızca cevap
+ // alınamayan network hatasında (catch) korunur — orada çifte harcama
+ // riski gerçek.
+ requestIdRef.current = null;
if (sonuc.code === "KREDI" || sonuc.code === "PAKET") {
setDurum("kredi-yetersiz");
} else if (sonuc.code === "AUTH") {
@@ -389,10 +399,14 @@ export function SihirbazBolumu({
sira={sira}
acilis={acilis}
onRevize={async (feedback) => {
+ if (!revizyonIdRef.current) {
+ revizyonIdRef.current = crypto.randomUUID();
+ }
const sonuc = await listeRevize({
feedback,
- requestId: crypto.randomUUID(),
+ requestId: revizyonIdRef.current,
});
+ revizyonIdRef.current = null;
if (sonuc.ok) {
olay("liste_revize", { sayfa: "sonuc" });
setRapor({
diff --git a/src/components/bolum-program-listesi.tsx b/src/components/bolum-program-listesi.tsx
index 8ac6355..3c92148 100644
--- a/src/components/bolum-program-listesi.tsx
+++ b/src/components/bolum-program-listesi.tsx
@@ -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(null);
const [yon, setYon] = useState("artan");
+ // Sıralama aktifken URL değişmeden istemci tarafında gezilen sayfa
+ const [istemciSayfa, setIstemciSayfa] = useState(1);
+ const listeRef = useRef(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 (
-