diff --git a/src/app/odeme/sonuc/loading.tsx b/src/app/odeme/sonuc/loading.tsx deleted file mode 100644 index 66aaee5..0000000 --- a/src/app/odeme/sonuc/loading.tsx +++ /dev/null @@ -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 ( -
-
- - - - - -
-
- ); -} diff --git a/src/app/odeme/sonuc/page.tsx b/src/app/odeme/sonuc/page.tsx index e4ff68f..74a9862 100644 --- a/src/app/odeme/sonuc/page.tsx +++ b/src/app/odeme/sonuc/page.tsx @@ -1,126 +1,29 @@ -import Link from "next/link"; +import { Suspense } from "react"; 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 { RaporYokCta } from "./rapor-yok-cta"; +import { + OdemeSonucKarti, + OdemeSonucKartiSkeleton, +} from "@/features/odeme/components/odeme-sonuc-karti"; export const metadata: Metadata = { title: "Ödeme Sonucu", robots: { index: false, follow: false }, }; -export default async function OdemeSonucPage({ +export default function OdemeSonucPage({ searchParams, }: { 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 (
- {basarili ? ( -
- -

- Ödeme alındı 🎉 -

-

- - {order!.credits} kredi - {" "} - 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." - : "."} -

- -
- {listeHref ? ( - <> - -

- 24 tercihin tamamı, gerekçeler, riskler ve PDF artık açık. -

- - ) : ( - - )} -
-
- ) : ( -
- -

- Ödeme tamamlanamadı -

-

- {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{" "} - - bizimle iletişime geç - - . - - )} -

- - -
- )} + }> + {searchParams.then(({ siparis }) => ( + + ))} +
diff --git a/src/app/paket/loading.tsx b/src/app/paket/loading.tsx deleted file mode 100644 index 4c967c4..0000000 --- a/src/app/paket/loading.tsx +++ /dev/null @@ -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 ( -
-
- - - - -
-
- - -
-
- ); -} diff --git a/src/app/paket/page.tsx b/src/app/paket/page.tsx index 70d9ee6..b04cfc1 100644 --- a/src/app/paket/page.tsx +++ b/src/app/paket/page.tsx @@ -1,23 +1,13 @@ -import Image from "next/image"; +import { Suspense } from "react"; import Link from "next/link"; 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 { Parallax } from "@/components/parallax"; -import { SatinAlForm } from "./satin-al-form"; import { SiteFooter } from "@/components/site-footer"; +import { + PaketSatinAl, + PaketSatinAlSkeleton, +} from "@/features/odeme/components/paket-satinal"; export const metadata: Metadata = { title: "Tercih Dönemi Paketi", @@ -26,193 +16,57 @@ export const metadata: Metadata = { alternates: { canonical: "/paket" }, }; -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; - -export default async function PaketPage({ +export default function PaketPage({ searchParams, }: { 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 (
-
- {odemeHatasi ? ( -
- Ödemen doğrulanamadı. Kartından çekim yapıldıysa kredilerin birkaç - dakika içinde otomatik tanımlanır; tanımlanmazsa aşağıdan tekrar - deneyebilirsin — çekim yapılmadıysa hiçbir ücret ödemedin. -
- ) : null} - - Tek seferlik ödeme · Abonelik yok -

- İnsan danışmanın işini,{" "} - onda bir fiyatına -

-

- Tercih danışmanları bu dönemde 2.000–10.000 TL alıyor. KolayTercih - aynı işi gerçek YÖK Atlas verisiyle, dakikalar içinde yapar. -

-
- - - {/* Ana paket */} -
-
-

- Tercih Dönemi Paketi -

-

- {tl(URUNLER.paket.amountKurus)} TL -

+
+ {/* Ödeme callback'i doğrulama hatasında buraya ?hata=token|siparis ile + döner; hata yokken hiçbir şey çizilmez — fallback bilerek boş. */} + + {searchParams.then(({ hata }) => + hata === "token" || hata === "siparis" ? ( +
+ Ödemen doğrulanamadı. Kartından çekim yapıldıysa kredilerin + birkaç dakika içinde otomatik tanımlanır; tanımlanmazsa + aşağıdan tekrar deneyebilirsin — çekim yapılmadıysa hiçbir + ücret ödemedin.
-
    - {OZELLIKLER.map((o) => ( -
  • - - {o.text} -
  • - ))} -
-
- {u ? ( - u.hasPaket ? ( -
- - Paketin aktif —{" "} - - listeni oluştur - -
- ) : ( - - ) - ) : ( -
- -

- Girişte {DENEME_KREDISI} deneme kredisi de tanımlanır. -

-
- )} -
- iyzico ile Öde — Mastercard, Visa, American Express ve Troy ile güvenli ödeme -

- iyzico güvenli ödeme · Kart bilgilerin bize hiç ulaşmaz ·{" "} - - İade koşulları - -

-
- - {/* Top-up */} -
-
- -

Kredi bitti mi?

-
-

- +{URUNLER.topup.credits} soru hakkı,{" "} - {tl(URUNLER.topup.amountKurus)} TL. -

-
- {/* 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. -

- ) : ( - - )} -
-
-
- -

- KolayTercih bir karar destek aracıdır; öneriler resmî YÖK Atlas - verisine dayanır ancak yerleşme garantisi verilmez. Tercih - listesinin nihai sorumluluğu adaya aittir. Sorun mu var?{" "} - - Bize yaz - - . + ) : null, + )} + + + Tek seferlik ödeme · Abonelik yok +

+ İnsan danışmanın işini,{" "} + onda bir fiyatına +

+

+ Tercih danışmanları bu dönemde 2.000–10.000 TL alıyor. KolayTercih + aynı işi gerçek YÖK Atlas verisiyle, dakikalar içinde yapar.

-
+ + + }> + + + +

+ KolayTercih bir karar destek aracıdır; öneriler resmî YÖK Atlas + verisine dayanır ancak yerleşme garantisi verilmez. Tercih + listesinin nihai sorumluluğu adaya aittir. Sorun mu var?{" "} + + Bize yaz + + . +

+
); diff --git a/src/features/odeme/components/odeme-sonuc-karti.tsx b/src/features/odeme/components/odeme-sonuc-karti.tsx new file mode 100644 index 0000000..1abdd78 --- /dev/null +++ b/src/features/odeme/components/odeme-sonuc-karti.tsx @@ -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 ? ( +
+ +

+ Ödeme alındı 🎉 +

+

+ + {order!.credits} kredi + {" "} + 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." + : "."} +

+ +
+ {listeHref ? ( + <> + +

+ 24 tercihin tamamı, gerekçeler, riskler ve PDF artık açık. +

+ + ) : ( + + )} +
+
+ ) : ( +
+ +

+ Ödeme tamamlanamadı +

+

+ {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{" "} + + bizimle iletişime geç + + . + + )} +

+ + +
+ ); +} + +// iyzico doğrulaması beklenirken ortalanmış sonuç kartı iskeleti. +export function OdemeSonucKartiSkeleton() { + return ( +
+ + + + + +
+ ); +} diff --git a/src/features/odeme/components/paket-satinal.tsx b/src/features/odeme/components/paket-satinal.tsx new file mode 100644 index 0000000..fa05aff --- /dev/null +++ b/src/features/odeme/components/paket-satinal.tsx @@ -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 ( + + {/* Ana paket */} +
+
+

+ Tercih Dönemi Paketi +

+

+ {tl(URUNLER.paket.amountKurus)} TL +

+
+ +
+ {u ? ( + u.hasPaket ? ( +
+ + Paketin aktif —{" "} + + listeni oluştur + +
+ ) : ( + + ) + ) : ( +
+ +

+ Girişte {DENEME_KREDISI} deneme kredisi de tanımlanır. +

+
+ )} +
+ iyzico ile Öde — Mastercard, Visa, American Express ve Troy ile güvenli ödeme +

+ iyzico güvenli ödeme · Kart bilgilerin bize hiç ulaşmaz ·{" "} + + İade koşulları + +

+
+ + {/* Top-up */} +
+
+ +

Kredi bitti mi?

+
+

+ +{URUNLER.topup.credits} soru hakkı,{" "} + {tl(URUNLER.topup.amountKurus)} TL. +

+
+ {/* 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. +

+ ) : ( + + )} +
+
+
+ ); +} + +export function PaketSatinAlSkeleton() { + return ( +
+ + +
+ ); +} diff --git a/src/app/odeme/sonuc/rapor-yok-cta.tsx b/src/features/odeme/components/rapor-yok-cta.tsx similarity index 100% rename from src/app/odeme/sonuc/rapor-yok-cta.tsx rename to src/features/odeme/components/rapor-yok-cta.tsx diff --git a/src/app/paket/satin-al-form.tsx b/src/features/odeme/components/satin-al-form.tsx similarity index 94% rename from src/app/paket/satin-al-form.tsx rename to src/features/odeme/components/satin-al-form.tsx index d6fe33c..290cf04 100644 --- a/src/app/paket/satin-al-form.tsx +++ b/src/features/odeme/components/satin-al-form.tsx @@ -5,7 +5,7 @@ import { ArrowRight } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { olay } from "@/lib/analitik"; -import { baslatOdeme, type OdemeBaslatDurum } from "./actions"; +import { baslatOdeme, type OdemeBaslatDurum } from "../odeme-actions"; export function SatinAlForm({ urun, diff --git a/src/app/paket/actions.ts b/src/features/odeme/odeme-actions.ts similarity index 100% rename from src/app/paket/actions.ts rename to src/features/odeme/odeme-actions.ts diff --git a/src/features/odeme/odeme-queries.ts b/src/features/odeme/odeme-queries.ts new file mode 100644 index 0000000..33052b2 --- /dev/null +++ b/src/features/odeme/odeme-queries.ts @@ -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), + }); +}