"use client"; import { useEffect, useRef, useState, useSyncExternalStore } from "react"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { ArrowRight } from "lucide-react"; import { animate } from "motion/mini"; import { useReducedMotion } from "motion/react"; import { Button } from "@/components/ui/button"; import { PixelField } from "@/components/pixel-decor"; import { useGorunumOlayi } from "@/components/use-gorunum-olayi"; import { olay, olayBirKez, siraKovasi } from "@/lib/analitik"; import type { SihirbazFacetleri } from "@/types/yokatlas"; import { SIHIRBAZ_AC_EVENT, SIHIRBAZ_STORAGE_KEY, sihirbazDogrula, type SihirbazSecimleri } from "@/features/sihirbaz/sihirbaz-sabitler"; import { tercihProfiliYaz } from "@/features/sihirbaz/sihirbaz-profil"; import { SihirbazModal } from "./sihirbaz-modal"; // Üretim /listem'de koşar (bkz. listem/liste-uretici.tsx); sihirbaz // tamamlanınca seçimler storage'a yazılıp kullanıcı buraya yönlendirilir. // `kaynak` denemenin hangi yüzeyden başladığını ölçer (bkz. // rapor/components/uretim-basladi-olayi.tsx — izinli değerler orada). // `uret=1` HER ZAMAN ilk parametre kalmalı: giris-karti.tsx:31 funnel // metnini `callbackURL.startsWith("/listem?uret=1")` ile seçiyor, sıra // bozulursa girişteki "Seçimlerin hazır" metni kaybolur. function listemUretHref(kaynak: string) { return `/listem?uret=1&kaynak=${kaynak}`; } // 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 (rapor yokken) * - Adımları toplayan modal * Liste bu sayfada üretilmez ve gösterilmez: sihirbaz tamamlanınca seçimler * storage'a yazılır ve kullanıcı /listem'e yönlendirilir; üretim animasyonu, * kredi/hata durumları ve listenin kendisi orada yaşar. 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üş doğrudan /listem'e düşer ve üretim orada başlar. */ export function SihirbazBolumu({ sira, tur, facetler, girisliMi, raporVar, otomatikAc, kaynak, secimlerHazir, }: { sira: number; tur: string; facetler: SihirbazFacetleri; girisliMi: boolean; /** Kullanıcının kayıtlı bir Yapay Zeka listesi var mı (listenin kendisi /listem'de) */ raporVar: boolean; /** ?sihirbaz=1 ile gelindiyse modalı otomatik aç */ otomatikAc?: boolean; kaynak?: string; /** ?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. */ secimlerHazir?: boolean; }) { const router = useRouter(); const monteEdildi = useMonteEdildi(); const hareketiAzalt = useReducedMotion(); const [elleAcik, setElleAcik] = useState(false); const [elleKapandi, setElleKapandi] = 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 otomatikBasladi = useRef(false); // 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); // Huni adımı: sonuç sayfası görüntülendi. URL sıralamayı taşıdığı için // pathname maskeli (bkz. app/layout.tsx); huni bu olayla kurulur. Oturum // başına girişli/anonim için birer kez — girişten dönen ziyaretçi ikinci // kez "girisli" olarak sayılır. Ham sıra gönderilmez, yalnızca kovası. useEffect(() => { olayBirKez( "sonuc_goruntulendi", { girisli: girisliMi ? "evet" : "hayir", sira_kovasi: siraKovasi(sira), }, girisliMi ? "girisli" : "anonim", ); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Mount sonrası storage'dan bekleyen seçimleri oku useEffect(() => { try { 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, }; } } if (yeniBekleyen) { // eslint-disable-next-line react-hooks/set-state-in-effect 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 }, []); 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; } /** * Sihirbaz tamamlandı: seçimleri storage'a yaz ve üretimin koştuğu * /listem'e geç. Girişsiz kullanıcı önce girişe uğrar; callback doğrudan * /listem'e düştüğünden üretim dönüşte orada otomatik başlar. */ async function ilerle(secimler: SihirbazSecimleri) { tercihProfiliYaz({ sira, tur, secimler }); await modalCikisiniOynat(); setElleAcik(false); if (!girisliMi) { router.push( `/giris?callback=${encodeURIComponent(listemUretHref("sihirbaz"))}`, ); return; } router.push(listemUretHref("sihirbaz")); } /** * Funnel CTA'larının (banner, büyük kart) ortak girişi: * adımlar zaten tamamlanıp kaydedildiyse modal açılmaz — girişli * kullanıcı doğrudan /listem'e gider, 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üşü (?sihirbaz=1) storage'da eşleşen seçimlerle geldiyse modal // açılmaz: seçimler hazır, üretim /listem'de başlasın (bir kez). const otomatikGonder = Boolean( otomatikAc && bekleyen?.eslesir && girisliMi && !raporVar, ); useEffect(() => { if (otomatikGonder && !otomatikBasladi.current) { otomatikBasladi.current = true; // replace: geri tuşu bu sayfaya dönünce tekrar /listem'e sekmesin // Etiket ?sihirbaz=1'i ÜRETEN yüzeyden gelir. Eskiden burası sabit // "giris_donusu" yazıyordu; oysa gerçek giriş dönüşü /listem'e // doğrudan iner, /sonuc'a hiç uğramaz — hero, katalog, arama, + ve // ödeme-sonrası tek yanlış kovada toplanıyordu. router.replace(listemUretHref(kaynak ?? "bilinmiyor")); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [otomatikGonder]); // ?sihirbaz=1: otomatik yönlendirme 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, /listem'e geçilir. const acik = !elleKapandi && (elleAcik || Boolean(otomatikAc && depoOkundu && !otomatikGonder && !raporVar)); function acikDegisti(v: boolean) { if (v) { setElleAcik(true); setElleKapandi(false); } else { setElleKapandi(true); setElleAcik(false); } } // 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 üretim /listem'de başlar. // Sinyal URL'den (?hazir=1) gelir ki SSR'da doğru panel render edilsin. const girisBekliyor = Boolean(!girisliMi && secimlerHazir && !raporVar); const girisUrl = `/giris?callback=${encodeURIComponent(listemUretHref("hazir_cta"))}`; // Kapı 1 ölçümü: panel yalnızca girisBekliyor dalında render edilir; ref // başka dalda bağlanmadığından observer da yalnızca o durumda kurulur. const hazirPanelRef = useGorunumOlayi("hazir_panel_goruntulendi"); return ( <> {/* Yapay Zeka çıktısı (üretim animasyonu dahil) bu sayfada render edilmez; panel yalnızca funnel CTA'larını gösterir, üretim ve listenin kendisi /listem'de yaşar. */} {raporVar ? (

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 güvenli dilimlerine dağıtılmış 24 tercihi — her satırı yerleşme riski ve gerekçesiyle — senin yerine dizer.

)} {monteEdildi ? ( ) : null} ); }