All checks were successful
Deploy / deploy (push) Successful in 7m45s
- Updated the app.db file to reflect recent changes in the application state. - Modified BACKLOG.md to include new entries regarding the removal of certain wizard steps, adjustments to priority chips, and the introduction of special quota conditions for candidates. - Added details on the decision to link priority chips to data sources and clarified the implications for user experience and data handling.
635 lines
25 KiB
TypeScript
635 lines
25 KiB
TypeScript
"use client";
|
||
|
||
// Sihirbazın 3 adımlık seçim gövdesi. Hem ana sayfadaki hero modalında hem de
|
||
// /sonuc'taki sihirbaz modalında kullanılır; seçim state'ini kendi tutar,
|
||
// tamamlanınca onTamamla(secimler) çağırır.
|
||
//
|
||
// Adımlar kademeli süzülür: 2. adımın illeri seçili kategorilere, 3. adımın
|
||
// üniversite tipi sayıları kategori+il'e göre /api/facetler'den tazelenir.
|
||
|
||
import { useEffect, useMemo, useState } from "react";
|
||
import { ArrowLeft, ArrowRight, Search, X } from "lucide-react";
|
||
import { useReducedMotion } from "motion/react";
|
||
import useMeasure from "react-use-measure";
|
||
import { toast } from "sonner";
|
||
import { cities } from "turkey-map-react/lib/data";
|
||
import { TransitionPanel } from "@/components/core/transition-panel";
|
||
import { Button } from "@/components/ui/button";
|
||
import { IlSecimHaritasi } from "./il-secim-haritasi";
|
||
import { normalizeIlAdi } from "@/lib/harita";
|
||
import { onbellekliJsonGetir } from "@/lib/istek-cache";
|
||
import { olay } from "@/lib/analitik";
|
||
import { trBaslikDuzeni } from "@/lib/slug";
|
||
import type { SihirbazFacetleri } from "@/types/yokatlas";
|
||
import { ONCELIKLER, UNIVERSITE_TIPI_ETIKET, type SihirbazSecimleri, type UniversiteTipi } from "@/features/sihirbaz/sihirbaz-sabitler";
|
||
import { OZEL_DURUMLAR, OZEL_DURUM_ETIKET, type OzelDurum } from "@/lib/ozel-durum";
|
||
|
||
export function Cip({
|
||
secili,
|
||
onClick,
|
||
children,
|
||
kucuk = false,
|
||
ton = "notr",
|
||
className = "",
|
||
}: {
|
||
secili: boolean;
|
||
onClick: () => void;
|
||
children: React.ReactNode;
|
||
kucuk?: boolean;
|
||
/**
|
||
* bilgi: özel kontenjan durumu çipi (DESIGN.md §4.21) — pasif hâli bilgi
|
||
* mavisi tonunda (§2 `primary` rolü), seçili hâli nötr çiple aynı.
|
||
*/
|
||
ton?: "notr" | "bilgi";
|
||
className?: string;
|
||
}) {
|
||
// Şişko ve sade (DESIGN.md §1.1): çip 44 px dokunma hedefi; il
|
||
// çipleri (kucuk) çok sayıda olduğu için 40 px. Hover §2.1 tek turuncu.
|
||
const boyut = kucuk ? "h-10 px-3.5 text-sm" : "h-11 px-4 text-sm";
|
||
const temel = `inline-flex cursor-pointer items-center rounded-full border font-semibold transition-[background-color,border-color,color,transform] duration-150 active:scale-[0.97] ${boyut} ${className}`;
|
||
const pasif =
|
||
ton === "bilgi"
|
||
? "border-primary/30 bg-primary/10 text-primary hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500"
|
||
: "border-slate-200 bg-card text-slate-700 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500";
|
||
return (
|
||
<button
|
||
type="button"
|
||
onClick={onClick}
|
||
aria-pressed={secili}
|
||
className={
|
||
secili ? `${temel} border-primary bg-primary text-white` : `${temel} ${pasif}`
|
||
}
|
||
>
|
||
{children}
|
||
</button>
|
||
);
|
||
}
|
||
|
||
const ADIM_ADLARI = ["Alan", "Şehir", "Öncelik"] as const;
|
||
|
||
// Haritada karşılığı olan iller (KKTC/yurtdışı kampüsler çip olarak kalır)
|
||
const HARITA_ILLERI = new Set(cities.map((c) => normalizeIlAdi(c.name)));
|
||
|
||
export function SihirbazAdimlar({
|
||
sira,
|
||
tur,
|
||
facetler,
|
||
baslangicSecimler,
|
||
sonButonEtiketi,
|
||
onTamamla,
|
||
}: {
|
||
sira: number;
|
||
tur: string;
|
||
facetler: SihirbazFacetleri;
|
||
baslangicSecimler?: SihirbazSecimleri | null;
|
||
sonButonEtiketi: string;
|
||
onTamamla: (secimler: SihirbazSecimleri) => void;
|
||
}) {
|
||
const [adim, setAdim] = useState(0);
|
||
const [yon, setYon] = useState(1);
|
||
const [gecisBasladi, setGecisBasladi] = useState(false);
|
||
const [panelRef, panelOlculeri] = useMeasure();
|
||
const hareketiAzalt = useReducedMotion();
|
||
const [kategoriler, setKategoriler] = useState<string[]>(
|
||
baslangicSecimler?.kategoriler ?? [],
|
||
);
|
||
const [iller, setIller] = useState<string[]>(baslangicSecimler?.iller ?? []);
|
||
const [universiteTipi, setUniversiteTipi] = useState<UniversiteTipi>(
|
||
baslangicSecimler?.universiteTipi ?? "farketmez",
|
||
);
|
||
const [oncelikler, setOncelikler] = useState<string[]>(
|
||
baslangicSecimler?.oncelikler ?? [],
|
||
);
|
||
// Özel kontenjan durumu: isteğe bağlı, havuzu daraltmaz (lib/ozel-durum.ts)
|
||
const [ozelDurum, setOzelDurum] = useState<OzelDurum[]>(
|
||
baslangicSecimler?.ozelDurum ?? [],
|
||
);
|
||
|
||
// Kademeli facet'ler: kategoriler daima ilk yükten gelir; il ve üniversite
|
||
// tipi sayıları adıma girerken o anki seçimlerle tazelenir. Fetch başarısız
|
||
// olursa eldeki (daha geniş) liste kalır — sihirbaz asla kilitlenmez.
|
||
const [canliFacetler, setCanliFacetler] = useState(facetler);
|
||
// 81 elemanlı listeyi her çip tıklamasında yeniden sıralamamak/filtrelememek
|
||
// için memo; haritaya giden dizinin referansı da sabit kalır (gereksiz
|
||
// IlSecimHaritasi render'ı biter).
|
||
const siraliIller = useMemo(
|
||
() => [...canliFacetler.iller].sort((a, b) => b.adet - a.adet),
|
||
[canliFacetler.iller],
|
||
);
|
||
// Türkiye illeri ana çip grubu; KKTC/yurtdışı kampüsler haritada olmadığı
|
||
// için ayrı alt grupta listelenir.
|
||
const turkiyeIlleri = useMemo(
|
||
() => siraliIller.filter((i) => HARITA_ILLERI.has(i.il)),
|
||
[siraliIller],
|
||
);
|
||
const disIller = useMemo(
|
||
() => siraliIller.filter((i) => !HARITA_ILLERI.has(i.il)),
|
||
[siraliIller],
|
||
);
|
||
// İl araması: Türkçe büyük/küçük harf ve şapkalı harf farklarına duyarsız
|
||
// eşleşme için iki taraf da normalizeIlAdi'dan geçirilir.
|
||
const [ilArama, setIlArama] = useState("");
|
||
const ilAramaNorm = normalizeIlAdi(ilArama.trim());
|
||
// 81 çip ilk bakışta boğucu; varsayılan görünüm ilk 10 il + "+N il daha"
|
||
// rozeti. Arama açıkken kırpma yok (arama zaten görünümü daraltıyor).
|
||
const [tumIllerAcik, setTumIllerAcik] = useState(false);
|
||
const gorunenTurkiyeIlleri = useMemo(
|
||
() =>
|
||
ilAramaNorm
|
||
? turkiyeIlleri.filter((i) =>
|
||
normalizeIlAdi(i.il).includes(ilAramaNorm),
|
||
)
|
||
: turkiyeIlleri,
|
||
[turkiyeIlleri, ilAramaNorm],
|
||
);
|
||
const IL_ON_IZLEME = 10;
|
||
const ilListesiKirpik =
|
||
!ilAramaNorm &&
|
||
!tumIllerAcik &&
|
||
gorunenTurkiyeIlleri.length > IL_ON_IZLEME;
|
||
const listelenenTurkiyeIlleri = ilListesiKirpik
|
||
? gorunenTurkiyeIlleri.slice(0, IL_ON_IZLEME)
|
||
: gorunenTurkiyeIlleri;
|
||
const gorunenDisIller = useMemo(
|
||
() =>
|
||
ilAramaNorm
|
||
? disIller.filter((i) => normalizeIlAdi(i.il).includes(ilAramaNorm))
|
||
: disIller,
|
||
[disIller, ilAramaNorm],
|
||
);
|
||
const haritaIlleri = useMemo(
|
||
() => canliFacetler.iller.filter((i) => HARITA_ILLERI.has(i.il)),
|
||
[canliFacetler.iller],
|
||
);
|
||
// Huninin paydası: 1. adım hiçbir zaman "geçilmediği" için `adimaGit`ten
|
||
// atılamıyordu (orası yalnız ileri geçişleri sayar) ve `sihirbaz_adim{1}`
|
||
// hiç düşmüyordu — payda `sira_girildi`den türetilmek zorunda kalıyordu.
|
||
// Modal kapanınca bileşen unmount olduğu için mount = "1. adım görüldü";
|
||
// 1→2→1→2 gidip gelmek unmount etmediğinden tekrar tetiklenmez.
|
||
//
|
||
// Bilerek `olayBirKez` KULLANILMAZ (bkz. lib/analitik.ts:22-24, aynı kural
|
||
// `liste_uretim_basladi` için yazılı): `:220`'deki ileri geçiş olayı
|
||
// (`adim:2`, `adim:3`) her geçişte atılıyor. Paydayı oturum başına tekil
|
||
// yapsaydık sihirbazı ikinci kez açan öğrencide pay 2, payda 1 kalır ve
|
||
// 1→2 geçiş oranı %100'ü aşardı. Payda ile pay aynı birimde olmalı:
|
||
// sihirbaz her açıldığında `adim:1` de düşer.
|
||
//
|
||
// SERİ KIRILMASI: bu satırın canlıya çıktığı tarihten önce `sihirbaz_adim`
|
||
// serisinde `adim:1` hiç yok; öncesi/sonrası karşılaştırılamaz.
|
||
useEffect(() => {
|
||
olay("sihirbaz_adim", { adim: 1 });
|
||
}, []);
|
||
|
||
const [facetYukleniyor, setFacetYukleniyor] = useState(false);
|
||
useEffect(() => {
|
||
if (adim === 0) return;
|
||
const ctrl = new AbortController();
|
||
const params = new URLSearchParams({ sira: String(sira), tur });
|
||
for (const k of kategoriler) params.append("kategori", k);
|
||
if (adim === 2) for (const i of iller) params.append("il", i);
|
||
// eslint-disable-next-line react-hooks/set-state-in-effect -- fetch başlangıç işareti
|
||
setFacetYukleniyor(true);
|
||
// Cache'li: 2↔3 adımları arasında gidip gelmek aynı URL'i yeniden çekmez
|
||
onbellekliJsonGetir<{ facetler: SihirbazFacetleri }>(
|
||
`/api/facetler?${params}`,
|
||
{ signal: ctrl.signal },
|
||
)
|
||
.then((veri) => {
|
||
setCanliFacetler((mevcut) => ({
|
||
...mevcut,
|
||
iller: veri.facetler.iller,
|
||
uniturler: veri.facetler.uniturler,
|
||
}));
|
||
// Seçili tip daralan filtrede yoksa (ör. seçilen ilde vakıf kalmadı)
|
||
// görünmez bir seçim bırakma
|
||
setUniversiteTipi((tip) =>
|
||
tip !== "farketmez" &&
|
||
!veri.facetler.uniturler.some((u) => u.grup === tip)
|
||
? "farketmez"
|
||
: tip,
|
||
);
|
||
})
|
||
.catch(() => {})
|
||
.finally(() => setFacetYukleniyor(false));
|
||
return () => ctrl.abort();
|
||
// Seçimler yalnızca önceki adımlarda değişebildiği için adım geçişinde
|
||
// okumak yeterli; her çip tıklamasında istek atmayalım.
|
||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||
}, [adim]);
|
||
|
||
function listeDegistir(
|
||
liste: string[],
|
||
setListe: (v: string[]) => void,
|
||
deger: string,
|
||
maks?: number,
|
||
) {
|
||
if (liste.includes(deger)) {
|
||
setListe(liste.filter((x) => x !== deger));
|
||
} else {
|
||
if (maks && liste.length >= maks) {
|
||
toast.info(`En fazla ${maks} seçebilirsin.`);
|
||
return;
|
||
}
|
||
setListe([...liste, deger]);
|
||
}
|
||
}
|
||
|
||
const adimGecerli = adim === 0 ? kategoriler.length > 0 : true;
|
||
|
||
function adimaGit(yeniAdim: number) {
|
||
setYon(yeniAdim > adim ? 1 : -1);
|
||
setGecisBasladi(true);
|
||
setAdim(yeniAdim);
|
||
// Yalnızca ileri geçişler huni adımı sayılır; geri dönüşler tekrar saymasın.
|
||
// `adim:1` buradan hiç geçmez — onu yukarıdaki mount effect'i atar.
|
||
if (yeniAdim > adim) olay("sihirbaz_adim", { adim: yeniAdim + 1 });
|
||
}
|
||
|
||
const panelYuksekligi =
|
||
gecisBasladi && panelOlculeri.height > 0
|
||
? panelOlculeri.height
|
||
: "auto";
|
||
|
||
return (
|
||
<div className="flex flex-col gap-5">
|
||
{/* Adım göstergesi (DESIGN.md §10): üç parçalı çubuk + adların
|
||
kendisi; öğrenci nerede olduğunu ve ne kaldığını görür. Değişim
|
||
ekran okuyucuya duyurulur (WCAG 4.1.3). */}
|
||
<div>
|
||
<ol className="grid grid-cols-3 gap-2" aria-label="Sihirbaz adımları">
|
||
{ADIM_ADLARI.map((ad, i) => (
|
||
<li key={ad} className="flex flex-col gap-1.5">
|
||
{/* Dolum transform ile (yalnız transform/opaklık canlandırılır);
|
||
soldan dolar, güçlü ease-out 300 ms. */}
|
||
<span className="h-1.5 overflow-hidden rounded-full bg-slate-200" aria-hidden>
|
||
<span
|
||
className={`block h-full origin-left rounded-full bg-primary transition-transform duration-300 ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none ${
|
||
i <= adim ? "scale-x-100" : "scale-x-0"
|
||
}`}
|
||
/>
|
||
</span>
|
||
<span
|
||
className={`text-sm ${
|
||
i === adim
|
||
? "font-semibold text-slate-900"
|
||
: i < adim
|
||
? "font-medium text-slate-600"
|
||
: "text-slate-400"
|
||
}`}
|
||
aria-current={i === adim ? "step" : undefined}
|
||
>
|
||
{ad}
|
||
</span>
|
||
</li>
|
||
))}
|
||
</ol>
|
||
<p role="status" className="sr-only">
|
||
Adım {adim + 1} / 3: {ADIM_ADLARI[adim]}
|
||
</p>
|
||
</div>
|
||
|
||
<TransitionPanel
|
||
activeIndex={adim}
|
||
custom={yon}
|
||
variants={{
|
||
enter: (gecisYonu: number) => ({
|
||
x: hareketiAzalt ? 0 : gecisYonu > 0 ? "100%" : "-100%",
|
||
opacity: 0,
|
||
height: panelYuksekligi,
|
||
position: "relative",
|
||
}),
|
||
center: {
|
||
zIndex: 1,
|
||
x: 0,
|
||
opacity: 1,
|
||
height: panelYuksekligi,
|
||
},
|
||
exit: (gecisYonu: number) => ({
|
||
zIndex: 0,
|
||
x: hareketiAzalt ? 0 : gecisYonu < 0 ? "100%" : "-100%",
|
||
opacity: 0,
|
||
position: "absolute",
|
||
top: 0,
|
||
left: 0,
|
||
width: "100%",
|
||
}),
|
||
}}
|
||
transition={
|
||
hareketiAzalt
|
||
? // Azaltılmış harekette yalnızca kısa çapraz solma; height/x
|
||
// varsayılan spring'e düşüp oynamasın diye açıkça sıfırlanır.
|
||
{
|
||
opacity: { duration: 0.12 },
|
||
x: { duration: 0 },
|
||
height: { duration: 0 },
|
||
}
|
||
: {
|
||
x: { type: "spring", stiffness: 300, damping: 30 },
|
||
height: { type: "spring", stiffness: 300, damping: 30 },
|
||
opacity: { duration: 0.2 },
|
||
}
|
||
}
|
||
>
|
||
<div ref={panelRef} className="px-px pb-2">
|
||
<h3 className="font-heading text-xl font-bold text-slate-900">
|
||
Hangi alanlara ilgi duyuyorsun?
|
||
</h3>
|
||
<p className="mt-1 text-sm text-slate-500">
|
||
Sıralamanla ulaşabileceğin alanlar. En az bir tane seç.
|
||
</p>
|
||
<div className="mt-4 flex flex-wrap gap-2">
|
||
{facetler.kategoriler.map((k) => (
|
||
<Cip
|
||
key={k.ad}
|
||
secili={kategoriler.includes(k.ad)}
|
||
onClick={() => listeDegistir(kategoriler, setKategoriler, k.ad)}
|
||
>
|
||
{k.ad}
|
||
<span className="ml-1.5 text-xs font-medium opacity-70">{k.adet}</span>
|
||
</Cip>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<div ref={panelRef}>
|
||
<h3 className="font-heading text-xl font-bold text-slate-900">
|
||
Nerede okumak istersin?
|
||
</h3>
|
||
<p className="mt-1 text-sm text-slate-500">
|
||
En fazla 5 il seçebilirsin. İstersen boş bırak, fark etmez.
|
||
</p>
|
||
|
||
<div className="mt-4 flex h-11 items-center gap-2 rounded-full border border-slate-200 bg-card px-4 transition-colors focus-within:border-slate-300">
|
||
<Search className="size-4 shrink-0 text-slate-400" aria-hidden />
|
||
<input
|
||
type="text"
|
||
value={ilArama}
|
||
onChange={(e) => setIlArama(e.target.value)}
|
||
placeholder="İl ara..."
|
||
aria-label="İl ara"
|
||
autoComplete="off"
|
||
className="min-w-0 flex-1 bg-transparent text-base text-slate-900 outline-none placeholder:text-slate-400"
|
||
/>
|
||
{ilArama ? (
|
||
<button
|
||
type="button"
|
||
onClick={() => setIlArama("")}
|
||
aria-label="Aramayı temizle"
|
||
className="-mr-2 flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-full text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700"
|
||
>
|
||
<X className="size-3.5" aria-hidden />
|
||
</button>
|
||
) : null}
|
||
</div>
|
||
|
||
{/* Çipler birincil seçim yolu (haritadaki küçük iller mobilde
|
||
güvenilir dokunma hedefi değil); iller seçili kategorilere göre
|
||
süzülmüş, program sayısına göre sıralı listelenir. Varsayılan
|
||
görünüm ilk 10 il; kalanı "+N il daha" rozetiyle açılır. Arama
|
||
yalnızca görünümü daraltır, seçimleri etkilemez ve kırpmayı
|
||
devre dışı bırakır. */}
|
||
<div
|
||
className={`mt-3 flex max-h-64 flex-wrap gap-2 overflow-y-auto sm:max-h-none sm:overflow-visible ${
|
||
facetYukleniyor ? "animate-pulse opacity-60" : ""
|
||
}`}
|
||
>
|
||
{listelenenTurkiyeIlleri.map((i) => (
|
||
<Cip
|
||
key={i.il}
|
||
kucuk
|
||
secili={iller.includes(i.il)}
|
||
onClick={() => listeDegistir(iller, setIller, i.il, 5)}
|
||
>
|
||
{trBaslikDuzeni(i.il)}
|
||
<span className="ml-1.5 text-xs font-medium opacity-70">{i.adet}</span>
|
||
</Cip>
|
||
))}
|
||
{ilListesiKirpik ? (
|
||
<button
|
||
type="button"
|
||
onClick={() => setTumIllerAcik(true)}
|
||
className="inline-flex h-10 cursor-pointer items-center rounded-full border border-dashed border-slate-300 bg-card px-3.5 text-sm font-semibold text-slate-500 transition-[background-color,border-color,color,transform] duration-150 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500 active:scale-[0.97]"
|
||
>
|
||
+{gorunenTurkiyeIlleri.length - IL_ON_IZLEME} il daha
|
||
</button>
|
||
) : null}
|
||
{!ilListesiKirpik &&
|
||
tumIllerAcik &&
|
||
!ilAramaNorm &&
|
||
gorunenTurkiyeIlleri.length > IL_ON_IZLEME ? (
|
||
<button
|
||
type="button"
|
||
onClick={() => setTumIllerAcik(false)}
|
||
className="inline-flex h-10 cursor-pointer items-center rounded-full border border-dashed border-slate-300 bg-card px-3.5 text-sm font-semibold text-slate-500 transition-[background-color,border-color,color,transform] duration-150 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500 active:scale-[0.97]"
|
||
>
|
||
Daha az göster
|
||
</button>
|
||
) : null}
|
||
</div>
|
||
|
||
{ilAramaNorm &&
|
||
gorunenTurkiyeIlleri.length === 0 &&
|
||
gorunenDisIller.length === 0 ? (
|
||
<p className="mt-3 text-sm text-slate-500">
|
||
“{ilArama.trim()}” ile eşleşen il yok.
|
||
</p>
|
||
) : null}
|
||
|
||
{gorunenDisIller.length > 0 ? (
|
||
<div className="mt-4">
|
||
<p className="mb-2 text-sm font-semibold text-slate-700">
|
||
KKTC & Yurt Dışı
|
||
</p>
|
||
<div
|
||
className={`flex flex-wrap gap-2 ${
|
||
facetYukleniyor ? "animate-pulse opacity-60" : ""
|
||
}`}
|
||
>
|
||
{gorunenDisIller.map((i) => (
|
||
<Cip
|
||
key={i.il}
|
||
kucuk
|
||
secili={iller.includes(i.il)}
|
||
onClick={() => listeDegistir(iller, setIller, i.il, 5)}
|
||
>
|
||
{trBaslikDuzeni(i.il)}
|
||
<span className="ml-1.5 text-xs font-medium opacity-70">{i.adet}</span>
|
||
</Cip>
|
||
))}
|
||
</div>
|
||
</div>
|
||
) : null}
|
||
|
||
{iller.length > 0 ? (
|
||
<div className="mt-3 flex flex-wrap items-center gap-2">
|
||
<span className="text-sm font-semibold text-slate-700">
|
||
Seçtiklerin:
|
||
</span>
|
||
{iller.map((il) => (
|
||
<button
|
||
key={il}
|
||
type="button"
|
||
onClick={() => listeDegistir(iller, setIller, il)}
|
||
className="inline-flex h-10 cursor-pointer items-center gap-1.5 rounded-full bg-primary px-3.5 text-sm font-semibold text-white transition-[background-color,transform] duration-150 hover:bg-primary/90 active:scale-[0.97]"
|
||
>
|
||
{trBaslikDuzeni(il)}
|
||
<X className="size-4" aria-hidden />
|
||
</button>
|
||
))}
|
||
</div>
|
||
) : null}
|
||
|
||
{/* Harita geniş ekranda görsel destek olarak kalır */}
|
||
<div className="mt-4 hidden sm:block">
|
||
<IlSecimHaritasi
|
||
iller={haritaIlleri}
|
||
secili={iller}
|
||
onToggle={(il) => listeDegistir(iller, setIller, il, 5)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
<div ref={panelRef} className="space-y-4">
|
||
<div>
|
||
<h3 className="font-heading text-xl font-bold text-slate-900">
|
||
Üniversite tipi & öncelikler
|
||
</h3>
|
||
<p className="mt-1 text-sm text-slate-500">
|
||
Bunlar listenin dengesini belirler.
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<p className="mb-2 text-sm font-semibold text-slate-700">
|
||
Üniversite tipi
|
||
</p>
|
||
<div
|
||
className={`flex flex-wrap gap-2 ${
|
||
facetYukleniyor ? "animate-pulse opacity-60" : ""
|
||
}`}
|
||
>
|
||
{canliFacetler.uniturler.map((u) => (
|
||
<Cip
|
||
key={u.grup}
|
||
secili={universiteTipi === u.grup}
|
||
onClick={() =>
|
||
setUniversiteTipi(
|
||
universiteTipi === u.grup ? "farketmez" : u.grup,
|
||
)
|
||
}
|
||
>
|
||
{UNIVERSITE_TIPI_ETIKET[u.grup]}
|
||
<span className="ml-1.5 text-xs font-medium opacity-70">{u.adet}</span>
|
||
</Cip>
|
||
))}
|
||
<Cip
|
||
secili={universiteTipi === "farketmez"}
|
||
onClick={() => setUniversiteTipi("farketmez")}
|
||
>
|
||
Fark etmez
|
||
</Cip>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<p className="mb-2 text-sm font-semibold text-slate-700">
|
||
Senin için önemli olan
|
||
</p>
|
||
{/* Telefonda iki sütun, uzun ad iki satıra sarar: tek sütunda
|
||
yedi çip 44 px'le pencereyi 812 px'in dışına itiyordu. */}
|
||
<div className="grid grid-cols-2 gap-2 sm:flex sm:flex-wrap">
|
||
{ONCELIKLER.map((o) => (
|
||
<Cip
|
||
key={o}
|
||
className="h-auto min-h-11 justify-center px-3 py-1.5 text-center leading-tight sm:h-11 sm:px-4 sm:py-0"
|
||
secili={oncelikler.includes(o)}
|
||
onClick={() => listeDegistir(oncelikler, setOncelikler, o)}
|
||
>
|
||
{o}
|
||
</Cip>
|
||
))}
|
||
</div>
|
||
<p className="mt-2 text-sm text-slate-500">
|
||
Listeyi daraltmaz; programların sırasını ve gerekçelerini
|
||
yönlendirir (istihdam ve kazanç TÜİK 2025, araştırma sırası URAP,
|
||
kadro YÖK Atlas).
|
||
</p>
|
||
</div>
|
||
{/* Özel kontenjan durumu (DESIGN.md §4.21): düz ayraç, bilgi tonlu
|
||
çipler. Cinsiyet ya da sağlık sorulmaz; "34 yaş üstü kadın" ÖSYM
|
||
kontenjan hakkının beyanıdır. Değer analitiğe gitmez. */}
|
||
<div className="border-t border-slate-100 pt-4">
|
||
<p className="mb-2 text-sm font-semibold text-slate-700">
|
||
Özel kontenjan durumun{" "}
|
||
<span className="font-normal text-slate-500">(isteğe bağlı)</span>
|
||
</p>
|
||
<div className="grid grid-cols-2 gap-2 sm:flex sm:flex-wrap">
|
||
{OZEL_DURUMLAR.map((d) => (
|
||
<Cip
|
||
key={d}
|
||
ton="bilgi"
|
||
className="h-auto min-h-11 justify-center px-3 py-1.5 text-center leading-tight sm:h-11 sm:px-4 sm:py-0"
|
||
secili={ozelDurum.includes(d)}
|
||
onClick={() =>
|
||
setOzelDurum(
|
||
ozelDurum.includes(d)
|
||
? ozelDurum.filter((x) => x !== d)
|
||
: OZEL_DURUMLAR.filter((x) => x === d || ozelDurum.includes(x)),
|
||
)
|
||
}
|
||
>
|
||
{OZEL_DURUM_ETIKET[d]}
|
||
</Cip>
|
||
))}
|
||
</div>
|
||
<p className="mt-2 text-sm text-slate-500">
|
||
ÖSYM bu durumlar için ayrı kontenjan ayırır. İşaretlersen o
|
||
kontenjanı olan programlar listende öne çıkar ve danışman bunu
|
||
bilir. Cinsiyet ya da sağlık bilgisi sorulmaz.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</TransitionPanel>
|
||
|
||
<div className="flex items-center justify-between gap-2 border-t border-slate-100 pt-4">
|
||
<Button
|
||
type="button"
|
||
variant="ghost"
|
||
onClick={() => adimaGit(adim - 1)}
|
||
disabled={adim === 0}
|
||
className={adim === 0 ? "invisible" : "h-11 cursor-pointer px-3"}
|
||
>
|
||
<ArrowLeft className="size-4" aria-hidden />
|
||
Geri
|
||
</Button>
|
||
{adim < 2 ? (
|
||
<Button
|
||
type="button"
|
||
onClick={() => adimaGit(adim + 1)}
|
||
disabled={!adimGecerli}
|
||
className="h-11 cursor-pointer bg-orange-500 px-5 text-white transition-[background-color,transform] duration-200 hover:bg-orange-600 active:scale-[0.97]"
|
||
>
|
||
Devam
|
||
<ArrowRight className="size-4" aria-hidden />
|
||
</Button>
|
||
) : (
|
||
<Button
|
||
type="button"
|
||
onClick={() => {
|
||
olay("sihirbaz_tamamlandi", {
|
||
kategori_sayisi: kategoriler.length,
|
||
il_sayisi: iller.length,
|
||
uni_tipi: universiteTipi,
|
||
oncelik_sayisi: oncelikler.length,
|
||
});
|
||
// ozelDurum olaya YAZILMAZ: profil değeri analitiğe gitmez (gizlilik)
|
||
onTamamla({ kategoriler, iller, universiteTipi, oncelikler, ozelDurum });
|
||
}}
|
||
disabled={kategoriler.length === 0}
|
||
className="h-11 cursor-pointer bg-orange-500 px-5 text-white transition-[background-color,transform] duration-200 hover:bg-orange-600 active:scale-[0.97]"
|
||
>
|
||
{sonButonEtiketi}
|
||
<ArrowRight className="size-4" aria-hidden />
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|