All checks were successful
Deploy / deploy (push) Successful in 7m45s
- Updated the app.db file to reflect recent changes in the application state. - Modified BACKLOG.md to include new entries regarding the removal of certain wizard steps, adjustments to priority chips, and the introduction of special quota conditions for candidates. - Added details on the decision to link priority chips to data sources and clarified the implications for user experience and data handling.
345 lines
14 KiB
TypeScript
345 lines
14 KiB
TypeScript
"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.
|
||
//
|
||
// Yüzey opak kutu değil, yarı saydam malzemedir.
|
||
|
||
import { useCallback, useEffect, useState } from "react";
|
||
import { useRouter } from "next/navigation";
|
||
import { Wrench, X } from "lucide-react";
|
||
import { toast } from "sonner";
|
||
import { authClient } from "@/lib/auth-client";
|
||
import { tercihProfiliSil } from "@/features/sihirbaz/sihirbaz-profil";
|
||
import {
|
||
devAnindaGiris,
|
||
devAyarla,
|
||
devDurum,
|
||
devRaporSil,
|
||
devSahteOdeme,
|
||
type DevDurum,
|
||
} from "@/lib/dev/actions";
|
||
|
||
const SAYFALAR: { ad: string; href: string }[] = [
|
||
{ ad: "Ana sayfa", href: "/" },
|
||
{ ad: "Sonuç (sırasız)", href: "/sonuc" },
|
||
{ 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: "Bölümler", href: "/bolumler" },
|
||
{ ad: "Bölüm (bilg. müh.)", href: "/bolum/bilgisayar-muhendisligi" },
|
||
{ ad: "Üniversiteler", href: "/universiteler" },
|
||
{ ad: "Üniversite (Boğaziçi)", href: "/universite/bogazici-universitesi" },
|
||
{ ad: "Rehber", href: "/rehber" },
|
||
{ ad: "Rehber (devlet/vakıf)", href: "/rehber/devlet-mi-vakif-mi" },
|
||
{ ad: "Meraklısına", href: "/meraklisina" },
|
||
{ 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" },
|
||
];
|
||
|
||
// Basış anında tepki (pointer-down feedback) — bırakınca değil.
|
||
const BASIS =
|
||
"active:scale-[0.97] transition-[transform,background-color,border-color,opacity] duration-150 ease-out";
|
||
|
||
// Yarı saydam malzeme; şeffaflık azaltılsın diyen kullanıcıya opak yüzey.
|
||
const MALZEME =
|
||
"border border-white/60 bg-card/80 shadow-2xl shadow-slate-900/20 backdrop-blur-2xl backdrop-saturate-150 " +
|
||
"[@media(prefers-reduced-transparency:reduce)]:bg-card [@media(prefers-reduced-transparency:reduce)]:backdrop-blur-none";
|
||
|
||
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();
|
||
}
|
||
|
||
// Yol bulma: panelden çıkış her an mümkün — Escape da kapatır.
|
||
useEffect(() => {
|
||
if (!acik) return;
|
||
const f = (e: KeyboardEvent) => {
|
||
if (e.key === "Escape") setAcik(false);
|
||
};
|
||
window.addEventListener("keydown", f);
|
||
return () => window.removeEventListener("keydown", f);
|
||
}, [acik]);
|
||
|
||
// 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);
|
||
} catch (e) {
|
||
// Sunucu hatası (ör. e-posta gönderimi) sessiz kalmasın (6 Eki 2026 QA)
|
||
toast.error(e instanceof Error ? e.message : "Anında giriş başarısız.");
|
||
} 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);
|
||
}
|
||
}
|
||
|
||
const g = durum?.girisli ? durum : null;
|
||
|
||
return (
|
||
<>
|
||
{!acik ? (
|
||
<button
|
||
type="button"
|
||
onClick={paneliAc}
|
||
title="Dev süperadmin paneli"
|
||
style={{ borderRadius: 18 }}
|
||
className={`fixed bottom-20 left-4 z-50 inline-flex h-9 cursor-pointer items-center gap-1.5 px-3 text-xs font-bold tracking-wide text-slate-50 ${MALZEME} !bg-slate-900/85 print:hidden ${BASIS} hover:!bg-slate-800/85`}
|
||
>
|
||
<Wrench className="size-3.5 text-violet-300" aria-hidden />
|
||
DEV
|
||
</button>
|
||
) : (
|
||
<div
|
||
role="dialog"
|
||
aria-label="Dev süperadmin paneli"
|
||
style={{ borderRadius: 24 }}
|
||
className={`fixed bottom-20 left-4 z-50 flex max-h-[72vh] w-80 flex-col overflow-hidden ${MALZEME} print:hidden`}
|
||
>
|
||
<div className="flex min-h-0 flex-col">
|
||
{/* Başlık: opak şerit değil, aynı malzemenin üstünde ince
|
||
bir kenar çizgisiyle ayrışan katman. */}
|
||
<div className="flex items-center justify-between border-b border-slate-900/5 px-4 py-3">
|
||
<p className="flex items-center gap-2 font-heading text-sm font-bold tracking-[-0.01em] text-slate-900">
|
||
<span className="flex size-6 items-center justify-center rounded-lg bg-violet-600 text-white shadow-sm">
|
||
<Wrench className="size-3.5" aria-hidden />
|
||
</span>
|
||
Dev süperadmin
|
||
{mesgul ? (
|
||
<span
|
||
className="size-2 animate-pulse rounded-full bg-slate-400"
|
||
aria-hidden
|
||
/>
|
||
) : null}
|
||
</p>
|
||
<button
|
||
type="button"
|
||
onClick={() => setAcik(false)}
|
||
className={`cursor-pointer rounded-full p-1.5 text-slate-500 hover:bg-slate-900/5 hover:text-slate-700 ${BASIS}`}
|
||
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-[11px] font-semibold uppercase tracking-wide text-slate-400">
|
||
Oturum
|
||
</p>
|
||
{g ? (
|
||
<div className="mt-2 rounded-xl border border-slate-900/5 bg-card/60 px-3 py-2 text-xs">
|
||
<p className="font-semibold text-slate-800">{g.email}</p>
|
||
<p className="mt-0.5 tabular-nums 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 font-medium 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-900/10 bg-card/70 px-2.5 text-xs outline-none transition-colors duration-150 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 ${BASIS}`}
|
||
>
|
||
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-[11px] 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-xl border border-slate-900/10 bg-card/60 px-2.5 py-2 text-left text-xs hover:border-violet-300 hover:bg-violet-50/80 disabled:cursor-default disabled:opacity-40 ${BASIS}`}
|
||
>
|
||
<span className="block font-semibold text-slate-800">
|
||
{s.ad}
|
||
</span>
|
||
<span className="text-[10px] tabular-nums 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-xl border border-emerald-600/15 bg-emerald-50/80 px-2.5 py-2 text-left text-xs font-semibold text-emerald-800 hover:bg-emerald-100/80 disabled:cursor-default disabled:opacity-40 ${BASIS}`}
|
||
>
|
||
Ö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>
|
||
{/* Sıra + sihirbaz profili localStorage'ta yaşar; girişten
|
||
bağımsızdır — bu yüzden !g ile kilitlenmez. */}
|
||
<button
|
||
type="button"
|
||
disabled={mesgul}
|
||
onClick={() => {
|
||
tercihProfiliSil();
|
||
toast.success("Sıralama ve tercih profili temizlendi.");
|
||
router.refresh();
|
||
}}
|
||
className={`mt-1.5 w-full cursor-pointer rounded-xl border border-rose-600/15 bg-rose-50/80 px-2.5 py-2 text-left text-xs font-semibold text-rose-800 hover:bg-rose-100/80 disabled:cursor-default disabled:opacity-40 ${BASIS}`}
|
||
>
|
||
Sıralamayı temizle
|
||
<span className="block text-[10px] font-normal text-rose-700">
|
||
localStorage'taki sıra + sihirbaz profilini siler
|
||
(navbar rozeti dahil)
|
||
</span>
|
||
</button>
|
||
</section>
|
||
|
||
{/* Sayfalar */}
|
||
<section>
|
||
<p className="text-[11px] 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-900/10 bg-card/60 px-2.5 py-1 text-[11px] text-slate-700 hover:border-violet-300 hover:bg-violet-50/80 ${BASIS}`}
|
||
>
|
||
{s.ad}
|
||
</a>
|
||
))}
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</>
|
||
);
|
||
}
|