641 lines
22 KiB
TypeScript
641 lines
22 KiB
TypeScript
"use client";
|
||
|
||
// /meraklisina'nın canlı anlatım katmanı: kullanıcı kendi sıralamasını girer,
|
||
// dilim şeridi ve kademeli filtre hunisi o sıralamanın GERÇEK sayılarıyla
|
||
// güncellenir. Provider sayfadaki metin bölümlerini (RSC children) sarar;
|
||
// görseller context'ten okur, böylece tek girişle iki diyagram birden yaşar.
|
||
|
||
import {
|
||
createContext,
|
||
useContext,
|
||
useEffect,
|
||
useRef,
|
||
useState,
|
||
type ReactNode,
|
||
} from "react";
|
||
import type { DilimKey, SihirbazFacetleri } from "@/lib/db";
|
||
|
||
const TUR_SECENEKLERI = [
|
||
{ deger: "say", etiket: "Sayısal" },
|
||
{ deger: "ea", etiket: "Eşit Ağırlık" },
|
||
{ deger: "soz", etiket: "Sözel" },
|
||
{ deger: "dil", etiket: "Dil" },
|
||
{ deger: "tyt", etiket: "TYT" },
|
||
] as const;
|
||
|
||
export type DemoVeri = {
|
||
facetler: SihirbazFacetleri;
|
||
dilimToplam: Record<DilimKey, number>;
|
||
};
|
||
|
||
type DemoDurum = {
|
||
sira: number;
|
||
tur: string;
|
||
veri: DemoVeri;
|
||
yukleniyor: boolean;
|
||
// Huni seçimleri ve kademeli facet cevapları
|
||
kategori: string | null;
|
||
il: string | null;
|
||
tip: "devlet" | "vakif" | null;
|
||
kategoriFacet: SihirbazFacetleri | null; // ?kategori= cevabı (iller buradan)
|
||
ilFacet: SihirbazFacetleri | null; // ?kategori&il cevabı (tipler buradan)
|
||
kategoriSec: (k: string | null) => void;
|
||
ilSec: (i: string | null) => void;
|
||
tipSec: (t: "devlet" | "vakif" | null) => void;
|
||
siraDegistir: (ham: string) => void;
|
||
turDegistir: (t: string) => void;
|
||
siraMetni: string;
|
||
};
|
||
|
||
const DemoContext = createContext<DemoDurum | null>(null);
|
||
|
||
function useDemo(): DemoDurum {
|
||
const ctx = useContext(DemoContext);
|
||
if (!ctx) throw new Error("Demo bileşenleri SiralamaDemo içinde kullanılmalı");
|
||
return ctx;
|
||
}
|
||
|
||
async function facetGetir(
|
||
sira: number,
|
||
tur: string,
|
||
secim: { kategori?: string; il?: string; dilimler?: boolean },
|
||
signal: AbortSignal,
|
||
): Promise<{ facetler: SihirbazFacetleri; dilimToplam?: Record<DilimKey, number> } | null> {
|
||
const params = new URLSearchParams({ sira: String(sira), tur });
|
||
if (secim.kategori) params.append("kategori", secim.kategori);
|
||
if (secim.il) params.append("il", secim.il);
|
||
if (secim.dilimler) params.set("dilimler", "1");
|
||
try {
|
||
const res = await fetch(`/api/facetler?${params}`, { signal });
|
||
if (!res.ok) return null;
|
||
return (await res.json()) as {
|
||
facetler: SihirbazFacetleri;
|
||
dilimToplam?: Record<DilimKey, number>;
|
||
};
|
||
} catch {
|
||
return null;
|
||
}
|
||
}
|
||
|
||
export function SiralamaDemo({
|
||
baslangicSira,
|
||
baslangicVeri,
|
||
children,
|
||
}: {
|
||
baslangicSira: number;
|
||
baslangicVeri: DemoVeri;
|
||
children: ReactNode;
|
||
}) {
|
||
const [siraMetni, setSiraMetni] = useState(
|
||
baslangicSira.toLocaleString("tr-TR"),
|
||
);
|
||
const [sira, setSira] = useState(baslangicSira);
|
||
const [tur, setTur] = useState("say");
|
||
const [veri, setVeri] = useState(baslangicVeri);
|
||
const [yukleniyor, setYukleniyor] = useState(false);
|
||
const [kategori, setKategori] = useState<string | null>(null);
|
||
const [il, setIl] = useState<string | null>(null);
|
||
const [tip, setTip] = useState<"devlet" | "vakif" | null>(null);
|
||
const [kategoriFacet, setKategoriFacet] = useState<SihirbazFacetleri | null>(
|
||
null,
|
||
);
|
||
const [ilFacet, setIlFacet] = useState<SihirbazFacetleri | null>(null);
|
||
const zamanlayici = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||
|
||
// Sıralama/tür değişti → ana veriyi tazele, huni seçimlerini sıfırla
|
||
useEffect(() => {
|
||
if (sira === baslangicSira && tur === "say" && veri === baslangicVeri) {
|
||
return; // ilk render: sunucudan gelen veri zaten güncel
|
||
}
|
||
const ctrl = new AbortController();
|
||
// eslint-disable-next-line react-hooks/set-state-in-effect -- fetch başlangıç işareti
|
||
setYukleniyor(true);
|
||
setKategori(null);
|
||
setIl(null);
|
||
setTip(null);
|
||
setKategoriFacet(null);
|
||
setIlFacet(null);
|
||
facetGetir(sira, tur, { dilimler: true }, ctrl.signal)
|
||
.then((cevap) => {
|
||
if (!cevap?.dilimToplam) return;
|
||
setVeri({ facetler: cevap.facetler, dilimToplam: cevap.dilimToplam });
|
||
})
|
||
.finally(() => setYukleniyor(false));
|
||
return () => ctrl.abort();
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [sira, tur]);
|
||
|
||
// Kategori seçildi → o kategorinin illerini getir
|
||
useEffect(() => {
|
||
if (!kategori) return;
|
||
const ctrl = new AbortController();
|
||
facetGetir(sira, tur, { kategori }, ctrl.signal).then((cevap) => {
|
||
if (cevap) setKategoriFacet(cevap.facetler);
|
||
});
|
||
return () => ctrl.abort();
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [kategori]);
|
||
|
||
// İl seçildi → kategori+il ile tipleri getir
|
||
useEffect(() => {
|
||
if (!kategori || !il) return;
|
||
const ctrl = new AbortController();
|
||
facetGetir(sira, tur, { kategori, il }, ctrl.signal).then((cevap) => {
|
||
if (cevap) setIlFacet(cevap.facetler);
|
||
});
|
||
return () => ctrl.abort();
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [kategori, il]);
|
||
|
||
function siraDegistir(ham: string) {
|
||
const rakamlar = ham.replace(/\D/g, "").slice(0, 7);
|
||
setSiraMetni(rakamlar ? Number(rakamlar).toLocaleString("tr-TR") : "");
|
||
if (zamanlayici.current) clearTimeout(zamanlayici.current);
|
||
const deger = Number(rakamlar);
|
||
if (!deger || deger < 1 || deger > 4_000_000) return;
|
||
zamanlayici.current = setTimeout(() => setSira(deger), 450);
|
||
}
|
||
|
||
const durum: DemoDurum = {
|
||
sira,
|
||
tur,
|
||
veri,
|
||
yukleniyor,
|
||
kategori,
|
||
il,
|
||
tip,
|
||
kategoriFacet,
|
||
ilFacet,
|
||
kategoriSec: (k) => {
|
||
setKategori(k);
|
||
setIl(null);
|
||
setTip(null);
|
||
setKategoriFacet(null);
|
||
setIlFacet(null);
|
||
},
|
||
ilSec: (i) => {
|
||
setIl(i);
|
||
setTip(null);
|
||
setIlFacet(null);
|
||
},
|
||
tipSec: setTip,
|
||
siraDegistir,
|
||
turDegistir: setTur,
|
||
siraMetni,
|
||
};
|
||
|
||
return <DemoContext.Provider value={durum}>{children}</DemoContext.Provider>;
|
||
}
|
||
|
||
/** Sıralama girişi + puan türü — iki diyagramı birden süren tek kumanda. */
|
||
export function DemoKumanda() {
|
||
const { siraMetni, siraDegistir, tur, turDegistir, sira, yukleniyor } =
|
||
useDemo();
|
||
return (
|
||
<div className="rounded-2xl border border-primary/20 bg-primary/5 p-5">
|
||
<label
|
||
htmlFor="demo-sira"
|
||
className="font-heading text-sm font-bold text-slate-900"
|
||
>
|
||
Kendi sıralamanı gir, aşağıdaki görseller sana göre değişsin
|
||
</label>
|
||
<div className="mt-3 flex flex-col gap-3 sm:flex-row sm:items-center">
|
||
<input
|
||
id="demo-sira"
|
||
inputMode="numeric"
|
||
autoComplete="off"
|
||
value={siraMetni}
|
||
onChange={(e) => siraDegistir(e.target.value)}
|
||
placeholder="ör. 85.000"
|
||
className="h-11 w-full rounded-xl border border-slate-200 bg-white px-4 text-base font-medium tabular-nums outline-none transition-colors focus:border-primary sm:w-44"
|
||
/>
|
||
<div
|
||
role="radiogroup"
|
||
aria-label="Puan türü"
|
||
className="flex flex-wrap gap-1.5"
|
||
>
|
||
{TUR_SECENEKLERI.map((t) => (
|
||
<button
|
||
key={t.deger}
|
||
type="button"
|
||
role="radio"
|
||
aria-checked={tur === t.deger}
|
||
onClick={() => turDegistir(t.deger)}
|
||
className={
|
||
tur === t.deger
|
||
? "cursor-pointer rounded-full border border-primary bg-primary px-3 py-1.5 text-xs font-medium text-white"
|
||
: "cursor-pointer rounded-full border border-slate-200 bg-white px-3 py-1.5 text-xs text-slate-600 transition-colors duration-200 hover:border-slate-300"
|
||
}
|
||
>
|
||
{t.etiket}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<p
|
||
className={`mt-2 text-xs text-slate-500 ${yukleniyor ? "animate-pulse" : ""}`}
|
||
aria-live="polite"
|
||
>
|
||
{yukleniyor
|
||
? "Hesaplanıyor…"
|
||
: `Aşağıdaki tüm sayılar ${sira.toLocaleString("tr-TR")}. sıradaki bir aday için gerçek YÖK Atlas verisidir.`}
|
||
</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/** Sıralama ekseni: girilen sıraya göre dilim sınırları ve gerçek adetler. */
|
||
export function DilimSeridiCanli() {
|
||
const { sira, veri, yukleniyor } = useDemo();
|
||
const t = veri.dilimToplam;
|
||
const dengeliUst = Math.round(sira * 1.4);
|
||
const fmt = (n: number) => n.toLocaleString("tr-TR");
|
||
|
||
return (
|
||
<figure className={`mt-6 ${yukleniyor ? "animate-pulse opacity-60" : ""}`}>
|
||
<svg
|
||
viewBox="0 0 640 168"
|
||
role="img"
|
||
aria-label={`Sıralama ekseni: ${fmt(sira)}. sıradaki aday için Hayal diliminde ${fmt(t.hayal)}, Dengeli diliminde ${fmt(t.dengeli)}, Garanti diliminde ${fmt(t.garanti)} program var.`}
|
||
className="w-full"
|
||
>
|
||
{/* eksen */}
|
||
<line
|
||
x1="20"
|
||
y1="86"
|
||
x2="620"
|
||
y2="86"
|
||
stroke="currentColor"
|
||
className="text-slate-300"
|
||
strokeWidth="2"
|
||
/>
|
||
<text x="20" y="150" className="fill-slate-400 text-[11px]">
|
||
1. sıra (en iyi)
|
||
</text>
|
||
<text
|
||
x="620"
|
||
y="150"
|
||
textAnchor="end"
|
||
className="fill-slate-400 text-[11px]"
|
||
>
|
||
taban sıralaması büyüdükçe →
|
||
</text>
|
||
|
||
{/* hayal */}
|
||
<rect x="20" y="70" width="150" height="32" rx="4" className="fill-red-100" />
|
||
<text x="95" y="40" textAnchor="middle" className="fill-red-600 text-[12px] font-semibold">
|
||
Hayal
|
||
</text>
|
||
<text x="95" y="56" textAnchor="middle" className="fill-slate-600 text-[11px] font-medium tabular-nums">
|
||
{fmt(t.hayal)} program
|
||
</text>
|
||
<text x="95" y="123" textAnchor="middle" className="fill-slate-500 text-[10px]">
|
||
tabanı senden iyi
|
||
</text>
|
||
|
||
{/* aday işareti */}
|
||
<line x1="172" y1="24" x2="172" y2="112" stroke="currentColor" className="text-slate-900" strokeWidth="2" strokeDasharray="4 3" />
|
||
<text x="172" y="16" textAnchor="middle" className="fill-slate-900 text-[12px] font-bold">
|
||
SEN · {fmt(sira)}.
|
||
</text>
|
||
|
||
{/* dengeli */}
|
||
<rect x="174" y="70" width="120" height="32" rx="4" className="fill-amber-100" />
|
||
<text x="234" y="40" textAnchor="middle" className="fill-amber-600 text-[12px] font-semibold">
|
||
Dengeli
|
||
</text>
|
||
<text x="234" y="56" textAnchor="middle" className="fill-slate-600 text-[11px] font-medium tabular-nums">
|
||
{fmt(t.dengeli)} program
|
||
</text>
|
||
<text x="234" y="123" textAnchor="middle" className="fill-slate-500 text-[10px]">
|
||
{fmt(sira)}. – {fmt(dengeliUst)}. arası taban
|
||
</text>
|
||
|
||
{/* garanti */}
|
||
<rect x="296" y="70" width="324" height="32" rx="4" className="fill-emerald-100" />
|
||
<text x="458" y="40" textAnchor="middle" className="fill-emerald-700 text-[12px] font-semibold">
|
||
Garanti
|
||
</text>
|
||
<text x="458" y="56" textAnchor="middle" className="fill-slate-600 text-[11px] font-medium tabular-nums">
|
||
{fmt(t.garanti)} program
|
||
</text>
|
||
<text x="458" y="123" textAnchor="middle" className="fill-slate-500 text-[10px]">
|
||
{fmt(dengeliUst + 1)}. sonrası — sonu yok, sayfaladıkça uzar
|
||
</text>
|
||
</svg>
|
||
<figcaption className="mt-2 text-xs text-slate-500">
|
||
Liste her dilimde{" "}
|
||
<span className="font-medium text-slate-700">
|
||
sana en yakın tabandan uzağa doğru
|
||
</span>{" "}
|
||
sıralanır; “Daha fazla göster” dedikçe uzaktakiler gelir.
|
||
</figcaption>
|
||
</figure>
|
||
);
|
||
}
|
||
|
||
function HuniCubugu({
|
||
etiket,
|
||
adet,
|
||
oran,
|
||
}: {
|
||
etiket: string;
|
||
adet: number;
|
||
oran: number;
|
||
}) {
|
||
return (
|
||
<div className="min-w-0 flex-1">
|
||
<div
|
||
className="flex h-9 min-w-36 items-center justify-between gap-2 rounded-lg bg-primary/85 px-3 transition-[width] duration-300"
|
||
style={{ width: `${Math.min(Math.max(oran * 100, 36), 100)}%` }}
|
||
>
|
||
<span className="truncate text-xs font-medium text-white">{etiket}</span>
|
||
<span className="shrink-0 text-xs font-bold tabular-nums text-white">
|
||
{adet.toLocaleString("tr-TR")}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
function SecimCipleri<T extends { anahtar: string; etiket: string; adet: number }>({
|
||
secenekler,
|
||
secili,
|
||
onSec,
|
||
}: {
|
||
secenekler: T[];
|
||
secili: string | null;
|
||
onSec: (anahtar: string | null) => void;
|
||
}) {
|
||
return (
|
||
<div className="flex flex-wrap gap-1.5">
|
||
{secenekler.map((s) => (
|
||
<button
|
||
key={s.anahtar}
|
||
type="button"
|
||
aria-pressed={secili === s.anahtar}
|
||
onClick={() => onSec(secili === s.anahtar ? null : s.anahtar)}
|
||
className={
|
||
secili === s.anahtar
|
||
? "cursor-pointer rounded-full border border-orange-500 bg-orange-500 px-3 py-1.5 text-xs font-medium text-white"
|
||
: "cursor-pointer rounded-full border border-slate-200 bg-white px-3 py-1.5 text-xs text-slate-600 transition-colors duration-200 hover:border-slate-300"
|
||
}
|
||
>
|
||
{s.etiket}
|
||
<span className="ml-1 text-[10px] opacity-70 tabular-nums">
|
||
{s.adet.toLocaleString("tr-TR")}
|
||
</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// AI listesinin dilim iskeleti: üstte hayal, ortada dengeli, sonda garanti.
|
||
// Ürünün risk renkleriyle boyanır; sayılar prompt'taki kuralın orta değeri.
|
||
const LISTE_ISKELETI: { dilim: string; adet: number; renk: string }[] = [
|
||
{ dilim: "hayal", adet: 5, renk: "bg-red-400" },
|
||
{ dilim: "dengeli", adet: 13, renk: "bg-amber-400" },
|
||
{ dilim: "garanti", adet: 6, renk: "bg-emerald-500" },
|
||
];
|
||
|
||
/** Huninin finali: kalan havuzun yapay zekâya gidişi ve 24'lük listenin
|
||
iskeleti. Havuz 24'ün altına düşerse gevşetme kuralını canlı gösterir. */
|
||
function YapayZekaAsamasi({ havuz }: { havuz: number }) {
|
||
const dar = havuz < 24;
|
||
return (
|
||
<div className="border-t border-slate-100 pt-4">
|
||
<p className="text-xs font-semibold text-slate-500">
|
||
Sonra sıra yapay zekâda
|
||
</p>
|
||
<p className="mt-1.5 text-xs leading-5 text-slate-600">
|
||
Seçimlerinden geriye kalan{" "}
|
||
<span className="font-bold tabular-nums text-slate-900">
|
||
{havuz.toLocaleString("tr-TR")} programlık havuz
|
||
</span>{" "}
|
||
yapay zekâya gider; o da bu havuzdan tam 24 tercih seçer ve şu iskelete
|
||
oturtur:
|
||
</p>
|
||
|
||
{/* 24 hücrelik liste şeridi — renk + metin birlikte (renk tek başına
|
||
anlam taşımaz) */}
|
||
<div
|
||
role="img"
|
||
aria-label="24 tercihlik listenin yapısı: en üstte yaklaşık 5 hayal, ortada 13 dengeli, sonda 6 garanti tercih."
|
||
className="mt-3"
|
||
>
|
||
<div className="flex gap-1">
|
||
{LISTE_ISKELETI.flatMap((b) =>
|
||
Array.from({ length: b.adet }, (_, i) => (
|
||
<span
|
||
key={`${b.dilim}-${i}`}
|
||
className={`h-6 min-w-0 flex-1 rounded ${b.renk}`}
|
||
/>
|
||
)),
|
||
)}
|
||
</div>
|
||
<div className="mt-1.5 flex justify-between text-[11px] text-slate-500">
|
||
<span>
|
||
üstte 4–6 <span className="font-semibold text-red-600">hayal</span>
|
||
</span>
|
||
<span>
|
||
ortada 12–14{" "}
|
||
<span className="font-semibold text-amber-600">dengeli</span>
|
||
</span>
|
||
<span>
|
||
sonda 5–7{" "}
|
||
<span className="font-semibold text-emerald-600">garanti</span>
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<ul className="mt-3 space-y-1 text-xs leading-5 text-slate-600">
|
||
<li>
|
||
· Havuzda olmayan bir bölümü yazamaz — listedeki her satırın kimliği
|
||
havuzla karşılaştırılır, uymayan liste yeniden ürettirilir.
|
||
</li>
|
||
<li>
|
||
· Her satıra tek cümlelik gerekçe ve bir risk notu ekler; sıraya neyi
|
||
neden koyduğu görünür.
|
||
</li>
|
||
<li>
|
||
· Önceliklerin (iş garantisi, maaş, prestij…) hangi programın öne
|
||
geçeceğini belirler.
|
||
</li>
|
||
<li>
|
||
· Havuz 24'ün altına düşerse filtreler adım adım gevşetilir —
|
||
önce il, sonra alan — ve bu, listende açıkça belirtilir.
|
||
</li>
|
||
</ul>
|
||
|
||
{dar ? (
|
||
<p className="mt-3 rounded-lg bg-amber-50 px-3 py-2 text-xs leading-5 text-amber-700">
|
||
Bu seçimle havuz 24'ün altına düştü. Gerçek sihirbaz bu durumda
|
||
filtreleri adım adım gevşetir — önce il, sonra alan tercihi esnetilir
|
||
— ve bunu listende açıkça belirtir; liste her zaman 24'e
|
||
tamamlanır.
|
||
</p>
|
||
) : null}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/** Kademeli filtre hunisi: gerçek sihirbaz mantığının oynanabilir kopyası. */
|
||
export function HuniCanli() {
|
||
const {
|
||
veri,
|
||
yukleniyor,
|
||
kategori,
|
||
il,
|
||
tip,
|
||
kategoriFacet,
|
||
ilFacet,
|
||
kategoriSec,
|
||
ilSec,
|
||
tipSec,
|
||
} = useDemo();
|
||
|
||
const toplam = veri.facetler.toplam;
|
||
const kategoriAdet =
|
||
kategori != null
|
||
? (veri.facetler.kategoriler.find((k) => k.ad === kategori)?.adet ?? 0)
|
||
: null;
|
||
const ilAdet =
|
||
il != null && kategoriFacet
|
||
? (kategoriFacet.iller.find((i) => i.il === il)?.adet ?? 0)
|
||
: null;
|
||
const tipAdet =
|
||
tip != null && ilFacet
|
||
? (ilFacet.uniturler.find((u) => u.grup === tip)?.adet ?? 0)
|
||
: null;
|
||
|
||
const kategoriSecenekleri = veri.facetler.kategoriler.slice(0, 6).map((k) => ({
|
||
anahtar: k.ad,
|
||
etiket: k.ad,
|
||
adet: k.adet,
|
||
}));
|
||
const ilSecenekleri = (kategoriFacet?.iller ?? []).slice(0, 6).map((i) => ({
|
||
anahtar: i.il,
|
||
etiket: i.il.toLocaleLowerCase("tr-TR"),
|
||
adet: i.adet,
|
||
}));
|
||
const tipSecenekleri = (ilFacet?.uniturler ?? []).map((u) => ({
|
||
anahtar: u.grup,
|
||
etiket: u.grup === "devlet" ? "Devlet" : "Vakıf",
|
||
adet: u.adet,
|
||
}));
|
||
|
||
// Yapay zekâya giden havuz: huninin en derin (en son seçilmiş) katmanı
|
||
const havuz = tipAdet ?? ilAdet ?? kategoriAdet ?? toplam;
|
||
|
||
return (
|
||
<div
|
||
className={`mt-6 space-y-4 rounded-2xl border border-slate-200 bg-white p-5 ${
|
||
yukleniyor ? "animate-pulse opacity-60" : ""
|
||
}`}
|
||
>
|
||
{/* Huni çubukları — her seçim bir çubuk daha ekler */}
|
||
<div className="space-y-2">
|
||
<div className="flex items-center gap-3">
|
||
<span className="w-6 shrink-0" />
|
||
<HuniCubugu
|
||
etiket="Sıralamanla açılan tüm programlar"
|
||
adet={toplam}
|
||
oran={1}
|
||
/>
|
||
</div>
|
||
{kategori != null && kategoriAdet != null ? (
|
||
<div className="flex items-center gap-3">
|
||
<span className="w-6 shrink-0 text-right text-xs font-bold text-slate-400">
|
||
1.
|
||
</span>
|
||
<HuniCubugu
|
||
etiket={`“${kategori}” seçince`}
|
||
adet={kategoriAdet}
|
||
oran={toplam > 0 ? kategoriAdet / toplam : 0}
|
||
/>
|
||
</div>
|
||
) : null}
|
||
{il != null && ilAdet != null ? (
|
||
<div className="flex items-center gap-3">
|
||
<span className="w-6 shrink-0 text-right text-xs font-bold text-slate-400">
|
||
2.
|
||
</span>
|
||
<HuniCubugu
|
||
etiket={`+ “${il.toLocaleLowerCase("tr-TR")}” seçince`}
|
||
adet={ilAdet}
|
||
oran={toplam > 0 ? ilAdet / toplam : 0}
|
||
/>
|
||
</div>
|
||
) : null}
|
||
{tip != null && tipAdet != null ? (
|
||
<div className="flex items-center gap-3">
|
||
<span className="w-6 shrink-0 text-right text-xs font-bold text-slate-400">
|
||
3.
|
||
</span>
|
||
<HuniCubugu
|
||
etiket={`+ “${tip === "devlet" ? "Devlet" : "Vakıf"}” seçince`}
|
||
adet={tipAdet}
|
||
oran={toplam > 0 ? tipAdet / toplam : 0}
|
||
/>
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
|
||
{/* Seçim adımları — sihirbazın küçük, oynanabilir hali */}
|
||
<div className="space-y-3 border-t border-slate-100 pt-4">
|
||
<div>
|
||
<p className="mb-1.5 text-xs font-semibold text-slate-500">
|
||
1. Bir alan seç
|
||
</p>
|
||
<SecimCipleri
|
||
secenekler={kategoriSecenekleri}
|
||
secili={kategori}
|
||
onSec={kategoriSec}
|
||
/>
|
||
</div>
|
||
{kategori != null ? (
|
||
<div>
|
||
<p className="mb-1.5 text-xs font-semibold text-slate-500">
|
||
2. Bir il seç{" "}
|
||
<span className="font-normal text-slate-400">
|
||
(iller artık yalnızca “{kategori}” programlarını sayıyor)
|
||
</span>
|
||
</p>
|
||
{ilSecenekleri.length > 0 ? (
|
||
<SecimCipleri
|
||
secenekler={ilSecenekleri}
|
||
secili={il}
|
||
onSec={ilSec}
|
||
/>
|
||
) : (
|
||
<p className="text-xs text-slate-400">Yükleniyor…</p>
|
||
)}
|
||
</div>
|
||
) : null}
|
||
{kategori != null && il != null ? (
|
||
<div>
|
||
<p className="mb-1.5 text-xs font-semibold text-slate-500">
|
||
3. Üniversite tipi seç{" "}
|
||
<span className="font-normal text-slate-400">
|
||
(sayılar hem alanı hem ili hesaba katıyor)
|
||
</span>
|
||
</p>
|
||
{tipSecenekleri.length > 0 ? (
|
||
<SecimCipleri
|
||
secenekler={tipSecenekleri}
|
||
secili={tip}
|
||
onSec={(t) => tipSec(t as "devlet" | "vakif" | null)}
|
||
/>
|
||
) : (
|
||
<p className="text-xs text-slate-400">Yükleniyor…</p>
|
||
)}
|
||
</div>
|
||
) : null}
|
||
</div>
|
||
|
||
<YapayZekaAsamasi havuz={havuz} />
|
||
</div>
|
||
);
|
||
}
|