fix(sonuc): ilk ekran ödeme öncesi tek gerçek önizlemeyi gösterir #5

Merged
bilal merged 8 commits from funnel/sonuc-ilk-ekran into main 2026-09-22 23:33:58 +03:00
6 changed files with 84 additions and 41 deletions

View File

@@ -97,15 +97,30 @@ export default function SonucPage({
return (
<div className="min-h-screen bg-slate-50 text-slate-900">
<main className="mx-auto max-w-4xl px-4 py-12">
{/* Sayfa hiyerarşisi: önce Türkiye haritası, sonra liste. Manuel
liste store'una abone olduğundan tablodan "+" ile eklenenler
buraya canlı yansır. */}
<ManuelHarita />
{/* Sayfa hiyerarşisi: tadımlık → ham tablo → manuel listenin
haritası. Tadımlık, ödeme öncesi gösterebildiğimiz TEK gerçek
önizleme olduğu için ilk ekranda durur.
{/* Yapay Zeka'dan bağımsız ham tablo — haritanın hemen altında,
sayfanın ana gövdesi; puan türü navbar'daki sıralama
rozetinden değiştirilir. Sihirbaz seçimleri (il, devlet/
vakıf) profil çerezinden tabloya uygulanır. */}
Harita neden TABLONUN ALTINDA: liste store'unun sunucu
snapshot'ı her zaman boş liste döner (liste-store.ts), kayıtlı
liste ancak hidrasyondan sonra okunur. Harita yukarıda olsaydı
dönüş ziyaretçisinde sayfa açıldıktan saniyeler sonra ~368 px
yüksekliğinde belirip tadımlığı ve tabloyu aşağı itecekti
(impact fraction ~1, CLS eşiğinin çok üstü). Aşağıda belirince
yalnızca sihirbaz ile dipnotu iter, ikisi de katlanın altında.
Bilgi mimarisi olarak da doğru sıra: harita, tablodan kurulan
listenin görselleştirmesidir. Liste boşken hiç render edilmez
(bkz. manuel-harita.tsx); store'a abone olduğu için tablodan
"+" ile eklenenler buraya canlı yansır. */}
{/* Girişsiz tadımlık: gerçek rapor satırı formatında tek örnek.
Havuzda içerik yoksa bileşen hiç render edilmez. */}
<TadimlikBolumu sira={sira} tur={turKey} />
{/* Yapay Zeka'dan bağımsız ham tablo — sayfanın ana gövdesi;
puan türü navbar'daki sıralama rozetinden değiştirilir.
Sihirbaz seçimleri (il, devlet/vakıf) profil çerezinden
tabloya uygulanır. */}
<SonucProgramTablosu
sira={sira}
tur={turKey}
@@ -113,9 +128,7 @@ export default function SonucPage({
uniturGrubu={uniturGrubu}
/>
{/* Girişsiz tadımlık: gerçek rapor satırı formatında tek örnek.
Havuzda içerik yoksa bileşen hiç render edilmez. */}
<TadimlikBolumu sira={sira} tur={turKey} />
<ManuelHarita />
{/* Sihirbaz akışı (CTA + modal) — üretim ve liste /listem'de
yaşar, burada yalnızca funnel kartları render edilir */}

View File

@@ -64,23 +64,18 @@ export function ManuelHarita() {
};
}, [liste]);
// Liste boşken 197 KB'lık il geometrisi hiç inmesin: aynı oranlı
// placeholder alanı rezerve eder, ilk eklemede lazy harita yerine oturur.
if (liste.length === 0) {
return (
<div>
<div className="flex aspect-[1050/447] w-full items-center justify-center rounded-xl bg-slate-100/60">
<p className="max-w-xs px-4 text-center text-xs text-slate-500">
Tablodan <span aria-hidden>+</span> ile ekledikçe üniversiteler
haritada belirir.
</p>
</div>
</div>
);
}
// Liste boşken hiç yer tutulmaz: boş harita /sonuc'un ilk ekranını işgal
// ediyordu, oysa ödeme öncesi ilk ekranda durması gereken şey tadımlık.
// İlk "+" ile liste dolunca harita kendiliğinden belirir; 197 KB'lık il
// geometrisi yine ancak o anda iner (TercihHaritasiLazy zaten `dynamic`).
if (liste.length === 0) return null;
// mt-16: /sonuc'ta harita artık tablonun ALTINDA duruyor (bkz. sonuc/page.tsx)
// ve sayfanın bölüm ritmini paylaşıyor — tablo <section>'ı ve sihirbaz
// kartları da mt-16 kullanır. Eskiden main'in ilk çocuğu olduğu için üst
// boşluğu main'in py-12'sinden alıyordu, kendi margin'i yoktu.
return (
<div>
<div className="mt-16">
<TercihHaritasiLazy
pinler={pinler}
seciliIl={seciliIl}

View File

@@ -25,7 +25,7 @@ import { useGorunumOlayi } from "@/components/use-gorunum-olayi";
import { olay, siraKovasi } from "@/lib/analitik";
import { dilimdenRisk, riskHesapla } from "@/lib/risk";
import { turkishSlugify } from "@/lib/slug";
import { PROFIL_DEGISTI_EVENT, sonucHref } from "@/features/sihirbaz/sihirbaz-sabitler";
import { listemUretHref, PROFIL_DEGISTI_EVENT } from "@/features/sihirbaz/sihirbaz-sabitler";
import { tercihProfiliOku } from "@/features/sihirbaz/sihirbaz-profil";
import type { TadimlikSatir, TadimlikTip } from "@/lib/tadimlik-havuzu";
@@ -97,6 +97,13 @@ export function TadimlikSatiri({
return () => window.removeEventListener(PROFIL_DEGISTI_EVENT, guncelle);
}, [sira, tur, ilk]);
// SERİ KIRILMASI (22 Eyl 2026): bu olay bu değişiklikten önce /sonuc'un
// ~2.200 px altında, tablonun arkasında yaşıyordu; artık tadımlık ilk
// ekranda. Adet bir gecede 0'dan yukarı fırlayacak — kırık ölçüm değil,
// kazanılan payda. Öncesi ve sonrası KARŞILAŞTIRILAMAZ; anlamlı ölçüt
// bundan sonra "tadımlık CTA tıklaması / tadımlık görüntülenmesi" oranıdır.
// (Not analitik.ts'e değil buraya yazıldı: o dosya aynı gece başka bir
// dalın altında — CTO şartnamesi, çakışma kararı #1.)
const gorunumRef = useGorunumOlayi<HTMLElement>("tadimlik_goruntulendi", {
kova: siraKovasi(sira),
});
@@ -108,18 +115,32 @@ export function TadimlikSatiri({
const seri = raporSiraSerisi(p.siraGecmisi, p.ilkYil);
const taban = p.efektifSira ?? programEtkinSira(seri);
const devlet = p.unitur === "DEVLET";
const girisUrl = `/giris?callback=${encodeURIComponent(sonucHref())}`;
// Giriş dönüşü kullanıcıyı /sonuc'a geri bırakmaz: callback doğrudan
// üretimin koştuğu /listem'e iner, liste ek tık olmadan orada başlar.
const girisUrl = `/giris?callback=${encodeURIComponent(listemUretHref("tadimlik"))}`;
return (
<section ref={gorunumRef} className="mt-16 min-w-0">
// first:mt-0 — /sonuc'ta bu <section> main'in ilk DOM çocuğudur (harita
// artık tablonun altında, üstte hiçbir kardeş yok; JSX yorumları düğüm
// üretmez): main'in py-12'si tek başına yeterli üst boşluğu verir.
// Tadımlığın render edilmediği durumlarda (girişli kullanıcı, boş havuz)
// bu bileşen hiç dönmediği için kural ilk gerçek bölüme geçer.
<section ref={gorunumRef} className="mt-16 min-w-0 first:mt-0">
<div className="flex flex-col items-center text-center">
<SectionEyebrow>Yapay zekâdan örnek satır</SectionEyebrow>
<h2 className="mt-4 font-heading text-2xl font-bold sm:text-3xl">
Yapay zekâ listendeki her satırı böyle anlatır
</h2>
{/* "ilk üçü" = rapor-maske.ts içindeki ACIK_SATIR (3). O dosya
`server-only`, bu dosya "use client" — sabit buraya import
edilemiyor, sayı elle yazıldı. ACIK_SATIR değişirse bu cümle de
aynı commit'te değişmeli. Cümle kasten "her satır bu formatta
kurulur"u sınırlıyor: maske girişsiz/paketsiz kullanıcıya 3 satır
açıyor, kalan 21'in gerekçe/risk/trend metni maskeli geliyor. */}
<p className="mx-auto mt-3 max-w-xl text-slate-600">
Sıralamana uygun bir programın gerekçesi, yerleşme riski ve taban
trendi — 24 tercihlik listenin her satırı bu formatta kurulur.
trendi — 24 tercihlik listenin her satırı bu formatta kurulur, ilk
üçü hemen açılır.
</p>
</div>
@@ -172,7 +193,8 @@ export function TadimlikSatiri({
<p className="text-sm text-slate-600">
Bu, 24 satırlık Yapay Zeka listenden yalnızca bir örnek.
<span className="block text-xs text-slate-500">
Kredi kartı gerekmez · Google ya da e-posta ile 30 saniyede
Kredi kartı gerekmez · Google ya da e-posta ile 30 saniyede ·
Giriş sonrası liste hemen üretilir
</span>
</p>
<Button
@@ -183,7 +205,10 @@ export function TadimlikSatiri({
href={girisUrl}
onClick={() => olay("giris_cta_tiklandi", { kaynak: "tadimlik" })}
>
24 satırın tamamı için giriş yap — 5 deneme kredin hazır
{/* Buton metni listenin tamamını vaat etmez: maske girişsiz
kullanıcıya 24 satırdan yalnız ilk birkaçının analizini
açıyor (rapor-maske.ts), gerisi teslim edilmiyor. */}
Kendi listemi oluştur
<ArrowRight className="size-4" aria-hidden />
</Link>
</Button>

View File

@@ -29,6 +29,8 @@ const IZINLI_KAYNAKLAR = new Set([
"giris_donusu",
"bos_durum",
"odeme_sonrasi",
// /sonuc'taki girişsiz tadımlık CTA'sı (tadimlik-satiri.tsx).
"tadimlik",
// /sonuc'ta ?sihirbaz=1 ile otomatik başlayan üretimin gerçek yüzeyleri.
// Eskiden hepsi "giris_donusu" sayılıyordu (güvenlik denetimi G3).
"hero",

View File

@@ -11,20 +11,14 @@ import { PixelField } from "@/components/pixel-decor";
import { useGorunumOlayi } from "@/components/use-gorunum-olayi";
import { olay, olayBirKez, siraKovasi } from "@/lib/analitik";
import type { SihirbazFacetleri } from "@/types/yokatlas";
import { SIHIRBAZ_AC_EVENT, SIHIRBAZ_STORAGE_KEY, sihirbazDogrula, type SihirbazSecimleri } from "@/features/sihirbaz/sihirbaz-sabitler";
import { listemUretHref, SIHIRBAZ_AC_EVENT, SIHIRBAZ_STORAGE_KEY, sihirbazDogrula, type SihirbazSecimleri } from "@/features/sihirbaz/sihirbaz-sabitler";
import { tercihProfiliYaz } from "@/features/sihirbaz/sihirbaz-profil";
import { SihirbazModal } from "./sihirbaz-modal";
// Ü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.
// `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}`;
}
// Hedef URL'i `listemUretHref` kurar (sihirbaz-sabitler.ts) — `uret=1`
// parametre sırası sözleşmesi ve izinli `kaynak` değerleri orada yazılı.
// SSR'da false, client'ta true — storage'a ancak mount sonrası dokunuruz
const bosAbone = () => () => {};

View File

@@ -168,6 +168,20 @@ export function sonucHref(ekstra?: Record<string, string>): string {
return `/sonuc?${new URLSearchParams(girdiler)}`;
}
/**
* Üretimin koştuğu /listem URL'i. `kaynak` denemenin hangi yüzeyden
* başladığını ölçer (izinli değerler:
* rapor/components/uretim-basladi-olayi.tsx — yeni yüzeyi önce oraya ekle).
*
* `uret=1` HER ZAMAN ilk parametre kalmalı: giris-karti.tsx funnel metnini
* `callbackURL.startsWith("/listem?uret=1")` ile seçiyor, sıra bozulursa
* girişteki "Seçimlerin hazır" metni sessizce kaybolur. Bu sözleşmenin
* literal kopyası çoğalmasın diye tek kaynak burası.
*/
export function listemUretHref(kaynak: string): string {
return `/listem?uret=1&kaynak=${kaynak}`;
}
/** Prompt'a enjekte edilecek Türkçe özet. */
export function secimOzeti(secimler: SihirbazSecimleri): string {
const satirlar = [