Compare commits
7 Commits
b11c2c9d45
...
eefac56659
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eefac56659 | ||
|
|
259534fa9c | ||
|
|
c27f24e6a6 | ||
|
|
be170fc28a | ||
|
|
d8a28b7c3a | ||
|
|
f9b6f0c235 | ||
|
|
4b28b353ee |
@@ -17,35 +17,31 @@ Her URL denetimi isteğinden sonra o günün satırı güncellenir. **13'e varı
|
|||||||
| Gün | Kullanılan / kota | Not |
|
| Gün | Kullanılan / kota | Not |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 22 Eyl 2026 | **6 / 13** | 4 başarılı bildirim + 2 boşa giden istek (arama kutusu dar pencerede kapandı — aşağıdaki nota bak) |
|
| 22 Eyl 2026 | **6 / 13** | 4 başarılı bildirim + 2 boşa giden istek (arama kutusu dar pencerede kapandı — aşağıdaki nota bak) |
|
||||||
| *(sonraki bildirim günü)* | — / 13 | Deploy sonrası doldurulur |
|
| 24 Eyl 2026 | **12 / 12** | 11 başarılı bildirim + 1 boşa giden istek: 12. istekte (`yerlesemedim-ne-yapmaliyim`, yeniden tarama) Search Console **"Kota Aşıldı"** dedi. Yani bu mülkte gerçek günlük sınır **11 başarılı istek**; 12.'yi deneme, o gün dur |
|
||||||
|
| *(sonraki bildirim günü)* | — / 11 | Önce aşağıdaki bekleyenler |
|
||||||
|
|
||||||
## Bekleyenler — 22 Eylül gece vardiyasının yazıları
|
## Bekleyenler — 24 Eylül'de güncellenen yazılar (yeni adres değil, yeniden tarama)
|
||||||
|
|
||||||
Hepsi `yazıldı`. Bilal commit'leyip deploy ettikten sonra canlıda 200 **ve sitemap'te var** doğrulanır, sonra tek tek bildirilir. Kota günde ~10-13 olduğu için 11 adres tek güne sığmaz — **sıra sütunundaki öncelikle** ilk 11'e kadar gidilir, kalan ertesi güne.
|
24 Eyl 2026'da beş rehber yazısı düzeltildi ve canlıya çıktı (commit `b11c2c9`); hepsi zaten dizinde ("URL Google'da mevcut"). Yeniden tarama isteği aynı kotadan düşer. 24 Eyl'de kota 11 yeni adresle dolduğu için bunlar **25 Eyl 2026 ve sonrasına** kaldı. Sıra, değişikliğin büyüklüğüne ve mevsime göre:
|
||||||
|
|
||||||
| Sıra | Adres | Durum |
|
| Sıra | Adres | Ne değişti | Durum |
|
||||||
|---|---|---|
|
|---|---|---|---|
|
||||||
| 1 | https://kolaytercih.com/rehber/taban-siralamalari-alti-yilda-nasil-degisti | yazıldı |
|
| 1 | https://kolaytercih.com/rehber/yerlesemedim-ne-yapmaliyim | "ekte tabanlar gevşer" hatası düzeltildi, OBP uyarısı, kaynaklar; ek yerleştirme sonucu bu hafta | yayında · sitemap ✓ · 24 Eyl'de istek kotaya takıldı |
|
||||||
| 2 | https://kolaytercih.com/rehber/kac-net-ile-hangi-bolum | yazıldı |
|
| 2 | https://kolaytercih.com/rehber/obp-nedir-nasil-hesaplanir | Meslek lisesi ek puanı bölümü (yeni arama kümesi) | yayında · sitemap ✓ |
|
||||||
| 3 | https://kolaytercih.com/rehber/siralama-bandlari-hangi-kapilar-acilir | yazıldı |
|
| 3 | https://kolaytercih.com/rehber/yatay-gecis-cap-yandal | ÇAP/yandal eşikleri, Ek Madde-1 kontenjanı, "baraj 90", kaynaklar, şema renkleri | yayında · sitemap ✓ |
|
||||||
| 4 | https://kolaytercih.com/rehber/bilgisayar-mi-yazilim-muhendisligi-mi | yazıldı |
|
| 4 | https://kolaytercih.com/rehber/kyk-burs-mu-kredi-mi | Yıl kaybı maddesi yumuşatıldı, kısmi zamanlı çalışma, TEV | yayında · sitemap ✓ |
|
||||||
| 5 | https://kolaytercih.com/rehber/yeni-acilan-bolumler-nasil-degerlendirilir | yazıldı |
|
| 5 | https://kolaytercih.com/rehber/ozel-kosullu-programlar | "sınavsız" ifadesi düzeltildi, kaynaklar | yayında · sitemap ✓ |
|
||||||
| 6 | https://kolaytercih.com/rehber/kktc-universiteleri-okunur-mu | yazıldı |
|
| 6 | https://kolaytercih.com/rehber/veliler-icin-tercih-rehberi | 22 Eyl: kapanış veli diline çevrildi (önceki kuyruktan) | yayında · sitemap ✓ |
|
||||||
| 7 | https://kolaytercih.com/rehber/ek-madde-1-puanim-yetiyor-mu | yazıldı |
|
|
||||||
| 8 | https://kolaytercih.com/rehber/universitede-ilk-hafta-ders-kaydi-muafiyet-kayit-dondurma | yazıldı |
|
|
||||||
| 9 | https://kolaytercih.com/rehber/yks-2027-takvimi | yazıldı |
|
|
||||||
| 10 | https://kolaytercih.com/rehber/yapay-zekaya-tercih-sordum-guvenilir-mi | yazıldı |
|
|
||||||
| 11 | https://kolaytercih.com/rehber/bolumumu-sevmedim-hangi-kapilar-var | yazıldı |
|
|
||||||
|
|
||||||
**Güncellenen yazı (yeni adres değil, bildirim önceliği düşük):** `https://kolaytercih.com/rehber/veliler-icin-tercih-rehberi` — kapanışı veli diline çevrildi; adres zaten dizinde, yeniden denetim **ancak kota artarsa** yapılır, yeni adreslerin önüne geçmez.
|
Ön kontrol komutu (her satır `200 1` olmalı):
|
||||||
|
|
||||||
Deploy sonrası tek komutla kontrol (HTTP kodu **ve** sitemap sayımı birlikte):
|
|
||||||
|
|
||||||
```
|
```
|
||||||
for s in taban-siralamalari-alti-yilda-nasil-degisti kac-net-ile-hangi-bolum siralama-bandlari-hangi-kapilar-acilir bilgisayar-mi-yazilim-muhendisligi-mi yeni-acilan-bolumler-nasil-degerlendirilir kktc-universiteleri-okunur-mu ek-madde-1-puanim-yetiyor-mu universitede-ilk-hafta-ders-kaydi-muafiyet-kayit-dondurma yks-2027-takvimi yapay-zekaya-tercih-sordum-guvenilir-mi bolumumu-sevmedim-hangi-kapilar-var; do printf "%s -> " "$s"; printf "%s " "$(curl -s -o /dev/null -w "%{http_code}" "https://kolaytercih.com/rehber/$s")"; curl -s https://kolaytercih.com/sitemap.xml | grep -c "/rehber/$s<"; done
|
for s in yerlesemedim-ne-yapmaliyim obp-nedir-nasil-hesaplanir yatay-gecis-cap-yandal kyk-burs-mu-kredi-mi ozel-kosullu-programlar veliler-icin-tercih-rehberi; do printf "%s -> " "$s"; printf "%s " "$(curl -s -o /dev/null -w "%{http_code}" "https://kolaytercih.com/rehber/$s")"; curl -s https://kolaytercih.com/sitemap.xml | grep -c "/rehber/$s<"; done
|
||||||
```
|
```
|
||||||
|
|
||||||
Her satır `<slug> -> 200 1` olmalı. `200 0` çıkan adres **bildirilmez**: önce sitemap yeniden gönderilir, birkaç dakika sonra sayım tekrarlanır.
|
**Bing Webmaster Tools:** 24 Eyl 2026'da `https://kolaytercih.com/sitemap.xml` yeniden gönderildi ("successfully submitted for processing"; önceki gönderim 20 Eyl, durum Success, 897 URL). Sayfa: `bing.com/webmasters/sitemaps?siteUrl=https://kolaytercih.com/` → "Submit sitemap".
|
||||||
|
|
||||||
|
**Yandex Webmaster:** sitemap 22 Eyl 2026'dan beri işleme kuyruğunda ("1–2 hafta sürebilir"); 24 Eyl'de yeniden ekleme denendi, Yandex aynı adresi ikinci kez kuyruğa almıyor, tek kayıt duruyor. Sayfa: `webmaster.yandex.com/site/https:kolaytercih.com:443/indexing/sitemap/`. Ayrı not: Yandex'in "Tools → Reindex pages" aracı günde 20 adres alıyor; istenirse yeni yazılar oradan da bildirilebilir (Bilal kararı).
|
||||||
|
|
||||||
## Bildirilenler
|
## Bildirilenler
|
||||||
|
|
||||||
@@ -55,7 +51,18 @@ Her satır `<slug> -> 200 1` olmalı. `200 0` çıkan adres **bildirilmez**: ön
|
|||||||
| https://kolaytercih.com/rehber/bos-kontenjanlar-ne-anlatiyor | 22 Eyl 2026 | "Keşfedildi - şu anda dizine eklenmiş değil" → dizine eklenmesi istendi |
|
| https://kolaytercih.com/rehber/bos-kontenjanlar-ne-anlatiyor | 22 Eyl 2026 | "Keşfedildi - şu anda dizine eklenmiş değil" → dizine eklenmesi istendi |
|
||||||
| https://kolaytercih.com/rehber/en-cok-ogrenci-alan-devlet-universiteleri | 22 Eyl 2026 | "URL Google'da yok" → dizine eklenmesi istendi |
|
| https://kolaytercih.com/rehber/en-cok-ogrenci-alan-devlet-universiteleri | 22 Eyl 2026 | "URL Google'da yok" → dizine eklenmesi istendi |
|
||||||
| https://kolaytercih.com/rehber/en-secici-devlet-universiteleri | 22 Eyl 2026 | "Keşfedildi - şu anda dizine eklenmiş değil" → dizine eklenmesi istendi |
|
| https://kolaytercih.com/rehber/en-secici-devlet-universiteleri | 22 Eyl 2026 | "Keşfedildi - şu anda dizine eklenmiş değil" → dizine eklenmesi istendi |
|
||||||
|
| https://kolaytercih.com/rehber/taban-siralamalari-alti-yilda-nasil-degisti | 24 Eyl 2026 | "Keşfedildi - şu anda dizine eklenmiş değil", sitemap görünüyor → dizine eklenmesi istendi |
|
||||||
|
| https://kolaytercih.com/rehber/kac-net-ile-hangi-bolum | 24 Eyl 2026 | "Keşfedildi", sitemap görünüyor → istendi |
|
||||||
|
| https://kolaytercih.com/rehber/siralama-bandlari-hangi-kapilar-acilir | 24 Eyl 2026 | "Keşfedildi", sitemap görünüyor → istendi |
|
||||||
|
| https://kolaytercih.com/rehber/bilgisayar-mi-yazilim-muhendisligi-mi | 24 Eyl 2026 | "URL Google tarafından bilinmiyor", **sitemap algılanmadı** (bizim sayım 1; Google okuma gecikmesi, tekrarlanmaz) → istendi |
|
||||||
|
| https://kolaytercih.com/rehber/yeni-acilan-bolumler-nasil-degerlendirilir | 24 Eyl 2026 | "Keşfedildi", sitemap görünüyor → istendi |
|
||||||
|
| https://kolaytercih.com/rehber/kktc-universiteleri-okunur-mu | 24 Eyl 2026 | "Keşfedildi", sitemap görünüyor → istendi |
|
||||||
|
| https://kolaytercih.com/rehber/ek-madde-1-puanim-yetiyor-mu | 24 Eyl 2026 | "Bilinmiyor", sitemap algılanmadı (sayım 1) → istendi |
|
||||||
|
| https://kolaytercih.com/rehber/universitede-ilk-hafta-ders-kaydi-muafiyet-kayit-dondurma | 24 Eyl 2026 | "Keşfedildi", sitemap görünüyor → istendi |
|
||||||
|
| https://kolaytercih.com/rehber/yks-2027-takvimi | 24 Eyl 2026 | "Bilinmiyor", sitemap algılanmadı (sayım 1) → istendi |
|
||||||
|
| https://kolaytercih.com/rehber/yapay-zekaya-tercih-sordum-guvenilir-mi | 24 Eyl 2026 | "Bilinmiyor", sitemap algılanmadı (sayım 1) → istendi |
|
||||||
|
| https://kolaytercih.com/rehber/bolumumu-sevmedim-hangi-kapilar-var | 24 Eyl 2026 | "Keşfedildi", sitemap görünüyor → istendi |
|
||||||
|
|
||||||
**Site haritası:** `https://kolaytercih.com/sitemap.xml` 22 Eyl 2026'da yeniden gönderildi ("Site haritası başarıyla gönderildi"). Önceki gönderim 21 Eyl, durum "Başarılı", 897 sayfa keşfedilmiş.
|
**Site haritası:** `https://kolaytercih.com/sitemap.xml` 24 Eyl 2026'da yeniden gönderildi ("Site haritası başarıyla gönderildi"; önceki satır 22 Eyl, "Başarılı", 904 sayfa keşfedilmiş). Bir önceki gönderim 22 Eyl 2026'da yapılmıştı ("Site haritası başarıyla gönderildi"). Önceki gönderim 21 Eyl, durum "Başarılı", 897 sayfa keşfedilmiş.
|
||||||
|
|
||||||
**Kotadan kaybedilen 2 istek (22 Eyl — yukarıdaki kota tablosunda 6/13'ün 2'si; notu duruyor ki tekrarlanmasın):** Search Console'un arama kutusu dar pencerede kapandığı için (a) `kyk-burs-mu-kredi-mi` için ikinci kez istek gönderildi, (b) iki adresin birbirine yapışmasıyla oluşan bozuk bir adres denetlendi. Doğru yöntem: pencereyi genişlet → büyüteç düğmesine bas → kutunun açıldığını **gör** → ancak o zaman yaz. Kutuya `form_input` ile değer basma, açılmamış kutuya yazma.
|
**Kotadan kaybedilen 2 istek (22 Eyl — yukarıdaki kota tablosunda 6/13'ün 2'si; notu duruyor ki tekrarlanmasın):** Search Console'un arama kutusu dar pencerede kapandığı için (a) `kyk-burs-mu-kredi-mi` için ikinci kez istek gönderildi, (b) iki adresin birbirine yapışmasıyla oluşan bozuk bir adres denetlendi. Doğru yöntem: pencereyi genişlet → büyüteç düğmesine bas → kutunun açıldığını **gör** → ancak o zaman yaz. Kutuya `form_input` ile değer basma, açılmamış kutuya yazma.
|
||||||
|
|||||||
@@ -352,7 +352,13 @@ aşağı kaymış, kare boyları farklı — ne tepeler ne bitişler hizalı.**
|
|||||||
Başlığın altında kurumun kimliği; navbar haplarının ölçüsünde:
|
Başlığın altında kurumun kimliği; navbar haplarının ölçüsünde:
|
||||||
`inline-flex h-10 items-center gap-2 rounded-full border border-slate-200 bg-white px-4 text-sm font-semibold text-slate-800`
|
`inline-flex h-10 items-center gap-2 rounded-full border border-slate-200 bg-white px-4 text-sm font-semibold text-slate-800`
|
||||||
+ başta `size-4 text-primary` ikon (`Landmark` devlet, `Building2` vakıf, `MapPin` il). Tür
|
+ başta `size-4 text-primary` ikon (`Landmark` devlet, `Building2` vakıf, `MapPin` il). Tür
|
||||||
renkle değil ikonla ayrılır. Etkileşimli değildir; rozet (§4.2) değildir.
|
renkle değil ikonla ayrılır. Rozet (§4.2) değildir. Tür hapı etkileşimsizdir; **il hapı şehir
|
||||||
|
sayfasına link**tir (Bilal, 24 Eyl 2026: "şehirlerin geçtiği her yer tıklanabilir") — aynı
|
||||||
|
sınıf + `hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500 active:scale-[0.97]`.
|
||||||
|
Kural genel: bir şehir adı nerede geçerse geçsin (`/universiteler` listesi, bölüm/sonuç/listem/
|
||||||
|
tadımlık satırlarındaki tür-il-süre metası) `ilSayfaHref(il)` ile `/universiteler/[il]`'e
|
||||||
|
gider; satır metasında `SATIR_IKINCIL_LINK`. İstisna: satırın tamamı zaten link olan yerler
|
||||||
|
(URAP sıralaması, arama sonucu) — iç içe `<a>` olmaz, orada düz metin kalır.
|
||||||
|
|
||||||
### 4.13 Piksel sıra şeridi — `PixelSira`, `src/components/pixel-decor.tsx`
|
### 4.13 Piksel sıra şeridi — `PixelSira`, `src/components/pixel-decor.tsx`
|
||||||
Bir sıranın (ör. 198 içinde 5.) yerini tek satır piksel şeridinde gösterir; sıranın düştüğü
|
Bir sıranın (ör. 198 içinde 5.) yerini tek satır piksel şeridinde gösterir; sıranın düştüğü
|
||||||
@@ -431,6 +437,40 @@ okunmuyor"). Kutu `rounded-2xl border-slate-200`; başlık `font-semibold text-s
|
|||||||
yazı, hover `slate-700`). Metin §10.4: ne oldu + ne yapılacak; kilometre taşı toast'ları
|
yazı, hover `slate-700`). Metin §10.4: ne oldu + ne yapılacak; kilometre taşı toast'ları
|
||||||
(`program-ekle-butonu.tsx`) tek cümle açıklama + tek aksiyon taşır.
|
(`program-ekle-butonu.tsx`) tek cümle açıklama + tek aksiyon taşır.
|
||||||
|
|
||||||
|
### 4.17 Şehir çipleri — `SehirCipleri`, `src/features/katalog/components/sehir-cipleri.tsx`
|
||||||
|
Şehre göre üniversite gezinmesi (`/universiteler` üstü ve `/universiteler/[il]` altı).
|
||||||
|
Sihirbazın il çipiyle aynı kalıp; etkileşimli çip olduğu için rozet (§4.2) değildir:
|
||||||
|
`inline-flex h-10 cursor-pointer items-center gap-1.5 rounded-full border px-3.5 text-sm font-semibold transition-[background-color,border-color,color,transform] duration-150 active:scale-[0.97]`
|
||||||
|
+ pasif `border-slate-200 bg-white text-slate-700 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500`,
|
||||||
|
bulunulan şehir `border-primary bg-primary text-white` + `aria-current="page"`. Ad yanında
|
||||||
|
üniversite sayısı `font-normal tabular-nums text-slate-400` (aktifte `text-white/80`).
|
||||||
|
- Çip satırında yalnız **2+ üniversiteli** şehirler (≈28) durur; 90'ı aşkın şehrin tamamı
|
||||||
|
`/universiteler#tum-sehirler` altındaki A–Z üç sütunlu metin listesindedir (§1.1 "sık
|
||||||
|
bakılan önde, ayrıntı bir dokunuş derinde"). Satırın sonunda "Tüm şehirler (N)" çipi.
|
||||||
|
- Şehir sayfası (`il-icerik.tsx`) katalog detay düzenindedir (§5.2): yol satırı, **başlık
|
||||||
|
yüzeyi** (`SehirYuzeyi`: `relative isolate overflow-hidden rounded-3xl border border-slate-200
|
||||||
|
bg-white p-6 sm:p-8`, `PikselZemin ton="acik"` seed 31, harita kabı `haric`), içinde h1
|
||||||
|
"İstanbul Üniversiteleri Taban Puanları 2026", kimlik hapları (`MapPin` il + üniversite
|
||||||
|
sayısı), özet paragraf; sağda (lg, 24rem sütun) ya da altta (telefon) **şehri temsil eden
|
||||||
|
piksel harita** `SehirPikselHaritasi`. Sonra kompakt `CtaSiraForm`, `PagePixelDivider`
|
||||||
|
(seed 71), devlet / vakıf / KKTC-yurt dışı öbekleri (`UniLinkleri`), "Diğer şehirler"
|
||||||
|
çipleri ve mavi CTA bandı. Şehir adı `trBaslikDuzeni` + tire sonrası baş harf
|
||||||
|
(Bakü-Azerbaycan). Bir üniversite programı olan her ilde sayılır (ODTÜ Ankara + KKTC).
|
||||||
|
- **Şehir piksel haritası** — `sehir-piksel-haritasi.tsx`: **hero'daki dot-matrix Türkiye'nin
|
||||||
|
kendisi** (Bilal, 24 Eyl 2026: ince taramalı ilk sürüm "oyunsu Apple/Duolingo hissine
|
||||||
|
uymuyor" diye geri alındı; seçenekler A/B/C arasından "A, pikseller C'deki gibi iri"
|
||||||
|
seçildi). Izgara 72 sütun × 31 satır (`scripts/il-izgara-uret.ts` →
|
||||||
|
`src/lib/il-izgara-verisi.ts`; her il en az 2×2 kare), kare hücrenin %74'ü, altı farklı
|
||||||
|
opaklık, kıyının altına seyrelen serpinti, `text-primary` zemin, imleç yaklaşınca turuncuya
|
||||||
|
ısınma (`usePixelHeat`). Bulunulan ilin kareleri `fill-orange-500`, hücrenin %88'i kadar
|
||||||
|
iri, 35 ms arayla belirir (`kt-piksel-belir`) ve grup hâlinde nabız atar (`animate-pulse`
|
||||||
|
3.6 s); hareketi azalt açıkken dolu ve durağan. KKTC/yurt dışı: ülke kontürü mavi doku,
|
||||||
|
şehir merkezinde 2×2 hücrelik tek iri turuncu kare; şehri bilinmeyen "KIBRIS" için ülkenin
|
||||||
|
tamamı turuncu. Markup hero'daki gibi deterministik string (`dangerouslySetInnerHTML`).
|
||||||
|
`role="img"` + "İstanbul Türkiye haritasında". Üniversite sayfasının konum kartı
|
||||||
|
(`universite-konum-haritasi.tsx`) aynı bileşeni kullanır. Veri taşıyan pikseldir (§1.2),
|
||||||
|
süs ayraç yasağına girmez.
|
||||||
|
|
||||||
## 5. Yerleşim
|
## 5. Yerleşim
|
||||||
|
|
||||||
### 5.1 Genel
|
### 5.1 Genel
|
||||||
@@ -700,17 +740,30 @@ karar.
|
|||||||
yan yana, buton sağda.
|
yan yana, buton sağda.
|
||||||
- Büyük (sayfa kapanışı) hâlinde aynı band ortalı ve iri: `px-5 py-8 text-center sm:p-12`,
|
- Büyük (sayfa kapanışı) hâlinde aynı band ortalı ve iri: `px-5 py-8 text-center sm:p-12`,
|
||||||
`-inset-y-10`, h2 `font-heading text-2xl font-bold sm:text-3xl`, paragraf `text-white/90`.
|
`-inset-y-10`, h2 `font-heading text-2xl font-bold sm:text-3xl`, paragraf `text-white/90`.
|
||||||
- Kullanıldığı yerler: `/sonuc`'taki iki sihirbaz paneli, katalog altındaki `CtaSiraForm`
|
- Kullanıldığı yerler: `/sonuc`'taki üç panel (sihirbaz, giriş bekleyen, "24 tercihlik listen
|
||||||
kart varyantı, "Kendi Listem" çekmecesinin altı (kompakt). Aynı sayfadaki iki band farklı
|
hazır"), `/meraklisina` kapanış kartı (`DemoKapanisCta`), katalog altındaki `CtaSiraForm`
|
||||||
`seed` alır.
|
kart varyantı, "Kendi Listem" çekmecesinin altı (kompakt), katalog üstü kompakt satırın
|
||||||
|
**kayıtlı sıralamalı** hâli ("Sıralaman … tabanlarının neresinde? · Sıralaman kayıtlı: …";
|
||||||
|
Bilal, 24 Eyl 2026: "bu kısma piksel dokusu ekle"). O satır CLS için tek `section` içinde
|
||||||
|
iki hâl taşıdığından `CtaBandi`'yi sarmaz; kompakt bandın sınıf setini birebir alır
|
||||||
|
(`relative isolate overflow-hidden rounded-2xl bg-primary px-5 py-4 text-left text-white
|
||||||
|
sm:py-5` + ilk çocuk `PikselZemin`), kenarlık saydam kalır ki iki hâlin kutusu aynı olsun.
|
||||||
|
Aynı sayfadaki iki band farklı `seed` alır.
|
||||||
|
|
||||||
**Piksel zemin yalnız CTA yüzeylerinde — `PikselZemin`, `src/components/piksel-zemin.tsx`**
|
**Piksel zemin yalnız CTA ve başlık yüzeylerinde — `PikselZemin`, `src/components/piksel-zemin.tsx`**
|
||||||
(Bilal, 24 Eyl 2026: önce "her CTA benzeri kısmın arkasında olmalı", sonra "/sonuc'taki kartın
|
(Bilal, 24 Eyl 2026: önce "her CTA benzeri kısmın arkasında olmalı", sonra "/sonuc'taki kartın
|
||||||
arkasına da piksel koymuşsun; pikseller CTA'larda olacak sadece"). CTA yüzeyi = renkli zeminli
|
arkasına da piksel koymuşsun; pikseller CTA'larda olacak sadece"). CTA yüzeyi = çağrı yüzeyi:
|
||||||
çağrı yüzeyi: mavi `CtaBandi`, koyu kapanış kartı `PikselCtaYuzeyi`, arama penceresindeki koyu
|
mavi `CtaBandi`, koyu kapanış kartı `PikselCtaYuzeyi`, arama penceresindeki koyu kart.
|
||||||
kart. Beyaz içerik kartlarına (tadımlık, `/listem` üretim/kilit kartları, paket kartı, kaydet
|
**Ton ikidir:** `ton="koyu"` (renkli zemin, beyaz kareler) ve `ton="acik"` (beyaz zemin,
|
||||||
bandı, katalog üstü kompakt satır, boş durumlar, sıralama-yok sayfası) **konmaz** — 24 Eyl'de
|
`text-slate-400` kareler; turuncu vurgu kareleri aynı). Bilal (24 Eyl 2026): "her kısımda mavi
|
||||||
denendi ve geri alındı. Kap `relative isolate overflow-hidden`; zemin ilk çocuk; kareler beyaz.
|
arka kullanmışsın, çeşitlendir; beyaz arka planda piksel de güzel görünüyor" — bu yüzden
|
||||||
|
`CtaBandi ton="beyaz"` (slate kenarlık, koyu yazı, paragraf `text-slate-600`) var ve aynı
|
||||||
|
sayfada iki bant üst üste gelmesin diye dönüşümlü kullanılır: `/universiteler` altı ve
|
||||||
|
`/meraklisina` kapanışı beyaz; şehir sayfası altı, `/sonuc` panelleri, çekmece altı mavi.
|
||||||
|
Şehir sayfasının başlık yüzeyi (§4.17) de açık tonlu piksel zemindir. Beyaz içerik kartlarına (tadımlık, `/listem` üretim/kilit kartları, paket kartı, kaydet
|
||||||
|
bandı, katalog üstü kompakt satırın **form** hâli, boş durumlar, sıralama-yok sayfası)
|
||||||
|
**konmaz** — 24 Eyl'de denendi ve geri alındı. Aynı kompakt satırın kayıtlı sıralamalı hâli
|
||||||
|
ise formsuz tek butonlu bir çağrı olduğundan mavi banttır ve dokuyu alır (yukarıda). Kap `relative isolate overflow-hidden`; zemin ilk çocuk; kareler beyaz.
|
||||||
Doku kabın gerçek ölçüsüne göre üretilir: yalnız kenar çerçevesinde (kısa kenarın %30'u), orta
|
Doku kabın gerçek ölçüsüne göre üretilir: yalnız kenar çerçevesinde (kısa kenarın %30'u), orta
|
||||||
tamamen boş; **küçük kare yok**, 2–4 hücrelik (17/27/37 px) kareler, iri olan seyrek ve daha
|
tamamen boş; **küçük kare yok**, 2–4 hücrelik (17/27/37 px) kareler, iri olan seyrek ve daha
|
||||||
silik (Bilal: "çok fazla piksel, küçük olanlar hiç olmasın").
|
silik (Bilal: "çok fazla piksel, küçük olanlar hiç olmasın").
|
||||||
|
|||||||
170
scripts/il-izgara-uret.ts
Normal file
170
scripts/il-izgara-uret.ts
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
// Şehir haritası (DESIGN.md §4.17) için il ve ülke kontürlerini hero'nun
|
||||||
|
// dot-matrix ızgarasına rasterize eder. Hero'daki TurkeyPixelMap 64 sütun;
|
||||||
|
// burada 72 sütun ki her il en az birkaç kare alsın (Yalova, Kilis gibi
|
||||||
|
// küçük iller için 2×2 blok garantisi vardır). Çıktı
|
||||||
|
// src/lib/il-izgara-verisi.ts dosyasına yazılır ve commit'lenir.
|
||||||
|
//
|
||||||
|
// Kaynak: turkey-map-react il path'leri (viewBox 1050×585, üstten 138 kırpık)
|
||||||
|
// ve harita-ulkeler-verisi.ts ülke kontürleri (1050×447). Hücre merkezi
|
||||||
|
// poligonun içindeyse hücre o ile aittir (ray casting; Bézier eğrileri uç
|
||||||
|
// noktalarıyla düzleştirilir — hücre ölçeğinde fark etmez).
|
||||||
|
//
|
||||||
|
// Çalıştırma: pnpm tsx scripts/il-izgara-uret.ts
|
||||||
|
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { cities } from "turkey-map-react/lib/data";
|
||||||
|
import { normalizeIlAdi, HARITA_UST_KIRPMA, HARITA_GENISLIK, HARITA_YUKSEKLIK } from "../src/lib/harita";
|
||||||
|
import { ULKE_HARITALARI } from "../src/lib/harita-ulkeler-verisi";
|
||||||
|
|
||||||
|
const SUTUN = 72;
|
||||||
|
const HUCRE = HARITA_GENISLIK / SUTUN;
|
||||||
|
const YUKSEKLIK = HARITA_YUKSEKLIK - HARITA_UST_KIRPMA; // 447
|
||||||
|
const SATIR = Math.ceil(YUKSEKLIK / HUCRE);
|
||||||
|
|
||||||
|
type Nokta = [number, number];
|
||||||
|
type Hucre = [number, number];
|
||||||
|
|
||||||
|
function poligonlar(d: string): Nokta[][] {
|
||||||
|
const toks = d.match(/[MLHVCZmlhvcz]|-?\d*\.?\d+(?:e-?\d+)?/g) ?? [];
|
||||||
|
const polys: Nokta[][] = [];
|
||||||
|
let cur: Nokta[] = [];
|
||||||
|
let x = 0;
|
||||||
|
let y = 0;
|
||||||
|
let cmd = "";
|
||||||
|
let i = 0;
|
||||||
|
const num = () => Number(toks[i++]);
|
||||||
|
const kapat = () => {
|
||||||
|
if (cur.length > 2) polys.push(cur);
|
||||||
|
cur = [];
|
||||||
|
};
|
||||||
|
while (i < toks.length) {
|
||||||
|
const t = toks[i];
|
||||||
|
if (/[A-Za-z]/.test(t)) {
|
||||||
|
cmd = t;
|
||||||
|
i++;
|
||||||
|
if (cmd === "Z" || cmd === "z") kapat();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
switch (cmd) {
|
||||||
|
case "M": x = num(); y = num(); kapat(); cur = [[x, y]]; cmd = "L"; break;
|
||||||
|
case "m": x += num(); y += num(); kapat(); cur = [[x, y]]; cmd = "l"; break;
|
||||||
|
case "L": x = num(); y = num(); cur.push([x, y]); break;
|
||||||
|
case "l": x += num(); y += num(); cur.push([x, y]); break;
|
||||||
|
case "H": x = num(); cur.push([x, y]); break;
|
||||||
|
case "h": x += num(); cur.push([x, y]); break;
|
||||||
|
case "V": y = num(); cur.push([x, y]); break;
|
||||||
|
case "v": y += num(); cur.push([x, y]); break;
|
||||||
|
case "C": num(); num(); num(); num(); x = num(); y = num(); cur.push([x, y]); break;
|
||||||
|
case "c": num(); num(); num(); num(); x += num(); y += num(); cur.push([x, y]); break;
|
||||||
|
default: i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kapat();
|
||||||
|
return polys;
|
||||||
|
}
|
||||||
|
|
||||||
|
function icinde(px: number, py: number, poly: Nokta[]): boolean {
|
||||||
|
let ic = false;
|
||||||
|
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
||||||
|
const [xi, yi] = poly[i];
|
||||||
|
const [xj, yj] = poly[j];
|
||||||
|
if (yi > py !== yj > py && px < ((xj - xi) * (py - yi)) / (yj - yi) + xi) ic = !ic;
|
||||||
|
}
|
||||||
|
return ic;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Her hücre merkezini ilk eşleşen bölgeye verir; sıra öncelik demektir. */
|
||||||
|
function rasterize(
|
||||||
|
bolgeler: { ad: string; polys: Nokta[][] }[],
|
||||||
|
ustKirpma: number,
|
||||||
|
): Map<string, Hucre[]> {
|
||||||
|
const sonuc = new Map<string, Hucre[]>();
|
||||||
|
for (let r = 0; r < SATIR; r++) {
|
||||||
|
for (let q = 0; q < SUTUN; q++) {
|
||||||
|
const px = (q + 0.5) * HUCRE;
|
||||||
|
const py = ustKirpma + (r + 0.5) * HUCRE;
|
||||||
|
const b = bolgeler.find((b) => b.polys.some((p) => icinde(px, py, p)));
|
||||||
|
if (!b) continue;
|
||||||
|
const liste = sonuc.get(b.ad) ?? [];
|
||||||
|
liste.push([q, r]);
|
||||||
|
sonuc.set(b.ad, liste);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sonuc;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Türkiye illeri
|
||||||
|
const iller = cities.map((c) => ({ ad: normalizeIlAdi(c.name), polys: poligonlar(c.path) }));
|
||||||
|
const ilHucre = rasterize(iller, HARITA_UST_KIRPMA);
|
||||||
|
// Küçük il garantisi: 4 hücreden azı olan il bbox merkezinde 2×2 blok alır,
|
||||||
|
// o hücreler komşulardan düşer.
|
||||||
|
for (const il of iller) {
|
||||||
|
const mevcut = ilHucre.get(il.ad) ?? [];
|
||||||
|
if (mevcut.length >= 4) continue;
|
||||||
|
const pts = il.polys.flat();
|
||||||
|
const cx = (Math.min(...pts.map((p) => p[0])) + Math.max(...pts.map((p) => p[0]))) / 2;
|
||||||
|
const cy = (Math.min(...pts.map((p) => p[1])) + Math.max(...pts.map((p) => p[1]))) / 2;
|
||||||
|
const q0 = Math.min(SUTUN - 2, Math.max(0, Math.round(cx / HUCRE) - 1));
|
||||||
|
const r0 = Math.min(SATIR - 2, Math.max(0, Math.round((cy - HARITA_UST_KIRPMA) / HUCRE) - 1));
|
||||||
|
const blok: Hucre[] = [[q0, r0], [q0 + 1, r0], [q0, r0 + 1], [q0 + 1, r0 + 1]];
|
||||||
|
const anahtar = new Set(blok.map(([q, r]) => `${q},${r}`));
|
||||||
|
for (const [ad, liste] of ilHucre) {
|
||||||
|
if (ad === il.ad) continue;
|
||||||
|
ilHucre.set(ad, liste.filter(([q, r]) => !anahtar.has(`${q},${r}`)));
|
||||||
|
}
|
||||||
|
const birlesik = new Map(mevcut.map((h) => [`${h[0]},${h[1]}`, h]));
|
||||||
|
for (const h of blok) birlesik.set(`${h[0]},${h[1]}`, h);
|
||||||
|
ilHucre.set(il.ad, [...birlesik.values()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Ülkeler (KKTC, yurt dışı): ülke öncelikli, bağlam ikincil
|
||||||
|
const ulkeHucre: Record<string, { ulke: Hucre[]; baglam: Hucre[] }> = {};
|
||||||
|
for (const [kod, harita] of Object.entries(ULKE_HARITALARI)) {
|
||||||
|
const r = rasterize(
|
||||||
|
[
|
||||||
|
{ ad: "ulke", polys: harita.ulke.flatMap(poligonlar) },
|
||||||
|
{ ad: "baglam", polys: harita.baglam.flatMap(poligonlar) },
|
||||||
|
],
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
ulkeHucre[kod] = { ulke: r.get("ulke") ?? [], baglam: r.get("baglam") ?? [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
const enKucuk = [...ilHucre.entries()].sort((a, b) => a[1].length - b[1].length).slice(0, 5);
|
||||||
|
console.log(
|
||||||
|
`${SUTUN}×${SATIR}, hücre ${HUCRE.toFixed(3)}; kara ${[...ilHucre.values()].reduce((a, b) => a + b.length, 0)}; en küçük: ${enKucuk.map(([a, l]) => `${a}:${l.length}`).join(" ")}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
const satirlar = [...ilHucre.entries()]
|
||||||
|
.sort((a, b) => a[0].localeCompare(b[0], "tr-TR"))
|
||||||
|
.map(([ad, liste]) => ` ${JSON.stringify(ad)}: ${JSON.stringify(liste)},`);
|
||||||
|
const ulkeSatirlar = Object.entries(ulkeHucre).map(
|
||||||
|
([kod, v]) => ` ${JSON.stringify(kod)}: { ulke: ${JSON.stringify(v.ulke)}, baglam: ${JSON.stringify(v.baglam)} },`,
|
||||||
|
);
|
||||||
|
const cikti = `// Bu dosya scripts/il-izgara-uret.ts tarafından üretildi — elle düzenleme.
|
||||||
|
// Hero'nun dot-matrix ızgarasında (${SUTUN} sütun × ${SATIR} satır, hücre ${HUCRE.toFixed(4)}
|
||||||
|
// viewBox birimi) her ilin ve ülkenin kapladığı hücreler [sütun, satır].
|
||||||
|
|
||||||
|
import type { UlkeKodu } from "./harita-ulkeler-verisi";
|
||||||
|
|
||||||
|
export const IL_IZGARA = {
|
||||||
|
sutun: ${SUTUN},
|
||||||
|
satir: ${SATIR},
|
||||||
|
hucre: ${HUCRE.toFixed(4)},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type IzgaraHucre = readonly [number, number];
|
||||||
|
|
||||||
|
/** Anahtar DB biçiminde il adı (normalizeIlAdi). */
|
||||||
|
export const IL_HUCRELERI: Record<string, readonly IzgaraHucre[]> = {
|
||||||
|
${satirlar.join("\n")}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ULKE_HUCRELERI: Record<UlkeKodu, { ulke: readonly IzgaraHucre[]; baglam: readonly IzgaraHucre[] }> = {
|
||||||
|
${ulkeSatirlar.join("\n")}
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
const hedef = path.join(process.cwd(), "src/lib/il-izgara-verisi.ts");
|
||||||
|
fs.writeFileSync(hedef, cikti);
|
||||||
|
console.log("yazıldı:", hedef, `${(cikti.length / 1024).toFixed(0)} KB`);
|
||||||
@@ -322,6 +322,31 @@
|
|||||||
animation: kt-piksel-nabiz 0.42s cubic-bezier(0.23, 1, 0.32, 1);
|
animation: kt-piksel-nabiz 0.42s cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Şehir haritasında (sehir-piksel-haritasi.tsx) ilin kareleri sırayla
|
||||||
|
belirir; her karenin gecikmesi inline (35 ms × sıra). DESIGN.md §4.17. */
|
||||||
|
@keyframes kt-piksel-belir {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.6);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.kt-piksel-belir {
|
||||||
|
transform-box: fill-box;
|
||||||
|
transform-origin: center;
|
||||||
|
animation: kt-piksel-belir 0.42s cubic-bezier(0.23, 1, 0.32, 1) both;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.kt-piksel-belir {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Bildirim (toast, sonner) — DESIGN.md §4.16. Başlık ve açıklama slate
|
/* Bildirim (toast, sonner) — DESIGN.md §4.16. Başlık ve açıklama slate
|
||||||
tonlarında okunur; aksiyon düğmesi üst çubuktaki koyu hap dilinde.
|
tonlarında okunur; aksiyon düğmesi üst çubuktaki koyu hap dilinde.
|
||||||
".toaster" öneki şart: sonner kendi stilini çalışma anında enjekte eder,
|
".toaster" öneki şart: sonner kendi stilini çalışma anında enjekte eder,
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import type { MetadataRoute } from "next";
|
import type { MetadataRoute } from "next";
|
||||||
import { KATALOG_VERI_TARIHI, SITE_URL } from "@/lib/seo";
|
import { KATALOG_VERI_TARIHI, SITE_URL } from "@/lib/seo";
|
||||||
import { getAllBolumSlugs, getAllUniversiteSlugs } from "@/lib/katalog";
|
import {
|
||||||
|
getAllBolumSlugs,
|
||||||
|
getAllIlSlugs,
|
||||||
|
getAllUniversiteSlugs,
|
||||||
|
} from "@/lib/katalog";
|
||||||
import { getAllRehberler } from "@/lib/rehber";
|
import { getAllRehberler } from "@/lib/rehber";
|
||||||
|
|
||||||
// Bilinçli olarak sitemap DIŞI: /sonuc (noindex), /giris, /listem,
|
// Bilinçli olarak sitemap DIŞI: /sonuc (noindex), /giris, /listem,
|
||||||
@@ -46,6 +50,12 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|||||||
changeFrequency: "monthly",
|
changeFrequency: "monthly",
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
const iller: MetadataRoute.Sitemap = getAllIlSlugs().map((slug) => ({
|
||||||
|
url: `${SITE_URL}/universiteler/${slug}`,
|
||||||
|
lastModified: KATALOG_VERI_TARIHI,
|
||||||
|
priority: 0.6,
|
||||||
|
changeFrequency: "monthly",
|
||||||
|
}));
|
||||||
const yazilar = getAllRehberler();
|
const yazilar = getAllRehberler();
|
||||||
const rehberler: MetadataRoute.Sitemap = yazilar.map((y) => ({
|
const rehberler: MetadataRoute.Sitemap = yazilar.map((y) => ({
|
||||||
url: `${SITE_URL}/rehber/${y.slug}`,
|
url: `${SITE_URL}/rehber/${y.slug}`,
|
||||||
@@ -72,6 +82,7 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|||||||
rehberDizini,
|
rehberDizini,
|
||||||
...bolumler,
|
...bolumler,
|
||||||
...universiteler,
|
...universiteler,
|
||||||
|
...iller,
|
||||||
...rehberler,
|
...rehberler,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
33
src/app/universiteler/[il]/page.tsx
Normal file
33
src/app/universiteler/[il]/page.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Suspense } from "react";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import { SiteFooter } from "@/components/site-footer";
|
||||||
|
import {
|
||||||
|
IlIcerik,
|
||||||
|
IlIcerikSkeleton,
|
||||||
|
ilSayfaMetadata,
|
||||||
|
ilStaticSlugs,
|
||||||
|
} from "@/features/katalog/components/il-icerik";
|
||||||
|
|
||||||
|
export function generateStaticParams() {
|
||||||
|
return ilStaticSlugs().map((il) => ({ il }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata({
|
||||||
|
params,
|
||||||
|
}: PageProps<"/universiteler/[il]">): Promise<Metadata> {
|
||||||
|
const { il } = await params;
|
||||||
|
return ilSayfaMetadata(il);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function IlPage({ params }: PageProps<"/universiteler/[il]">) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Suspense fallback={<IlIcerikSkeleton />}>
|
||||||
|
{params.then(({ il }) => (
|
||||||
|
<IlIcerik slug={il} />
|
||||||
|
))}
|
||||||
|
</Suspense>
|
||||||
|
<SiteFooter />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,6 +11,11 @@ import { cn } from "@/lib/utils";
|
|||||||
*
|
*
|
||||||
* - `buyuk` (varsayılan): sayfa kapanışı / panel — ortalı, iri boşluk.
|
* - `buyuk` (varsayılan): sayfa kapanışı / panel — ortalı, iri boşluk.
|
||||||
* - `kompakt`: çekmece altı gibi dar yerler — sola yaslı, alçak.
|
* - `kompakt`: çekmece altı gibi dar yerler — sola yaslı, alçak.
|
||||||
|
* - `ton="beyaz"`: aynı bant beyaz zeminde — slate kenarlık, koyu yazı,
|
||||||
|
* slate pikseller. Aynı sayfada üst üste iki mavi bant olmasın diye
|
||||||
|
* (Bilal, 24 Eyl 2026: "her kısımda mavi arka kullanmışsın, çeşitlendir").
|
||||||
|
* Çağıran paragraf renklerini tona göre verir (`text-white/90` ↔
|
||||||
|
* `text-slate-600`).
|
||||||
*
|
*
|
||||||
* İçerik (başlık, paragraf, buton) çağıranındır; yalnız kabuk burada durur.
|
* İçerik (başlık, paragraf, buton) çağıranındır; yalnız kabuk burada durur.
|
||||||
* Bilal'in kararı (24 Eyl 2026): koyu slate-950 yüzey yerine bu band.
|
* Bilal'in kararı (24 Eyl 2026): koyu slate-950 yüzey yerine bu band.
|
||||||
@@ -20,6 +25,7 @@ export function CtaBandi({
|
|||||||
className,
|
className,
|
||||||
seed = 8,
|
seed = 8,
|
||||||
boyut = "buyuk",
|
boyut = "buyuk",
|
||||||
|
ton = "mavi",
|
||||||
etiket = "section",
|
etiket = "section",
|
||||||
ref,
|
ref,
|
||||||
...props
|
...props
|
||||||
@@ -27,6 +33,7 @@ export function CtaBandi({
|
|||||||
/** Piksel dokusunun deseni; aynı sayfadaki iki band farklı olsun */
|
/** Piksel dokusunun deseni; aynı sayfadaki iki band farklı olsun */
|
||||||
seed?: number;
|
seed?: number;
|
||||||
boyut?: "buyuk" | "kompakt";
|
boyut?: "buyuk" | "kompakt";
|
||||||
|
ton?: "mavi" | "beyaz";
|
||||||
/** Çekmece altı gibi başlıksız yerlerde `div` */
|
/** Çekmece altı gibi başlıksız yerlerde `div` */
|
||||||
etiket?: "section" | "div";
|
etiket?: "section" | "div";
|
||||||
ref?: Ref<HTMLElement>;
|
ref?: Ref<HTMLElement>;
|
||||||
@@ -38,13 +45,20 @@ export function CtaBandi({
|
|||||||
<Etiket
|
<Etiket
|
||||||
ref={ref as Ref<HTMLDivElement>}
|
ref={ref as Ref<HTMLDivElement>}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative isolate overflow-hidden rounded-2xl bg-primary text-white",
|
"relative isolate overflow-hidden rounded-2xl",
|
||||||
|
ton === "beyaz"
|
||||||
|
? "border border-slate-200 bg-white text-slate-900"
|
||||||
|
: "bg-primary text-white",
|
||||||
buyuk ? "px-5 py-8 text-center sm:p-12" : "px-5 py-4 text-left sm:py-5",
|
buyuk ? "px-5 py-8 text-center sm:p-12" : "px-5 py-4 text-left sm:py-5",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
<PikselZemin seed={seed} className={buyuk ? "-inset-y-10" : undefined} />
|
<PikselZemin
|
||||||
|
seed={seed}
|
||||||
|
ton={ton === "beyaz" ? "acik" : "koyu"}
|
||||||
|
className={buyuk ? "-inset-y-10" : undefined}
|
||||||
|
/>
|
||||||
{children}
|
{children}
|
||||||
</Etiket>
|
</Etiket>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -21,8 +21,14 @@ export function PikselZemin({
|
|||||||
yogunluk,
|
yogunluk,
|
||||||
kenar,
|
kenar,
|
||||||
haric,
|
haric,
|
||||||
|
ton = "koyu",
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
|
/**
|
||||||
|
* `koyu`: renkli (mavi) zemin, kareler beyaz. `acik`: beyaz zemin, kareler
|
||||||
|
* slate (Bilal, 24 Eyl 2026: "beyaz arka planda piksel de güzel görünüyor").
|
||||||
|
*/
|
||||||
|
ton?: "koyu" | "acik";
|
||||||
/** Aynı ekrandaki iki yüzey farklı desen alsın */
|
/** Aynı ekrandaki iki yüzey farklı desen alsın */
|
||||||
seed?: number;
|
seed?: number;
|
||||||
/** Doluluk çarpanı (1 varsayılan) */
|
/** Doluluk çarpanı (1 varsayılan) */
|
||||||
@@ -38,7 +44,8 @@ export function PikselZemin({
|
|||||||
<div
|
<div
|
||||||
aria-hidden
|
aria-hidden
|
||||||
className={cn(
|
className={cn(
|
||||||
"pointer-events-none absolute inset-x-0 -inset-y-6 -z-10 text-white",
|
"pointer-events-none absolute inset-x-0 -inset-y-6 -z-10",
|
||||||
|
ton === "acik" ? "text-slate-400" : "text-white",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -4,7 +4,8 @@ import { startTransition, useMemo, useRef, useState } from "react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ChevronDown, ChevronUp } from "lucide-react";
|
import { ChevronDown, ChevronUp } from "lucide-react";
|
||||||
import type { Program } from "@/types/yokatlas";
|
import type { Program } from "@/types/yokatlas";
|
||||||
import { trBaslikDuzeni } from "@/lib/slug";
|
import { ilSayfaHref, trBaslikDuzeni } from "@/lib/slug";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { UNITUR_ETIKET } from "@/lib/unitur";
|
import { UNITUR_ETIKET } from "@/lib/unitur";
|
||||||
import { VERI_YIL_SAYISI } from "@/lib/veri-yillari";
|
import { VERI_YIL_SAYISI } from "@/lib/veri-yillari";
|
||||||
import { Sayfalama, SAYFA_BOYU } from "./sayfalama";
|
import { Sayfalama, SAYFA_BOYU } from "./sayfalama";
|
||||||
@@ -23,6 +24,7 @@ import {
|
|||||||
SATIR_BUTON_KABI,
|
SATIR_BUTON_KABI,
|
||||||
SATIR_CIP,
|
SATIR_CIP,
|
||||||
SATIR_GRUP,
|
SATIR_GRUP,
|
||||||
|
SATIR_IKINCIL_LINK,
|
||||||
SATIR_META,
|
SATIR_META,
|
||||||
SATIR_MOBIL_SAYI,
|
SATIR_MOBIL_SAYI,
|
||||||
} from "@/components/liste-satiri";
|
} from "@/components/liste-satiri";
|
||||||
@@ -247,9 +249,13 @@ export function BolumProgramListesi({
|
|||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
{p.il ? (
|
{p.il ? (
|
||||||
<span className="shrink-0">
|
<Link
|
||||||
|
href={ilSayfaHref(p.il)}
|
||||||
|
className={cn("shrink-0", SATIR_IKINCIL_LINK)}
|
||||||
|
title={`${trBaslikDuzeni(p.il)} üniversiteleri`}
|
||||||
|
>
|
||||||
{trBaslikDuzeni(p.il)}
|
{trBaslikDuzeni(p.il)}
|
||||||
</span>
|
</Link>
|
||||||
) : null}
|
) : null}
|
||||||
{p.sure ? (
|
{p.sure ? (
|
||||||
<span className="shrink-0">{p.sure} yıl</span>
|
<span className="shrink-0">{p.sure} yıl</span>
|
||||||
|
|||||||
220
src/features/katalog/components/il-icerik.tsx
Normal file
220
src/features/katalog/components/il-icerik.tsx
Normal file
@@ -0,0 +1,220 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { notFound } from "next/navigation";
|
||||||
|
import { Building2, ChevronRight, Landmark, MapPin } from "lucide-react";
|
||||||
|
import {
|
||||||
|
getAllIller,
|
||||||
|
getAllIlSlugs,
|
||||||
|
getIlBySlug,
|
||||||
|
type UniOzet,
|
||||||
|
} from "../katalog-queries";
|
||||||
|
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
|
||||||
|
import { PagePixelDivider } from "@/components/pixel-decor";
|
||||||
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import {
|
||||||
|
JsonLd,
|
||||||
|
breadcrumbJsonLd,
|
||||||
|
sayfaMetadata,
|
||||||
|
ustSegmentOgGorseli,
|
||||||
|
} from "@/lib/seo";
|
||||||
|
import { BASLIK_YILI, SON_YIL, TERCIH_YILI } from "@/lib/veri-yillari";
|
||||||
|
import { NavBaslik } from "./nav-baslik";
|
||||||
|
import { SehirCipleri, oneCikanIller } from "./sehir-cipleri";
|
||||||
|
import { SehirPikselHaritasi } from "./sehir-piksel-haritasi";
|
||||||
|
import { SehirYuzeyi } from "./sehir-yuzeyi";
|
||||||
|
import { UniLinkleri } from "./universite-listesi";
|
||||||
|
|
||||||
|
const sayi = (n: number) => n.toLocaleString("tr-TR");
|
||||||
|
|
||||||
|
export function ilStaticSlugs(): string[] {
|
||||||
|
return getAllIlSlugs();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ilSayfaMetadata(slug: string): Metadata {
|
||||||
|
const il = getIlBySlug(slug);
|
||||||
|
if (!il) return {};
|
||||||
|
return sayfaMetadata({
|
||||||
|
title: `${il.ad} Üniversiteleri Taban Puanları ve Sıralamaları ${BASLIK_YILI}`,
|
||||||
|
description: `${il.ad}'daki ${il.uniler.length} üniversitenin ${SON_YIL} YKS yerleştirme verileri: ${sayi(il.programSayisi)} programın taban puanları, başarı sıralamaları ve kontenjanları. ${TERCIH_YILI} tercih rehberi.`,
|
||||||
|
path: `/universiteler/${slug}`,
|
||||||
|
openGraph: {
|
||||||
|
images: ustSegmentOgGorseli(
|
||||||
|
"/universiteler",
|
||||||
|
"Üniversitelerin taban puanları ve sıralamaları — KolayTercih",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function UniGrubu({
|
||||||
|
baslik,
|
||||||
|
uniler,
|
||||||
|
}: {
|
||||||
|
baslik: string;
|
||||||
|
uniler: UniOzet[];
|
||||||
|
}) {
|
||||||
|
if (uniler.length === 0) return null;
|
||||||
|
return (
|
||||||
|
<section className="mt-10 text-center lg:text-left">
|
||||||
|
<h2 className="font-heading text-2xl font-bold text-slate-900">
|
||||||
|
{baslik}
|
||||||
|
<span className="ml-2 text-sm font-normal text-slate-400">
|
||||||
|
{sayi(uniler.length)}
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<UniLinkleri uniler={uniler} />
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* /universiteler/[il]: bir şehirdeki üniversiteler, devlet / vakıf / diğer
|
||||||
|
* öbekleriyle. Düzen katalog detay sayfalarıyla aynı (DESIGN.md §5.2):
|
||||||
|
* yol satırı, başlık, kimlik hapları ve özet lg altında ortalı; listeler
|
||||||
|
* her zaman sola yaslı.
|
||||||
|
*/
|
||||||
|
export function IlIcerik({ slug }: { slug: string }) {
|
||||||
|
const il = getIlBySlug(slug);
|
||||||
|
if (!il) notFound();
|
||||||
|
|
||||||
|
const devlet: UniOzet[] = [];
|
||||||
|
const vakif: UniOzet[] = [];
|
||||||
|
const diger: UniOzet[] = [];
|
||||||
|
for (const u of il.uniler) {
|
||||||
|
if (u.unitur === "DEVLET") devlet.push(u);
|
||||||
|
else if (u.unitur === "VAKIF" || u.unitur === "VAKIF MYO") vakif.push(u);
|
||||||
|
else diger.push(u);
|
||||||
|
}
|
||||||
|
const ozetParcalar = [
|
||||||
|
devlet.length > 0 ? `${sayi(devlet.length)} devlet` : null,
|
||||||
|
vakif.length > 0 ? `${sayi(vakif.length)} vakıf` : null,
|
||||||
|
diger.length > 0 ? `${sayi(diger.length)} KKTC/yurt dışı` : null,
|
||||||
|
].filter(Boolean);
|
||||||
|
const ctaBaslik = `Sıralaman ${il.ad} üniversitelerinin neresinde?`;
|
||||||
|
const digerSehirler = oneCikanIller(getAllIller());
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="mx-auto w-full max-w-5xl px-4 py-12 sm:py-16">
|
||||||
|
<JsonLd
|
||||||
|
data={breadcrumbJsonLd([
|
||||||
|
{ name: "Ana Sayfa", path: "/" },
|
||||||
|
{ name: "Üniversiteler", path: "/universiteler" },
|
||||||
|
{ name: il.ad, path: `/universiteler/${slug}` },
|
||||||
|
])}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<nav
|
||||||
|
aria-label="İçerik yolu"
|
||||||
|
className="flex flex-wrap items-center justify-center gap-1.5 text-sm text-slate-500 lg:justify-start"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="transition-colors hover:text-orange-500 hover:underline"
|
||||||
|
>
|
||||||
|
Ana Sayfa
|
||||||
|
</Link>
|
||||||
|
<ChevronRight className="size-3.5 shrink-0" aria-hidden />
|
||||||
|
<Link
|
||||||
|
href="/universiteler"
|
||||||
|
className="transition-colors hover:text-orange-500 hover:underline"
|
||||||
|
>
|
||||||
|
Üniversiteler
|
||||||
|
</Link>
|
||||||
|
<ChevronRight className="size-3.5 shrink-0" aria-hidden />
|
||||||
|
<span className="text-slate-700">{il.ad}</span>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<NavBaslik baslikId="il-baslik" ad={`${il.ad} üniversiteleri`} />
|
||||||
|
{/* Başlık yüzeyi: beyaz kart + slate piksel doku, şehrin dot-matrix
|
||||||
|
haritası (DESIGN.md §4.17). lg altında metin ortalı, harita altta. */}
|
||||||
|
<SehirYuzeyi
|
||||||
|
seed={31}
|
||||||
|
harita={<SehirPikselHaritasi il={il.il} ad={il.ad} />}
|
||||||
|
metin={
|
||||||
|
<div className="text-center lg:text-left">
|
||||||
|
<h1
|
||||||
|
id="il-baslik"
|
||||||
|
className="font-heading text-3xl font-bold leading-tight text-slate-900 sm:text-5xl"
|
||||||
|
>
|
||||||
|
{il.ad} Üniversiteleri Taban Puanları {BASLIK_YILI}
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
{/* Kimlik hapları (DESIGN.md §4.12): il + üniversite sayısı */}
|
||||||
|
<p className="mt-5 flex flex-wrap items-center justify-center gap-2 lg:justify-start">
|
||||||
|
<span className="inline-flex h-10 items-center gap-2 rounded-full border border-slate-200 bg-white px-4 text-sm font-semibold text-slate-800">
|
||||||
|
<MapPin className="size-4 text-primary" aria-hidden />
|
||||||
|
{il.ad}
|
||||||
|
</span>
|
||||||
|
<span className="inline-flex h-10 items-center gap-2 rounded-full border border-slate-200 bg-white px-4 text-sm font-semibold text-slate-800">
|
||||||
|
{devlet.length >= vakif.length ? (
|
||||||
|
<Landmark className="size-4 text-primary" aria-hidden />
|
||||||
|
) : (
|
||||||
|
<Building2 className="size-4 text-primary" aria-hidden />
|
||||||
|
)}
|
||||||
|
{sayi(il.uniler.length)} üniversite
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p className="mx-auto mt-6 max-w-2xl text-base leading-relaxed text-slate-600 sm:text-lg lg:mx-0">
|
||||||
|
{il.ad}'da {SON_YIL} YKS yerleştirmesinde öğrenci alan{" "}
|
||||||
|
<strong>{sayi(il.uniler.length)} üniversite</strong>
|
||||||
|
{ozetParcalar.length > 1 ? ` (${ozetParcalar.join(", ")})` : ""}{" "}
|
||||||
|
ve toplam <strong>{sayi(il.programSayisi)} program</strong> var.
|
||||||
|
Üniversiteye tıklayınca programlarının taban puanlarını, başarı
|
||||||
|
sıralamalarını ve kontenjanlarını fakülte fakülte görürsün.
|
||||||
|
Veriler YÖK Atlas {SON_YIL} yerleştirme sonuçlarına dayanır;{" "}
|
||||||
|
{TERCIH_YILI} tercihleri için rehber niteliğindedir.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<CtaSiraForm varyant="kompakt" konum="ust" baslik={ctaBaslik} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<PagePixelDivider seed={71} className="mx-auto mt-8 lg:mx-0" />
|
||||||
|
|
||||||
|
<UniGrubu baslik="Devlet Üniversiteleri" uniler={devlet} />
|
||||||
|
<UniGrubu baslik="Vakıf Üniversiteleri" uniler={vakif} />
|
||||||
|
<UniGrubu baslik="KKTC ve Yurt Dışı" uniler={diger} />
|
||||||
|
|
||||||
|
<section className="mt-12 text-center lg:text-left" aria-labelledby="diger-sehirler">
|
||||||
|
<h2
|
||||||
|
id="diger-sehirler"
|
||||||
|
className="font-heading text-2xl font-bold text-slate-900"
|
||||||
|
>
|
||||||
|
Diğer şehirler
|
||||||
|
</h2>
|
||||||
|
<SehirCipleri
|
||||||
|
iller={digerSehirler}
|
||||||
|
aktifSlug={slug}
|
||||||
|
tumuHref="/universiteler#tum-sehirler"
|
||||||
|
className="mt-4"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="mt-12">
|
||||||
|
<CtaSiraForm
|
||||||
|
baslik={ctaBaslik}
|
||||||
|
aciklama={`YKS başarı sıralamanı gir; ${il.ad} dahil tüm şehirlerde sıralamana uygun hayal, dengeli ve güvenli dilimlerindeki programları anında listele. Ücretsiz, üyelik gerekmez.`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function IlIcerikSkeleton() {
|
||||||
|
return (
|
||||||
|
<main className="mx-auto w-full max-w-5xl px-4 py-12 sm:py-16">
|
||||||
|
<Skeleton className="mx-auto h-4 w-64 lg:mx-0" />
|
||||||
|
<Skeleton className="mt-6 h-72 w-full rounded-3xl" />
|
||||||
|
<div className="mt-10 space-y-4">
|
||||||
|
<Skeleton className="h-8 w-72" />
|
||||||
|
{Array.from({ length: 6 }, (_, i) => (
|
||||||
|
<Skeleton key={i} className="h-9 w-full max-w-2xl" />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
80
src/features/katalog/components/sehir-cipleri.tsx
Normal file
80
src/features/katalog/components/sehir-cipleri.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { IlOzet } from "../katalog-queries";
|
||||||
|
|
||||||
|
const sayi = (n: number) => n.toLocaleString("tr-TR");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Şehir çipi (DESIGN.md §4.17): sihirbazın il çipiyle aynı kalıp — 40 px,
|
||||||
|
* tam yuvarlak, hover §2.1 tek turuncu; seçili/aktif hâl mavi dolu.
|
||||||
|
* Etkileşimli çip olduğu için rozet (§4.2) kuralının dışındadır.
|
||||||
|
*/
|
||||||
|
const CIP_TEMEL =
|
||||||
|
"inline-flex h-10 cursor-pointer items-center gap-1.5 rounded-full border px-3.5 text-sm font-semibold transition-[background-color,border-color,color,transform] duration-150 active:scale-[0.97]";
|
||||||
|
const CIP_PASIF =
|
||||||
|
"border-slate-200 bg-white text-slate-700 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500";
|
||||||
|
const CIP_AKTIF = "border-primary bg-primary text-white";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Şehre göre üniversite gezinme çipleri. `iller` verilen sırayla dizilir
|
||||||
|
* (çağıran genelde üniversite sayısına göre azalan verir). `aktifSlug`
|
||||||
|
* bulunulan şehir sayfasıdır: mavi dolu ve `aria-current="page"`.
|
||||||
|
* `tumuHref` verilirse sona "Tüm şehirler" çipi eklenir (A–Z listesine).
|
||||||
|
*/
|
||||||
|
export function SehirCipleri({
|
||||||
|
iller,
|
||||||
|
aktifSlug,
|
||||||
|
tumuHref,
|
||||||
|
tumuEtiket,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
iller: IlOzet[];
|
||||||
|
aktifSlug?: string;
|
||||||
|
tumuHref?: string;
|
||||||
|
tumuEtiket?: string;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<ul
|
||||||
|
className={cn(
|
||||||
|
"flex flex-wrap items-center justify-center gap-2 lg:justify-start",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{iller.map((il) => {
|
||||||
|
const aktif = il.slug === aktifSlug;
|
||||||
|
return (
|
||||||
|
<li key={il.slug}>
|
||||||
|
<Link
|
||||||
|
href={`/universiteler/${il.slug}`}
|
||||||
|
aria-current={aktif ? "page" : undefined}
|
||||||
|
className={cn(CIP_TEMEL, aktif ? CIP_AKTIF : CIP_PASIF)}
|
||||||
|
>
|
||||||
|
{il.ad}
|
||||||
|
<span
|
||||||
|
className={cn(
|
||||||
|
"font-normal tabular-nums",
|
||||||
|
aktif ? "text-white/80" : "text-slate-400",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{sayi(il.uniler.length)}
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
{tumuHref ? (
|
||||||
|
<li>
|
||||||
|
<Link href={tumuHref} className={cn(CIP_TEMEL, CIP_PASIF)}>
|
||||||
|
{tumuEtiket ?? "Tüm şehirler"}
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
) : null}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Çip satırında gösterilecek şehirler: en az iki üniversitesi olanlar. */
|
||||||
|
export function oneCikanIller(iller: IlOzet[]): IlOzet[] {
|
||||||
|
return iller.filter((il) => il.uniler.length >= 2);
|
||||||
|
}
|
||||||
225
src/features/katalog/components/sehir-piksel-haritasi.tsx
Normal file
225
src/features/katalog/components/sehir-piksel-haritasi.tsx
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Şehri temsil eden piksel harita (DESIGN.md §4.17) — hero'daki dot-matrix
|
||||||
|
* Türkiye'nin (turkey-pixel-map.tsx) şehir sayfası ve üniversite konum kartı
|
||||||
|
* için genellenmiş hâli. Aynı dil: iri kareler, altı farklı opaklık, kıyının
|
||||||
|
* altına seyrelen serpinti, imleç yaklaşınca turuncuya ısınan pikseller
|
||||||
|
* (usePixelHeat). Fark: ızgara 72 sütun (her il en az 2×2 kare) ve
|
||||||
|
* bulunulan ilin kareleri turuncu, hücrenin %88'i kadar iri (Bilal, 24 Eyl
|
||||||
|
* 2026: "C'deki gibi piksel olabilir"), sırayla belirir ve nabız atar.
|
||||||
|
*
|
||||||
|
* Markup hero'daki gibi deterministik string olarak üretilir ve
|
||||||
|
* dangerouslySetInnerHTML ile basılır: ~1300 rect React elemanı olarak
|
||||||
|
* hydrate edilmez; server ve client aynı çıktıyı verir.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useRef } from "react";
|
||||||
|
import { usePixelHeat } from "@/components/use-pixel-heat";
|
||||||
|
import { ilUlkeHaritasi, type UlkeKodu } from "@/lib/harita-ulkeler";
|
||||||
|
import {
|
||||||
|
IL_HUCRELERI,
|
||||||
|
IL_IZGARA,
|
||||||
|
ULKE_HUCRELERI,
|
||||||
|
type IzgaraHucre,
|
||||||
|
} from "@/lib/il-izgara-verisi";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const { sutun: SUTUN, satir: SATIR, hucre: CELL } = IL_IZGARA;
|
||||||
|
const DOT = CELL * 0.74;
|
||||||
|
const INSET = (CELL - DOT) / 2;
|
||||||
|
// Vurgu kareleri iri ve dolu (C seçeneğinin hissi)
|
||||||
|
const DOT_VURGU = CELL * 0.88;
|
||||||
|
const INSET_VURGU = (CELL - DOT_VURGU) / 2;
|
||||||
|
// Güney kıyının altına seyrelerek dağılan serpinti derinliği (hücre)
|
||||||
|
const SERPINTI = 5;
|
||||||
|
const VIEW_W = SUTUN * CELL;
|
||||||
|
const VIEW_H = (SATIR + SERPINTI) * CELL;
|
||||||
|
// Hero'nun ısı fırçası 640 birimlik viewBox'ta 34; burada aynı oran.
|
||||||
|
const ISI_YARICAPI = 34 * (VIEW_W / 640);
|
||||||
|
|
||||||
|
const OPACITIES = [0.3, 0.16, 0.24, 0.12, 0.27, 0.19];
|
||||||
|
const opacityFor = (x: number, y: number) =>
|
||||||
|
OPACITIES[(x * 7 + y * 11) % OPACITIES.length];
|
||||||
|
const VURGU_OPACITIES = [1, 0.9, 0.82, 0.95];
|
||||||
|
const vurguOpacityFor = (x: number, y: number) =>
|
||||||
|
VURGU_OPACITIES[(x * 3 + y * 5) % VURGU_OPACITIES.length];
|
||||||
|
|
||||||
|
// Deterministik sözde-rastgele (hero ile aynı): server ve client aynı deseni üretir
|
||||||
|
const hash = (x: number, y: number, seed = 0) => {
|
||||||
|
const n = Math.sin(x * 127.1 + y * 311.7 + seed * 74.7) * 43758.5453;
|
||||||
|
return n - Math.floor(n);
|
||||||
|
};
|
||||||
|
|
||||||
|
const f = (n: number) => n.toFixed(2);
|
||||||
|
|
||||||
|
function zeminKare(q: number, r: number, op: number) {
|
||||||
|
return `<rect data-px data-cx="${f(q * CELL + CELL / 2)}" data-cy="${f(r * CELL + CELL / 2)}" data-op="${op}" x="${f(q * CELL + INSET)}" y="${f(r * CELL + INSET)}" width="${f(DOT)}" height="${f(DOT)}" rx="${f(CELL * 0.2)}" fill="currentColor" fill-opacity="${op}"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function vurguKare(q: number, r: number, i: number) {
|
||||||
|
return `<rect class="kt-piksel-belir fill-orange-500" style="animation-delay:${i * 35}ms" x="${f(q * CELL + INSET_VURGU)}" y="${f(r * CELL + INSET_VURGU)}" width="${f(DOT_VURGU)}" height="${f(DOT_VURGU)}" rx="${f(CELL * 0.26)}" fill-opacity="${vurguOpacityFor(q, r)}"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Zemin (mavi doku) + vurgu (turuncu) hücrelerinden statik SVG içi markup.
|
||||||
|
* `isaret`: ülke haritasında şehir merkezi — 2×2 hücrelik tek iri kare.
|
||||||
|
*/
|
||||||
|
function markupUret(
|
||||||
|
zemin: readonly IzgaraHucre[],
|
||||||
|
vurgu: readonly IzgaraHucre[],
|
||||||
|
isaret: IzgaraHucre | null,
|
||||||
|
): string {
|
||||||
|
const parts: string[] = [];
|
||||||
|
const vurguSet = new Set(vurgu.map(([q, r]) => `${q},${r}`));
|
||||||
|
const enAlt: number[] = Array(SUTUN).fill(-1);
|
||||||
|
for (const [q, r] of zemin) {
|
||||||
|
if (vurguSet.has(`${q},${r}`)) continue;
|
||||||
|
parts.push(zeminKare(q, r, opacityFor(q, r)));
|
||||||
|
if (r > enAlt[q]) enAlt[q] = r;
|
||||||
|
}
|
||||||
|
for (const [q, r] of vurgu) if (r > enAlt[q]) enAlt[q] = r;
|
||||||
|
// Kıyının altına serpinti (hero ile aynı kural): kıyıdan uzaklaştıkça
|
||||||
|
// seyrelen, derinlikle sönen kareler; ısı fırçasına katılırlar.
|
||||||
|
for (let q = 0; q < SUTUN; q++) {
|
||||||
|
if (enAlt[q] < 0) continue;
|
||||||
|
for (let d = 1; d <= SERPINTI; d++) {
|
||||||
|
const r = enAlt[q] + d;
|
||||||
|
if (r >= SATIR + SERPINTI) break;
|
||||||
|
const density = 0.05 + 0.4 * (1 - (d - 1) / SERPINTI) ** 2;
|
||||||
|
if (hash(q, r) > density) continue;
|
||||||
|
const op = Math.round(opacityFor(q, r) * (1 - d / (SERPINTI + 1)) * 100) / 100;
|
||||||
|
parts.push(zeminKare(q, r, op));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Vurgu: sırayla belirir (kt-piksel-belir), grup hâlinde nabız atar
|
||||||
|
parts.push(
|
||||||
|
`<g class="animate-pulse motion-reduce:animate-none" style="animation-duration:3.6s">`,
|
||||||
|
);
|
||||||
|
vurgu.forEach(([q, r], i) => parts.push(vurguKare(q, r, i)));
|
||||||
|
if (isaret) {
|
||||||
|
const [q, r] = isaret;
|
||||||
|
const boy = 2 * CELL - 2 * INSET_VURGU;
|
||||||
|
parts.push(
|
||||||
|
`<rect class="kt-piksel-belir fill-orange-500" x="${f(q * CELL + INSET_VURGU)}" y="${f(r * CELL + INSET_VURGU)}" width="${f(boy)}" height="${f(boy)}" rx="${f(CELL * 0.5)}"/>`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
parts.push("</g>");
|
||||||
|
return parts.join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
const onbellek = new Map<string, string>();
|
||||||
|
function markup(anahtar: string, uret: () => string): string {
|
||||||
|
let m = onbellek.get(anahtar);
|
||||||
|
if (!m) {
|
||||||
|
m = uret();
|
||||||
|
onbellek.set(anahtar, m);
|
||||||
|
}
|
||||||
|
return m;
|
||||||
|
}
|
||||||
|
|
||||||
|
function PikselSvg({
|
||||||
|
html,
|
||||||
|
etiket,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
html: string;
|
||||||
|
etiket: string;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const svgRef = useRef<SVGSVGElement>(null);
|
||||||
|
usePixelHeat(svgRef, { radius: ISI_YARICAPI });
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
ref={svgRef}
|
||||||
|
viewBox={`0 0 ${VIEW_W} ${VIEW_H}`}
|
||||||
|
className={cn("h-auto w-full text-primary", className)}
|
||||||
|
role="img"
|
||||||
|
aria-label={etiket}
|
||||||
|
// Statik, deterministik markup — her render'da yeniden kurulmaz
|
||||||
|
dangerouslySetInnerHTML={{ __html: html }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const TUM_IL_HUCRELERI = Object.values(IL_HUCRELERI).flat();
|
||||||
|
|
||||||
|
/** Türkiye; `il` (DB biçimi, ör. "İZMİR") turuncu karelerle yanar. */
|
||||||
|
export function TurkiyePikselHaritasi({
|
||||||
|
il,
|
||||||
|
etiket,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
il: string;
|
||||||
|
etiket: string;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const html = markup(`tr:${il}`, () =>
|
||||||
|
markupUret(TUM_IL_HUCRELERI, IL_HUCRELERI[il] ?? [], null),
|
||||||
|
);
|
||||||
|
return <PikselSvg html={html} etiket={etiket} className={className} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ülke (KKTC, yurt dışı). Şehir konumu verilirse ülke mavi kalır ve şehirde
|
||||||
|
* 2×2 hücrelik iri turuncu kare durur; verilmezse ülkenin tamamı turuncu.
|
||||||
|
*/
|
||||||
|
export function UlkePikselHaritasi({
|
||||||
|
ulke,
|
||||||
|
sehir,
|
||||||
|
etiket,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
ulke: UlkeKodu;
|
||||||
|
/** viewBox koordinatı (harita-ulkeler'deki proje ile) */
|
||||||
|
sehir: { x: number; y: number } | null;
|
||||||
|
etiket: string;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const html = markup(
|
||||||
|
`ulke:${ulke}:${sehir ? `${Math.round(sehir.x)},${Math.round(sehir.y)}` : "-"}`,
|
||||||
|
() => {
|
||||||
|
const h = ULKE_HUCRELERI[ulke];
|
||||||
|
if (sehir) {
|
||||||
|
const q = Math.min(SUTUN - 2, Math.max(0, Math.round(sehir.x / CELL) - 1));
|
||||||
|
const r = Math.min(SATIR - 2, Math.max(0, Math.round(sehir.y / CELL) - 1));
|
||||||
|
return markupUret([...h.baglam, ...h.ulke], [], [q, r]);
|
||||||
|
}
|
||||||
|
return markupUret(h.baglam, h.ulke, null);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
return <PikselSvg html={html} etiket={etiket} className={className} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Şehir sayfası: il değerinden Türkiye ya da ülke haritasını seçer. */
|
||||||
|
export function SehirPikselHaritasi({
|
||||||
|
il,
|
||||||
|
ad,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
/** DB biçiminde il ("İSTANBUL", "LEFKOŞA", "KIBRIS") */
|
||||||
|
il: string;
|
||||||
|
/** Görünen ad (etiket için) */
|
||||||
|
ad: string;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
if (IL_HUCRELERI[il]) {
|
||||||
|
return (
|
||||||
|
<TurkiyePikselHaritasi
|
||||||
|
il={il}
|
||||||
|
etiket={`${ad} Türkiye haritasında`}
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const ulke = ilUlkeHaritasi(il);
|
||||||
|
if (!ulke) return null;
|
||||||
|
return (
|
||||||
|
<UlkePikselHaritasi
|
||||||
|
ulke={ulke.ulke}
|
||||||
|
sehir={ulke.sehir ? { x: ulke.sehir.x, y: ulke.sehir.y } : null}
|
||||||
|
etiket={`${ad} ${ulke.ulkeAd} haritasında`}
|
||||||
|
className={className}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
35
src/features/katalog/components/sehir-yuzeyi.tsx
Normal file
35
src/features/katalog/components/sehir-yuzeyi.tsx
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRef, type ReactNode } from "react";
|
||||||
|
import { PikselZemin } from "@/components/piksel-zemin";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Şehir sayfasının başlık yüzeyi (DESIGN.md §4.17): beyaz kart, slate
|
||||||
|
* piksel doku, sağda (lg) ya da altta (telefon) şehrin dot-matrix haritası.
|
||||||
|
* Doku ile harita üst üste binmez: harita kabı `haric` ile zemine verilir
|
||||||
|
* (§10.8 kuralı). Yalnız ref için client; metin ve harita sunucudan gelir.
|
||||||
|
*/
|
||||||
|
export function SehirYuzeyi({
|
||||||
|
metin,
|
||||||
|
harita,
|
||||||
|
seed = 31,
|
||||||
|
}: {
|
||||||
|
metin: ReactNode;
|
||||||
|
harita: ReactNode;
|
||||||
|
seed?: number;
|
||||||
|
}) {
|
||||||
|
const haritaRef = useRef<HTMLDivElement>(null);
|
||||||
|
return (
|
||||||
|
<section className="relative isolate mt-6 overflow-hidden rounded-3xl border border-slate-200 bg-white p-6 sm:p-8">
|
||||||
|
<PikselZemin ton="acik" seed={seed} haric={haritaRef} />
|
||||||
|
<div className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_minmax(0,24rem)] lg:items-center lg:gap-10">
|
||||||
|
<div>{metin}</div>
|
||||||
|
{harita ? (
|
||||||
|
<div ref={haritaRef} className="mx-auto w-full max-w-md lg:max-w-none">
|
||||||
|
{harita}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ import {
|
|||||||
getUniversiteBySlug,
|
getUniversiteBySlug,
|
||||||
bolumSlugFromIsim,
|
bolumSlugFromIsim,
|
||||||
} from "../katalog-queries";
|
} from "../katalog-queries";
|
||||||
import { trBaslikDuzeni } from "@/lib/slug";
|
import { ilSayfaHref, trBaslikDuzeni } from "@/lib/slug";
|
||||||
import { UNITUR_ETIKET, uniturDevletMi } from "@/lib/unitur";
|
import { UNITUR_ETIKET, uniturDevletMi } from "@/lib/unitur";
|
||||||
import {
|
import {
|
||||||
JsonLd,
|
JsonLd,
|
||||||
@@ -579,10 +579,16 @@ export function UniversiteIcerik({
|
|||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
{uni.il ? (
|
{uni.il ? (
|
||||||
<span className="inline-flex h-10 min-w-0 items-center gap-2 rounded-full border border-slate-200 bg-white px-4 text-sm font-semibold text-slate-800">
|
// İl hapı şehir sayfasına gider (Bilal, 24 Eyl 2026: "şehirlerin
|
||||||
|
// geçtiği her yer tıklanabilir"); hover §2.1 tek turuncu.
|
||||||
|
<Link
|
||||||
|
href={ilSayfaHref(uni.il)}
|
||||||
|
title={`${trBaslikDuzeni(uni.il)} üniversiteleri`}
|
||||||
|
className="inline-flex h-10 min-w-0 items-center gap-2 rounded-full border border-slate-200 bg-white px-4 text-sm font-semibold text-slate-800 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]"
|
||||||
|
>
|
||||||
<MapPin className="size-4 shrink-0 text-primary" aria-hidden />
|
<MapPin className="size-4 shrink-0 text-primary" aria-hidden />
|
||||||
<span className="truncate">{trBaslikDuzeni(uni.il)}</span>
|
<span className="truncate">{trBaslikDuzeni(uni.il)}</span>
|
||||||
</span>
|
</Link>
|
||||||
) : null}
|
) : null}
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -1,20 +1,20 @@
|
|||||||
|
import { normalizeIlAdi } from "@/lib/harita";
|
||||||
|
import { uniUlkeKonum, yurtdisiUniKonum } from "@/lib/harita-ulkeler";
|
||||||
import { cities } from "turkey-map-react/lib/data";
|
import { cities } from "turkey-map-react/lib/data";
|
||||||
import {
|
import {
|
||||||
HARITA_GENISLIK,
|
TurkiyePikselHaritasi,
|
||||||
HARITA_UST_KIRPMA,
|
UlkePikselHaritasi,
|
||||||
HARITA_YUKSEKLIK,
|
} from "./sehir-piksel-haritasi";
|
||||||
normalizeIlAdi,
|
|
||||||
} from "@/lib/harita";
|
|
||||||
import {
|
|
||||||
ULKE_HARITA_GENISLIK,
|
|
||||||
ULKE_HARITA_YUKSEKLIK,
|
|
||||||
uniUlkeKonum,
|
|
||||||
type UlkeKonum,
|
|
||||||
} from "@/lib/harita-ulkeler";
|
|
||||||
|
|
||||||
const SECILI_DOLGU = "oklch(0.809 0.105 251.8)";
|
// Harita il adları modül yükünde bir kez DB biçimine çevrilir.
|
||||||
const ZEMIN_DOLGU = "oklch(0.929 0.013 255.5)";
|
const HARITA_ILLERI = new Set(cities.map((c) => normalizeIlAdi(c.name)));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Üniversite sayfasındaki konum kartı: şehir sayfasıyla aynı dot-matrix
|
||||||
|
* harita (DESIGN.md §4.17; Bilal, 24 Eyl 2026: "bu kart kısmına piksel
|
||||||
|
* dokuları ekle"). Türkiye'de il turuncu noktalarla yanar; KKTC/yurt dışında
|
||||||
|
* ülke kontürü + kampüs şehrinde iri turuncu piksel.
|
||||||
|
*/
|
||||||
export function UniversiteKonumHaritasi({
|
export function UniversiteKonumHaritasi({
|
||||||
universite,
|
universite,
|
||||||
il,
|
il,
|
||||||
@@ -24,9 +24,15 @@ export function UniversiteKonumHaritasi({
|
|||||||
}) {
|
}) {
|
||||||
const yurtdisi = uniUlkeKonum(universite, il);
|
const yurtdisi = uniUlkeKonum(universite, il);
|
||||||
if (yurtdisi) {
|
if (yurtdisi) {
|
||||||
|
const konum = yurtdisiUniKonum(universite, il);
|
||||||
return (
|
return (
|
||||||
<KonumBolumu universite={universite}>
|
<KonumBolumu universite={universite}>
|
||||||
<UlkeHaritasi universite={universite} konum={yurtdisi} />
|
<UlkePikselHaritasi
|
||||||
|
ulke={yurtdisi.ulke}
|
||||||
|
sehir={konum ? { x: konum.x, y: konum.y } : null}
|
||||||
|
etiket={`${universite} konumunun ${yurtdisi.ulkeAd} haritasındaki görünümü${yurtdisi.sehir ? ` (${yurtdisi.sehir})` : ""}`}
|
||||||
|
className="absolute inset-0 h-full w-full"
|
||||||
|
/>
|
||||||
<p className="pointer-events-none absolute bottom-0 left-0 text-xs font-medium text-slate-500">
|
<p className="pointer-events-none absolute bottom-0 left-0 text-xs font-medium text-slate-500">
|
||||||
{yurtdisi.ulkeAd}
|
{yurtdisi.ulkeAd}
|
||||||
{yurtdisi.sehir ? ` · ${yurtdisi.sehir}` : ""}
|
{yurtdisi.sehir ? ` · ${yurtdisi.sehir}` : ""}
|
||||||
@@ -35,31 +41,15 @@ export function UniversiteKonumHaritasi({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!il) return null;
|
if (!il || !HARITA_ILLERI.has(il)) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<KonumBolumu universite={universite}>
|
<KonumBolumu universite={universite}>
|
||||||
<svg
|
<TurkiyePikselHaritasi
|
||||||
viewBox={`0 ${HARITA_UST_KIRPMA} ${HARITA_GENISLIK} ${HARITA_YUKSEKLIK - HARITA_UST_KIRPMA}`}
|
il={il}
|
||||||
|
etiket={`${universite} konumunun Türkiye haritasındaki görünümü`}
|
||||||
className="absolute inset-0 h-full w-full"
|
className="absolute inset-0 h-full w-full"
|
||||||
role="img"
|
/>
|
||||||
aria-label={`${universite} konumunun Türkiye haritasındaki görünümü`}
|
|
||||||
>
|
|
||||||
{cities.map((city) => {
|
|
||||||
const secili = il ? normalizeIlAdi(city.name) === il : false;
|
|
||||||
return (
|
|
||||||
<path
|
|
||||||
key={city.id}
|
|
||||||
d={city.path}
|
|
||||||
fill={secili ? SECILI_DOLGU : ZEMIN_DOLGU}
|
|
||||||
stroke="white"
|
|
||||||
strokeWidth={secili ? 1.8 : 1}
|
|
||||||
>
|
|
||||||
<title>{city.name}</title>
|
|
||||||
</path>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</svg>
|
|
||||||
</KonumBolumu>
|
</KonumBolumu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -83,30 +73,3 @@ function KonumBolumu({
|
|||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function UlkeHaritasi({
|
|
||||||
universite,
|
|
||||||
konum,
|
|
||||||
}: {
|
|
||||||
universite: string;
|
|
||||||
konum: UlkeKonum;
|
|
||||||
}) {
|
|
||||||
const { harita, ulkeAd, sehir } = konum;
|
|
||||||
return (
|
|
||||||
<svg
|
|
||||||
viewBox={`0 0 ${ULKE_HARITA_GENISLIK} ${ULKE_HARITA_YUKSEKLIK}`}
|
|
||||||
className="absolute inset-0 h-full w-full"
|
|
||||||
role="img"
|
|
||||||
aria-label={`${universite} konumunun ${ulkeAd} haritasındaki görünümü${sehir ? ` (${sehir})` : ""}`}
|
|
||||||
>
|
|
||||||
{harita.baglam.map((d, i) => (
|
|
||||||
<path key={`b${i}`} d={d} fill={ZEMIN_DOLGU} stroke="white" strokeWidth={1} />
|
|
||||||
))}
|
|
||||||
{harita.ulke.map((d, i) => (
|
|
||||||
<path key={`u${i}`} d={d} fill={SECILI_DOLGU} stroke="white" strokeWidth={1.8}>
|
|
||||||
<title>{ulkeAd}</title>
|
|
||||||
</path>
|
|
||||||
))}
|
|
||||||
</svg>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getAllUniversiteler, type UniOzet } from "../katalog-queries";
|
import {
|
||||||
import { trBaslikDuzeni } from "@/lib/slug";
|
getAllIller,
|
||||||
|
getAllUniversiteler,
|
||||||
|
type UniOzet,
|
||||||
|
} from "../katalog-queries";
|
||||||
|
import { ilSayfaHref, trBaslikDuzeni } from "@/lib/slug";
|
||||||
|
import { SehirCipleri, oneCikanIller } from "./sehir-cipleri";
|
||||||
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
|
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
|
||||||
import { PagePixelDivider } from "@/components/pixel-decor";
|
import { PagePixelDivider } from "@/components/pixel-decor";
|
||||||
import { UniLogo } from "@/components/uni-logo";
|
import { UniLogo } from "@/components/uni-logo";
|
||||||
@@ -9,26 +14,42 @@ import { SON_YIL, TERCIH_YILI } from "@/lib/veri-yillari";
|
|||||||
|
|
||||||
const sayi = (n: number) => n.toLocaleString("tr-TR");
|
const sayi = (n: number) => n.toLocaleString("tr-TR");
|
||||||
|
|
||||||
function UniLinkleri({ uniler }: { uniler: UniOzet[] }) {
|
export function UniLinkleri({ uniler }: { uniler: UniOzet[] }) {
|
||||||
|
// Ad ve il ayrı linkler (iç içe <a> olmaz): logo + ad üniversiteye, alt
|
||||||
|
// satırdaki şehir adı şehir sayfasına (Bilal, 24 Eyl 2026: "şehirlerin
|
||||||
|
// geçtiği her yer tıklanabilir"). Hover §2.1 tek turuncu.
|
||||||
return (
|
return (
|
||||||
<ul className="mt-4 grid gap-x-6 gap-y-2 sm:grid-cols-2">
|
<ul className="mt-4 grid gap-x-6 gap-y-2 sm:grid-cols-2">
|
||||||
{uniler.map((u) => (
|
{uniler.map((u) => (
|
||||||
<li key={u.slug}>
|
<li key={u.slug} className="grid grid-cols-[auto_minmax(0,1fr)] items-center gap-x-3 text-sm">
|
||||||
<Link
|
<Link
|
||||||
href={`/universite/${u.slug}`}
|
href={`/universite/${u.slug}`}
|
||||||
className="group flex items-center gap-3 text-sm text-slate-700 transition-colors hover:text-orange-500"
|
tabIndex={-1}
|
||||||
|
aria-hidden
|
||||||
|
className="row-span-2"
|
||||||
>
|
>
|
||||||
<UniLogo slug={u.slug} ad={u.ad} boy="sm" />
|
<UniLogo slug={u.slug} ad={u.ad} boy="sm" />
|
||||||
<span className="min-w-0">
|
</Link>
|
||||||
<span className="block truncate group-hover:underline">
|
<Link
|
||||||
|
href={`/universite/${u.slug}`}
|
||||||
|
className="block truncate text-slate-700 transition-colors hover:text-orange-500 hover:underline"
|
||||||
|
>
|
||||||
{u.ad}
|
{u.ad}
|
||||||
</span>
|
</Link>
|
||||||
<span className="block text-xs text-slate-400">
|
<span className="block text-xs text-slate-400">
|
||||||
{u.il ? `${trBaslikDuzeni(u.il)} · ` : ""}
|
{u.il ? (
|
||||||
|
<>
|
||||||
|
<Link
|
||||||
|
href={ilSayfaHref(u.il)}
|
||||||
|
className="transition-colors hover:text-orange-500 hover:underline"
|
||||||
|
>
|
||||||
|
{trBaslikDuzeni(u.il)}
|
||||||
|
</Link>
|
||||||
|
{" · "}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
{sayi(u.programSayisi)} program
|
{sayi(u.programSayisi)} program
|
||||||
</span>
|
</span>
|
||||||
</span>
|
|
||||||
</Link>
|
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
@@ -47,6 +68,10 @@ export function UniversiteKatalogu() {
|
|||||||
else digerUniler.push(u);
|
else digerUniler.push(u);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const iller = getAllIller();
|
||||||
|
const oneCikan = oneCikanIller(iller);
|
||||||
|
const illerAz = [...iller].sort((a, b) => a.ad.localeCompare(b.ad, "tr-TR"));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p className="mt-4 max-w-3xl text-sm leading-7 text-slate-600">
|
<p className="mt-4 max-w-3xl text-sm leading-7 text-slate-600">
|
||||||
@@ -57,6 +82,27 @@ export function UniversiteKatalogu() {
|
|||||||
için rehber niteliğindedir.
|
için rehber niteliğindedir.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{/* Şehre göre gezinme: sık bakılan (2+ üniversiteli) şehirler çip
|
||||||
|
olarak önde, tam A–Z liste sayfanın altında (DESIGN.md §1.1 "sık
|
||||||
|
bakılan önde, ayrıntı bir dokunuş derinde"). */}
|
||||||
|
<section className="mt-8" aria-labelledby="sehre-gore">
|
||||||
|
<h2
|
||||||
|
id="sehre-gore"
|
||||||
|
className="font-heading text-2xl font-bold text-slate-900"
|
||||||
|
>
|
||||||
|
Şehre göre
|
||||||
|
<span className="ml-2 text-sm font-normal text-slate-400">
|
||||||
|
{sayi(iller.length)} şehir
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<SehirCipleri
|
||||||
|
iller={oneCikan}
|
||||||
|
tumuHref="#tum-sehirler"
|
||||||
|
tumuEtiket={`Tüm şehirler (${sayi(iller.length)})`}
|
||||||
|
className="mt-4 justify-start"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
<PagePixelDivider seed={63} className="mt-8" />
|
<PagePixelDivider seed={63} className="mt-8" />
|
||||||
|
|
||||||
<section className="mt-8">
|
<section className="mt-8">
|
||||||
@@ -91,8 +137,32 @@ export function UniversiteKatalogu() {
|
|||||||
</section>
|
</section>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
<section id="tum-sehirler" className="mt-10 scroll-mt-24">
|
||||||
|
<h2 className="font-heading text-2xl font-bold text-slate-900">
|
||||||
|
Tüm şehirler A–Z
|
||||||
|
<span className="ml-2 text-sm font-normal text-slate-400">
|
||||||
|
{sayi(iller.length)}
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<ul className="mt-4 grid gap-x-6 gap-y-2 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{illerAz.map((il) => (
|
||||||
|
<li key={il.slug}>
|
||||||
|
<Link
|
||||||
|
href={`/universiteler/${il.slug}`}
|
||||||
|
className="group inline-flex items-baseline gap-2 text-sm text-slate-700 transition-colors hover:text-orange-500"
|
||||||
|
>
|
||||||
|
<span className="group-hover:underline">{il.ad}</span>
|
||||||
|
<span className="text-xs text-slate-400">
|
||||||
|
{sayi(il.uniler.length)} üniversite
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
|
||||||
<div className="mt-12">
|
<div className="mt-12">
|
||||||
<CtaSiraForm />
|
<CtaSiraForm ton="beyaz" />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,10 +8,14 @@ export {
|
|||||||
getAllBolumSlugs,
|
getAllBolumSlugs,
|
||||||
getAllUniversiteler,
|
getAllUniversiteler,
|
||||||
getAllUniversiteSlugs,
|
getAllUniversiteSlugs,
|
||||||
|
getAllIller,
|
||||||
|
getAllIlSlugs,
|
||||||
getBolumBySlug,
|
getBolumBySlug,
|
||||||
|
getIlBySlug,
|
||||||
getUniversiteBySlug,
|
getUniversiteBySlug,
|
||||||
uniSlugFromAd,
|
uniSlugFromAd,
|
||||||
bolumSlugFromIsim,
|
bolumSlugFromIsim,
|
||||||
type BolumOzet,
|
type BolumOzet,
|
||||||
|
type IlOzet,
|
||||||
type UniOzet,
|
type UniOzet,
|
||||||
} from "@/lib/katalog";
|
} from "@/lib/katalog";
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ import type {
|
|||||||
RankResultsListe,
|
RankResultsListe,
|
||||||
} from "@/types/yokatlas";
|
} from "@/types/yokatlas";
|
||||||
import { DILIM_ETIKET, riskHesapla } from "@/lib/risk";
|
import { DILIM_ETIKET, riskHesapla } from "@/lib/risk";
|
||||||
import { bolumSayfaSlug, trBaslikDuzeni, uniSayfaSlug } from "@/lib/slug";
|
import { bolumSayfaSlug, ilSayfaHref, trBaslikDuzeni, uniSayfaSlug } from "@/lib/slug";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { VERI_YIL_SAYISI } from "@/lib/veri-yillari";
|
import { VERI_YIL_SAYISI } from "@/lib/veri-yillari";
|
||||||
import {
|
import {
|
||||||
ProgramSiraGecmisi,
|
ProgramSiraGecmisi,
|
||||||
@@ -374,7 +375,14 @@ function ProgramSatiri({
|
|||||||
<span className={SATIR_META}>
|
<span className={SATIR_META}>
|
||||||
<span className={SATIR_CIP}>{devlet ? "Devlet" : "Vakıf"}</span>
|
<span className={SATIR_CIP}>{devlet ? "Devlet" : "Vakıf"}</span>
|
||||||
{p.il ? (
|
{p.il ? (
|
||||||
<span className="shrink-0">{trBaslikDuzeni(p.il)}</span>
|
<Link
|
||||||
|
href={ilSayfaHref(p.il)}
|
||||||
|
className={cn("shrink-0", SATIR_IKINCIL_LINK)}
|
||||||
|
title={`${trBaslikDuzeni(p.il)} üniversiteleri`}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{trBaslikDuzeni(p.il)}
|
||||||
|
</Link>
|
||||||
) : null}
|
) : null}
|
||||||
{p.sure ? (
|
{p.sure ? (
|
||||||
<span className="shrink-0">{p.sure} yıl</span>
|
<span className="shrink-0">{p.sure} yıl</span>
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ArrowRight } from "lucide-react";
|
import { ArrowRight } from "lucide-react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { CtaBandi } from "@/components/cta-bandi";
|
||||||
import { useTercihProfili } from "@/features/liste/hooks/use-tercih-profili";
|
import { useTercihProfili } from "@/features/liste/hooks/use-tercih-profili";
|
||||||
import { PUAN_TURU_ETIKET, sonucHref } from "@/features/sihirbaz/sihirbaz-sabitler";
|
import { PUAN_TURU_ETIKET, sonucHref } from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||||
import type { DilimKey, SihirbazFacetleri } from "@/types/yokatlas";
|
import type { DilimKey, SihirbazFacetleri } from "@/types/yokatlas";
|
||||||
@@ -233,20 +235,26 @@ export function DilimSeridiCanli() {
|
|||||||
export function DemoKapanisCta() {
|
export function DemoKapanisCta() {
|
||||||
const profil = useTercihProfili();
|
const profil = useTercihProfili();
|
||||||
|
|
||||||
|
// Sayfa kapanışı bir CTA yüzeyidir: mavi bant + piksel doku (DESIGN.md
|
||||||
|
// §10.8; Bilal, 24 Eyl 2026: "burda piksel dokuları yok"). Tek başlık, tek
|
||||||
|
// turuncu buton; sayfadaki diğer bantlardan farklı seed.
|
||||||
return (
|
return (
|
||||||
<div className="rounded-2xl border border-slate-200 bg-white p-6 text-center">
|
<CtaBandi etiket="div" seed={29} ton="beyaz">
|
||||||
<p className="font-heading text-base font-bold text-slate-900">
|
<p className="font-heading text-2xl font-bold text-balance sm:text-3xl">
|
||||||
{profil
|
{profil
|
||||||
? "Kuralları öğrendin — sayılar zaten senin sıralamanla hesaplandı."
|
? "Kuralları öğrendin — sayılar zaten senin sıralamanla hesaplandı."
|
||||||
: "Kuralları öğrendin — şimdi kendi sıralamanla dene."}
|
: "Kuralları öğrendin — şimdi kendi sıralamanla dene."}
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Button
|
||||||
href={profil ? sonucHref() : "/"}
|
asChild
|
||||||
className="mt-3 inline-flex items-center gap-1.5 rounded-full bg-orange-500 px-5 py-2.5 text-sm font-medium text-white transition-colors duration-200 hover:bg-orange-600"
|
size="lg"
|
||||||
|
className="mt-6 h-12 w-full cursor-pointer bg-orange-500 px-6 text-white transition-[background-color,transform] duration-200 hover:bg-orange-600 active:scale-[0.97] sm:w-auto"
|
||||||
>
|
>
|
||||||
|
<Link href={profil ? sonucHref() : "/"}>
|
||||||
{profil ? "Sıralamana uygun programları gör" : "Sıralamanı gir"}
|
{profil ? "Sıralamana uygun programları gör" : "Sıralamanı gir"}
|
||||||
<ArrowRight className="size-4" aria-hidden />
|
<ArrowRight className="size-4" aria-hidden />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</Button>
|
||||||
|
</CtaBandi>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,8 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { bolumSayfaSlug, trBaslikDuzeni, uniSayfaSlug } from "@/lib/slug";
|
import { bolumSayfaSlug, ilSayfaHref, trBaslikDuzeni, uniSayfaSlug } from "@/lib/slug";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { UNITUR_ETIKET } from "@/lib/unitur";
|
import { UNITUR_ETIKET } from "@/lib/unitur";
|
||||||
import { RiskEtiketi } from "@/components/risk-etiketi";
|
import { RiskEtiketi } from "@/components/risk-etiketi";
|
||||||
import {
|
import {
|
||||||
@@ -266,7 +267,14 @@ export function RaporListesi({
|
|||||||
<span className={SATIR_CIP}>{devlet ? "Devlet" : "Vakıf"}</span>
|
<span className={SATIR_CIP}>{devlet ? "Devlet" : "Vakıf"}</span>
|
||||||
) : null}
|
) : null}
|
||||||
{p?.il ? (
|
{p?.il ? (
|
||||||
<span className="shrink-0">{trBaslikDuzeni(p.il)}</span>
|
<Link
|
||||||
|
href={ilSayfaHref(p.il)}
|
||||||
|
className={cn("shrink-0", SATIR_IKINCIL_LINK)}
|
||||||
|
title={`${trBaslikDuzeni(p.il)} üniversiteleri`}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{trBaslikDuzeni(p.il)}
|
||||||
|
</Link>
|
||||||
) : null}
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -24,12 +24,14 @@ import {
|
|||||||
import { useGorunumOlayi } from "@/components/use-gorunum-olayi";
|
import { useGorunumOlayi } from "@/components/use-gorunum-olayi";
|
||||||
import { olay, siraKovasi } from "@/lib/analitik";
|
import { olay, siraKovasi } from "@/lib/analitik";
|
||||||
import { dilimdenRisk, riskHesapla } from "@/lib/risk";
|
import { dilimdenRisk, riskHesapla } from "@/lib/risk";
|
||||||
import { trBaslikDuzeni, turkishSlugify } from "@/lib/slug";
|
import { ilSayfaHref, trBaslikDuzeni, turkishSlugify } from "@/lib/slug";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import {
|
import {
|
||||||
SATIR_ALT_AD,
|
SATIR_ALT_AD,
|
||||||
SATIR_AD_DUZ,
|
SATIR_AD_DUZ,
|
||||||
SATIR_CIP,
|
SATIR_CIP,
|
||||||
SATIR_ALT_GRUP,
|
SATIR_ALT_GRUP,
|
||||||
|
SATIR_IKINCIL_LINK,
|
||||||
SATIR_META,
|
SATIR_META,
|
||||||
} from "@/components/liste-satiri";
|
} from "@/components/liste-satiri";
|
||||||
import { listemUretHref, PROFIL_DEGISTI_EVENT } from "@/features/sihirbaz/sihirbaz-sabitler";
|
import { listemUretHref, PROFIL_DEGISTI_EVENT } from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||||
@@ -166,7 +168,13 @@ export function TadimlikSatiri({
|
|||||||
<span className={SATIR_CIP}>{devlet ? "Devlet" : "Vakıf"}</span>
|
<span className={SATIR_CIP}>{devlet ? "Devlet" : "Vakıf"}</span>
|
||||||
) : null}
|
) : null}
|
||||||
{p.il ? (
|
{p.il ? (
|
||||||
<span className="shrink-0">{trBaslikDuzeni(p.il)}</span>
|
<Link
|
||||||
|
href={ilSayfaHref(p.il)}
|
||||||
|
className={cn("shrink-0", SATIR_IKINCIL_LINK)}
|
||||||
|
title={`${trBaslikDuzeni(p.il)} üniversiteleri`}
|
||||||
|
>
|
||||||
|
{trBaslikDuzeni(p.il)}
|
||||||
|
</Link>
|
||||||
) : null}
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
DrawerDescription,
|
DrawerDescription,
|
||||||
DrawerTitle,
|
DrawerTitle,
|
||||||
} from "@/components/ui/drawer";
|
} from "@/components/ui/drawer";
|
||||||
import { PagePixelDivider } from "@/components/pixel-decor";
|
import { PikselZemin } from "@/components/piksel-zemin";
|
||||||
import { useGorunumOlayi } from "@/components/use-gorunum-olayi";
|
import { useGorunumOlayi } from "@/components/use-gorunum-olayi";
|
||||||
import {
|
import {
|
||||||
SihirbazAdimlarLazy,
|
SihirbazAdimlarLazy,
|
||||||
@@ -39,7 +39,7 @@ const PUAN_TURLERI_SECENEK = [
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// Kayıtlı profilli CTA'lar /listem boş durumunun dilini taşır (bkz.
|
// Kayıtlı profilli CTA'lar /listem boş durumunun dilini taşır (bkz.
|
||||||
// listem-bos-cta.tsx): büyük turuncu buton, piksel ayraç. Sınıf seti tek yerde.
|
// listem-bos-cta.tsx): büyük turuncu buton. Sınıf seti tek yerde.
|
||||||
const PROFILLI_CTA_SINIF =
|
const PROFILLI_CTA_SINIF =
|
||||||
"h-12 cursor-pointer bg-orange-500 px-6 text-white transition-colors duration-200 hover:bg-orange-600";
|
"h-12 cursor-pointer bg-orange-500 px-6 text-white transition-colors duration-200 hover:bg-orange-600";
|
||||||
|
|
||||||
@@ -101,6 +101,9 @@ function KayitliSira({
|
|||||||
* tabloya bağlayan köprü cümlesini buradan verir. Başlık ve açıklamalar
|
* tabloya bağlayan köprü cümlesini buradan verir. Başlık ve açıklamalar
|
||||||
* yerleşme vaadi içermez: ürün sıralamaya yakın programları listeler.
|
* yerleşme vaadi içermez: ürün sıralamaya yakın programları listeler.
|
||||||
*
|
*
|
||||||
|
* `ton="beyaz"`: kart varyantının beyaz zeminli bandı (aynı sayfada iki mavi
|
||||||
|
* bant üst üste gelmesin diye; DESIGN.md §10.8).
|
||||||
|
*
|
||||||
* `varyant="kompakt"`: katalog sayfalarında başlığın hemen altına, tablonun
|
* `varyant="kompakt"`: katalog sayfalarında başlığın hemen altına, tablonun
|
||||||
* ÜSTÜNE konan tek satırlık sürüm. Sıralama alanı çekmecede değil doğrudan
|
* ÜSTÜNE konan tek satırlık sürüm. Sıralama alanı çekmecede değil doğrudan
|
||||||
* sayfadadır (tek sayfada çıkan ziyaretçi alttaki karta hiç inmiyor); gönderince
|
* sayfadadır (tek sayfada çıkan ziyaretçi alttaki karta hiç inmiyor); gönderince
|
||||||
@@ -113,10 +116,13 @@ export function CtaSiraForm({
|
|||||||
aciklama,
|
aciklama,
|
||||||
varyant = "kart",
|
varyant = "kart",
|
||||||
konum = "alt",
|
konum = "alt",
|
||||||
|
ton = "mavi",
|
||||||
}: {
|
}: {
|
||||||
baslik?: string;
|
baslik?: string;
|
||||||
aciklama?: string;
|
aciklama?: string;
|
||||||
varyant?: "kart" | "kompakt";
|
varyant?: "kart" | "kompakt";
|
||||||
|
/** Yalnız kart varyantı: bandın zemini */
|
||||||
|
ton?: "mavi" | "beyaz";
|
||||||
/** Yalnız ölçüm içindir: formun sayfadaki yeri (tablonun üstü / altı). */
|
/** Yalnız ölçüm içindir: formun sayfadaki yeri (tablonun üstü / altı). */
|
||||||
konum?: "ust" | "alt";
|
konum?: "ust" | "alt";
|
||||||
}) {
|
}) {
|
||||||
@@ -369,14 +375,21 @@ export function CtaSiraForm({
|
|||||||
// bindirilir; hücrenin yüksekliğini her iki durumda da başlık + form belirler.
|
// bindirilir; hücrenin yüksekliğini her iki durumda da başlık + form belirler.
|
||||||
// Profilli içerik bu yüksekliği aşmayacak kadar kısa tutulmalı.
|
// Profilli içerik bu yüksekliği aşmayacak kadar kısa tutulmalı.
|
||||||
const kompaktBaslik = baslik ?? "Sıralaman bu tabanların neresinde?";
|
const kompaktBaslik = baslik ?? "Sıralaman bu tabanların neresinde?";
|
||||||
|
// Profilli hâl bir CTA yüzeyidir (tek iş: turuncu buton): CtaBandi'nin
|
||||||
|
// kompakt bandıyla birebir aynı kabuk — mavi zemin, beyaz yazı, arkada
|
||||||
|
// PikselZemin (DESIGN.md §10.8; piksel yalnız CTA yüzeylerinde). Profilsiz
|
||||||
|
// form satırı beyaz içerik kartıdır, piksel almaz. İç boşluk ve 1 px
|
||||||
|
// kenarlık (profilli hâlde saydam) iki hâlde aynıdır ki hydration sonrası
|
||||||
|
// geçişte satır yüksekliği oynamasın.
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
aria-labelledby={baslikId}
|
aria-labelledby={baslikId}
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-2xl border p-4 transition-colors duration-200 sm:p-5",
|
"relative isolate overflow-hidden rounded-2xl border px-5 py-4 text-left transition-colors duration-200 sm:py-5",
|
||||||
profil ? "border-primary/20 bg-primary/5" : "border-slate-200 bg-white",
|
profil ? "border-transparent bg-primary text-white" : "border-slate-200 bg-white",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
{profil ? <PikselZemin seed={11} /> : null}
|
||||||
<div className="grid">
|
<div className="grid">
|
||||||
<div
|
<div
|
||||||
ref={profil ? undefined : formRef}
|
ref={profil ? undefined : formRef}
|
||||||
@@ -399,13 +412,12 @@ export function CtaSiraForm({
|
|||||||
<div className="flex min-w-0 flex-col items-center gap-1 sm:items-start">
|
<div className="flex min-w-0 flex-col items-center gap-1 sm:items-start">
|
||||||
<p
|
<p
|
||||||
id={baslikId}
|
id={baslikId}
|
||||||
className="font-heading text-base font-bold leading-6 text-slate-900"
|
className="font-heading text-base font-bold leading-6 text-white"
|
||||||
>
|
>
|
||||||
{kompaktBaslik}
|
{kompaktBaslik}
|
||||||
</p>
|
</p>
|
||||||
<KayitliSira sira={profil.sira} tur={profil.tur} />
|
<KayitliSira sira={profil.sira} tur={profil.tur} beyaz />
|
||||||
</div>
|
</div>
|
||||||
<PagePixelDivider seed={67} className="w-24 sm:hidden" />
|
|
||||||
<Button asChild size="lg" className={cn(PROFILLI_CTA_SINIF, "w-full sm:w-auto")}>
|
<Button asChild size="lg" className={cn(PROFILLI_CTA_SINIF, "w-full sm:w-auto")}>
|
||||||
<Link href={sonucHref()}>
|
<Link href={sonucHref()}>
|
||||||
Sıralamana uygun programları gör
|
Sıralamana uygun programları gör
|
||||||
@@ -421,16 +433,18 @@ export function CtaSiraForm({
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Kart varyantı sayfa sonundaki CTA bandıdır (DESIGN.md §10.8, CtaBandi).
|
// Kart varyantı sayfa sonundaki CTA bandıdır (DESIGN.md §10.8, CtaBandi).
|
||||||
|
const beyaz = ton === "beyaz";
|
||||||
|
const paragrafSinifi = beyaz ? "text-slate-600" : "text-white/90";
|
||||||
if (profil) {
|
if (profil) {
|
||||||
return (
|
return (
|
||||||
<CtaBandi seed={5}>
|
<CtaBandi seed={5} ton={ton}>
|
||||||
<h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl">
|
<h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl">
|
||||||
{baslik ?? "Sıralamana yakın programlar hangileri? Saniyede gör."}
|
{baslik ?? "Sıralamana yakın programlar hangileri? Saniyede gör."}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<KayitliSira sira={profil.sira} tur={profil.tur} beyaz />
|
<KayitliSira sira={profil.sira} tur={profil.tur} beyaz={!beyaz} />
|
||||||
</div>
|
</div>
|
||||||
<p className="mx-auto mt-2 max-w-xl text-base leading-relaxed text-white/90">
|
<p className={cn("mx-auto mt-2 max-w-xl text-base leading-relaxed", paragrafSinifi)}>
|
||||||
Gerçek YÖK Atlas verisiyle hayal, dengeli ve güvenli dilimlerindeki
|
Gerçek YÖK Atlas verisiyle hayal, dengeli ve güvenli dilimlerindeki
|
||||||
programların hazır.
|
programların hazır.
|
||||||
</p>
|
</p>
|
||||||
@@ -450,11 +464,11 @@ export function CtaSiraForm({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={formRef}>
|
<div ref={formRef}>
|
||||||
<CtaBandi seed={9}>
|
<CtaBandi seed={9} ton={ton}>
|
||||||
<h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl">
|
<h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl">
|
||||||
{baslik ?? "Sıralamana yakın programlar hangileri? Saniyede gör."}
|
{baslik ?? "Sıralamana yakın programlar hangileri? Saniyede gör."}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mx-auto mt-3 max-w-xl text-base leading-relaxed text-white/90">
|
<p className={cn("mx-auto mt-3 max-w-xl text-base leading-relaxed", paragrafSinifi)}>
|
||||||
{aciklama ??
|
{aciklama ??
|
||||||
"YKS başarı sıralamanı gir; gerçek YÖK Atlas verisiyle sıralamana uygun hayal, dengeli ve güvenli dilimlerindeki programları anında listele. Ücretsiz, üyelik gerekmez."}
|
"YKS başarı sıralamanı gir; gerçek YÖK Atlas verisiyle sıralamana uygun hayal, dengeli ve güvenli dilimlerindeki programları anında listele. Ücretsiz, üyelik gerekmez."}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -262,22 +262,28 @@ export function SihirbazBolumu({
|
|||||||
edilmez; panel yalnızca funnel CTA'larını gösterir, üretim ve
|
edilmez; panel yalnızca funnel CTA'larını gösterir, üretim ve
|
||||||
listenin kendisi /listem'de yaşar. */}
|
listenin kendisi /listem'de yaşar. */}
|
||||||
{raporVar ? (
|
{raporVar ? (
|
||||||
<section className="mt-16 rounded-2xl border border-slate-200 bg-white p-8 text-center sm:p-12">
|
// Kardeş panellerle aynı yüzey (DESIGN.md §10.8 CtaBandi): mavi zemin,
|
||||||
<h2 className="font-heading text-2xl font-bold sm:text-3xl">
|
// piksel doku, tek turuncu buton (Bilal, 24 Eyl 2026: "buraya piksel
|
||||||
|
// doku ekle"). Seed diğer iki panelden (13, 19) farklı.
|
||||||
|
<CtaBandi seed={23} className="mt-16 min-w-0">
|
||||||
|
<h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl">
|
||||||
24 tercihlik listen hazır
|
24 tercihlik listen hazır
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mx-auto mt-3 max-w-xl text-slate-600">
|
<p className="mx-auto mt-3 max-w-xl text-base leading-relaxed text-white/90">
|
||||||
Yapay zekânın kurduğu risk analizli listen Listem sayfasında
|
Yapay zekânın kurduğu risk analizli listen Listem sayfasında
|
||||||
seni bekliyor.
|
seni bekliyor.
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
size="lg"
|
size="lg"
|
||||||
className="mt-6 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
className="mt-6 h-12 w-full cursor-pointer bg-orange-500 px-6 text-white transition-[background-color,transform] duration-200 hover:bg-orange-600 active:scale-[0.97] sm:w-auto"
|
||||||
>
|
>
|
||||||
<Link href="/listem">Listeni gör</Link>
|
<Link href="/listem">
|
||||||
|
Listeni gör
|
||||||
|
<ArrowRight className="size-4" aria-hidden />
|
||||||
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</section>
|
</CtaBandi>
|
||||||
) : girisBekliyor ? (
|
) : girisBekliyor ? (
|
||||||
<CtaBandi ref={hazirPanelRef} seed={13} className="mt-16 min-w-0">
|
<CtaBandi ref={hazirPanelRef} seed={13} className="mt-16 min-w-0">
|
||||||
<h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl">
|
<h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl">
|
||||||
|
|||||||
@@ -180,6 +180,39 @@ export function yurtdisiUniKonum(
|
|||||||
return cozum && "anahtar" in cozum ? sehirKonumu(cozum.anahtar) : null;
|
return cozum && "anahtar" in cozum ? sehirKonumu(cozum.anahtar) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Yalnızca il değerinden ülke haritası (şehir sayfası). "KIBRIS" genel
|
||||||
|
* değeri ülke seviyesinde kalır; "ÜSKÜP-MAKEDONYA" gibi ülke ekli değerler
|
||||||
|
* ilk parçasıyla çözülür. Türkiye ili ya da bilinmeyen değer → null.
|
||||||
|
*/
|
||||||
|
export function ilUlkeHaritasi(il: string): {
|
||||||
|
ulke: UlkeKodu;
|
||||||
|
ulkeAd: string;
|
||||||
|
harita: UlkeHarita;
|
||||||
|
sehir: YurtdisiKonum | null;
|
||||||
|
} | null {
|
||||||
|
const anahtar =
|
||||||
|
il in SEHIRLER
|
||||||
|
? (il as SehirAnahtari)
|
||||||
|
: (() => {
|
||||||
|
const ilk = il.split("-")[0].trim();
|
||||||
|
return ilk in SEHIRLER ? (ilk as SehirAnahtari) : null;
|
||||||
|
})();
|
||||||
|
const ulke: UlkeKodu | null = anahtar
|
||||||
|
? SEHIRLER[anahtar].ulke
|
||||||
|
: il === "KIBRIS"
|
||||||
|
? "kktc"
|
||||||
|
: null;
|
||||||
|
if (!ulke) return null;
|
||||||
|
const harita = ULKE_HARITALARI[ulke];
|
||||||
|
return {
|
||||||
|
ulke,
|
||||||
|
ulkeAd: harita.ad,
|
||||||
|
harita,
|
||||||
|
sehir: anahtar ? sehirKonumu(anahtar) : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** Yalnızca il değerinden (kilitli satırlar) yurtdışı şehir konumu. */
|
/** Yalnızca il değerinden (kilitli satırlar) yurtdışı şehir konumu. */
|
||||||
export function yurtdisiIlKonum(il: string | null): YurtdisiKonum | null {
|
export function yurtdisiIlKonum(il: string | null): YurtdisiKonum | null {
|
||||||
return il && il in SEHIRLER ? sehirKonumu(il as SehirAnahtari) : null;
|
return il && il in SEHIRLER ? sehirKonumu(il as SehirAnahtari) : null;
|
||||||
|
|||||||
108
src/lib/il-izgara-verisi.ts
Normal file
108
src/lib/il-izgara-verisi.ts
Normal file
File diff suppressed because one or more lines are too long
@@ -62,6 +62,10 @@ const globalKatalog = globalThis as unknown as {
|
|||||||
bolumOzetler?: BolumOzet[];
|
bolumOzetler?: BolumOzet[];
|
||||||
uniMap?: Map<string, UniKayit>;
|
uniMap?: Map<string, UniKayit>;
|
||||||
uniOzetler?: UniOzet[];
|
uniOzetler?: UniOzet[];
|
||||||
|
iller?: IlOzet[];
|
||||||
|
ilMap?: Map<string, IlOzet>;
|
||||||
|
/** slug → (il → o ildeki program sayısı); getAllUniversiteler doldurur */
|
||||||
|
uniIlAdetleri?: Map<string, Map<string, number>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
function bolumMapGetir(): Map<string, BolumKayit> {
|
function bolumMapGetir(): Map<string, BolumKayit> {
|
||||||
@@ -333,6 +337,7 @@ export function getAllUniversiteler(): UniOzet[] {
|
|||||||
}
|
}
|
||||||
sayac.set(row.il, (sayac.get(row.il) ?? 0) + row.adet);
|
sayac.set(row.il, (sayac.get(row.il) ?? 0) + row.adet);
|
||||||
}
|
}
|
||||||
|
globalKatalog.uniIlAdetleri = ilAdetleri;
|
||||||
for (const [slug, sayac] of ilAdetleri) {
|
for (const [slug, sayac] of ilAdetleri) {
|
||||||
let enIyi: string | null = null;
|
let enIyi: string | null = null;
|
||||||
let enCok = 0;
|
let enCok = 0;
|
||||||
@@ -369,6 +374,87 @@ export function getAllUniversiteSlugs(): string[] {
|
|||||||
return [...uniMapGetir().keys()];
|
return [...uniMapGetir().keys()];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- İl (şehir) tarafı: /universiteler/[il] ----------
|
||||||
|
|
||||||
|
export type IlOzet = {
|
||||||
|
/** turkishSlugify(il): "istanbul", "saraybosna-bosna-hersek" */
|
||||||
|
slug: string;
|
||||||
|
/** Görünen ad: İstanbul, Bakü-Azerbaycan */
|
||||||
|
ad: string;
|
||||||
|
/** DB biçiminde ham il ("İSTANBUL"); harita anahtarı */
|
||||||
|
il: string;
|
||||||
|
/** Bu ilde programı olan üniversiteler (ad sırasıyla) */
|
||||||
|
uniler: UniOzet[];
|
||||||
|
programSayisi: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "BAKÜ-AZERBAYCAN" → "Bakü-Azerbaycan": trBaslikDuzeni yalnız boşlukta
|
||||||
|
* böler; yurt dışı şehirlerin tireli ülke eki de baş harfli yazılır.
|
||||||
|
*/
|
||||||
|
function ilGorunenAd(il: string): string {
|
||||||
|
return trBaslikDuzeni(il).replace(/-(\p{L})/gu, (_, harf: string) =>
|
||||||
|
`-${harf.toLocaleUpperCase("tr-TR")}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Üniversiteleri iline göre kümeler; sıralama üniversite sayısına göre
|
||||||
|
* azalan, eşitlikte ad. Bir üniversite **programı olan her ilde** sayılır
|
||||||
|
* (ODTÜ hem Ankara'da hem KKTC'de; öğrenci "KKTC'de okuyabileceğim
|
||||||
|
* üniversiteler" diye bakar) — böylece DB'deki her il değerinin sayfası
|
||||||
|
* vardır ve program satırlarındaki her şehir adı güvenle link olur
|
||||||
|
* (`ilSayfaHref`). `programSayisi` o ildeki program sayısıdır.
|
||||||
|
*/
|
||||||
|
export function getAllIller(): IlOzet[] {
|
||||||
|
if (!globalKatalog.iller) {
|
||||||
|
// Dev'de globalThis önbelleği HMR'ı aşar: uniOzetler eski modül koduyla
|
||||||
|
// kurulduysa il sayacı yoktur; özetler yeniden kurulur.
|
||||||
|
if (!globalKatalog.uniIlAdetleri) globalKatalog.uniOzetler = undefined;
|
||||||
|
const uniler = getAllUniversiteler();
|
||||||
|
const ilAdetleri = globalKatalog.uniIlAdetleri!;
|
||||||
|
const bySlug = new Map<string, IlOzet>();
|
||||||
|
for (const u of uniler) {
|
||||||
|
const sayac = ilAdetleri.get(u.slug);
|
||||||
|
if (!sayac) continue;
|
||||||
|
for (const [ham, adet] of sayac) {
|
||||||
|
const il = ham.trim();
|
||||||
|
if (!il) continue;
|
||||||
|
const slug = turkishSlugify(il);
|
||||||
|
if (!slug) continue;
|
||||||
|
let kayit = bySlug.get(slug);
|
||||||
|
if (!kayit) {
|
||||||
|
kayit = {
|
||||||
|
slug,
|
||||||
|
ad: ilGorunenAd(il),
|
||||||
|
il,
|
||||||
|
uniler: [],
|
||||||
|
programSayisi: 0,
|
||||||
|
};
|
||||||
|
bySlug.set(slug, kayit);
|
||||||
|
}
|
||||||
|
kayit.uniler.push(u);
|
||||||
|
kayit.programSayisi += adet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
globalKatalog.ilMap = bySlug;
|
||||||
|
globalKatalog.iller = [...bySlug.values()].sort(
|
||||||
|
(a, b) =>
|
||||||
|
b.uniler.length - a.uniler.length || a.ad.localeCompare(b.ad, "tr-TR"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return globalKatalog.iller;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllIlSlugs(): string[] {
|
||||||
|
return getAllIller().map((il) => il.slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getIlBySlug(slug: string): IlOzet | null {
|
||||||
|
getAllIller();
|
||||||
|
return globalKatalog.ilMap!.get(slug) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
export function getUniversiteBySlug(slug: string): UniDetay | null {
|
export function getUniversiteBySlug(slug: string): UniDetay | null {
|
||||||
const kayit = uniMapGetir().get(slug);
|
const kayit = uniMapGetir().get(slug);
|
||||||
if (!kayit) return null;
|
if (!kayit) return null;
|
||||||
|
|||||||
@@ -52,6 +52,14 @@ export function bolumSayfaSlug(isim: string): string {
|
|||||||
return turkishSlugify(bolumBazAdi(isim));
|
return turkishSlugify(bolumBazAdi(isim));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* /universiteler/[il] şehir sayfası adresi. Şehir sayfaları DB'deki her il
|
||||||
|
* değeri için üretildiğinden (katalog.ts getAllIller) adres her zaman vardır.
|
||||||
|
*/
|
||||||
|
export function ilSayfaHref(il: string): string {
|
||||||
|
return `/universiteler/${turkishSlugify(il)}`;
|
||||||
|
}
|
||||||
|
|
||||||
/** ALL-CAPS veritabanı adlarını tr-TR başlık düzenine çevirir (İSTANBUL → İstanbul). */
|
/** ALL-CAPS veritabanı adlarını tr-TR başlık düzenine çevirir (İSTANBUL → İstanbul). */
|
||||||
export function trBaslikDuzeni(s: string): string {
|
export function trBaslikDuzeni(s: string): string {
|
||||||
return s
|
return s
|
||||||
|
|||||||
Reference in New Issue
Block a user