Sonner/vaul yerine kendi bildirim yığını ve alt sayfa; çip grubu, kart destesi, kaydırma eylemi, yazı dönüşümü, bölme tutamacı; landing'e program duvarı, süzülen haplar ve veri akışı; yaylar tek dosyada (lib/hareket.ts). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
87 lines
2.7 KiB
TypeScript
87 lines
2.7 KiB
TypeScript
"use client";
|
||
|
||
import { useLayoutEffect, useState, useTransition } from "react";
|
||
import { useRouter } from "next/navigation";
|
||
import { ArrowRight, Loader2 } from "lucide-react";
|
||
import { toast } from "@/components/ui/bildirim";
|
||
import { Button } from "@/components/ui/button";
|
||
import { olay } from "@/lib/analitik";
|
||
import { baslatOdeme } from "../odeme-actions";
|
||
|
||
const BAGLANTI_HATASI =
|
||
"Ödeme başlatılamadı; kartından çekim yapılmadı. Bağlantını kontrol edip tekrar dener misin?";
|
||
|
||
export function SatinAlForm({
|
||
urun,
|
||
label,
|
||
vurgulu = false,
|
||
}: {
|
||
urun: "paket" | "topup";
|
||
label: string;
|
||
vurgulu?: boolean;
|
||
}) {
|
||
const router = useRouter();
|
||
const [pending, startTransition] = useTransition();
|
||
// Action bitti, /odeme'ye geçiş sürüyor: buton o arada yeniden açılıp ikinci
|
||
// bir sipariş başlatmasın.
|
||
const [gidiyor, setGidiyor] = useState(false);
|
||
const mesgul = pending || gidiyor;
|
||
|
||
// cacheComponents açıkken /paket, /odeme'ye geçince unmount olmaz; gizli
|
||
// <Activity> içinde state'iyle saklanır. Geri dönen kullanıcı kilitli değil
|
||
// taze bir buton bulsun diye sayfa gizlenirken sıfırlanır (layout effect
|
||
// cleanup'ı Activity gizlenince çalışır).
|
||
useLayoutEffect(() => {
|
||
return () => setGidiyor(false);
|
||
}, []);
|
||
|
||
// Yönlendirme client'ta: action `redirect()` fırlatmaz, hedefi döndürür
|
||
// (nedeni odeme-actions.ts'te). Hata toast'ı da burada, effect'te değil —
|
||
// effect'ler Activity geri açılınca yeniden koştuğu için eski hatayı
|
||
// tekrar gösteriyordu.
|
||
function gonder() {
|
||
olay("odeme_baslatildi", { urun });
|
||
startTransition(async () => {
|
||
let sonuc;
|
||
try {
|
||
sonuc = await baslatOdeme(urun);
|
||
} catch {
|
||
toast.error(BAGLANTI_HATASI);
|
||
return;
|
||
}
|
||
if ("error" in sonuc) {
|
||
toast.error(sonuc.error);
|
||
return;
|
||
}
|
||
setGidiyor(true);
|
||
router.push(sonuc.yonlendir);
|
||
});
|
||
}
|
||
|
||
return (
|
||
<form action={gonder}>
|
||
<Button
|
||
type="submit"
|
||
disabled={mesgul}
|
||
aria-busy={mesgul}
|
||
className={
|
||
vurgulu
|
||
? "h-12 w-full cursor-pointer bg-orange-500 px-8 text-white transition-colors duration-200 hover:bg-orange-600"
|
||
: "h-11 w-full cursor-pointer"
|
||
}
|
||
variant={vurgulu ? "default" : "outline"}
|
||
>
|
||
{/* Tıklamada metin değişmez (dar top-up kartında taşıyordu); geri
|
||
bildirim aynı boyuttaki ikon takasıyla verilir: ok → dönen halka.
|
||
Çift gönderim `disabled` ile engellenir. */}
|
||
{label}
|
||
{mesgul ? (
|
||
<Loader2 className="size-4 animate-spin" aria-hidden />
|
||
) : (
|
||
<ArrowRight className="size-4" aria-hidden />
|
||
)}
|
||
</Button>
|
||
</form>
|
||
);
|
||
}
|