merge(olcum/liste-uretim-basladi): feat(olcum): /listem?uret=1 yollarına kaynak parametresi
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,12 +20,18 @@ export const metadata: Metadata = {
|
|||||||
export default function ListemPage({
|
export default function ListemPage({
|
||||||
searchParams,
|
searchParams,
|
||||||
}: {
|
}: {
|
||||||
searchParams: Promise<{ acildi?: string; uret?: string }>;
|
// kaynak: üretim denemesini hangi yüzeyin başlattığı (bkz.
|
||||||
|
// uretim-basladi-olayi.tsx). Doğrulaması okuyan tarafta, allowlist ile.
|
||||||
|
searchParams: Promise<{ acildi?: string; uret?: string; kaynak?: string }>;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<ListemIcerikSkeleton />}>
|
<Suspense fallback={<ListemIcerikSkeleton />}>
|
||||||
{searchParams.then(({ acildi, uret }) => (
|
{searchParams.then(({ acildi, uret, kaynak }) => (
|
||||||
<ListemIcerik uret={uret === "1"} acildi={acildi === "1"} />
|
<ListemIcerik
|
||||||
|
uret={uret === "1"}
|
||||||
|
acildi={acildi === "1"}
|
||||||
|
kaynak={kaynak}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ async function PaketCta() {
|
|||||||
{/* ?uret=1: rapor varsa /listem raporu gösterir (bayrak yok
|
{/* ?uret=1: rapor varsa /listem raporu gösterir (bayrak yok
|
||||||
sayılır), yoksa bekleyen seçimlerle üretim başlar; seçim
|
sayılır), yoksa bekleyen seçimlerle üretim başlar; seçim
|
||||||
de yoksa boş durum sihirbaza yönlendirir. */}
|
de yoksa boş durum sihirbaza yönlendirir. */}
|
||||||
<Link href="/listem?uret=1" className="underline">
|
<Link href="/listem?uret=1&kaynak=odeme_sonrasi" className="underline">
|
||||||
listeni oluştur
|
listeni oluştur
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import type { Mesaj } from "./sohbet-client";
|
|||||||
import { ListemGovde } from "./listem-govde";
|
import { ListemGovde } from "./listem-govde";
|
||||||
import { ListemBosCta } from "./listem-bos-cta";
|
import { ListemBosCta } from "./listem-bos-cta";
|
||||||
import { ListeUretici } from "./liste-uretici";
|
import { ListeUretici } from "./liste-uretici";
|
||||||
|
import { UretimBasladiOlayi } from "./uretim-basladi-olayi";
|
||||||
|
|
||||||
const PAKET_FIYATI = (URUNLER.paket.amountKurus / 100).toLocaleString("tr-TR", {
|
const PAKET_FIYATI = (URUNLER.paket.amountKurus / 100).toLocaleString("tr-TR", {
|
||||||
maximumFractionDigits: 0,
|
maximumFractionDigits: 0,
|
||||||
@@ -28,9 +29,13 @@ const PAKET_FIYATI = (URUNLER.paket.amountKurus / 100).toLocaleString("tr-TR", {
|
|||||||
export async function ListemIcerik({
|
export async function ListemIcerik({
|
||||||
uret,
|
uret,
|
||||||
acildi,
|
acildi,
|
||||||
|
kaynak,
|
||||||
}: {
|
}: {
|
||||||
uret: boolean;
|
uret: boolean;
|
||||||
acildi: boolean;
|
acildi: boolean;
|
||||||
|
/** Üretim denemesini başlatan yüzey (`?kaynak=`); doğrulaması
|
||||||
|
* UretimBasladiOlayi'nın allowlist'inde. */
|
||||||
|
kaynak?: string;
|
||||||
}) {
|
}) {
|
||||||
// Tercihleri girili olan (profil çerezi duran) ziyaretçi girişten dönünce
|
// Tercihleri girili olan (profil çerezi duran) ziyaretçi girişten dönünce
|
||||||
// "Henüz listen yok" boşluğuyla karşılaşmasın: callback ?uret=1 taşır,
|
// "Henüz listen yok" boşluğuyla karşılaşmasın: callback ?uret=1 taşır,
|
||||||
@@ -39,7 +44,14 @@ export async function ListemIcerik({
|
|||||||
const cerezProfili = cerezdenTercihProfili(
|
const cerezProfili = cerezdenTercihProfili(
|
||||||
(await cookies()).get(PROFIL_CEREZ_ADI)?.value,
|
(await cookies()).get(PROFIL_CEREZ_ADI)?.value,
|
||||||
);
|
);
|
||||||
const geriYol = uret || cerezProfili ? "/listem?uret=1" : "/listem";
|
// `kaynak` giriş round-trip'inde korunur; yoksa girişten dönen herkes
|
||||||
|
// "bilinmiyor" okunurdu. `uret=1` HER ZAMAN ilk parametre kalmalı:
|
||||||
|
// giris-karti.tsx:31 funnel metnini `startsWith("/listem?uret=1")` ile
|
||||||
|
// seçiyor, sıra bozulursa "Seçimlerin hazır" metni kaybolur.
|
||||||
|
const geriYol =
|
||||||
|
uret || cerezProfili
|
||||||
|
? `/listem?uret=1${kaynak ? `&kaynak=${encodeURIComponent(kaynak)}` : ""}`
|
||||||
|
: "/listem";
|
||||||
const session = await verifySession(geriYol);
|
const session = await verifySession(geriYol);
|
||||||
const [user, satir, sohbetGecmisi] = await Promise.all([
|
const [user, satir, sohbetGecmisi] = await Promise.all([
|
||||||
getCurrentUser(),
|
getCurrentUser(),
|
||||||
@@ -71,11 +83,20 @@ export async function ListemIcerik({
|
|||||||
{/* ?uret=1: sihirbazdan yönlendirildi — bekleyen seçimlerle üretim
|
{/* ?uret=1: sihirbazdan yönlendirildi — bekleyen seçimlerle üretim
|
||||||
burada koşar, animasyon ve hata/kredi durumları burada yaşar. */}
|
burada koşar, animasyon ve hata/kredi durumları burada yaşar. */}
|
||||||
{uret ? (
|
{uret ? (
|
||||||
<ListeUretici
|
<>
|
||||||
hasPaket={Boolean(user.hasPaket)}
|
{/* Denemenin paydası: üretim gövdesiyle kardeş, aynı koşulda
|
||||||
sunucuProfil={cerezProfili}
|
mount olur (bkz. uretim-basladi-olayi.tsx). */}
|
||||||
bosDurum={bosDurum}
|
<UretimBasladiOlayi
|
||||||
/>
|
kaynak={kaynak ?? "bilinmiyor"}
|
||||||
|
paketli={Boolean(user.hasPaket)}
|
||||||
|
sunucuProfil={cerezProfili}
|
||||||
|
/>
|
||||||
|
<ListeUretici
|
||||||
|
hasPaket={Boolean(user.hasPaket)}
|
||||||
|
sunucuProfil={cerezProfili}
|
||||||
|
bosDurum={bosDurum}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
bosDurum
|
bosDurum
|
||||||
)}
|
)}
|
||||||
|
|||||||
70
src/features/rapor/components/uretim-basladi-olayi.tsx
Normal file
70
src/features/rapor/components/uretim-basladi-olayi.tsx
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
// Üretim denemesinin paydasını ölçen görünmez bileşen: /listem?uret=1 ile
|
||||||
|
// üretim gövdesi kurulduğunda `liste_uretim_basladi` atar. Bugüne kadar
|
||||||
|
// yalnız başarı (`liste_olusturuldu`) sayıldığı için "kaç kişi denedi, kaçı
|
||||||
|
// listeye ulaştı" sorusunun paydası yoktu.
|
||||||
|
//
|
||||||
|
// Olay bilerek üretimi başlatan `liste-uretici.tsx`'te değil, onun kardeşi
|
||||||
|
// olan bu dosyada duruyor (o dosya bu turda başka bir işin altında). İkisi de
|
||||||
|
// `listem-icerik.tsx`'te aynı `uret` dalında mount edildiğinden koşulları
|
||||||
|
// birebir aynı: biri mount oluyorsa diğeri de oluyor.
|
||||||
|
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { olay } from "@/lib/analitik";
|
||||||
|
import type { TercihProfili } from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||||
|
import { tercihProfiliOku } from "@/features/sihirbaz/sihirbaz-profil";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `?kaynak=` sözleşmesinin izinli değerleri. Değer URL'den, yani kullanıcının
|
||||||
|
* denetiminden geliyor: allowlist olmadan Rybbit'e keyfi dizgi yazılabilir
|
||||||
|
* (kardinalite patlaması, panele enjekte edilen metin). Listede olmayan her
|
||||||
|
* şey `"bilinmiyor"`a düşer. Yeni bir üretim yüzeyi eklerken değerini önce
|
||||||
|
* buraya ekle, sonra href'e koy.
|
||||||
|
*/
|
||||||
|
const IZINLI_KAYNAKLAR = new Set([
|
||||||
|
"sihirbaz",
|
||||||
|
"hazir_cta",
|
||||||
|
"giris_donusu",
|
||||||
|
"bos_durum",
|
||||||
|
"odeme_sonrasi",
|
||||||
|
"bilinmiyor",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function UretimBasladiOlayi({
|
||||||
|
kaynak,
|
||||||
|
paketli,
|
||||||
|
sunucuProfil,
|
||||||
|
}: {
|
||||||
|
/** `?kaynak=` ham değeri; allowlist dışındaysa "bilinmiyor" raporlanır. */
|
||||||
|
kaynak: string;
|
||||||
|
paketli: boolean;
|
||||||
|
/** Profil çerezinden SSR'da çözülen yedek profil (bkz. listem-icerik). */
|
||||||
|
sunucuProfil: TercihProfili | null;
|
||||||
|
}) {
|
||||||
|
// Mount başına tek atış. StrictMode geliştirmede effect'i iki kez koşuyor;
|
||||||
|
// `liste-uretici.tsx`'teki `basladi` ref'iyle aynı desen. `olayBirKez`
|
||||||
|
// BİLEREK kullanılmıyor: o oturum başına tekilleştirir, oysa aynı oturumda
|
||||||
|
// ikinci deneme de paydaya girmeli.
|
||||||
|
const atildi = useRef(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (atildi.current) return;
|
||||||
|
atildi.current = true;
|
||||||
|
const kaynakGuvenli = IZINLI_KAYNAKLAR.has(kaynak) ? kaynak : "bilinmiyor";
|
||||||
|
olay("liste_uretim_basladi", {
|
||||||
|
kaynak: kaynakGuvenli,
|
||||||
|
paketli: paketli ? "evet" : "hayir",
|
||||||
|
// Üretimin gerçekten başlayabilmesinin koşulu: bu ifade
|
||||||
|
// `liste-uretici.tsx`'teki `tercihProfiliOku() ?? sunucuProfil`
|
||||||
|
// ifadesinin ikizidir — ikisi ayrı dosyada olduğu için elle eşit
|
||||||
|
// tutulmalı. O dosya açıldığında (bkz. backlog: olayın gerçek çağrı
|
||||||
|
// yerine taşınması) tek yere alınacak. Ham sıralama/seçim GÖNDERİLMEZ
|
||||||
|
// (bkz. lib/analitik.ts kuralı) — yalnız profilin varlığı.
|
||||||
|
profil: (tercihProfiliOku() ?? sunucuProfil) ? "var" : "yok",
|
||||||
|
});
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
@@ -17,7 +17,14 @@ import { SihirbazModal } from "./sihirbaz-modal";
|
|||||||
|
|
||||||
// Üretim /listem'de koşar (bkz. listem/liste-uretici.tsx); sihirbaz
|
// Üretim /listem'de koşar (bkz. listem/liste-uretici.tsx); sihirbaz
|
||||||
// tamamlanınca seçimler storage'a yazılıp kullanıcı buraya yönlendirilir.
|
// tamamlanınca seçimler storage'a yazılıp kullanıcı buraya yönlendirilir.
|
||||||
const LISTEM_URET_HREF = "/listem?uret=1";
|
// `kaynak` denemenin hangi yüzeyden başladığını ölçer (bkz.
|
||||||
|
// rapor/components/uretim-basladi-olayi.tsx — izinli değerler orada).
|
||||||
|
// `uret=1` HER ZAMAN ilk parametre kalmalı: giris-karti.tsx:31 funnel
|
||||||
|
// metnini `callbackURL.startsWith("/listem?uret=1")` ile seçiyor, sıra
|
||||||
|
// bozulursa girişteki "Seçimlerin hazır" metni kaybolur.
|
||||||
|
function listemUretHref(kaynak: string) {
|
||||||
|
return `/listem?uret=1&kaynak=${kaynak}`;
|
||||||
|
}
|
||||||
|
|
||||||
// SSR'da false, client'ta true — storage'a ancak mount sonrası dokunuruz
|
// SSR'da false, client'ta true — storage'a ancak mount sonrası dokunuruz
|
||||||
const bosAbone = () => () => {};
|
const bosAbone = () => () => {};
|
||||||
@@ -182,10 +189,12 @@ export function SihirbazBolumu({
|
|||||||
await modalCikisiniOynat();
|
await modalCikisiniOynat();
|
||||||
setElleAcik(false);
|
setElleAcik(false);
|
||||||
if (!girisliMi) {
|
if (!girisliMi) {
|
||||||
router.push(`/giris?callback=${encodeURIComponent(LISTEM_URET_HREF)}`);
|
router.push(
|
||||||
|
`/giris?callback=${encodeURIComponent(listemUretHref("sihirbaz"))}`,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
router.push(LISTEM_URET_HREF);
|
router.push(listemUretHref("sihirbaz"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -213,7 +222,7 @@ export function SihirbazBolumu({
|
|||||||
if (otomatikGonder && !otomatikBasladi.current) {
|
if (otomatikGonder && !otomatikBasladi.current) {
|
||||||
otomatikBasladi.current = true;
|
otomatikBasladi.current = true;
|
||||||
// replace: geri tuşu bu sayfaya dönünce tekrar /listem'e sekmesin
|
// replace: geri tuşu bu sayfaya dönünce tekrar /listem'e sekmesin
|
||||||
router.replace(LISTEM_URET_HREF);
|
router.replace(listemUretHref("giris_donusu"));
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [otomatikGonder]);
|
}, [otomatikGonder]);
|
||||||
@@ -242,7 +251,7 @@ export function SihirbazBolumu({
|
|||||||
// kaydedilmesi olarak sunulur. Girişten dönüşte üretim /listem'de başlar.
|
// kaydedilmesi olarak sunulur. Girişten dönüşte üretim /listem'de başlar.
|
||||||
// Sinyal URL'den (?hazir=1) gelir ki SSR'da doğru panel render edilsin.
|
// Sinyal URL'den (?hazir=1) gelir ki SSR'da doğru panel render edilsin.
|
||||||
const girisBekliyor = Boolean(!girisliMi && secimlerHazir && !raporVar);
|
const girisBekliyor = Boolean(!girisliMi && secimlerHazir && !raporVar);
|
||||||
const girisUrl = `/giris?callback=${encodeURIComponent(LISTEM_URET_HREF)}`;
|
const girisUrl = `/giris?callback=${encodeURIComponent(listemUretHref("hazir_cta"))}`;
|
||||||
// Kapı 1 ölçümü: panel yalnızca girisBekliyor dalında render edilir; ref
|
// Kapı 1 ölçümü: panel yalnızca girisBekliyor dalında render edilir; ref
|
||||||
// başka dalda bağlanmadığından observer da yalnızca o durumda kurulur.
|
// başka dalda bağlanmadığından observer da yalnızca o durumda kurulur.
|
||||||
const hazirPanelRef = useGorunumOlayi<HTMLElement>("hazir_panel_goruntulendi");
|
const hazirPanelRef = useGorunumOlayi<HTMLElement>("hazir_panel_goruntulendi");
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ export type OlayAdi =
|
|||||||
| "sihirbaz_adim"
|
| "sihirbaz_adim"
|
||||||
| "sihirbaz_tamamlandi"
|
| "sihirbaz_tamamlandi"
|
||||||
| "giris_denendi"
|
| "giris_denendi"
|
||||||
|
// Üretim denemesinin paydası: /listem?uret=1 ile üretim gövdesi mount
|
||||||
|
// olduğunda atılır (sihirbaz, hazır panel CTA'sı, giriş dönüşü, boş durum
|
||||||
|
// CTA'sı ve ödeme sonrası kartı — hangisi olduğu `kaynak` özelliğinde).
|
||||||
|
// Bilerek `olayBirKez` KULLANILMAZ: aynı oturumdaki ikinci deneme de
|
||||||
|
// sayılmalı, yoksa payda başarıdan (liste_olusturuldu) küçük kalır.
|
||||||
|
| "liste_uretim_basladi"
|
||||||
| "liste_olusturuldu"
|
| "liste_olusturuldu"
|
||||||
| "liste_guncellendi"
|
| "liste_guncellendi"
|
||||||
| "liste_revize"
|
| "liste_revize"
|
||||||
|
|||||||
Reference in New Issue
Block a user