UX/CRO denetimi uygulandı: değer-önce funnel, güven sayfaları, dev paneli
Kritik (K1-K6) sonrası kalan tüm maddeler: - Değer önce: girişsiz kullanıcı sihirbaz bitince /giris yerine /sonuc'taki ücretsiz tabloya gider (?hazir=1); AI CTA'sı "5 deneme kredinle listeni kur — giriş yap" paneli + sabit alt karttan verilir, girişte otomatik üretim - Ö1-Ö10: paywall kopyası, aranabilir il seçimi, "Seçtiklerim" adlandırması, gizlilik/koşullar/iletişim sayfaları, danışman özeti + uyarılar, revizyon kutusu, dilim özeti + tek şehir uyarısı, eksik veri rozetleri, magic link yeniden gönder, mobil sohbet kısayolu - G1-G5: binlik ayraçlı input + inline hata, adım göstergesi, route-level loading/error/not-found, dürüst konumlandırma kartı, dürüst buton etiketleri - Seçtiklerim butonu liste boşken gizli; ilk eklemede belirir - Dev süperadmin paneli (yalnızca development): hızlı giriş, kredi/paket/rapor senaryoları, sahte ödeme dönüşü Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
273
src/components/dev/dev-panel.tsx
Normal file
273
src/components/dev/dev-panel.tsx
Normal file
@@ -0,0 +1,273 @@
|
||||
"use client";
|
||||
|
||||
// Dev süperadmin paneli: yalnızca geliştirme ortamında render edilir
|
||||
// (layout.tsx NODE_ENV kontrolüyle monte eder; aksiyonlar da sunucuda
|
||||
// ayrıca korunur). Amaç: satın alma / giriş / kredi durumlarını tek
|
||||
// tıkla taklit edip uygulamanın tüm sayfalarını gerçek akışta gezmek.
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Loader2, Wrench, X } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { authClient } from "@/lib/auth-client";
|
||||
import {
|
||||
devAnindaGiris,
|
||||
devAyarla,
|
||||
devDurum,
|
||||
devRaporSil,
|
||||
devSahteOdeme,
|
||||
type DevDurum,
|
||||
} from "@/lib/dev/actions";
|
||||
|
||||
const SAYFALAR: { ad: string; href: string }[] = [
|
||||
{ ad: "Ana sayfa", href: "/" },
|
||||
{ ad: "Sonuç (85k · SAY)", href: "/sonuc?sira=85000&tur=say&sihirbaz=1" },
|
||||
{ ad: "Sonuç (girişsiz hazır)", href: "/sonuc?sira=85000&tur=say&hazir=1" },
|
||||
{ ad: "Listem", href: "/listem" },
|
||||
{ ad: "Listem (kilit açılışı)", href: "/listem?acildi=1" },
|
||||
{ ad: "Paket", href: "/paket" },
|
||||
{ ad: "Paket (ödeme hatası)", href: "/paket?hata=token" },
|
||||
{ ad: "PDF / yazdır", href: "/rapor/yazdir" },
|
||||
{ ad: "Giriş", href: "/giris" },
|
||||
{ ad: "Gizlilik", href: "/gizlilik" },
|
||||
{ ad: "Koşullar", href: "/kosullar" },
|
||||
{ ad: "İletişim", href: "/iletisim" },
|
||||
{ ad: "404", href: "/boyle-bir-sayfa-yok" },
|
||||
];
|
||||
|
||||
export function DevPanel() {
|
||||
const [acik, setAcik] = useState(false);
|
||||
const [durum, setDurum] = useState<DevDurum | null>(null);
|
||||
const [email, setEmail] = useState("test@kolaytercih.dev");
|
||||
const [mesgul, setMesgul] = useState(false);
|
||||
const router = useRouter();
|
||||
|
||||
const yenile = useCallback(async () => {
|
||||
try {
|
||||
setDurum(await devDurum());
|
||||
} catch {
|
||||
setDurum(null);
|
||||
}
|
||||
}, []);
|
||||
|
||||
function paneliAc() {
|
||||
setAcik(true);
|
||||
void yenile();
|
||||
}
|
||||
|
||||
// Her aksiyonu tek kalıptan geçir: meşgul kilidi + durum tazeleme +
|
||||
// server component'lerin (header'daki kredi rozeti dahil) yenilenmesi
|
||||
async function calistir(fn: () => Promise<unknown>) {
|
||||
setMesgul(true);
|
||||
try {
|
||||
await fn();
|
||||
await yenile();
|
||||
router.refresh();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Dev aksiyonu başarısız.");
|
||||
} finally {
|
||||
setMesgul(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function anindaGiris() {
|
||||
setMesgul(true);
|
||||
try {
|
||||
const sonuc = await devAnindaGiris(email);
|
||||
if (!sonuc.ok) {
|
||||
toast.error(sonuc.error);
|
||||
return;
|
||||
}
|
||||
// Verify URL'i oturumu kurup callback'e yönlendirir
|
||||
window.location.assign(sonuc.url);
|
||||
} finally {
|
||||
setMesgul(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function odemeEkrani() {
|
||||
setMesgul(true);
|
||||
try {
|
||||
const sonuc = await devSahteOdeme();
|
||||
if (!sonuc.ok) {
|
||||
toast.error(sonuc.error);
|
||||
return;
|
||||
}
|
||||
router.push(`/odeme/sonuc?siparis=${sonuc.siparis}`);
|
||||
} finally {
|
||||
setMesgul(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (!acik) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={paneliAc}
|
||||
title="Dev süperadmin paneli"
|
||||
className="fixed bottom-20 left-4 z-50 inline-flex h-9 cursor-pointer items-center gap-1.5 rounded-full border border-violet-300 bg-violet-600 px-3 text-xs font-bold text-white shadow-lg transition-transform duration-150 hover:bg-violet-700 active:scale-95 print:hidden"
|
||||
>
|
||||
<Wrench className="size-3.5" aria-hidden />
|
||||
DEV
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
const g = durum?.girisli ? durum : null;
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-20 left-4 z-50 flex max-h-[72vh] w-80 flex-col overflow-hidden rounded-2xl border border-violet-200 bg-white shadow-2xl print:hidden">
|
||||
<div className="flex items-center justify-between border-b border-slate-100 bg-violet-600 px-4 py-2.5 text-white">
|
||||
<p className="flex items-center gap-1.5 text-sm font-bold">
|
||||
<Wrench className="size-4" aria-hidden />
|
||||
Dev süperadmin
|
||||
{mesgul ? (
|
||||
<Loader2 className="size-3.5 animate-spin" aria-hidden />
|
||||
) : null}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAcik(false)}
|
||||
className="cursor-pointer rounded-full p-1 hover:bg-white/20"
|
||||
aria-label="Kapat"
|
||||
>
|
||||
<X className="size-4" aria-hidden />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 overflow-y-auto p-4 text-sm">
|
||||
{/* Oturum */}
|
||||
<section>
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-slate-400">
|
||||
Oturum
|
||||
</p>
|
||||
{g ? (
|
||||
<div className="mt-2 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-xs">
|
||||
<p className="font-semibold text-slate-800">{g.email}</p>
|
||||
<p className="mt-0.5 text-slate-500">
|
||||
{g.kredi} kredi · {g.hasPaket ? "paketli" : "paketsiz"} ·{" "}
|
||||
{g.raporVar
|
||||
? `rapor var (${g.sira?.toLocaleString("tr-TR")} · ${g.tur})`
|
||||
: "rapor yok"}
|
||||
</p>
|
||||
<button
|
||||
type="button"
|
||||
disabled={mesgul}
|
||||
onClick={() =>
|
||||
calistir(async () => {
|
||||
await authClient.signOut();
|
||||
})
|
||||
}
|
||||
className="mt-2 cursor-pointer text-violet-700 underline-offset-2 hover:underline"
|
||||
>
|
||||
Çıkış yap
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-2 text-xs text-slate-500">Girişsiz geziyorsun.</p>
|
||||
)}
|
||||
<div className="mt-2 flex gap-1.5">
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="deneme@ornek.dev"
|
||||
className="h-9 min-w-0 flex-1 rounded-lg border border-slate-200 px-2.5 text-xs outline-none focus-visible:border-violet-400"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
disabled={mesgul}
|
||||
onClick={anindaGiris}
|
||||
className="h-9 shrink-0 cursor-pointer rounded-lg bg-violet-600 px-3 text-xs font-semibold text-white hover:bg-violet-700 disabled:opacity-50"
|
||||
>
|
||||
Anında gir
|
||||
</button>
|
||||
</div>
|
||||
<p className="mt-1 text-[10px] text-slate-400">
|
||||
Yeni e-posta = 5 kredili yeni kullanıcı (magic link atlanır).
|
||||
</p>
|
||||
</section>
|
||||
|
||||
{/* Senaryolar */}
|
||||
<section>
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-slate-400">
|
||||
Senaryo
|
||||
</p>
|
||||
<div className="mt-2 grid grid-cols-2 gap-1.5">
|
||||
{(
|
||||
[
|
||||
{
|
||||
ad: "Yeni kullanıcı",
|
||||
aciklama: "5 kredi · paketsiz · raporsuz",
|
||||
fn: async () => {
|
||||
await devAyarla({ kredi: 5, hasPaket: false });
|
||||
await devRaporSil();
|
||||
},
|
||||
},
|
||||
{
|
||||
ad: "Paketli",
|
||||
aciklama: "60 kredi · paket aktif",
|
||||
fn: () => devAyarla({ kredi: 60, hasPaket: true }),
|
||||
},
|
||||
{
|
||||
ad: "Kredisi bitmiş",
|
||||
aciklama: "0 kredi",
|
||||
fn: () => devAyarla({ kredi: 0 }),
|
||||
},
|
||||
{
|
||||
ad: "Raporu sil",
|
||||
aciklama: "boş durumlar",
|
||||
fn: () => devRaporSil(),
|
||||
},
|
||||
] as const
|
||||
).map((s) => (
|
||||
<button
|
||||
key={s.ad}
|
||||
type="button"
|
||||
disabled={mesgul || !g}
|
||||
onClick={() => calistir(s.fn)}
|
||||
className="cursor-pointer rounded-lg border border-slate-200 px-2.5 py-2 text-left text-xs hover:border-violet-300 hover:bg-violet-50 disabled:cursor-default disabled:opacity-40"
|
||||
>
|
||||
<span className="block font-semibold text-slate-800">
|
||||
{s.ad}
|
||||
</span>
|
||||
<span className="text-[10px] text-slate-500">
|
||||
{s.aciklama}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={mesgul || !g}
|
||||
onClick={odemeEkrani}
|
||||
className="mt-1.5 w-full cursor-pointer rounded-lg border border-emerald-200 bg-emerald-50 px-2.5 py-2 text-left text-xs font-semibold text-emerald-800 hover:bg-emerald-100 disabled:cursor-default disabled:opacity-40"
|
||||
>
|
||||
Ödeme başarı ekranı 🎉
|
||||
<span className="block text-[10px] font-normal text-emerald-700">
|
||||
sahte "paid" sipariş oluşturup /odeme/sonuc'a gider
|
||||
</span>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
{/* Sayfalar */}
|
||||
<section>
|
||||
<p className="text-xs font-semibold uppercase tracking-wide text-slate-400">
|
||||
Sayfalar
|
||||
</p>
|
||||
<div className="mt-2 flex flex-wrap gap-1.5">
|
||||
{SAYFALAR.map((s) => (
|
||||
<a
|
||||
key={s.href}
|
||||
href={s.href}
|
||||
className="cursor-pointer rounded-full border border-slate-200 px-2.5 py-1 text-[11px] text-slate-700 hover:border-violet-300 hover:bg-violet-50"
|
||||
>
|
||||
{s.ad}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user