Compare commits
4 Commits
5ccc9c1ffa
...
d03ba91bf0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d03ba91bf0 | ||
|
|
f3c4ce4e76 | ||
|
|
88eefb3062 | ||
|
|
a71740767d |
BIN
data/app.db
BIN
data/app.db
Binary file not shown.
@@ -1,6 +1,6 @@
|
||||
import { Suspense } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import { OdemeUstSerit } from "@/features/odeme/components/odeme-kromu";
|
||||
import {
|
||||
OdemeSayfasi,
|
||||
OdemeSayfasiSkeleton,
|
||||
@@ -17,7 +17,10 @@ export default function OdemePage({
|
||||
searchParams: Promise<{ siparis?: string }>;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col bg-slate-50 text-slate-900">
|
||||
// -mb-28: layout'taki ScrollFlow sarmalayıcısının pb-28'ini nötrler — o
|
||||
// padding gizlenen alt blur katmanı içindi, checkout'ta krom kapalı.
|
||||
<div className="-mb-28 flex min-h-dvh flex-col bg-slate-50 text-slate-900">
|
||||
<OdemeUstSerit />
|
||||
<main className="mx-auto flex w-full max-w-xl flex-1 flex-col px-4 py-10">
|
||||
<Suspense fallback={<OdemeSayfasiSkeleton />}>
|
||||
{searchParams.then(({ siparis }) => (
|
||||
@@ -25,7 +28,6 @@ export default function OdemePage({
|
||||
))}
|
||||
</Suspense>
|
||||
</main>
|
||||
<SiteFooter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,14 +4,25 @@ import { usePathname } from "next/navigation";
|
||||
|
||||
// Site kromunun (banner, header, üst/alt blur) gizleneceği rotalar
|
||||
// (tam eşleşme veya alt yol). KaydetBanner da aynı listeye uyar.
|
||||
const HIDDEN_PREFIXES = ["/giris"];
|
||||
// /odeme: checkout ekranında nav/arama/geri sayım birer kaçış yolu; üstelik
|
||||
// alt blur katmanı iyzico iframe'inin son satırlarını bulanıklaştırıyor.
|
||||
const HIDDEN_PREFIXES = ["/giris", "/odeme"];
|
||||
|
||||
export function kromGizliMi(pathname: string) {
|
||||
return HIDDEN_PREFIXES.some(
|
||||
// Kromun geri geldiği istisnalar: ödeme sonucu sayfasında kullanıcıyı ürüne
|
||||
// döndürmek istiyoruz, o yüzden orada tam krom kalır.
|
||||
const ISTISNA_PREFIXES = ["/odeme/sonuc"];
|
||||
|
||||
function yolEslesir(pathname: string, prefixler: string[]) {
|
||||
return prefixler.some(
|
||||
(p) => pathname === p || pathname.startsWith(`${p}/`),
|
||||
);
|
||||
}
|
||||
|
||||
export function kromGizliMi(pathname: string) {
|
||||
if (yolEslesir(pathname, ISTISNA_PREFIXES)) return false;
|
||||
return yolEslesir(pathname, HIDDEN_PREFIXES);
|
||||
}
|
||||
|
||||
export function HeaderGate({ children }: { children: React.ReactNode }) {
|
||||
const pathname = usePathname();
|
||||
if (kromGizliMi(pathname)) return null;
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useEffect, useMemo } from "react";
|
||||
//
|
||||
// iyzico script'i `typeof iyziInit == 'undefined'` koruması taşır: aynı oturumda
|
||||
// ikinci kez mount olursa (geri gel / tekrar dene) global temizlenmezse form
|
||||
// çizilmez. Bu yüzden enjeksiyondan önce ve unmount'ta global silinir.
|
||||
// çizilmez. Bu yüzden enjeksiyondan önce ve unmount'ta global sıfırlanır.
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
@@ -17,6 +17,23 @@ declare global {
|
||||
}
|
||||
}
|
||||
|
||||
// iyzico bundle'ı `iyziInit`i global `var` ile tanımlıyor; bu da property'yi
|
||||
// non-configurable yapıyor, yani `delete window.iyziInit` strict mode'da
|
||||
// TypeError fırlatıyor ("Cannot delete property 'iyziInit'"). Önce silmeyi
|
||||
// dene, olmazsa undefined ata — script'in aradığı `typeof ... == 'undefined'`
|
||||
// koşulu iki yolla da sağlanır.
|
||||
function iyziInitSifirla() {
|
||||
try {
|
||||
delete window.iyziInit;
|
||||
} catch {
|
||||
try {
|
||||
window.iyziInit = undefined;
|
||||
} catch {
|
||||
// global kilitliyse yapacak bir şey yok; form yine de çizilmeye çalışılır
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function IyzicoFormu({ icerik }: { icerik: string }) {
|
||||
const govde = useMemo(
|
||||
() => icerik.match(/<script[^>]*>([\s\S]*?)<\/script>/i)?.[1],
|
||||
@@ -25,13 +42,13 @@ export function IyzicoFormu({ icerik }: { icerik: string }) {
|
||||
|
||||
useEffect(() => {
|
||||
if (!govde) return;
|
||||
delete window.iyziInit;
|
||||
iyziInitSifirla();
|
||||
const script = document.createElement("script");
|
||||
script.text = govde;
|
||||
document.body.appendChild(script);
|
||||
return () => {
|
||||
script.remove();
|
||||
delete window.iyziInit;
|
||||
iyziInitSifirla();
|
||||
};
|
||||
}, [govde]);
|
||||
|
||||
|
||||
54
src/features/odeme/components/odeme-kromu.tsx
Normal file
54
src/features/odeme/components/odeme-kromu.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
|
||||
/**
|
||||
* Checkout üst şeridi: /odeme'de site header'ı ve SEO footer'ı kapalı
|
||||
* (bkz. `kromGizliMi`), yerine bu daraltılmış şerit gelir. Ayrı bir footer
|
||||
* yok — yasal linkler ve iyzico bandı doğrudan formun altında duruyor
|
||||
* (bkz. `OdemeSayfasi`).
|
||||
*
|
||||
* Tasarım dili birebir `SiteHeader`dan alınır — aynı kap (`max-w-6xl`), aynı
|
||||
* logo ölçüleri, aynı tipografi, çerçevesiz/zeminsiz yerleşim. Fark yalnızca
|
||||
* içerikte: ödeme adımında kaçış yolu bırakmıyoruz, bu yüzden arama + user
|
||||
* nav yerine tek çıkış olarak "Paket sayfasına dön" duruyor ve logo bilerek
|
||||
* link değil.
|
||||
*/
|
||||
export function OdemeUstSerit() {
|
||||
return (
|
||||
// SiteHeader sticky; burada arkasında progressive blur katmanı olmadığı
|
||||
// için (krom kapalı) statik bırakıldı — durağan haldeki görünüm aynı.
|
||||
<header className="mt-4 sm:mt-6 print:hidden">
|
||||
<div className="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-x-2 gap-y-2 px-3 sm:gap-x-3 sm:px-4 lg:h-16 lg:flex-nowrap">
|
||||
<div
|
||||
aria-label="KolayTercih"
|
||||
className="flex min-w-0 shrink items-center font-bricolage"
|
||||
>
|
||||
<Image
|
||||
src="/kolay-tercih-mark.svg"
|
||||
alt=""
|
||||
width={44}
|
||||
height={44}
|
||||
aria-hidden="true"
|
||||
className="size-8 shrink-0 sm:size-11"
|
||||
/>
|
||||
<span className="truncate">
|
||||
<span className="text-lg font-light tracking-tighter sm:text-3xl">
|
||||
Kolay
|
||||
</span>
|
||||
<span className="text-lg font-semibold tracking-tighter sm:text-3xl">
|
||||
Tercih
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<Link
|
||||
href="/paket"
|
||||
className="inline-flex shrink-0 items-center gap-1.5 text-sm text-slate-500 transition-colors hover:text-slate-700"
|
||||
>
|
||||
<ArrowLeft className="size-4" aria-hidden />
|
||||
Paket sayfasına dön
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +1,15 @@
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { ArrowLeft, Lock } from "lucide-react";
|
||||
import { Lock } from "lucide-react";
|
||||
import { verifySession } from "@/lib/session";
|
||||
import { URUNLER } from "@/lib/credits";
|
||||
import { SectionEyebrow } from "@/components/pixel-decor";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { siparisGetir } from "../odeme-queries";
|
||||
import { IyzicoFormu } from "./iyzico-formu";
|
||||
|
||||
function tl(kurus: number) {
|
||||
return (kurus / 100).toLocaleString("tr-TR", { maximumFractionDigits: 0 });
|
||||
}
|
||||
|
||||
// Kendi tasarımımızdaki ödeme sayfası: sipariş özeti bizden, kart formu
|
||||
// iyzico'nun gömülü iframe'inden gelir (kart verisi bize hiç uğramaz).
|
||||
// Kendi tasarımımızdaki ödeme sayfası: kart formu iyzico'nun gömülü
|
||||
// iframe'inden gelir (kart verisi bize hiç uğramaz).
|
||||
export async function OdemeSayfasi({ siparisId }: { siparisId?: string }) {
|
||||
if (!siparisId) redirect("/paket");
|
||||
// Oturum düşmüşse geri dönüşte ?siparis= kaybolmasın
|
||||
@@ -34,49 +29,24 @@ export async function OdemeSayfasi({ siparisId }: { siparisId?: string }) {
|
||||
redirect("/paket");
|
||||
}
|
||||
|
||||
const urun = URUNLER[order.product];
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<Link
|
||||
href="/paket"
|
||||
className="inline-flex items-center gap-1.5 text-sm text-slate-500 transition-colors hover:text-slate-700"
|
||||
>
|
||||
<ArrowLeft className="size-4" aria-hidden />
|
||||
Paket sayfasına dön
|
||||
</Link>
|
||||
|
||||
<div className="mt-4 text-center">
|
||||
{/* "Paket sayfasına dön" linki checkout üst şeridinde (OdemeUstSerit) */}
|
||||
<div className="text-center">
|
||||
<SectionEyebrow>Güvenli ödeme</SectionEyebrow>
|
||||
<h1 className="mt-3 font-heading text-2xl font-bold">
|
||||
Ödemeyi tamamla
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Sipariş özeti */}
|
||||
<section
|
||||
aria-label="Sipariş özeti"
|
||||
className="mt-6 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm"
|
||||
>
|
||||
<div className="flex items-baseline justify-between gap-4">
|
||||
<div>
|
||||
<p className="font-heading font-bold">{urun.label}</p>
|
||||
<p className="mt-1 text-sm text-slate-600">
|
||||
{order.credits} Yapay Zeka danışman kredisi
|
||||
{order.product === "paket" &&
|
||||
" · 24 tercihlik liste, risk analizi ve PDF rapor"}
|
||||
</p>
|
||||
</div>
|
||||
<p className="shrink-0 font-heading text-2xl font-bold">
|
||||
{tl(order.amountKurus)} TL
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
{/* Sipariş özeti bilerek yok: paket/ürün ve tutar bir önceki adımda
|
||||
(/paket) seçildi, iyzico formu da ödeme butonunda tutarı gösteriyor —
|
||||
burada üçüncü kez tekrar etmiyoruz. */}
|
||||
|
||||
{/* iyzico gömülü kart formu */}
|
||||
<section
|
||||
aria-label="Kart bilgileri"
|
||||
className="mt-4 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm"
|
||||
className="mt-6 rounded-2xl border border-slate-200 bg-white p-5 shadow-sm"
|
||||
>
|
||||
<IyzicoFormu icerik={order.iyzicoFormContent} />
|
||||
</section>
|
||||
@@ -85,19 +55,39 @@ export async function OdemeSayfasi({ siparisId }: { siparisId?: string }) {
|
||||
<Lock className="size-3.5" aria-hidden />
|
||||
256-bit SSL ile şifreli bağlantı
|
||||
</div>
|
||||
<p className="mt-3 text-center text-xs text-slate-500">
|
||||
Kart bilgilerin iyzico'da işlenir, bize hiç ulaşmaz
|
||||
</p>
|
||||
|
||||
{/* Ayrı bir footer yerine yasal linkler ve iyzico bandı doğrudan formun
|
||||
altında: mesafeli satış mevzuatı ve iyzico işyeri sözleşmesi bunların
|
||||
ödeme adımında görünür olmasını istiyor. */}
|
||||
<nav
|
||||
aria-label="Yasal sayfalar"
|
||||
className="mt-3 flex flex-wrap items-center justify-center gap-x-4 gap-y-2 text-xs text-slate-500"
|
||||
>
|
||||
<Link href="/gizlilik" className="hover:text-slate-700 hover:underline">
|
||||
Gizlilik & KVKK
|
||||
</Link>
|
||||
<Link
|
||||
href="/kosullar#iade"
|
||||
className="hover:text-slate-700 hover:underline"
|
||||
>
|
||||
Kullanım & iade koşulları
|
||||
</Link>
|
||||
<Link href="/iletisim" className="hover:text-slate-700 hover:underline">
|
||||
İletişim
|
||||
</Link>
|
||||
</nav>
|
||||
{/* iyzico işyeri sözleşmesi gereği: "iyzico ile Öde" + kart logoları
|
||||
görünür olmalı. Resmî logo bandı (iyzico logo paketi). */}
|
||||
<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-3 h-5 w-auto"
|
||||
className="mx-auto mt-5 h-6 w-auto"
|
||||
/>
|
||||
<p className="mt-3 text-center text-xs text-slate-500">
|
||||
Kart bilgilerin iyzico'da işlenir, bize hiç ulaşmaz ·{" "}
|
||||
<Link href="/kosullar#iade" className="underline hover:text-slate-700">
|
||||
İade koşulları
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -105,11 +95,9 @@ export async function OdemeSayfasi({ siparisId }: { siparisId?: string }) {
|
||||
export function OdemeSayfasiSkeleton() {
|
||||
return (
|
||||
<div aria-live="polite" className="w-full">
|
||||
<Skeleton className="h-5 w-40" />
|
||||
<Skeleton className="mx-auto mt-4 h-7 w-32 rounded-full" />
|
||||
<Skeleton className="mx-auto h-7 w-32 rounded-full" />
|
||||
<Skeleton className="mx-auto mt-3 h-8 w-56" />
|
||||
<Skeleton className="mt-6 h-24 w-full rounded-2xl" />
|
||||
<Skeleton className="mt-4 h-96 w-full rounded-2xl" />
|
||||
<Skeleton className="mt-6 h-96 w-full rounded-2xl" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,14 +41,11 @@ export function SatinAlForm({
|
||||
}
|
||||
variant={vurgulu ? "default" : "outline"}
|
||||
>
|
||||
{pending ? (
|
||||
"Ödeme sayfasına yönlendiriliyorsun…"
|
||||
) : (
|
||||
<>
|
||||
{/* Tıklamada buton içeriği değişmez (metin/spinner takası yok); dar
|
||||
top-up kartında taşmaya yol açıyordu. Çift gönderim `disabled`
|
||||
ile engellenir. */}
|
||||
{label}
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user