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 { Suspense } from "react";
import type { Metadata } from "next"; import type { Metadata } from "next";
import { SiteFooter } from "@/components/site-footer"; import { OdemeUstSerit } from "@/features/odeme/components/odeme-kromu";
import { import {
OdemeSayfasi, OdemeSayfasi,
OdemeSayfasiSkeleton, OdemeSayfasiSkeleton,
@@ -17,7 +17,10 @@ export default function OdemePage({
searchParams: Promise<{ siparis?: string }>; searchParams: Promise<{ siparis?: string }>;
}) { }) {
return ( 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"> <main className="mx-auto flex w-full max-w-xl flex-1 flex-col px-4 py-10">
<Suspense fallback={<OdemeSayfasiSkeleton />}> <Suspense fallback={<OdemeSayfasiSkeleton />}>
{searchParams.then(({ siparis }) => ( {searchParams.then(({ siparis }) => (
@@ -25,7 +28,6 @@ export default function OdemePage({
))} ))}
</Suspense> </Suspense>
</main> </main>
<SiteFooter />
</div> </div>
); );
} }

View File

@@ -4,14 +4,25 @@ import { usePathname } from "next/navigation";
// Site kromunun (banner, header, üst/alt blur) gizleneceği rotalar // Site kromunun (banner, header, üst/alt blur) gizleneceği rotalar
// (tam eşleşme veya alt yol). KaydetBanner da aynı listeye uyar. // (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) { // Kromun geri geldiği istisnalar: ödeme sonucu sayfasında kullanıcıyı ürüne
return HIDDEN_PREFIXES.some( // 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}/`), (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 }) { export function HeaderGate({ children }: { children: React.ReactNode }) {
const pathname = usePathname(); const pathname = usePathname();
if (kromGizliMi(pathname)) return null; 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 // iyzico script'i `typeof iyziInit == 'undefined'` koruması taşır: aynı oturumda
// ikinci kez mount olursa (geri gel / tekrar dene) global temizlenmezse form // 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 { declare global {
interface Window { 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 }) { export function IyzicoFormu({ icerik }: { icerik: string }) {
const govde = useMemo( const govde = useMemo(
() => icerik.match(/<script[^>]*>([\s\S]*?)<\/script>/i)?.[1], () => icerik.match(/<script[^>]*>([\s\S]*?)<\/script>/i)?.[1],
@@ -25,13 +42,13 @@ export function IyzicoFormu({ icerik }: { icerik: string }) {
useEffect(() => { useEffect(() => {
if (!govde) return; if (!govde) return;
delete window.iyziInit; iyziInitSifirla();
const script = document.createElement("script"); const script = document.createElement("script");
script.text = govde; script.text = govde;
document.body.appendChild(script); document.body.appendChild(script);
return () => { return () => {
script.remove(); script.remove();
delete window.iyziInit; iyziInitSifirla();
}; };
}, [govde]); }, [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 Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { notFound, redirect } from "next/navigation"; import { notFound, redirect } from "next/navigation";
import { ArrowLeft, Lock } from "lucide-react"; import { Lock } from "lucide-react";
import { verifySession } from "@/lib/session"; import { verifySession } from "@/lib/session";
import { URUNLER } from "@/lib/credits";
import { SectionEyebrow } from "@/components/pixel-decor"; import { SectionEyebrow } from "@/components/pixel-decor";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
import { siparisGetir } from "../odeme-queries"; import { siparisGetir } from "../odeme-queries";
import { IyzicoFormu } from "./iyzico-formu"; import { IyzicoFormu } from "./iyzico-formu";
function tl(kurus: number) { // Kendi tasarımımızdaki ödeme sayfası: kart formu iyzico'nun gömülü
return (kurus / 100).toLocaleString("tr-TR", { maximumFractionDigits: 0 }); // iframe'inden gelir (kart verisi bize hiç uğramaz).
}
// 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).
export async function OdemeSayfasi({ siparisId }: { siparisId?: string }) { export async function OdemeSayfasi({ siparisId }: { siparisId?: string }) {
if (!siparisId) redirect("/paket"); if (!siparisId) redirect("/paket");
// Oturum düşmüşse geri dönüşte ?siparis= kaybolmasın // Oturum düşmüşse geri dönüşte ?siparis= kaybolmasın
@@ -34,49 +29,24 @@ export async function OdemeSayfasi({ siparisId }: { siparisId?: string }) {
redirect("/paket"); redirect("/paket");
} }
const urun = URUNLER[order.product];
return ( return (
<div className="w-full"> <div className="w-full">
<Link {/* "Paket sayfasına dön" linki checkout üst şeridinde (OdemeUstSerit) */}
href="/paket" <div className="text-center">
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">
<SectionEyebrow>Güvenli ödeme</SectionEyebrow> <SectionEyebrow>Güvenli ödeme</SectionEyebrow>
<h1 className="mt-3 font-heading text-2xl font-bold"> <h1 className="mt-3 font-heading text-2xl font-bold">
Ödemeyi tamamla Ödemeyi tamamla
</h1> </h1>
</div> </div>
{/* Sipariş özeti */} {/* Sipariş özeti bilerek yok: paket/ürün ve tutar bir önceki adımda
<section (/paket) seçildi, iyzico formu da ödeme butonunda tutarı gösteriyor —
aria-label="Sipariş özeti" burada üçüncü kez tekrar etmiyoruz. */}
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>
{/* iyzico gömülü kart formu */} {/* iyzico gömülü kart formu */}
<section <section
aria-label="Kart bilgileri" 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} /> <IyzicoFormu icerik={order.iyzicoFormContent} />
</section> </section>
@@ -85,19 +55,39 @@ export async function OdemeSayfasi({ siparisId }: { siparisId?: string }) {
<Lock className="size-3.5" aria-hidden /> <Lock className="size-3.5" aria-hidden />
256-bit SSL ile şifreli bağlantı 256-bit SSL ile şifreli bağlantı
</div> </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 <Image
src="/odeme/iyzico-logo-band.svg" src="/odeme/iyzico-logo-band.svg"
alt="iyzico ile Öde — Mastercard, Visa, American Express ve Troy ile güvenli ödeme" alt="iyzico ile Öde — Mastercard, Visa, American Express ve Troy ile güvenli ödeme"
width={429} width={429}
height={32} 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> </div>
); );
} }
@@ -105,11 +95,9 @@ export async function OdemeSayfasi({ siparisId }: { siparisId?: string }) {
export function OdemeSayfasiSkeleton() { export function OdemeSayfasiSkeleton() {
return ( return (
<div aria-live="polite" className="w-full"> <div aria-live="polite" className="w-full">
<Skeleton className="h-5 w-40" /> <Skeleton className="mx-auto h-7 w-32 rounded-full" />
<Skeleton className="mx-auto mt-4 h-7 w-32 rounded-full" />
<Skeleton className="mx-auto mt-3 h-8 w-56" /> <Skeleton className="mx-auto mt-3 h-8 w-56" />
<Skeleton className="mt-6 h-24 w-full rounded-2xl" /> <Skeleton className="mt-6 h-96 w-full rounded-2xl" />
<Skeleton className="mt-4 h-96 w-full rounded-2xl" />
</div> </div>
); );
} }

View File

@@ -41,14 +41,11 @@ export function SatinAlForm({
} }
variant={vurgulu ? "default" : "outline"} variant={vurgulu ? "default" : "outline"}
> >
{pending ? ( {/* Tıklamada buton içeriği değişmez (metin/spinner takası yok); dar
"Ödeme sayfasına yönlendiriliyorsun…" top-up kartında taşmaya yol açıyordu. Çift gönderim `disabled`
) : ( ile engellenir. */}
<>
{label} {label}
<ArrowRight className="size-4" aria-hidden /> <ArrowRight className="size-4" aria-hidden />
</>
)}
</Button> </Button>
</form> </form>
); );