merge: origin/gece/p6-arti-aninda-ekle (gece vardiyası 2026-09-21)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bilalgursen
2026-09-21 15:27:27 +03:00
2 changed files with 79 additions and 27 deletions

View File

@@ -1,6 +1,6 @@
"use client";
import { Check, Plus } from "lucide-react";
import { ArrowRight, Check, Plus } from "lucide-react";
import { toast } from "sonner";
import type { Program } from "@/types/yokatlas";
import { olay } from "@/lib/analitik";
@@ -10,7 +10,10 @@ import {
useListedeMi,
type ManuelKaynak,
} 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";
type ProgramOzet = Pick<
@@ -25,6 +28,8 @@ type ProgramOzet = Pick<
| "sira2024"
>;
const PROFILSIZ_EKLEME_TOAST_ID = "profilsiz-ekleme";
export function ProgramEkleButonu({
program,
kaynak = "sonuc",
@@ -53,10 +58,32 @@ export function ProgramEkleButonu({
} else if (sonuc.zatenVar) {
toast.info("Bu program zaten seçtiklerinde.");
} else if (sonuc.eklendi) {
// Yalnız gerçek eklemede: profilsiz + kapıyı açar (kapiAcildi) ve
// program o an listeye girmez — o yolun olayı kapı tamamlanınca
// profilTamamlandi içinden {yol: "kapi"} ile atılır.
olay("program_eklendi", { kaynak, 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"
disabled={listede || doldu}
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}
onFocus={preloadTercihProfiliKapisi}
title={

View File

@@ -1,6 +1,7 @@
"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.
import { useSyncExternalStore } from "react";
@@ -45,7 +46,15 @@ export type BekleyenHedef = "sonuc" | "sihirbaz";
* `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";
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 = {
acik: boolean;
@@ -230,32 +239,35 @@ export function programEkleIste(opts: {
eklendi: boolean;
zatenVar: boolean;
doldu: boolean;
/** Artık hep false: + kapıyı açmıyor (çağıranlar kırılmasın diye duruyor). */
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)) {
return { eklendi: false, zatenVar: true, doldu: false, kapiAcildi: false };
return {
eklendi: false,
zatenVar: true,
doldu: false,
kapiAcildi: false,
profilsiz: false,
};
}
if (!tercihProfiliTamamlandiMi()) {
kapiyiAc(
{
duzenle: false,
dogrudanSira: false,
bekleyen: { program, kaynak, adaySira, adayTur },
bekleyenHedef: null,
},
"arti",
);
return { eklendi: false, zatenVar: false, doldu: false, kapiAcildi: true };
}
// [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 (!durum.profilYuklendi) {
if (!profilsiz && !durum.profilYuklendi) {
profiliTazele();
}
const profil = durum.profil ?? tercihProfiliOku();
const profil = profilsiz ? null : (durum.profil ?? tercihProfiliOku());
const tercih = programdanManuelTercih(
program,
adaySira ?? profil?.sira ?? null,
@@ -263,9 +275,21 @@ export function programEkleIste(opts: {
);
const ok = ekle(tercih);
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.