feat: kimlik doğrulama, ödeme, kredi ve AI rapor sihirbazı ekle

- better-auth ile giriş/oturum yönetimi (src/lib/auth, giris sayfası)
- iyzico ödeme entegrasyonu ve paket satın alma akışı
- kredi sistemi ve uygulama veritabanı şeması (drizzle)
- AI destekli rapor üretimi ve tercih sihirbazı
- rapor yazdırma sayfası ve site header/kullanıcı menüsü

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
bilalgursen
2026-07-22 02:13:39 +03:00
parent 7c3f17b9b5
commit cee6200237
64 changed files with 7966 additions and 89 deletions

View File

@@ -0,0 +1,201 @@
"use client";
import { useEffect, useState, useSyncExternalStore } from "react";
import { Sparkles, X } from "lucide-react";
import { Button } from "@/components/ui/button";
import type { SihirbazFacetleri } from "@/lib/db";
import {
SIHIRBAZ_STORAGE_KEY,
sihirbazDogrula,
type SihirbazSecimleri,
} from "@/lib/sihirbaz";
import {
SihirbazModal,
type MevcutRapor,
type OnizlemeSatir,
} from "./sihirbaz-modal";
const KAPATMA_KEY = "kolaytercih.sihirbaz-karti-kapali";
// SSR'da false, client'ta true — storage'a ancak mount sonrası dokunuruz
const bosAbone = () => () => {};
const useMonteEdildi = () =>
useSyncExternalStore(
bosAbone,
() => true,
() => false,
);
type BekleyenSecim = {
secimler: SihirbazSecimleri;
eslesir: boolean; // storage'daki sıra/tur bu sayfayla aynı mı
};
/**
* Sonuç sayfasında sihirbaz akışını yöneten client wrapper:
* - Sayfa ortasındaki büyük CTA (satır içi)
* - Sabit alt çağrı kartı
* - Modal
* hepsi tek `acik` state'ini paylaşır.
*/
export function SihirbazBolumu({
sira,
tur,
facetler,
girisliMi,
hasPaket,
kredi,
mevcutRapor,
onizlemeSatirlari,
otomatikAc,
}: {
sira: number;
tur: string;
facetler: SihirbazFacetleri;
girisliMi: boolean;
hasPaket: boolean;
kredi: number;
mevcutRapor: MevcutRapor | null;
onizlemeSatirlari: OnizlemeSatir[];
/** ?sihirbaz=1 ile gelindiyse modalı otomatik aç */
otomatikAc?: boolean;
}) {
const monteEdildi = useMonteEdildi();
const [elleAcik, setElleAcik] = useState(false);
const [elleKapandi, setElleKapandi] = useState(false);
const [kartGizle, setKartGizle] = useState(false);
const [bekleyen, setBekleyen] = useState<BekleyenSecim | null>(null);
// Mount sonrası storage'dan bekleyen seçimleri + kalıcı kart kapatmayı oku
// (dış store'dan başlangıç senkronizasyonu — bir kez).
useEffect(() => {
try {
const kaliciKapali = Boolean(sessionStorage.getItem(KAPATMA_KEY));
const ham = localStorage.getItem(SIHIRBAZ_STORAGE_KEY);
let yeniBekleyen: BekleyenSecim | null = null;
if (ham) {
const veri = JSON.parse(ham) as {
sira?: number;
tur?: string;
secimler?: unknown;
};
const secimler = sihirbazDogrula(veri.secimler);
if (secimler) {
yeniBekleyen = {
secimler,
eslesir: veri.sira === sira && veri.tur === tur,
};
}
}
// eslint-disable-next-line react-hooks/set-state-in-effect
if (kaliciKapali) setKartGizle(true);
if (yeniBekleyen) setBekleyen(yeniBekleyen);
} catch {}
}, [sira, tur]);
// Giriş/ödeme dönüşünde otomatik üretim: eşleşen bekleyen seçim + paket
const otomatikUret = Boolean(bekleyen?.eslesir && girisliMi && hasPaket);
// Açık durumu türetilmiş: elle veya otomatik tetik, kullanıcı kapatmadıkça
const acik = !elleKapandi && (elleAcik || Boolean(otomatikAc) || otomatikUret);
const baslangicSecimler = bekleyen?.eslesir ? bekleyen.secimler : null;
function acikDegisti(v: boolean) {
if (v) {
setElleAcik(true);
setElleKapandi(false);
} else {
setElleKapandi(true);
setElleAcik(false);
}
}
// Üretim başladıysa storage'ı temizle (modal artık state'te tutuyor)
useEffect(() => {
if (acik && otomatikUret) {
try {
localStorage.removeItem(SIHIRBAZ_STORAGE_KEY);
} catch {}
}
}, [acik, otomatikUret]);
const raporVar = Boolean(mevcutRapor);
return (
<>
{/* Sayfa ortasındaki büyük CTA */}
<section className="mt-16 rounded-2xl bg-primary p-8 text-center text-white sm:p-12">
<h2 className="font-heading text-2xl font-bold sm:text-3xl">
{raporVar
? "24 tercihlik listen hazır"
: "Bu listeyi 24 tercihlik plana çevirelim mi?"}
</h2>
<p className="mx-auto mt-3 max-w-xl text-white/90">
{raporVar
? "Listeni görüntüle, revize et veya listen hakkında soru sor."
: "Birkaç adımda ilgi alanların ve şehir tercihine göre gerekçeli, dengeli bir tercih listesi kuralım — her satır için yerleşme riski analiziyle."}
</p>
<Button
size="lg"
onClick={() => acikDegisti(true)}
className="mt-6 h-12 cursor-pointer bg-orange-500 px-8 text-white transition-colors duration-200 hover:bg-orange-600"
>
<Sparkles className="size-4" aria-hidden />
{raporVar ? "Listemi görüntüle" : "Listemi oluştur"}
</Button>
</section>
{/* Sabit alt çağrı kartı — rapor yoksa ve kart kapatılmadıysa */}
{monteEdildi && !raporVar && !kartGizle && !acik ? (
<div className="pointer-events-none fixed inset-x-0 bottom-4 z-40 px-4">
<div className="pointer-events-auto mx-auto flex max-w-xl items-center gap-4 rounded-2xl border border-slate-200 bg-white p-4 shadow-xl">
<div className="flex-1">
<p className="font-heading text-sm font-bold text-slate-900">
3 adımda 24 tercihlik listen
</p>
<p className="text-xs text-slate-500">
Sıralamana uygun bölümleri seç, yapay zekâ listeni kursun.
</p>
</div>
<Button
size="sm"
onClick={() => acikDegisti(true)}
className="cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
>
Başla
</Button>
<button
type="button"
aria-label="Kapat"
onClick={() => {
setKartGizle(true);
try {
sessionStorage.setItem(KAPATMA_KEY, "1");
} catch {}
}}
className="cursor-pointer rounded-full p-1 text-slate-400 transition-colors duration-200 hover:bg-slate-100 hover:text-slate-600"
>
<X className="size-4" aria-hidden />
</button>
</div>
</div>
) : null}
{monteEdildi ? (
<SihirbazModal
acik={acik}
onAcikDegisti={acikDegisti}
sira={sira}
tur={tur}
facetler={facetler}
girisliMi={girisliMi}
hasPaket={hasPaket}
kredi={kredi}
mevcutRapor={mevcutRapor}
onizlemeSatirlari={onizlemeSatirlari}
otomatikUret={otomatikUret}
baslangicSecimler={baslangicSecimler}
/>
) : null}
</>
);
}