feat(tasarim): üniversite sayfası "şişko ve sade" dile göre baştan, DESIGN.md know-how'ı
Üniversite sayfası (canlı referans ekran /universite/ankara-universitesi): - Program listesi: ad kesilmez/sarar, telefonda taban sıra alt satıra iner, satır butonları 44 px, tür çipi okunur boyda - Başlık bloğu lg altında ortalı (logo başlığın üstünde), kimlik hapları (Devlet/Ankara) navbar ölçüsünde, ikon + metin - Harita yanında 4 özet kartı (OzetKart): sayı solda iri, ikon sağda; 4. kart URAP Türkiye sırası + piksel sıra şeridi (PixelSira) - Akademik göstergeler (URAP + YÖK İstatistik) sayfadan "Akademik göstergeler ›" satırının açtığı alt sayfaya taşındı: kartsız, ortalı, masonry gösterge kareleri, tek ekrana sığar - Hover tek turuncu (orange-500); liste satırı adı mavi kalır - Üst çubuk başlık geçişi (NavBaslik): h1 kaydırılınca üst çubukta logo + ad belirir; bölüm sayfasında da (yalnız ad) Tasarım dili: - DESIGN.md: temel his (§1.1), hover kuralı (§2.1), yazı alt sınırları, yeni tek kaynak parçalar (§4.8–4.15), lg altı ortala (§5.2), reddedilen denemeler (§7), ölç-tahmin-etme ve tuzaklar (§9) - Denetim: gösterge karesi renk istisnası (bg-indigo-500, bg-green-600) ve site taraması için dört uyarı (minik-yazi, buyuk-harf-etiket, mavi-hover, iri-sayi-tabular) Not: universite-icerik.tsx'teki akademik göstergeler bölümü (URAP/YÖK kartları) Bilal'in commit'lenmemiş işiydi; tasarım değişiklikleriyle aynı dosyada iç içe olduğu için bu commit'e girdi. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@ yön zaten belli. Kaynak tek: `docs/tasarim/DESIGN.md`. Bu dosya onu nasıl uygu
|
||||
|
||||
## 1. Başlamadan önce (zorunlu)
|
||||
|
||||
1. `docs/tasarim/DESIGN.md` dosyasını **tamamını** oku (181 satır). Özellikle §4 tek kaynaklar ve §7 yap/yapma.
|
||||
1. `docs/tasarim/DESIGN.md` dosyasını **tamamını** oku (477 satır). Özellikle §1.1 temel his, §4 tek kaynaklar, §7 yap/yapma ve §9.2 ölç-tahmin-etme. Canlı referans ekran: `/universite/ankara-universitesi`.
|
||||
2. Dört satırlık brief'i doldur (DESIGN.md §9): amaç · referans ekran · tek kaynak parçalar · durumlar.
|
||||
3. Referans ekranı bul ve aç; kopyalayacağın sınıf dizilerini oradan al:
|
||||
|
||||
|
||||
@@ -3,23 +3,57 @@
|
||||
Bu dosya sitenin görsel dilinin **tek yazılı kaynağıdır**. Yeni ekran, bileşen, modal, kart
|
||||
ya da buton çizmeden önce okunur; mevcut arayüz değiştirilirken buna bakılır. Değerler koddan
|
||||
çıkarılmıştır (`src/app/globals.css`, `src/app/layout.tsx`, `src/components/pixel-decor.tsx`,
|
||||
`src/components/ui/*`, `src/app/page.tsx`); kodla çelişirse kod değil, bu dosya düzeltilir ve
|
||||
çelişki raporlanır. Kural ihlallerini `pnpm tasarim:denetim` yakalar.
|
||||
`src/components/ui/*`, `src/app/page.tsx`, `src/features/katalog/components/universite-icerik.tsx`);
|
||||
kodla çelişirse kod değil, bu dosya düzeltilir ve çelişki raporlanır. Kural ihlallerini
|
||||
`pnpm tasarim:denetim` yakalar.
|
||||
|
||||
Biçim: Google Stitch'in DESIGN.md düzeni (9 bölüm). Sabit değerler Tailwind sınıfı olarak
|
||||
verilir; kodda da sınıf kullanılır, hex/oklch elle yazılmaz.
|
||||
|
||||
**Canlı referans ekran:** `/universite/ankara-universitesi` (23 Eyl 2026'da Bilal'le birlikte
|
||||
bu belgedeki "şişko ve sade" dile göre baştan tasarlandı). Bir kural belirsiz kalırsa o sayfanın
|
||||
kodu (`universite-icerik.tsx`, `universite-program-tablosu.tsx`, `akademik-ayrinti.tsx`,
|
||||
`nav-baslik.tsx`) ölçüdür. Site genelinde dönüşüm bu ekranı örnek alır.
|
||||
|
||||
---
|
||||
|
||||
## 1. Görsel tema ve atmosfer
|
||||
|
||||
**Bir cümle:** Beyaz, bol nefesli, mavi bilgi + turuncu aksiyon; her yüzeyde küçük bir
|
||||
**piksel imzası** (hero'daki dot-matrix Türkiye haritasının dili) dolaşır.
|
||||
**Bir cümle:** Beyaz, bol nefesli, **şişko ve sade** — kullanıcıya "basit bir şey
|
||||
kullanıyorum" hissi veren, Apple uygulamaları kadar kolay anlaşılır bir arayüz; mavi bilgi +
|
||||
turuncu aksiyon; her yüzeyde küçük bir **piksel imzası** (hero'daki dot-matrix Türkiye
|
||||
haritasının ve logodaki üç karenin dili) dolaşır.
|
||||
|
||||
### 1.1 Temel his: şişko ve sade
|
||||
|
||||
Referans: header (`src/components/site-header.tsx` ve içindeki arama/kullanıcı hapları), hero
|
||||
ve üniversite sayfası. Bu hissi veren şeyler, ekrandan bağımsız:
|
||||
|
||||
- **İri dokunma alanı.** Butonlar ve haplar 44–48 px (`h-11`/`h-12`), tam yuvarlak.
|
||||
- **İri yazı, az eleman.** Bir ekranda az şey var ve her biri büyük. Küçük gri yazı yığını yok.
|
||||
- **Sayı kahramandır.** Bir veri gösteriliyorsa sayı kartın en büyük öğesidir; etiket ve
|
||||
açıklama onun altında ikinci planda.
|
||||
- **Sık bakılan önde, ayrıntı bir dokunuş derinde** (Apple'ın "önce ortak yol, gelişmiş
|
||||
seçenekler bir seviye aşağıda" ilkesi). Sayfada en çok dört özet sayı; fazlası ayrıntı
|
||||
satırının açtığı alt sayfaya (§4.10).
|
||||
- **Nefes alanı korunur.** Harita gibi görsel dinlenme alanları küçültülmez, sıkıştırılmaz,
|
||||
bilgiyle doldurulmaz. Sadeleştirmek = bu alanları silmek değil, etraflarındaki kalabalığı
|
||||
azaltmaktır.
|
||||
- **Verimli ama havadar.** Alt sayfa ve paneller dikine uzamaz; ilişkili öğeler yan yana
|
||||
dizilir, aralarında boşluk kalır (§5.3).
|
||||
- **Doğal duruş.** Eşit, hizalı ızgaralar yerine logodaki üç kare gibi hafif kaymış yerleşim
|
||||
(§4.11). Her şey birbirine cetvelle hizalıysa ekran "tablo" gibi durur.
|
||||
- **Basınca tepki.** Her tıklanabilir öğe basınca hafifçe küçülür (`active:scale-[0.97]`,
|
||||
geniş satırlarda `[0.99]`), üzerine gelince turuncuya döner (§2.1).
|
||||
|
||||
### 1.2 Genel kurallar
|
||||
|
||||
- Zemin her zaman beyaz; koyu zemin yalnızca CTA bandı ve çekmece başlığı gibi tek bir
|
||||
vurgu yüzeyinde (`PixelField` ile).
|
||||
- İmza dekoratiftir ama rastgele değildir: deterministik ızgara (hücre 10, nokta 7.4, rx 2),
|
||||
imleç yaklaşınca turuncuya ısınan pikseller (`usePixelHeat`), uzaklaşınca yavaş soğuma.
|
||||
Piksel **veri taşıyorsa** (ör. `PixelSira`) kalır; iki bölüm arasına **süs olarak** konmuş
|
||||
piksel ayraç alt sayfalarda ve kompakt panellerde kullanılmaz.
|
||||
- Ton: öğrenciye "sen" diyen, sakin, vaatsiz. Süs yok, gösteriş yok; hareket yalnızca
|
||||
bir şey söylüyorsa var.
|
||||
- **Karanlık mod yoktur.** `dark:` sınıfı yazılmaz (`.dark` token'ları shadcn kalıntısıdır).
|
||||
@@ -28,31 +62,66 @@ verilir; kodda da sınıf kullanılır, hex/oklch elle yazılmaz.
|
||||
|
||||
| Rol | Sınıf | Nerede |
|
||||
|---|---|---|
|
||||
| Bilgi/vurgu mavisi (`primary`) | `text-primary`, `bg-primary/10`, `border-primary/30` | Rozet metni, ikon kutusu, hero'daki italik vurgu, linkler, kart hover kenarlığı |
|
||||
| Bilgi/vurgu mavisi (`primary`) | `text-primary`, `bg-primary/10`, `border-primary/30` | Rozet metni, ikon kutusu, hero'daki italik vurgu, linklerin durağan rengi |
|
||||
| **Aksiyon turuncusu** | `bg-orange-500 hover:bg-orange-600 text-white` | Funnel CTA butonları. Bir ekranda **tek** turuncu buton |
|
||||
| **Hover turuncusu** | `hover:text-orange-500`, `hover:border-orange-500`, `hover:bg-orange-50/60`, `group-hover:text-orange-500` | Link, kart, satır ve ikincil butonların üzerine gelme hâli (§2.1); liste satırı adı hariç (mavi kalır) |
|
||||
| Piksel vurgusu | `fill-orange-500` (opaklık 0.55–0.85) | Yalnızca piksel süslerinin "sıcak" noktaları |
|
||||
| Başlık metni | `text-slate-900` | h1–h3, kart başlıkları |
|
||||
| Başlık metni | `text-slate-900` | h1–h3, kart başlıkları, iri sayılar |
|
||||
| Gövde metni | `text-slate-600` | Paragraf, açıklama |
|
||||
| İkincil metin | `text-slate-500` | Meta bilgi, yardımcı satır |
|
||||
| Silik metin | `text-slate-400` | İpucu, devre dışı, placeholder |
|
||||
| İkincil metin | `text-slate-500` | Meta bilgi, yardımcı satır, kart açıklaması |
|
||||
| Silik metin | `text-slate-400` | İpucu, devre dışı, placeholder, sayının yanındaki birim (`/ 198`) |
|
||||
| Kenarlık | `border-slate-200` (landing kartlarında `border-slate-200/70`) | Kart, rozet, çarpı, giriş alanı |
|
||||
| İç zemin | `bg-slate-50` | Kartın içindeki ikincil kutu, tablo başlığı |
|
||||
| Hover zemini | `hover:bg-slate-100` | Çarpı, ghost buton |
|
||||
| Nötr hover zemini | `hover:bg-slate-100` | Yalnız kapatma çarpısı ve ghost buton |
|
||||
| Risk: güvenli | `emerald-500` / `text-emerald-600` | **Her zaman "Güvenli" metniyle** |
|
||||
| Risk: az riskli | `amber-500` / `text-amber-600` | **Her zaman "Az riskli" metniyle** |
|
||||
| Risk: riskli | `red-500` / `text-red-600` | **Her zaman "Riskli" metniyle** |
|
||||
| Uyarı kutusu | `border-amber-200 bg-amber-50 text-amber-900` | Bilgilendirme notu |
|
||||
| Gösterge karesi | Dolu zemin + beyaz yazı: `bg-orange-500`, `bg-primary`, `bg-indigo-500`, `bg-green-600`; `rounded-3xl` | Yalnız sayı gösterge kareleri (§4.11). Renk sempati katar, anlam taşımaz: etiket her karede yazılı |
|
||||
| Hata | `text-destructive`, `text-red-600` | Form hatası |
|
||||
|
||||
Kurallar:
|
||||
### 2.1 Hover: tek turuncu
|
||||
|
||||
Bilal'in kararı (23 Eyl 2026): üzerine gelme hâlinde **sitenin turuncusu** kullanılır ve bu
|
||||
turuncu **tek tondur: `orange-500`** — CTA butonunun turuncusuyla birebir aynı değer.
|
||||
|
||||
- Link / metin: `transition-colors hover:text-orange-500` (altı çizili linklerde
|
||||
`hover:underline` kalır).
|
||||
- Tıklanabilir kart / satır: `hover:border-orange-500 hover:bg-orange-50/60`; içindeki iri
|
||||
sayı ya da başlık `group-hover:text-orange-500`; sağdaki ok
|
||||
`group-hover:translate-x-0.5 group-hover:text-orange-500`.
|
||||
- İkincil (outline) buton: `hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500`.
|
||||
- **İstisna — liste satırındaki adlar mavi kalır:** program/üniversite listelerinde satırın
|
||||
adı (bölüm ya da üniversite sayfasına giden link) `hover:text-primary hover:underline`.
|
||||
Bilal'in kararı (23 Eyl 2026): uzun bir listede imleç satırdan satıra geçerken yanıp sönen
|
||||
turuncu adlar yorucu; mavi sakin kalır. Satırın butonları ve listedeki diğer linkler §2.1'e
|
||||
uyar.
|
||||
- **Yapılmaz:** hover'da `text-primary`/`border-primary` (eski mavi hover — yukarıdaki
|
||||
istisna dışında), `orange-600` yazı, `orange-300` kenarlık — aynı ekranda iki turuncu tonu
|
||||
göze "farklı turuncu" diye batar.
|
||||
Tek istisna: turuncu CTA'nın kendi zemininin hover'ı (`hover:bg-orange-600`), kanonik.
|
||||
- Geçiş notu: kodda hâlâ `hover:text-primary` / `hover:border-primary/30` kullanan yerler var
|
||||
(landing kartı dahil); tarama sırasında bu kurala çevrilir. Denetim bunları `mavi-hover`
|
||||
uyarısıyla listeler; liste satırı adındaki uyarı istisnadır, dokunulmaz.
|
||||
|
||||
### 2.2 Palet kuralları
|
||||
|
||||
- Nötr yalnızca **slate**. `gray`, `zinc`, `neutral`, `stone` yazılmaz.
|
||||
- `indigo`, `purple`, `violet`, `fuchsia`, `pink`, `cyan`, `teal`, `lime`, `green`, `yellow`
|
||||
yeni kullanım açmaz. (`bg-violet-50 text-violet-600` tek bir mevcut durum çipidir; yeni yere taşıma.)
|
||||
yeni kullanım açmaz. Tek istisna "Gösterge karesi" satırındaki dolu `bg-indigo-500` ve
|
||||
`bg-green-600` zeminleri (Bilal'in kararı, 23 Eyl 2026); yazı, kenarlık ya da başka
|
||||
ton/opaklıkta bu renkler yine yasak. (`bg-violet-50 text-violet-600` tek bir mevcut durum
|
||||
çipidir; yeni yere taşıma — üniversite sayfasında "Vakıf" çipi bu yüzden hap + ikonla
|
||||
değiştirildi, §4.12.)
|
||||
- Gradyan metin (`bg-clip-text text-transparent`) ve parlama gölgesi (`shadow-[0_0_…]`) yok.
|
||||
- Hex/oklch değeri sınıf içine yazılmaz (`text-[#…]`); token ya da Tailwind tonu kullanılır.
|
||||
Risk renklerinin SVG karşılığı `src/lib/risk.ts` içindeki `RISK_PIN_RENK`'ten okunur.
|
||||
- Renk tek başına anlam taşımaz: risk rengi her zaman metin etiketiyle birlikte gider
|
||||
(`RISK_ETIKET`, `DILIM_ETIKET` — üçüncü dilimin görünen adı "Güvenli").
|
||||
(`RISK_ETIKET`, `DILIM_ETIKET` — üçüncü dilimin görünen adı "Güvenli"). Tür ayrımı (devlet /
|
||||
vakıf) da renkle değil ikon + metinle yapılır.
|
||||
- Renkli dolu zeminde beyaz yazı: turuncu ve yeşil zeminde küçük beyaz yazı WCAG AA'nın
|
||||
altında kalır (sitenin turuncu CTA'sı da aynı ikiliyi kullanır). Bu zeminlerde etiket
|
||||
`font-bold`, sayı en az `text-3xl`; uzun metin konmaz.
|
||||
|
||||
## 3. Tipografi
|
||||
|
||||
@@ -60,27 +129,51 @@ Dört yazı tipi, her birinin tek bir işi var:
|
||||
|
||||
| Sınıf | Yazı tipi | İş |
|
||||
|---|---|---|
|
||||
| `font-heading` | Cal Sans | Tüm başlıklar (h1–h3, kart başlığı). Her zaman `font-bold` ile |
|
||||
| `font-heading` | Cal Sans | Tüm başlıklar (h1–h3, kart başlığı) ve **iri sayılar**. Her zaman `font-bold` ile |
|
||||
| `font-sans` (varsayılan) | Clarity City 400–800 | Gövde, buton, form, tablo |
|
||||
| `font-bricolage` | Bricolage Grotesque (opsz ekseni) | Vurgu: hero'da italik mavi span, küçük etiketler/çipler, sayısal rozetler |
|
||||
| `font-bricolage` | Bricolage Grotesque (opsz ekseni) | Vurgu: hero'da italik mavi span, logo yazısı, üst çubuktaki sayfa adı (§4.14), küçük etiketler/çipler |
|
||||
| `font-mono` | Geist Mono | Yalnızca kod/markdown blokları. Arayüzde kullanılmaz |
|
||||
|
||||
Ölçek (kopyala, uydurma):
|
||||
|
||||
- Hero h1: `font-heading text-5xl font-bold tracking-[0.015em] sm:text-6xl`; vurgu kelimesi
|
||||
`<span className="font-bricolage italic tracking-tighter font-bold text-primary">`
|
||||
- Bölüm h2: `font-heading text-3xl font-bold sm:text-4xl` (istenirse `text-slate-900`)
|
||||
- Panel/kart h3: `font-heading text-xl font-bold` · liste satırı başlığı: `font-heading text-lg font-bold text-slate-900`
|
||||
- Alt sayfa h1 (katalog, rehber): `font-heading text-3xl font-bold leading-tight text-slate-900 sm:text-5xl`
|
||||
- Bölüm h2: `font-heading text-3xl font-bold sm:text-4xl` (istenirse `text-slate-900`);
|
||||
alt sayfada liste bölümü başlığı `font-heading text-2xl font-bold text-slate-900 sm:text-3xl`
|
||||
- Panel/kart h3: `font-heading text-xl font-bold` · grup başlığı (fakülte vb.):
|
||||
`font-heading text-xl font-bold text-slate-900 sm:text-2xl`
|
||||
- Küçük başlık: `font-heading text-base font-bold text-slate-900`
|
||||
- Bölüm giriş paragrafı: `max-w-xl text-lg leading-relaxed text-slate-600`
|
||||
- Gövde: `text-sm` (tablo, form, kart içi) ya da `text-base leading-relaxed` (açıklama)
|
||||
- Sayfa özet paragrafı: `text-base leading-relaxed text-slate-600 sm:text-lg`
|
||||
- Gövde: `text-base leading-relaxed` (açıklama, liste satırı adı) ya da `text-sm` (kart
|
||||
açıklaması, tablo hücresi, meta)
|
||||
- Etiket/rozet metni: `text-xs font-semibold`
|
||||
- Yasak: Inter, Roboto, Arial, sistem fontu, serif, `font-[…]` keyfi font; tek kelimelik
|
||||
büyük harf etiketler (uppercase yok, rozet metni büyük-küçük harfli yazılır).
|
||||
|
||||
**İri sayı ölçeği** (§1.1 "sayı kahramandır"):
|
||||
|
||||
| Yer | Sınıf |
|
||||
|---|---|
|
||||
| Özet kartı sayısı | `font-heading text-4xl font-bold leading-none tracking-tight text-slate-900 sm:text-5xl` |
|
||||
| Gösterge karesi, büyük | `font-heading text-5xl font-bold leading-none tracking-tight` |
|
||||
| Gösterge karesi, orta | `font-heading text-3xl font-bold leading-none` |
|
||||
| Sayının birimi (`/ 198`, `kişi`) | `text-base font-semibold text-slate-400` (renkli zeminde `text-white/80`) |
|
||||
|
||||
**Alt sınırlar:**
|
||||
- Gövde metni telefonda 16 px'in (`text-base`) altına inmez; `text-sm` yalnız ikincil
|
||||
bilgide, `text-xs` yalnız ince yazıda (kaynak satırı, eksen etiketi, çip).
|
||||
- **12 px altı yok:** `text-[10px]`, `text-[11px]` yazılmaz (denetim uyarır).
|
||||
- **Büyük harfli minik etiket yok:** `uppercase tracking-wide text-xs` kalıbı ("PROGRAM",
|
||||
"TOPLAM KONTENJAN") yazılmaz; etiket büyük-küçük harfli, `text-sm font-semibold`.
|
||||
- **İri sayıda `tabular-nums` yok:** Cal Sans'ta sabit genişlikli rakam "daktilo" gibi
|
||||
aralıklı durur ("13.88 9"). `tabular-nums` yalnız `font-sans` tablo sütunlarında.
|
||||
- Yasak: Inter, Roboto, Arial, sistem fontu, serif, `font-[…]` keyfi font.
|
||||
|
||||
## 4. Bileşen stilleri (tek kaynaklar)
|
||||
|
||||
Bir eleman iki yerde görünüyorsa stili tek kaynaktan gelir. Aşağıdakiler yeniden tasarlanmaz.
|
||||
§4.8–§4.15 üniversite sayfasında doğdu; ikinci bir ekranda gerektiğinde **kopyalanmaz**, ortak
|
||||
bir dosyaya (`src/components/…`) çıkarılıp iki yer de oradan kullanır.
|
||||
|
||||
### 4.1 Buton — `src/components/ui/button.tsx`
|
||||
- Her buton `rounded-full`; `h-9` varsayılan, `h-11`/`h-12` funnel CTA'ları (44 px dokunma hedefi), `size-9` ikon butonu.
|
||||
@@ -93,24 +186,32 @@ Bir eleman iki yerde görünüyorsa stili tek kaynaktan gelir. Aşağıdakiler y
|
||||
</Button>
|
||||
```
|
||||
Metin önce, ok sonda. Başta ikon yok. `Sparkles` yok. İstenirse `active:scale-[0.97]`.
|
||||
- İkincil: `variant="outline"`; sessiz: `variant="ghost"`; link görünümü: `variant="link"`.
|
||||
- **İkincil gezinme butonu** ("Tüm sıralamayı gör →"):
|
||||
```tsx
|
||||
<Button asChild size="lg" variant="outline" className="cursor-pointer px-6 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]">
|
||||
<Link href="…">Tüm sıralamayı gör<ArrowRight className="size-4" aria-hidden /></Link>
|
||||
</Button>
|
||||
```
|
||||
- Sessiz: `variant="ghost"`; link görünümü: `variant="link"`.
|
||||
|
||||
### 4.2 Rozet (bölüm başlığı üstündeki kapsül) — `SectionEyebrow`, `src/components/pixel-decor.tsx`
|
||||
```tsx
|
||||
<SectionEyebrow>Nasıl çalışır?</SectionEyebrow>
|
||||
```
|
||||
Sınıfı: `inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3.5 py-1.5 text-xs font-semibold text-primary` + başta `PixelMark`.
|
||||
`@/components/ui/badge` **kullanılmaz**; elle pill span yazılmaz. Filtre çipleri ve nav butonları bu kuralın dışındadır (onlar etkileşimlidir).
|
||||
`@/components/ui/badge` **kullanılmaz**; elle pill span yazılmaz. Filtre çipleri, nav butonları
|
||||
ve kimlik hapları (§4.12) bu kuralın dışındadır.
|
||||
|
||||
### 4.3 Modal kapatma çarpısı — `DialogContent` varsayılanı, `src/components/ui/dialog.tsx`
|
||||
`absolute top-3 right-3 z-10 flex size-9 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-popover text-slate-500 transition-[background-color,transform] duration-150 hover:bg-slate-100 hover:text-slate-800 active:scale-[0.96]` + `<XIcon className="size-4" aria-hidden />` + `sr-only "Kapat"`.
|
||||
Özel yerleşim gerekiyorsa `showCloseButton={false}` ver ve **aynı sınıf setini** kopyala.
|
||||
Alt sayfada (`Drawer`) aynı sınıf seti `DrawerClose` üstünde kullanılır (§4.10).
|
||||
|
||||
### 4.4 Kart ve kutu
|
||||
- Landing/öne çıkan kart: `<Card className="relative h-full overflow-hidden border-slate-200/70 shadow-sm transition-all duration-300 hover:-translate-y-1 hover:border-primary/30 hover:shadow-lg hover:shadow-primary/5">` + sağ üstte `<PixelCorner className="absolute right-4 top-4" />`.
|
||||
- Landing/öne çıkan kart: `<Card className="relative h-full overflow-hidden border-slate-200/70 shadow-sm transition-all duration-300 hover:-translate-y-1 hover:border-orange-500 hover:shadow-lg hover:shadow-orange-500/5">` + sağ üstte `<PixelCorner className="absolute right-4 top-4" />`. (Kodda hâlâ `hover:border-primary/30 hover:shadow-primary/5`; §2.1 geçiş notu.)
|
||||
- Sade kutu (form, panel, liste satırı): `rounded-2xl border border-slate-200 bg-white` (+ `p-5`/`p-6`). Küçük iç kutu: `rounded-xl border border-slate-200 bg-slate-50`. Not kutusu: `rounded-lg`.
|
||||
- İkon kutusu: `flex size-11 items-center justify-center rounded-xl bg-primary/10` içinde `size-5 text-primary` ikon.
|
||||
- Gölge: `shadow-sm` varsayılan; hover'da `shadow-lg shadow-primary/5`. Başka gölge yok.
|
||||
- İkon kutusu: `flex size-11 items-center justify-center rounded-xl bg-primary/10` içinde `size-5 text-primary` ikon. Kart içinde sayının yanında küçük hâli: `size-8 … sm:size-10` + `size-4 sm:size-5` ikon (§4.8).
|
||||
- Gölge: `shadow-sm` varsayılan; hover'da `shadow-lg shadow-orange-500/5`. Başka gölge yok.
|
||||
|
||||
### 4.5 Risk çipi / etiketi
|
||||
Nokta + metin: `<span className="size-2 rounded-full bg-emerald-500" />` yanında `text-xs font-semibold text-emerald-600`; metin `RISK_ETIKET`'ten. Renk tek başına asla.
|
||||
@@ -121,8 +222,131 @@ shadcn `Input`, `Select`, `Textarea`, `Slider`. Etiket her alanda; hata mesajı
|
||||
### 4.7 İkonlar
|
||||
Yalnızca `lucide-react`. Boyut: `size-4` buton/satır içi (varsayılan), `size-5` ikon kutusu, `size-3.5` küçük çip, `size-6` üstü yalnızca boş durum. Dekoratif ikon `aria-hidden`; tek başına ikon buton `aria-label` ya da `sr-only` metin taşır. Emoji ikon olarak kullanılmaz.
|
||||
|
||||
### 4.8 Özet kartı — `OzetKart`, `universite-icerik.tsx`
|
||||
Apple Fitness özet kartının sitenin ölçüleriyle hâli. Sayfada **en çok dört** tane (harita
|
||||
yanındaki 2×2).
|
||||
|
||||
```
|
||||
┌──────────────────────┐
|
||||
│ 13.889 [👥] │ ← sayı solda, iri; ikon kutusu sağda, küçük
|
||||
│ Kontenjan │ ← başlık text-base font-semibold text-slate-900
|
||||
│ 2026 toplamı │ ← açıklama text-sm text-slate-500, tek satır
|
||||
└──────────────────────┘
|
||||
```
|
||||
|
||||
- Kap: `flex flex-col rounded-2xl border border-slate-200 bg-white p-4 sm:p-5`.
|
||||
- Sayı satırı: `flex items-start justify-between gap-1.5`; sayı §3 "Özet kartı sayısı";
|
||||
ikon kutusu `flex size-8 shrink-0 items-center justify-center rounded-xl bg-primary/10 sm:size-10`.
|
||||
- `dl` anlamı için `dt` (başlık) DOM'da önce gelir, görsel sıra `order-*` ile kurulur.
|
||||
- Telefonda kart iç genişliği ~131 px: en uzun sayı ("13.889", 36 px'te ~90 px) + 32 px ikon
|
||||
sığar. Daha uzun sayı gelecekse önce ölç (§9.2), sonra yaz.
|
||||
- Tıklanabilir kart (ör. Türkiye sırası → sıralama sayfası): kartın tamamı link olur
|
||||
(`after:absolute after:inset-0` gerilmiş link), iç kaynak linki `relative z-10` ile üstte
|
||||
kalır; hover §2.1.
|
||||
- Açıklama altında veri görseli gerekiyorsa (ör. `PixelSira`) `ek` yuvasına konur.
|
||||
|
||||
### 4.9 Ayrıntı satırı — `AkademikAyrinti`, `akademik-ayrinti.tsx`
|
||||
Özet kartlarının altında "fazlası burada" satırı (Apple Sağlık'taki "Tüm verileri göster").
|
||||
|
||||
- `mt-4 flex w-full items-center gap-4 rounded-2xl border border-slate-200 bg-white p-4 sm:p-5`
|
||||
+ hover §2.1 + `active:scale-[0.99]`.
|
||||
- Solda ikon kutusu (`size-11`), ortada iki satır: başlık `font-heading text-lg font-bold`,
|
||||
altında **önizleme** `truncate text-sm text-slate-500` — içeride ne olduğunu söyler ("Hoca
|
||||
başına 25 öğrenci"), boş "Daha fazla" yazmaz. Sağda `ChevronRight size-5 text-slate-400`.
|
||||
- Dokununca alt sayfa açılır (§4.10).
|
||||
|
||||
### 4.10 Alt sayfa (Drawer) içeriği
|
||||
Ayrıntı her zaman **alttan açılan sayfada** (`Drawer handleOnly`, tutamaçlı); ekranın ortasında
|
||||
açılan modal değil. Çekmece kabı ve çarpı `cta-sira-form.tsx`'teki çekmeceyle birebir:
|
||||
`DrawerContent className="max-h-[92dvh] bg-white sm:mx-auto sm:max-w-2xl sm:border-x sm:border-slate-200 sm:shadow-2xl"`.
|
||||
|
||||
İçerik kuralları (Apple ayrıntı sayfası):
|
||||
- **Her ekranda ortalı:** başlık `px-10 text-center font-heading text-2xl font-bold`, altında
|
||||
bağlam (`text-sm text-slate-500`, ör. üniversite adı).
|
||||
- **Kart yok.** Çekmece zaten bir yüzey; içine çerçeveli kart koymak "pencere içinde pencere"
|
||||
olur. Gösterge kareleri (§4.11) bu kuralın istisnası değildir — onlar çerçevesiz, dolu renk.
|
||||
- **Süs ayraç yok.** Bölümler arasına piksel ayraç ya da çizgi konmaz; ayrım boşlukla ve
|
||||
yan yana dizilişle yapılır. Yalnız en alttaki kaynak satırı `border-t border-slate-100`.
|
||||
- **Tek ekrana sığar:** 375×812'de kaydırmasız. Dikine uzuyorsa öğeler yan yana dizilir,
|
||||
kareler kısaltılır, not kısaltılır — içerik silinmez (§9.2 ölçüm).
|
||||
- Sıra: gösterge kareleri → tek cümlelik not (`text-sm text-slate-500`) → ikincil buton (§4.1)
|
||||
→ kaynaklar (logo solda, `text-xs` iki satır).
|
||||
- **Arama motoru uyarısı:** çekmece içeriği sayfa açılırken HTML'de yoktur (kapalıyken
|
||||
çizilmez). Google'ın görmesi gereken metin ve **zorunlu atıflar** (ör. URAP sırası + payda +
|
||||
dönem + aktif bağlantı) çekmeceye taşınmaz, sayfada da görünür kalır.
|
||||
|
||||
### 4.11 Gösterge kareleri (masonry)
|
||||
Birden çok sayıyı alt sayfada göstermenin yolu. Logodaki üç kare gibi: **iki sütun, sağdaki
|
||||
aşağı kaymış, kare boyları farklı — ne tepeler ne bitişler hizalı.**
|
||||
|
||||
```
|
||||
┌────────┐
|
||||
│ Türkiye│ ┌────────┐
|
||||
│ sırası │ │ Hoca │
|
||||
│ 5. │ │ başına │
|
||||
│ İlk %3 │ │ 25 │
|
||||
└────────┘ └────────┘
|
||||
┌────────┐ ┌────────┐
|
||||
│ 56.128 │ │ 2.225 │
|
||||
└────────┘ │ │
|
||||
└────────┘
|
||||
```
|
||||
|
||||
- Kap: `flex w-full max-w-md items-start gap-3`; iki sütun `flex flex-1 flex-col gap-3`,
|
||||
sağdaki `mt-12`.
|
||||
- Kare: `flex flex-col items-center justify-center rounded-3xl px-3 text-white` + zemin (§2
|
||||
"Gösterge karesi") + `min-h-*` ile farklı boy (ör. 40 / 24 / 32 / doğal).
|
||||
- **Ölçüyle doğrula:** komşu kareler arasında tepe ve bitiş farkı **en az 24 px**. 8 px fark
|
||||
"yanlışlıkla hizalanmış" görünür (§9.2).
|
||||
- Etiket `text-sm font-bold text-white/90`; sayı §3 gösterge ölçeği; vurgu satırı
|
||||
`text-base font-bold`.
|
||||
- Renk sırası: en önemli gösterge turuncu, ikincisi mavi, sonra indigo, yeşil.
|
||||
|
||||
### 4.12 Kimlik hapları (tür, il)
|
||||
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`
|
||||
+ 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.
|
||||
|
||||
### 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üğü
|
||||
dilim turuncu yanar, geri kalanı hero dokusunda. **Veri taşıyan** piksel olduğu için süs
|
||||
ayraç yasağına girmez.
|
||||
- Açık zeminde: `<PixelSira sira={5} toplam={198} className="h-4" />` (8 hücre varsayılan).
|
||||
- Renkli dolu zeminde: `zemin="koyu"` — işaret beyaz yanar, ısı fırçası kapanır.
|
||||
- Anlamı her zaman yanındaki metin taşır ("5. / 198", "İlk %3"); şerit `aria-hidden`.
|
||||
|
||||
### 4.14 Üst çubuk başlık geçişi — `NavBaslik`, `nav-baslik.tsx`
|
||||
iOS'taki büyük başlık davranışı: sayfa h1'i üst çubuğun altına girince üst çubuktaki
|
||||
"KolayTercih" yazısı yukarı kayıp söner, yerine sayfanın logosu + adı gelir; dokununca sayfa
|
||||
başına döner. Katalog detay sayfalarında (üniversite, bölüm) zorunlu; yeni detay sayfası
|
||||
eklenince aynısı yapılır:
|
||||
|
||||
```tsx
|
||||
<NavBaslik baslikId="uni-baslik" ad={uni.ad} logo={<UniLogo slug={slug} ad={uni.ad} boy="xs" />} />
|
||||
<h1 id="uni-baslik" …>…</h1>
|
||||
```
|
||||
Logosu olmayan sayfada `logo` verilmez; üst çubukta **yalnız ad** görünür (bölüm sayfası —
|
||||
Bilal'in kararı: yerine ikon konmaz, üst çubukta süs ikona gerek yok). Logo sunucuda üretilip
|
||||
verilir (logo listesi istemciye inmez).
|
||||
Hareket: yay `bounce: 0, duration: 0.35`, giriş ve çıkış aynı yoldan; "hareketi azalt"ta yalnız
|
||||
opaklık.
|
||||
|
||||
### 4.15 Liste satırı (program/üniversite listeleri) — `universite-program-tablosu.tsx`
|
||||
- **Ad kesilmez, sarar:** `whitespace-normal` + `block text-base font-semibold leading-snug`.
|
||||
`truncate` ile "İngiliz Dili ve E…" yazılmaz.
|
||||
- Ad linkinin hover'ı **mavi**: `transition-colors hover:text-primary hover:underline`
|
||||
(§2.1 istisnası).
|
||||
- Telefonda yalnız ad + satır butonları sütun olarak kalır; sayısal sütunlar (taban sıra vb.)
|
||||
adın altındaki satıra iner: `mt-1.5 flex items-center gap-1.5 text-sm text-slate-500 sm:hidden`
|
||||
içinde değer `font-semibold text-slate-900`.
|
||||
- Tür çipi: `rounded-full bg-slate-100 px-2.5 py-0.5 text-xs font-semibold text-slate-700`.
|
||||
- Satır butonları varsayılan `size-11` (44 px); `size-8` gibi küçültme verilmez.
|
||||
- Tablo başlığı `h-12 text-sm font-semibold text-slate-500`; hücre dolgusu `py-4 pl-4 sm:pl-5`.
|
||||
|
||||
## 5. Yerleşim
|
||||
|
||||
### 5.1 Genel
|
||||
- Sayfa kabı: `mx-auto max-w-6xl px-4`. Metin sütunu: `max-w-xl` (paragraf) / `max-w-2xl`–`max-w-3xl` (uzun içerik).
|
||||
- Landing bölümü: `<section className="py-20 sm:py-24">`; alt sayfalar `py-12`–`py-16`.
|
||||
- Bölüm başlık bloğu (kanonik):
|
||||
@@ -137,45 +361,118 @@ Yalnızca `lucide-react`. Boyut: `size-4` buton/satır içi (varsayılan), `size
|
||||
- Alt sayfa başlığının altında `PagePixelDivider` (`mt-6`/`mt-8`), her sayfada **farklı seed**.
|
||||
- Landing bölümleri arasında çizgi yok; `PixelDivider` (`mx-auto w-56`, farklı seed).
|
||||
- Boşluk ölçeği: 4 → 6 → 8 → 12 → 16/20 → 24. Ara değer (`mt-7`, `gap-5`) yeni yerde açılmaz.
|
||||
- Mobil önce (trafiğin ~%90'ı 375 px): uzun üniversite/program adları `min-w-0` + sarma ya da `truncate`; tablo yatay kaydırmaz, kart olur; sabit alt çubuk klavyeyle çakışmaz; `vh` yerine `dvh`.
|
||||
- Mobil önce (trafiğin ~%90'ı 375 px): uzun adlar sarar (§4.15); tablo yatay kaydırmaz,
|
||||
dar ekranda sütunlar satır altına iner; sabit alt çubuk klavyeyle çakışmaz; `vh` yerine `dvh`.
|
||||
|
||||
## 6. Derinlik ve katman
|
||||
### 5.2 Geniş ekran dışında ortala
|
||||
Alt sayfaların (katalog, detay) başlık bloğu **`lg` (1024 px) altında ortalı**, `lg`'de sola
|
||||
yaslıdır — mobil uygulama gibi telefonda ve tablette her şey ortada, masaüstünde tablo ve
|
||||
kartlarla aynı hizada:
|
||||
- Yol satırı: `flex flex-wrap items-center justify-center gap-1.5 text-sm text-slate-500 lg:justify-start`
|
||||
- Logo + h1: `flex flex-col items-center gap-4 text-center lg:flex-row lg:text-left` — logo
|
||||
telefonda **başlığın üstünde, ortada**.
|
||||
- Kimlik hapları: `flex flex-wrap items-center justify-center gap-2 lg:justify-start`
|
||||
- Özet paragraf: `mx-auto … text-center lg:mx-0 lg:text-left`
|
||||
- Sayfa ayracı: `mx-auto … lg:mx-0`; liste bölümü ve grup başlıkları `text-center lg:text-left`.
|
||||
- **Ortalanmaz:** liste/tablo satırlarının içeriği (göz satırdan satıra kayarken ortalı liste
|
||||
yorar), form alanları, uzun rehber metni.
|
||||
- Alt sayfa (§4.10) içeriği her ekranda ortalıdır.
|
||||
|
||||
### 5.3 Verimli ve havadar
|
||||
- İlişkili öğeler yan yana dizilir (iki sütun) — tek sütunda alt alta uzayan panel yapılmaz.
|
||||
- Yan yana dizmek "hepsini tek şeride sıkıştırmak" değildir: 23 Eyl'de denenen yatay "bilgi
|
||||
şeridi" (harita dahil altı değer tek satırda, yana kayan) reddedildi — harita küçüldü,
|
||||
sayılar ezildi, ekran kalabalıklaştı.
|
||||
|
||||
## 6. Derinlik, katman ve hareket
|
||||
|
||||
Derinlik gölgeyle değil **piksel katmanı ve hareket farkıyla** verilir: başlık bloğu gövdeden hafif hızlı akar (`Parallax strength={16}` başlık, `7` içerik, `26` ayraç). Kartlar hover'da 4 px yükselir. Modal arka planı `bg-black/10` + hafif blur. Yığın: içerik < sabit bar < çekmece/modal (`z-50`).
|
||||
|
||||
Hareket kuralları (Apple, `motion/react`):
|
||||
- Varsayılan yay: `{ type: "spring", bounce: 0, duration: 0.35 }` — sekmesiz, sakin. Sekme
|
||||
yalnızca kullanıcı bir şeyi fırlattıysa (sürükle-bırak).
|
||||
- Giriş ve çıkış **aynı yoldan** (aşağıdan geldiyse aşağı gider).
|
||||
- Basınca anında tepki: `active:scale-[0.97]` (buton), `[0.99]` (geniş satır), `[0.96]` (çarpı).
|
||||
- Hover'da ok `translate-x-0.5` kayar; kart yükselmesi yalnız landing kartında.
|
||||
- Kaydırmaya bağlı tetik `IntersectionObserver` ile (kaydırma dinleyicisi değil).
|
||||
- "Hareketi azalt": kayma/yay yok, yalnız opaklık (`useReducedMotion`,
|
||||
`motion-reduce:transition-opacity`).
|
||||
|
||||
## 7. Yap / Yapma
|
||||
|
||||
| Yap | Yapma (AI izi) |
|
||||
|---|---|
|
||||
| Rozet için `SectionEyebrow` | `Badge` import'u, elle `rounded-full border … px-3.5 py-1.5` span |
|
||||
| CTA: metin + sondaki `ArrowRight` | Başta `Sparkles`/yıldız ikonu, "✨" |
|
||||
| Nötr = slate; vurgu = primary mavi; aksiyon = orange-500 | `indigo/purple` primary, mor→mavi gradyan, `gray/zinc` nötr |
|
||||
| Nötr = slate; vurgu = primary mavi; aksiyon ve hover = orange-500 | `indigo/purple` primary, mor→mavi gradyan, `gray/zinc` nötr |
|
||||
| Hover'da tek turuncu (`orange-500`); liste satırı adı mavi (§2.1 istisnası) | Liste dışında hover'da mavi; aynı ekranda `orange-600` yazı + `orange-300` kenarlık |
|
||||
| Kart: `rounded-2xl border-slate-200 bg-white shadow-sm` | Her şeyde `rounded-3xl`, camsı `backdrop-blur` kartlar, parlama gölgesi |
|
||||
| Hareket: `Reveal` (bölüm girişi), piksel ısısı, `kt-unblur`; `animate-ping` yalnızca "şu anda canlı" noktasında | Her elemanda `fade-up`, `whileHover scale 1.05`, `animate-bounce` |
|
||||
| Başlık `font-heading`, vurgu `font-bricolage` | Inter/Roboto/serif, tek kelimelik BÜYÜK HARF etiket |
|
||||
| Risk rengi + metin etiketi | Renk tek başına, "yerleşirsin" gibi vaat cümlesi |
|
||||
| Sayı kartın en iri öğesi, ikon sağda küçük | İkon üstte büyük, sayı küçük; BÜYÜK HARFLİ `text-xs` etiket |
|
||||
| Sayfada ≤4 özet sayı, fazlası ayrıntı satırı → alt sayfa | 6+ kartı sayfaya dizmek ya da hepsini yatay şeride sıkıştırmak |
|
||||
| Harita gibi nefes alanını olduğu gibi bırakmak | Nefes alanını küçültüp bilgi şeridine gömmek |
|
||||
| Alt sayfada kartsız, ortalı, tek ekrana sığan içerik | Çekmece içinde çerçeveli kart; bölümler arasında süs piksel ayraç |
|
||||
| Gösterge kareleri masonry (kaymış sütun, farklı boy) | Hizalı eşit ızgara; 8 px'lik "neredeyse hizalı" kaymalar |
|
||||
| Liste satırında ad sarar, sayı alt satıra iner | `truncate` ile yarım ad; 32 px satır butonu |
|
||||
| Hareket: `Reveal`, piksel ısısı, `kt-unblur`, sekmesiz yay; `animate-ping` yalnızca "şu anda canlı" noktasında | Her elemanda `fade-up`, `whileHover scale 1.05`, `animate-bounce` |
|
||||
| Başlık `font-heading`, vurgu `font-bricolage` | Inter/Roboto/serif, tek kelimelik BÜYÜK HARF etiket, 10–11 px yazı |
|
||||
| Risk rengi + metin etiketi; tür ayrımı ikonla | Renk tek başına, "yerleşirsin" gibi vaat cümlesi |
|
||||
| Üç kart yalnızca üç gerçek öğe varsa | Hero + üç kart iskeleti refleksi |
|
||||
| Beyaz zemin, tek koyu vurgu yüzeyi | İstenmemiş karanlık mod, `dark:` sınıfı |
|
||||
| Kapatma çarpısı `DialogContent` varsayılanı | Başka boyut/renkte çarpı |
|
||||
|
||||
## 8. Duyarlılık
|
||||
|
||||
- Kırılma noktaları: 375 (temel), 640 `sm:`, 768 `md:`, 1024 `lg:`. Her bileşen önce 375'te tasarlanır.
|
||||
- Kırılma noktaları: 375 (temel), 640 `sm:`, 768 `md:`, 1024 `lg:`. Her bileşen önce 375'te
|
||||
tasarlanır. Ortalı ↔ sola yaslı geçişi `lg`'dedir (§5.2).
|
||||
- Dokunma hedefi en az 44 px (`h-11`, `size-11`); satır içi küçük buton `h-9` yalnızca masaüstü tablo aksiyonlarında.
|
||||
- Başlık ölçeği bir kademe düşer (`text-3xl` → `sm:text-4xl`), gövde 16 px altına inmez.
|
||||
- Modal genişliği `max-w-[calc(100%-2rem)]`; çekmece (`Drawer`) mobilde alttan açılır.
|
||||
- `prefers-reduced-motion`: her animasyonun sade karşılığı var (`motion-reduce:animate-none`, `.kt-unblur-sade`).
|
||||
- Başlık ölçeği bir kademe düşer (`text-3xl` → `sm:text-5xl` alt sayfa h1), gövde 16 px altına inmez.
|
||||
- Modal genişliği `max-w-[calc(100%-2rem)]`; ayrıntı her zaman çekmecede (`Drawer`), alttan açılır.
|
||||
- Çekmece içeriği 375×812'de kaydırmasız sığar (§4.10).
|
||||
- `prefers-reduced-motion`: her animasyonun sade karşılığı var (`motion-reduce:animate-none`, `.kt-unblur-sade`, `useReducedMotion`).
|
||||
|
||||
## 9. Ajan için brief şablonu
|
||||
## 9. Ajan için brief şablonu ve iş akışı
|
||||
|
||||
Yeni bir UI işi istenince önce şu dört satır doldurulur, sonra kod yazılır:
|
||||
### 9.1 Brief
|
||||
Yeni bir UI işi istenince önce şu beş satır doldurulur, sonra kod yazılır:
|
||||
|
||||
```
|
||||
Amaç: Kim, hangi ekranda, ne yapacak? (ör. veli, /paket, fiyatı karşılaştırıp satın alacak)
|
||||
Referans: Sitedeki en yakın mevcut ekran/bileşen (dosya yolu). Yoksa "yok" yaz ve ana sayfa bölüm bloğunu al.
|
||||
Tek kaynak: Kullanılacak kanonik parçalar (SectionEyebrow, Button+ArrowRight, Card+PixelCorner, PagePixelDivider…)
|
||||
Referans: Sitedeki en yakın mevcut ekran/bileşen (dosya yolu). Yoksa üniversite sayfası.
|
||||
Tek kaynak: Kullanılacak kanonik parçalar (SectionEyebrow, Button+ArrowRight, OzetKart, AkademikAyrinti, NavBaslik…)
|
||||
Durumlar: boş / yükleniyor / hata / başarılı / girişsiz / kredisiz — hangileri var?
|
||||
His: Ekranın tek ana işi ne? Hangi 4 sayı önde, ne bir dokunuş derinde?
|
||||
```
|
||||
|
||||
"Modern", "temiz", "şık" gibi yön belirtmeyen kelimeler kullanılmaz; yön = bu dosya.
|
||||
İş bitince: `pnpm tasarim:denetim` sıfır hata + 375 px'te tek bakış (taşma, üst üste binme, dokunma hedefi).
|
||||
|
||||
### 9.2 Ölç, tahmin etme
|
||||
Bu dilde sorunların çoğu koddan görünmez (taşma, sığma, hizalanma). Tasarım işinde:
|
||||
- **Sığma:** metin genişliğini tarayıcıda ölç (gizli `span` + `getBoundingClientRect`);
|
||||
"sanırım sığar" ile yazma. Örnek: özet kartında "13.889" 36 px'te 95 px.
|
||||
- **Hizasızlık:** masonry'de karelerin `top`/`bottom` değerlerini ölç; komşu fark ≥ 24 px.
|
||||
- **Tek ekran:** çekmecede kaydırma kabının `scrollHeight` ≤ `clientHeight` (375×812).
|
||||
- **Genişlikler:** 375 / 768 / 1280 px'te bak; ortalı ↔ sola yaslı geçişini `lg`'de doğrula.
|
||||
- **Uç veri:** en uzun ad ("Adana Alparslan Türkeş Bilim ve Teknoloji Üniversitesi"), en
|
||||
büyük sayı, verisi eksik kurum (URAP'ta olmayan, istatistiği olmayan).
|
||||
|
||||
### 9.3 Tuzaklar (23 Eyl 2026'da yaşandı)
|
||||
- **Prettier'ı toplu çalıştırma.** Depo prettier düzeninde değil; bir dosyayı prettier'dan
|
||||
geçirmek yüzlerce satırlık biçim farkı üretir ve başkasının kaydedilmemiş işine karışır.
|
||||
Yalnız kendi yazdığın satırları elle düzenle.
|
||||
- **Geliştirme sunucusunda ilk tık.** Kod değişikliğinden hemen sonra sunucu derleyip sayfayı
|
||||
tazeleyebilir; o anda yapılan tıklama boşa gider. "Çekmece açılmıyor" demeden önce sayfa
|
||||
hazır olduktan sonra tekrar dene.
|
||||
- **Sayfada ikinci `<header>`.** Bölüm sayfasında içerik `<header>`'ı var; site üst çubuğunu
|
||||
`querySelector("header")` ile değil, bilinen bir öğenin `closest("header")`'ı ile bul.
|
||||
- **"use client" dosyasından sabit import etme.** Sunucu bileşeni o sabiti değer olarak
|
||||
değil istemci referansı olarak alır; paylaşılan sabit ayrı dosyada durur ya da elle yazılır.
|
||||
|
||||
### 9.4 Site taraması için denetim uyarıları
|
||||
`pnpm tasarim:denetim` bu dilin eski izlerini **uyarı** olarak listeler (lint'i kırmaz, dönüşüm
|
||||
listesidir): `minik-yazi` (10–11 px), `buyuk-harf-etiket` (uppercase + tracking-wide),
|
||||
`mavi-hover` (§2.1; liste satırı adı istisna), `iri-sayi-tabular` (Cal Sans + tabular-nums).
|
||||
Bir ekranı dönüştüren ajan o ekrandaki uyarıları sıfırlar ya da neden kaldığını yazar.
|
||||
|
||||
### 9.5 Bitirmeden
|
||||
`pnpm tasarim:denetim` sıfır hata (uyarılar raporda listelenir) + 375 px'te tek bakış (taşma,
|
||||
üst üste binme, dokunma hedefi, çekmece tek ekran) + §9.2 ölçümleri.
|
||||
|
||||
@@ -115,8 +115,12 @@ const KURALLAR: Kural[] = [
|
||||
ad: "yabanci-renk",
|
||||
seviye: "hata",
|
||||
mesaj: "Paletin dışında ton; primary mavi / orange-500 / slate / risk renkleri kullanılır (DESIGN.md §2)",
|
||||
// İstisna (DESIGN.md §2 "Gösterge karesi"): yalnız dolu bg-indigo-500 ve
|
||||
// bg-green-600 zeminleri serbest; başka ton/opaklık/özellik yine hata.
|
||||
bul: regexKurali(
|
||||
new RegExp(`\\b${RENK_ONEK}-(?:indigo|purple|fuchsia|pink|cyan|teal|lime|green|yellow)-\\d+`),
|
||||
new RegExp(
|
||||
`\\b${RENK_ONEK}-(?:indigo|purple|fuchsia|pink|cyan|teal|lime|green|yellow)-\\d+(?!\\d)(?!(?<=\\bbg-(?:indigo-500|green-600))(?![\\w/-]))`,
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -171,6 +175,30 @@ const KURALLAR: Kural[] = [
|
||||
muaf: /src\/features\/rehber\/components\/rehber-kapak\.tsx$/,
|
||||
bul: regexKurali(/\[#[0-9a-fA-F]{3,8}\]/),
|
||||
},
|
||||
{
|
||||
ad: "minik-yazi",
|
||||
seviye: "uyari",
|
||||
mesaj: "12 px altı yazı yok; en küçük text-xs, yalnız ince yazıda (DESIGN.md §3 alt sınırlar)",
|
||||
bul: regexKurali(/\btext-\[(?:[0-9]|1[01])px\]/),
|
||||
},
|
||||
{
|
||||
ad: "buyuk-harf-etiket",
|
||||
seviye: "uyari",
|
||||
mesaj: "Büyük harfli minik etiket yok; büyük-küçük harfli text-sm font-semibold (DESIGN.md §3 alt sınırlar)",
|
||||
bul: regexKurali(/\buppercase\b[^"'`\n]*\btracking-(?:wide|wider|widest)\b|\btracking-(?:wide|wider|widest)\b[^"'`\n]*\buppercase\b/),
|
||||
},
|
||||
{
|
||||
ad: "mavi-hover",
|
||||
seviye: "uyari",
|
||||
mesaj: "Hover rengi tek turuncu (orange-500); liste satırındaki ad linkiyse istisna, mavi kalır (DESIGN.md §2.1)",
|
||||
bul: regexKurali(/\b(?:group-)?hover:(?:text|border|shadow)-primary\b/),
|
||||
},
|
||||
{
|
||||
ad: "iri-sayi-tabular",
|
||||
seviye: "uyari",
|
||||
mesaj: "Cal Sans iri sayıda tabular-nums 'daktilo' gibi durur; yalnız font-sans tablo sütunlarında (DESIGN.md §3)",
|
||||
bul: regexKurali(/\bfont-heading\b[^"'`\n]*\btabular-nums\b|\btabular-nums\b[^"'`\n]*\bfont-heading\b/),
|
||||
},
|
||||
{
|
||||
ad: "emoji-ikon",
|
||||
seviye: "uyari",
|
||||
|
||||
@@ -298,6 +298,65 @@ export function PixelCorner({ className }: { className?: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bir sıranın yerini tek satır piksel şeridinde gösteren sessiz gösterge
|
||||
* (ör. URAP'ta 198 üniversite içinde 5.). Şerit soldan (1.) sağa
|
||||
* (sonuncu) dilimlere bölünür; sıranın düştüğü dilim hero'daki şehirler
|
||||
* gibi turuncu yanar, geri kalanı hero dokusunda durur ve imleç fırçasına
|
||||
* katılır. Dekoratiftir: anlamı yanındaki metin taşır (renk tek başına
|
||||
* anlam taşımaz), bu yüzden aria-hidden.
|
||||
*/
|
||||
export function PixelSira({
|
||||
sira,
|
||||
toplam,
|
||||
hucre = 8,
|
||||
zemin = "acik",
|
||||
className,
|
||||
}: {
|
||||
sira: number;
|
||||
toplam: number;
|
||||
hucre?: number;
|
||||
/** "koyu": renkli dolu zemin üstünde (ör. turuncu gösterge karesi) —
|
||||
* işaret beyaz yanar, doku zeminin üstünde beyaz kalır. */
|
||||
zemin?: "acik" | "koyu";
|
||||
className?: string;
|
||||
}) {
|
||||
const svgRef = useRef<SVGSVGElement>(null);
|
||||
// Isı fırçası turuncuya ısıtır; turuncu zeminde görünmez, orada kapalı.
|
||||
usePixelHeat(svgRef, { radius: zemin === "koyu" ? 0 : 28 });
|
||||
const yer = Math.min(
|
||||
hucre - 1,
|
||||
Math.max(0, Math.floor(((sira - 1) / toplam) * hucre)),
|
||||
);
|
||||
|
||||
return (
|
||||
<svg
|
||||
ref={svgRef}
|
||||
viewBox={`0 0 ${hucre * CELL} ${CELL}`}
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
className={cn("h-3.5 w-auto shrink-0 text-primary", className)}
|
||||
>
|
||||
{Array.from({ length: hucre }, (_, i) =>
|
||||
i === yer ? (
|
||||
<rect
|
||||
key={i}
|
||||
x={i * CELL + INSET}
|
||||
y={INSET}
|
||||
width={DOT}
|
||||
height={DOT}
|
||||
rx={2}
|
||||
className={zemin === "koyu" ? "fill-white" : "fill-orange-500"}
|
||||
fillOpacity={zemin === "koyu" ? 1 : 0.85}
|
||||
/>
|
||||
) : (
|
||||
<g key={i}>{fixedPixel(i * CELL, 0, opacityFor(i, 0) + 0.1)}</g>
|
||||
),
|
||||
)}
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
/** Bölüm başlıklarının üstünde duran piksel imzalı eyebrow rozeti. */
|
||||
export function SectionEyebrow({
|
||||
children,
|
||||
|
||||
@@ -10,15 +10,21 @@ export function SiteHeader() {
|
||||
<header className="sticky top-4 z-50 mt-4 sm:top-6 sm:mt-6 print:hidden">
|
||||
{/* lg altında 2 satır: logo+UserNav / sıralama+ara. lg+: tek satır, ortada nav. */}
|
||||
<div className="relative mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-x-2 gap-y-2 px-3 sm:gap-x-3 sm:px-4 lg:h-16 lg:flex-nowrap">
|
||||
{/* Sayfa başlığı yuvası (NavBaslik): bir sayfa <html>'e
|
||||
data-nav-baslik koyunca yazı logosu yukarı kayıp söner, yuvaya
|
||||
sayfanın başlığı (ör. üniversite logosu + adı) gelir. Yuva Link'in
|
||||
dışında: içine gelen düğme bağlantı içinde bağlantı olmasın.
|
||||
lg'de ortadaki nav hapına taşmasın diye genişliği sınırlı. */}
|
||||
<div className="relative order-1 flex min-w-0 flex-1 items-center lg:flex-none">
|
||||
<Link
|
||||
href="/"
|
||||
aria-label="KolayTercih"
|
||||
className="group order-1 flex min-w-0 shrink items-center font-bricolage"
|
||||
className="group flex min-w-0 shrink items-center font-bricolage"
|
||||
>
|
||||
{/* Inline animasyonlu mark: Link'teki `group` sayesinde hover'da
|
||||
kareler danışman panelindeki nabız animasyonuyla canlanır. */}
|
||||
<AnimasyonluMark sadeceHover className="size-8 sm:size-11" />
|
||||
<span className="truncate">
|
||||
<span className="truncate transition-[opacity,translate] duration-300 ease-out in-data-[nav-baslik]:pointer-events-none in-data-[nav-baslik]:-translate-y-2 in-data-[nav-baslik]:opacity-0 motion-reduce:transition-opacity">
|
||||
<span className="text-lg font-light tracking-tighter sm:text-3xl">
|
||||
Kolay
|
||||
</span>
|
||||
@@ -27,6 +33,11 @@ export function SiteHeader() {
|
||||
</span>
|
||||
</span>
|
||||
</Link>
|
||||
<div
|
||||
id="nav-baslik-yuvasi"
|
||||
className="pointer-events-none absolute inset-y-0 right-0 left-9 flex min-w-0 items-center sm:left-12 lg:right-auto lg:w-52 xl:w-72"
|
||||
/>
|
||||
</div>
|
||||
<KatalogArama rehberler={getRehberAramaKayitlari()} />
|
||||
{/* UserNav session+DB bekler; Suspense dışına taşarsa tüm kabuk bloklanır */}
|
||||
<Suspense fallback={<UserNavFallback />}>
|
||||
|
||||
72
src/features/katalog/components/akademik-ayrinti.tsx
Normal file
72
src/features/katalog/components/akademik-ayrinti.tsx
Normal file
@@ -0,0 +1,72 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import { ChevronRight, GraduationCap, X } from "lucide-react";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
DrawerContent,
|
||||
DrawerDescription,
|
||||
DrawerTitle,
|
||||
DrawerTrigger,
|
||||
} from "@/components/ui/drawer";
|
||||
|
||||
/**
|
||||
* Üniversite sayfasında özet kartlarının altındaki "Akademik göstergeler ›"
|
||||
* satırı (Apple Sağlık'taki "Tüm verileri göster" gibi): sık bakılan sayılar
|
||||
* kartlarda, ayrıntı bir dokunuş derinde. İkinci satır içeride ne olduğunu
|
||||
* önceden söyler. Ayrıntı alttan açılan çekmecede; çekmece ve kapatma
|
||||
* çarpısı cta-sira-form.tsx'teki çekmeceyle birebir aynı sınıf setinde.
|
||||
* Kartlar sunucuda üretilip `children` olarak gelir.
|
||||
*/
|
||||
export function AkademikAyrinti({
|
||||
universite,
|
||||
onizleme,
|
||||
children,
|
||||
}: {
|
||||
universite: string;
|
||||
onizleme: string;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<Drawer handleOnly>
|
||||
<DrawerTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
className="mt-4 flex w-full cursor-pointer items-center gap-4 group rounded-2xl border border-slate-200 bg-white p-4 text-left transition-[background-color,border-color,transform] duration-150 hover:border-orange-500 hover:bg-orange-50/60 active:scale-[0.99] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring sm:p-5"
|
||||
>
|
||||
<span className="flex size-11 shrink-0 items-center justify-center rounded-xl bg-primary/10">
|
||||
<GraduationCap className="size-5 text-primary" aria-hidden />
|
||||
</span>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block font-heading text-lg font-bold text-slate-900">
|
||||
Akademik göstergeler
|
||||
</span>
|
||||
<span className="block truncate text-sm text-slate-500">
|
||||
{onizleme}
|
||||
</span>
|
||||
</span>
|
||||
<ChevronRight
|
||||
className="size-5 shrink-0 text-slate-400 transition-[color,transform] duration-150 group-hover:translate-x-0.5 group-hover:text-orange-500"
|
||||
aria-hidden
|
||||
/>
|
||||
</button>
|
||||
</DrawerTrigger>
|
||||
<DrawerContent className="max-h-[92dvh] bg-white sm:mx-auto sm:max-w-2xl sm:border-x sm:border-slate-200 sm:shadow-2xl">
|
||||
<DrawerClose className="absolute top-3 right-3 z-10 flex size-9 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-popover text-slate-500 transition-[background-color,transform] duration-150 hover:bg-slate-100 hover:text-slate-800 active:scale-[0.96]">
|
||||
<X className="size-4" aria-hidden />
|
||||
<span className="sr-only">Kapat</span>
|
||||
</DrawerClose>
|
||||
<div className="min-h-0 flex-1 overflow-y-auto px-4 pt-5 pb-[max(1.25rem,env(safe-area-inset-bottom))] sm:px-6">
|
||||
<DrawerTitle className="px-10 text-center font-heading text-2xl font-bold text-slate-900">
|
||||
Akademik göstergeler
|
||||
</DrawerTitle>
|
||||
<DrawerDescription className="mt-1 px-10 text-center text-sm text-slate-500">
|
||||
{universite}
|
||||
</DrawerDescription>
|
||||
<div className="mt-5">{children}</div>
|
||||
</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from "./sayfalama";
|
||||
import { BolumProgramListesi } from "./bolum-program-listesi";
|
||||
import { VeriKaynagi } from "./veri-kaynagi";
|
||||
import { NavBaslik } from "./nav-baslik";
|
||||
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
|
||||
import { SiteFooter } from "@/components/site-footer";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
@@ -224,8 +225,12 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
|
||||
)}
|
||||
</nav>
|
||||
|
||||
{/* Başlık üst çubuğun altına girince üst çubukta bölüm adı belirir */}
|
||||
<NavBaslik baslikId="bolum-baslik" ad={bolum.ad} />
|
||||
<header className="border-b pb-7 sm:pb-9">
|
||||
<h1 className="max-w-3xl font-heading text-3xl font-bold tracking-tight text-foreground sm:text-5xl">
|
||||
<h1
|
||||
id="bolum-baslik"
|
||||
className="max-w-3xl font-heading text-3xl font-bold tracking-tight text-foreground sm:text-5xl">
|
||||
{bolum.ad}
|
||||
{ilkSayfa ? "" : ` — Sayfa ${sayfa}`}
|
||||
{!ilkSayfa && aralik ? (
|
||||
|
||||
116
src/features/katalog/components/nav-baslik.tsx
Normal file
116
src/features/katalog/components/nav-baslik.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
type ReactNode,
|
||||
useEffect,
|
||||
useState,
|
||||
useSyncExternalStore,
|
||||
} from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
|
||||
|
||||
/** SiteHeader'daki yuva; logo yazısının hemen sağında. */
|
||||
export const NAV_BASLIK_YUVASI = "nav-baslik-yuvasi";
|
||||
|
||||
const subscribe = () => () => {};
|
||||
|
||||
/**
|
||||
* iOS'taki büyük başlık davranışı: sayfa başlığı (h1) üst çubuğun altına
|
||||
* girince üst çubukta "KolayTercih" yazısı yukarı kayıp söner (SiteHeader'da
|
||||
* `in-data-[nav-baslik]` ile), yerine üniversitenin logosu ve adı gelir.
|
||||
* Dokununca sayfanın başına döner. Tetik IntersectionObserver'la: kaydırma
|
||||
* dinleyicisi yok, üst sınır üst çubuğun alt kenarı.
|
||||
*
|
||||
* Hareket Apple'ın varsayılanı: sekmesiz yay (bounce 0, 0,35 sn), giriş ve
|
||||
* çıkış aynı yoldan. "Hareketi azalt" açıksa yalnız opaklık değişir.
|
||||
* Logo sunucuda üretilip `logo` olarak gelir (logo listesi istemciye inmez);
|
||||
* logosu olmayan sayfada verilmez, yalnız ad görünür.
|
||||
*/
|
||||
export function NavBaslik({
|
||||
baslikId,
|
||||
ad,
|
||||
logo,
|
||||
}: {
|
||||
baslikId: string;
|
||||
ad: string;
|
||||
/** Verilmezse yalnız ad görünür (ör. bölüm sayfası). */
|
||||
logo?: ReactNode;
|
||||
}) {
|
||||
const [goster, setGoster] = useState(false);
|
||||
const azHareket = useReducedMotion();
|
||||
const yuva = useSyncExternalStore(
|
||||
subscribe,
|
||||
() => document.getElementById(NAV_BASLIK_YUVASI),
|
||||
() => null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const baslik = document.getElementById(baslikId);
|
||||
// Sayfada başka <header> olabilir (bölüm sayfası): yuvanın kendi
|
||||
// üst çubuğu alınır.
|
||||
const ustCubuk = document
|
||||
.getElementById(NAV_BASLIK_YUVASI)
|
||||
?.closest("header");
|
||||
if (!baslik || !ustCubuk) return;
|
||||
|
||||
let gozlemci: IntersectionObserver | null = null;
|
||||
const kur = () => {
|
||||
gozlemci?.disconnect();
|
||||
const alt = Math.round(ustCubuk.getBoundingClientRect().bottom);
|
||||
gozlemci = new IntersectionObserver(
|
||||
([giris]) => {
|
||||
// Görünmüyor ve üst çubuğun üstünde kaldıysa (aşağıda değil)
|
||||
setGoster(
|
||||
!giris.isIntersecting && giris.boundingClientRect.top < alt,
|
||||
);
|
||||
},
|
||||
{ rootMargin: `-${alt}px 0px 0px 0px` },
|
||||
);
|
||||
gozlemci.observe(baslik);
|
||||
};
|
||||
kur();
|
||||
window.addEventListener("resize", kur);
|
||||
return () => {
|
||||
window.removeEventListener("resize", kur);
|
||||
gozlemci?.disconnect();
|
||||
};
|
||||
}, [baslikId]);
|
||||
|
||||
useEffect(() => {
|
||||
const kok = document.documentElement;
|
||||
if (goster) kok.dataset.navBaslik = "";
|
||||
else delete kok.dataset.navBaslik;
|
||||
return () => {
|
||||
delete kok.dataset.navBaslik;
|
||||
};
|
||||
}, [goster]);
|
||||
|
||||
if (!yuva) return null;
|
||||
|
||||
const kayma = azHareket ? 0 : 10;
|
||||
return createPortal(
|
||||
<AnimatePresence>
|
||||
{goster ? (
|
||||
<motion.button
|
||||
key="nav-baslik"
|
||||
type="button"
|
||||
onClick={() =>
|
||||
window.scrollTo({ top: 0, behavior: azHareket ? "auto" : "smooth" })
|
||||
}
|
||||
aria-label={`${ad} — sayfanın başına dön`}
|
||||
initial={{ opacity: 0, y: kayma }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: kayma }}
|
||||
transition={{ type: "spring", bounce: 0, duration: 0.35 }}
|
||||
className="pointer-events-auto flex min-w-0 max-w-full cursor-pointer items-center gap-2 rounded-full py-1 pr-2 pl-1 transition-colors duration-150 hover:text-orange-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring active:scale-[0.97]"
|
||||
>
|
||||
{logo ?? null}
|
||||
<span className="truncate font-bricolage text-base font-semibold tracking-tight sm:text-xl">
|
||||
{ad}
|
||||
</span>
|
||||
</motion.button>
|
||||
) : null}
|
||||
</AnimatePresence>,
|
||||
yuva,
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,19 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { notFound } from "next/navigation";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import {
|
||||
ArrowRight,
|
||||
Building2,
|
||||
ChevronRight,
|
||||
Landmark,
|
||||
LayoutList,
|
||||
Medal,
|
||||
MapPin,
|
||||
Target,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import type { ReactNode } from "react";
|
||||
import {
|
||||
getAllUniversiteler,
|
||||
getAllUniversiteSlugs,
|
||||
@@ -26,17 +38,23 @@ import {
|
||||
sayfaDilimi,
|
||||
siraYiliEtiketi,
|
||||
} from "./sayfalama";
|
||||
import { PagePixelDivider } from "@/components/pixel-decor";
|
||||
import { PagePixelDivider, PixelSira } from "@/components/pixel-decor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { UniversiteKonumHaritasi } from "./universite-konum-haritasi";
|
||||
import { uniUlkeKonum } from "@/lib/harita-ulkeler";
|
||||
import { UniversiteProgramTablosu } from "./universite-program-tablosu";
|
||||
import { VeriKaynagi } from "./veri-kaynagi";
|
||||
import { URAP_DONEM, URAP_KAYNAK_URL, URAP_TOPLAM } from "@/lib/urap-siralama";
|
||||
import {
|
||||
URAP_DONEM,
|
||||
URAP_KAYNAK_URL,
|
||||
URAP_TOPLAM,
|
||||
} from "@/lib/urap-siralama";
|
||||
YOK_ISTATISTIK_DONEM,
|
||||
YOK_ISTATISTIK_KAYNAK_URL,
|
||||
OGRENCI_BASINA_UYE_MEDYAN,
|
||||
ogrenciBasinaUye,
|
||||
} from "@/lib/yok-istatistik";
|
||||
import { UniLogo } from "@/components/uni-logo";
|
||||
import { UrapLogo } from "./urap-logo";
|
||||
import { AkademikAyrinti } from "./akademik-ayrinti";
|
||||
import { NavBaslik } from "./nav-baslik";
|
||||
import { uniLogoYolu } from "@/lib/uni-logolar";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import type { Program } from "@/types/yokatlas";
|
||||
@@ -124,8 +142,8 @@ function fakulteOzeti({
|
||||
* Atıf pazarlık dışı: URAP'ın izni "urapcenter.org adresine aktif bağlantı"
|
||||
* şartına bağlı, o yüzden gerçek <a href>, yeni sekme, rel="noopener" ve
|
||||
* nofollow YOK. Sıra payda ("198 üniversite arasında") ve dönem olmadan
|
||||
* gösterilmez. URAP listesinde olmayan üniversitede sayı yoktur; eksiklik
|
||||
* gizlenmez, nedeni tek cümleyle yazılır.
|
||||
* gösterilmez. Sıra ve atıf artık AkademikGostergeler kartında; bu satır
|
||||
* yalnız URAP listesinde olmayan üniversitede nedeni tek cümleyle yazar.
|
||||
*/
|
||||
function UrapKaynakSatiri({ uni }: { uni: UniDetay }) {
|
||||
if (!uni.urap) {
|
||||
@@ -139,19 +157,289 @@ function UrapKaynakSatiri({ uni }: { uni: UniDetay }) {
|
||||
</p>
|
||||
);
|
||||
}
|
||||
// Sıra ve zorunlu atıf AkademikGostergeler kartında.
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Akademik göstergeler: URAP sırası + YÖK İstatistik öğrenci/öğretim üyesi.
|
||||
* Sayfada değil, özet kartlarının altındaki "Akademik göstergeler ›"
|
||||
* satırının açtığı alt sayfada (AkademikAyrinti) durur; sıra sayısı ayrıca
|
||||
* dördüncü özet kartında.
|
||||
*
|
||||
* Apple ayrıntı sayfası düzeni (Hava Durumu gibi): kart ve süs ayraç yok,
|
||||
* her şey ortalı; iki dev sayı yan yana ince çizgiyle ayrılır, altında ham
|
||||
* sayılar, tek satır not, buton ve en altta ince yazıyla kaynaklar. Alt
|
||||
* sayfa ekranı doldurmasın diye dikey değil yan yana dizilir.
|
||||
*
|
||||
* URAP: sıra payda ve dönem olmadan gösterilmez; sıranın yeri piksel
|
||||
* şeridinde (PixelSira) turuncu yanar. Atıf pazarlık dışı — URAP'ın izni
|
||||
* urapcenter.org'a aktif bağlantı şartına bağlı: gerçek <a href>, yeni
|
||||
* sekme, rel="noopener", nofollow YOK. Oran kıyası sayfası olan
|
||||
* üniversitelerin medyanıyla yapılır; renk kullanılmaz.
|
||||
*/
|
||||
function AkademikGostergeler({ uni }: { uni: UniDetay }) {
|
||||
const { urap, istatistik: i } = uni;
|
||||
const oran = i ? ogrenciBasinaUye(i) : null;
|
||||
const oranVar = i != null && oran != null;
|
||||
if (!urap && !oranVar) return null;
|
||||
const dilim = urap ? Math.ceil((urap.sira / URAP_TOPLAM) * 100) : 0;
|
||||
const kaynakLinki =
|
||||
"font-medium text-primary transition-colors hover:text-orange-500 hover:underline";
|
||||
// Dört gösterge dört canlı karede, logodaki kareler gibi yerleşir: iki
|
||||
// sütun, sağdaki aşağı kaymış, kare boyları farklı — ne tepeler ne
|
||||
// bitişler hizalı (masonry). Renk sempati katar, anlamı etiket taşır;
|
||||
// yazı beyaz (turuncu/mavi zeminde sitenin CTA'larıyla aynı ikili).
|
||||
const kare =
|
||||
"flex flex-col items-center justify-center rounded-3xl px-3 text-white";
|
||||
const etiket = "text-sm font-bold text-white/90";
|
||||
const devSayi =
|
||||
"font-heading text-5xl font-bold leading-none tracking-tight";
|
||||
const orta = "font-heading text-3xl font-bold leading-none";
|
||||
|
||||
const siraKaresi = urap ? (
|
||||
<div className={`${kare} min-h-40 bg-orange-500 py-4`}>
|
||||
<dt className={etiket}>Türkiye sırası</dt>
|
||||
<dd className="mt-3 flex items-baseline justify-center gap-1">
|
||||
<span className={devSayi}>{sayi(urap.sira)}.</span>
|
||||
<span className="text-base font-bold text-white/80">
|
||||
/ {sayi(URAP_TOPLAM)}
|
||||
</span>
|
||||
</dd>
|
||||
<dd className="mt-2 text-base font-bold">
|
||||
{dilim <= 50 ? `İlk %${dilim}` : `${URAP_DONEM} listesi`}
|
||||
</dd>
|
||||
<dd className="mt-3 text-white">
|
||||
<PixelSira
|
||||
sira={urap.sira}
|
||||
toplam={URAP_TOPLAM}
|
||||
hucre={10}
|
||||
zemin="koyu"
|
||||
className="h-3"
|
||||
/>
|
||||
</dd>
|
||||
</div>
|
||||
) : null;
|
||||
const ogrenciKaresi = oranVar ? (
|
||||
<div className={`${kare} min-h-24 bg-indigo-500 py-3`}>
|
||||
<dd className={orta}>{sayi(i.ogrenci)}</dd>
|
||||
<dt className={`mt-1.5 ${etiket}`}>Örgün öğrenci</dt>
|
||||
</div>
|
||||
) : null;
|
||||
const oranKaresi = oranVar ? (
|
||||
<div className={`${kare} min-h-32 bg-primary py-4`}>
|
||||
<dt className={etiket}>Hoca başına öğrenci</dt>
|
||||
<dd className={`mt-3 ${devSayi}`}>{sayi(oran)}</dd>
|
||||
<dd className="mt-2 text-base font-bold text-white/90">
|
||||
Türkiye: {sayi(OGRENCI_BASINA_UYE_MEDYAN)}
|
||||
</dd>
|
||||
</div>
|
||||
) : null;
|
||||
const uyeKaresi = oranVar ? (
|
||||
<div className={`${kare} bg-green-600 py-5`}>
|
||||
<dd className={orta}>{sayi(i.ogretimUyesi)}</dd>
|
||||
<dt className={`mt-1.5 ${etiket}`}>Öğretim üyesi</dt>
|
||||
</div>
|
||||
) : null;
|
||||
const ikiSutun = urap != null && oranVar;
|
||||
|
||||
return (
|
||||
<p className="mt-2 text-xs leading-5 text-slate-500">
|
||||
URAP Türkiye Sıralaması {URAP_DONEM} ({sayi(URAP_TOPLAM)} üniversite
|
||||
arasında) — akademik yayın ve araştırma performansını ölçer; eğitim
|
||||
kalitesini ya da iş bulma oranını ölçmez. Kaynak:{" "}
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<dl className="flex w-full max-w-md items-start gap-3">
|
||||
<div className="flex flex-1 flex-col gap-3">
|
||||
{siraKaresi}
|
||||
{ogrenciKaresi}
|
||||
</div>
|
||||
{ikiSutun || (!urap && oranVar) ? (
|
||||
<div className={`flex flex-1 flex-col gap-3 ${ikiSutun ? "mt-12" : ""}`}>
|
||||
{oranKaresi}
|
||||
{uyeKaresi}
|
||||
</div>
|
||||
) : null}
|
||||
</dl>
|
||||
|
||||
{urap ? (
|
||||
<>
|
||||
<p className="mt-4 max-w-md text-sm leading-relaxed text-slate-500">
|
||||
Türkiye sırası araştırma performansını ölçer; eğitim kalitesini
|
||||
ya da iş bulma oranını ölçmez.
|
||||
</p>
|
||||
<Button
|
||||
asChild
|
||||
size="lg"
|
||||
variant="outline"
|
||||
className="mt-4 cursor-pointer px-6 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]"
|
||||
>
|
||||
<Link href="/universite-siralamasi">
|
||||
Tüm sıralamayı gör
|
||||
<ArrowRight className="size-4" aria-hidden />
|
||||
</Link>
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<div className="mt-5 flex w-full max-w-md items-center justify-center gap-3 border-t border-slate-100 pt-4 text-left text-xs leading-5 text-slate-500">
|
||||
{urap ? <UrapLogo className="h-7 shrink-0" /> : null}
|
||||
<div>
|
||||
{urap ? (
|
||||
<p>
|
||||
URAP {URAP_DONEM} ·{" "}
|
||||
<a
|
||||
href={URAP_KAYNAK_URL}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className="font-medium text-primary hover:underline"
|
||||
className={kaynakLinki}
|
||||
>
|
||||
urapcenter.org
|
||||
</a>
|
||||
</p>
|
||||
) : null}
|
||||
{i ? (
|
||||
<p>
|
||||
YÖK {YOK_ISTATISTIK_DONEM} ·{" "}
|
||||
<a
|
||||
href={YOK_ISTATISTIK_KAYNAK_URL}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className={kaynakLinki}
|
||||
>
|
||||
istatistik.yok.gov.tr
|
||||
</a>
|
||||
</p>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Özet kartı (Apple Fitness özet kartlarının düzeni, sitenin ölçüleriyle):
|
||||
* kartın en baskın öğesi iri sayı; ikonu sağında, küçük ikon kutusunda
|
||||
* (DESIGN.md §4.4 ikon kutusunun küçük hali). Altında başlık ve tek satır
|
||||
* açıklama. Telefonda kart iç genişliği ~131 px: en uzun sayı ("13.889",
|
||||
* text-4xl ≈ 90 px) + size-8 ikon kutusu sığar; sm'den itibaren text-5xl.
|
||||
* Harita yanındaki dört hücrenin tek kaynağı. `ek` verilirse açıklamanın
|
||||
* altında durur (ör. Türkiye sırasındaki piksel şeridi). dl anlamı için dt
|
||||
* DOM'da önce gelir, görsel sıra `order` ile kurulur.
|
||||
*/
|
||||
function OzetKart({
|
||||
ikon: Ikon,
|
||||
etiket,
|
||||
deger,
|
||||
birim,
|
||||
alt,
|
||||
ek,
|
||||
className,
|
||||
}: {
|
||||
ikon: LucideIcon;
|
||||
etiket: string;
|
||||
deger: ReactNode;
|
||||
birim?: string;
|
||||
alt: ReactNode;
|
||||
ek?: ReactNode;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col rounded-2xl border border-slate-200 bg-white p-4 sm:p-5 ${className ?? ""}`}
|
||||
>
|
||||
<dt className="order-2 mt-2 text-base font-semibold text-slate-900">
|
||||
{etiket}
|
||||
</dt>
|
||||
<dd className="order-1 flex items-start justify-between gap-1.5">
|
||||
<span className="flex min-w-0 flex-wrap items-baseline gap-x-1">
|
||||
<span className="font-heading text-4xl font-bold leading-none tracking-tight text-slate-900 sm:text-5xl">
|
||||
{deger}
|
||||
</span>
|
||||
{birim ? (
|
||||
<span className="text-base font-semibold text-slate-400">
|
||||
{birim}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
<span className="flex size-8 shrink-0 items-center justify-center rounded-xl bg-primary/10 sm:size-10">
|
||||
<Ikon className="size-4 text-primary sm:size-5" aria-hidden />
|
||||
</span>
|
||||
</dd>
|
||||
<dd className="order-3 mt-0.5 text-sm text-slate-500">{alt}</dd>
|
||||
{ek ? <dd className="order-4 mt-3">{ek}</dd> : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dördüncü özet kartı: URAP Türkiye sırası. Açıklamanın altında sıranın
|
||||
* yerini gösteren piksel şeridi (PixelSira) durur. Kartın tamamı sıralama
|
||||
* sayfasına gider (after: ile gerilmiş link); hover kenarlığı bunu belli
|
||||
* eder. Atıf pazarlık dışı: sıra payda ve dönem olmadan gösterilmez;
|
||||
* urapcenter.org'a aktif bağlantı (yeni sekme, rel="noopener", nofollow YOK)
|
||||
* z-10 ile kart linkinin üstünde kalır.
|
||||
*/
|
||||
function TurkiyeSirasiKarti({ sira }: { sira: number }) {
|
||||
const dilim = Math.ceil((sira / URAP_TOPLAM) * 100);
|
||||
return (
|
||||
<OzetKart
|
||||
className="group relative transition-[background-color,border-color] duration-150 hover:border-orange-500 hover:bg-orange-50/60"
|
||||
ikon={Medal}
|
||||
ek={<PixelSira sira={sira} toplam={URAP_TOPLAM} className="h-3" />}
|
||||
etiket="Türkiye sırası"
|
||||
deger={
|
||||
<Link
|
||||
href="/universite-siralamasi"
|
||||
aria-label={`Türkiye sıralamasında ${sayi(sira)}. — tüm üniversite sıralamasını gör`}
|
||||
className="transition-colors after:absolute after:inset-0 after:rounded-2xl group-hover:text-orange-500 focus-visible:outline-none focus-visible:after:ring-2 focus-visible:after:ring-primary"
|
||||
>
|
||||
{sayi(sira)}.
|
||||
</Link>
|
||||
}
|
||||
birim={`/ ${sayi(URAP_TOPLAM)}`}
|
||||
alt={
|
||||
<>
|
||||
{dilim <= 50 ? (
|
||||
<span className="block font-semibold text-slate-700">
|
||||
İlk %{dilim}
|
||||
</span>
|
||||
) : null}
|
||||
<a
|
||||
href={URAP_KAYNAK_URL}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className="relative z-10 font-medium text-primary transition-colors hover:text-orange-500 hover:underline"
|
||||
>
|
||||
URAP {URAP_DONEM}
|
||||
</a>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
/** "Akademik göstergeler ›" satırının ikinci satırı: içeride ne var. */
|
||||
function akademikOnizleme(uni: UniDetay): string {
|
||||
const oran = uni.istatistik ? ogrenciBasinaUye(uni.istatistik) : null;
|
||||
if (oran != null) return `Hoca başına ${sayi(oran)} öğrenci`;
|
||||
return "Türkiye sıralaması ayrıntısı";
|
||||
}
|
||||
|
||||
/** YÖK İstatistik'in ayrı kaynak satırı — dönemi YÖK Atlas'tan ve URAP'tan farklı. */
|
||||
function IstatistikKaynakSatiri({ uni }: { uni: UniDetay }) {
|
||||
if (!uni.istatistik) return null;
|
||||
return (
|
||||
<p className="mt-2 text-xs leading-5 text-slate-500">
|
||||
Öğrenci ve öğretim üyesi sayıları: YÖK Yükseköğretim İstatistikleri{" "}
|
||||
{YOK_ISTATISTIK_DONEM}. Örgün öğrenci birinci ve ikinci öğretimdeki tüm
|
||||
düzeyleri (ön lisans–doktora) kapsar, uzaktan ve açık öğretim dahil
|
||||
değildir; öğretim üyesi profesör, doçent ve doktor öğretim üyesidir.
|
||||
Kaynak:{" "}
|
||||
<a
|
||||
href={YOK_ISTATISTIK_KAYNAK_URL}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
className="font-medium text-primary transition-colors hover:text-orange-500 hover:underline"
|
||||
>
|
||||
istatistik.yok.gov.tr
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
);
|
||||
@@ -231,7 +519,7 @@ export function UniversiteIcerik({
|
||||
// özet paragrafının hemen altında, tablonun (ve ilk sayfadaki istatistik
|
||||
// ızgarasının) üstünde durur. Alttaki kart yerinde kalır.
|
||||
const ustSiraSatiri = (
|
||||
<div className="mt-6">
|
||||
<div className="mt-8">
|
||||
<CtaSiraForm varyant="kompakt" konum="ust" baslik={ctaBaslik} />
|
||||
</div>
|
||||
);
|
||||
@@ -276,71 +564,88 @@ export function UniversiteIcerik({
|
||||
|
||||
<nav
|
||||
aria-label="breadcrumb"
|
||||
className="flex items-center gap-1 text-xs text-slate-500"
|
||||
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"
|
||||
>
|
||||
<Link href="/" className="hover:text-primary hover:underline">
|
||||
Ana Sayfa
|
||||
</Link>
|
||||
<ChevronRight className="size-3" aria-hidden />
|
||||
<ChevronRight className="size-3.5 shrink-0" aria-hidden />
|
||||
<Link
|
||||
href="/universiteler"
|
||||
className="hover:text-primary hover:underline"
|
||||
className="transition-colors hover:text-orange-500 hover:underline"
|
||||
>
|
||||
Üniversiteler
|
||||
</Link>
|
||||
<ChevronRight className="size-3" aria-hidden />
|
||||
<ChevronRight className="size-3.5 shrink-0" aria-hidden />
|
||||
{ilkSayfa ? (
|
||||
<span className="text-slate-700">{uni.ad}</span>
|
||||
) : (
|
||||
<>
|
||||
<Link
|
||||
href={`/universite/${slug}`}
|
||||
className="hover:text-primary hover:underline"
|
||||
className="transition-colors hover:text-orange-500 hover:underline"
|
||||
>
|
||||
{uni.ad}
|
||||
</Link>
|
||||
<ChevronRight className="size-3" aria-hidden />
|
||||
<ChevronRight className="size-3.5 shrink-0" aria-hidden />
|
||||
<span className="text-slate-700">Sayfa {sayfa}</span>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
<div className="mt-4 flex items-center gap-4">
|
||||
{/* Başlık üst çubuğun altına girince üst çubukta logo + ad belirir */}
|
||||
<NavBaslik
|
||||
baslikId="uni-baslik"
|
||||
ad={uni.ad}
|
||||
logo={<UniLogo slug={slug} ad={uni.ad} boy="xs" />}
|
||||
/>
|
||||
{/* Geniş ekran (lg) altında başlık bloğu ortalı: logo başlığın
|
||||
üstünde, yol satırı, çipler ve özet paragraf ortada. */}
|
||||
<div className="mt-6 flex flex-col items-center gap-4 text-center lg:flex-row lg:text-left">
|
||||
<UniLogo slug={slug} ad={uni.ad} boy="lg" />
|
||||
<h1 className="font-heading text-3xl font-bold text-slate-900 sm:text-4xl">
|
||||
<h1
|
||||
id="uni-baslik"
|
||||
className="font-heading text-3xl font-bold leading-tight text-slate-900 sm:text-5xl"
|
||||
>
|
||||
{uni.ad} Taban Puanları {BASLIK_YILI}
|
||||
{ilkSayfa ? "" : ` — Sayfa ${sayfa}`}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Kurumun kimliği (tür + il): ızgaradaki kartlar sayısal ve
|
||||
karşılaştırılabilir metriklerdir, tür ise kategoridir — sitenin
|
||||
geri kalanında da başlığın altında çip olarak duruyor. Çipin sınıf
|
||||
seti program-tablosu.tsx:329-333'ten birebir kopyadır (AGENTS.md
|
||||
"ortak elemanlar tek kaynaktan"); yeni varyant üretilmez.
|
||||
Sayfalama sayfalarında da yazar: ziyaretçi Google'dan doğrudan
|
||||
/sayfa/N'ye inebiliyor, kimlik orada da aynı. `unitur` NULL ise
|
||||
satırın tamamı düşer, kopuk boşluk kalmaz. */}
|
||||
karşılaştırılabilir metriklerdir, tür ise kategoridir — başlığın
|
||||
altında iki iri hap olarak durur (navbar'daki hap butonların
|
||||
ölçüsü: h-10, tam yuvarlak, ikon + tek kelime). Tür renkle değil
|
||||
ikon + metinle ayrılır. Sayfalama sayfalarında da yazar:
|
||||
ziyaretçi Google'dan doğrudan /sayfa/N'ye inebiliyor. `unitur`
|
||||
NULL ise tür hapı düşer, kopuk boşluk kalmaz. */}
|
||||
{turEtiketi || uni.il ? (
|
||||
<p className="mt-5 flex flex-wrap items-center justify-center gap-2 lg:justify-start">
|
||||
{turEtiketi ? (
|
||||
<p className="mt-3 flex min-w-0 items-center gap-1.5 text-xs text-slate-500">
|
||||
<span
|
||||
className={`shrink-0 rounded px-1 py-px text-[10px] font-medium leading-4 ${
|
||||
uniturDevletMi(uni.unitur)
|
||||
? "bg-slate-100 text-slate-600"
|
||||
: "bg-violet-50 text-violet-600"
|
||||
}`}
|
||||
>
|
||||
<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">
|
||||
{uniturDevletMi(uni.unitur) ? (
|
||||
<Landmark className="size-4 text-primary" aria-hidden />
|
||||
) : (
|
||||
<Building2 className="size-4 text-primary" aria-hidden />
|
||||
)}
|
||||
{turEtiketi.cip}
|
||||
</span>
|
||||
) : null}
|
||||
{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">
|
||||
<MapPin className="size-4 shrink-0 text-primary" aria-hidden />
|
||||
<span className="truncate">{trBaslikDuzeni(uni.il)}</span>
|
||||
</span>
|
||||
) : null}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{ilkSayfa ? (
|
||||
<>
|
||||
<p className="mt-4 max-w-3xl text-sm leading-7 text-slate-600">
|
||||
<p className="mx-auto mt-6 max-w-3xl text-center text-base leading-relaxed text-slate-600 sm:text-lg lg:mx-0 lg:text-left">
|
||||
{uni.ad}
|
||||
{uni.il ? ` (${trBaslikDuzeni(uni.il)})` : ""}
|
||||
{turCumlesi ? `, ${turCumlesi};` : ","} {SON_YIL} YKS yerleştirme
|
||||
@@ -370,82 +675,68 @@ export function UniversiteIcerik({
|
||||
|
||||
{ustSiraSatiri}
|
||||
|
||||
<div className="mt-6 grid grid-cols-2 gap-3 lg:grid-cols-[1fr_1fr_1.35fr] lg:grid-rows-2">
|
||||
<div className="mt-8 grid grid-cols-2 gap-4 lg:grid-cols-[1fr_1fr_1.35fr] lg:grid-rows-2">
|
||||
<UniversiteKonumHaritasi universite={uni.ad} il={uni.il} />
|
||||
<dl className="contents">
|
||||
<div className="rounded-xl border border-slate-200 bg-white p-4">
|
||||
<dt className="text-xs font-medium uppercase tracking-wide text-slate-500">
|
||||
Program
|
||||
</dt>
|
||||
<dd className="mt-1 font-heading text-2xl font-bold text-slate-900">
|
||||
{sayi(uni.programSayisi)}
|
||||
</dd>
|
||||
</div>
|
||||
<div className="rounded-xl border border-slate-200 bg-white p-4">
|
||||
<dt className="text-xs font-medium uppercase tracking-wide text-slate-500">
|
||||
Fakülte / Birim
|
||||
</dt>
|
||||
<dd className="mt-1 font-heading text-2xl font-bold text-slate-900">
|
||||
{sayi(uni.fakulteSayisi)}
|
||||
</dd>
|
||||
</div>
|
||||
{/* Şablon tam 4 hücre bırakıyor: masaüstünde harita 3. sütunu
|
||||
iki satır boyunca kaplıyor (lg:grid-rows-2 + row-span-2), 5.
|
||||
kart ızgarayı bozar. Bu yüzden 4. hücre tek: URAP'ta olan
|
||||
üniversitede URAP sırası, olmayan 31 sayfada toplam
|
||||
kontenjan. Toplam kontenjanı 0 olan üniversite yok
|
||||
(yokatlas.db: GROUP BY universite HAVING
|
||||
SUM(COALESCE(kontenjan2026,0))=0 → 0 satır), yani hiçbir
|
||||
sayfada "—" çıkmaz. URAP'ta payda kartın içindedir: "18."
|
||||
tek başına yanıltır. */}
|
||||
<div className="rounded-xl border border-slate-200 bg-white p-4">
|
||||
iki satır boyunca kaplıyor, 5. kart ızgarayı bozar. Fakülte
|
||||
sayısı Program kartının alt satırında; boşalan hücrede
|
||||
Türkiye sırası. Geri kalan akademik ayrıntı alttaki
|
||||
"Akademik göstergeler ›" penceresinde. */}
|
||||
<dl className="contents">
|
||||
<OzetKart
|
||||
ikon={LayoutList}
|
||||
etiket="Program"
|
||||
deger={sayi(uni.programSayisi)}
|
||||
alt={`${sayi(uni.fakulteSayisi)} birimde`}
|
||||
/>
|
||||
<OzetKart
|
||||
ikon={Users}
|
||||
etiket="Kontenjan"
|
||||
deger={sayi(uni.stats.toplamKontenjan)}
|
||||
alt={`${SON_YIL} toplamı`}
|
||||
/>
|
||||
<OzetKart
|
||||
ikon={Target}
|
||||
etiket="En iyi sıralama"
|
||||
deger={
|
||||
uni.stats.minSira != null ? sayi(uni.stats.minSira) : "—"
|
||||
}
|
||||
alt={`${SON_YIL} taban sırası`}
|
||||
/>
|
||||
{uni.urap ? (
|
||||
<>
|
||||
<dt className="text-xs font-medium uppercase tracking-wide text-slate-500">
|
||||
URAP Türkiye
|
||||
</dt>
|
||||
<dd className="mt-1 font-heading text-2xl font-bold text-slate-900">
|
||||
{sayi(uni.urap.sira)} / {sayi(URAP_TOPLAM)}
|
||||
<span className="mt-0.5 block font-sans text-xs font-medium text-slate-500">
|
||||
{URAP_DONEM}
|
||||
</span>
|
||||
</dd>
|
||||
</>
|
||||
<TurkiyeSirasiKarti sira={uni.urap.sira} />
|
||||
) : (
|
||||
<>
|
||||
<dt className="text-xs font-medium uppercase tracking-wide text-slate-500">
|
||||
Toplam kontenjan
|
||||
</dt>
|
||||
<dd className="mt-1 font-heading text-2xl font-bold text-slate-900">
|
||||
{sayi(uni.stats.toplamKontenjan)}
|
||||
</dd>
|
||||
</>
|
||||
<OzetKart
|
||||
ikon={Building2}
|
||||
etiket="Fakülte / birim"
|
||||
deger={sayi(uni.fakulteSayisi)}
|
||||
alt="programların dağıldığı"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="rounded-xl border border-slate-200 bg-white p-4">
|
||||
<dt className="text-xs font-medium uppercase tracking-wide text-slate-500">
|
||||
En iyi sıralama
|
||||
</dt>
|
||||
<dd className="mt-1 font-heading text-2xl font-bold text-slate-900">
|
||||
{uni.stats.minSira != null ? sayi(uni.stats.minSira) : "—"}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
{uni.urap || uni.istatistik ? (
|
||||
<AkademikAyrinti
|
||||
universite={uni.ad}
|
||||
onizleme={akademikOnizleme(uni)}
|
||||
>
|
||||
<AkademikGostergeler uni={uni} />
|
||||
</AkademikAyrinti>
|
||||
) : null}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="mt-4 max-w-3xl text-sm leading-7 text-slate-600">
|
||||
<p className="mx-auto mt-6 max-w-3xl text-center text-base leading-relaxed text-slate-600 sm:text-lg lg:mx-0 lg:text-left">
|
||||
{uni.ad} taban puanları tablosunun {sayfa}. sayfası (
|
||||
{sayi(tumProgramlar.length)} programın{" "}
|
||||
{sayi((sayfa - 1) * SAYFA_BOYU + 1)}–
|
||||
{sayi(Math.min(sayfa * SAYFA_BOYU, tumProgramlar.length))}.
|
||||
satırları, fakülte sırasıyla).
|
||||
{birimler ? ` Bu sayfadaki birimler: ${birimler}.` : ""} Üniversite
|
||||
özeti ve istatistikler{" "}
|
||||
{birimler ? ` Bu sayfadaki birimler: ${birimler}.` : ""}{" "}
|
||||
Üniversite özeti ve istatistikler{" "}
|
||||
<Link
|
||||
href={`/universite/${slug}`}
|
||||
className="font-medium text-primary hover:underline"
|
||||
className="font-medium text-primary transition-colors hover:text-orange-500 hover:underline"
|
||||
>
|
||||
ilk sayfada
|
||||
</Link>
|
||||
@@ -455,13 +746,13 @@ export function UniversiteIcerik({
|
||||
</>
|
||||
)}
|
||||
|
||||
<PagePixelDivider seed={57} className="mt-8" />
|
||||
<PagePixelDivider seed={57} className="mx-auto mt-12 lg:mx-0" />
|
||||
|
||||
<div id="programlar" className="mt-8">
|
||||
<h2 className="font-heading text-xl font-bold text-slate-900">
|
||||
<div id="programlar" className="mt-10 text-center lg:text-left">
|
||||
<h2 className="font-heading text-2xl font-bold text-slate-900 sm:text-3xl">
|
||||
Üniversite programları
|
||||
</h2>
|
||||
<p className="mt-1 text-xs text-slate-500">
|
||||
<p className="mt-2 text-base text-slate-500">
|
||||
Fakülte ve birimlere göre
|
||||
</p>
|
||||
</div>
|
||||
@@ -474,6 +765,7 @@ export function UniversiteIcerik({
|
||||
</VeriKaynagi>
|
||||
|
||||
{ilkSayfa ? <UrapKaynakSatiri uni={uni} /> : null}
|
||||
{ilkSayfa ? <IstatistikKaynakSatiri uni={uni} /> : null}
|
||||
|
||||
<Sayfalama
|
||||
tabanYol={`/universite/${slug}`}
|
||||
@@ -497,21 +789,21 @@ export function UniversiteIcerik({
|
||||
export function UniversiteIcerikSkeleton() {
|
||||
return (
|
||||
<main className="mx-auto w-full max-w-5xl px-4 py-12 sm:py-16">
|
||||
<Skeleton className="h-4 w-64" />
|
||||
<div className="mt-4 flex items-center gap-4">
|
||||
<Skeleton className="h-5 w-64" />
|
||||
<div className="mt-6 flex items-center gap-4">
|
||||
<Skeleton className="size-14 rounded-lg" />
|
||||
<Skeleton className="h-10 w-2/3 max-w-xl" />
|
||||
</div>
|
||||
<Skeleton className="mt-4 h-5 w-full max-w-3xl" />
|
||||
<div className="mt-6 grid grid-cols-2 gap-3 lg:grid-cols-[1fr_1fr_1.35fr] lg:grid-rows-2">
|
||||
<div className="mt-8 grid grid-cols-2 gap-4 lg:grid-cols-[1fr_1fr_1.35fr] lg:grid-rows-2">
|
||||
{Array.from({ length: 5 }, (_, i) => (
|
||||
<Skeleton key={i} className="h-24 w-full rounded-xl" />
|
||||
<Skeleton key={i} className="h-28 w-full rounded-2xl" />
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-16 space-y-3">
|
||||
<Skeleton className="h-7 w-64" />
|
||||
{Array.from({ length: 5 }, (_, i) => (
|
||||
<Skeleton key={i} className="h-12 w-full" />
|
||||
<Skeleton key={i} className="h-20 w-full rounded-2xl" />
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -27,7 +27,7 @@ export function UniversiteKonumHaritasi({
|
||||
return (
|
||||
<KonumBolumu universite={universite}>
|
||||
<UlkeHaritasi universite={universite} konum={yurtdisi} />
|
||||
<p className="pointer-events-none absolute bottom-0 left-0 text-[11px] 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.sehir ? ` · ${yurtdisi.sehir}` : ""}
|
||||
</p>
|
||||
|
||||
@@ -36,29 +36,36 @@ export function UniversiteProgramTablosu({
|
||||
}: {
|
||||
gruplar: { fakulte: string; programlar: UniProgramSatiri[] }[];
|
||||
}) {
|
||||
// Şişko ve sade: program adı kesilmez (sarar), sıra ve butonlar iri.
|
||||
// 375 px'te yalnız ad + butonlar sütun olarak kalır; taban sıra adın
|
||||
// altındaki satıra iner, böylece ad bütün genişliği kullanır.
|
||||
return (
|
||||
<>
|
||||
{gruplar.map(({ fakulte, programlar }, i) => (
|
||||
<section key={`${fakulte}-${i}`} className="mt-6">
|
||||
<h2 className="mb-3 font-heading text-xl font-bold text-slate-900">
|
||||
<section key={`${fakulte}-${i}`} className="mt-10">
|
||||
<h2 className="mb-4 text-center font-heading text-xl font-bold text-slate-900 sm:text-2xl lg:text-left">
|
||||
{fakulte}
|
||||
</h2>
|
||||
<div className="overflow-hidden rounded-2xl border border-slate-200 bg-white">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Program</TableHead>
|
||||
<TableHead className="hidden text-right md:table-cell">
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableHead className="h-12 pl-4 text-sm font-semibold text-slate-500 sm:pl-5">
|
||||
Program
|
||||
</TableHead>
|
||||
<TableHead className="hidden h-12 text-right text-sm font-semibold text-slate-500 lg:table-cell">
|
||||
Son {VERI_YIL_SAYISI} yıl
|
||||
</TableHead>
|
||||
<TableHead className="hidden text-right lg:table-cell">
|
||||
<TableHead className="hidden h-12 text-right text-sm font-semibold text-slate-500 lg:table-cell">
|
||||
{SON_YIL} puan
|
||||
</TableHead>
|
||||
<TableHead className="hidden text-right sm:table-cell">
|
||||
<TableHead className="hidden h-12 text-right text-sm font-semibold text-slate-500 md:table-cell">
|
||||
Kontenjan
|
||||
</TableHead>
|
||||
<TableHead className="text-right">Taban sıra</TableHead>
|
||||
<TableHead className="w-24 text-right">
|
||||
<TableHead className="hidden h-12 text-right text-sm font-semibold text-slate-500 sm:table-cell">
|
||||
Taban sıra
|
||||
</TableHead>
|
||||
<TableHead className="h-12 w-28 text-right">
|
||||
<span className="sr-only">Netler ve listeye ekle</span>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
@@ -66,61 +73,64 @@ export function UniversiteProgramTablosu({
|
||||
<TableBody>
|
||||
{programlar.map((p) => {
|
||||
const taban = programEtkinSira(p);
|
||||
const tabanMetni = taban != null ? `~${sayi(taban)}.` : "—";
|
||||
return (
|
||||
<TableRow key={p.id} className="kt-cv-satir">
|
||||
<TableCell className="max-w-0 w-full">
|
||||
<TableCell className="w-full whitespace-normal py-4 pl-4 sm:pl-5">
|
||||
{p.bolumSlug ? (
|
||||
<Link
|
||||
href={`/bolum/${p.bolumSlug}`}
|
||||
className="block truncate text-sm font-semibold text-slate-900 hover:text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
className="block text-base font-semibold leading-snug text-slate-900 transition-colors hover:text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{p.isim.trim()}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="block truncate text-sm font-semibold">
|
||||
<span className="block text-base font-semibold leading-snug text-slate-900">
|
||||
{p.isim.trim()}
|
||||
</span>
|
||||
)}
|
||||
<span className="mt-0.5 flex items-center gap-1.5 text-xs text-slate-500">
|
||||
<span className="shrink-0 rounded bg-slate-100 px-1 py-px text-[10px] font-medium leading-4 text-slate-600">
|
||||
<span className="mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-slate-500">
|
||||
<span className="shrink-0 rounded-full bg-slate-100 px-2.5 py-0.5 text-xs font-semibold text-slate-700">
|
||||
{TUR_ETIKET[p.tur] ?? p.tur}
|
||||
</span>
|
||||
{p.sure ? (
|
||||
<span className="shrink-0">{p.sure} yıl</span>
|
||||
) : null}
|
||||
</span>
|
||||
<span className="mt-1.5 flex items-center gap-1.5 text-sm text-slate-500 sm:hidden">
|
||||
Taban sıra
|
||||
<span className="font-semibold tabular-nums text-slate-900">
|
||||
{tabanMetni}
|
||||
</span>
|
||||
<ProgramTabanTrendi program={p} />
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="hidden text-right text-xs md:table-cell">
|
||||
<TableCell className="hidden text-right text-sm lg:table-cell">
|
||||
<ProgramSiraGecmisi program={p} />
|
||||
</TableCell>
|
||||
<TableCell className="hidden text-right text-xs tabular-nums text-slate-500 lg:table-cell">
|
||||
<TableCell className="hidden text-right text-sm tabular-nums text-slate-600 lg:table-cell">
|
||||
{p.puanSon != null
|
||||
? p.puanSon.toFixed(2).replace(".", ",")
|
||||
: "—"}
|
||||
</TableCell>
|
||||
<TableCell className="hidden text-right text-xs tabular-nums text-slate-500 sm:table-cell">
|
||||
<TableCell className="hidden text-right text-sm tabular-nums text-slate-600 md:table-cell">
|
||||
{p.kontenjanSon != null
|
||||
? `${sayi(p.kontenjanSon)} kişi`
|
||||
: "—"}
|
||||
</TableCell>
|
||||
<TableCell className="text-right text-xs tabular-nums text-slate-500">
|
||||
<span className="inline-flex items-center gap-1">
|
||||
<TableCell className="hidden text-right text-base font-semibold tabular-nums text-slate-900 sm:table-cell">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<ProgramTabanTrendi program={p} />
|
||||
{taban != null ? `~${sayi(taban)}.` : "—"}
|
||||
{tabanMetni}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="pr-3 text-right">
|
||||
<span className="inline-flex items-center gap-1.5">
|
||||
<TableCell className="py-4 pr-4 text-right sm:pr-5">
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<ProgramNetleriButonu
|
||||
programId={p.id}
|
||||
isim={p.isim.trim()}
|
||||
className="size-8 min-h-8 min-w-8"
|
||||
/>
|
||||
<ProgramEkleButonu
|
||||
program={p}
|
||||
kaynak="universite"
|
||||
className="size-8 min-h-8 min-w-8"
|
||||
/>
|
||||
<ProgramEkleButonu program={p} kaynak="universite" />
|
||||
</span>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
Reference in New Issue
Block a user