Files
kolaytercih/src/features/odeme/components/paket-satinal.tsx
bilalgursen 78ad84922a
All checks were successful
Deploy / deploy (push) Successful in 6m23s
feat(tema): açık / koyu / sistem teması
- TemaSaglayici (next-themes) + alt bilgide TemaSecici
- globals.css: .dark bloğunda palet token'ları çevrilir; bileşenlere
  dark: sınıfı yazılmaz
- yüzeyler bg-white → bg-card, koyu haplar text-slate-50, SVG'lerde
  sabit renk yerine değişken
- DESIGN.md §2.3 + tasarim-denetim kuralları (yuzey-beyaz,
  koyu-hap-beyaz-yazi, dark-sinif), AGENTS.md karanlık mod bölümü
- docs: koyu tema denetimi raporu

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-05 10:20:02 +03:00

207 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Suspense } from "react";
import Image from "next/image";
import Link from "next/link";
import {
ArrowRight,
CheckCircle2,
Coins,
FileText,
ListChecks,
MessageCircleQuestion,
RefreshCcw,
ShieldCheck,
} from "lucide-react";
import { getCurrentUser } from "@/lib/session";
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";
import { Skeleton } from "@/components/ui/skeleton";
import { SatinAlForm } from "./satin-al-form";
function tl(kurus: number) {
return (kurus / 100).toLocaleString("tr-TR", { maximumFractionDigits: 0 });
}
const OZELLIKLER = [
{
icon: ListChecks,
text: "Sıralamana özel, gerekçeli 24 tercihlik dengeli liste",
},
{
icon: ShieldCheck,
text: `Her tercih için yerleşme riski ve trend analizi (son ${VERI_YIL_SAYISI} yıl)`,
},
{ icon: FileText, text: "Veliyle paylaşılabilir PDF rapor" },
{ icon: RefreshCcw, text: "2 liste revizyon hakkı" },
{
icon: MessageCircleQuestion,
text: `${URUNLER.paket.credits} Yapay Zeka danışman sorusu (her mesaj 1 kredi)`,
},
] as const;
// 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}
className="mt-10 grid grid-cols-[minmax(0,1fr)] gap-6 sm:grid-cols-[minmax(0,1fr)_auto]"
>
{/* Ana paket */}
<section className="rounded-2xl border-2 border-orange-500 bg-card p-8 shadow-sm">
<div className="flex items-baseline justify-between">
<h2 className="font-heading text-xl font-bold">
Tercih Dönemi Paketi
</h2>
<p className="font-heading text-3xl font-bold">
{tl(URUNLER.paket.amountKurus)} TL
</p>
</div>
<ul className="mt-6 space-y-3">
{OZELLIKLER.map((o) => (
<li key={o.text} className="flex items-start gap-3 text-sm">
<o.icon
className="mt-0.5 size-4 shrink-0 text-emerald-600"
aria-hidden
/>
{o.text}
</li>
))}
</ul>
<div className="mt-8">
<Suspense fallback={<Skeleton className="h-12 w-full rounded-full" />}>
<PaketCta />
</Suspense>
</div>
<Image
src="/odeme/iyzico-logo-band.svg"
alt="iyzico ile Öde — Mastercard, Visa, American Express ve Troy ile güvenli ödeme"
width={429}
height={32}
className="kt-acik-logo mx-auto mt-5 h-5 w-auto"
/>
<p className="mt-3 text-center text-xs text-slate-500">
iyzico güvenli ödeme · Kart bilgilerin bize hiç ulaşmaz ·{" "}
<Link
href="/kosullar#iade"
className="underline hover:text-slate-700"
>
İade koşulları
</Link>
</p>
</section>
{/* Top-up */}
<section className="flex h-fit flex-col rounded-2xl border border-slate-200 bg-card p-6 shadow-sm sm:w-56">
<div className="flex items-center gap-2">
<Coins className="size-4 text-amber-500" aria-hidden />
<h2 className="font-heading font-bold">Kredi bitti mi?</h2>
</div>
<p className="mt-2 flex-1 text-sm text-slate-600">
+{URUNLER.topup.credits} soru hakkı,{" "}
{tl(URUNLER.topup.amountKurus)} TL.
</p>
<div className="mt-4">
<Suspense fallback={<Skeleton className="h-11 w-full rounded-full" />}>
<TopupCta />
</Suspense>
</div>
</section>
</Parallax>
);
}
/** 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-auto min-h-12 w-full cursor-pointer bg-orange-500 px-6 py-2 text-center whitespace-normal text-white transition-colors duration-200 hover:bg-orange-600 sm:px-8"
>
<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&kaynak=odeme_sonrasi" className="underline">
listeni oluştur
</Link>
</div>
);
}
return (
<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>
);
}