fix(paket): girişsiz ziyaretçide boş sayfa — kart gövdesi statik kabukta, yalnız CTA dinamik
Kök neden: React Flight ~3,2 KB'ı aşan dinamik parçayı ayrı satırlara bölüyor; PPR resume bunlara verdiği segment kimliğini kabukta geç tamamlanan sınırın (kök loading.tsx) kimliğiyle çakıştırıyor (S:3 iki kez) → HierarchyRequestError. Lokal prod build'de yeniden üretildi, düzeltmeden sonra 5/5 temiz. Girişsiz CTA metni: "ilk 3 tercihini ücretsiz gör" (sayılar sabitlerden). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -4,10 +4,7 @@ import type { Metadata } from "next";
|
||||
import { SectionEyebrow } from "@/components/pixel-decor";
|
||||
import { Parallax } from "@/components/parallax";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import {
|
||||
PaketSatinAl,
|
||||
PaketSatinAlSkeleton,
|
||||
} from "@/features/odeme/components/paket-satinal";
|
||||
import { PaketSatinAl } from "@/features/odeme/components/paket-satinal";
|
||||
import { sayfaMetadata } from "@/lib/seo";
|
||||
|
||||
export const metadata: Metadata = sayfaMetadata({
|
||||
@@ -57,9 +54,9 @@ export default function PaketPage({
|
||||
</p>
|
||||
</Parallax>
|
||||
|
||||
<Suspense fallback={<PaketSatinAlSkeleton />}>
|
||||
<PaketSatinAl />
|
||||
</Suspense>
|
||||
{/* Kart gövdesi statik kabukta; yalnız CTA alanları içeride kendi
|
||||
Suspense sınırlarıyla istek anında gelir (bkz. paket-satinal.tsx). */}
|
||||
<PaketSatinAl />
|
||||
|
||||
<p className="mt-10 text-center text-xs leading-relaxed text-slate-500">
|
||||
KolayTercih bir karar destek aracıdır; öneriler resmî YÖK Atlas
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Suspense } from "react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import {
|
||||
@@ -11,7 +12,8 @@ import {
|
||||
ShieldCheck,
|
||||
} from "lucide-react";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
import { URUNLER, DENEME_KREDISI } from "@/lib/credits";
|
||||
import { URUNLER, DENEME_KREDISI, RAPOR_KREDI } from "@/lib/credits";
|
||||
import { ACIK_SATIR } from "@/lib/rapor-maske";
|
||||
import { VERI_YIL_SAYISI } from "@/lib/veri-kapsami";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Parallax } from "@/components/parallax";
|
||||
@@ -39,11 +41,17 @@ const OZELLIKLER = [
|
||||
},
|
||||
] as const;
|
||||
|
||||
// Paket + top-up kartları: kullanıcının paket/kredi durumuna göre CTA'lar
|
||||
// değişir, okuma burada yaşar.
|
||||
export async function PaketSatinAl() {
|
||||
const u = await getCurrentUser();
|
||||
|
||||
// Paket + top-up kartları. Kartların gövdesi (başlık, fiyat, özellikler,
|
||||
// iyzico bandı) STATİK kabukta kalır; kullanıcıya bağlı olan yalnızca iki küçük
|
||||
// CTA alanıdır (PaketCta, TopupCta) ve her biri kendi Suspense sınırındadır.
|
||||
//
|
||||
// Neden bu kadar küçük parçalar: React Flight ~3,2 KB'ı aşan bir dinamik
|
||||
// parçanın devamını ayrı satırlara böler; PPR resume bunlara yeni segment
|
||||
// kimliği verirken kabukta geç tamamlanan sınırın (kök loading.tsx) kimliğiyle
|
||||
// çakışıyor (Next 16.2 / React 19.2) ve tarayıcıda HierarchyRequestError ile
|
||||
// sayfa boş kalıyordu. Dinamik parçayı bu sınırın altında tut: kartın gövdesini
|
||||
// yeniden bu async bileşenlerin içine TAŞIMA.
|
||||
export function PaketSatinAl() {
|
||||
return (
|
||||
<Parallax
|
||||
strength={6}
|
||||
@@ -71,44 +79,9 @@ export async function PaketSatinAl() {
|
||||
))}
|
||||
</ul>
|
||||
<div className="mt-8">
|
||||
{u ? (
|
||||
u.hasPaket ? (
|
||||
<div className="flex items-center gap-2 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-800">
|
||||
<CheckCircle2 className="size-4" aria-hidden />
|
||||
Paketin aktif —{" "}
|
||||
{/* ?uret=1: rapor varsa /listem raporu gösterir (bayrak yok
|
||||
sayılır), yoksa bekleyen seçimlerle üretim başlar; seçim
|
||||
de yoksa boş durum sihirbaza yönlendirir. */}
|
||||
<Link href="/listem?uret=1" className="underline">
|
||||
listeni oluştur
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<SatinAlForm
|
||||
urun="paket"
|
||||
label={`${tl(URUNLER.paket.amountKurus)} TL — Paketi al`}
|
||||
vurgulu
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
asChild
|
||||
className="h-12 w-full cursor-pointer bg-orange-500 px-8 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||
>
|
||||
<Link
|
||||
href={`/giris?callback=${encodeURIComponent("/paket")}`}
|
||||
>
|
||||
Giriş yap — sonra {tl(URUNLER.paket.amountKurus)} TL'ye
|
||||
al
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</Link>
|
||||
</Button>
|
||||
<p className="text-center text-xs text-slate-500">
|
||||
Girişte {DENEME_KREDISI} deneme kredisi de tanımlanır.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<Suspense fallback={<Skeleton className="h-12 w-full rounded-full" />}>
|
||||
<PaketCta />
|
||||
</Suspense>
|
||||
</div>
|
||||
<Image
|
||||
src="/odeme/iyzico-logo-band.svg"
|
||||
@@ -139,47 +112,95 @@ export async function PaketSatinAl() {
|
||||
{tl(URUNLER.topup.amountKurus)} TL.
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
{/* 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 ? (
|
||||
<SatinAlForm
|
||||
urun="topup"
|
||||
label={`+${URUNLER.topup.credits} kredi — ${tl(URUNLER.topup.amountKurus)} TL`}
|
||||
/>
|
||||
) : u ? (
|
||||
<p className="rounded-xl border border-slate-200 bg-slate-50 px-3 py-2.5 text-xs leading-5 text-slate-600">
|
||||
Ek kredi, kilitli analizleri açmaz — o yüzden önce paket
|
||||
gerekir. Paket zaten {URUNLER.paket.credits} kredi içerir;
|
||||
bitince buradan yükleyebilirsin.
|
||||
</p>
|
||||
) : (
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
className="h-11 w-full cursor-pointer"
|
||||
>
|
||||
<Link
|
||||
href={`/giris?callback=${encodeURIComponent("/paket")}`}
|
||||
>
|
||||
Önce giriş yap
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
<Suspense fallback={<Skeleton className="h-11 w-full rounded-full" />}>
|
||||
<TopupCta />
|
||||
</Suspense>
|
||||
</div>
|
||||
</section>
|
||||
</Parallax>
|
||||
);
|
||||
}
|
||||
|
||||
export function PaketSatinAlSkeleton() {
|
||||
/** Ana paket kartının kullanıcıya bağlı CTA'sı — bilerek küçük (bkz. üstteki not). */
|
||||
async function PaketCta() {
|
||||
const u = await getCurrentUser();
|
||||
|
||||
if (!u) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
asChild
|
||||
className="h-12 w-full cursor-pointer bg-orange-500 px-8 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||
>
|
||||
<Link href={`/giris?callback=${encodeURIComponent("/paket")}`}>
|
||||
Giriş yap — ilk {ACIK_SATIR} tercihini ücretsiz gör
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</Link>
|
||||
</Button>
|
||||
<p className="text-center text-xs text-slate-500">
|
||||
Girişte {DENEME_KREDISI} deneme kredisi tanımlanır; liste{" "}
|
||||
{RAPOR_KREDI} kredi, ilk {ACIK_SATIR} satır açık. Paketi sonra
|
||||
alırsın.
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (u.hasPaket) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm font-medium text-emerald-800">
|
||||
<CheckCircle2 className="size-4" aria-hidden />
|
||||
Paketin aktif —{" "}
|
||||
{/* ?uret=1: rapor varsa /listem raporu gösterir (bayrak yok
|
||||
sayılır), yoksa bekleyen seçimlerle üretim başlar; seçim
|
||||
de yoksa boş durum sihirbaza yönlendirir. */}
|
||||
<Link href="/listem?uret=1" className="underline">
|
||||
listeni oluştur
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
aria-live="polite"
|
||||
className="mt-10 grid gap-6 sm:grid-cols-[1fr_auto]"
|
||||
>
|
||||
<Skeleton className="h-80 w-full rounded-2xl" />
|
||||
<Skeleton className="h-80 w-full rounded-2xl sm:w-64" />
|
||||
</div>
|
||||
<SatinAlForm
|
||||
urun="paket"
|
||||
label={`${tl(URUNLER.paket.amountKurus)} TL — Paketi al`}
|
||||
vurgulu
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/** Top-up kartının kullanıcıya bağlı CTA'sı — bilerek küçük (bkz. üstteki not). */
|
||||
async function TopupCta() {
|
||||
const u = await getCurrentUser();
|
||||
|
||||
// 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.
|
||||
if (u?.hasPaket) {
|
||||
return (
|
||||
<SatinAlForm
|
||||
urun="topup"
|
||||
label={`+${URUNLER.topup.credits} kredi — ${tl(URUNLER.topup.amountKurus)} TL`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (u) {
|
||||
return (
|
||||
<p className="rounded-xl border border-slate-200 bg-slate-50 px-3 py-2.5 text-xs leading-5 text-slate-600">
|
||||
Ek kredi, kilitli analizleri açmaz — o yüzden önce paket gerekir.
|
||||
Paket zaten {URUNLER.paket.credits} kredi içerir; bitince buradan
|
||||
yükleyebilirsin.
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Button asChild variant="outline" className="h-11 w-full cursor-pointer">
|
||||
<Link href={`/giris?callback=${encodeURIComponent("/paket")}`}>
|
||||
Önce giriş yap
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user