All checks were successful
Deploy / deploy (push) Successful in 16m7s
- Added Rybbit analytics configuration in .mcp.json for improved data collection. - Updated layout.tsx to conditionally load the Rybbit analytics script in production. - Enhanced user interaction tracking in various components, including GirisForm, RevizyonKutusu, and SatinAlForm, by logging specific events. - Updated privacy policy page to reflect new data handling practices and added a section on service providers and data transfer. - Improved user navigation and experience by integrating Rybbit's live visitor widget in the site footer.
563 lines
21 KiB
TypeScript
563 lines
21 KiB
TypeScript
"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<BekleyenSecim | null>(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 | null>(mevcutRapor);
|
||
const [kredi, setKredi] = useState(baslangicKredi);
|
||
const [paketli, setPaketli] = useState(hasPaket);
|
||
const [durum, setDurum] = useState<PanelDurum>("bos");
|
||
|
||
// Adımlarda yapılan son seçimler (hata sonrası modala dönüşte prefill)
|
||
const [sonSecimler, setSonSecimler] = useState<SihirbazSecimleri | null>(
|
||
null,
|
||
);
|
||
|
||
// Aynı üretim için sabit requestId (retry çifte harcama yapmaz)
|
||
const requestIdRef = useRef<string | null>(null);
|
||
const otomatikBasladi = useRef(false);
|
||
const bolumRef = useRef<HTMLDivElement>(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<BekleyenSecim | null>(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<HTMLElement>("[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<HTMLElement>(
|
||
'[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. */}
|
||
<div ref={bolumRef} className="relative">
|
||
<AnimatePresence mode="popLayout" initial={false}>
|
||
<motion.div
|
||
key={panelAnahtari}
|
||
initial={
|
||
hareketiAzalt
|
||
? { opacity: 0 }
|
||
: {
|
||
opacity: 0,
|
||
transform: "translate3d(0, 8px, 0)",
|
||
}
|
||
}
|
||
animate={{
|
||
opacity: 1,
|
||
transform: "translate3d(0, 0, 0)",
|
||
transition: hareketiAzalt
|
||
? { duration: 0.12, ease: "easeOut" }
|
||
: {
|
||
duration: 0.28,
|
||
ease: [0.23, 1, 0.32, 1],
|
||
},
|
||
}}
|
||
exit={
|
||
hareketiAzalt
|
||
? {
|
||
opacity: 0,
|
||
transition: { duration: 0.08, ease: "easeOut" },
|
||
}
|
||
: {
|
||
opacity: 0,
|
||
transform: "translate3d(0, -4px, 0)",
|
||
transition: {
|
||
duration: 0.14,
|
||
ease: [0.4, 0, 0.7, 0.2],
|
||
},
|
||
}
|
||
}
|
||
>
|
||
{durum !== "bos" ? (
|
||
<ListePaneli
|
||
durum={durum}
|
||
rapor={null}
|
||
hasPaket={paketli}
|
||
kredi={kredi}
|
||
sira={sira}
|
||
acilis={acilis}
|
||
onRevize={async (feedback) => {
|
||
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 ? (
|
||
<section className="mt-16 rounded-2xl border border-slate-200 bg-white p-8 text-center sm:p-12">
|
||
<h2 className="font-heading text-2xl font-bold sm:text-3xl">
|
||
24 tercihlik listen hazır
|
||
</h2>
|
||
<p className="mx-auto mt-3 max-w-xl text-slate-600">
|
||
Yapay zekânın kurduğu risk analizli listen Listem sayfasında
|
||
seni bekliyor.
|
||
</p>
|
||
<Button
|
||
asChild
|
||
size="lg"
|
||
className="mt-6 h-12 cursor-pointer bg-orange-500 px-8 text-white transition-colors duration-200 hover:bg-orange-600"
|
||
>
|
||
<Link href="/listem">Listeni gör</Link>
|
||
</Button>
|
||
</section>
|
||
) : girisBekliyor ? (
|
||
<section className="relative isolate mt-16 min-w-0 overflow-hidden rounded-2xl bg-primary px-5 py-8 text-center text-white sm:p-12">
|
||
<div
|
||
aria-hidden
|
||
className="pointer-events-none absolute inset-x-0 -inset-y-10 -z-10 text-white"
|
||
>
|
||
<PixelField seed={13} />
|
||
</div>
|
||
<h2 className="relative font-heading text-2xl font-bold sm:text-3xl">
|
||
Seçimlerin hazır — Yapay Zeka listen bir adım uzakta
|
||
</h2>
|
||
<p className="relative mx-auto mt-3 max-w-xl text-white/90">
|
||
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).
|
||
</p>
|
||
<Button
|
||
asChild
|
||
size="lg"
|
||
className="relative mt-6 h-auto min-h-12 w-full cursor-pointer whitespace-normal bg-orange-500 px-4 py-3 text-white transition-colors duration-200 hover:bg-orange-600 sm:w-auto sm:px-8"
|
||
>
|
||
<Link href={girisUrl}>
|
||
5 deneme kredinle Yapay Zeka listeni kur — giriş yap
|
||
<ArrowRight className="size-4" aria-hidden />
|
||
</Link>
|
||
</Button>
|
||
<p className="relative mt-3 text-xs text-white/70">
|
||
Kredi kartı gerekmez · Google ya da e-posta ile 30 saniyede
|
||
</p>
|
||
</section>
|
||
) : (
|
||
<section className="relative isolate mt-16 min-w-0 overflow-hidden rounded-2xl bg-primary px-5 py-8 text-center text-white sm:p-12">
|
||
<div
|
||
aria-hidden
|
||
className="pointer-events-none absolute inset-x-0 -inset-y-10 -z-10 text-white"
|
||
>
|
||
<PixelField seed={19} />
|
||
</div>
|
||
<h2 className="relative font-heading text-2xl font-bold sm:text-3xl">
|
||
24 tercihi tek tek elemek yerine, yapay zekâ kursun
|
||
</h2>
|
||
<p className="relative mx-auto mt-3 max-w-xl text-white/90">
|
||
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.
|
||
</p>
|
||
<Button
|
||
size="lg"
|
||
onClick={sihirbaziBaslat}
|
||
className="relative mt-6 h-auto min-h-12 w-full cursor-pointer whitespace-normal bg-orange-500 px-4 py-3 text-white transition-colors duration-200 hover:bg-orange-600 sm:w-auto sm:px-8"
|
||
>
|
||
Yapay Zeka listemi kur
|
||
<ArrowRight className="size-4" aria-hidden />
|
||
</Button>
|
||
</section>
|
||
)}
|
||
</motion.div>
|
||
</AnimatePresence>
|
||
</div>
|
||
|
||
{/* Sabit alt çağrı kartı — liste/panel yokken ve kart kapatılmadıysa */}
|
||
<ViewportPortal>
|
||
{monteEdildi && !panelde && !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">
|
||
{girisBekliyor
|
||
? "Seçimlerin kayıtlı — Yapay Zeka listen hazır bekliyor"
|
||
: "Tek tek ekleme, yapay zekâ kursun"}
|
||
</p>
|
||
<p className="text-xs text-slate-500">
|
||
{girisBekliyor
|
||
? "Ücretsiz giriş yap, 5 deneme kredinle listen otomatik kurulsun."
|
||
: "Üç adımda seçimlerini söyle; risk analizli, gerekçeli 24 tercih hazır."}
|
||
</p>
|
||
</div>
|
||
{girisBekliyor ? (
|
||
<Button
|
||
asChild
|
||
size="sm"
|
||
className="cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
||
>
|
||
<Link href={girisUrl}>Giriş yap</Link>
|
||
</Button>
|
||
) : (
|
||
<Button
|
||
size="sm"
|
||
onClick={sihirbaziBaslat}
|
||
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}
|
||
</ViewportPortal>
|
||
|
||
{/* Ü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" ? (
|
||
<SihirbazModal
|
||
acik={acik}
|
||
onAcikDegisti={acikDegisti}
|
||
sira={sira}
|
||
tur={tur}
|
||
facetler={facetler}
|
||
baslangicSecimler={
|
||
sonSecimler ?? (bekleyen?.eslesir ? bekleyen.secimler : null)
|
||
}
|
||
sonButonEtiketi={
|
||
girisliMi ? "Yapay Zeka listemi kur (3 kredi)" : "Giriş yap ve listeni gör"
|
||
}
|
||
onTamamla={ilerle}
|
||
/>
|
||
) : null}
|
||
</>
|
||
);
|
||
}
|