feat(analitik): kapi_acildi + kapi_kapatildi olayları

Profil kapısının açılışı ve tamamlanmadan kapanışı doğrudan ölçülmüyordu;
"82 oturum kapıyı gördü" kod davranışından çıkarımdı (09-analitik-tur2).
İki yeni olay (adlar kalıcı sözleşme):

- kapi_acildi {tetik: arti|sonuc|sihirbaz|duzenle|diger}: depoda tek
  açılış noktası (kapiyiAc). Her açıcı kendi tetik'ini yazar:
  programEkleIste → arti, profilDuzenlemeyiAc → duzenle, profilToplamaAc →
  isteğe bağlı 3. parametre, verilmezse hedeften (sonuc/sihirbaz), o da
  yoksa diger. Depoya `tetik` alanı eklendi.
- kapi_kapatildi {adim: giris|sira|sihirbaz, tetik}: yalnız TAMAMLANMADAN
  kapanışta; kapı bileşeninde tek noktadan (vazgec) — X, dışarı tıklama,
  Esc ve "Vazgeç" butonları. Tamamlanan kapı profilTamamlandi ile depodan
  kapanır, bu noktaya uğramaz.

Ham sıra/e-posta olaylara girmez. Davranış değişmez.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-21 05:25:23 +03:00
parent 4b87a3e71e
commit 67be93c394
3 changed files with 109 additions and 37 deletions

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

@@ -41,11 +41,19 @@ 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" | "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;
@@ -56,6 +64,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,
@@ -107,6 +116,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,
@@ -134,44 +144,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,
}; },
yayinla(); "duzenle",
);
} }
/** /**
* 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,
}; },
yayinla(); tetik ?? hedef ?? "diger",
);
} }
/** /**
@@ -210,15 +239,15 @@ export function programEkleIste(opts: {
} }
if (!tercihProfiliTamamlandiMi()) { if (!tercihProfiliTamamlandiMi()) {
durum = { kapiyiAc(
...durum, {
acik: true,
duzenle: false, duzenle: false,
dogrudanSira: false, dogrudanSira: false,
bekleyen: { program, kaynak, adaySira, adayTur }, bekleyen: { program, kaynak, adaySira, adayTur },
bekleyenHedef: null, bekleyenHedef: null,
}; },
yayinla(); "arti",
);
return { eklendi: false, zatenVar: false, doldu: false, kapiAcildi: true }; return { eklendi: false, zatenVar: false, doldu: false, kapiAcildi: true };
} }
@@ -265,6 +294,7 @@ export function profilTamamlandi(
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

@@ -39,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>;