"use client"; import { useEffect, useRef, useState, useSyncExternalStore } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { ArrowRight, X } from "lucide-react"; import { animate } from "motion"; import { AnimatePresence, motion, useReducedMotion } from "motion/react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { ViewportPortal } from "@/components/viewport-portal"; import { PixelField } from "@/components/pixel-decor"; import type { SihirbazFacetleri } from "@/lib/db"; import { SIHIRBAZ_AC_EVENT, SIHIRBAZ_STORAGE_KEY, sihirbazDogrula, sonucHref, tercihProfiliSil, tercihProfiliYaz, type SihirbazSecimleri, } from "@/lib/sihirbaz"; import { olay, siraKovasi } from "@/lib/analitik"; import { SihirbazModal } from "./sihirbaz-modal"; import { ListePaneli, type MevcutRapor, type PanelDurum } from "./liste-paneli"; import { listeOlustur, listeRevize } from "./actions"; 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 orkestratör: * - Sayfa ortasındaki büyük CTA + sabit alt çağrı kartı (rapor yokken) * - Adımları toplayan modal * - Üretim/önizleme/liste paneli (sayfa gövdesinde) * Akış: adımlar → kredi ile üretim → paketsizse progressive-blur önizleme, * paketliyse tam liste. Girişsiz kullanıcı hero'dan seçimleri tamamlanmış * gelirse önce ücretsiz tabloyu görür; giriş CTA'sı "hazır seçimlerin * kaydedilmesi" olarak panelde sunulur, girişten dönüşte üretim otomatik * başlar. Modal içinden ilerleyen girişsiz kullanıcı ise butonun söylediği * gibi doğrudan girişe gider. */ export function SihirbazBolumu({ sira, tur, facetler, girisliMi, hasPaket, kredi: baslangicKredi, mevcutRapor, otomatikAc, acilis, secimlerHazir, }: { sira: number; tur: string; facetler: SihirbazFacetleri; girisliMi: boolean; hasPaket: boolean; kredi: number; mevcutRapor: MevcutRapor | null; /** ?sihirbaz=1 ile gelindiyse modalı otomatik aç */ otomatikAc?: boolean; /** ?acildi=1 (ödeme dönüşü): blur çözülme animasyonuyla aç */ acilis?: boolean; /** ?hazir=1 (hero sihirbazından girişsiz geliş): seçimler storage'da, panel giriş CTA'sına döner. URL'den gelir ki SSR'da doğru panel render edilsin — mount sonrası anahtar değişimi animasyonu donduruyor. */ secimlerHazir?: boolean; }) { const router = useRouter(); const monteEdildi = useMonteEdildi(); const hareketiAzalt = useReducedMotion(); const [elleAcik, setElleAcik] = useState(false); const [elleKapandi, setElleKapandi] = useState(false); const [kartGizle, setKartGizle] = useState(false); const [bekleyen, setBekleyen] = useState(null); // ?sihirbaz=1 fallback modalı storage okunmadan açılmasın: hero'dan gelen // kayıtlı seçimler varken modal bir anlığına açılıp kapanıyordu. const [depoOkundu, setDepoOkundu] = useState(false); const [rapor, setRapor] = useState(mevcutRapor); const [kredi, setKredi] = useState(baslangicKredi); const [paketli, setPaketli] = useState(hasPaket); const [durum, setDurum] = useState("bos"); // Adımlarda yapılan son seçimler (hata sonrası modala dönüşte prefill) const [sonSecimler, setSonSecimler] = useState( null, ); // Aynı üretim için sabit requestId (retry çifte harcama yapmaz) const requestIdRef = useRef(null); const otomatikBasladi = useRef(false); const bolumRef = useRef(null); // Funnel CTA'ları (banner olayı dahil) tıklama anındaki bekleyen seçimi // görsün diye ref'te tutulur; olay dinleyicisi bir kez bağlanır. const bekleyenRef = useRef(null); // Mount sonrası storage'dan bekleyen seçimleri + kalıcı kart kapatmayı oku 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); bekleyenRef.current = yeniBekleyen; } } catch {} setDepoOkundu(true); }, [sira, tur]); // Arama modalındaki funnel kartının CTA'sı sihirbazı bu olayla başlatır useEffect(() => { const ac = () => sihirbaziBaslat(); window.addEventListener(SIHIRBAZ_AC_EVENT, ac); return () => window.removeEventListener(SIHIRBAZ_AC_EVENT, ac); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); /** * Bölüm ekran dışındaysa (kullanıcı sihirbazı üstteki banner'dan ya da * sabit alt karttan açmış olabilir) modal hâlâ perdeyi kaplarken kaydırırız; * böylece Motion ile giren hedef panel görünür alanda kalır. */ function bolumuGorunureAl() { const el = bolumRef.current; if (!el) return; const kutu = el.getBoundingClientRect(); if (kutu.top >= 0 && kutu.bottom <= window.innerHeight) return; el.scrollIntoView({ block: "center", behavior: "instant" }); } async function modalCikisiniOynat() { if (hareketiAzalt) return; const modal = document.querySelector("[data-sihirbaz-modal]"); if (!modal) return; const mevcutTransform = getComputedStyle(modal).transform; const hedefTransform = mevcutTransform === "none" ? "translate3d(0, 48px, 0)" : new DOMMatrixReadOnly(mevcutTransform).translate(0, 48).toString(); const perde = document.querySelector( '[data-slot="dialog-overlay"]', ); const modalAnimasyonu = animate( modal, { opacity: 0, transform: hedefTransform, }, { duration: 0.24, ease: [0.4, 0, 0.7, 0.2], }, ); if (perde) { await Promise.all([ modalAnimasyonu, animate(perde, { opacity: 0 }, { duration: 0.2, ease: "easeOut" }), ]); return; } await modalAnimasyonu; } async function uret(secimler: SihirbazSecimleri) { if (!requestIdRef.current) requestIdRef.current = crypto.randomUUID(); bolumuGorunureAl(); await modalCikisiniOynat(); setElleAcik(false); setDurum("uretiliyor"); try { const sonuc = await listeOlustur({ sira, tur, secimler, requestId: requestIdRef.current, }); if (sonuc.ok) { requestIdRef.current = null; olay("liste_olusturuldu", { tur, sira_kovasi: siraKovasi(sira) }); setRapor({ rapor: sonuc.rapor, params: sonuc.params, revisionCount: sonuc.revisionCount, }); setKredi(sonuc.kredi); setPaketli(sonuc.hasPaket); tercihProfiliSil(); // Yapay Zeka çıktısı bu sayfada gösterilmez: üretim animasyonu açık kalır, // liste (paketsizse paywall'lı hâliyle) /listem'de karşılar. router.push("/listem"); return; } // Hata: aynı requestId'yi koru ki tekrar denemede çifte harcama olmasın if (sonuc.code === "KREDI" || sonuc.code === "PAKET") { setDurum("kredi-yetersiz"); } else if (sonuc.code === "AUTH") { secimleriKaydetVeGirise(secimler); } else { toast.error(sonuc.error); modalaDon(); } } catch { toast.error("Beklenmeyen bir hata oluştu, tekrar dener misin?"); modalaDon(); } } /** Hata sonrası panelden modala dönüş. */ function modalaDon() { setDurum("bos"); setElleAcik(true); } function secimleriKaydetVeGirise(secimler: SihirbazSecimleri) { tercihProfiliYaz({ sira, tur, secimler }); const geri = sonucHref({ sira, tur }, { sihirbaz: "1" }); router.push(`/giris?callback=${encodeURIComponent(geri)}`); } async function ilerle(secimler: SihirbazSecimleri) { setSonSecimler(secimler); if (!girisliMi) { await modalCikisiniOynat(); secimleriKaydetVeGirise(secimler); return; } await uret(secimler); } /** * Funnel CTA'larının (banner, büyük kart, sabit alt kart) ortak girişi: * adımlar zaten tamamlanıp kaydedildiyse modal açılmaz — girişli * kullanıcıda üretim doğrudan başlar, girişsiz kullanıcı girişe gider. * Kayıtlı seçim yoksa (ya da başka sıra/türe aitse) modal açılır. */ function sihirbaziBaslat() { const hazir = bekleyenRef.current; if (hazir?.eslesir) { void ilerle(hazir.secimler); return; } setElleAcik(true); setElleKapandi(false); } // Giriş dönüşü: storage'daki eşleşen seçimlerle otomatik üretime başla // (bir kez, modal açmadan — üretim sayfa gövdesinde oynar). const otomatikUret = Boolean(bekleyen?.eslesir && girisliMi && !rapor); useEffect(() => { if (otomatikUret && bekleyen && !otomatikBasladi.current) { otomatikBasladi.current = true; setSonSecimler(bekleyen.secimler); void uret(bekleyen.secimler); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [otomatikUret]); // ?sihirbaz=1: otomatik üretim devreye girmediyse (seçim kaybolduysa/ // eşleşmiyorsa) modalı aç ki kullanıcı adımları tamamlayabilsin. Karar // storage okunmadan verilmez: kayıtlı seçimlerle gelen kullanıcıya modal // hiç gösterilmez, üretim doğrudan başlar. const acik = !elleKapandi && (elleAcik || Boolean( otomatikAc && depoOkundu && !otomatikUret && !rapor && durum === "bos", )); function acikDegisti(v: boolean) { if (v) { setElleAcik(true); setElleKapandi(false); } else { setElleKapandi(true); setElleAcik(false); } } const panelde = Boolean(rapor) || durum !== "bos"; // Değer önce: sihirbazı bitirmiş ama girişsiz kullanıcı ücretsiz tabloyu // zaten görüyor; giriş burada bariyer değil, hazır seçimlerin (ödülün) // kaydedilmesi olarak sunulur. Girişten dönüşte otomatik üretim başlar. // Sinyal URL'den (?hazir=1) gelir; storage'a bakılsaydı panel anahtarı // mount sonrası değişir ve AnimatePresence geçişi donardı. const girisBekliyor = Boolean(!girisliMi && secimlerHazir && !rapor); const girisUrl = `/giris?callback=${encodeURIComponent( sonucHref({ sira, tur }, { sihirbaz: "1" }), )}`; const panelAnahtari = durum !== "bos" ? durum : rapor ? "rapor-hazir" : girisBekliyor ? "giris-cta" : "sihirbaz-cta"; return ( <> {/* Yapay Zeka çıktısı (rapor listesi/haritası) bu sayfada render edilmez; panel yalnızca üretim ve kredi-yetersiz gibi geçici funnel durumlarını gösterir, listenin kendisi /listem'de yaşar. */}
{durum !== "bos" ? ( { const sonuc = await listeRevize({ feedback, requestId: crypto.randomUUID(), }); if (sonuc.ok) { olay("liste_revize", { sayfa: "sonuc" }); setRapor({ rapor: sonuc.rapor, params: sonuc.params, revisionCount: sonuc.revisionCount, }); setKredi(sonuc.kredi); toast.success("Listen yeniden kuruldu."); } else { toast.error(sonuc.error); } return sonuc.ok; }} /> ) : rapor ? (

24 tercihlik listen hazır

Yapay zekânın kurduğu risk analizli listen Listem sayfasında seni bekliyor.

) : girisBekliyor ? (

Seçimlerin hazır — Yapay Zeka listen bir adım uzakta

Yukarıdaki tablo her zaman ücretsiz. 5 deneme kredinle 24 tercihlik Yapay Zeka listeni de kurabilirsin: ücretsiz giriş yap, seçimlerin kayıtlı — listen otomatik kurulsun (üretim 3 kredi, kalan 2 kredinle danışmana soru sorarsın).

Kredi kartı gerekmez · Google ya da e-posta ile 30 saniyede

) : (

24 tercihi tek tek elemek yerine, yapay zekâ kursun

Tablodan elle eklemek her zaman serbest. Ama üç adımda ilgi alanlarını ve şehirlerini söylersen, yapay zekâ hayal, dengeli ve garanti dilimlerine dağıtılmış 24 tercihi — her satırı yerleşme riski ve gerekçesiyle — senin yerine dizer.

)}
{/* Sabit alt çağrı kartı — liste/panel yokken ve kart kapatılmadıysa */} {monteEdildi && !panelde && !kartGizle && !acik ? (

{girisBekliyor ? "Seçimlerin kayıtlı — Yapay Zeka listen hazır bekliyor" : "Tek tek ekleme, yapay zekâ kursun"}

{girisBekliyor ? "Ücretsiz giriş yap, 5 deneme kredinle listen otomatik kurulsun." : "Üç adımda seçimlerini söyle; risk analizli, gerekçeli 24 tercih hazır."}

{girisBekliyor ? ( ) : ( )}
) : null}
{/* Üretim başladığında modalı kaldır; yeni panelin geçişini Motion üstlenir ve odağı panel içeriğine taşır. */} {monteEdildi && durum === "bos" ? ( ) : null} ); }