Compare commits

..

4 Commits

Author SHA1 Message Date
bilalgursen
d03ba91bf0 chore: dev veritabanı anlık görüntüsünü güncelle
All checks were successful
Deploy / deploy (push) Successful in 14m9s
2026-08-11 07:36:07 +03:00
bilalgursen
f3c4ce4e76 fix(odeme): satın al butonunda pending metin takasını kaldır
Tıklamada buton içeriği "Ödeme sayfasına yönlendiriliyorsun…" ile
değişiyordu; dar top-up kartında metin taşıyordu. Çift gönderim zaten
`disabled` ile engelleniyor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 07:18:17 +03:00
bilalgursen
88eefb3062 fix(odeme): iyziInit'i silmek yerine güvenli sıfırla
iyzico bundle'ı `iyziInit`i global `var` ile tanımlıyor; property
non-configurable olduğu için `delete window.iyziInit` strict mode'da
TypeError fırlatıyordu. Önce delete denenir, olmazsa undefined atanır —
script'in aradığı `typeof ... == 'undefined'` koşulu iki yolla da sağlanır.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 07:18:08 +03:00
bilalgursen
a71740767d feat(odeme): checkout ekranında site kromunu daralt
Ödeme adımında ekranda ~20 kaçış yolu vardı (SEO footer kolonları, katalog
arama, user nav) ve geri sayım banner'ı kart formunun üstünde duruyordu.
Ayrıca layout'un alt progressive blur katmanı iyzico iframe'inin son
satırlarını bulanıklaştırıyordu.

- kromGizliMi: /odeme kroma kapatıldı, /odeme/sonuc istisna tutuldu
  (orada kullanıcıyı ürüne döndürmek istiyoruz)
- OdemeUstSerit: SiteHeader'ın tasarım dilinde daraltılmış şerit — logo
  bilerek link değil, tek çıkış "Paket sayfasına dön"
- Sipariş özeti kartı kaldırıldı: ürün ve tutar /paket'te seçiliyor,
  iyzico da ödeme butonunda tutarı gösteriyor
- Yasal linkler + iyzico logo bandı ayrı footer yerine doğrudan formun
  altında; bant sayfada artık tek kez görünüyor

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 07:03:32 +03:00
7 changed files with 136 additions and 67 deletions

Binary file not shown.

View File

@@ -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>
);
}

View File

@@ -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;

View File

@@ -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]);

View 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>
);
}

View File

@@ -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&apos;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 &amp; KVKK
</Link>
<Link
href="/kosullar#iade"
className="hover:text-slate-700 hover:underline"
>
Kullanım &amp; 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&apos;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>
);
}

View File

@@ -41,14 +41,11 @@ export function SatinAlForm({
}
variant={vurgulu ? "default" : "outline"}
>
{pending ? (
"Ödeme sayfasına yönlendiriliyorsun…"
) : (
<>
{label}
<ArrowRight className="size-4" aria-hidden />
</>
)}
{/* 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>
);