Merge branch 'gece/p6-arti-aninda-ekle' into gece/entegrasyon-test-2

This commit is contained in:
bilalgursen
2026-09-21 09:27:12 +03:00
4 changed files with 232 additions and 62 deletions

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { Check, Plus } from "lucide-react"; import { ArrowRight, Check, Plus } from "lucide-react";
import { toast } from "sonner"; import { toast } from "sonner";
import type { Program } from "@/types/yokatlas"; import type { Program } from "@/types/yokatlas";
import { olay } from "@/lib/analitik"; import { olay } from "@/lib/analitik";
@@ -10,7 +10,10 @@ import {
useListedeMi, useListedeMi,
type ManuelKaynak, type ManuelKaynak,
} from "../liste-store"; } from "../liste-store";
import { programEkleIste } from "../hooks/use-tercih-profili"; import {
profilToplamaAc,
programEkleIste,
} from "../hooks/use-tercih-profili";
import { preloadTercihProfiliKapisi } from "./tercih-profili-kapisi-lazy"; import { preloadTercihProfiliKapisi } from "./tercih-profili-kapisi-lazy";
type ProgramOzet = Pick< type ProgramOzet = Pick<
@@ -25,6 +28,8 @@ type ProgramOzet = Pick<
| "sira2024" | "sira2024"
>; >;
const PROFILSIZ_EKLEME_TOAST_ID = "profilsiz-ekleme";
export function ProgramEkleButonu({ export function ProgramEkleButonu({
program, program,
kaynak = "sonuc", kaynak = "sonuc",
@@ -52,8 +57,33 @@ export function ProgramEkleButonu({
toast.info(`Listen ${MANUEL_LISTE_MAX} tercihe ulaştı.`); toast.info(`Listen ${MANUEL_LISTE_MAX} tercihe ulaştı.`);
} else if (sonuc.zatenVar) { } else if (sonuc.zatenVar) {
toast.info("Bu program zaten seçtiklerinde."); toast.info("Bu program zaten seçtiklerinde.");
} else { } else if (sonuc.eklendi) {
olay("program_eklendi", { kaynak }); olay("program_eklendi", {
kaynak,
yol: "dogrudan",
profilli: sonuc.profilsiz ? "hayir" : "evet",
});
if (sonuc.profilsiz) {
// Önce ver, sonra iste: program eklendi; sıralama sorusu değer
// karşılığında gelir. Sabit id → art arda +'larda toast yığılmaz,
// aynı toast tazelenir.
toast.success("Seçtiklerine eklendi", {
id: PROFILSIZ_EKLEME_TOAST_ID,
duration: 8000,
description:
"Sıralamanı girersen bu programın risk seviyesini de gösteririz.",
action: {
label: (
<span className="inline-flex items-center gap-1">
Sıralamamı gir
<ArrowRight className="size-4" aria-hidden />
</span>
),
// Tanıtım ekranını atla, doğrudan sıralama adımı
onClick: () => profilToplamaAc(null, true, "arti_toast"),
},
});
}
} }
} }
@@ -62,7 +92,8 @@ export function ProgramEkleButonu({
type="button" type="button"
disabled={listede || doldu} disabled={listede || doldu}
onClick={handleClick} onClick={handleClick}
// Profilsiz kullanıcıda + kapıyı açar — chunk'ı niyet anında ısıt // Profilsiz eklemenin toast aksiyonu kapıyı açar — chunk'ı niyet
// anında ısıt
onPointerEnter={preloadTercihProfiliKapisi} onPointerEnter={preloadTercihProfiliKapisi}
onFocus={preloadTercihProfiliKapisi} onFocus={preloadTercihProfiliKapisi}
title={ title={

View File

@@ -1,6 +1,6 @@
"use client"; "use client";
import { useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import { ArrowRight, Trophy } from "lucide-react"; import { ArrowRight, Trophy } from "lucide-react";
import { useReducedMotion } from "motion/react"; import { useReducedMotion } from "motion/react";
@@ -53,17 +53,38 @@ type Adim = "giris" | "sira" | "sihirbaz";
const ADIM_SIRASI: Record<Adim, number> = { giris: 0, sira: 1, sihirbaz: 2 }; const ADIM_SIRASI: Record<Adim, number> = { giris: 0, sira: 1, sihirbaz: 2 };
export function TercihProfiliKapisi() { export function TercihProfiliKapisi() {
const { acik, duzenle, dogrudanSira, bekleyen, bekleyenHedef, profil } = const {
useProfilKapisi(); acik,
duzenle,
dogrudanSira,
tetik,
bekleyen,
bekleyenHedef,
profil,
} = useProfilKapisi();
// Kapanış olayı için o an görünen adım; KapıIcerik her adım değişiminde
// (ve mount'ta) bildirir. Render'ı etkilemediği için state değil ref.
const adimRef = useRef<Adim>("giris");
const oturumAnahtari = acik const oturumAnahtari = acik
? `${duzenle ? "d" : "n"}-${bekleyen?.program.id ?? "yok"}-${profil?.sira ?? 0}-${profil?.tur ?? ""}` ? `${duzenle ? "d" : "n"}-${bekleyen?.program.id ?? "yok"}-${profil?.sira ?? 0}-${profil?.tur ?? ""}`
: "kapali"; : "kapali";
// Tamamlanmadan kapanışın TEK noktası: X, dışarı tıklama, Esc (Dialog
// onOpenChange) ve içerideki "Vazgeç" butonları buradan geçer.
// Tamamlanan kapı buraya uğramaz: secimleriTamamla → profilTamamlandi
// depoda `acik`'ı doğrudan kapatır; Radix kontrollü `open` prop'u
// değişince onOpenChange çağırmaz → `kapi_kapatildi` atılmaz.
function vazgec() {
if (!acik) return;
olay("kapi_kapatildi", { adim: adimRef.current, tetik });
profilKapisiniKapat();
}
return ( return (
<Dialog <Dialog
open={acik} open={acik}
onOpenChange={(v) => { onOpenChange={(v) => {
if (!v) profilKapisiniKapat(); if (!v) vazgec();
}} }}
> >
<DialogContent <DialogContent
@@ -85,6 +106,10 @@ export function TercihProfiliKapisi() {
bekleyen={bekleyen} bekleyen={bekleyen}
hedef={bekleyenHedef} hedef={bekleyenHedef}
profil={profil} profil={profil}
onAdimDegisti={(yeniAdim) => {
adimRef.current = yeniAdim;
}}
onVazgec={vazgec}
/> />
) : null} ) : null}
</DialogContent> </DialogContent>
@@ -98,21 +123,32 @@ function KapıIcerik({
bekleyen, bekleyen,
hedef, hedef,
profil, profil,
onAdimDegisti,
onVazgec,
}: { }: {
duzenle: boolean; duzenle: boolean;
dogrudanSira: boolean; dogrudanSira: boolean;
bekleyen: BekleyenProgram | null; bekleyen: BekleyenProgram | null;
hedef: BekleyenHedef | null; hedef: BekleyenHedef | null;
profil: TercihProfili | null; profil: TercihProfili | null;
/** Görünen adımı üst bileşene bildirir (yalnız `kapi_kapatildi` ölçümü). */
onAdimDegisti: (adim: Adim) => void;
/** Tamamlamadan kapat — olayı üst bileşen atar, sonra kapıyı kapatır. */
onVazgec: () => void;
}) { }) {
const router = useRouter(); const router = useRouter();
const bilinenSira = bekleyen?.adaySira ?? profil?.sira ?? null; const bilinenSira = bekleyen?.adaySira ?? profil?.sira ?? null;
const bilinenTur = bekleyen?.adayTur ?? profil?.tur ?? "say"; const bilinenTur = bekleyen?.adayTur ?? profil?.tur ?? "say";
// Düzenlemede ve navbar "Sıralamanı gir" girişinde doğrudan sıralama adımı // Düzenlemede ve navbar "Sıralamanı gir" girişinde doğrudan sıralama adımı
const [adim, setAdim] = useState<Adim>( const ilkAdim: Adim = duzenle || dogrudanSira ? "sira" : "giris";
duzenle || dogrudanSira ? "sira" : "giris", const [adim, setAdim] = useState<Adim>(ilkAdim);
); // Her açılışta (key ile yeniden mount) üstteki adım kaydını başa sar;
// önceki açılıştan kalan "sihirbaz" değeri yeni kapanışa sızmasın.
useEffect(() => {
onAdimDegisti(ilkAdim);
// eslint-disable-next-line react-hooks/exhaustive-deps -- yalnız mount
}, []);
// Adım geçişleri sihirbazın kendi içindeki TransitionPanel kaymasıyla oynar // Adım geçişleri sihirbazın kendi içindeki TransitionPanel kaymasıyla oynar
const [yon, setYon] = useState(1); const [yon, setYon] = useState(1);
const [gecisBasladi, setGecisBasladi] = useState(false); const [gecisBasladi, setGecisBasladi] = useState(false);
@@ -140,6 +176,7 @@ function KapıIcerik({
setYon(ADIM_SIRASI[yeniAdim] > ADIM_SIRASI[adim] ? 1 : -1); setYon(ADIM_SIRASI[yeniAdim] > ADIM_SIRASI[adim] ? 1 : -1);
setGecisBasladi(true); setGecisBasladi(true);
setAdim(yeniAdim); setAdim(yeniAdim);
onAdimDegisti(yeniAdim);
} }
async function facetYukle(siraDeger: number, turDeger: string) { async function facetYukle(siraDeger: number, turDeger: string) {
@@ -287,7 +324,7 @@ function KapıIcerik({
<Button <Button
type="button" type="button"
variant="ghost" variant="ghost"
onClick={profilKapisiniKapat} onClick={onVazgec}
className="cursor-pointer" className="cursor-pointer"
> >
Vazgeç Vazgeç
@@ -385,7 +422,7 @@ function KapıIcerik({
onClick={() => onClick={() =>
// Doğrudan sıralama adımıyla açıldıysa tanıtım adımı hiç // Doğrudan sıralama adımıyla açıldıysa tanıtım adımı hiç
// görülmedi; "Geri" yerine kapıyı kapatmak beklenen davranış. // görülmedi; "Geri" yerine kapıyı kapatmak beklenen davranış.
duzenle || dogrudanSira ? profilKapisiniKapat() : adimaGec("giris") duzenle || dogrudanSira ? onVazgec() : adimaGec("giris")
} }
className="cursor-pointer" className="cursor-pointer"
> >

View File

@@ -1,10 +1,12 @@
"use client"; "use client";
// Global "+" kapısı: profil yokken bekleyen programı tutar, modalı açar. // Global profil kapısı deposu. + profil yokken de programı hemen ekler
// (prototip); kapı, ekleme toast'ındaki aksiyondan ve diğer CTA'lardan açılır.
// Layout'ta bir kez monte edilen TercihProfiliKapisi bu store'a abone olur. // Layout'ta bir kez monte edilen TercihProfiliKapisi bu store'a abone olur.
import { useSyncExternalStore } from "react"; import { useSyncExternalStore } from "react";
import type { Program } from "@/types/yokatlas"; import type { Program } from "@/types/yokatlas";
import { olay } from "@/lib/analitik";
import { PROFIL_DEGISTI_EVENT, sonucHref, type TercihProfili } from "@/features/sihirbaz/sihirbaz-sabitler"; import { PROFIL_DEGISTI_EVENT, sonucHref, type TercihProfili } from "@/features/sihirbaz/sihirbaz-sabitler";
import { tercihProfiliOku, tercihProfiliTamamlandiMi } from "@/features/sihirbaz/sihirbaz-profil"; import { tercihProfiliOku, tercihProfiliTamamlandiMi } from "@/features/sihirbaz/sihirbaz-profil";
import { import {
@@ -40,11 +42,27 @@ export type BekleyenProgram = {
* girişsizse giriş CTA paneli; hero-form ile aynı desen). */ * girişsizse giriş CTA paneli; hero-form ile aynı desen). */
export type BekleyenHedef = "sonuc" | "sihirbaz"; export type BekleyenHedef = "sonuc" | "sihirbaz";
/** Kapıyı kimin açtığı — yalnız ölçüm içindir (`kapi_acildi` /
* `kapi_kapatildi` olaylarının `tetik` özelliği), davranışı etkilemez.
* "arti": + butonu · "sonuc": sonuca gitmek isteyen CTA'lar ·
* "sihirbaz": Yapay Zeka niyetli CTA'lar · "duzenle": profil düzenleme. */
export type KapiTetik =
| "arti"
// Profilsiz + artık anında ekliyor; kapı, ekleme toast'ındaki
// "Sıralamamı gir" aksiyonundan açılır ("arti" yeni akışta atılmaz).
| "arti_toast"
| "sonuc"
| "sihirbaz"
| "duzenle"
| "diger";
type KapıDurumu = { type KapıDurumu = {
acik: boolean; acik: boolean;
duzenle: boolean; duzenle: boolean;
/** Tanıtım (giriş) adımını atla, doğrudan sıralama adımıyla aç. */ /** Tanıtım (giriş) adımını atla, doğrudan sıralama adımıyla aç. */
dogrudanSira: boolean; dogrudanSira: boolean;
/** Açık kapıyı kimin açtığı; kapalıyken "diger". */
tetik: KapiTetik;
bekleyen: BekleyenProgram | null; bekleyen: BekleyenProgram | null;
bekleyenHedef: BekleyenHedef | null; bekleyenHedef: BekleyenHedef | null;
profil: TercihProfili | null; profil: TercihProfili | null;
@@ -55,6 +73,7 @@ let durum: KapıDurumu = {
acik: false, acik: false,
duzenle: false, duzenle: false,
dogrudanSira: false, dogrudanSira: false,
tetik: "diger",
bekleyen: null, bekleyen: null,
bekleyenHedef: null, bekleyenHedef: null,
profil: null, profil: null,
@@ -106,6 +125,7 @@ const SSR_DURUM: KapıDurumu = {
acik: false, acik: false,
duzenle: false, duzenle: false,
dogrudanSira: false, dogrudanSira: false,
tetik: "diger",
bekleyen: null, bekleyen: null,
bekleyenHedef: null, bekleyenHedef: null,
profil: null, profil: null,
@@ -133,44 +153,63 @@ export function profilKapisiniKapat() {
acik: false, acik: false,
duzenle: false, duzenle: false,
dogrudanSira: false, dogrudanSira: false,
tetik: "diger",
bekleyen: null, bekleyen: null,
bekleyenHedef: null, bekleyenHedef: null,
}; };
yayinla(); yayinla();
} }
/** Kapının tek açılış noktası: durumu yazar ve `kapi_acildi`'yi atar; her
* açıcı kendi `tetik` değerini verir. (Tamamlanmadan kapanışın olayı
* `kapi_kapatildi` kapı bileşeninden atılır — adımı yalnız o bilir.) */
function kapiyiAc(
yama: Pick<
KapıDurumu,
"duzenle" | "dogrudanSira" | "bekleyen" | "bekleyenHedef"
>,
tetik: KapiTetik,
) {
durum = { ...durum, acik: true, ...yama, tetik };
yayinla();
olay("kapi_acildi", { tetik });
}
export function profilDuzenlemeyiAc() { export function profilDuzenlemeyiAc() {
if (!durum.profilYuklendi) yukle(); if (!durum.profilYuklendi) yukle();
durum = { kapiyiAc(
...durum, {
acik: true, duzenle: true,
duzenle: true, dogrudanSira: false,
dogrudanSira: false, bekleyen: null,
bekleyen: null, bekleyenHedef: null,
bekleyenHedef: null, },
}; "duzenle",
yayinla(); );
} }
/** /**
* Profil yokken kapıyı açar; tamamlanınca `hedef`'e yönlendirilir. * Profil yokken kapıyı açar; tamamlanınca `hedef`'e yönlendirilir.
* Navbar, çekmece +, /sonuc boş hali ve listem gibi yerler bunu kullanır. * Navbar, çekmece +, /sonuc boş hali ve listem gibi yerler bunu kullanır.
* `dogrudanSira` tanıtım adımını atlar — kapı sıralama adımıyla açılır. * `dogrudanSira` tanıtım adımını atlar — kapı sıralama adımıyla açılır.
* `tetik` yalnız ölçüm içindir; verilmezse hedeften türetilir ("sonuc" /
* "sihirbaz"), hedef de yoksa "diger".
*/ */
export function profilToplamaAc( export function profilToplamaAc(
hedef: BekleyenHedef | null = null, hedef: BekleyenHedef | null = null,
dogrudanSira = false, dogrudanSira = false,
tetik?: KapiTetik,
) { ) {
if (!durum.profilYuklendi) yukle(); if (!durum.profilYuklendi) yukle();
durum = { kapiyiAc(
...durum, {
acik: true, duzenle: false,
duzenle: false, dogrudanSira,
dogrudanSira, bekleyen: null,
bekleyen: null, bekleyenHedef: hedef,
bekleyenHedef: hedef, },
}; tetik ?? hedef ?? "diger",
yayinla(); );
} }
/** /**
@@ -200,32 +239,35 @@ export function programEkleIste(opts: {
eklendi: boolean; eklendi: boolean;
zatenVar: boolean; zatenVar: boolean;
doldu: boolean; doldu: boolean;
/** Artık hep false: + kapıyı açmıyor (çağıranlar kırılmasın diye duruyor). */
kapiAcildi: boolean; kapiAcildi: boolean;
/** Program profil (sıralama) olmadan eklendi → risk henüz hesaplanamadı. */
profilsiz: boolean;
} { } {
const { program, kaynak = "sonuc", adaySira, adayTur } = opts; const { program, kaynak = "sonuc", adaySira } = opts;
if (listedeMi(program.id)) { if (listedeMi(program.id)) {
return { eklendi: false, zatenVar: true, doldu: false, kapiAcildi: false }; return {
} eklendi: false,
zatenVar: true,
if (!tercihProfiliTamamlandiMi()) { doldu: false,
durum = { kapiAcildi: false,
...durum, profilsiz: false,
acik: true,
duzenle: false,
dogrudanSira: false,
bekleyen: { program, kaynak, adaySira, adayTur },
bekleyenHedef: null,
}; };
yayinla();
return { eklendi: false, zatenVar: false, doldu: false, kapiAcildi: true };
} }
if (!durum.profilYuklendi) { // [PROTOTİP] Önce ver, sonra iste: profil yokken de program HEMEN listeye
// girer (sıra bilinmiyorsa risk null), kapı açılmaz. Sıralama sorusu
// butondaki toast aksiyonuyla gelir; profil girilince profilTamamlandi →
// riskleriYenile tüm listedeki null riskleri doldurur. Eski davranış
// (bekleyen program + kapı) için bu commit'i geri almak yeterli.
const profilsiz = !tercihProfiliTamamlandiMi();
if (!profilsiz && !durum.profilYuklendi) {
profiliTazele(); profiliTazele();
} }
const profil = durum.profil ?? tercihProfiliOku(); const profil = profilsiz ? null : (durum.profil ?? tercihProfiliOku());
const tercih = programdanManuelTercih( const tercih = programdanManuelTercih(
program, program,
adaySira ?? profil?.sira ?? null, adaySira ?? profil?.sira ?? null,
@@ -233,9 +275,21 @@ export function programEkleIste(opts: {
); );
const ok = ekle(tercih); const ok = ekle(tercih);
if (!ok) { if (!ok) {
return { eklendi: false, zatenVar: false, doldu: true, kapiAcildi: false }; return {
eklendi: false,
zatenVar: false,
doldu: true,
kapiAcildi: false,
profilsiz,
};
} }
return { eklendi: true, zatenVar: false, doldu: false, kapiAcildi: false }; return {
eklendi: true,
zatenVar: false,
doldu: false,
kapiAcildi: false,
profilsiz,
};
} }
/** Profil yazıldıktan sonra; bekleyen hedef varsa yönlendirme href'i döner. /** Profil yazıldıktan sonra; bekleyen hedef varsa yönlendirme href'i döner.
@@ -250,13 +304,21 @@ export function profilTamamlandi(
const bekleyen = durum.bekleyen; const bekleyen = durum.bekleyen;
const hedef = durum.bekleyenHedef; const hedef = durum.bekleyenHedef;
if (bekleyen) { if (bekleyen) {
ekle(programdanManuelTercih(bekleyen.program, profil.sira, bekleyen.kaynak)); const eklendi = ekle(
programdanManuelTercih(bekleyen.program, profil.sira, bekleyen.kaynak),
);
// Kapıdan geçen +: program ancak şimdi listeye girdi. Butondaki
// {yol: "dogrudan"} atışının eşi; liste doluysa/zaten varsa atılmaz.
if (eklendi) {
olay("program_eklendi", { kaynak: bekleyen.kaynak, yol: "kapi" });
}
} }
durum = { durum = {
...durum, ...durum,
acik: false, acik: false,
duzenle: false, duzenle: false,
dogrudanSira: false, dogrudanSira: false,
tetik: "diger",
bekleyen: null, bekleyen: null,
bekleyenHedef: null, bekleyenHedef: null,
profil, profil,

View File

@@ -20,6 +20,9 @@ export type OlayAdi =
| "liste_olusturuldu" | "liste_olusturuldu"
| "liste_guncellendi" | "liste_guncellendi"
| "liste_revize" | "liste_revize"
// Seri kırılması: bu düzeltmenin canlıya çıktığı tarihten önce = +
// tıklaması (kapı açılsa da atılıyordu), sonrası = gerçek ekleme
// ({yol: "dogrudan" | "kapi"}); seriler karşılaştırılamaz.
| "program_eklendi" | "program_eklendi"
| "netler_goruntulendi" | "netler_goruntulendi"
| "danisman_sorusu" | "danisman_sorusu"
@@ -36,7 +39,12 @@ export type OlayAdi =
| "sonuc_goruntulendi" | "sonuc_goruntulendi"
| "paket_goruntulendi" | "paket_goruntulendi"
| "liste_uretim_hata" | "liste_uretim_hata"
| "giris_hata"; | "giris_hata"
// Profil kapısı (sıra + sihirbaz modalı). kapi_acildi {tetik}: her açılış.
// kapi_kapatildi {adim, tetik}: yalnız TAMAMLANMADAN kapanış (X, dışarı
// tıklama, Vazgeç); tamamlanan kapıda atılmaz.
| "kapi_acildi"
| "kapi_kapatildi";
export type Ozellikler = Record<string, string | number>; export type Ozellikler = Record<string, string | number>;
@@ -66,6 +74,10 @@ const DENEME_ARALIGI_MS = 500;
const DENEME_SINIRI = 20; const DENEME_SINIRI = 20;
let kuyruk: Array<[OlayAdi, Ozellikler | undefined]> = []; let kuyruk: Array<[OlayAdi, Ozellikler | undefined]> = [];
// Script'ten önce gelen identify için tek "son değer" slotu: sert yüklemede
// (OAuth / e-posta linki dönüşü) RybbitIdentify effect'i de script'ten önce
// koşar. Kuyruk değil slot — yalnız en son kimlik anlamlıdır.
let bekleyenKimlik: [string, Record<string, string> | undefined] | null = null;
let zamanlayici: ReturnType<typeof setInterval> | null = null; let zamanlayici: ReturnType<typeof setInterval> | null = null;
let denemeSayisi = 0; let denemeSayisi = 0;
let kuyrukKapali = false; let kuyrukKapali = false;
@@ -75,11 +87,21 @@ function zamanlayiciyiDurdur(): void {
zamanlayici = null; zamanlayici = null;
} }
/** Bekleyen olayları geliş sırasıyla gönderir. */ /** Önce bekleyen kimliği uygular, sonra olayları geliş sırasıyla gönderir
* (kuyruktaki olaylar anonim profile değil tanımlı kullanıcıya yazılsın). */
function kuyruguBosalt(rybbit: RybbitApi): void { function kuyruguBosalt(rybbit: RybbitApi): void {
const bekleyenler = kuyruk; const bekleyenler = kuyruk;
const kimlik = bekleyenKimlik;
kuyruk = []; kuyruk = [];
bekleyenKimlik = null;
zamanlayiciyiDurdur(); zamanlayiciyiDurdur();
if (kimlik) {
try {
rybbit.identify(kimlik[0], kimlik[1]);
} catch {
// yut
}
}
for (const [ad, ozellikler] of bekleyenler) { for (const [ad, ozellikler] of bekleyenler) {
try { try {
rybbit.event(ad, ozellikler); rybbit.event(ad, ozellikler);
@@ -89,23 +111,29 @@ function kuyruguBosalt(rybbit: RybbitApi): void {
} }
} }
function kuyrugaEkle(ad: OlayAdi, ozellikler?: Ozellikler): void { /** Script'i bekleyen yoklamayı başlatır (olay kuyruğu ve kimlik slotu ortak). */
if (kuyrukKapali || kuyruk.length >= KUYRUK_SINIRI) return; function zamanlayiciyiBaslat(): void {
kuyruk.push([ad, ozellikler]);
if (zamanlayici !== null) return; if (zamanlayici !== null) return;
zamanlayici = setInterval(() => { zamanlayici = setInterval(() => {
denemeSayisi += 1; denemeSayisi += 1;
if (window.rybbit) { if (window.rybbit) {
kuyruguBosalt(window.rybbit); kuyruguBosalt(window.rybbit);
} else if (denemeSayisi >= DENEME_SINIRI) { } else if (denemeSayisi >= DENEME_SINIRI) {
// Script gelmedi: bekleyenleri at, sonraki olaylar eski no-op'a dönsün // Script gelmedi: bekleyenleri at, sonraki çağrılar eski no-op'a dönsün
kuyruk = []; kuyruk = [];
bekleyenKimlik = null;
kuyrukKapali = true; kuyrukKapali = true;
zamanlayiciyiDurdur(); zamanlayiciyiDurdur();
} }
}, DENEME_ARALIGI_MS); }, DENEME_ARALIGI_MS);
} }
function kuyrugaEkle(ad: OlayAdi, ozellikler?: Ozellikler): void {
if (kuyrukKapali || kuyruk.length >= KUYRUK_SINIRI) return;
kuyruk.push([ad, ozellikler]);
zamanlayiciyiBaslat();
}
/** Özel olay gönderir. Script henüz yoksa kısa süre kuyrukta bekletir. */ /** Özel olay gönderir. Script henüz yoksa kısa süre kuyrukta bekletir. */
export function olay(ad: OlayAdi, ozellikler?: Ozellikler): void { export function olay(ad: OlayAdi, ozellikler?: Ozellikler): void {
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
@@ -114,8 +142,8 @@ export function olay(ad: OlayAdi, ozellikler?: Ozellikler): void {
kuyrugaEkle(ad, ozellikler); kuyrugaEkle(ad, ozellikler);
return; return;
} }
// Sıra bozulmasın: bekleyen varsa önce onlar gider // Sıra bozulmasın: bekleyen (kimlik ya da olay) varsa önce onlar gider
if (kuyruk.length > 0) kuyruguBosalt(window.rybbit); if (kuyruk.length > 0 || bekleyenKimlik) kuyruguBosalt(window.rybbit);
window.rybbit.event(ad, ozellikler); window.rybbit.event(ad, ozellikler);
} catch { } catch {
// Analitik hiçbir koşulda kullanıcı akışını bozmaz // Analitik hiçbir koşulda kullanıcı akışını bozmaz
@@ -166,6 +194,8 @@ export function siraKovasi(sira: number): string {
* Giriş yapan kullanıcıyı eşler. İlk çağrıda o cihazdaki önceki anonim * Giriş yapan kullanıcıyı eşler. İlk çağrıda o cihazdaki önceki anonim
* olaylar geriye dönük olarak bu kullanıcıya bağlanır (sihirbazı doldurup * olaylar geriye dönük olarak bu kullanıcıya bağlanır (sihirbazı doldurup
* sonra giriş yapan öğrencinin yolculuğu tek profilde birleşsin diye). * sonra giriş yapan öğrencinin yolculuğu tek profilde birleşsin diye).
* Script henüz yoksa kimlik tek slotta bekler; kuyruk boşaltılırken
* olaylardan ÖNCE uygulanır.
*/ */
export function kullaniciTanit( export function kullaniciTanit(
kullaniciId: string, kullaniciId: string,
@@ -173,15 +203,25 @@ export function kullaniciTanit(
): void { ): void {
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
try { try {
window.rybbit?.identify(kullaniciId, traits); if (!window.rybbit) {
if (kuyrukKapali) return;
bekleyenKimlik = [kullaniciId, traits];
zamanlayiciyiBaslat();
return;
}
// Doğrudan uygulanır; kuyrukta olay bekliyorsa sonraki boşaltmada bu
// kimlikle gider. Eski slot değeri geçersiz kalmasın diye silinir.
bekleyenKimlik = null;
window.rybbit.identify(kullaniciId, traits);
} catch { } catch {
// yut // yut
} }
} }
/** Çıkışta kimliği temizler. */ /** Çıkışta kimliği temizler (bekleyen kimlik slotu dahil). */
export function kullaniciTemizle(): void { export function kullaniciTemizle(): void {
if (typeof window === "undefined") return; if (typeof window === "undefined") return;
bekleyenKimlik = null;
try { try {
window.rybbit?.clearUserId(); window.rybbit?.clearUserId();
} catch { } catch {