# 09 — Tasarım: `/sonuc` haritası en üste + üniversite sayfasında tür > Temel: `is/0923-harita-ve-tur` worktree'si, `origin/main` (c5769e2). Karşılaştırma için > `gece/0922-urap` dalı `git show` ile okundu. Tarayıcı/dev sunucusu açılmadı, kod değiştirilmedi. > Piksel ve CLS değerleri **token/kod okumasından hesaplandı**, ölçülmedi — hesap §1.2'de açık. > Her iki talimat da "yapılacak" kabul edildi; aşağıdakiler *nasıl* yapılacağıdır. --- ## Özet — 5 madde 1. **Harita en üste çıkabilir ve CLS bedeli sıfıra inebilir.** Tek gereken, listenin *sayısını* sunucunun hidrasyondan önce bilmesi. Profil zaten `kolaytercih.profil` çerezi ile sunucuya aynalanıyor (`sihirbaz-profil.ts:51-57`); aynı desenle `kolaytercih.liste-sayi` çerezi yazılır, `/sonuc` bunu okuyup haritaya **tam ölçülü** yer ayırır. Uygulama yükü: 3 dosya, ~25 satır. 2. **"Boşken hiç gözükmesin" şartı bozulmuyor.** Çerez yoksa ya da 0 ise `ManuelHarita` bugünkü gibi `null` döner — ilk kez gelen ziyaretçi için DOM'da hiçbir düğüm yoktur, dolayısıyla tadımlığın `first:mt-0` kuralı (`tadimlik-satiri.tsx:134`) da bozulmaz. **İlk ziyaretçide sıra değişmiyor — doğrulandı** (`manuel-harita.tsx:71` `return null`). 3. **Tadımlığın kaybı kabul edilebilir ve dar.** Kayıp yalnız "girişsiz + kayıtlı listesi olan" ziyaretçide var. 375×812'de tadımlığın rozeti, H2'si ve açıklaması harita eklendikten sonra da katlamanın üstünde kalıyor (hesap §1.4); yalnız örnek satır kartı aşağı iniyor. Bu ziyaretçi "bu ürün ne?" aşamasını geçmiş, ekranda önce kendi listesini görmesi doğru. 4. **Üniversite sayfasında tür, ızgaraya değil kimlik satırına gider.** Izgara 5 karta çıkamaz: `lg:grid-cols-[1fr_1fr_1.35fr] lg:grid-rows-2` şablonunda harita 2 satır kaplıyor, kartlar için **tam 4 hücre** var (`universite-icerik.tsx:291`, `universite-konum-haritasi.tsx:77`). Tür zaten bir *metrik* değil *kategori*; sitenin geri kalanında (4 ayrı dosyada) başlığın altındaki meta satırında çip olarak yaşıyor. Aynı çip H1'in altına konur, ızgara 4 sayısal kart kalır. 5. **Yan bulgu: `unitur` etiketinin 4 farklı sözlüğü var** ve ikisi bozuk metin basıyor (`universite-icerik.tsx:318` ve `bolum-program-listesi.tsx:227` → "Kktc", "Vakıf Myo"). `UNITUR_ETIKET` (bugün `gece/0922-urap`'ta, bileşenin içinde) `src/lib/`'e taşınmalı — tıpkı `DILIM_ETIKET`'in `src/lib/risk.ts`'te tek kaynak olması gibi (AGENTS.md). --- # Talimat 1 — `/sonuc`'ta harita en üste ## 1.1 Bugünkü durum (doğrulandı) | Olgu | Kanıt | |---|---| | Sıra: tadımlık → tablo → harita | `src/app/sonuc/page.tsx:118, 124, 131` | | Liste boşken harita hiç çizilmiyor | `manuel-harita.tsx:71` (`if (liste.length === 0) return null`) | | Sunucu snapshot'ı **her zaman** boş | `liste-store.ts:30, 86` (`getServerSnapshot → BOS_LISTE`) | | Liste yalnız localStorage'ta | `liste-store.ts:27, 42-46` | | Profil ise sunucuya **çerezle** aynalanıyor | `sihirbaz-profil.ts:51-57`, `sonuc/page.tsx:65` | | Store her sayfada hidrasyonda okunuyor | `layout.tsx:98-102` → `ListeCekmecesiLazy`/`KaydetBannerLazy` → `useManuelListe` | Son satır kritik: liste store'u **site genelinde**, ilk açılan sayfada yükleniyor. Yani yazılacak çerez, kullanıcı `/sonuc`'a varmadan önce (ana sayfada) yazılmış oluyor. ## 1.2 CLS hesabı — "368 px" rakamı masaüstündü `main` = `max-w-4xl px-4` (`sonuc/page.tsx:99`). Harita `w-full`, viewBox `1050×447` (`harita.ts:10-13`, `tercih-haritasi.tsx:46-51`), iskelet de aynı oranı rezerve ediyor (`tercih-haritasi-lazy.tsx:14`). | Ekran | İçerik genişliği | Harita yüksekliği | |---|---|---| | 375 px (mobil, trafiğin ~%90'ı) | 343 px | 343 × 447/1050 = **146 px** | | ≥ 928 px (masaüstü) | 864 px | 864 × 447/1050 = **368 px** | Yani `sonuc/page.tsx:107`'deki "~368 px" **masaüstü** rakamı; mobilde 146 px (+ bugünkü `mt-16` = 64 px). CLS = etki payı × mesafe payı: | Senaryo | Etki payı | Mesafe payı | **CLS** | |---|---|---|---| | Bugün (harita altta), dönüş ziyaretçisi | ~0 (kayan içerik ~2,4 ekran aşağıda) | — | **~0** | | Harita üstte, yer ayırmadan, 375×812 | (812−220)/812 ≈ 0,73 | 146/812 = 0,18 | **~0,13** ("iyileştirilmeli") | | Harita üstte, yer ayırmadan, 1280×800 | ~0,75 | 368/800 = 0,46 | **~0,35** ("kötü") | | **Harita üstte + çerezle yer ayrılmış** | 0 | 0 | **0** | (Hesap; ölçüm değil. 220 px = bant + başlık + `py-12`, önceki raporun tahmini.) ## 1.3 İki alternatif ### Alternatif A — liste sayısı çerezi, tek yuva (ÖNERİLEN) ``` 375 px · /sonuc · DÖNÜŞ ziyaretçisi (çerez: liste-sayi=7) ┌───────────────────────────────┐ ← ilk boyada sunucudan gelen HTML │ [bant] [başlık] │ │ │ │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ 146 px · HaritaIskeleti │ ░░░ harita yer tutucu ░░░ │ (aspect-[1050/447], tam ölçü) │ ░░░░░░░░░░░░░░░░░░░░░░░░░░░ │ │ │ │ ◇ Yapay zekâdan örnek satır │ chunk inince yer tutucunun │ Yapay zekâ listendeki her │ İÇİ dolar; hiçbir şey kaymaz │ satırı böyle anlatır │ └───────────────────────────────┘ 375 px · /sonuc · İLK KEZ gelen ziyaretçi (çerez yok) ┌───────────────────────────────┐ │ [bant] [başlık] │ │ │ │ ◇ Yapay zekâdan örnek satır │ ← harita DOM'da hiç yok, │ Yapay zekâ listendeki her │ tadımlık first:mt-0'ı korur: │ satırı böyle anlatır │ BUGÜNKÜ EKRANIN AYNISI └───────────────────────────────┘ ``` - **CLS:** çerez isabetliyse **0**. Çerez yoksa ama liste varsa (aşağıdaki dar kaçak) 0,13 (mobil). - **Çerezin kaçacağı tek durum:** dağıtım sonrası ilk sayfa açılışı (kullanıcı başına **bir kez**; ana sayfada yazıldığı için `/sonuc`'a client-side geçişte bile kapanır) ve localStorage yazma hatası. Safari ITP kaçağı **yok sayılabilir**: ITP script'le yazılan çerezi 7 günde siler ama aynı koşulda localStorage'ı da siler — ikisi birlikte ölür, ayrışmaz. Aynı risk zaten `kolaytercih.profil` çerezinde de var, yeni bir sınıf risk getirmiyor. ### Alternatif B — çift yuva (çerez varsa üstte, yoksa altta) `ManuelHarita` iki konumda da yazılır; sunucu çereze bakarak hangisinin aktif olacağını seçer. ``` çerez > 0 → [HARİTA] → tadımlık → tablo → sihirbaz çerez yok → tadımlık → tablo → [HARİTA] → sihirbaz (bugünkü davranış) ``` - **CLS:** her durumda ~0. Kaçak senaryoda bile sıfır, çünkü harita eski (güvenli) yerine düşer. - **Bedeli:** aynı bileşen iki DOM konumunda yaşar — AGENTS.md'nin "bir eleman iki yerde görünüyorsa tek kaynaktan" ilkesine biçimsel olarak uymasa da okuma yükü artar; ve kullanıcı bazı ziyaretlerde haritayı üstte, bazılarında altta görür. **Tutarsızlık, kazanılan 0,13'lük tek seferlik CLS'ten daha pahalı.** ### Hüküm: **A** B'nin kapattığı tek delik, kullanıcı başına bir kez yaşanan ve "iyileştirilmeli" bandındaki (kırmızı değil) bir kayma. Bunun için haritanın sabit bir yeri olmamasını kabul etmek doğru takas değil. ## 1.4 Tadımlık ne kaybediyor — hüküm: kabul edilebilir | Ziyaretçi | Sıra değişiyor mu? | Tadımlık nerede? | |---|---|---| | İlk kez gelen (liste boş) | **Hayır** — harita hiç render edilmez | Bugünkü yerinde, ~220 px | | Girişli (tadımlık zaten yok, `tadimlik-bolumu.tsx:19`) | Evet, harita tablonun üstüne çıkar | — | | **Girişsiz + kayıtlı listesi olan** | Evet | ~220 → **~366 px** | Üçüncü satır için 375×812 hesabı (koddan çıkarım): tadımlığın `SectionEyebrow`'u, H2'si ("Yapay zekâ listendeki her satırı böyle anlatır") ve üç satırlık açıklaması katlamanın üstünde kalıyor; aşağı inen şey örnek satır kartının gövdesi ve CTA'sı. 375×667'de (iPhone SE) rozet + H2 görünür, paragraf yarılanır. **Hüküm: kabul edilebilir.** Gerekçe kaybın büyüklüğü değil, kime denk geldiği: tadımlık "ödeme öncesi tek gerçek önizleme" argümanı **ürünü ilk kez gören** kişi içindir ve o kişide sıra hiç değişmiyor. Zaten liste kurmuş ziyaretçi için ekranın ilk işi, kendi kurduğu listeyi göstermektir; tadımlığı onun üstüne koymak "sana hâlâ ürünü tanıtıyorum" demek olurdu. ## 1.5 Uygulayıcıya tarif (yazılımcı) **Dosya 1 — `src/features/liste/liste-store.ts`** 1. `PROFIL_CEREZ_ADI` deseninin eşi olarak sabit ekle: `LISTE_SAYI_CEREZ_ADI = "kolaytercih.liste-sayi"` (sabitin yeri: bu dosya; sunucu tarafı buradan import eder — `liste-store.ts` `"use client"` olduğu için sabiti `liste-dogrula.ts`'e koymak daha temiz, orası zaten sunucu action'larıyla paylaşılıyor, bkz. `:23-25`). 2. `sayiCereziniYaz(n: number)` — `profilCerezineYaz` (`sihirbaz-profil.ts:51-57`) ile **birebir aynı** öznitelikler: `path=/; max-age=31536000; samesite=lax` + https'te `secure`. Değer düz tamsayı. 3. Üç çağrı yeri (listenin değiştiği her yer): - `kaydet()` içinde (`:42-46`) — ekle/çıkar/sırala/temizle hepsi buradan geçiyor; - `yukle()` içinde, `liste = dogrula(...)` satırından sonra (`:53`) — dağıtım sonrası ilk açılışta çerezi kendiliğinden doğurur; - `"storage"` dinleyicisinde `liste` atandıktan sonra (`:64`) — başka sekmedeki değişiklik. **Dosya 2 — `src/features/liste/components/manuel-harita.tsx`** 4. İmza: `export function ManuelHarita({ yerAyir = false }: { yerAyir?: boolean })`. 5. `:71` → `if (liste.length === 0 && !yerAyir) return null;` Boş+`yerAyir` dalında **ayrı bir iskelet dalı yazma**: fonksiyon akmaya devam etsin, `pinler` boş dizi olur, `TercihHaritasiLazy` monte olur ve chunk inene kadar zaten `HaritaIskeleti`'ni gösterir (`tercih-haritasi-lazy.tsx:10-21`). Böylece (a) yer tutucu ile gerçek harita **aynı** kutudan gelir, yükseklik ayrışamaz; (b) 197 KB'lık chunk hidrasyonu beklemeden inmeye başlar. 6. `:78` `
…çip… {il}
``` Çipin sınıfı `program-tablosu.tsx:329-333`'ten **kopyalanır**, yeni varyant üretilmez. Renk kuralı da aynı: `DEVLET` → slate, diğer her şey → violet. `uni.unitur` NULL ise satırın tamamı render edilmez (kopuk boşluk kalmaz). 3. Izgaradaki 4. hücre (`gece/0922-urap`, `:394-417`): `uni.urap ? URAP : Tür` koşulu → `uni.urap ? URAP : Toplam kontenjan`. `dt`: "Toplam kontenjan", `dd`: `sayi(uni.stats.toplamKontenjan)`. 4. `bolum-program-listesi.tsx:227` `trBaslikDuzeni(p.unitur)` → `UNITUR_ETIKET[p.unitur].cip`. Bugün o çip "Kktc" / "Vakıf Myo" / "Yurtdısı Kamu" basıyor (`slug.ts:56-64` tr-TR küçültmesi). **Aynı commit'e girmeli**: tek kaynak taşınırken düzelmeyen tek tüketici kalmasın. **Kabul kriterleri** 1. `bogazici-universitesi.html` → çipte "Devlet" **1 kez**, özet cümlesinde "devlet üniversitesi" **1 kez**, ızgarada "Tür" **0 kez**. 2. `dogu-akdeniz-universitesi.html` (URAP yok) → çipte "KKTC", 4. hücrede "Toplam kontenjan" + sayı, hiçbir hücrede "—" yok. 3. `/bolum/...` sayfalarında KKTC programının çipinde "KKTC" yazıyor, "Kktc" değil. 4. 390 px'te H1 bloğu + çip satırı yatay taşma üretmiyor (uzun üniversite adı: "İstanbul Medeniyet Üniversitesi"). 5. `pnpm build` temiz; 229 üniversite sayfasının 229'unda çip var. **Büyüklük: S** (2 yeni/taşınan dosya + 3 dokunuş, ~50 satır). ## 2.5 Aynı işte kapanacak tutarlılık borcu | Yer | Sorun | Kanonik kaynak | |---|---|---| | `program-tablosu.tsx:329` · `rapor-listesi.tsx:227` · `tadimlik-satiri.tsx:161` · `bolum-program-listesi.tsx:222` | Aynı çip sınıf seti **4 kez elle kopyalanmış**; 5.'si ekleniyor | AGENTS.md "ortak elemanlar tek kaynaktan". **Öneri:** `src/components/unitur-cipi.tsx` — küçük, tek amaçlı sunum bileşeni. Bu, "yeni bileşen tasarlama" yasağının kapsamına girmez: yeni bir *tasarım* değil, var olan beş kopyanın tek kaynağa alınmasıdır. Ayrı ve tek amaçlı commit. | | `app/api/katalog-ara/route.ts:49-56` | `unitur` için **beşinci** sözlük: "KKTC / Yurt dışı" (arama sonucunda böyle, sayfada "KKTC") | Aynı `UNITUR_ETIKET`. Düşük öncelik; ayrı iş. | | `program-tablosu.tsx:175` | Root `