All checks were successful
Deploy / deploy (push) Successful in 6m23s
- 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>
207 lines
7.1 KiB
TypeScript
207 lines
7.1 KiB
TypeScript
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>
|
||
);
|
||
}
|