refactor: odeme feature'ı + son süpürme — geçiş tamamlandı (Faz 10)
Some checks failed
Deploy / deploy (push) Failing after 3m46s

- paket/actions.ts → odeme-actions.ts; satin-al-form + rapor-yok-cta
  features/odeme/components/ altına; odeme-queries.ts (siparisGetir)
- Yeni async OdemeSonucKarti: verifySession + sahiplik kontrolü + self-healing
  odemeyiSonuclandir çağrısı akıştaki yerinde (artık Suspense altındaki feature
  bileşeninde, aynı render semantiği); başarı/hata kartları içeride
- Yeni async PaketSatinAl: paket + top-up kartları, getCurrentUser okuması
  içeride; statik hero/dipnot sayfada kaldı
- İki sayfa senkron searchParams.then(); paket hata uyarısı sayfada çözülmüş
  JSX (hata yokken hiçbir şey çizilmediği için fallback bilerek null);
  paket/loading.tsx + odeme/sonuc/loading.tsx silindi
- Süpürme doğrulandı: app/ altında route-dışı bileşen dosyası kalmadı; eski
  yol grep'leri sıfır; 7 *-queries server-only, 3 *-actions 'use server';
  async sayfa sıfır; /api/odeme/callback el değmedi
- Davranış değişikliği yok

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-08-08 01:46:23 +03:00
parent 22e2fde058
commit d2732828cc
10 changed files with 373 additions and 345 deletions

View File

@@ -1,19 +0,0 @@
import { Skeleton } from "@/components/ui/skeleton";
// /odeme/sonuc iyzico doğrulaması bekler — ortalanmış sonuç kartı iskeleti.
export default function Loading() {
return (
<main
aria-live="polite"
className="mx-auto flex w-full max-w-md flex-1 flex-col justify-center px-4 py-16"
>
<div className="rounded-2xl border border-slate-200 bg-white p-8 shadow-sm">
<Skeleton className="mx-auto size-12 rounded-full" />
<Skeleton className="mx-auto mt-5 h-7 w-2/3" />
<Skeleton className="mx-auto mt-3 h-5 w-full" />
<Skeleton className="mx-auto mt-2 h-5 w-3/4" />
<Skeleton className="mx-auto mt-6 h-12 w-full rounded-lg" />
</div>
</main>
);
}

View File

@@ -1,126 +1,29 @@
import Link from "next/link"; import { Suspense } from "react";
import type { Metadata } from "next"; import type { Metadata } from "next";
import { eq } from "drizzle-orm";
import { ArrowRight, CheckCircle2, XCircle } from "lucide-react";
import { verifySession } from "@/lib/session";
import { appDb, schema } from "@/lib/appdb";
import { kullanicininRaporu } from "@/features/rapor/rapor-queries";
import { odemeyiSonuclandir } from "@/lib/odeme";
import { Button } from "@/components/ui/button";
import { PagePixelDivider } from "@/components/pixel-decor";
import { SiteFooter } from "@/components/site-footer"; import { SiteFooter } from "@/components/site-footer";
import { RaporYokCta } from "./rapor-yok-cta"; import {
OdemeSonucKarti,
OdemeSonucKartiSkeleton,
} from "@/features/odeme/components/odeme-sonuc-karti";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Ödeme Sonucu", title: "Ödeme Sonucu",
robots: { index: false, follow: false }, robots: { index: false, follow: false },
}; };
export default async function OdemeSonucPage({ export default function OdemeSonucPage({
searchParams, searchParams,
}: { }: {
searchParams: Promise<{ siparis?: string }>; searchParams: Promise<{ siparis?: string }>;
}) { }) {
const { siparis } = await searchParams;
// Oturum düşmüşse geri dönüşte ?siparis= kaybolmasın (başarı/unblur deneyimi)
const geriDonus = siparis
? `/odeme/sonuc?siparis=${encodeURIComponent(siparis)}`
: "/paket";
const session = await verifySession(geriDonus);
const order = siparis
? await appDb.query.orders.findFirst({
where: eq(schema.orders.id, siparis),
})
: null;
// Yalnızca kendi siparişi görüntülenebilir
const sahibiMi = order?.userId === session.user.id;
// Self-healing: callback kaybolduysa burada tekrar doğrula
const durum =
order && sahibiMi
? order.status === "paid"
? "paid"
: await odemeyiSonuclandir(order.id)
: "not_found";
const basarili = durum === "paid";
// Rapor zaten üretildiyse ödül bir tık uzakta: kullanıcıyı doğrudan
// rapor sayfasına döndür ki blur çözülme animasyonuyla tam listeyi görsün.
const rapor = basarili ? await kullanicininRaporu(session.user.id) : null;
const listeHref = rapor ? "/listem?acildi=1" : null;
return ( return (
<div className="flex min-h-screen flex-col bg-slate-50 text-slate-900"> <div className="flex min-h-screen flex-col bg-slate-50 text-slate-900">
<main className="mx-auto flex w-full max-w-md flex-1 flex-col justify-center px-4 py-16 text-center"> <main className="mx-auto flex w-full max-w-md flex-1 flex-col justify-center px-4 py-16 text-center">
{basarili ? ( <Suspense fallback={<OdemeSonucKartiSkeleton />}>
<div className="rounded-2xl border border-emerald-200 bg-white p-8 shadow-sm"> {searchParams.then(({ siparis }) => (
<CheckCircle2 <OdemeSonucKarti siparis={siparis} />
className="mx-auto size-12 text-emerald-500" ))}
aria-hidden </Suspense>
/>
<h1 className="mt-4 font-heading text-2xl font-bold">
Ödeme alındı 🎉
</h1>
<p className="mt-2 text-sm text-slate-600">
<span className="font-semibold">
{order!.credits} kredi
</span>{" "}
hesabına tanımlandı
{order!.product === "paket"
? " ve Tercih Dönemi Paketi'n aktif — kişisel listen, risk raporun ve PDF çıktın hazır olduğunda burada."
: "."}
</p>
<PagePixelDivider seed={79} className="mx-auto mt-5" />
<div className="mt-6 flex flex-col gap-2">
{listeHref ? (
<>
<Button
asChild
className="h-11 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
>
<Link href={listeHref}>
Listeni kilidi kaldırdık
<ArrowRight className="size-4" aria-hidden />
</Link>
</Button>
<p className="text-center text-xs text-slate-500">
24 tercihin tamamı, gerekçeler, riskler ve PDF artık ık.
</p>
</>
) : (
<RaporYokCta />
)}
</div>
</div>
) : (
<div className="rounded-2xl border border-red-200 bg-white p-8 shadow-sm">
<XCircle className="mx-auto size-12 text-red-500" aria-hidden />
<h1 className="mt-4 font-heading text-2xl font-bold">
Ödeme tamamlanamadı
</h1>
<p className="mt-2 text-sm text-slate-600">
{durum === "pending" ? (
"Ödemen hâlâ işleniyor. Birkaç saniye sonra bu sayfayı yenile — kartından çekim yapıldıysa krediler mutlaka tanımlanır."
) : (
<>
Çekim yapılmadı ya da işlem iptal edildi. Kartından para
çekildiğini düşünüyorsan{" "}
<Link href="/iletisim" className="underline">
bizimle iletişime geç
</Link>
.
</>
)}
</p>
<PagePixelDivider seed={83} className="mx-auto mt-5" />
<Button asChild variant="outline" className="mt-6 h-11 cursor-pointer">
<Link href="/paket">Tekrar dene</Link>
</Button>
</div>
)}
</main> </main>
<SiteFooter /> <SiteFooter />
</div> </div>

View File

@@ -1,23 +0,0 @@
import { Skeleton } from "@/components/ui/skeleton";
// /paket dinamik (kullanıcı kredisi) — statik kabuk için sayfa geometrisine
// uygun iskelet: ortalanmış başlık bloğu + paket kartı şeridi.
export default function Loading() {
return (
<main
aria-live="polite"
className="mx-auto w-full max-w-3xl flex-1 px-4 py-16"
>
<div className="flex flex-col items-center text-center">
<Skeleton className="h-6 w-56 rounded-full" />
<Skeleton className="mt-4 h-10 w-3/4 max-w-xl" />
<Skeleton className="mt-3 h-5 w-2/3 max-w-lg" />
<Skeleton className="mt-2 h-5 w-1/2 max-w-md" />
</div>
<div 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>
</main>
);
}

View File

@@ -1,23 +1,13 @@
import Image from "next/image"; import { Suspense } from "react";
import Link from "next/link"; import Link from "next/link";
import type { Metadata } from "next"; import type { Metadata } from "next";
import {
ArrowRight,
CheckCircle2,
Coins,
FileText,
ListChecks,
MessageCircleQuestion,
RefreshCcw,
ShieldCheck,
} from "lucide-react";
import { getCurrentUser } from "@/lib/session";
import { URUNLER, DENEME_KREDISI } from "@/lib/credits";
import { Button } from "@/components/ui/button";
import { SectionEyebrow } from "@/components/pixel-decor"; import { SectionEyebrow } from "@/components/pixel-decor";
import { Parallax } from "@/components/parallax"; import { Parallax } from "@/components/parallax";
import { SatinAlForm } from "./satin-al-form";
import { SiteFooter } from "@/components/site-footer"; import { SiteFooter } from "@/components/site-footer";
import {
PaketSatinAl,
PaketSatinAlSkeleton,
} from "@/features/odeme/components/paket-satinal";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Tercih Dönemi Paketi", title: "Tercih Dönemi Paketi",
@@ -26,49 +16,31 @@ export const metadata: Metadata = {
alternates: { canonical: "/paket" }, alternates: { canonical: "/paket" },
}; };
function tl(kurus: number) { export default function PaketPage({
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 olasılığı ve risk analizi (son 4 yıl trend)",
},
{ 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;
export default async function PaketPage({
searchParams, searchParams,
}: { }: {
searchParams: Promise<{ hata?: string }>; searchParams: Promise<{ hata?: string }>;
}) { }) {
const [u, { hata }] = await Promise.all([getCurrentUser(), searchParams]);
// Ödeme callback'i doğrulama hatasında buraya ?hata=token|siparis ile döner
const odemeHatasi = hata === "token" || hata === "siparis";
return ( return (
<div className="flex min-h-screen flex-col bg-slate-50 text-slate-900"> <div className="flex min-h-screen flex-col bg-slate-50 text-slate-900">
<main className="mx-auto w-full max-w-3xl flex-1 px-4 py-16"> <main className="mx-auto w-full max-w-3xl flex-1 px-4 py-16">
{odemeHatasi ? ( {/* Ödeme callback'i doğrulama hatasında buraya ?hata=token|siparis ile
döner; hata yokken hiçbir şey çizilmez — fallback bilerek boş. */}
<Suspense fallback={null}>
{searchParams.then(({ hata }) =>
hata === "token" || hata === "siparis" ? (
<div <div
role="alert" role="alert"
className="mb-8 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800" className="mb-8 rounded-2xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-800"
> >
Ödemen doğrulanamadı. Kartından çekim yapıldıysa kredilerin birkaç Ödemen doğrulanamadı. Kartından çekim yapıldıysa kredilerin
dakika içinde otomatik tanımlanır; tanımlanmazsa aşağıdan tekrar birkaç dakika içinde otomatik tanımlanır; tanımlanmazsa
deneyebilirsin çekim yapılmadıysa hiçbir ücret ödemedin. aşağıdan tekrar deneyebilirsin çekim yapılmadıysa hiçbir
ücret ödemedin.
</div> </div>
) : null} ) : null,
)}
</Suspense>
<Parallax fromStart strength={12} className="text-center"> <Parallax fromStart strength={12} className="text-center">
<SectionEyebrow>Tek seferlik ödeme · Abonelik yok</SectionEyebrow> <SectionEyebrow>Tek seferlik ödeme · Abonelik yok</SectionEyebrow>
<h1 className="mt-4 font-heading text-3xl font-bold sm:text-4xl"> <h1 className="mt-4 font-heading text-3xl font-bold sm:text-4xl">
@@ -81,127 +53,9 @@ export default async function PaketPage({
</p> </p>
</Parallax> </Parallax>
<Parallax <Suspense fallback={<PaketSatinAlSkeleton />}>
strength={6} <PaketSatinAl />
className="mt-10 grid gap-6 sm:grid-cols-[1fr_auto]" </Suspense>
>
{/* Ana paket */}
<section className="rounded-2xl border-2 border-orange-500 bg-white 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">
{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 {" "}
<Link href="/" 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&apos;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>
)}
</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="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-white 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">
{/* Top-up hasPaket vermez: paketsiz biri alırsa parasını verip
yine maskeli liste görür. Satın alma yalnızca paketliye
ı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>
)}
</div>
</section>
</Parallax>
<p className="mt-10 text-center text-xs leading-relaxed text-slate-500"> <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 KolayTercih bir karar destek aracıdır; öneriler resmî YÖK Atlas

View File

@@ -0,0 +1,121 @@
import Link from "next/link";
import { ArrowRight, CheckCircle2, XCircle } from "lucide-react";
import { verifySession } from "@/lib/session";
import { siparisGetir } from "../odeme-queries";
import { kullanicininRaporu } from "@/features/rapor/rapor-queries";
import { odemeyiSonuclandir } from "@/lib/odeme";
import { Button } from "@/components/ui/button";
import { PagePixelDivider } from "@/components/pixel-decor";
import { Skeleton } from "@/components/ui/skeleton";
import { RaporYokCta } from "./rapor-yok-cta";
export async function OdemeSonucKarti({ siparis }: { siparis?: string }) {
// Oturum düşmüşse geri dönüşte ?siparis= kaybolmasın (başarı/unblur deneyimi)
const geriDonus = siparis
? `/odeme/sonuc?siparis=${encodeURIComponent(siparis)}`
: "/paket";
const session = await verifySession(geriDonus);
const order = siparis ? await siparisGetir(siparis) : null;
// Yalnızca kendi siparişi görüntülenebilir
const sahibiMi = order?.userId === session.user.id;
// Self-healing: callback kaybolduysa burada tekrar doğrula
const durum =
order && sahibiMi
? order.status === "paid"
? "paid"
: await odemeyiSonuclandir(order.id)
: "not_found";
const basarili = durum === "paid";
// Rapor zaten üretildiyse ödül bir tık uzakta: kullanıcıyı doğrudan
// rapor sayfasına döndür ki blur çözülme animasyonuyla tam listeyi görsün.
const rapor = basarili ? await kullanicininRaporu(session.user.id) : null;
const listeHref = rapor ? "/listem?acildi=1" : null;
return basarili ? (
<div className="rounded-2xl border border-emerald-200 bg-white p-8 shadow-sm">
<CheckCircle2
className="mx-auto size-12 text-emerald-500"
aria-hidden
/>
<h1 className="mt-4 font-heading text-2xl font-bold">
Ödeme alındı 🎉
</h1>
<p className="mt-2 text-sm text-slate-600">
<span className="font-semibold">
{order!.credits} kredi
</span>{" "}
hesabına tanımlandı
{order!.product === "paket"
? " ve Tercih Dönemi Paketi'n aktif — kişisel listen, risk raporun ve PDF çıktın hazır olduğunda burada."
: "."}
</p>
<PagePixelDivider seed={79} className="mx-auto mt-5" />
<div className="mt-6 flex flex-col gap-2">
{listeHref ? (
<>
<Button
asChild
className="h-11 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
>
<Link href={listeHref}>
Listeni kilidi kaldırdık
<ArrowRight className="size-4" aria-hidden />
</Link>
</Button>
<p className="text-center text-xs text-slate-500">
24 tercihin tamamı, gerekçeler, riskler ve PDF artık ık.
</p>
</>
) : (
<RaporYokCta />
)}
</div>
</div>
) : (
<div className="rounded-2xl border border-red-200 bg-white p-8 shadow-sm">
<XCircle className="mx-auto size-12 text-red-500" aria-hidden />
<h1 className="mt-4 font-heading text-2xl font-bold">
Ödeme tamamlanamadı
</h1>
<p className="mt-2 text-sm text-slate-600">
{durum === "pending" ? (
"Ödemen hâlâ işleniyor. Birkaç saniye sonra bu sayfayı yenile — kartından çekim yapıldıysa krediler mutlaka tanımlanır."
) : (
<>
Çekim yapılmadı ya da işlem iptal edildi. Kartından para
çekildiğini düşünüyorsan{" "}
<Link href="/iletisim" className="underline">
bizimle iletişime geç
</Link>
.
</>
)}
</p>
<PagePixelDivider seed={83} className="mx-auto mt-5" />
<Button asChild variant="outline" className="mt-6 h-11 cursor-pointer">
<Link href="/paket">Tekrar dene</Link>
</Button>
</div>
);
}
// iyzico doğrulaması beklenirken ortalanmış sonuç kartı iskeleti.
export function OdemeSonucKartiSkeleton() {
return (
<div
aria-live="polite"
className="rounded-2xl border border-slate-200 bg-white p-8 shadow-sm"
>
<Skeleton className="mx-auto size-12 rounded-full" />
<Skeleton className="mx-auto mt-5 h-7 w-2/3" />
<Skeleton className="mx-auto mt-3 h-5 w-full" />
<Skeleton className="mx-auto mt-2 h-5 w-3/4" />
<Skeleton className="mx-auto mt-6 h-12 w-full rounded-lg" />
</div>
);
}

View File

@@ -0,0 +1,181 @@
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 } from "@/lib/credits";
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 olasılığı ve risk analizi (son 4 yıl trend)",
},
{ 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ı: 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();
return (
<Parallax
strength={6}
className="mt-10 grid gap-6 sm:grid-cols-[1fr_auto]"
>
{/* Ana paket */}
<section className="rounded-2xl border-2 border-orange-500 bg-white 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">
{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 {" "}
<Link href="/" 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&apos;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>
)}
</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="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-white 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">
{/* Top-up hasPaket vermez: paketsiz biri alırsa parasını verip
yine maskeli liste görür. Satın alma yalnızca paketliye
ı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>
)}
</div>
</section>
</Parallax>
);
}
export function PaketSatinAlSkeleton() {
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>
);
}

View File

@@ -5,7 +5,7 @@ import { ArrowRight } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { olay } from "@/lib/analitik"; import { olay } from "@/lib/analitik";
import { baslatOdeme, type OdemeBaslatDurum } from "./actions"; import { baslatOdeme, type OdemeBaslatDurum } from "../odeme-actions";
export function SatinAlForm({ export function SatinAlForm({
urun, urun,

View File

@@ -0,0 +1,11 @@
import "server-only";
import { eq } from "drizzle-orm";
import { appDb, schema } from "@/lib/appdb";
// Ödeme feature'ının sorgu kapısı. Sahiplik kontrolü çağıran tarafta —
// sipariş yalnızca kendi kullanıcısına gösterilir.
export function siparisGetir(siparisId: string) {
return appDb.query.orders.findFirst({
where: eq(schema.orders.id, siparisId),
});
}