feat(liste): ilk + profilsiz ziyaretçide anında eklesin [Bilal onayı gerekir]

PROTOTİP — Bilal telefonda bakıp karar verecek (CEO tur 2, karar #12).
Katalogdan +'ya basan 82 profilsiz oturumun 77'si 5 ekranlık kapıdan
dönüyordu (09-analitik-tur2, S6). Sıra tersine çevrildi: önce ver, sonra
iste.

- programEkleIste: profil yokken programı hemen ekler (sıra bilinmiyorsa
  risk null), kapıyı açmaz; dönüşe `profilsiz` eklendi.
- Buton: program_eklendi {kaynak, yol: "dogrudan", profilli: evet|hayir}.
  Profilsiz eklemede sonner toast (sabit id, 8 sn): "Seçtiklerine eklendi"
  + "Sıralamamı gir →" aksiyonu → profilToplamaAc(null, true,
  "arti_toast") (tanıtım ekranı atlanır). KapiTetik'e "arti_toast".
- Profil kapıdan girilince mevcut profilTamamlandi → riskleriYenile null
  riskleri doldurur (yeni kod yok).

Kapı bileşeni, sihirbaz adımları, SEMA ve meraklisina-icerik değişmedi
(+ kapısı şemada yok → senkron kuralı tetiklenmez). Geri almak için bu
commit'i revert etmek yeterli; K1 ölçümü (gece/p5-kapi-olcum) bağımsız.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-21 09:24:51 +03:00
parent 643e41cc98
commit 1a87d89b5a
2 changed files with 79 additions and 27 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",
@@ -53,10 +58,32 @@ export function ProgramEkleButonu({
} else if (sonuc.zatenVar) { } else if (sonuc.zatenVar) {
toast.info("Bu program zaten seçtiklerinde."); toast.info("Bu program zaten seçtiklerinde.");
} else if (sonuc.eklendi) { } else if (sonuc.eklendi) {
// Yalnız gerçek eklemede: profilsiz + kapıyı açar (kapiAcildi) ve olay("program_eklendi", {
// program o an listeye girmez — o yolun olayı kapı tamamlanınca kaynak,
// profilTamamlandi içinden {yol: "kapi"} ile atılır. yol: "dogrudan",
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"),
},
});
}
} }
} }
@@ -65,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,7 @@
"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";
@@ -45,7 +46,15 @@ export type BekleyenHedef = "sonuc" | "sihirbaz";
* `kapi_kapatildi` olaylarının `tetik` özelliği), davranışı etkilemez. * `kapi_kapatildi` olaylarının `tetik` özelliği), davranışı etkilemez.
* "arti": + butonu · "sonuc": sonuca gitmek isteyen CTA'lar · * "arti": + butonu · "sonuc": sonuca gitmek isteyen CTA'lar ·
* "sihirbaz": Yapay Zeka niyetli CTA'lar · "duzenle": profil düzenleme. */ * "sihirbaz": Yapay Zeka niyetli CTA'lar · "duzenle": profil düzenleme. */
export type KapiTetik = "arti" | "sonuc" | "sihirbaz" | "duzenle" | "diger"; 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;
@@ -230,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,
doldu: false,
kapiAcildi: false,
profilsiz: false,
};
} }
if (!tercihProfiliTamamlandiMi()) { // [PROTOTİP] Önce ver, sonra iste: profil yokken de program HEMEN listeye
kapiyiAc( // girer (sıra bilinmiyorsa risk null), kapı açılmaz. Sıralama sorusu
{ // butondaki toast aksiyonuyla gelir; profil girilince profilTamamlandi →
duzenle: false, // riskleriYenile tüm listedeki null riskleri doldurur. Eski davranış
dogrudanSira: false, // (bekleyen program + kapı) için bu commit'i geri almak yeterli.
bekleyen: { program, kaynak, adaySira, adayTur }, const profilsiz = !tercihProfiliTamamlandiMi();
bekleyenHedef: null,
},
"arti",
);
return { eklendi: false, zatenVar: false, doldu: false, kapiAcildi: true };
}
if (!durum.profilYuklendi) { 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,
@@ -263,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.