Files
kolaytercih/src/features/rapor/components/tadimlik-satiri.tsx
bilalgursen e3519a50cc [Bilal] content(tadimlik): paragraf maskenin gerçeğini söyler
Buton "24 satırın tamamı" iddiasından arındırılmıştı (2373a9a) ama iki
eleman yukarıdaki paragraf aynı vaadi sürdürüyordu: "24 tercihlik
listenin her satırı bu formatta kurulur". Maske girişsiz/paketsiz
kullanıcıya 3 satır açıyor (rapor-maske.ts:12 ACIK_SATIR), kalan 21'in
gerekçe/risk/trend metni maskeli geliyor.

Cümleye "ilk üçü hemen açılır" eklendi — sayı yazıldı, bulanıklaştırma
ve garanti dili yok. "3" elle yazıldığı için (ACIK_SATIR `server-only`,
bu dosya "use client") tek kaynak bağı koda yorum olarak iliştirildi.

Bulgu: 36-tasarimci-g1-ve-navbar.md B2. Metin yönü Bilal'in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-22 23:21:31 +03:00

220 lines
9.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
// Girişsiz yapay zekâ tadımlığı (PRD İP-B): /sonuc'ta, sıra kovası + (varsa)
// sihirbaz alan tercihine uyan TEK örnek tercih satırı — gerçek rapor
// satırının detay bloğuyla (RaporSatirDetayi) birebir aynı formatta.
// İçerik batch üretilmiş havuzdan gelir; bu bileşen LLM'e istek attırmaz.
// SSR "genel" satırı render eder (prop), mount sonrası sihirbaz kategorisi
// varsa /api/tadimlik'ten kategoriye özel satır çekilip yerinde değiştirilir
// (seçim sunucuda kalır). Havuzda içerik yoksa hiç render edilmez.
import { useEffect, useRef, useState } from "react";
import Link from "next/link";
import { ArrowRight } from "lucide-react";
import { Button } from "@/components/ui/button";
import { UniLogo } from "@/components/uni-logo";
import { SectionEyebrow } from "@/components/pixel-decor";
import { RaporSatirDetayi } from "./rapor-listesi";
import {
ProgramSiraGecmisi,
ProgramTabanTrendi,
programEtkinSira,
raporSiraSerisi,
} from "@/components/program-liste-verileri";
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 { listemUretHref, PROFIL_DEGISTI_EVENT } from "@/features/sihirbaz/sihirbaz-sabitler";
import { tercihProfiliOku } from "@/features/sihirbaz/sihirbaz-profil";
import type { TadimlikSatir, TadimlikTip } from "@/lib/tadimlik-havuzu";
const GENEL = "genel";
/** Program tipi ile sihirbaz tercihi eşleşiyor mu (uniturFiltreSql semantiği). */
function tipeUyar(program: TadimlikSatir["program"], tip: TadimlikTip): boolean {
if (tip === "genel") return true;
if (tip === "devlet") return program.unitur === "DEVLET";
return program.unitur != null && program.unitur !== "DEVLET";
}
export function TadimlikSatiri({
sira,
tur,
ilk,
}: {
sira: number;
tur: string;
/** SSR'da seçilmiş "genel" satır; havuz boşsa null. */
ilk: TadimlikSatir | null;
}) {
const [satir, setSatir] = useState<TadimlikSatir | null>(ilk);
const yuklenenAnahtar = useRef(`${GENEL}|${GENEL}`);
// Sihirbaz kategorisi ve devlet/vakıf tercihi yalnızca localStorage'da
// yaşar; mount'ta ve profil değiştikçe okunur, seçime özel satır sunucudan
// çekilir. Tip tercihi varsa yanlış tipte örnek bir an bile gösterilmez.
useEffect(() => {
const guncelle = () => {
const profil = tercihProfiliOku();
const kategori = profil?.secimler.kategoriler[0];
const slug = kategori ? turkishSlugify(kategori) : GENEL;
const tipTercihi = profil?.secimler.universiteTipi;
const tip: TadimlikTip =
tipTercihi === "devlet" || tipTercihi === "vakif"
? tipTercihi
: GENEL;
const anahtar = `${slug}|${tip}`;
if (anahtar === yuklenenAnahtar.current) return;
yuklenenAnahtar.current = anahtar;
if (slug === GENEL && tip === GENEL) {
setSatir(ilk);
return;
}
// SSR "genel" satırı seçilen tipe uymuyorsa fetch bitene kadar gizle
setSatir((mevcut) =>
mevcut && !tipeUyar(mevcut.program, tip) ? null : mevcut,
);
const q = new URLSearchParams({ sira: String(sira), tur });
if (slug !== GENEL) q.set("kategori", slug);
if (tip !== GENEL) q.set("tip", tip);
fetch(`/api/tadimlik?${q}`)
.then((yanit) => (yanit.ok ? yanit.json() : null))
.then((veri: { tadimlik: TadimlikSatir | null } | null) => {
// Yarış koruması: bu arada başka seçim yüklendiyse dokunma
if (yuklenenAnahtar.current !== anahtar) return;
if (veri?.tadimlik) {
setSatir(veri.tadimlik);
} else if (tip !== GENEL) {
// Uygun tipte satır yok: yanlış tip göstermektense gizle
setSatir(null);
}
})
.catch(() => {});
};
guncelle();
window.addEventListener(PROFIL_DEGISTI_EVENT, guncelle);
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),
});
if (!satir) return null;
const p = satir.program;
const risk = riskHesapla(p.efektifSira, sira) ?? dilimdenRisk(satir.dilim);
const seri = raporSiraSerisi(p.siraGecmisi, p.ilkYil);
const taban = p.efektifSira ?? programEtkinSira(seri);
const devlet = p.unitur === "DEVLET";
// 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 (
// 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, ilk
üçü hemen açılır.
</p>
</div>
<div className="mt-6 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm sm:p-6">
<div className="flex min-w-0 flex-wrap items-center gap-2.5">
<UniLogo ad={p.universite} boy="sm" />
<span className="min-w-0 flex-1">
<span className="block truncate text-sm font-semibold">
{p.isim}
</span>
<span className="mt-0.5 flex items-center gap-1.5 text-xs text-slate-500">
{p.unitur ? (
<span
className={`shrink-0 rounded px-1 py-px text-[10px] font-medium leading-4 ${
devlet
? "bg-slate-100 text-slate-600"
: "bg-violet-50 text-violet-600"
}`}
>
{devlet ? "Devlet" : "Vakıf"}
</span>
) : null}
<span className="truncate">
{p.universite}
{p.il ? ` · ${p.il.toLocaleLowerCase("tr-TR")}` : ""}
</span>
</span>
</span>
<span className="hidden text-right text-xs md:inline-flex">
<ProgramSiraGecmisi program={seri} />
</span>
<span className="inline-flex items-center gap-1 text-xs tabular-nums text-slate-500">
<ProgramTabanTrendi program={seri} />
{taban != null ? `~${taban.toLocaleString("tr-TR")}.` : "—"}
</span>
</div>
<div className="mt-4 rounded-lg bg-slate-50/60 px-4 py-4">
<RaporSatirDetayi
dilim={satir.dilim}
risk={risk}
unitur={p.unitur}
gerekce={satir.gerekce}
riskNotu={satir.riskNotu}
trendOzeti={satir.trendOzeti}
/>
</div>
<div className="mt-5 flex flex-col items-start gap-3 border-t border-slate-100 pt-4 sm:flex-row sm:items-center sm:justify-between">
<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 ·
Giriş sonrası liste hemen üretilir
</span>
</p>
<Button
asChild
className="h-11 w-full shrink-0 cursor-pointer bg-orange-500 text-white transition-[background-color,transform] duration-200 active:scale-[0.97] hover:bg-orange-600 sm:w-auto"
>
<Link
href={girisUrl}
onClick={() => olay("giris_cta_tiklandi", { kaynak: "tadimlik" })}
>
{/* 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>
</div>
</div>
</section>
);
}