v2 #10

Merged
bilal merged 34 commits from v2 into main 2026-10-11 01:38:50 +03:00
6 changed files with 931 additions and 208 deletions
Showing only changes of commit 52f565ea4a - Show all commits

View File

@@ -668,8 +668,9 @@ sihirbaz modalı içindir; yeni bir ayrıntı için `Dialog` açılmaz.
tek çekmece açar; veri `/api/program-ayrinti`. Sıra: başlık = program adı, bağlam = tek çekmece açar; veri `/api/program-ayrinti`. Sıra: başlık = program adı, bağlam =
üniversite · il · fakülte; **künye hapları** (§4.12 ölçüsü, link değil: süre + tür, dil, üniversite · il · fakülte; **künye hapları** (§4.12 ölçüsü, link değil: süre + tür, dil,
öğretim türü (örgün değilse), burs/ücret, akreditasyon, baraj, öğretim üyesi sayısı, öğretim türü (örgün değilse), burs/ücret, akreditasyon, baraj, öğretim üyesi sayısı,
yalnız kız/erkek koşulu, kampüs ilçesi — yalnız verisi olanlar); "Kontenjan türleri (yıl)" `dl` satırları yalnız kız/erkek koşulu, kampüs ilçesi — yalnız verisi olanlar); "Kontenjan türleri (yıl)" satırları
(yalnız >0 olanlar; özel kontenjanın ayrı taban sırasının elimizde olmadığı notu); (yalnız >0 olanlar; özel kontenjanın ayrı taban sırasının elimizde olmadığı notu; güncel
düzen aşağıda, 11 Eki 2026);
"Son yerleşenin netleri" (eski tablo + OBP katsayısı satırı; 0,18 = meslek lisesi ek "Son yerleşenin netleri" (eski tablo + OBP katsayısı satırı; 0,18 = meslek lisesi ek
puanı notu); "Yerleşme koşulları (N)" yerel `details` ile kapalı başlar; "Mezun puanı notu); "Yerleşme koşulları (N)" yerel `details` ile kapalı başlar; "Mezun
istihdamı" ve "Üniversite" tek paragraf; en altta kaynak satırı `border-t border-slate-100 istihdamı" ve "Üniversite" tek paragraf; en altta kaynak satırı `border-t border-slate-100
@@ -693,6 +694,37 @@ sihirbaz modalı içindir; yeni bir ayrıntı için `Dialog` açılmaz.
göstergeler çekmecesi (§4.9) aynı bileşenin kartlı modunu kullanır (her açılışta oynar). göstergeler çekmecesi (§4.9) aynı bileşenin kartlı modunu kullanır (her açılışta oynar).
Gövde kaydırılınca içerik tutamaç ve çarpının altına maskesiz girmesin diye `AltSayfaGovde` Gövde kaydırılınca içerik tutamaç ve çarpının altına maskesiz girmesin diye `AltSayfaGovde`
üst kenarında 20 px saydam → dolu `mask-image` var (§4.10; tüm çekmeceler). üst kenarında 20 px saydam → dolu `mask-image` var (§4.10; tüm çekmeceler).
- **11 Eki 2026 — kontenjan türleri (açık panelin içi): önce bütün, sonra parçalar:** Bilal: "daha anlaşılabilir
bir UI." Eski hâl (uzun açıklama paragrafı + iki sütunlu `dl`, "70 kontenjan · 72 yerleşen")
kimin hangi yere girebildiğini söylemiyordu. Yeni sıra: tek cümle toplam (`text-base
text-slate-600`, sayı `font-semibold text-slate-900`: "Toplam 81 kişilik yer açıldı: 70 herkese
açık, 11 yalnız şartı taşıyan adaylara") → oran çubuğu (`flex h-3 gap-0.5 rounded-full`,
dilim genişliği kontenjanla orantılı, `min-w-1.5`; genel `bg-primary`, özel `bg-slate-300`,
`aria-hidden`) → iki grup: "Herkese açık" ve "Yalnız şartı taşıyan adaylara" (grup etiketi
`text-sm font-semibold text-slate-500` + çubukla aynı renkte `size-2.5` nokta). Satır tek
sütun, `border-b border-slate-100 py-3`: solda ad (`text-base font-semibold`) ve altında
kimin girebildiği (`KONTENJAN_TURU_KIME`, `src/lib/ozel-durum.ts`); sağda sayı (`font-heading
text-xl font-bold` + `kişi` birimi) ve altında "N yerleşti · M boş kaldı". Renk yalnız
açık/şartlı ayrımını taşır, türün adı her satırda yazılıdır. Altta not: özel taban sırası
bizde yok; genelde yerleşen kontenjanı aşıyorsa aktarım cümlesi eklenir. Yalnız genel
kontenjan varsa çubuk ve grup yok, tek satır. Kart yok kuralı duruyor.
- **11 Eki 2026 — üç bölüm dönüşen gösterge karesi:** Bilal: "Kontenjan türleri, son
yerleşenin netleri, mezun istihdamı akademik göstergelerdeki gibi kutu olsun; tıklanınca o
kutu morph olarak genişlesin ve içeriğin tam hâli gözüksün." Bu üç bölüm artık künye
haplarının altında §4.11 gösterge kareleridir (`BolumKutulari`; sınıflar §4.11 ile birebir,
masonry: sol sütun Kontenjan `bg-orange-500 min-h-40` + İstihdam `bg-indigo-500 min-h-28`,
sağ sütun `mt-12` Son yerleşen `bg-primary min-h-40`). Kare bir düğmedir: etiket + sayı +
vurgu satırı, sağ üstte `Maximize2 size-4 text-white/80`, `active:scale-[0.97]`. Dokununca
kare **tam genişlikte panele dönüşür** (`motion` `layoutId`, yay `YAY.donusum`; köşe 24 px
stilde ki ölçeklenirken bozulmasın): panel `rounded-3xl bg-slate-50 p-4 sm:p-5`, kenarlıksız;
karenin rengi panelin üstünde `SURE.standart`'ta söner, içerik 0,14 s gecikmeyle belirir.
Panel başlığı: karenin renginde `size-3 rounded-[4px]` kare + `font-heading text-base
font-bold` başlık + §4.3 çarpısı ("küçült"). Aynı anda tek panel açık; kalan iki kare panelin
altında yan yana durur, dokununca yer değiştirir. Odak dönüşen öğeyle gider (açılınca çarpı,
kapanınca kare). "Hareketi azalt"ta dönüşüm yok (`MotionConfig reducedMotion="user"`). Verisi
olmayan bölümün karesi çizilmez. 10 Ekim'deki "bu çekmecede kart yok" kuralı bu üç bölüm için
kalktı; koşullar, üniversite ve kaynak satırı kartsız kalır. Açılış sahnesi değişmedi (logo →
başlık); kareler tek `data-sonra` bloğu olarak netleşir.
- **10 Eki 2026 — büyük logo, logo → başlık (morph):** Bilal: "logo küçücük kalmış; logolar - **10 Eki 2026 — büyük logo, logo → başlık (morph):** Bilal: "logo küçücük kalmış; logolar
küçülürken başlıktaki yazıya morph olsun." Logo kabı `size-44` (akademik göstergelerdeki küçülürken başlıktaki yazıya morph olsun." Logo kabı `size-44` (akademik göstergelerdeki
logoyla aynı ölçek). Kareler sönmek yerine küçülerek (scale 0.2) başlık yazısının üç eşit logoyla aynı ölçek). Kareler sönmek yerine küçülerek (scale 0.2) başlık yazısının üç eşit
@@ -890,43 +922,58 @@ dilindedir, burada kullanılmaz). Kaynak Arc UI "Card stack".
kullanacağız". Dosyalar duruyor; şu an hiçbir sayfada çizilmiyor. kullanacağız". Dosyalar duruyor; şu an hiçbir sayfada çizilmiyor.
### 4.28 Veri akışı — `VeriAkisi`, `src/features/pazarlama/components/veri-akisi.tsx` ### 4.28 Veri akışı — `VeriAkisi`, `src/features/pazarlama/components/veri-akisi.tsx`
Ana sayfa "Nasıl çalışır" bölümünde, üç adımın (§4.32) altında: ortada KolayTercih işareti, Ana sayfa "Nasıl çalışır" bölümünde, üç adımın (§4.32) altında. **Üstte öğrencinin üç seçimi**
çevresinde altı girdi; her girdiden merkeze **düz çizgi** iner, üzerinde piksel kare akar. **Solda (turuncu ikon ve kayıt), **altta YÖK Atlas'ın üç verisi** (mavi ikon ve kayıt), **ortada
öğrencinin üç seçimi** (turuncu ikon ve kare), **sağda YÖK Atlas'ın üç verisi** (mavi ikon ve danışman**: "DanışmanlaKonuş" çekirdeği. Her kutudan çekirdeğe kıvrımlı bir yol iner, üzerinde
kare); ayrımı etiket ve yan taşır, renk eşlik eder (§2.2). Akış CSS'tir (`kt-akis`, globals.css), kayıt akar; ayrımı etiket ve sıra taşır, renk eşlik eder (§2.2). Akış CSS'tir (`kt-akis-*`,
yalnız ekrandayken oynar; hareketi azalt'ta kareler durur. Esin React Bits "Center Flow"; kod globals.css), yalnız ekrandayken oynar; hareketi azalt'ta her yolda bir ham kayıt durağan durur.
bizim — parlama ve bulanıklık yok, akış içeri doğru. Yeni bir girdi eklenirse sihirbaz/havuz Yerleşim ve kaydın yaşam döngüsü Arc UI "Data flow"dan (Bilal, 11 Eki 2026: önce "logoya kareler
kurallarıyla aynı olmalı (`/meraklisina` şemasıyla çelişmez). gelen kısım bunun gibi gözükmeli", sonra "ortadaki kısmı dikdörtgen formuna sokup DanışmanlaKonuş
- **Çizgi düz, karolar beyaz** (Bilal, 7 Eki 2026: "aradaki yerler solid çizgi olsun, etrafındaki yazsak, kutular attığım örnekteki gibi"). Arc'ın bileşeni Pro'dur, kaynağı alınmadı (§11.3
kartların bg'si ve görünümü daha iyi olabilir"). Çizgi `var(--color-slate-300)`, 1,5 px md. 7); önizlemedeki davranış DOM kaydıyla ölçülüp bizim dille yeniden yazıldı — parlama ve
(`vectorEffect="non-scaling-stroke"`), karonun merkezinden işaretin merkezine çekilir ve bulanıklık yok. Yeni bir girdi eklenirse sihirbaz/havuz kurallarıyla aynı olmalı
karoların **altında** kalır; kesik/noktalı çizgi yazılmaz. Girdi karosu (`/meraklisina` şemasıyla çelişmez). Eski altıgen yerleşim (ortada kare logo karosu, çevresinde
`size-14 rounded-2xl border border-slate-200 bg-card` + `size-6` ikon; ortadaki karo altı karo, düz çizgiler) kaldırıldı.
`size-24 rounded-3xl border border-slate-200 bg-card`. Renkli soluk zemin (`bg-orange-50/60`, - **Kutu:** `h-20 w-[30%] rounded-2xl border border-slate-200 bg-card`, ikon üstte (`size-5`,
`bg-primary/10`) kaldırıldı: karo dolu olmalı ki çizgi içinden görünmesin. Gölge yok (§6). yanın renginde), etiket altta (`text-xs font-semibold leading-tight text-balance
text-slate-700`, en çok iki satır). Etiket kutunun **içindedir**; dışarıda yüzen etiket yok.
Üç kutu sütunları kabın %16,7 / %50 / %83,3'ünde. Gölge yok (§6).
- **Çekirdek:** `h-16 rounded-2xl border border-slate-200 bg-card`, içinde danışman panelinin
başlığı (`DanismanBaslik` deseni, `sohbet-client.tsx`): turuncu `AnimasyonluMark`
(`varyant="nabiz" renk="turuncu"`, `size-7`) + `font-bricolage text-xl tracking-tighter`
"Danışmanla" (`font-light`) "Konuş" (`font-semibold`), boşluksuz. İşaret ≈3 sn nabız atar,
sonra girdilerden çıkana dönüşür (liste → trend → sohbet, `bicimler`, §4.34) ve toplanır.
- **Yol:** `var(--color-slate-300)`, 1,5 px, kesiksiz; iki ucu dikey çıkan S kıvrımı
(`C x0 ym, x1 ym`). Yollar çekirdeğin kenarına ortadan 4'er birim açılarak varır. Kutudan
çıktığı yerde 7 px'lik **port** halkası durur.
- **Kayıt ham çıkar, işlenmiş varır.** Ömrü 1,4 sn: porttan küçük ve soluk doğar (ölçek
0,6 → 1), arkasında iki küçük noktadan bir **iz** taşır; yol boyunca **içi boş nokta**dır (ham
girdi: `bg-card` dolgu, 1,5 px renkli çerçeve). Çekirdeğe yaklaşınca hafifçe şişer (1,2),
**dolar ve piksel kareye döner** (işlenmiş), kenarda sönerek içeri alınır. O an **çekirdeğin
çerçevesi** kaydın renginde yanar, genişleyip söner; kalkışta da kutunun çerçevesi ve portu
kısa bir an yanar (tepe %50). Ham ile işlenmiş yalnız renkle değil **biçimle** ayrılır.
- **Ritim:** 6 sn'lik döngüde altı kayıt, 1 sn arayla, bir üstten bir alttan kalkar; yol 1,2 sn
sürdüğü için biri varırken sıradaki yola çıkmıştır, akış hiç durmaz.
- **Kayıt eğriyi JS'siz izler.** Bu S kıvrımında parametre zamanla doğrusal ilerlerken x tam
olarak `cubic-bezier(1/3, 0, 2/3, 1)`, y tam olarak `cubic-bezier(1/3, .5, 2/3, .5)` izler;
kayıt iç içe iki öğedir (dıştaki yatay, içteki dikey kayar) ve çizginin tam üstünde yürür. Bu
iki eğri hareket zevki değil yolun geometrisidir (`--akis-x`, `--akis-y`); `hareket.ts`'e
girmez, yol değişirse onlar da değişir. İz noktaları aynı iki kaymayı 70 ve 140 ms gecikmeyle
yapar. Yatay eksen kabın yüzdesidir (kap `@container`, x'ler `cqw`), dikey eksen rem; ölçüm
ya da `ResizeObserver` yok. Eğri öğenin `animation` kısayolundadır: tarayıcı `@keyframes`
içindeki `animation-timing-function: var(--…)` değerini çözmez, doğrusala düşer.
- **Kartsız, zeminsiz; bilgiler iki yanında, onlar da kartsız** (Bilal, 7 Eki 2026: "kartın - **Kartsız, zeminsiz; bilgiler iki yanında, onlar da kartsız** (Bilal, 7 Eki 2026: "kartın
dışında, arka plansız, sağında solunda da alttaki bilgiler olacak şekilde"; aynı gün: "hero'daki dışında, arka plansız, sağında solunda da alttaki bilgiler olacak şekilde"; aynı gün: "hero'daki
gibi ferah"). Geniş ekranda (lg+) üç sütun (`lg:grid-cols-[minmax(0,1fr)_22rem_minmax(0,1fr)]`, gibi ferah"). Geniş ekranda (lg+) üç sütun (`lg:grid-cols-[minmax(0,1fr)_22rem_minmax(0,1fr)]`,
xl'de orta 24rem, dikeyde ortalı): **solda dürüstlük notu** — `SectionEyebrow` + `h3` xl'de orta 24rem, dikeyde ortalı): **solda dürüstlük notu** — `SectionEyebrow` + `h3`
(`font-heading text-2xl font-bold text-balance`, ikinci yarısı hero başlığındaki vurgu: (`font-heading text-2xl font-bold text-balance`, ikinci yarısı hero başlığındaki vurgu:
`font-bricolage font-bold tracking-tighter text-primary italic`) + `text-base` paragraf, sola `font-bricolage font-bold tracking-tighter text-primary italic`) + `text-base` paragraf, sola
yaslı, `max-w-xs`; **ortada animasyon**; **sağda üç sayı** alt alta, sağa yaslı (YÖK Atlas yaslı, `max-w-xs`; **ortada animasyon**; **sağda üç sayı** alt alta, sağa yaslı (sayılar
girdilerinin yanında — sayılar o verinin sayılarıdır). Sayı §1.1: `font-heading text-4xl alt sıradaki YÖK Atlas verisinin sayılarıdır). Sayı §1.1: `font-heading text-4xl
font-bold leading-none tracking-tight text-primary sm:text-5xl`, birim ("yıl") §3'teki gibi font-bold leading-none tracking-tight text-primary sm:text-5xl`, birim ("yıl") §3'teki gibi
`font-sans text-base font-semibold text-slate-400`, etiket `text-sm font-semibold `font-sans text-base font-semibold text-slate-400`, etiket `text-sm font-semibold
text-slate-500`. Çerçeve, zemin, ayırıcı çizgi ve sayının yanında piksel süs **yok**. text-slate-500`. Çerçeve, zemin, ayırıcı çizgi ve sayının yanında piksel süs **yok**.
lg altında tek sütun, her şey ortalı: animasyon → sayılar (sm'de üçü yan yana) → dürüstlük notu. lg altında tek sütun, her şey ortalı: animasyon → sayılar (sm'de üçü yan yana) → dürüstlük notu.
- **Ortadaki işaret danışmandır: nabız atar, sonra çıktıya dönüşür** (Bilal, 11 Eki 2026:
"logo morph ile ikonlara dönüşsün; belirli süre DanışmanlaKonuş'un animasyonu oynayıp morph
olsun, şu ankini kaldır"). İşaret ≈3 sn DanışmanlaKonuş başlığındaki gibi yerinde nabız atar
(`varyant="nabiz"`: danışman düşünüyor), sonra üç kare girdilerden çıkan şeye dönüşür, ≈1,3 sn
durur ve logoya toplanır (toplam 2,5 sn); nabız yerinden sürer, sıra bir sonraki biçime
geçer. Sıra paketin verdiğidir: **liste** (deste) → **trend** (yükselen üç çubuk) → **sohbet**
(iki balon). Biçimler §4.34'tekilerle aynı dilde, üç kareden ve logonun üç renginden
(`AnimasyonluMark` `bicimler`). Yalnız ekrandayken oynar; ekrandan çıkınca kareler yerine
oturur. Hareketi azalt'ta durağan logo. Eski "rota" (kareler birbirinin yerine süzülür) bu
karodan kaldırıldı; ondan önceki nabız + tüm karoyu döndüren hâl (`kt-akis-donus`) de yanlıştı
(Bilal, 7 Eki 2026).
### 4.29 Program duvarı — `ProgramDuvari`, `src/features/pazarlama/components/program-duvari.tsx` ### 4.29 Program duvarı — `ProgramDuvari`, `src/features/pazarlama/components/program-duvari.tsx`
**Şu an sayfada kullanılmıyor:** ana sayfa "Sorun" bölümünden 7 Eki 2026'da çıkarıldı (Bilal: **Şu an sayfada kullanılmıyor:** ana sayfa "Sorun" bölümünden 7 Eki 2026'da çıkarıldı (Bilal:

View File

@@ -455,39 +455,277 @@
} }
/* Veri akışı (veri-akisi.tsx, DESIGN.md §4.28): kare, çizgi boyunca karodan /* Veri akışı (veri-akisi.tsx, DESIGN.md §4.28). Üstte üç kutu, ortada
merkeze akar. Uç noktalar satır içi --x1/--y1/--x2/--y2 ile gelir (SVG çekirdek, altta üç kutu; yollar SVG, kayıtlar HTML (yalnız translate /
içinde px = viewBox birimi). Yalnız ekrandayken oynar. */ scale / opacity oynar).
@keyframes kt-akis {
Geometri: yatay eksen kabın eninin yüzdesi (kap `@container`, x'ler cqw),
dikey eksen rem: kutular 5rem, çekirdek 10,5–14,5rem arası, sahne 25rem.
Kaydın iki ucu satır içi gelir (--x0/--y0 port, --x1/--y1 çekirdek kenarı).
Eğri: yol, iki ucu dikey çıkan S kıvrımıdır — C(x0 ym, x1 ym), ym orta
yükseklik. Bu eğride parametre zamanla doğrusal ilerlerken x tam olarak
cubic-bezier(1/3, 0, 2/3, 1), y tam olarak cubic-bezier(1/3, .5, 2/3, .5)
izler. Kayıt bu yüzden iç içe iki öğedir: dıştaki yatay, içteki dikey
kayar; ikisi birlikte çizginin tam üstünde yürür, ölçüm ya da JS gerekmez.
Bu iki eğri hareket zevki değil yolun geometrisidir (--akis-x / --akis-y);
yol değişirse onlar da değişir. İz, aynı iki kaymayı --ek kadar gecikmeyle
yapan iki küçük noktadır; böylece o da çizginin üstünde kalır.
Döngü 6 sn, altı kayıt 1 sn arayla (--g satır içi gecikme). Bir kaydın ömrü:
0 → 20 % yol (1,2 sn)
0 → 4 % kalkış: porttan küçük ve soluk çıkar (ölçek 0,6 → 1)
13 → 17 % işlenir: içi boş nokta şişer (1 → 1,2), dolar ve piksel kareye döner
15 → 19 % iz çekirdek kenarına varmadan söner
20 → 23 % kayıt kenarda sönerek içeri alınır
20 → 32 % çekirdek nabzı: çerçeve kaydın renginde yanar, genişleyip söner
0 → 10 % kaynak vurgusu: kutunun çerçevesi ve port kısa bir an yanar
Eğri öğenin `animation` kısayolundadır, keyframe'in içinde değil: tarayıcı
`@keyframes` içindeki `animation-timing-function: var(--…)` değerini çözmez
(doğrusala düşer). Kısayoldaki eğri her durak aralığına ayrı ayrı uygulanır.
Yalnız ekrandayken oynar (data-oynuyor). Hareketi azalt'ta her yolda bir
ham kayıt durağan durur. */
.kt-akis-sahne {
height: 25rem;
--akis-x: cubic-bezier(0.3333, 0, 0.6667, 1);
--akis-y: cubic-bezier(0.3333, 0.5, 0.6667, 0.5);
}
.kt-akis-kayit-kap {
position: absolute;
top: 0;
left: 0;
width: 0;
height: 0;
pointer-events: none;
}
/* Port: yolun kutudan çıktığı nokta; kayıt buradan doğar. */
.kt-akis-port {
position: absolute;
top: -3.5px;
left: -3.5px;
width: 7px;
height: 7px;
border: 1px solid var(--color-slate-300);
border-radius: 9999px;
background: var(--card);
translate: var(--x0) var(--y0);
}
.kt-akis-port > .kt-akis-vurgu {
position: absolute;
inset: 1px;
border-radius: inherit;
background: var(--renk);
}
.kt-akis-x,
.kt-akis-y {
position: absolute;
top: 0;
left: 0;
}
.kt-akis-x {
translate: var(--x0) 0;
animation: kt-akis-x 6s var(--akis-x) calc(var(--g, 0s) + var(--ek, 0s)) infinite;
}
.kt-akis-y {
translate: 0 var(--y0);
animation: kt-akis-y 6s var(--akis-y) calc(var(--g, 0s) + var(--ek, 0s)) infinite;
}
/* İz noktası: kayıttan küçük, dolu ve soluk. */
.kt-akis-iz {
position: absolute;
top: -2px;
left: -2px;
width: 4px;
height: 4px;
border-radius: 9999px;
background: var(--renk);
opacity: 0;
animation: kt-akis-iz 6s var(--egri-standart) calc(var(--g, 0s) + var(--ek, 0s)) infinite;
}
/* Ham kayıt: içi boş nokta. */
.kt-akis-kare {
position: absolute;
top: -5px;
left: -5px;
box-sizing: border-box;
width: 10px;
height: 10px;
border: 1.5px solid var(--renk);
border-radius: 50%;
background: var(--card);
opacity: 0;
scale: 0.6;
animation: kt-akis-kare 6s var(--egri-standart) var(--g, 0s) infinite;
}
/* İşlenmiş kayıt: aynı kutunun dolusu, üstüne solarak gelir. */
.kt-akis-dolgu {
position: absolute;
inset: 0;
border-radius: inherit;
background: var(--renk);
opacity: 0;
animation: kt-akis-dolgu 6s var(--egri-standart) var(--g, 0s) infinite;
}
.kt-akis-nabiz,
.kt-akis-vurgu {
border-color: var(--renk);
opacity: 0;
}
.kt-akis-nabiz {
animation: kt-akis-nabiz 6s var(--egri-guclu-cikis) var(--g, 0s) infinite;
}
.kt-akis-vurgu {
animation: kt-akis-vurgu 6s var(--egri-standart) var(--g, 0s) infinite;
}
@keyframes kt-akis-x {
0% { 0% {
translate: var(--x1) var(--y1); translate: var(--x0) 0;
}
20%,
100% {
translate: var(--x1) 0;
}
}
@keyframes kt-akis-y {
0% {
translate: 0 var(--y0);
}
20%,
100% {
translate: 0 var(--y1);
}
}
@keyframes kt-akis-iz {
0%,
3% {
opacity: 0; opacity: 0;
} }
12%, 7%,
82% { 15% {
opacity: 1; opacity: 0.4;
} }
19%,
100% { 100% {
translate: var(--x2) var(--y2);
opacity: 0; opacity: 0;
} }
} }
.kt-akis-kare { @keyframes kt-akis-kare {
translate: var(--x1) var(--y1); 0% {
scale: 0.6;
opacity: 0; opacity: 0;
animation: kt-akis 2.4s cubic-bezier(0.45, 0, 0.55, 1) infinite; border-radius: 50%;
}
4%,
13% {
scale: 1;
opacity: 1;
border-radius: 50%;
}
17% {
scale: 1.2;
opacity: 1;
border-radius: 25%;
}
20% {
scale: 1;
opacity: 1;
border-radius: 25%;
}
23%,
100% {
scale: 0.8;
opacity: 0;
border-radius: 25%;
}
}
@keyframes kt-akis-dolgu {
0%,
13% {
opacity: 0;
}
17%,
100% {
opacity: 1;
}
}
/* Çekirdek geniş bir dikdörtgen: yatayda az, dikeyde çok büyür ki halka her
yanda aynı kadar (≈6 px) açılsın. */
@keyframes kt-akis-nabiz {
0%,
19.9% {
scale: 1;
opacity: 0;
}
20% {
scale: 1;
opacity: 0.5;
}
32%,
100% {
scale: 1.06 1.2;
opacity: 0;
}
}
@keyframes kt-akis-vurgu {
0% {
opacity: 0;
}
2% {
opacity: 0.5;
}
10%,
100% {
opacity: 0;
}
}
/* Oynatma durumu `animation` kısayolundan sonra gelmeli (kısayol sıfırlar). */
:is(.kt-akis-x, .kt-akis-y, .kt-akis-iz, .kt-akis-kare, .kt-akis-dolgu, .kt-akis-nabiz, .kt-akis-vurgu) {
animation-play-state: paused; animation-play-state: paused;
} }
[data-oynuyor] .kt-akis-kare { [data-oynuyor]
:is(.kt-akis-x, .kt-akis-y, .kt-akis-iz, .kt-akis-kare, .kt-akis-dolgu, .kt-akis-nabiz, .kt-akis-vurgu) {
animation-play-state: running; animation-play-state: running;
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
.kt-akis-kare { :is(.kt-akis-x, .kt-akis-y, .kt-akis-iz, .kt-akis-kare, .kt-akis-dolgu, .kt-akis-nabiz, .kt-akis-vurgu) {
animation: none; animation: none;
} }
/* Durağan kare: yolun ortasında ham kayıt — bilgi eksilmez. */
.kt-akis-x {
translate: calc((var(--x0) + var(--x1)) / 2) 0;
}
.kt-akis-y {
translate: 0 calc((var(--y0) + var(--y1)) / 2);
}
.kt-akis-kare {
opacity: 1;
scale: 1;
}
} }
/* Tema geçişi (DESIGN.md §2.3, lib/tema-gecisi.ts): yeni tema header'daki /* Tema geçişi (DESIGN.md §2.3, lib/tema-gecisi.ts): yeni tema header'daki

View File

@@ -7,7 +7,9 @@
// türleri, son yerleşenin netleri, yerleşme koşulları, TÜİK mezun istihdamı, // türleri, son yerleşenin netleri, yerleşme koşulları, TÜİK mezun istihdamı,
// üniversite özeti. API'nin `gecmis` (yıllara göre sıra/puan/kontenjan) // üniversite özeti. API'nin `gecmis` (yıllara göre sıra/puan/kontenjan)
// alanı burada gösterilmez (Bilal, 7 Eki 2026: yıl tablosu kaldırıldı; // alanı burada gösterilmez (Bilal, 7 Eki 2026: yıl tablosu kaldırıldı;
// backlog). Açılış: veri ilk gelince logo kareleri belirir, söner, içerik // backlog). Kontenjan türleri, netler ve mezun istihdamı kapalıyken gösterge
// karesidir; dokununca kare panele dönüşüp bölümün tamamını açar
// (BolumKutulari; Bilal, 11 Eki 2026). Açılış: veri ilk gelince logo kareleri belirir, söner, içerik
// bloklar hâlinde netleşir (AcilisSahnesi kartsız modu); veri önbellekteyken // bloklar hâlinde netleşir (AcilisSahnesi kartsız modu); veri önbellekteyken
// (çekmece yeniden açıldı) sahne yok, içerik 0,25 s solmayla gelir. // (çekmece yeniden açıldı) sahne yok, içerik 0,25 s solmayla gelir.
// Kadın-erkek dağılımı program düzeyinde bilinçli olarak yok: YÖK Atlas 2026 // Kadın-erkek dağılımı program düzeyinde bilinçli olarak yok: YÖK Atlas 2026
@@ -18,7 +20,8 @@
// değiştiği için takılmış görünüyordu (7 Eki 2026). // değiştiği için takılmış görünüyordu (7 Eki 2026).
import { SATIR_BASLIK, SATIR_DUGME_BOYU } from "@/components/liste-satiri"; import { SATIR_BASLIK, SATIR_DUGME_BOYU } from "@/components/liste-satiri";
import { useState } from "react"; import { useEffect, useRef, useState, type ReactNode } from "react";
import { LayoutGroup, MotionConfig, motion } from "motion/react";
import { import {
BadgeCheck, BadgeCheck,
Clock, Clock,
@@ -27,17 +30,27 @@ import {
Info, Info,
Languages, Languages,
MapPin, MapPin,
Maximize2,
Monitor, Monitor,
Users, Users,
Wallet, Wallet,
X, X,
} from "lucide-react"; } from "lucide-react";
import type { ProgramAyrintisi } from "@/app/api/program-ayrinti/route"; import type { ProgramAyrintisi } from "@/app/api/program-ayrinti/route";
import type { KontenjanTuruAnahtari, ProgramNetSatiri } from "@/types/yokatlas"; import type {
KontenjanTuruAnahtari,
KontenjanTurleri,
ProgramNetSatiri,
} from "@/types/yokatlas";
import { olay } from "@/lib/analitik"; import { olay } from "@/lib/analitik";
import { SURE, YAY } from "@/lib/hareket";
// Yalnız saf modüller: bu bileşen istemcide çalışır, db'ye dokunan // Yalnız saf modüller: bu bileşen istemcide çalışır, db'ye dokunan
// program-ozellik.ts import EDİLMEZ (kadro toplamı API'den gelir) // program-ozellik.ts import EDİLMEZ (kadro toplamı API'den gelir)
import { CINSIYET_KOSULU_METNI, KONTENJAN_TURU_ADI } from "@/lib/ozel-durum"; import {
CINSIYET_KOSULU_METNI,
KONTENJAN_TURU_ADI,
KONTENJAN_TURU_KIME,
} from "@/lib/ozel-durum";
import { trBaslikDuzeni } from "@/lib/slug"; import { trBaslikDuzeni } from "@/lib/slug";
import { UNITUR_ETIKET } from "@/lib/unitur"; import { UNITUR_ETIKET } from "@/lib/unitur";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@@ -252,18 +265,8 @@ function AyrintiIcerik({ durum, veri }: { durum: Durum; veri: ProgramAyrintisi |
))} ))}
</div> </div>
<Kunye veri={veri} /> <Kunye veri={veri} />
<KontenjanTurleriBolumu veri={veri} /> <BolumKutulari veri={veri} />
<div data-sonra>
<h3 className={BOLUM_BASLIK}>Son yerleşenin netleri</h3>
<p className={NOT}>
Bölüme en düşük puanla yerleşen adayın sınav netleri (YÖK Atlas).
</p>
<div className="mt-3">
<NetlerIcerik netler={veri.netler} />
</div>
</div>
<Kosullar veri={veri} /> <Kosullar veri={veri} />
<Istihdam veri={veri} />
<UniversiteOzeti veri={veri} /> <UniversiteOzeti veri={veri} />
<p data-sonra className="mt-7 border-t border-slate-100 pt-3 text-xs leading-5 text-slate-500"> <p data-sonra className="mt-7 border-t border-slate-100 pt-3 text-xs leading-5 text-slate-500">
Kaynaklar: YÖK Atlas (yerleştirme, kontenjan, netler, koşullar; {SON_YIL}{" "} Kaynaklar: YÖK Atlas (yerleştirme, kontenjan, netler, koşullar; {SON_YIL}{" "}
@@ -316,35 +319,364 @@ function Kunye({ veri }: { veri: ProgramAyrintisi }) {
); );
} }
function KontenjanTurleriBolumu({ veri }: { veri: ProgramAyrintisi }) { /** "2 yerleşti · 1 boş kaldı" — yerleşen kontenjanı aşabilir (aktarım), o zaman yalnız sayı. */
const k = veri.kontenjanTurleri; function yerlesenMetni(kontenjan: number, yerlesen: number | null): string {
if (!k) return null; if (yerlesen == null) return "Yerleşen verisi yok";
const satirlar = KONTENJAN_TURU_SIRASI.filter((t) => (k[t].kontenjan ?? 0) > 0); if (yerlesen === 0) return "Yerleşen olmadı";
if (satirlar.length === 0) return null; const bos = kontenjan - yerlesen;
const ozelVar = satirlar.some((t) => t !== "genel"); return bos > 0 ? `${sayi(yerlesen)} yerleşti · ${sayi(bos)} boş kaldı` : `${sayi(yerlesen)} yerleşti`;
}
function KontenjanSatiri({ tur, k }: { tur: KontenjanTuruAnahtari; k: KontenjanTurleri }) {
const kontenjan = k[tur].kontenjan ?? 0;
return ( return (
<div data-sonra> <li className="flex items-start justify-between gap-4 border-b border-slate-100 py-3">
<h3 className={BOLUM_BASLIK}>Kontenjan türleri ({k.yil})</h3> <div className="min-w-0">
<p className={NOT}> <p className="text-base font-semibold text-slate-900">{KONTENJAN_TURU_ADI[tur]}</p>
{ozelVar <p className="text-sm leading-snug text-slate-500">{KONTENJAN_TURU_KIME[tur]}</p>
? "ÖSYM okul birincileri, şehit ve gazi yakınları, depremzedeler ve 34 yaş üstü kadınlar için ayrı kontenjan ayırır; bu kontenjanlar yalnız o durumdaki adaylara açıktır ve her birinin taban sırası ayrı yayımlanır — o sıra burada yok." </div>
: "Bu programda yalnız genel kontenjan var."} <div className="shrink-0 text-right">
<p className="font-heading text-xl font-bold leading-6 text-slate-900">
{sayi(kontenjan)}
<span className="font-sans text-sm font-semibold text-slate-400"> kişi</span>
</p>
<p className="text-sm leading-snug tabular-nums text-slate-500">
{yerlesenMetni(kontenjan, k[tur].yerlesen)}
</p> </p>
<dl className="mt-3 grid gap-x-6 gap-y-2 sm:grid-cols-2">
{satirlar.map((t) => (
<div key={t} className="flex items-baseline justify-between gap-3 border-b border-slate-100 py-2">
<dt className="text-sm text-slate-700">{KONTENJAN_TURU_ADI[t]}</dt>
<dd className="text-right text-sm tabular-nums text-slate-900">
<span className="font-semibold">{sayi(k[t].kontenjan ?? 0)}</span>
<span className="text-slate-500">
{" "}
kontenjan · {k[t].yerlesen != null ? sayi(k[t].yerlesen) : "—"} yerleşen
</span>
</dd>
</div> </div>
</li>
);
}
const GRUP_ETIKET = "flex items-center gap-2 text-sm font-semibold text-slate-500";
/** Kontenjan kırılımının özeti: kutudaki sayı ve açık paneldeki satırlar aynı hesaptan. */
function kontenjanOzeti(k: KontenjanTurleri) {
const satirlar = KONTENJAN_TURU_SIRASI.filter((t) => (k[t].kontenjan ?? 0) > 0);
const adet = (t: KontenjanTuruAnahtari) => k[t].kontenjan ?? 0;
const ozel = satirlar.filter((t) => t !== "genel");
const genelVar = satirlar.includes("genel");
const genel = genelVar ? adet("genel") : 0;
const ozelToplam = ozel.reduce((a, t) => a + adet(t), 0);
return {
satirlar,
ozel,
genelVar,
genel,
ozelToplam,
toplam: genel + ozelToplam,
// Dolmayan özel kontenjan genele aktarılır: genelde yerleşen kontenjanı aşar.
aktarim: genelVar && (k.genel.yerlesen ?? 0) > genel,
};
}
// Önce bütün (toplam + oran çubuğu), sonra parçalar: herkese açık yer ile
// yalnız şartı taşıyana açık yer iki ayrı grup. Çubukta yalnız bu ikili ayrım
// renklidir (mavi / nötr); türün adı her satırda yazılı, renk tek başına
// anlam taşımaz.
function KontenjanGovde({ k }: { k: KontenjanTurleri }) {
const o = kontenjanOzeti(k);
if (o.ozel.length === 0) {
return (
<>
<p className={NOT}>
Bu programda yalnız genel kontenjan var; özel durumlar için ayrı yer ayrılmamış.
</p>
<ul className="mt-1">
<KontenjanSatiri tur="genel" k={k} />
</ul>
</>
);
}
return (
<>
<p className="mt-2 text-base leading-relaxed text-slate-600">
Toplam <span className="font-semibold text-slate-900">{sayi(o.toplam)} kişilik</span> yer
açıldı
{o.genelVar
? `: ${sayi(o.genel)} herkese açık, ${sayi(o.ozelToplam)} yalnız şartı taşıyan adaylara.`
: "; tamamı yalnız şartı taşıyan adaylara."}
</p>
<div className="mt-3 flex h-3 gap-0.5 overflow-hidden rounded-full" aria-hidden>
{o.satirlar.map((t) => (
<span
key={t}
className={cn("min-w-1.5", t === "genel" ? "bg-primary" : "bg-slate-300")}
style={{ flexGrow: k[t].kontenjan ?? 0, flexBasis: 0 }}
/>
))} ))}
</dl>
</div> </div>
{o.genelVar ? (
<>
<p className={cn(GRUP_ETIKET, "mt-5")}>
<span className="size-2.5 rounded-full bg-primary" aria-hidden />
Herkese açık
</p>
<ul>
<KontenjanSatiri tur="genel" k={k} />
</ul>
</>
) : null}
<p className={cn(GRUP_ETIKET, "mt-5")}>
<span className="size-2.5 rounded-full bg-slate-300" aria-hidden />
Yalnız şartı taşıyan adaylara
</p>
<ul>
{o.ozel.map((t) => (
<KontenjanSatiri key={t} tur={t} k={k} />
))}
</ul>
<p className={cn(NOT, "mt-3")}>
Özel kontenjanların taban sırası ayrı yayımlanır; o sıra burada yok.
{o.aktarim
? " Dolmayan özel kontenjan genele aktarılır; genel kontenjanda yerleşenin kontenjanı aşması bundandır."
: ""}
</p>
</>
);
}
// Gösterge karesi (DESIGN.md §4.11) — akademik göstergelerdeki karelerle aynı
// sınıf seti; burada kare bir düğmedir.
const KARE = "flex flex-col items-center justify-center rounded-3xl px-3 text-white";
const KARE_ETIKET = "text-sm font-bold text-white/90";
const KARE_DEV_SAYI = "font-heading text-5xl font-bold leading-none tracking-tight";
const KARE_ORTA_SAYI = "font-heading text-3xl font-bold leading-none";
const KARE_VURGU = "text-base font-bold text-white/90";
const KOSE = 24; // rounded-3xl; motion köşeyi ölçek sırasında düzeltsin diye stilde
type KutuAnahtari = "kontenjan" | "netler" | "istihdam";
type Kutu = {
anahtar: KutuAnahtari;
baslik: string;
/** Karenin zemini (§2 "Gösterge karesi"); anlam taşımaz, etiket yazılı */
renk: string;
boy: string;
ozet: ReactNode;
icerik: ReactNode;
};
function kutulariKur(veri: ProgramAyrintisi): Kutu[] {
const kutular: Kutu[] = [];
const k = veri.kontenjanTurleri;
const o = k ? kontenjanOzeti(k) : null;
if (k && o && o.satirlar.length > 0) {
kutular.push({
anahtar: "kontenjan",
baslik: `Kontenjan türleri (${k.yil})`,
renk: "bg-orange-500",
boy: "min-h-40 py-4",
ozet: (
<>
<span className={KARE_ETIKET}>Kontenjan</span>
<span className={cn("mt-3", KARE_DEV_SAYI)}>{sayi(o.toplam)}</span>
<span className={cn("mt-2", KARE_VURGU)}>
{o.ozel.length === 0
? "Tamamı genel"
: o.genelVar
? `${sayi(o.genel)} genel · ${sayi(o.ozelToplam)} özel`
: "Tamamı özel"}
</span>
</>
),
icerik: <KontenjanGovde k={k} />,
});
}
if (veri.netler.length > 0) {
const son = veri.netler.reduce((a, b) => (b.yil > a.yil ? b : a));
kutular.push({
anahtar: "netler",
baslik: "Son yerleşenin netleri",
renk: "bg-primary",
boy: "min-h-40 py-4",
ozet:
son.tabanPuan != null ? (
<>
<span className={KARE_ETIKET}>Son yerleşen</span>
<span className={cn("mt-3", KARE_ORTA_SAYI)}>{ondalik(son.tabanPuan)}</span>
<span className={cn("mt-2", KARE_VURGU)}>Puan ve netler</span>
</>
) : (
<>
<span className={KARE_ORTA_SAYI}>{son.yil}</span>
<span className={cn("mt-1.5", KARE_ETIKET)}>Son yerleşenin netleri</span>
</>
),
icerik: (
<>
<p className={NOT}>
Bölüme en düşük puanla yerleşen adayın sınav netleri (YÖK Atlas).
</p>
<div className="mt-3">
<NetlerIcerik netler={veri.netler} />
</div>
</>
),
});
}
const istihdamCumlesi = istihdamMetni(veri);
const v = veri.istihdam;
if (v && istihdamCumlesi) {
kutular.push({
anahtar: "istihdam",
baslik: "Mezun istihdamı",
renk: "bg-indigo-500",
boy: "min-h-28 py-3",
ozet:
v.istihdam != null ? (
<>
<span className={KARE_ORTA_SAYI}>%{ondalik(v.istihdam)}</span>
<span className={cn("mt-1.5", KARE_ETIKET)}>İşe yerleşen mezun</span>
</>
) : v.isBulmaAy != null ? (
<>
<span className={KARE_ORTA_SAYI}>{ondalik(v.isBulmaAy)} ay</span>
<span className={cn("mt-1.5", KARE_ETIKET)}>İlk işi bulma</span>
</>
) : (
<>
<span className={KARE_ORTA_SAYI}>{v.kazanc}</span>
<span className={cn("mt-1.5", KARE_ETIKET)}>Kazanç grubu</span>
</>
),
icerik: <p className={NOT}>{istihdamCumlesi}</p>,
});
}
return kutular;
}
/**
* Kontenjan türleri · Son yerleşenin netleri · Mezun istihdamı: kapalıyken
* gösterge kareleri (§4.11 masonry), dokununca kare tam genişlikte panele
* dönüşür ve bölümün tamamı açılır (Bilal, 11 Eki 2026). Aynı anda tek panel
* açıktır; öbür kareler panelin altında yan yana kalır ve dokununca yer
* değiştirir. Dönüşüm `layoutId` ile (kare → panel aynı öğe sayılır), yay
* `YAY.donusum`; "hareketi azalt"ta dönüşüm yok, içerik doğrudan değişir.
*/
function BolumKutulari({ veri }: { veri: ProgramAyrintisi }) {
const [acik, setAcik] = useState<KutuAnahtari | null>(null);
// Az önce kapanan kare: panelden küçülürken içi ezik görünmesin diye solarak gelir.
const [kapanan, setKapanan] = useState<KutuAnahtari | null>(null);
const kapatRef = useRef<HTMLButtonElement>(null);
const kareRef = useRef<Partial<Record<KutuAnahtari, HTMLButtonElement | null>>>({});
const ilk = useRef(true);
// Odak dönüşen öğeyle birlikte gider: açılınca çarpıya, kapanınca kareye.
useEffect(() => {
if (ilk.current) {
ilk.current = false;
return;
}
if (acik) kapatRef.current?.focus({ preventScroll: true });
else if (kapanan) kareRef.current[kapanan]?.focus({ preventScroll: true });
}, [acik, kapanan]);
const kutular = kutulariKur(veri);
if (kutular.length === 0) return null;
const acikKutu = kutular.find((x) => x.anahtar === acik) ?? null;
const kapali = kutular.filter((x) => x.anahtar !== acik);
// Kapalıyken masonry (sol: 1. ve 3., sağ: 2., aşağı kaymış); panel açıkken
// kalan kareler yan yana.
const sol = kapali.filter((_, i) => i % 2 === 0);
const sag = kapali.filter((_, i) => i % 2 === 1);
function ac(anahtar: KutuAnahtari) {
setKapanan(acik);
setAcik(anahtar);
}
function kapat() {
setKapanan(acik);
setAcik(null);
}
const kare = (x: Kutu) => (
<motion.button
key={x.anahtar}
ref={(el) => {
kareRef.current[x.anahtar] = el;
}}
layoutId={`ayrinti-kutu-${x.anahtar}`}
type="button"
aria-expanded={false}
onClick={() => ac(x.anahtar)}
style={{ borderRadius: KOSE }}
className={cn(
KARE,
"relative w-full cursor-pointer transition-[scale] duration-150 active:scale-[0.97] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
x.renk,
x.boy,
)}
>
<motion.span
className="flex flex-col items-center"
initial={kapanan === x.anahtar ? { opacity: 0 } : false}
animate={{ opacity: 1 }}
transition={{ duration: SURE.standart, delay: 0.1 }}
>
{x.ozet}
</motion.span>
<Maximize2 className="absolute top-3 right-3 size-4 text-white/80" aria-hidden />
<span className="sr-only"> — {x.baslik}, tamamını göster</span>
</motion.button>
);
return (
<MotionConfig reducedMotion="user" transition={YAY.donusum}>
<LayoutGroup>
<div data-sonra className="mt-7">
{acikKutu ? (
<motion.section
key={acikKutu.anahtar}
layoutId={`ayrinti-kutu-${acikKutu.anahtar}`}
aria-label={acikKutu.baslik}
style={{ borderRadius: KOSE }}
className="relative overflow-hidden bg-slate-50 p-4 sm:p-5"
>
{/* Karenin rengi panelin üstünde söner: renk de biçimle birlikte dönüşür. */}
<motion.span
aria-hidden
className={cn("pointer-events-none absolute inset-0", acikKutu.renk)}
initial={{ opacity: 1 }}
animate={{ opacity: 0 }}
transition={{ duration: SURE.standart, ease: "easeOut" }}
/>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: SURE.standart, delay: 0.14 }}
>
<div className="flex items-center justify-between gap-3">
<h3 className="flex items-center gap-2.5 font-heading text-base font-bold text-slate-900">
<span className={cn("size-3 shrink-0 rounded-[4px]", acikKutu.renk)} aria-hidden />
{acikKutu.baslik}
</h3>
<button
ref={kapatRef}
type="button"
onClick={kapat}
className="flex size-9 shrink-0 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-card text-slate-500 transition-[background-color,transform] duration-150 hover:bg-slate-100 hover:text-slate-800 active:scale-[0.96] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
>
<X className="size-4" aria-hidden />
<span className="sr-only">{acikKutu.baslik} bölümünü küçült</span>
</button>
</div>
{acikKutu.icerik}
</motion.div>
</motion.section>
) : null}
{kapali.length > 0 ? (
<div className={cn("flex items-start gap-3", acikKutu && "mt-3")}>
<div className="flex flex-1 flex-col gap-3">{sol.map(kare)}</div>
{sag.length > 0 ? (
<div className={cn("flex flex-1 flex-col gap-3", !acikKutu && sol.length > 0 && "mt-12")}>
{sag.map(kare)}
</div>
) : null}
</div>
) : null}
</div>
</LayoutGroup>
</MotionConfig>
); );
} }
@@ -480,7 +812,8 @@ function Kosullar({ veri }: { veri: ProgramAyrintisi }) {
); );
} }
function Istihdam({ veri }: { veri: ProgramAyrintisi }) { /** Mezun istihdamı cümlesi (TÜİK); gösterecek değer yoksa null. */
function istihdamMetni(veri: ProgramAyrintisi): string | null {
const v = veri.istihdam; const v = veri.istihdam;
if (!v) return null; if (!v) return null;
const parcalar = [ const parcalar = [
@@ -493,15 +826,8 @@ function Istihdam({ veri }: { veri: ProgramAyrintisi }) {
const birlesik = parcalar.join(", "); const birlesik = parcalar.join(", ");
const cumle = birlesik.charAt(0).toLocaleUpperCase("tr-TR") + birlesik.slice(1); const cumle = birlesik.charAt(0).toLocaleUpperCase("tr-TR") + birlesik.slice(1);
const ort = v.ortalama.istihdam != null ? ` Türkiye ortalaması %${ondalik(v.ortalama.istihdam)}.` : ""; const ort = v.ortalama.istihdam != null ? ` Türkiye ortalaması %${ondalik(v.ortalama.istihdam)}.` : "";
return ( const duzey = v.duzey === "lisans" ? "lisans" : "ön lisans";
<div data-sonra> return `${cumle} (TÜİK ${v.yil}, ülke geneli, ${duzey} mezunları; üniversiteye göre değişir, kırılım yok).${ort}`;
<h3 className={BOLUM_BASLIK}>Mezun istihdamı</h3>
<p className={NOT}>
{cumle} (TÜİK {v.yil}, ülke geneli, {v.duzey === "lisans" ? "lisans" : "ön lisans"}{" "}
mezunları; üniversiteye göre değişir, kırılım yok).{ort}
</p>
</div>
);
} }
function UniversiteOzeti({ veri }: { veri: ProgramAyrintisi }) { function UniversiteOzeti({ veri }: { veri: ProgramAyrintisi }) {

View File

@@ -162,6 +162,15 @@ export function TercihHaritasi({
} }
}, []); }, []);
// Kenar eritme mask'ı yalnızca yakınlaşmış hâlde: tam görünümde Türkiye'nin
// uçları (Edirne, Hakkari) rampaya giriyor ve harita kırpılmış gibi
// okunuyordu (Bilal, 11 Eki 2026). Yakınlaşırken hemen açılır (ilk karede
// kenarlar boş, geçiş görünmez); uzaklaşırken animasyon bitene kadar kalır
// ki komşu iller sert kenarla kırpılmış görünmesin.
// Uzaklaşma animasyonu sürerken true; rAF içinde yazılır (effect'te senkron
// setState değil), bitince düşer.
const [uzaklasiyor, setUzaklasiyor] = useState(false);
const animateTo = useCallback((hedef: ViewBox) => { const animateTo = useCallback((hedef: ViewBox) => {
cancelAnimationFrame(animRef.current); cancelAnimationFrame(animRef.current);
const kaynak = { ...vbRef.current }; const kaynak = { ...vbRef.current };
@@ -175,6 +184,8 @@ export function TercihHaritasi({
// sf'e bağlı ölçüler tek render'da nihai değere oturur; rAF içinde // sf'e bağlı ölçüler tek render'da nihai değere oturur; rAF içinde
// (asenkron) çağrıldığı için effect'te senkron setState sayılmaz. // (asenkron) çağrıldığı için effect'te senkron setState sayılmaz.
setVb(hedef); setVb(hedef);
// Mount'taki tam görünüm → tam görünüm çağrısında açılmasın
setUzaklasiyor(hedef === TAM_GORUNUM && kaynak.w !== hedef.w);
} }
const t = Math.min(1, (now - t0) / SURE); const t = Math.min(1, (now - t0) / SURE);
const k = ease(t); const k = ease(t);
@@ -190,6 +201,7 @@ export function TercihHaritasi({
`${sonraki.x} ${sonraki.y} ${sonraki.w} ${sonraki.h}`, `${sonraki.x} ${sonraki.y} ${sonraki.w} ${sonraki.h}`,
); );
if (t < 1) animRef.current = requestAnimationFrame(adim); if (t < 1) animRef.current = requestAnimationFrame(adim);
else setUzaklasiyor(false);
}; };
animRef.current = requestAnimationFrame(adim); animRef.current = requestAnimationFrame(adim);
}, []); }, []);
@@ -239,6 +251,7 @@ export function TercihHaritasi({
// Ekran boyutu sabit kalsın diye SVG içi ölçüler zoom oranıyla çarpılır // Ekran boyutu sabit kalsın diye SVG içi ölçüler zoom oranıyla çarpılır
const sf = vb.w / HARITA_GENISLIK; const sf = vb.w / HARITA_GENISLIK;
const zoomda = Boolean(seciliIl); const zoomda = Boolean(seciliIl);
const kenarErit = zoomda || uzaklasiyor;
const seciliPinler = useMemo( const seciliPinler = useMemo(
() => (seciliIl ? (ilPinleri.get(seciliIl) ?? []) : []), () => (seciliIl ? (ilPinleri.get(seciliIl) ?? []) : []),
[ilPinleri, seciliIl], [ilPinleri, seciliIl],
@@ -297,6 +310,28 @@ export function TercihHaritasi({
return ( return (
<div> <div>
<div className="relative"> <div className="relative">
{/* Harita arka plandır, kesilmez (DESIGN.md §4.17): il'e yakınlaşınca
viewBox komşu illeri dikdörtgenle kırpıyor ve sert kenarlı bir
kanvas görünüyordu (Bilal, 11 Eki 2026: "canvas çok keskin").
Şehir/üniversite sayfalarındaki piksel haritayla aynı iki katlı
mask: dış kat üst-alt, iç kat sol-sağ kenarı eritir; hiçbir
kenarda yarım kesilmiş il kalmaz. Yalnız yakınlaşmışken (kenarErit);
tam görünümde mask yok, ülke uçları solmaz. "Türkiye" düğmesi
mask'ın dışında, erimez. */}
<div
className={
kenarErit
? "[mask-image:linear-gradient(to_bottom,transparent_0%,rgba(0,0,0,0.35)_7%,black_16%,black_84%,rgba(0,0,0,0.35)_93%,transparent_100%)]"
: undefined
}
>
<div
className={
kenarErit
? "[mask-image:linear-gradient(to_right,transparent_0%,rgba(0,0,0,0.35)_5%,black_12%,black_88%,rgba(0,0,0,0.35)_95%,transparent_100%)]"
: undefined
}
>
<svg <svg
ref={svgMount} ref={svgMount}
className="h-auto w-full" className="h-auto w-full"
@@ -485,6 +520,8 @@ export function TercihHaritasi({
}) })
: null} : null}
</svg> </svg>
</div>
</div>
{zoomda ? ( {zoomda ? (
<Button <Button

View File

@@ -1,24 +1,33 @@
"use client"; "use client";
// Veri akışı (DESIGN.md §4.28): listenin neyle kurulduğunu tek bakışta // Veri akışı (DESIGN.md §4.28): listenin neyle kurulduğunu tek bakışta
// gösterir. Ortada KolayTercih işareti, çevresinde altı girdi; her girdiden // gösterir. Üstte öğrencinin üç seçimi (turuncu), altta YÖK Atlas'ın üç
// merkeze düz bir çizgi iner, üzerinde piksel kare akar. Soldaki üçü // verisi (mavi), ortada danışman: "DanışmanlaKonuş" çekirdeği. Her kutudan
// öğrencinin seçimi (turuncu), sağdaki üçü YÖK Atlas verisi (mavi) — ayrımı // çekirdeğe kıvrımlı bir yol iner, üzerinde kayıt akar. Ayrımı renk değil,
// renk değil, etiket ve yan taşır; renk yalnız eşlik eder (§2.2). // etiket ve sıra taşır; renk yalnız eşlik eder (§2.2).
// //
// Kartsız ve zeminsizdir (Bilal, 7 Eki 2026): sayfa zemininde açıkta durur, // Kartsız ve zeminsizdir (Bilal, 7 Eki 2026): sayfa zemininde açıkta durur,
// eskiden altında duran sayılar ve dürüstlük notu geniş ekranda iki yanına // sayılar ve dürüstlük notu geniş ekranda iki yanına dizilir (app/page.tsx).
// dizilir (yerleşim app/page.tsx'te).
// //
// Ortadaki işaret danışmanın kendisidir (Bilal, 11 Eki 2026): bir süre // Yerleşim ve kaydın yaşam döngüsü Arc UI "Data flow" gibidir (Bilal, 11 Eki
// DanışmanlaKonuş başlığındaki gibi nabız atar (danışman düşünüyor), sonra // 2026: "ortadaki kısmı dikdörtgen formuna sokup DanışmanlaKonuş yazsak,
// üç kare girdilerden çıkan şeye dönüşür, durur ve logoya toplanır — sırayla // kutular attığım örnekteki gibi"; uiarc.dev/components/data-flow — Pro
// liste, trend çubukları, sohbet balonları (`AnimasyonluMark` `bicimler`, // bileşen, kaynağı alınmadı: önizlemedeki davranış DOM kaydıyla ölçülüp bizim
// §4.34). Eski "rota" (kareler birbirinin yerine süzülür) kaldırıldı. // dille yeniden yazıldı, §11.3). Kutu = ikon üstte, etiket altta, tek yüzey.
// Kayıt kutunun portundan küçük ve soluk çıkar, arkasında iki noktalık bir iz taşır;
// yol boyunca içi boş noktadır (ham girdi). Çekirdeğe yaklaşınca hafifçe
// şişer, dolar ve piksel kareye döner (işlenmiş), kenarda sönerek içeri
// alınır; o an çekirdeğin çerçevesi kaydın renginde yanıp genişleyerek söner.
// Her 1 sn'de bir kayıt kalkar; altı kayıt 6 sn'lik döngüde bir üstten bir
// alttan yola çıkar.
// //
// Esin: React Bits "Center Flow" (merkezden çevreye akan çizgiler). Kod bize // Çekirdek danışman panelinin başlığıdır (`DanismanBaslik`, sohbet-client.tsx
// ait: parlama/bulanıklık yok (§2.2), akış yönü içeri doğrudur ve yalnız // ile aynı desen): turuncu işaret nabız atar, yanında "DanışmanlaKonuş".
// ekrandayken oynar. // İşaret aralıklarla girdilerden çıkana dönüşür (liste → trend → sohbet,
// `AnimasyonluMark` `bicimler`, §4.34).
//
// Geometri ve zamanlama globals.css'teki `kt-akis-*` kurallarındadır; buradan
// yalnız kaydın iki ucu (--x0/--x1/--y0/--y1), rengi ve gecikmesi gider.
import { useEffect, useRef, useState, type CSSProperties } from "react"; import { useEffect, useRef, useState, type CSSProperties } from "react";
import { Compass, MapPin, PieChart, TrendingUp, Trophy, Users } from "lucide-react"; import { Compass, MapPin, PieChart, TrendingUp, Trophy, Users } from "lucide-react";
@@ -30,41 +39,77 @@ import { cn } from "@/lib/utils";
// başa döner (animasyonlu-mark.tsx). // başa döner (animasyonlu-mark.tsx).
const CIKTILAR: readonly Bicim[] = ["deste", "trend", "sohbet"]; const CIKTILAR: readonly Bicim[] = ["deste", "trend", "sohbet"];
// Sağ yarı (−60°, 0°, 60°) veri; sol yarı (120°, 180°, 240°) öğrencinin seçimi. // Üst sıra öğrencinin seçimi, alt sıra veri; `sutun` soldan sağa 0–2.
const DUGUMLER = [ const DUGUMLER = [
{ etiket: "Taban sıralamaları", ikon: TrendingUp, aci: -60, kaynak: "veri" }, { etiket: "Senin sıralaman", ikon: Trophy, kaynak: "sen", sutun: 0 },
{ etiket: "Kontenjanlar", ikon: Users, aci: 0, kaynak: "veri" }, { etiket: "İlgi alanların", ikon: Compass, kaynak: "sen", sutun: 1 },
{ etiket: "Doluluk oranları", ikon: PieChart, aci: 60, kaynak: "veri" }, { etiket: "Şehir tercihin", ikon: MapPin, kaynak: "sen", sutun: 2 },
{ etiket: "Şehir tercihin", ikon: MapPin, aci: 120, kaynak: "sen" }, { etiket: "Taban sıralamaları", ikon: TrendingUp, kaynak: "veri", sutun: 0 },
{ etiket: "Senin sıralaman", ikon: Trophy, aci: 180, kaynak: "sen" }, { etiket: "Kontenjanlar", ikon: Users, kaynak: "veri", sutun: 1 },
{ etiket: "İlgi alanların", ikon: Compass, aci: 240, kaynak: "sen" }, { etiket: "Doluluk oranları", ikon: PieChart, kaynak: "veri", sutun: 2 },
] as const; ] as const;
/** viewBox 100×100: düğümlerin merkeze uzaklığı. Çizgi merkezden merkeze // Kalkış sırası (DUGUMLER indeksleri): bir üstten bir alttan, komşu kutular
* çekilir ve karoların altında kalır; akan kare ise karoların dışında // art arda kalkmaz; sahne bir yana yığılmaz.
* kalan aralıkta yürür (iki uçtaki boşluk). */ const KALKIS_SIRASI: readonly number[] = [0, 5, 1, 3, 2, 4];
const YARICAP = 36; /** İz noktalarının kayda göre gecikmesi (sn): uzaktaki önce çizilir, küçük ve soluk kalır. */
const DIS_BOSLUK = 9.5; const IZ_GECIKMELERI = [0.14, 0.07] as const;
const IC_BOSLUK = 15.5; /** İki kalkış arası (sn). Döngü 6 kayıt × 1 sn = 6 sn (globals.css). */
/** Akan karenin kenarı (hero noktasının viewBox karşılığı). */ const KALKIS_ARALIGI = 1;
const KARE = 2.4;
// Üç ondalığa yuvarlanır: sunucu (Node) ile tarayıcının sin/cos'u son // Sahne ölçüleri. Yatay eksen kabın eninin yüzdesi (SVG viewBox 0–100, CSS'te
// basamakta ayrışıyor, yuvarlanmazsa hidrasyon uyuşmazlığı çıkıyor. // cqw), dikey eksen rem'in 16'da biri (viewBox 0–400 = 25rem). globals.css'teki
const yuvarla = (v: number) => Math.round(v * 1000) / 1000; // `.kt-akis-sahne` ile aynı sayılar; biri değişirse öbürü de.
const nokta = (aci: number, r: number) => { const SUTUN_X = [16.667, 50, 83.333] as const;
const rad = (aci * Math.PI) / 180; /** Yolların çekirdek kenarına vardığı noktalar: ortadan 4'er birim açılır. */
const VARIS_X = [46, 50, 54] as const;
const KUTU_BOY = 80;
const CEKIRDEK_UST = 168;
const CEKIRDEK_ALT = 232;
const SAHNE_BOY = 400;
type Dugum = (typeof DUGUMLER)[number];
/** Yolun iki ucu: kutunun çekirdeğe bakan kenarının ortası → çekirdek kenarı. */
const uclar = (d: Dugum) => {
const ustte = d.kaynak === "sen";
return { return {
x: yuvarla(50 + Math.cos(rad) * r), x0: SUTUN_X[d.sutun],
y: yuvarla(50 + Math.sin(rad) * r), x1: VARIS_X[d.sutun],
y0: ustte ? KUTU_BOY : SAHNE_BOY - KUTU_BOY,
y1: ustte ? CEKIRDEK_UST : CEKIRDEK_ALT,
}; };
}; };
/** Arc'ın yolu: iki ucu dikey çıkan S kıvrımı (kontrol noktaları orta yükseklikte). */
const yol = (d: Dugum) => {
const { x0, x1, y0, y1 } = uclar(d);
const ym = (y0 + y1) / 2;
return `M${x0} ${y0} C${x0} ${ym} ${x1} ${ym} ${x1} ${y1}`;
};
/** Kaydın rengi: öğrencinin seçimi turuncu, YÖK Atlas verisi mavi (§2.2). */
const renk = (kaynak: Dugum["kaynak"]) =>
kaynak === "sen" ? "var(--color-orange-500)" : "var(--primary)";
/** Renk ve döngüdeki kalkış anı; kayda, kutu vurgusuna ve çekirdek nabzına gider. */
const kayitStili = (i: number) =>
({
"--renk": renk(DUGUMLER[i].kaynak),
"--g": `${KALKIS_SIRASI.indexOf(i) * KALKIS_ARALIGI}s`,
}) as CSSProperties;
/** Kaydın iki ucu CSS'e: x cqw, y rem. */
const ucStili = (d: Dugum) => {
const { x0, x1, y0, y1 } = uclar(d);
return {
"--x0": `${x0}cqw`,
"--x1": `${x1}cqw`,
"--y0": `${y0 / 16}rem`,
"--y1": `${y1 / 16}rem`,
} as CSSProperties;
};
export function VeriAkisi({ className }: { className?: string }) { export function VeriAkisi({ className }: { className?: string }) {
const kapRef = useRef<HTMLDivElement>(null); const kapRef = useRef<HTMLDivElement>(null);
const [oynuyor, setOynuyor] = useState(false); const [oynuyor, setOynuyor] = useState(false);
// Ekran dışındayken akış da işaret de durur (kareler CSS'te `data-oynuyor` // Ekran dışındayken akış da işaret de durur (kayıtlar CSS'te `data-oynuyor`
// ile, işaret `oynat` ile). // ile, işaret `oynat` ile).
useEffect(() => { useEffect(() => {
const kap = kapRef.current; const kap = kapRef.current;
@@ -82,94 +127,112 @@ export function VeriAkisi({ className }: { className?: string }) {
ref={kapRef} ref={kapRef}
data-oynuyor={oynuyor ? "" : undefined} data-oynuyor={oynuyor ? "" : undefined}
role="img" role="img"
aria-label="Listen altı şeyden kurulur: senin sıralaman, ilgi alanların ve şehir tercihin; YÖK Atlas'ın taban sıralamaları, kontenjanları ve doluluk oranları." aria-label="Listen altı şeyden kurulur ve danışmanda birleşir: senin sıralaman, ilgi alanların ve şehir tercihin; YÖK Atlas'ın taban sıralamaları, kontenjanları ve doluluk oranları."
className="group/akis relative aspect-square w-full" className="kt-akis-sahne group/akis @container relative w-full"
> >
<svg viewBox="0 0 100 100" className="absolute inset-0 size-full" aria-hidden> {/* Yollar: x kabın yüzdesi, y rem; çizgi kalınlığı ölçeklenmez */}
{DUGUMLER.map((d, i) => { <svg
const dugum = nokta(d.aci, YARICAP); viewBox={`0 0 100 ${SAHNE_BOY}`}
const dis = nokta(d.aci, YARICAP - DIS_BOSLUK); preserveAspectRatio="none"
const ic = nokta(d.aci, IC_BOSLUK); className="absolute inset-0 size-full"
return ( aria-hidden
<g key={d.etiket}> >
{/* Düz çizgi: karodan merkeze; uçları karoların altında */} {DUGUMLER.map((d) => (
<line <path
x1={dugum.x} key={d.etiket}
y1={dugum.y} d={yol(d)}
x2={50} fill="none"
y2={50}
stroke="var(--color-slate-300)" stroke="var(--color-slate-300)"
strokeWidth={1.5} strokeWidth={1.5}
vectorEffect="non-scaling-stroke" vectorEffect="non-scaling-stroke"
/> />
<rect ))}
x={-KARE / 2}
y={-KARE / 2}
width={KARE}
height={KARE}
rx={0.6}
className={cn(
"kt-akis-kare",
d.kaynak === "sen" ? "fill-orange-500" : "fill-primary",
)}
style={
{
"--x1": `${dis.x}px`,
"--y1": `${dis.y}px`,
"--x2": `${ic.x}px`,
"--y2": `${ic.y}px`,
// Sırayla değil, dağınık kalkış: 0 · 1,1 · 0,5 · 1,6 · 0,3 · 1,3 sn
animationDelay: `${[0, 1.1, 0.5, 1.6, 0.3, 1.3][i]}s`,
} as CSSProperties
}
/>
</g>
);
})}
</svg> </svg>
{DUGUMLER.map((d) => { {DUGUMLER.map((d, i) => (
const p = nokta(d.aci, YARICAP); // Kutu (Arc "chip"): ikon üstte, etiket altta, tek beyaz yüzey
const ustte = d.aci < 0 || d.aci > 180;
return (
<div <div
key={d.etiket} key={d.etiket}
className="absolute flex -translate-x-1/2 -translate-y-1/2 flex-col items-center" className={cn(
style={{ left: `${p.x}%`, top: `${p.y}%` }} "absolute flex h-20 w-[30%] -translate-x-1/2 flex-col items-center justify-center gap-1.5 rounded-2xl border border-slate-200 bg-card px-1.5",
d.kaynak === "sen" ? "top-0" : "bottom-0",
)}
style={{ left: `${SUTUN_X[d.sutun]}%` }}
> >
{/* Beyaz, dolu karo (çizgi altından geçer); yanı ikonun rengi söyler */} {/* Kalkış vurgusu: kayıt yola çıkarken çerçeve onun renginde kısa bir an yanar */}
<span className="flex size-14 items-center justify-center rounded-2xl border border-slate-200 bg-card"> <span
className="kt-akis-vurgu pointer-events-none absolute -inset-px rounded-2xl border"
style={kayitStili(i)}
/>
<d.ikon <d.ikon
className={cn("size-6", d.kaynak === "sen" ? "text-orange-500" : "text-primary")} className={cn(
"size-5 shrink-0",
d.kaynak === "sen" ? "text-orange-500" : "text-primary",
)}
aria-hidden aria-hidden
/> />
</span> <span className="text-center text-xs font-semibold leading-tight text-balance text-slate-700">
{/* Üstteki iki düğümde etiket karonun üstünde: altında çizgi merkeze iniyor */}
<span
className={cn(
"absolute w-20 text-center text-xs font-semibold leading-tight text-slate-700",
ustte ? "bottom-full mb-1.5" : "top-full mt-1.5",
)}
>
{d.etiket} {d.etiket}
</span> </span>
</div> </div>
); ))}
})}
<div className="absolute top-1/2 left-1/2 flex size-24 -translate-x-1/2 -translate-y-1/2 items-center justify-center rounded-3xl border border-slate-200 bg-card"> {/* Çekirdek: danışman panelinin başlığı (DanismanBaslik deseni) */}
{/* Danışmanın nabzı, aralıklarla çıktıya dönüşür (liste → trend → <div className="absolute top-[10.5rem] left-1/2 flex h-16 -translate-x-1/2 items-center gap-1 rounded-2xl border border-slate-200 bg-card pr-5 pl-4">
sohbet) ve logoya toplanır; yalnız ekrandayken oynar. */} {/* Varış nabzı: kayıt içeri alınınca çerçeve onun renginde yanar,
genişleyerek söner. Her kaydın kendi nabzı, aynı gecikmeyle. */}
{DUGUMLER.map((d, i) => (
<span
key={d.etiket}
className="kt-akis-nabiz pointer-events-none absolute -inset-px rounded-2xl border"
style={kayitStili(i)}
/>
))}
<AnimasyonluMark <AnimasyonluMark
oynat={oynuyor} oynat={oynuyor}
varyant="nabiz" varyant="nabiz"
renk="turuncu"
bicimler={CIKTILAR} bicimler={CIKTILAR}
className="size-14" className="size-7"
/> />
<span className="font-bricolage text-xl tracking-tighter whitespace-nowrap text-foreground">
<span className="font-light">Danışmanla</span>
<span className="font-semibold">Konuş</span>
</span>
</div> </div>
{/* Kayıtlar kutuların üstünde: porttan doğar, çekirdek kenarında söner.
Yatay ve dikey kayma ayrı öğelerde; ikisi birlikte yolun eğrisini çizer. */}
{DUGUMLER.map((d, i) => (
<span
key={d.etiket}
aria-hidden
className="kt-akis-kayit-kap"
style={{ ...kayitStili(i), ...ucStili(d) }}
>
<span className="kt-akis-port">
<span className="kt-akis-vurgu" />
</span>
{/* İz: kaydın ardından aynı eğriyi biraz gecikmeyle izleyen iki nokta */}
{IZ_GECIKMELERI.map((ek) => (
<span key={ek} className="kt-akis-x" style={{ "--ek": `${ek}s` } as CSSProperties}>
<span className="kt-akis-y">
<span className="kt-akis-iz" />
</span>
</span>
))}
<span className="kt-akis-x">
<span className="kt-akis-y">
<span className="kt-akis-kare">
<span className="kt-akis-dolgu" />
</span>
</span>
</span>
</span>
))}
</div> </div>
<figcaption className="mt-8 text-center text-sm leading-relaxed text-slate-500"> <figcaption className="mt-8 text-center text-sm leading-relaxed text-slate-500">
Solda senin seçimlerin, sağda YÖK Atlas verisi. Yapay zekâ yalnız bu Üstte senin seçimlerin, altta YÖK Atlas verisi. Yapay zekâ yalnız bu
ikisinin kesiştiği programlardan seçer. ikisinin kesiştiği programlardan seçer.
</figcaption> </figcaption>
</figure> </figure>

View File

@@ -68,6 +68,18 @@ export const KONTENJAN_TURU_ADI: Record<KontenjanTuruAnahtari, string> = {
kadin34: "34 yaş üstü kadın", kadin34: "34 yaş üstü kadın",
}; };
/**
* Kontenjan türü → kimin başvurabildiği (çekmecedeki satırın alt satırı).
* Tam şartlar ÖSYM kılavuzundadır; burası tek satırlık tarif.
*/
export const KONTENJAN_TURU_KIME: Record<KontenjanTuruAnahtari, string> = {
genel: "Sıralaması yeten her aday",
okulBirincisi: "Lisesini bu yıl birincilikle bitirenler",
sehitGaziYakini: "Şehit yakınları, gaziler ve gazi yakınları",
depremzede: "6 Şubat 2023 depremlerinden etkilenen adaylar",
kadin34: "34 yaşını doldurmuş kadın adaylar",
};
/** Bilinmeyen değerleri atar, tekrarları birleştirir; sıra sabit listeye göre. */ /** Bilinmeyen değerleri atar, tekrarları birleştirir; sıra sabit listeye göre. */
export function ozelDurumDogrula(girdi: unknown): OzelDurum[] { export function ozelDurumDogrula(girdi: unknown): OzelDurum[] {
if (!Array.isArray(girdi)) return []; if (!Array.isArray(girdi)) return [];