merge(0924): bölüm sayfası ve öğrenci-samimi dalları — şişko ve sade dil tüm ekranlarda, samimi ton (DESIGN.md §10), funnel CTA piksel liste
All checks were successful
Deploy / deploy (push) Successful in 4m36s
All checks were successful
Deploy / deploy (push) Successful in 4m36s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
106
.claude/skills/better-ui/SKILL.md
Normal file
106
.claude/skills/better-ui/SKILL.md
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
---
|
||||||
|
name: better-ui
|
||||||
|
description: Design engineering principles for making interfaces feel polished. Use when building UI components, reviewing frontend code, implementing animations, hover states, shadows, borders, micro-interactions, enter/exit animations, or any visual detail work. Triggers on UI polish, design details, "make it feel better", "feels off", stagger animations, border radius, optical alignment, image outlines, box shadows.
|
||||||
|
---
|
||||||
|
|
||||||
|
> **KolayTercih notu (25 Eyl 2026, Bilal'in isteğiyle eklendi).** Kaynak: Jakub Krehel,
|
||||||
|
> https://github.com/jakubkrehel/skills (ui-skills.com üzerinden okundu, elle kopyalandı).
|
||||||
|
> Bu projede `docs/tasarim/DESIGN.md` önceliklidir. Çakışan iki nokta:
|
||||||
|
> basınca küçülme bizde `scale-[0.97]` (geniş satır `0.99`, çarpı `0.96`), buradaki `0.96`
|
||||||
|
> değil; kart ve satırlarda yapıyı `border-slate-200` verir, gölge yalnız §4.4'teki kadar.
|
||||||
|
> Görsel ayar yaparken önce DESIGN.md, sonra bu dosya.
|
||||||
|
|
||||||
|
# UI polish
|
||||||
|
|
||||||
|
Polish comes from a pile of small details that compound. This skill is the reference for which are worth having and what values they take.
|
||||||
|
|
||||||
|
When reviewing, slow the interface down. What feels off at 10% speed is what is subtly wrong at full speed.
|
||||||
|
|
||||||
|
Keep the project's component library, tokens and density, and match its motion language except where a rule below prescribes an exact interaction.
|
||||||
|
|
||||||
|
Every duration, curve, scale and blur below is a specific value, not a range to approximate. cubic-bezier(0.2, 0, 0, 1) is not cubic-bezier(0.4, 0, 0.2, 1), and 0.96 is not 0.95. Use what is written.
|
||||||
|
|
||||||
|
## Concentric border radius
|
||||||
|
|
||||||
|
Outer radius = inner radius + padding. Mismatched radii on nested elements is the most common thing that makes an interface feel off.
|
||||||
|
|
||||||
|
## Optical over geometric alignment
|
||||||
|
|
||||||
|
When geometric centering looks off, align optically. Buttons with icons, play triangles and asymmetric icons all need a manual nudge.
|
||||||
|
|
||||||
|
## Shadows for elevation, borders for structure
|
||||||
|
|
||||||
|
Where a border exists only to create depth, prefer layered transparent box-shadow values. Keep borders that communicate structure or state: dividers, separators and selected or focus states.
|
||||||
|
|
||||||
|
## Interruptible animations
|
||||||
|
|
||||||
|
Use CSS transitions for interactive state changes, because they can be interrupted mid-animation. Reserve keyframes for staged sequences that run once.
|
||||||
|
|
||||||
|
## Split and stagger enter animations
|
||||||
|
|
||||||
|
For an infrequent staged entrance where sequence communicates hierarchy, break the content into semantic chunks and stagger them by ~100ms. Animating one container gets you less for the same cost. Leave high-frequency interactions unstaggered.
|
||||||
|
|
||||||
|
## Subtle exit animations
|
||||||
|
|
||||||
|
Use a small fixed translateY rather than full height. Exits should be softer than enters. Use ease-out for both directions.
|
||||||
|
|
||||||
|
## Contextual icon animations
|
||||||
|
|
||||||
|
Animate icons with opacity, scale and blur rather than toggling visibility. Use exactly these values: scale 0.25 to 1, opacity 0 to 1, blur 4px to 0px.
|
||||||
|
|
||||||
|
With a motion library (motion or framer-motion in package.json), match that package's import path. Use transition: { type: "spring", duration: 0.3, bounce: 0 }. Bounce is always 0.
|
||||||
|
|
||||||
|
Without one, keep both icons in the DOM with one absolutely positioned, and cross-fade with cubic-bezier(0.2, 0, 0, 1).
|
||||||
|
|
||||||
|
## Image outlines
|
||||||
|
|
||||||
|
Give images a 1px outline at low opacity for consistent depth. Pure black in light mode (oklch(0 0 0 / 0.1)), pure white in dark (oklch(1 0 0 / 0.1)). Never a near-black like slate or zinc and never a tinted neutral.
|
||||||
|
|
||||||
|
## Scale on press
|
||||||
|
|
||||||
|
A scale on click gives a button tactile feedback (upstream value 0.96; this project uses 0.97, see note above). Add a static prop to switch it off where motion would distract.
|
||||||
|
|
||||||
|
## Skip animation on page load
|
||||||
|
|
||||||
|
Use initial={false} on AnimatePresence to keep enter animations off the first render. Check that it leaves intentional page entrances intact.
|
||||||
|
|
||||||
|
## Suppress transitions on theme switch
|
||||||
|
|
||||||
|
A theme flip changes color, background, border and shadow on nearly every element at once. Inject *,*::before,*::after{transition:none !important}, force a reflow, then remove it on the next frame.
|
||||||
|
|
||||||
|
## Transition only what changes
|
||||||
|
|
||||||
|
Always name the exact properties: transition-property: scale, opacity. Tailwind's transition-transform covers transform, translate, scale, rotate.
|
||||||
|
|
||||||
|
## Use will-change sparingly
|
||||||
|
|
||||||
|
Only for transform, opacity and filter. Never will-change: all. Add it when you see first-frame stutter, not before.
|
||||||
|
|
||||||
|
## Match icon stroke to text weight
|
||||||
|
|
||||||
|
1.5px stroke beside regular (400) text, 2px beside semibold (600). One stroke weight per icon set and one icon library per surface.
|
||||||
|
|
||||||
|
## One SVG, recolored per state
|
||||||
|
|
||||||
|
Icons use currentColor and take hover, selected and disabled states from CSS color and opacity. Outline is the default variant; fill marks the active state.
|
||||||
|
|
||||||
|
## Motion restraint
|
||||||
|
|
||||||
|
Give high-frequency interactions instant feedback, or a transition of 150ms or less on opacity and color. Every animated state change also needs a static cue: color, an icon, or a label. Motion is never the only feedback channel.
|
||||||
|
|
||||||
|
## Before you finish
|
||||||
|
|
||||||
|
| Mistake | Fix |
|
||||||
|
|---|---|
|
||||||
|
| Icons look off-center | Nudge optically with padding, or fix the SVG |
|
||||||
|
| Jarring staged entrance or exit | Stagger infrequent entrances; keep exits subtle |
|
||||||
|
| Theme toggle crossfades the whole page | Disable transitions for the swap, force a reflow, restore on the next frame |
|
||||||
|
| transition: all on elements | Specify exact properties |
|
||||||
|
| First-frame animation stutter | Add will-change: transform (sparingly) |
|
||||||
|
| Hairline icon beside bold text | Match the stroke width to the text weight |
|
||||||
|
|
||||||
|
## Reporting
|
||||||
|
|
||||||
|
Severity. HIGH breaks an interaction, makes motion unusable, or leaves a state change visible only while the animation runs. MEDIUM is a visible inconsistency in surfaces, icons, or motion. LOW is isolated polish.
|
||||||
|
|
||||||
|
Format: group findings under the principle each violates, ordered by severity, one row per root cause: Severity | Location | Before | After | Why. End with Block when any HIGH remains, Approve otherwise. Report every check you could not run as Not verified.
|
||||||
146
docs/tasarim/2026-09-25-samimi-arayuz-arastirmasi.md
Normal file
146
docs/tasarim/2026-09-25-samimi-arayuz-arastirmasi.md
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
# Samimi ve oyunsu arayüz: araştırma ve öneriler
|
||||||
|
|
||||||
|
> 25 Eylül 2026. Bilal'in isteği: "Duolingo gibi basit ve oyunsu, Apple gibi irdeleyici gözle
|
||||||
|
> kurulmuş, samimi bir arayüz." Kaynaklar sonda. Uygulaması `tasarim/ogrenci-samimi` dalında;
|
||||||
|
> kurallar `DESIGN.md` §10'a işlendi.
|
||||||
|
|
||||||
|
## Tek cümle
|
||||||
|
|
||||||
|
Bizim öğrencimiz için "samimi" neşe değil, **güven** demek: kaygılı öğrenciye her ekranda
|
||||||
|
nerede olduğunu, sıradaki tek adımı ve kendi yaptığı küçük ilerlemeyi göstermek. Duolingo'nun
|
||||||
|
ilerleme ve küçük kutlama dilini alıyoruz. Seri, puan ve kayıp korkusunu almıyoruz.
|
||||||
|
|
||||||
|
## Neden böyle: öğrencimiz kim?
|
||||||
|
|
||||||
|
Aynı klasördeki öğrenci senaryoları (`docs/arastirma/2026-09-24-ogrenci-senaryolari/`) üç
|
||||||
|
duyguyu tekrar tekrar gösteriyor:
|
||||||
|
|
||||||
|
- **Yanlış yapma korkusu.** Can, geçen yılın tabanına güvenip listeyi yanlış kurmaktan korkuyor.
|
||||||
|
- **Mahcubiyet.** Ayşe Hanım "sistemi bilmiyorum" demekten, Yusuf "ailem para gönderemiyor"
|
||||||
|
demekten çekiniyor.
|
||||||
|
- **Dağınık bilgi.** Bilgi var ama dağınık ve zamansız. Kimse "şimdi şunu yap" demiyor.
|
||||||
|
|
||||||
|
Duolingo günlük bir alışkanlık ürünü. Tercih ise birkaç haftalık, tek seferlik ve yüksek riskli
|
||||||
|
bir karar. Bu yüzden oyunlaştırmanın yalnız bir kısmı bize uyuyor.
|
||||||
|
|
||||||
|
## Duolingo'dan alınacaklar ve alınmayacaklar
|
||||||
|
|
||||||
|
| Alıyoruz | Almıyoruz, çünkü |
|
||||||
|
|---|---|
|
||||||
|
| **Görünür ilerleme.** "Listen 7/24", "Adım 2/3: Şehir" | **Seri (streak).** Günlük alışkanlık için. Tercih günlük iş değil. |
|
||||||
|
| **Küçük kilometre taşı kutlaması.** İlk program eklendi, liste 24/24 doldu. Seri ekranındaki kutlama animasyonu yeni kullanıcının 7. günde kalma oranını %1,7 artırmış. | **Kayıp korkusu.** Duolingo uzun seride bunu bilerek kullanıyor, ama kırılınca motivasyonun çöktüğünü kendisi yazıyor. Kaygılı öğrenciye uygun değil. |
|
||||||
|
| **Başta küçük adımı büyük hissettirmek.** 2'den 3'e çıkan seri %50 büyür. Bizde ilk eklenen program. | **Puan, rozet koleksiyonu, liderlik tablosu.** Tek seferlik kararda toplanacak bir şey yok. Başkasıyla yarış kaygıyı artırır. |
|
||||||
|
| **Beyaz boşlukla ayırmak, kutuyla değil.** Duolingo 2026 sekme yenilemesi. | **Karakter ve maskot.** Yeni çizim işi. Piksel imzamız zaten bizim karakterimiz. |
|
||||||
|
| **Tutarlılık amaca hizmet eder.** Her başlığa süs koymak tutarlı ama zorlama. | **Şaka ve "Hay aksi!" tonu.** Apple: samimiyetsiz duruyor. Kaygılı kullanıcıya hiç. |
|
||||||
|
|
||||||
|
## Apple'dan alınacaklar (irdeleyici göz)
|
||||||
|
|
||||||
|
- **Sesimiz sabit, tonumuz duruma göre.** Liste tamamlanınca hafif bir tebrik. Hata ya da
|
||||||
|
ödemede düz ve ciddi. Açıkta kalan öğrenciye kutlama yok. NN/g'nin deyişiyle "neşe
|
||||||
|
dayatılamaz".
|
||||||
|
- **Ekranın amacı ilk satırda.** En önemli bilgi önce. Birden çok fikir varsa ekranı böl.
|
||||||
|
- **Düğmede fiil.** "Listemi aç", "Sıralamamı gir". "Hadi bakalım!" gibi şirin etiket yok.
|
||||||
|
Çok adımlı akışta tutarlı dil: "Devam" → "Devam" → son adımda ne olacağını söyleyen fiil.
|
||||||
|
- **"Biz" az.** Hata mesajında "biz" yok. "Bir sorun oluştu" değil, ne olduğu ve ne
|
||||||
|
yapılacağı yazılır.
|
||||||
|
- **Boş ekranda net sonraki adım** ve tek düğme.
|
||||||
|
- **Geri bildirim önemiyle orantılı.** Sıradan eklemede sessiz onay (düğme ✓ olur, sayaç
|
||||||
|
artar). Önemli anda (ilk program, liste doldu) görünür onay. Uyarı penceresi yalnız geri
|
||||||
|
dönüşsüz kayıpta.
|
||||||
|
- **Dürüst ilerleme.** Sahte yüzde yok. Beklerken ne yapıldığı kısa ve doğru yazılır.
|
||||||
|
- **İpucu yerinde.** Baştan eğitim turu yerine, öğrenci bir şeye ilk dokunduğunda.
|
||||||
|
|
||||||
|
## İlkeler
|
||||||
|
|
||||||
|
1. **Her ekranın tek cümlelik cevabı var.** `/sonuc`'a gelen öğrenci önce kendi sonucunu
|
||||||
|
görür: "85.000. sıradasın · Sayısal. 4.760 program açık."
|
||||||
|
2. **Öğrenci nerede olduğunu her ekranda görür.** Liste durumu sayıyla ("Listende 5/24
|
||||||
|
program · 19 yer kaldı"), sihirbazda adın kendisi ("Şehir"). Hedef eğimi ve Zeigarnik
|
||||||
|
etkisi: yarım işi tamamlama isteği. (Üç adımlı bir "tercih yolu" şeridi de düşünüldü;
|
||||||
|
ücretsiz liste ile yapay zekâ listesi iki ayrı yol olduğu için tek şeride sığdırmak
|
||||||
|
yanıltıcı olurdu, yapılmadı.)
|
||||||
|
3. **İlerleme dürüst ve somut.** Sayı ile yazılır ("7/24"), yüzde ya da sahte hız yok.
|
||||||
|
4. **Kutlama küçük, yerinde ve hak edilmiş.** Yalnız öğrencinin kendi başardığı adımda.
|
||||||
|
Yerleşme gibi sonuçlarda kutlama yok. "Hareketi azalt" açıksa animasyon yok, metin var.
|
||||||
|
5. **Metin sen diliyle, sakin, vaatsiz.** Fiil düğmeler. Jargon ilk geçtiği yerde açıklanır.
|
||||||
|
Hata mesajında suçlama ve ünlem yok.
|
||||||
|
6. **Boş durum = ne olduğu + tek düğme.**
|
||||||
|
7. **Anlamsal yapı herkes için.** Sayfa başına tek `h1`, bölümler `h2`. Odak almayan durum
|
||||||
|
değişiklikleri (`Listene eklendi`, `Adım 2/3`) ekran okuyucuya duyurulur (WCAG 4.1.3).
|
||||||
|
Açılır satırda `aria-expanded` ve `aria-controls` (APG). Dokunma hedefi 44 px (WCAG 2.5.5).
|
||||||
|
|
||||||
|
**Yapmadıklarımız (aldatıcı desenler):** sahte aciliyet ve geri sayım baskısı, sahte kıtlık,
|
||||||
|
utandırarak onay ("Hayır, geleceğimi umursamıyorum"), ısrarlı açılır pencere. "Kredi" dilinin
|
||||||
|
TL karşılığı açık bir karar olarak Bilal'e bırakıldı (aşağıda).
|
||||||
|
|
||||||
|
## Ekran ekran öneriler (öğrencinin kullandığı yerler)
|
||||||
|
|
||||||
|
| Ekran | Bugün | Öneri |
|
||||||
|
|---|---|---|
|
||||||
|
| `/sonuc` | Sayfada `h1` yok. İlk ekran yapay zekâ örneği, öğrencinin kendi sonucu değil. | Üstte sonuç başlığı: `h1` + sıra + açık program sayısı; dilim sayıları zaten hemen alttaki sekmelerde. Altında liste durumu. Tadımlık kartı yerinde kalır, bir blok aşağı iner. |
|
||||||
|
| Liste çekmecesi | "5 / 24 tercih" ve çubuk var. Doluluğun anlamı yok. | Risk dağılımı metinle ("2 riskli · 2 az riskli · 1 güvenli"). 24/24'te sakin bir tebrik satırı. |
|
||||||
|
| + ile ekleme | Profilliyken hiç geri bildirim yok. Profilsizde uzun bir açıklama. | İlk eklemede tek sefer "İlk programın listende · 1/24". Sonrakiler sessiz (düğme ✓, sayaç artar). |
|
||||||
|
| Sihirbaz | "Adım 1/3" ve üç nokta. Adımların adı yok. | Adımların adı: Alan · Şehir · Öncelik. Adım değişince ekran okuyucuya duyurulur. |
|
||||||
|
| Sıralama yok sayfası | "Geçerli bir sıralama gerekli" (robotik). İkon düğmenin başında. | "Önce sıralamanı gir" ve bunun ne işe yaradığını söyleyen tek cümle. CTA kurala uygun (ok sonda). |
|
||||||
|
| Liste hazırlanırken | Aşama mesajları var (iyi). "Sayfayı kapatma" uyarısı 12 px gri. | Önemli uyarı okunur boyda. Başlık iri. |
|
||||||
|
| Rapor satırı aç/kapa | `aria-expanded` var, `aria-controls` yok. | Açılan bölgeye kimlik ver ve bağla. |
|
||||||
|
|
||||||
|
## Skill'lerle değerlendirme (25 Eyl 2026)
|
||||||
|
|
||||||
|
- **Emil Kowalski, `emil-design-eng`:** Nadir ve hak edilmiş an küçük bir girişle kutlanır
|
||||||
|
(scale 0.95 + opaklık, güçlü ease-out, 300 ms altı); sık tekrarlanan etkileşim
|
||||||
|
canlandırılmaz; sekme (bounce) yok. Uygulandı: "Listen tamam" girişi, sihirbaz çubuğu
|
||||||
|
transform ile doluyor, dilim sekmesinde sekme kaldırıldı. Zaten uygundu: hover yalnız gerçek
|
||||||
|
imleçli cihazda (Tailwind v4), `transition-[…]` ile açık özellik, `active:scale`.
|
||||||
|
- **Emil Kowalski, `apple-design`:** Yön bulma (her ekran "neredeyim"i söyler), özerklik
|
||||||
|
(onay penceresi yerine geri al), "keyif konfeti değil, diğer yedi ilkenin sonucu".
|
||||||
|
- **Jakub Krehel, `better-ui` (ui-skills.com):** projeye eklendi
|
||||||
|
(`.claude/skills/better-ui`), başına DESIGN.md önceliği notu düşüldü.
|
||||||
|
- **Vercel Web Interface Guidelines** (`web-design-guidelines` skill'inin kaynağı): skill
|
||||||
|
eklenmedi, çünkü her çalışmada uzak bir adresten talimat çekip uyguluyor; kurallar bir kez
|
||||||
|
okunup uygulandı: yıkıcı işleme geri al, çekmecede `overscroll-contain`, "İçeriğe geç"
|
||||||
|
linki, başlıkta `text-balance`, bekleme metninde "…".
|
||||||
|
- `ui-ux-pro-max` AGENTS.md gereği kullanılmadı.
|
||||||
|
|
||||||
|
## Bu dalda yapılanlar (`tasarim/ogrenci-samimi`)
|
||||||
|
|
||||||
|
| Ekran | Yapılan |
|
||||||
|
|---|---|
|
||||||
|
| `/sonuc` | `h1` + tek cümlelik cevap ("85.000. sıradasın… 4.760 program sana açık"), liste durumu düğmesi |
|
||||||
|
| + ile ekleme | İlk programda ve 24/24'te tek seferlik onay; dolu listede ne yapılacağı |
|
||||||
|
| Bildirimler | Denendi, geri alındı: sitenin geri kalanıyla uyumsuz duruyordu (Bilal) |
|
||||||
|
| Liste çekmecesi | Risk dağılımı metinle, "Listen tamam", güvenli yoksa ipucu, silmede geri al |
|
||||||
|
| Sihirbaz | Adlı adımlar (Alan · Şehir · Öncelik), ekran okuyucuya duyuru |
|
||||||
|
| Sıralama yok | "Önce sıralamanı gir" + ne işe yaradığı |
|
||||||
|
| `/listem` | "Tercih listen" + sayılarla özet; **telefonda 434 px yana taşma hatası düzeldi** |
|
||||||
|
| Kaydet bandı | Telefonda kelime kelime kırılma düzeldi |
|
||||||
|
| Site geneli | "İçeriğe geç" linki |
|
||||||
|
|
||||||
|
## Bilal'e kalan kararlar
|
||||||
|
|
||||||
|
- **Kredi ve TL.** Aldatıcı desenler listesinde "para birimi karmaşası" var. Bildirimde "(3
|
||||||
|
kredi)" yazıyor; kredinin TL karşılığı pakete göre ≈4–5 TL. Yanına TL yazılsın mı?
|
||||||
|
|
||||||
|
- **`/sonuc` sırası.** Tadımlık 22 Eylül'de dönüşüm için bilerek ilk ekrana alındı. Sonuç
|
||||||
|
başlığı onu yaklaşık 170 px aşağı itiyor (375 px'te hâlâ ilk ekranda başlıyor). Ölçümde
|
||||||
|
"tadımlık görüntülenme" düşerse başlık küçültülür, kaldırılmaz.
|
||||||
|
- **Kutlama hareketi.** Şimdilik yalnız metin, hafif bir büyüme ve renk. Konfeti ya da ses
|
||||||
|
yok. Daha "Duolingo" isteniyorsa bir sonraki adım piksel imzasıyla kısa bir "ısınma"
|
||||||
|
(hero'daki pikseller gibi) olabilir.
|
||||||
|
|
||||||
|
## Kaynaklar
|
||||||
|
|
||||||
|
1. Duolingo, "Elevating craft: How we refreshed our core tabs", 4 Şub 2026. https://blog.duolingo.com/core-tabs-redesign/
|
||||||
|
2. Duolingo, "The habit-building research behind your Duolingo streak", 31 Oca 2022. https://blog.duolingo.com/how-duolingo-streak-builds-habit/
|
||||||
|
3. Duolingo, "Start earning brand-new Achievements", 11 Ara 2023. https://blog.duolingo.com/achievement-badges/
|
||||||
|
4. NN/g, Tanner Kohler, "Autonomy, Relatedness, and Competence in UX Design", 9 Oca 2022. https://www.nngroup.com/articles/autonomy-relatedness-competence/
|
||||||
|
5. NN/g, Therese Fessenden, "Three Pillars of User Delight", 27 Kas 2022. https://www.nngroup.com/articles/pillars-user-delight/
|
||||||
|
6. NN/g, Jakob Nielsen, "Progressive Disclosure", 2006. https://www.nngroup.com/articles/progressive-disclosure/
|
||||||
|
7. NN/g, Kate Kaplan, "Designing Empty States in Complex Applications", 19 Eyl 2021. https://www.nngroup.com/articles/empty-state-interface-design/
|
||||||
|
8. Apple HIG, Writing (güncelleme 16 Ara 2025). https://developer.apple.com/design/human-interface-guidelines/writing
|
||||||
|
9. Apple HIG, Onboarding (10 Haz 2024). https://developer.apple.com/design/human-interface-guidelines/onboarding
|
||||||
|
10. Apple HIG, Feedback. https://developer.apple.com/design/human-interface-guidelines/feedback
|
||||||
|
11. Apple HIG, Progress indicators. https://developer.apple.com/design/human-interface-guidelines/progress-indicators
|
||||||
|
12. Laws of UX (Jon Yablonski): Goal-Gradient Effect, Peak-End Rule, Hick's Law, Zeigarnik Effect. https://lawsofux.com
|
||||||
|
13. W3C WCAG 2.2: 2.5.8 Target Size (Minimum), 4.1.3 Status Messages; WAI-ARIA APG Disclosure Pattern. https://www.w3.org/WAI/
|
||||||
|
14. Harry Brignull, Deceptive Patterns: Types. https://www.deceptive.design/types
|
||||||
@@ -213,7 +213,8 @@ Alt sayfada (`Drawer`) aynı sınıf seti `DrawerClose` üstünde kullanılır (
|
|||||||
- İ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).
|
- İ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.
|
- Gölge: `shadow-sm` varsayılan; hover'da `shadow-lg shadow-orange-500/5`. Başka gölge yok.
|
||||||
|
|
||||||
### 4.5 Risk çipi / etiketi
|
### 4.5 Risk çipi / etiketi — `RiskEtiketi`, `src/components/risk-etiketi.tsx`
|
||||||
|
Tablolar, çekmece ve rapor listesi `<RiskEtiketi risk={…} />` kullanır; elle yazılmaz.
|
||||||
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.
|
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.
|
||||||
|
|
||||||
### 4.6 Form
|
### 4.6 Form
|
||||||
@@ -222,7 +223,7 @@ shadcn `Input`, `Select`, `Textarea`, `Slider`. Etiket her alanda; hata mesajı
|
|||||||
### 4.7 İkonlar
|
### 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.
|
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`
|
### 4.8 Özet kartı — `OzetKart`, `src/components/ozet-kart.tsx`
|
||||||
Apple Fitness özet kartının sitenin ölçüleriyle hâli. Sayfada **en çok dört** tane (harita
|
Apple Fitness özet kartının sitenin ölçüleriyle hâli. Sayfada **en çok dört** tane (harita
|
||||||
yanındaki 2×2).
|
yanındaki 2×2).
|
||||||
|
|
||||||
@@ -239,7 +240,11 @@ yanındaki 2×2).
|
|||||||
ikon kutusu `flex size-8 shrink-0 items-center justify-center rounded-xl bg-primary/10 sm:size-10`.
|
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.
|
- `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
|
- 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.
|
sığar. Daha uzun sayı gelecekse önce ölç (§9.2), sonra yaz. Ölçülen sınır: 36 px'te
|
||||||
|
"1.618.988" 123 px — ikonun altına girer. 6–7 haneli sayı birimle kısalır: 245.300 →
|
||||||
|
`245` + birim `bin`, 1.618.988 → `1,62` + birim `milyon` (bölüm sayfasındaki `kartSayisi`).
|
||||||
|
- Kullanıldığı yerler: üniversite ve bölüm sayfası (bölümde dört kart: Program, Devlet,
|
||||||
|
Vakıf, En iyi sıralama; `grid grid-cols-2 gap-4 lg:grid-cols-4`, haritası olmadığı için).
|
||||||
- Tıklanabilir kart (ör. Türkiye sırası → sıralama sayfası): kartın tamamı link olur
|
- 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
|
(`after:absolute after:inset-0` gerilmiş link), iç kaynak linki `relative z-10` ile üstte
|
||||||
kalır; hover §2.1.
|
kalır; hover §2.1.
|
||||||
@@ -271,6 +276,12 @@ açılan modal değil. Çekmece kabı ve çarpı `cta-sira-form.tsx`'teki çekme
|
|||||||
kareler kısaltılır, not kısaltılır — içerik silinmez (§9.2 ölçüm).
|
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)
|
- 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).
|
→ kaynaklar (logo solda, `text-xs` iki satır).
|
||||||
|
- **Liste çekmecesi** (`liste-cekmecesi.tsx`, 24 Eyl 2026): başlık aynı düzende (temizle
|
||||||
|
düğmesi çarpıyla aynı sınıf setinde solda). Satırlar çerçeveli kart değil, düz liste satırı
|
||||||
|
(`border-b border-slate-100`); ad §4.15 gibi sarar, risk + taban sıra adın altında, çıkar
|
||||||
|
düğmesi `size-11` **çerçevesiz `CircleMinus`** — çarpı yalnız çekmeceyi kapatır (Bilal, 25 Eyl
|
||||||
|
2026: "2 çarpı var"). Listeler uzun olduğu için bu çekmece kaydırılır (tek ekran kuralının
|
||||||
|
tek istisnası); alt bandı telefonda alt alta dizilir ki listeye en az ~280 px kalsın.
|
||||||
- **Arama motoru uyarısı:** çekmece içeriği sayfa açılırken HTML'de yoktur (kapalıyken
|
- **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 +
|
ç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.
|
dönem + aktif bağlantı) çekmeceye taşınmaz, sayfada da görünür kalır.
|
||||||
@@ -332,7 +343,13 @@ verilir (logo listesi istemciye inmez).
|
|||||||
Hareket: yay `bounce: 0, duration: 0.35`, giriş ve çıkış aynı yoldan; "hareketi azalt"ta yalnız
|
Hareket: yay `bounce: 0, duration: 0.35`, giriş ve çıkış aynı yoldan; "hareketi azalt"ta yalnız
|
||||||
opaklık.
|
opaklık.
|
||||||
|
|
||||||
### 4.15 Liste satırı (program/üniversite listeleri) — `universite-program-tablosu.tsx`
|
### 4.15 Liste satırı (program/üniversite listeleri) — `src/components/liste-satiri.ts`
|
||||||
|
Sınıf dizileri tek dosyada (`SATIR_AD`, `SATIR_META`, `SATIR_CIP`, `SATIR_MOBIL_SAYI`,
|
||||||
|
`SATIR_BUTON_KABI`…); üniversite, bölüm ve `/sonuc` tabloları oradan okur. Yeni bir liste
|
||||||
|
tablosu bu sabitleri kullanır, sınıfı elle yazmaz.
|
||||||
|
- Risk sütunu (`/sonuc`) sm'den itibaren ayrı sütun; telefonda adın altındaki sayı satırına
|
||||||
|
iner. Etiket §4.5: `size-2` nokta + `text-xs font-semibold text-{renk}-600` + `RISK_ETIKET`.
|
||||||
|
- Veride BÜYÜK HARF gelen üniversite adı ekranda `trBaslikDuzeni` ile yazılır.
|
||||||
- **Ad kesilmez, sarar:** `whitespace-normal` + `block text-base font-semibold leading-snug`.
|
- **Ad kesilmez, sarar:** `whitespace-normal` + `block text-base font-semibold leading-snug`.
|
||||||
`truncate` ile "İngiliz Dili ve E…" yazılmaz.
|
`truncate` ile "İngiliz Dili ve E…" yazılmaz.
|
||||||
- Ad linkinin hover'ı **mavi**: `transition-colors hover:text-primary hover:underline`
|
- Ad linkinin hover'ı **mavi**: `transition-colors hover:text-primary hover:underline`
|
||||||
@@ -341,7 +358,22 @@ opaklık.
|
|||||||
adın altındaki satıra iner: `mt-1.5 flex items-center gap-1.5 text-sm text-slate-500 sm:hidden`
|
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`.
|
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`.
|
- 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.
|
- Satır butonları varsayılan `size-11` (44 px); `size-8` gibi küçültme verilmez. Telefonda
|
||||||
|
iki buton alt alta durur (`inline-flex flex-col items-center gap-2 sm:flex-row`, sütun
|
||||||
|
`w-14 sm:w-28`): yan yana 96 px yiyip adı 157 px'e sıkıştırıyordu, alt alta 273 px kalıyor
|
||||||
|
(24 Eyl 2026 ölçümü).
|
||||||
|
- Rapor listesinde (`rapor-listesi.tsx`) telefonda logo gizlenir (`hidden sm:block`): solda
|
||||||
|
sıra numarası zaten satırı çapalıyor, logo adı üç satıra sıkıştırıyordu (satır 214 →
|
||||||
|
152 px). Açılan ayrıntı iç kutuları `rounded-xl`, etiket `text-sm font-semibold
|
||||||
|
text-slate-900`, metin `text-base`.
|
||||||
|
- **Geniş ekranda ince satır** (Bilal, 25 Eyl 2026: "geniş ekranda listeler çok kalın"):
|
||||||
|
sm'den itibaren tür/il/süre satırı adla aynı satıra çıkar (`SATIR_GRUP`); lg'de hücre
|
||||||
|
dolgusu `py-3` ve satır düğmeleri 36 px (`SATIR_DUGME_BOYU`, §8 masaüstü tablo istisnası).
|
||||||
|
Ölçüm 1280 px, bölüm listesi: 83 → 61 px. Telefonda değişmez (44 px düğme).
|
||||||
|
- Satırda logo varsa (bölüm sayfası) `UniLogo boy="sm"` adın solunda, `items-start`.
|
||||||
|
- Program varyantı ("… (İngilizce) (Burslu)") adın altında ayrı satır
|
||||||
|
`mt-0.5 block text-sm leading-snug text-slate-600`; tür çipine yapıştırılıp kesilmez.
|
||||||
|
- Tür çipi devlet/vakıf için de aynı slate çip; vakıf violet değildir (§2.2).
|
||||||
- Tablo başlığı `h-12 text-sm font-semibold text-slate-500`; hücre dolgusu `py-4 pl-4 sm:pl-5`.
|
- 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. Yerleşim
|
||||||
@@ -378,6 +410,22 @@ kartlarla aynı hizada:
|
|||||||
yorar), form alanları, uzun rehber metni.
|
yorar), form alanları, uzun rehber metni.
|
||||||
- Alt sayfa (§4.10) içeriği her ekranda ortalıdır.
|
- Alt sayfa (§4.10) içeriği her ekranda ortalıdır.
|
||||||
|
|
||||||
|
### 5.2.1 Sayfalama — `Sayfalama`, `sayfalama.tsx`
|
||||||
|
Haplar `h-11` (44 px), hover §2.1. Telefonda (sm altı) tek satır kalsın diye yalnız Önceki ·
|
||||||
|
aktif sayfa · Sonraki görünür; diğer numaralar `hidden sm:inline-flex` ile HTML'de durur
|
||||||
|
(tarayıcı botları tüm sayfaları görür). 375 px'te 7 sayfalık şerit iki satıra kırılıyordu.
|
||||||
|
|
||||||
|
### 5.2.2 Dilim sekmeleri (Hayal / Dengeli / Güvenli) — `program-tablosu.tsx`
|
||||||
|
Tek satırda üç hap 375 px'e sığmıyordu (418 px, sayfa yana kayıyordu). Telefonda
|
||||||
|
`grid w-full grid-cols-3`, hapın içinde ikon+ad üstte, sayı altta; sm'den tek satır. Hap en az
|
||||||
|
44 px (`min-h-11`), hover §2.1.
|
||||||
|
|
||||||
|
### 5.2.3 Karşılaştırma tablosu (ana sayfa) — yatay kaydırma yok
|
||||||
|
Dört sütunlu tablo yalnız `lg`'de. Altında her özellik kendi satırında (başlık ortalı
|
||||||
|
`text-base font-semibold`), üç cevap altında üç sütun; KolayTercih sütunu `bg-primary/5`
|
||||||
|
kutuda. Sütun adları üstte yapışık: `sticky top-24 sm:top-32` (site başlığı telefonda 96 px,
|
||||||
|
sm'den itibaren 128 px — ölçüldü). Bilal, 25 Eyl 2026: "kaydırması gerekiyor, olmamalı".
|
||||||
|
|
||||||
### 5.3 Verimli ve havadar
|
### 5.3 Verimli ve havadar
|
||||||
- İlişkili öğeler yan yana dizilir (iki sütun) — tek sütunda alt alta uzayan panel yapılmaz.
|
- İ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
|
- Yan yana dizmek "hepsini tek şeride sıkıştırmak" değildir: 23 Eyl'de denenen yatay "bilgi
|
||||||
@@ -470,9 +518,94 @@ Bu dilde sorunların çoğu koddan görünmez (taşma, sığma, hizalanma). Tasa
|
|||||||
### 9.4 Site taraması için denetim uyarıları
|
### 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
|
`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),
|
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).
|
`mavi-hover` (§2.1), `iri-sayi-tabular` (Cal Sans + tabular-nums).
|
||||||
|
`mavi-hover` §4.15 kanonik liste satırı adını (`text-base font-semibold leading-snug` +
|
||||||
|
`hover:text-primary hover:underline` aynı satırda) kendiliğinden atlar — istisna uyarı
|
||||||
|
vermez (Bilal'in kararı, 24 Eyl 2026). Eski `truncate text-sm` adlar yine uyarır; önce §4.15'e
|
||||||
|
çevrilir.
|
||||||
Bir ekranı dönüştüren ajan o ekrandaki uyarıları sıfırlar ya da neden kaldığını yazar.
|
Bir ekranı dönüştüren ajan o ekrandaki uyarıları sıfırlar ya da neden kaldığını yazar.
|
||||||
|
|
||||||
|
**Bilerek bırakılan uyarılar (24 Eyl 2026 taraması sonunda 12):**
|
||||||
|
- `meraklisina-demo.tsx` (8 × minik-yazi): SVG `viewBox` 640 birim; 375 px'te 0,54 ölçekle
|
||||||
|
çiziliyor, 11 birimlik yazı ekranda 5,9 px (ölçüldü). 12'ye çıkarmak yalnız 6,5 px eder ve
|
||||||
|
etiketler çakışır — çözüm telefona ayrı (dikey) çizim; ayrı iş.
|
||||||
|
- `secimlerim-paneli.tsx` (4): hiçbir yerde kullanılmıyor (ölü kod); dönüştürülmedi,
|
||||||
|
silinmesi ayrı iş.
|
||||||
|
|
||||||
### 9.5 Bitirmeden
|
### 9.5 Bitirmeden
|
||||||
`pnpm tasarim:denetim` sıfır hata (uyarılar raporda listelenir) + 375 px'te tek bakış (taşma,
|
`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.
|
üst üste binme, dokunma hedefi, çekmece tek ekran) + §9.2 ölçümleri.
|
||||||
|
|
||||||
|
## 10. Samimi ton, ilerleme ve kutlama (öğrenci ekranları)
|
||||||
|
|
||||||
|
Bilal'in isteği (25 Eyl 2026): "Duolingo gibi basit ve oyunsu, Apple gibi irdeleyici, samimi."
|
||||||
|
Araştırma ve kaynaklar: `docs/tasarim/2026-09-25-samimi-arayuz-arastirmasi.md`. Özü: bizim
|
||||||
|
öğrencimiz için samimi = **güven**; neşe dayatılmaz.
|
||||||
|
|
||||||
|
### 10.1 Her ekranın tek cümlelik cevabı
|
||||||
|
- Öğrenci ekranı `h1` ile açılır ve ilk paragraf öğrencinin kendi durumunu sayıyla söyler.
|
||||||
|
Örnek (`/sonuc`, `SonucBasligi`): "**85.000.** sıradasın (Sayısal). **4.760 program** sana
|
||||||
|
açık; aşağıda riskine göre ayrıldı."
|
||||||
|
- Altında liste durumu (`ListeDurumuSatiri`): boşsa ne yapılacağı, doluysa "Listende 5/24
|
||||||
|
program · 19 yer kaldı" düğmesi (çekmeceyi açar).
|
||||||
|
|
||||||
|
### 10.2 İlerleme
|
||||||
|
- Sayıyla ve dürüst: "7/24", "Adım 2/3: Şehir". Yüzde, sahte hız, yapay ilerleme yok.
|
||||||
|
- Sihirbaz adım göstergesi adlı (Alan · Şehir · Öncelik); dolum `scale-x` ile soldan,
|
||||||
|
`duration-300 ease-[cubic-bezier(0.23,1,0.32,1)]`, hareketi azalt'ta geçişsiz.
|
||||||
|
- Liste çekmecesinde risk dağılımı metinle (`RiskEtiketi` + sayı). Riskleri bilinen 6+
|
||||||
|
tercihli listede güvenli yoksa tek cümle ipucu — vaat değil, gerekçe.
|
||||||
|
|
||||||
|
### 10.3 Kutlama
|
||||||
|
- Yalnız öğrencinin kendi yaptığı küçük adımda: ilk program (1/24) ve dolu liste (24/24).
|
||||||
|
Bir kez, sakin bir bildirimle; aradaki eklemeler sessiz (düğme ✓, sayaç artar).
|
||||||
|
- Yerleşme gibi **sonuçlarda kutlama yok**; açıkta kalan öğrenciye neşeli dil yok.
|
||||||
|
- Hareket: `starting:scale-95 starting:opacity-0` + güçlü ease-out, 300 ms altı; konfeti,
|
||||||
|
ses, sekme (bounce) yok. Hareketi azalt'ta yalnız opaklık.
|
||||||
|
|
||||||
|
### 10.4 Metin
|
||||||
|
- Sen dili, etken çatı, fiil düğme ("Listemi aç", "Sıralamamı gir"). Şirin/zekice etiket yok.
|
||||||
|
- Hata: ne olduğu + ne yapılacağı; suçlama ve ünlem yok. "Hay aksi", "Eyvah", "Oops" yok
|
||||||
|
(denetim: `samimiyetsiz-unlem`). "Buraya tıkla" yok; link ne açtığını söyler.
|
||||||
|
- Bekleme metni "…" ile biter ("Kontrol ediliyor…").
|
||||||
|
- "Kredi" dili: gerçek parayı sanal birime çevirmek aldatıcı desenler listesinde ("para
|
||||||
|
birimi karmaşası"). Kredinin TL karşılığı pakete göre değiştiği için (≈4–5 TL) ekranda TL
|
||||||
|
gösterip göstermemek Bilal'in kararına bırakıldı (25 Eyl 2026, açık).
|
||||||
|
|
||||||
|
### 10.5 Geri alınabilir işlem
|
||||||
|
- Listeden çıkarma ve listeyi temizleme anında olur ama 8 sn "Geri al" bildirimi verir;
|
||||||
|
çıkanlar eski sıralarına döner (`geriAlinabilirCikar`, `liste-cekmecesi.tsx`). Beklenen
|
||||||
|
işlemde "Emin misin?" penceresi açılmaz (Apple: beklenen kayıpta uyarı yok).
|
||||||
|
|
||||||
|
### 10.6 Anlamsal yapı
|
||||||
|
- Sayfa başına tek `h1`; bölümler `h2`. Başlıklarda `text-balance`.
|
||||||
|
- Odak almayan durum değişikliği ekran okuyucuya duyurulur: `role="status"` (liste sayacı,
|
||||||
|
sihirbaz adımı, bekleme). Bildirimler (sonner) bunu kendisi yapar.
|
||||||
|
- Açılır satır: `aria-expanded` + `aria-controls` (APG disclosure).
|
||||||
|
- Site geneli "İçeriğe geç" linki `layout.tsx`'te; içerik kabı `#icerik`.
|
||||||
|
- Çekmece kaydırma alanı `overscroll-contain`.
|
||||||
|
|
||||||
|
### 10.7 Almadıklarımız (Duolingo'dan)
|
||||||
|
Seri (streak), kayıp korkusu, puan/XP, rozet koleksiyonu, liderlik tablosu, maskot, sahte
|
||||||
|
aciliyet/kıtlık, utandırarak onay. Tercih günlük alışkanlık değil, tek seferlik kaygılı bir
|
||||||
|
karar.
|
||||||
|
|
||||||
|
### 10.8 Funnel CTA yüzeyi ve 24'lük piksel liste
|
||||||
|
Bilal'in isteği (25 Eyl 2026): "CTA alanlarını arama içindeki alandan ilham, piksel detaylı yap;
|
||||||
|
pikseller bir kümede kümeleşmesin, interaktif kreatif olsun."
|
||||||
|
- **Yüzey — `PikselCtaYuzeyi`, `src/components/piksel-cta-yuzeyi.tsx`:** arama penceresindeki
|
||||||
|
kartın dili: `rounded-3xl border border-blue-400/20 bg-slate-950 text-white`, altta yazılıp
|
||||||
|
silinen satırlar (`FUNNEL_METINLERI`, `src/lib/funnel-metinleri.ts`), tek turuncu CTA.
|
||||||
|
Kullanıldığı yerler: ana sayfa kapanış CTA'sı, `/sonuc`'taki iki sihirbaz paneli, katalog
|
||||||
|
sayfalarının altındaki `CtaSiraForm` kart varyantı (üstteki kompakt satır form olduğu için
|
||||||
|
açık zeminde kalır). Eski
|
||||||
|
`bg-primary` + köşeye kümelenmiş `PixelField` bu yüzeylerde kullanılmaz.
|
||||||
|
- **Kreatif — `PikselListe`, `src/components/piksel-liste.tsx`:** 24 kare = 24 tercih, hero'nun
|
||||||
|
nokta ölçüsüyle (hücre 10, nokta 7.4, rx 2). Örnek modda liste kendini kurar (5 hayal · 13
|
||||||
|
dengeli · 6 güvenli, 110 ms arayla), yalnız ekrandayken. İmleç yakın kareleri büyütür; kart
|
||||||
|
içindeki düğmeye imleç/odak gelince 24 kare birden dolar; dokunmada basılan kare ve komşuları
|
||||||
|
nabız atar (`kt-piksel-nabiz`). Hareketi azalt: dolu, durağan. Renk her zaman
|
||||||
|
`PikselListeAciklama` metniyle.
|
||||||
|
- **Gerçek liste:** çekmecede ilerleme çubuğunun yerinde öğrencinin kendi listesi (`gercek`,
|
||||||
|
12×2): her kare o tercihin riskiyle, riski bilinmeyen beyaz; metin karşılığı yanındaki risk
|
||||||
|
dağılımında.
|
||||||
|
|||||||
@@ -191,7 +191,21 @@ const KURALLAR: Kural[] = [
|
|||||||
ad: "mavi-hover",
|
ad: "mavi-hover",
|
||||||
seviye: "uyari",
|
seviye: "uyari",
|
||||||
mesaj: "Hover rengi tek turuncu (orange-500); liste satırındaki ad linkiyse istisna, mavi kalır (DESIGN.md §2.1)",
|
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/),
|
// §2.1 istisnası: §4.15 kanonik liste satırı adı (sarar, text-base
|
||||||
|
// font-semibold leading-snug + hover:text-primary hover:underline) mavi
|
||||||
|
// kalır. Eski truncate/text-sm adlar istisnaya girmez — önce §4.15'e
|
||||||
|
// çevrilir. Satırın tamamı link olduğunda group-hover çifti de sayılır.
|
||||||
|
// (Bilal'in kararı, 24 Eyl 2026: istisna uyarı vermesin.)
|
||||||
|
bul: (icerik) =>
|
||||||
|
regexKurali(/\b(?:group-)?hover:(?:text|border|shadow)-primary\b/)(icerik).filter((e) => {
|
||||||
|
const bas = icerik.lastIndexOf("\n", e.index) + 1;
|
||||||
|
const son = icerik.indexOf("\n", e.index);
|
||||||
|
const satir = icerik.slice(bas, son === -1 ? undefined : son);
|
||||||
|
return !(
|
||||||
|
/\btext-base font-semibold leading-snug\b/.test(satir) &&
|
||||||
|
/\b(?:group-)?hover:text-primary (?:group-)?hover:underline\b/.test(satir)
|
||||||
|
);
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
ad: "iri-sayi-tabular",
|
ad: "iri-sayi-tabular",
|
||||||
@@ -199,6 +213,12 @@ const KURALLAR: Kural[] = [
|
|||||||
mesaj: "Cal Sans iri sayıda tabular-nums 'daktilo' gibi durur; yalnız font-sans tablo sütunlarında (DESIGN.md §3)",
|
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/),
|
bul: regexKurali(/\bfont-heading\b[^"'`\n]*\btabular-nums\b|\btabular-nums\b[^"'`\n]*\bfont-heading\b/),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
ad: "samimiyetsiz-unlem",
|
||||||
|
seviye: "uyari",
|
||||||
|
mesaj: "Hata/bildirim metninde 'Hay aksi/Eyvah/Oops' ya da 'buraya tıkla' yok; ne oldu + ne yapılacak (DESIGN.md §10.4)",
|
||||||
|
bul: regexKurali(/Hay aksi|Eyvah|\bOops|\bUups|[Bb]uraya tıkla|[Tt]ıklayın(?:ız)?\b/),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
ad: "emoji-ikon",
|
ad: "emoji-ikon",
|
||||||
seviye: "uyari",
|
seviye: "uyari",
|
||||||
|
|||||||
@@ -303,3 +303,21 @@
|
|||||||
mask-image: none;
|
mask-image: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 24'lük piksel listede dokunma nabzı (bkz. components/piksel-liste.tsx):
|
||||||
|
basılan kare ve komşuları kısa bir büyüyüp söner. Tek seferlik, 420 ms. */
|
||||||
|
@keyframes kt-piksel-nabiz {
|
||||||
|
0% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
35% {
|
||||||
|
transform: scale(1.3);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.kt-piksel-nabiz {
|
||||||
|
animation: kt-piksel-nabiz 0.42s cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
}
|
||||||
|
|||||||
@@ -82,6 +82,14 @@ export default function RootLayout({
|
|||||||
className={`${calSans.variable} ${clarityCity.variable} ${geistMono.variable} ${bricolage.variable} antialiased`}
|
className={`${calSans.variable} ${clarityCity.variable} ${geistMono.variable} ${bricolage.variable} antialiased`}
|
||||||
>
|
>
|
||||||
<body className="min-h-dvh flex flex-col bg-slate-50">
|
<body className="min-h-dvh flex flex-col bg-slate-50">
|
||||||
|
{/* Klavyeyle gelen için ilk durak: üst bandı ve menüyü atla
|
||||||
|
(WCAG 2.4.1). Odaklanınca görünür, sitenin hap ölçüsünde. */}
|
||||||
|
<a
|
||||||
|
href="#icerik"
|
||||||
|
className="sr-only focus:not-sr-only focus:fixed focus:top-3 focus:left-3 focus:z-[60] focus:inline-flex focus:h-11 focus:items-center focus:rounded-full focus:border focus:border-slate-200 focus:bg-white focus:px-4 focus:text-sm focus:font-semibold focus:text-slate-900 focus:shadow-lg focus:outline-none focus-visible:ring-3 focus-visible:ring-ring/50"
|
||||||
|
>
|
||||||
|
İçeriğe geç
|
||||||
|
</a>
|
||||||
<HeaderGate>
|
<HeaderGate>
|
||||||
{/* Site geneli progressive blur — üst ve alt. Stil globals.css'te
|
{/* Site geneli progressive blur — üst ve alt. Stil globals.css'te
|
||||||
(.kt-sayfa-blur): iOS/iPadOS'ta fixed + backdrop-filter katmanı
|
(.kt-sayfa-blur): iOS/iPadOS'ta fixed + backdrop-filter katmanı
|
||||||
@@ -92,7 +100,11 @@ export default function RootLayout({
|
|||||||
<SiteTopBanner />
|
<SiteTopBanner />
|
||||||
<SiteHeader />
|
<SiteHeader />
|
||||||
</HeaderGate>
|
</HeaderGate>
|
||||||
<div className="flex min-h-0 flex-1 flex-col pb-28 print:pb-0">
|
<div
|
||||||
|
id="icerik"
|
||||||
|
tabIndex={-1}
|
||||||
|
className="flex min-h-0 flex-1 flex-col pb-28 outline-none print:pb-0"
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
{/* Manuel 24'lük liste çekmecesi + "+" uçuş katmanı (navbar'dan açılır) */}
|
{/* Manuel 24'lük liste çekmecesi + "+" uçuş katmanı (navbar'dan açılır) */}
|
||||||
|
|||||||
166
src/app/page.tsx
166
src/app/page.tsx
@@ -25,9 +25,9 @@ import { TurkeyPixelMap } from "@/features/pazarlama/components/turkey-pixel-map
|
|||||||
import {
|
import {
|
||||||
PixelCorner,
|
PixelCorner,
|
||||||
PixelDivider,
|
PixelDivider,
|
||||||
PixelField,
|
|
||||||
SectionEyebrow,
|
SectionEyebrow,
|
||||||
} from "@/components/pixel-decor";
|
} from "@/components/pixel-decor";
|
||||||
|
import { PikselCtaYuzeyi } from "@/components/piksel-cta-yuzeyi";
|
||||||
import { MAX_REVIZYON, URUNLER } from "@/lib/credits";
|
import { MAX_REVIZYON, URUNLER } from "@/lib/credits";
|
||||||
import { VERI_YIL_SAYISI } from "@/lib/veri-kapsami";
|
import { VERI_YIL_SAYISI } from "@/lib/veri-kapsami";
|
||||||
import {
|
import {
|
||||||
@@ -282,7 +282,7 @@ export default function Home() {
|
|||||||
<Parallax strength={7} className="mt-12 grid gap-6 md:grid-cols-3">
|
<Parallax strength={7} className="mt-12 grid gap-6 md:grid-cols-3">
|
||||||
{problems.map((p, i) => (
|
{problems.map((p, i) => (
|
||||||
<Reveal key={p.title} delay={i * 0.08}>
|
<Reveal key={p.title} delay={i * 0.08}>
|
||||||
<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">
|
<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">
|
||||||
<PixelCorner className="absolute right-4 top-4" />
|
<PixelCorner className="absolute right-4 top-4" />
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="mb-2 flex size-11 items-center justify-center rounded-xl bg-primary/10">
|
<div className="mb-2 flex size-11 items-center justify-center rounded-xl bg-primary/10">
|
||||||
@@ -340,7 +340,7 @@ export default function Home() {
|
|||||||
<Parallax strength={7} className="mt-12 grid gap-6 md:grid-cols-3">
|
<Parallax strength={7} className="mt-12 grid gap-6 md:grid-cols-3">
|
||||||
{steps.map((s, i) => (
|
{steps.map((s, i) => (
|
||||||
<Reveal key={s.step} delay={i * 0.08} className="h-full">
|
<Reveal key={s.step} delay={i * 0.08} className="h-full">
|
||||||
<div className="relative h-full rounded-2xl border border-slate-200/70 bg-white p-8 shadow-sm transition-all duration-300 hover:-translate-y-1 hover:border-primary/30 hover:shadow-lg hover:shadow-primary/5">
|
<div className="relative h-full rounded-2xl border border-slate-200/70 bg-white p-8 shadow-sm transition-all duration-300 hover:-translate-y-1 hover:border-orange-500 hover:shadow-lg hover:shadow-orange-500/5">
|
||||||
<span className="absolute -top-4 left-8 flex size-8 items-center justify-center rounded-full bg-orange-500 font-heading text-sm font-bold text-white">
|
<span className="absolute -top-4 left-8 flex size-8 items-center justify-center rounded-full bg-orange-500 font-heading text-sm font-bold text-white">
|
||||||
{s.step}
|
{s.step}
|
||||||
</span>
|
</span>
|
||||||
@@ -372,9 +372,13 @@ export default function Home() {
|
|||||||
},
|
},
|
||||||
{ value: "%100", label: "Resmî YÖK Atlas kaynağı" },
|
{ value: "%100", label: "Resmî YÖK Atlas kaynağı" },
|
||||||
].map((stat) => (
|
].map((stat) => (
|
||||||
<div key={stat.label} className="p-6 text-center">
|
// Sayı kahramandır (DESIGN.md §1.1): iri sayı üstte, etiket
|
||||||
<dt className="text-sm text-slate-500">{stat.label}</dt>
|
// altında; dt anlam için DOM'da önce, görsel sıra order ile.
|
||||||
<dd className="mt-1 font-heading text-3xl font-bold text-primary">
|
<div key={stat.label} className="flex flex-col p-6 text-center">
|
||||||
|
<dt className="order-2 mt-2 text-sm font-semibold text-slate-500">
|
||||||
|
{stat.label}
|
||||||
|
</dt>
|
||||||
|
<dd className="order-1 font-heading text-4xl font-bold leading-none tracking-tight text-primary">
|
||||||
{stat.value}
|
{stat.value}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
@@ -386,7 +390,7 @@ export default function Home() {
|
|||||||
<ShieldCheck className="size-5" aria-hidden />
|
<ShieldCheck className="size-5" aria-hidden />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-xs font-semibold uppercase tracking-wider text-primary">
|
<p className="text-sm font-semibold text-primary">
|
||||||
Dürüstlük notu
|
Dürüstlük notu
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 font-heading text-lg font-bold text-slate-900 sm:text-xl">
|
<p className="mt-1 font-heading text-lg font-bold text-slate-900 sm:text-xl">
|
||||||
@@ -425,45 +429,93 @@ export default function Home() {
|
|||||||
</p>
|
</p>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</Parallax>
|
</Parallax>
|
||||||
|
{/* Tablo yatay kaydırmaz (DESIGN.md §5.1): lg altında her özellik
|
||||||
|
kendi satırında, üç cevap altında üç sütun; sütun adları
|
||||||
|
üstte yapışık durur (site başlığının hemen altında: telefonda 96 px,
|
||||||
|
sm'den itibaren 128 px — ölçüldü).
|
||||||
|
lg'de klasik dört sütunlu tablo. */}
|
||||||
<Parallax strength={7} className="mt-12">
|
<Parallax strength={7} className="mt-12">
|
||||||
<Reveal
|
<Reveal delay={0.08}>
|
||||||
delay={0.08}
|
<div className="rounded-2xl border border-slate-200/70 bg-white shadow-sm lg:hidden">
|
||||||
className="overflow-x-auto rounded-2xl border border-slate-200/70 bg-white shadow-sm"
|
<div
|
||||||
>
|
aria-hidden
|
||||||
<Table>
|
className="sticky top-24 z-10 grid grid-cols-3 sm:top-32 gap-2 rounded-t-2xl border-b border-slate-100 bg-white/95 px-3 py-3 text-center text-sm font-semibold text-slate-500 backdrop-blur"
|
||||||
<TableHeader>
|
>
|
||||||
<TableRow className="bg-slate-50">
|
<span>Robotlar ve botlar</span>
|
||||||
<TableHead className="w-[40%]">Özellik</TableHead>
|
<span>İnsan danışman</span>
|
||||||
<TableHead className="text-center">
|
<span className="font-heading font-bold text-primary">
|
||||||
Ücretsiz robotlar ve sohbet botları
|
KolayTercih
|
||||||
</TableHead>
|
</span>
|
||||||
<TableHead className="text-center">
|
</div>
|
||||||
İnsan danışman
|
<ul className="divide-y divide-slate-100">
|
||||||
</TableHead>
|
|
||||||
<TableHead className="text-center font-heading font-bold text-primary">
|
|
||||||
KolayTercih
|
|
||||||
</TableHead>
|
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{comparison.map((row) => (
|
{comparison.map((row) => (
|
||||||
<TableRow key={row.feature}>
|
<li key={row.feature} className="px-3 py-5">
|
||||||
<TableCell className="font-medium">
|
<p className="px-1 text-center text-base font-semibold leading-snug text-slate-900">
|
||||||
{row.feature}
|
{row.feature}
|
||||||
</TableCell>
|
</p>
|
||||||
<TableCell className="text-center">
|
<dl className="mt-3 grid grid-cols-3 items-center gap-2 text-center">
|
||||||
<ComparisonCell value={row.robot} />
|
<div>
|
||||||
</TableCell>
|
<dt className="sr-only">Robotlar ve botlar</dt>
|
||||||
<TableCell className="text-center">
|
<dd>
|
||||||
<ComparisonCell value={row.danisman} />
|
<ComparisonCell value={row.robot} />
|
||||||
</TableCell>
|
</dd>
|
||||||
<TableCell className="bg-primary/5 text-center">
|
</div>
|
||||||
<ComparisonCell value={row.tercihai} />
|
<div>
|
||||||
</TableCell>
|
<dt className="sr-only">İnsan danışman</dt>
|
||||||
</TableRow>
|
<dd>
|
||||||
|
<ComparisonCell value={row.danisman} />
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div className="rounded-xl bg-primary/5 py-2">
|
||||||
|
<dt className="sr-only">KolayTercih</dt>
|
||||||
|
<dd>
|
||||||
|
<ComparisonCell value={row.tercihai} vurgu />
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</li>
|
||||||
))}
|
))}
|
||||||
</TableBody>
|
</ul>
|
||||||
</Table>
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden overflow-hidden rounded-2xl border border-slate-200/70 bg-white shadow-sm lg:block">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="bg-slate-50 hover:bg-slate-50">
|
||||||
|
<TableHead className="h-12 w-[40%] pl-5 text-sm font-semibold text-slate-500">
|
||||||
|
Özellik
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="h-12 text-center text-sm font-semibold text-slate-500">
|
||||||
|
Robotlar ve sohbet botları
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="h-12 text-center text-sm font-semibold text-slate-500">
|
||||||
|
İnsan danışman
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className="h-12 text-center font-heading text-base font-bold text-primary">
|
||||||
|
KolayTercih
|
||||||
|
</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{comparison.map((row) => (
|
||||||
|
<TableRow key={row.feature} className="hover:bg-transparent">
|
||||||
|
<TableCell className="whitespace-normal py-4 pl-5 text-base font-semibold text-slate-900">
|
||||||
|
{row.feature}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="whitespace-normal py-4 text-center">
|
||||||
|
<ComparisonCell value={row.robot} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="whitespace-normal py-4 text-center">
|
||||||
|
<ComparisonCell value={row.danisman} />
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="whitespace-normal bg-primary/5 py-4 text-center">
|
||||||
|
<ComparisonCell value={row.tercihai} vurgu />
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</Parallax>
|
</Parallax>
|
||||||
</div>
|
</div>
|
||||||
@@ -629,28 +681,14 @@ export default function Home() {
|
|||||||
<section className="px-4 pb-24 pt-6 sm:px-6">
|
<section className="px-4 pb-24 pt-6 sm:px-6">
|
||||||
<Parallax strength={12} className="mx-auto max-w-6xl">
|
<Parallax strength={12} className="mx-auto max-w-6xl">
|
||||||
<Reveal>
|
<Reveal>
|
||||||
<div className="relative isolate overflow-hidden rounded-3xl bg-primary px-4 py-20 text-white shadow-xl shadow-primary/20 sm:py-24">
|
{/* Funnel CTA yüzeyi (DESIGN.md §10.8): köşede kümelenen
|
||||||
{/* Hero'nun piksel dokusu koyu zeminde: ortası boş, kenarlara doğru yoğun */}
|
piksel zemini yerine 24'lük listenin interaktif kreatifi */}
|
||||||
{/* Piksel zemin panel içinde geride kalır; taşma payı için dikeyde büyütülür */}
|
<PikselCtaYuzeyi metinOfseti={7}>
|
||||||
<div
|
<h2 className="font-heading text-3xl font-bold text-balance sm:text-4xl">
|
||||||
aria-hidden
|
Tercih listen, pişmanlık listesi olmasın
|
||||||
className="pointer-events-none absolute inset-x-0 -inset-y-10 -z-10 text-white"
|
</h2>
|
||||||
>
|
<KapanisCta />
|
||||||
<Parallax strength={-22} className="h-full">
|
</PikselCtaYuzeyi>
|
||||||
<PixelField seed={8} />
|
|
||||||
</Parallax>
|
|
||||||
</div>
|
|
||||||
<div className="mx-auto flex max-w-3xl flex-col items-center text-center">
|
|
||||||
<MessageCircleQuestion
|
|
||||||
className="size-10 opacity-80"
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
<h2 className="mt-6 font-heading text-3xl font-bold sm:text-4xl">
|
|
||||||
Tercih listen, pişmanlık listesi olmasın
|
|
||||||
</h2>
|
|
||||||
<KapanisCta />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Reveal>
|
</Reveal>
|
||||||
</Parallax>
|
</Parallax>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -29,13 +29,19 @@ export default function RehberYaziPage({
|
|||||||
<main className="mx-auto w-full max-w-3xl px-4 py-12 sm:py-16">
|
<main className="mx-auto w-full max-w-3xl px-4 py-12 sm:py-16">
|
||||||
<nav
|
<nav
|
||||||
aria-label="breadcrumb"
|
aria-label="breadcrumb"
|
||||||
className="flex items-center gap-1 text-xs text-slate-500"
|
className="flex items-center gap-1.5 text-sm text-slate-500"
|
||||||
>
|
>
|
||||||
<Link href="/" className="hover:text-primary hover:underline">
|
<Link
|
||||||
|
href="/"
|
||||||
|
className="transition-colors hover:text-orange-500 hover:underline"
|
||||||
|
>
|
||||||
Ana Sayfa
|
Ana Sayfa
|
||||||
</Link>
|
</Link>
|
||||||
<ChevronRight className="size-3" aria-hidden />
|
<ChevronRight className="size-3.5" aria-hidden />
|
||||||
<Link href="/rehber" className="hover:text-primary hover:underline">
|
<Link
|
||||||
|
href="/rehber"
|
||||||
|
className="transition-colors hover:text-orange-500 hover:underline"
|
||||||
|
>
|
||||||
Rehber
|
Rehber
|
||||||
</Link>
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
PROFIL_CEREZ_ADI,
|
PROFIL_CEREZ_ADI,
|
||||||
} from "@/features/sihirbaz/sihirbaz-sabitler";
|
} from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||||
import {
|
import {
|
||||||
|
SonucBasligi,
|
||||||
SonucProgramTablosu,
|
SonucProgramTablosu,
|
||||||
SonucProgramTablosuSkeleton,
|
SonucProgramTablosuSkeleton,
|
||||||
} from "@/features/liste/components/sonuc-program-tablosu";
|
} from "@/features/liste/components/sonuc-program-tablosu";
|
||||||
@@ -133,7 +134,16 @@ export default function SonucPage({
|
|||||||
boyada haritanın tam ölçülü kutusunu çizer, chunk inince
|
boyada haritanın tam ölçülü kutusunu çizer, chunk inince
|
||||||
kutunun yalnızca İÇİ dolar. Haritanın yerini bir daha
|
kutunun yalnızca İÇİ dolar. Haritanın yerini bir daha
|
||||||
aşağı almadan önce bu mekanizmayı kontrol et. */}
|
aşağı almadan önce bu mekanizmayı kontrol et. */}
|
||||||
<ManuelHarita yerAyir={haritayaYerAyir} />
|
{/* Sayfanın tek cümlelik cevabı ve h1'i: öğrenci önce kendi
|
||||||
|
sonucunu görür (DESIGN.md §10). Harita ve tadımlık altında. */}
|
||||||
|
<SonucBasligi
|
||||||
|
sira={sira}
|
||||||
|
tur={turKey}
|
||||||
|
iller={iller}
|
||||||
|
uniturGrubu={uniturGrubu}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<ManuelHarita yerAyir={haritayaYerAyir} className="mt-10" />
|
||||||
|
|
||||||
{/* Girişsiz tadımlık: gerçek rapor satırı formatında tek örnek.
|
{/* Girişsiz tadımlık: gerçek rapor satırı formatında tek örnek.
|
||||||
Havuzda içerik yoksa bileşen hiç render edilmez. Listesi
|
Havuzda içerik yoksa bileşen hiç render edilmez. Listesi
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
ListChecks,
|
ListChecks,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { FUNNEL_METINLERI } from "@/lib/funnel-metinleri";
|
||||||
import { TypingAnimation } from "@/components/ui/typing-animation";
|
import { TypingAnimation } from "@/components/ui/typing-animation";
|
||||||
import { SIHIRBAZ_AC_EVENT, sonucHref } from "@/features/sihirbaz/sihirbaz-sabitler";
|
import { SIHIRBAZ_AC_EVENT, sonucHref } from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||||
import {
|
import {
|
||||||
@@ -16,39 +17,6 @@ import {
|
|||||||
useTercihProfili,
|
useTercihProfili,
|
||||||
} from "@/features/liste/hooks/use-tercih-profili";
|
} from "@/features/liste/hooks/use-tercih-profili";
|
||||||
|
|
||||||
const FUNNEL_METINLERI = [
|
|
||||||
"Sıralamana en uygun listeyi birlikte kuralım.",
|
|
||||||
"Geleceğin 24 satırlık bir plana sığar.",
|
|
||||||
"Gerçek YÖK Atlas verisiyle güvenle seç.",
|
|
||||||
"Her tercihinin bir nedeni olsun.",
|
|
||||||
"Riskleri biz hesaplayalım, kararı sen ver.",
|
|
||||||
"Sana uygun bölümler bir adım uzağında.",
|
|
||||||
"Dengeli bir liste, rahat bir tercih dönemi.",
|
|
||||||
"Tercihlerini şansa bırakma.",
|
|
||||||
"Doğru strateji, doğru yerleşme.",
|
|
||||||
"Hayalindeki bölüm için doğru plan.",
|
|
||||||
"Verilerle konuşan bir tercih listesi.",
|
|
||||||
"Üç adımda sana özel 24 tercih.",
|
|
||||||
"Tercih dönemini stressiz geçir.",
|
|
||||||
"Geçen yılın verisi, senin avantajın.",
|
|
||||||
"Güvenli ve iddialı tercihleri dengele.",
|
|
||||||
"Sıralaman ne söylüyor? Birlikte bakalım.",
|
|
||||||
"Her satırı senin için hesaplıyoruz.",
|
|
||||||
"Akıllı tercih, içi rahat bir bekleyiş.",
|
|
||||||
"Yerleşme riskini görerek seç.",
|
|
||||||
"Sana özel liste birkaç dakikada hazır.",
|
|
||||||
"İlgi alanların ve şehrin, tek listede.",
|
|
||||||
"Boş kalan tercih, kaçan fırsattır.",
|
|
||||||
"Uzman işi bir liste, senin onayınla.",
|
|
||||||
"Kararını veriye dayandır.",
|
|
||||||
"Doğru tercih, doğru bilgiyle başlar.",
|
|
||||||
"Listeni bugün kur, için rahat olsun.",
|
|
||||||
"Yerleşme riskin renklerle önünde.",
|
|
||||||
"En önemli 24 kararını birlikte verelim.",
|
|
||||||
"Senin sıralaman, sana özel plan.",
|
|
||||||
"İyi bir liste, iyi bir başlangıçtır.",
|
|
||||||
];
|
|
||||||
|
|
||||||
/** Fisher-Yates ile kopya üzerinde karıştırır; her açılışta farklı sıra. */
|
/** Fisher-Yates ile kopya üzerinde karıştırır; her açılışta farklı sıra. */
|
||||||
function karistir<T>(dizi: readonly T[]): T[] {
|
function karistir<T>(dizi: readonly T[]): T[] {
|
||||||
const kopya = [...dizi];
|
const kopya = [...dizi];
|
||||||
@@ -102,7 +70,7 @@ export function AramaFunnelKarti({ onSec }: { onSec: () => void }) {
|
|||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
aria-label="Tercih listeni kur"
|
aria-label="Tercih listeni kur"
|
||||||
className="mt-5 overflow-hidden rounded-2xl border border-blue-400/20 bg-[#0b1f3a] px-4 py-5 text-white sm:px-5"
|
className="mt-5 overflow-hidden rounded-2xl border border-blue-400/20 bg-slate-950 px-4 py-5 text-white sm:px-5"
|
||||||
>
|
>
|
||||||
<div className="grid items-center gap-4 sm:grid-cols-[minmax(0,1fr)_auto] sm:gap-5">
|
<div className="grid items-center gap-4 sm:grid-cols-[minmax(0,1fr)_auto] sm:gap-5">
|
||||||
<div className="min-w-0">
|
<div className="min-w-0">
|
||||||
|
|||||||
58
src/components/liste-satiri.ts
Normal file
58
src/components/liste-satiri.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* Liste satırının (DESIGN.md §4.15) sınıf dizileri — tek kaynak. Üniversite,
|
||||||
|
* bölüm ve /sonuc program tabloları (ileride rapor listesi) aynı satırı
|
||||||
|
* çizer; sınıflar her dosyada ayrı yazılınca ölçüler kayıyordu. "use client"
|
||||||
|
* yok: sunucu bileşenleri de değer olarak okuyabilsin (§9.3).
|
||||||
|
*/
|
||||||
|
|
||||||
|
/** Satırın adı (bölüm/üniversite sayfasına link): sarar, hover mavi (§2.1 istisnası). */
|
||||||
|
export const SATIR_AD =
|
||||||
|
"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";
|
||||||
|
|
||||||
|
/** Link olmayan ad (slug yoksa). */
|
||||||
|
export const SATIR_AD_DUZ =
|
||||||
|
"block text-base font-semibold leading-snug text-slate-900";
|
||||||
|
|
||||||
|
/** Adın altındaki ikinci satır (varyant ya da üniversite adı). */
|
||||||
|
export const SATIR_ALT_AD = "mt-0.5 block text-sm leading-snug text-slate-600";
|
||||||
|
|
||||||
|
/** Satırdaki ikincil link (ör. üniversite adı) — §2.1 tek turuncu. */
|
||||||
|
export const SATIR_IKINCIL_LINK =
|
||||||
|
"transition-colors hover:text-orange-500 hover:underline";
|
||||||
|
|
||||||
|
/** Tür/il/süre satırı. Geniş ekranda (sm+) üstündeki adla aynı satıra
|
||||||
|
* çıkar (SATIR_GRUP), telefonda altına iner. */
|
||||||
|
export const SATIR_META =
|
||||||
|
"mt-2 flex flex-wrap items-center gap-x-2 gap-y-1 text-sm text-slate-500 sm:mt-0";
|
||||||
|
|
||||||
|
/** Ad (ya da üniversite adı) + tür/il/süre satırını geniş ekranda yan yana
|
||||||
|
* dizen sarmal. Bilal'in isteği (25 Eyl 2026): geniş ekranda satırlar üç
|
||||||
|
* katlı ve "çok kalın" duruyordu. Telefonda alt alta kalır. */
|
||||||
|
export const SATIR_GRUP =
|
||||||
|
"sm:flex sm:flex-wrap sm:items-center sm:gap-x-3 sm:gap-y-1";
|
||||||
|
|
||||||
|
/** Tür çipi (Devlet, Vakıf, SAY…). */
|
||||||
|
export const SATIR_CIP =
|
||||||
|
"shrink-0 rounded-full bg-slate-100 px-2.5 py-0.5 text-xs font-semibold text-slate-700";
|
||||||
|
|
||||||
|
/** Telefonda sayısal sütunun adın altına indiği satır. */
|
||||||
|
export const SATIR_MOBIL_SAYI =
|
||||||
|
"mt-1.5 flex flex-wrap items-center gap-x-1.5 gap-y-1 text-sm text-slate-500 sm:hidden";
|
||||||
|
|
||||||
|
/** Tablo başlık hücresi. */
|
||||||
|
export const SATIR_BASLIK = "h-12 text-sm font-semibold text-slate-500";
|
||||||
|
|
||||||
|
/** Adın bulunduğu hücre. */
|
||||||
|
export const SATIR_AD_HUCRE = "w-full whitespace-normal py-4 pl-4 sm:pl-5 lg:py-3";
|
||||||
|
|
||||||
|
/** Butonların hücresi ve kabı: telefonda alt alta, sm'den yan yana.
|
||||||
|
* Masaüstünde (lg) satır ince kalsın diye dolgu bir kademe az ve satır
|
||||||
|
* düğmeleri 36 px (DESIGN.md §8 masaüstü tablo istisnası). */
|
||||||
|
export const SATIR_BUTON_HUCRE = "py-4 pr-4 text-right sm:pr-5 lg:py-3";
|
||||||
|
export const SATIR_BUTON_KABI =
|
||||||
|
"inline-flex flex-col items-center gap-2 sm:flex-row";
|
||||||
|
export const SATIR_BUTON_BASLIK = "h-12 w-14 text-right sm:w-28";
|
||||||
|
|
||||||
|
/** Satır düğmesinin boyu: dokunmada 44 px, masaüstü tabloda 36 px (§8). */
|
||||||
|
export const SATIR_DUGME_BOYU =
|
||||||
|
"size-11 min-h-11 min-w-11 lg:size-9 lg:min-h-9 lg:min-w-9";
|
||||||
58
src/components/ozet-kart.tsx
Normal file
58
src/components/ozet-kart.tsx
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Özet kartı (DESIGN.md §4.8 — 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 (§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. Üniversite ve bölüm sayfalarının 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; çağıran
|
||||||
|
* kartları bir `<dl>` içine koyar.
|
||||||
|
*/
|
||||||
|
export 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
77
src/components/piksel-cta-yuzeyi.tsx
Normal file
77
src/components/piksel-cta-yuzeyi.tsx
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, type ReactNode } from "react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { TypingAnimation } from "@/components/ui/typing-animation";
|
||||||
|
import { FUNNEL_METINLERI } from "@/lib/funnel-metinleri";
|
||||||
|
import { PikselListe, PikselListeAciklama } from "@/components/piksel-liste";
|
||||||
|
|
||||||
|
/** Listeyi `ofset` kadar döndürür: aynı sayfadaki yüzeyler farklı satırla
|
||||||
|
* başlar. Rastgele değil, deterministik — sunucu ve istemci aynı çizer. */
|
||||||
|
function dondur<T>(dizi: readonly T[], ofset: number): T[] {
|
||||||
|
const n = ofset % dizi.length;
|
||||||
|
return [...dizi.slice(n), ...dizi.slice(0, n)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Funnel CTA yüzeyi (DESIGN.md §10.8) — arama penceresindeki kartın dili:
|
||||||
|
* koyu slate-950 zemin, ince mavi kenar, yazılıp silinen kısa satırlar,
|
||||||
|
* turuncu CTA. Pikseller bir köşede kümelenmez; yüzeyin kreatifi 24'lük
|
||||||
|
* listenin kendisidir (PikselListe). Kartın içindeki bir düğmeye ya da
|
||||||
|
* linke imleç/odak gelince 24 kare birden dolar — "listen böyle hazır olur".
|
||||||
|
*/
|
||||||
|
export function PikselCtaYuzeyi({
|
||||||
|
children,
|
||||||
|
className,
|
||||||
|
metinOfseti = 0,
|
||||||
|
kayanMetin = true,
|
||||||
|
}: {
|
||||||
|
children: ReactNode;
|
||||||
|
className?: string;
|
||||||
|
/** Aynı sayfadaki iki yüzey aynı satırla başlamasın */
|
||||||
|
metinOfseti?: number;
|
||||||
|
kayanMetin?: boolean;
|
||||||
|
}) {
|
||||||
|
const [tamamla, setTamamla] = useState(false);
|
||||||
|
const [metinler] = useState(() => dondur(FUNNEL_METINLERI, metinOfseti));
|
||||||
|
|
||||||
|
const hedefMi = (t: EventTarget | null) =>
|
||||||
|
t instanceof Element && t.closest("a, button") != null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section
|
||||||
|
onPointerOver={(e) => setTamamla(hedefMi(e.target))}
|
||||||
|
onPointerLeave={() => setTamamla(false)}
|
||||||
|
onFocusCapture={(e) => setTamamla(hedefMi(e.target))}
|
||||||
|
onBlurCapture={() => setTamamla(false)}
|
||||||
|
className={cn(
|
||||||
|
"relative isolate overflow-hidden rounded-3xl border border-blue-400/20 bg-slate-950 text-white shadow-xl shadow-slate-900/10",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="grid items-center gap-8 px-5 py-10 sm:px-10 sm:py-12 lg:grid-cols-[minmax(0,1fr)_16rem] lg:gap-12">
|
||||||
|
<div className="flex min-w-0 flex-col items-center text-center lg:items-start lg:text-left">
|
||||||
|
{children}
|
||||||
|
{kayanMetin ? (
|
||||||
|
<TypingAnimation
|
||||||
|
as="p"
|
||||||
|
words={metinler}
|
||||||
|
loop
|
||||||
|
typeSpeed={38}
|
||||||
|
deleteSpeed={18}
|
||||||
|
pauseDelay={2600}
|
||||||
|
className="mt-5 min-h-[2.75em] font-heading text-base font-bold leading-snug tracking-[-0.02em] text-slate-300 sm:min-h-[1.4em]"
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="order-first flex flex-col items-center lg:order-none">
|
||||||
|
<PikselListe
|
||||||
|
tamamla={tamamla}
|
||||||
|
className="w-full max-w-[9rem] sm:max-w-[11rem] lg:max-w-[14rem]"
|
||||||
|
/>
|
||||||
|
<PikselListeAciklama className="mt-4 justify-center" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
191
src/components/piksel-liste.tsx
Normal file
191
src/components/piksel-liste.tsx
Normal file
@@ -0,0 +1,191 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
useCallback,
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
type PointerEvent as ReactPointerEvent,
|
||||||
|
type ReactNode,
|
||||||
|
} from "react";
|
||||||
|
import { useReducedMotion } from "motion/react";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { DILIM_ETIKET } from "@/lib/risk";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 24'lük tercih listesinin piksel hâli — CTA yüzeylerinin interaktif
|
||||||
|
* kreatifi (Bilal'in isteği, 25 Eyl 2026: "pikseller bir köşede
|
||||||
|
* kümelenmesin, interaktif kreatif olsun"). Her kare bir tercih; hero'daki
|
||||||
|
* dot-matrix ölçüsüyle (hücre 10, nokta 7.4, rx 2).
|
||||||
|
*
|
||||||
|
* Davranış (DESIGN.md §10.8):
|
||||||
|
* - Örnek modda liste kendini kurar: kareler sırayla dolar (5 hayal ·
|
||||||
|
* 13 dengeli · 6 güvenli), bir süre durur, sakin söner, yeniden başlar.
|
||||||
|
* Yalnız ekrandayken çalışır.
|
||||||
|
* - İmleç ızgaranın üstündeyken yakındaki kareler hafifçe büyür.
|
||||||
|
* - `tamamla` true ise (ör. CTA düğmesine gelince) 24 kare birden dolar.
|
||||||
|
* - Dokunmada kareye basınca o kare ve komşuları kısa bir nabız atar.
|
||||||
|
* - Hareketi azalt: dolu ve durağan ızgara.
|
||||||
|
* - `gercek` verilirse döngü yok; kareler öğrencinin kendi listesidir
|
||||||
|
* (her kare o tercihin riskiyle).
|
||||||
|
* Renk tek başına anlam taşımaz: çağıran `PikselListeAciklama` ile metni
|
||||||
|
* yanına koyar.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type PikselKare = "hayal" | "dengeli" | "guvenli" | "belirsiz" | "bos";
|
||||||
|
|
||||||
|
const SUTUN = 6;
|
||||||
|
const SATIR = 4;
|
||||||
|
const TOPLAM = SUTUN * SATIR;
|
||||||
|
const CELL = 10;
|
||||||
|
const DOT = 7.4;
|
||||||
|
const INSET = (CELL - DOT) / 2;
|
||||||
|
|
||||||
|
const ORNEK: PikselKare[] = Array.from({ length: TOPLAM }, (_, i) =>
|
||||||
|
i < 5 ? "hayal" : i < 18 ? "dengeli" : "guvenli",
|
||||||
|
);
|
||||||
|
|
||||||
|
const RENK: Record<PikselKare, string> = {
|
||||||
|
hayal: "fill-red-500/85",
|
||||||
|
dengeli: "fill-amber-400/85",
|
||||||
|
guvenli: "fill-emerald-500/85",
|
||||||
|
belirsiz: "fill-white/55",
|
||||||
|
bos: "fill-white/15",
|
||||||
|
};
|
||||||
|
|
||||||
|
const ADIM_MS = 110;
|
||||||
|
const BEKLE_MS = 2600;
|
||||||
|
|
||||||
|
export function PikselListe({
|
||||||
|
tamamla = false,
|
||||||
|
gercek,
|
||||||
|
sutun = SUTUN,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
tamamla?: boolean;
|
||||||
|
/** Öğrencinin gerçek listesi (sırasıyla); verilirse örnek döngü kapanır */
|
||||||
|
gercek?: PikselKare[];
|
||||||
|
sutun?: number;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
|
const hareketAzalt = useReducedMotion();
|
||||||
|
const satir = Math.ceil(TOPLAM / sutun);
|
||||||
|
const svgRef = useRef<SVGSVGElement>(null);
|
||||||
|
const [dolu, setDolu] = useState(hareketAzalt || gercek ? TOPLAM : 0);
|
||||||
|
const [gorunur, setGorunur] = useState(false);
|
||||||
|
const [imlec, setImlec] = useState<{ x: number; y: number } | null>(null);
|
||||||
|
const [nabiz, setNabiz] = useState<{ i: number; t: number } | null>(null);
|
||||||
|
|
||||||
|
// Yalnız ekrandayken çalış (IntersectionObserver, DESIGN.md §6)
|
||||||
|
useEffect(() => {
|
||||||
|
const el = svgRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
const io = new IntersectionObserver(([e]) => setGorunur(e.isIntersecting), {
|
||||||
|
rootMargin: "0px 0px -10% 0px",
|
||||||
|
});
|
||||||
|
io.observe(el);
|
||||||
|
return () => io.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Örnek döngü: 0 → 24 sırayla, bekle, sıfırla
|
||||||
|
useEffect(() => {
|
||||||
|
if (gercek || hareketAzalt || !gorunur || tamamla) return;
|
||||||
|
const id = window.setTimeout(
|
||||||
|
() => setDolu((d) => (d >= TOPLAM ? 0 : d + 1)),
|
||||||
|
dolu >= TOPLAM ? BEKLE_MS : dolu === 0 ? 500 : ADIM_MS,
|
||||||
|
);
|
||||||
|
return () => window.clearTimeout(id);
|
||||||
|
}, [dolu, gorunur, gercek, hareketAzalt, tamamla]);
|
||||||
|
|
||||||
|
const kareler: PikselKare[] = gercek
|
||||||
|
? Array.from({ length: TOPLAM }, (_, i) => gercek[i] ?? "bos")
|
||||||
|
: ORNEK.map((k, i) => (tamamla || i < dolu ? k : "bos"));
|
||||||
|
|
||||||
|
const imlecGuncelle = useCallback((e: ReactPointerEvent<SVGSVGElement>) => {
|
||||||
|
if (e.pointerType !== "mouse") return;
|
||||||
|
const svg = svgRef.current;
|
||||||
|
if (!svg) return;
|
||||||
|
const r = svg.getBoundingClientRect();
|
||||||
|
setImlec({
|
||||||
|
x: ((e.clientX - r.left) / r.width) * sutun * CELL,
|
||||||
|
y: ((e.clientY - r.top) / r.height) * satir * CELL,
|
||||||
|
});
|
||||||
|
}, [sutun, satir]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
ref={svgRef}
|
||||||
|
viewBox={`0 0 ${sutun * CELL} ${satir * CELL}`}
|
||||||
|
aria-hidden="true"
|
||||||
|
focusable="false"
|
||||||
|
onPointerMove={imlecGuncelle}
|
||||||
|
onPointerLeave={() => setImlec(null)}
|
||||||
|
className={cn("h-auto w-full touch-manipulation", className)}
|
||||||
|
>
|
||||||
|
{kareler.map((k, i) => {
|
||||||
|
const x = (i % sutun) * CELL;
|
||||||
|
const y = Math.floor(i / sutun) * CELL;
|
||||||
|
const cx = x + CELL / 2;
|
||||||
|
const cy = y + CELL / 2;
|
||||||
|
// İmlece yakın kare büyür (en çok %18), uzaklaştıkça etkisi söner
|
||||||
|
let olcek = 1;
|
||||||
|
if (imlec && !hareketAzalt) {
|
||||||
|
const d = Math.hypot(imlec.x - cx, imlec.y - cy);
|
||||||
|
olcek = 1 + Math.max(0, 1 - d / 16) * 0.18;
|
||||||
|
}
|
||||||
|
const nabizda =
|
||||||
|
nabiz &&
|
||||||
|
Math.abs((nabiz.i % sutun) - (i % sutun)) <= 1 &&
|
||||||
|
Math.abs(Math.floor(nabiz.i / sutun) - Math.floor(i / sutun)) <= 1;
|
||||||
|
return (
|
||||||
|
<rect
|
||||||
|
key={nabizda ? `${i}-${nabiz.t}` : i}
|
||||||
|
x={x + INSET}
|
||||||
|
y={y + INSET}
|
||||||
|
width={DOT}
|
||||||
|
height={DOT}
|
||||||
|
rx={2}
|
||||||
|
onPointerDown={(e) => {
|
||||||
|
if (e.pointerType === "mouse" || hareketAzalt) return;
|
||||||
|
setNabiz({ i, t: Date.now() });
|
||||||
|
}}
|
||||||
|
className={cn(
|
||||||
|
RENK[k],
|
||||||
|
"origin-center transition-[fill,transform] duration-300 ease-[cubic-bezier(0.23,1,0.32,1)] [transform-box:fill-box] motion-reduce:transition-none",
|
||||||
|
nabizda && "kt-piksel-nabiz",
|
||||||
|
)}
|
||||||
|
style={{ transform: olcek !== 1 ? `scale(${olcek})` : undefined }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Renklerin metin karşılığı (§2.2: renk tek başına anlam taşımaz). */
|
||||||
|
export function PikselListeAciklama({
|
||||||
|
className,
|
||||||
|
sayilar = { hayal: 5, dengeli: 13, guvenli: 6 },
|
||||||
|
}: {
|
||||||
|
className?: string;
|
||||||
|
sayilar?: { hayal: number; dengeli: number; guvenli: number };
|
||||||
|
}) {
|
||||||
|
const oge = (renk: string, sayi: number, etiket: string): ReactNode => (
|
||||||
|
<span className="inline-flex items-center gap-1.5">
|
||||||
|
<span className={cn("size-2 shrink-0 rounded-[2px]", renk)} aria-hidden />
|
||||||
|
{sayi} {etiket.toLocaleLowerCase("tr-TR")}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
"flex flex-wrap items-center gap-x-3 gap-y-1 text-sm font-medium text-slate-300",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{oge("bg-red-500", sayilar.hayal, DILIM_ETIKET.hayal)}
|
||||||
|
{oge("bg-amber-400", sayilar.dengeli, DILIM_ETIKET.dengeli)}
|
||||||
|
{oge("bg-emerald-500", sayilar.guvenli, DILIM_ETIKET.garanti)}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
32
src/components/risk-etiketi.tsx
Normal file
32
src/components/risk-etiketi.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { RISK_ETIKET, type RiskSeviyesi } from "@/lib/risk";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Risk etiketi (DESIGN.md §4.5) — tek kaynak: renkli nokta + aynı renkte
|
||||||
|
* metin. Renk hiçbir yerde tek başına durmaz; metin RISK_ETIKET'ten gelir.
|
||||||
|
* /sonuc tablosu, liste çekmecesi ve rapor listesi buradan kullanır.
|
||||||
|
*/
|
||||||
|
export const RISK_NOKTA: Record<RiskSeviyesi, string> = {
|
||||||
|
guvenli: "bg-emerald-500",
|
||||||
|
"az-riskli": "bg-amber-500",
|
||||||
|
riskli: "bg-red-500",
|
||||||
|
};
|
||||||
|
|
||||||
|
const RISK_METIN: Record<RiskSeviyesi, string> = {
|
||||||
|
guvenli: "text-emerald-600",
|
||||||
|
"az-riskli": "text-amber-600",
|
||||||
|
riskli: "text-red-600",
|
||||||
|
};
|
||||||
|
|
||||||
|
export function RiskEtiketi({ risk }: { risk: RiskSeviyesi }) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center gap-1.5 text-xs font-semibold ${RISK_METIN[risk]}`}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`size-2 shrink-0 rounded-full ${RISK_NOKTA[risk]}`}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
{RISK_ETIKET[risk]}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ export function SiteFooter() {
|
|||||||
<div className="mx-auto max-w-6xl px-4">
|
<div className="mx-auto max-w-6xl px-4">
|
||||||
<div className="grid gap-10 sm:grid-cols-2 lg:grid-cols-3">
|
<div className="grid gap-10 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
<nav aria-label="Popüler bölümler">
|
<nav aria-label="Popüler bölümler">
|
||||||
<h2 className="text-xs font-semibold uppercase tracking-wide text-slate-500">
|
<h2 className="text-sm font-semibold text-slate-900">
|
||||||
Popüler Bölümler
|
Popüler Bölümler
|
||||||
</h2>
|
</h2>
|
||||||
<ul className="mt-3 grid grid-cols-1 gap-1.5">
|
<ul className="mt-3 grid grid-cols-1 gap-1.5">
|
||||||
@@ -26,7 +26,7 @@ export function SiteFooter() {
|
|||||||
<li key={b.slug}>
|
<li key={b.slug}>
|
||||||
<Link
|
<Link
|
||||||
href={`/bolum/${b.slug}`}
|
href={`/bolum/${b.slug}`}
|
||||||
className="text-sm text-slate-600 hover:text-slate-900 hover:underline"
|
className="text-sm text-slate-600 transition-colors hover:text-orange-500 hover:underline"
|
||||||
>
|
>
|
||||||
{b.ad} Taban Puanları
|
{b.ad} Taban Puanları
|
||||||
</Link>
|
</Link>
|
||||||
@@ -36,14 +36,14 @@ export function SiteFooter() {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<nav aria-label="Keşfet">
|
<nav aria-label="Keşfet">
|
||||||
<h2 className="text-xs font-semibold uppercase tracking-wide text-slate-500">
|
<h2 className="text-sm font-semibold text-slate-900">
|
||||||
Keşfet
|
Keşfet
|
||||||
</h2>
|
</h2>
|
||||||
<ul className="mt-3 space-y-1.5">
|
<ul className="mt-3 space-y-1.5">
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<Link
|
||||||
href="/#hero-form"
|
href="/#hero-form"
|
||||||
className="text-sm text-slate-600 hover:text-slate-900 hover:underline"
|
className="text-sm text-slate-600 transition-colors hover:text-orange-500 hover:underline"
|
||||||
>
|
>
|
||||||
YKS Tercih Robotu 2026
|
YKS Tercih Robotu 2026
|
||||||
</Link>
|
</Link>
|
||||||
@@ -51,7 +51,7 @@ export function SiteFooter() {
|
|||||||
<li>
|
<li>
|
||||||
<Link
|
<Link
|
||||||
href="/bolumler"
|
href="/bolumler"
|
||||||
className="text-sm text-slate-600 hover:text-slate-900 hover:underline"
|
className="text-sm text-slate-600 transition-colors hover:text-orange-500 hover:underline"
|
||||||
>
|
>
|
||||||
Bölüm Taban Puanları
|
Bölüm Taban Puanları
|
||||||
</Link>
|
</Link>
|
||||||
@@ -59,7 +59,7 @@ export function SiteFooter() {
|
|||||||
<li>
|
<li>
|
||||||
<Link
|
<Link
|
||||||
href="/universiteler"
|
href="/universiteler"
|
||||||
className="text-sm text-slate-600 hover:text-slate-900 hover:underline"
|
className="text-sm text-slate-600 transition-colors hover:text-orange-500 hover:underline"
|
||||||
>
|
>
|
||||||
Üniversite Taban Puanları
|
Üniversite Taban Puanları
|
||||||
</Link>
|
</Link>
|
||||||
@@ -67,7 +67,7 @@ export function SiteFooter() {
|
|||||||
<li>
|
<li>
|
||||||
<Link
|
<Link
|
||||||
href="/rehber"
|
href="/rehber"
|
||||||
className="text-sm text-slate-600 hover:text-slate-900 hover:underline"
|
className="text-sm text-slate-600 transition-colors hover:text-orange-500 hover:underline"
|
||||||
>
|
>
|
||||||
Tercih Rehberi
|
Tercih Rehberi
|
||||||
</Link>
|
</Link>
|
||||||
@@ -75,7 +75,7 @@ export function SiteFooter() {
|
|||||||
<li>
|
<li>
|
||||||
<Link
|
<Link
|
||||||
href="/meraklisina"
|
href="/meraklisina"
|
||||||
className="text-sm text-slate-600 hover:text-slate-900 hover:underline"
|
className="text-sm text-slate-600 transition-colors hover:text-orange-500 hover:underline"
|
||||||
>
|
>
|
||||||
Meraklısına: Nasıl çalışır?
|
Meraklısına: Nasıl çalışır?
|
||||||
</Link>
|
</Link>
|
||||||
@@ -84,14 +84,14 @@ export function SiteFooter() {
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<nav aria-label="Rehber yazıları">
|
<nav aria-label="Rehber yazıları">
|
||||||
<h2 className="text-xs font-semibold uppercase tracking-wide text-slate-500">
|
<h2 className="text-sm font-semibold text-slate-900">
|
||||||
Rehberden
|
Rehberden
|
||||||
</h2>
|
</h2>
|
||||||
<ul className="mt-3 space-y-1.5">
|
<ul className="mt-3 space-y-1.5">
|
||||||
<li>
|
<li>
|
||||||
<Link
|
<Link
|
||||||
href="/rehber/tercih-listesi-nasil-yapilir"
|
href="/rehber/tercih-listesi-nasil-yapilir"
|
||||||
className="text-sm text-slate-600 hover:text-slate-900 hover:underline"
|
className="text-sm text-slate-600 transition-colors hover:text-orange-500 hover:underline"
|
||||||
>
|
>
|
||||||
Tercih Listesi Nasıl Yapılır?
|
Tercih Listesi Nasıl Yapılır?
|
||||||
</Link>
|
</Link>
|
||||||
@@ -99,7 +99,7 @@ export function SiteFooter() {
|
|||||||
<li>
|
<li>
|
||||||
<Link
|
<Link
|
||||||
href="/rehber/kac-siralama-ile-hangi-bolume-girebilirim"
|
href="/rehber/kac-siralama-ile-hangi-bolume-girebilirim"
|
||||||
className="text-sm text-slate-600 hover:text-slate-900 hover:underline"
|
className="text-sm text-slate-600 transition-colors hover:text-orange-500 hover:underline"
|
||||||
>
|
>
|
||||||
Kaç Sıralama ile Hangi Bölüme Girebilirim?
|
Kaç Sıralama ile Hangi Bölüme Girebilirim?
|
||||||
</Link>
|
</Link>
|
||||||
@@ -107,7 +107,7 @@ export function SiteFooter() {
|
|||||||
<li>
|
<li>
|
||||||
<Link
|
<Link
|
||||||
href="/rehber/olu-tercih-nedir"
|
href="/rehber/olu-tercih-nedir"
|
||||||
className="text-sm text-slate-600 hover:text-slate-900 hover:underline"
|
className="text-sm text-slate-600 transition-colors hover:text-orange-500 hover:underline"
|
||||||
>
|
>
|
||||||
Ölü Tercih Nedir?
|
Ölü Tercih Nedir?
|
||||||
</Link>
|
</Link>
|
||||||
@@ -115,7 +115,7 @@ export function SiteFooter() {
|
|||||||
<li>
|
<li>
|
||||||
<Link
|
<Link
|
||||||
href="/rehber/ek-yerlestirme-2026"
|
href="/rehber/ek-yerlestirme-2026"
|
||||||
className="text-sm text-slate-600 hover:text-slate-900 hover:underline"
|
className="text-sm text-slate-600 transition-colors hover:text-orange-500 hover:underline"
|
||||||
>
|
>
|
||||||
Ek Yerleştirme Rehberi
|
Ek Yerleştirme Rehberi
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ export function SiteTopBanner() {
|
|||||||
) : (
|
) : (
|
||||||
"--"
|
"--"
|
||||||
)}
|
)}
|
||||||
<span className="ml-0.5 text-[10px] font-medium opacity-70">
|
<span className="ml-0.5 text-xs font-medium opacity-70">
|
||||||
{etiket}
|
{etiket}
|
||||||
</span>
|
</span>
|
||||||
</dd>
|
</dd>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ const BOYUT: Record<"xs" | "sm" | "md" | "lg", string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const MONOGRAM_METIN: Record<"xs" | "sm" | "md" | "lg", string> = {
|
const MONOGRAM_METIN: Record<"xs" | "sm" | "md" | "lg", string> = {
|
||||||
xs: "text-[10px]",
|
xs: "text-xs",
|
||||||
sm: "text-xs",
|
sm: "text-xs",
|
||||||
md: "text-sm",
|
md: "text-sm",
|
||||||
lg: "text-lg sm:text-xl",
|
lg: "text-lg sm:text-xl",
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { ChevronRight } from "lucide-react";
|
import {
|
||||||
|
Building2,
|
||||||
|
Calculator,
|
||||||
|
ChevronRight,
|
||||||
|
GraduationCap,
|
||||||
|
Landmark,
|
||||||
|
LayoutList,
|
||||||
|
Target,
|
||||||
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
getAllBolumler,
|
getAllBolumler,
|
||||||
getAllBolumSlugs,
|
getAllBolumSlugs,
|
||||||
@@ -23,6 +31,8 @@ import {
|
|||||||
import { BolumProgramListesi } from "./bolum-program-listesi";
|
import { BolumProgramListesi } from "./bolum-program-listesi";
|
||||||
import { VeriKaynagi } from "./veri-kaynagi";
|
import { VeriKaynagi } from "./veri-kaynagi";
|
||||||
import { NavBaslik } from "./nav-baslik";
|
import { NavBaslik } from "./nav-baslik";
|
||||||
|
import { OzetKart } from "@/components/ozet-kart";
|
||||||
|
import { PagePixelDivider } from "@/components/pixel-decor";
|
||||||
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
|
import { CtaSiraForm } from "@/features/sihirbaz/components/cta-sira-form";
|
||||||
import { SiteFooter } from "@/components/site-footer";
|
import { SiteFooter } from "@/components/site-footer";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
@@ -35,6 +45,32 @@ import {
|
|||||||
|
|
||||||
const sayi = (n: number) => n.toLocaleString("tr-TR");
|
const sayi = (n: number) => n.toLocaleString("tr-TR");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Özet kartında iri sayı telefonda ~94 px'e sığmalı (DESIGN.md §4.8):
|
||||||
|
* 36 px Cal Sans'ta "1.618.988" 123 px ölçüldü, ikonun altına giriyor. 6 ve
|
||||||
|
* 7 haneli sıralar birimle kısalır: 245.300 → "245" + "bin",
|
||||||
|
* 1.618.988 → "1,62" + "milyon".
|
||||||
|
*/
|
||||||
|
function kartSayisi(n: number): { deger: string; birim?: string } {
|
||||||
|
// 999.500+ "1.000 bin" olmasın diye milyona geçer
|
||||||
|
if (n >= 999_500) {
|
||||||
|
return {
|
||||||
|
deger: (n / 1e6).toLocaleString("tr-TR", { maximumFractionDigits: 2 }),
|
||||||
|
birim: "milyon",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (n >= 100_000) return { deger: sayi(Math.round(n / 1000)), birim: "bin" };
|
||||||
|
return { deger: sayi(n) };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Lisans puan türünün kimlik hapındaki adı (DB: SAYISAL, EŞİT AĞIRLIK…). */
|
||||||
|
const PUAN_TURU_HAP: Record<string, string> = {
|
||||||
|
SAYISAL: "Sayısal puan",
|
||||||
|
"EŞİT AĞIRLIK": "Eşit ağırlık puanı",
|
||||||
|
SÖZEL: "Sözel puan",
|
||||||
|
DİL: "Dil puanı",
|
||||||
|
};
|
||||||
|
|
||||||
export function bolumToplamSayfa(programSayisi: number): number {
|
export function bolumToplamSayfa(programSayisi: number): number {
|
||||||
return Math.max(1, Math.ceil(programSayisi / SAYFA_BOYU));
|
return Math.max(1, Math.ceil(programSayisi / SAYFA_BOYU));
|
||||||
}
|
}
|
||||||
@@ -179,76 +215,126 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
|
|||||||
// adı geçer (ziyaretçi Google'dan doğrudan /sayfa/N'ye iniyor).
|
// adı geçer (ziyaretçi Google'dan doğrudan /sayfa/N'ye iniyor).
|
||||||
const ctaBaslik = `Sıralaman ${bolum.ad} tabanlarının neresinde?`;
|
const ctaBaslik = `Sıralaman ${bolum.ad} tabanlarının neresinde?`;
|
||||||
|
|
||||||
|
// Kimlik hapları (DESIGN.md §4.12): düzey + lisans puan türleri. TYT
|
||||||
|
// puanı önlisansın kendisi olduğundan ayrı hap açmaz.
|
||||||
|
const puanTurleri = [
|
||||||
|
...new Set(bolum.lisansProgramlari.map((p) => p.tur)),
|
||||||
|
].flatMap((t) => (PUAN_TURU_HAP[t] ? [PUAN_TURU_HAP[t]] : []));
|
||||||
|
const duzeyler = [
|
||||||
|
...(bolum.lisansProgramlari.length > 0 ? ["Lisans"] : []),
|
||||||
|
...(bolum.onlisansProgramlari.length > 0 ? ["Önlisans"] : []),
|
||||||
|
];
|
||||||
|
// Özet kartları: devlet ve vakıf ayrı sayılır; KKTC ve yurt dışı
|
||||||
|
// programları toplamda var ama iki karta da girmez (vakıf sayılmaz).
|
||||||
|
const devletUni = new Set<string>();
|
||||||
|
const vakifUni = new Set<string>();
|
||||||
|
let vakifProgram = 0;
|
||||||
|
for (const p of tumProgramlar) {
|
||||||
|
if (p.unitur === "DEVLET") devletUni.add(p.universite);
|
||||||
|
else if (p.unitur === "VAKIF" || p.unitur === "VAKIF MYO") {
|
||||||
|
vakifProgram += 1;
|
||||||
|
vakifUni.add(p.universite);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const { stats } = bolum;
|
||||||
|
const hapSinifi =
|
||||||
|
"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";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<main className="mx-auto w-full max-w-5xl px-4 py-10 sm:py-14">
|
<main className="mx-auto w-full max-w-5xl px-4 py-12 sm:py-16">
|
||||||
<JsonLd
|
<JsonLd
|
||||||
data={breadcrumbJsonLd([
|
data={breadcrumbJsonLd([
|
||||||
{ name: "Ana Sayfa", path: "/" },
|
{ name: "Ana Sayfa", path: "/" },
|
||||||
{ name: "Bölümler", path: "/bolumler" },
|
{ name: "Bölümler", path: "/bolumler" },
|
||||||
{ name: bolum.ad, path: `/bolum/${slug}` },
|
{ name: bolum.ad, path: `/bolum/${slug}` },
|
||||||
...(ilkSayfa ? [] : [{ name: `Sayfa ${sayfa}`, path: buYol }]),
|
...(ilkSayfa ? [] : [{ name: `Sayfa ${sayfa}`, path: buYol }]),
|
||||||
])}
|
])}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<nav
|
<nav
|
||||||
aria-label="İçerik yolu"
|
aria-label="İçerik yolu"
|
||||||
className="mb-5 flex flex-wrap items-center gap-1.5 text-sm text-muted-foreground"
|
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-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
||||||
>
|
>
|
||||||
Ana sayfa
|
<Link
|
||||||
</Link>
|
href="/"
|
||||||
<ChevronRight className="size-3.5" aria-hidden />
|
className="transition-colors hover:text-orange-500 hover:underline"
|
||||||
<Link
|
>
|
||||||
href="/bolumler"
|
Ana Sayfa
|
||||||
className="transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
</Link>
|
||||||
>
|
<ChevronRight className="size-3.5 shrink-0" aria-hidden />
|
||||||
Bölümler
|
<Link
|
||||||
</Link>
|
href="/bolumler"
|
||||||
<ChevronRight className="size-3.5" aria-hidden />
|
className="transition-colors hover:text-orange-500 hover:underline"
|
||||||
{ilkSayfa ? (
|
>
|
||||||
<span className="text-foreground">{bolum.ad}</span>
|
Bölümler
|
||||||
) : (
|
</Link>
|
||||||
<>
|
<ChevronRight className="size-3.5 shrink-0" aria-hidden />
|
||||||
<Link
|
{ilkSayfa ? (
|
||||||
href={`/bolum/${slug}`}
|
<span className="text-slate-700">{bolum.ad}</span>
|
||||||
className="transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
) : (
|
||||||
>
|
<>
|
||||||
{bolum.ad}
|
<Link
|
||||||
</Link>
|
href={`/bolum/${slug}`}
|
||||||
<ChevronRight className="size-3.5" aria-hidden />
|
className="transition-colors hover:text-orange-500 hover:underline"
|
||||||
<span className="text-foreground">Sayfa {sayfa}</span>
|
>
|
||||||
</>
|
{bolum.ad}
|
||||||
)}
|
</Link>
|
||||||
</nav>
|
<ChevronRight className="size-3.5 shrink-0" aria-hidden />
|
||||||
|
<span className="text-slate-700">Sayfa {sayfa}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
|
||||||
{/* Başlık üst çubuğun altına girince üst çubukta bölüm adı belirir */}
|
{/* Başlık üst çubuğun altına girince üst çubukta bölüm adı belirir
|
||||||
<NavBaslik baslikId="bolum-baslik" ad={bolum.ad} />
|
(logosu yok — DESIGN.md §4.14). Geniş ekran (lg) altında başlık
|
||||||
<header className="border-b pb-7 sm:pb-9">
|
bloğu ortalı (§5.2). */}
|
||||||
|
<NavBaslik baslikId="bolum-baslik" ad={bolum.ad} />
|
||||||
<h1
|
<h1
|
||||||
id="bolum-baslik"
|
id="bolum-baslik"
|
||||||
className="max-w-3xl font-heading text-3xl font-bold tracking-tight text-foreground sm:text-5xl">
|
className="mt-6 text-center font-heading text-3xl font-bold leading-tight text-slate-900 sm:text-5xl lg:text-left"
|
||||||
|
>
|
||||||
{bolum.ad}
|
{bolum.ad}
|
||||||
{ilkSayfa ? "" : ` — Sayfa ${sayfa}`}
|
{ilkSayfa ? "" : ` — Sayfa ${sayfa}`}
|
||||||
{!ilkSayfa && aralik ? (
|
{!ilkSayfa && aralik ? (
|
||||||
<>
|
<>
|
||||||
{" "}
|
{" "}
|
||||||
<span className="mt-2 block text-lg font-semibold tracking-normal text-muted-foreground sm:text-2xl">
|
<span className="mt-2 block text-lg font-semibold text-slate-500 sm:text-2xl">
|
||||||
{aralik.charAt(0).toLocaleUpperCase("tr-TR") + aralik.slice(1)}
|
{aralik.charAt(0).toLocaleUpperCase("tr-TR") + aralik.slice(1)}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
{duzeyler.length > 0 || puanTurleri.length > 0 ? (
|
||||||
|
<p className="mt-5 flex flex-wrap items-center justify-center gap-2 lg:justify-start">
|
||||||
|
{duzeyler.map((d) => (
|
||||||
|
<span key={d} className={hapSinifi}>
|
||||||
|
<GraduationCap className="size-4 text-primary" aria-hidden />
|
||||||
|
{d}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{puanTurleri.map((t) => (
|
||||||
|
<span key={t} className={hapSinifi}>
|
||||||
|
<Calculator className="size-4 text-primary" aria-hidden />
|
||||||
|
{t}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{ilkSayfa ? (
|
{ilkSayfa ? (
|
||||||
<p className="mt-3 max-w-2xl text-sm leading-6 text-muted-foreground sm:text-base">
|
<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">
|
||||||
{sayi(tumProgramlar.length)} üniversite programının {SON_YIL} taban
|
{bolum.ad} bölümünü açan {sayi(tumProgramlar.length)} üniversite
|
||||||
puanlarını, başarı sıralamalarını ve kontenjanlarını incele.
|
programının {SON_YIL} taban puanlarını, başarı sıralamalarını ve
|
||||||
|
kontenjanlarını incele.{" "}
|
||||||
|
<em>
|
||||||
|
{TERCIH_YILI} tabanları, yerleştirme sonuçları açıklandığında
|
||||||
|
netleşir.
|
||||||
|
</em>
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="mt-3 max-w-2xl text-sm leading-6 text-muted-foreground sm:text-base">
|
<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">
|
||||||
{bolum.ad} taban puanları tablosunun {sayfa}. sayfası (
|
{bolum.ad} taban puanları tablosunun {sayfa}. sayfası (
|
||||||
{sayi(tumProgramlar.length)} programın{" "}
|
{sayi(tumProgramlar.length)} programın{" "}
|
||||||
{sayi((sayfa - 1) * SAYFA_BOYU + 1)}–
|
{sayi((sayfa - 1) * SAYFA_BOYU + 1)}–
|
||||||
@@ -257,54 +343,99 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
|
|||||||
{aralik ? `: ${aralik}` : ""}. Listenin başı{" "}
|
{aralik ? `: ${aralik}` : ""}. Listenin başı{" "}
|
||||||
<Link
|
<Link
|
||||||
href={`/bolum/${slug}`}
|
href={`/bolum/${slug}`}
|
||||||
className="font-medium text-primary hover:underline"
|
className="font-medium text-primary transition-colors hover:text-orange-500 hover:underline"
|
||||||
>
|
>
|
||||||
ilk sayfada
|
ilk sayfada
|
||||||
</Link>
|
</Link>
|
||||||
.
|
.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</header>
|
|
||||||
|
|
||||||
{/* Tek sayfada çıkan ziyaretçi alttaki karta inmiyor: kompakt sıra girişi
|
{/* Tek sayfada çıkan ziyaretçi alttaki karta inmiyor: kompakt sıra
|
||||||
başlık bloğunun hemen altında, listenin üstünde. Alttaki kart kalır. */}
|
girişi özet paragrafın hemen altında, listenin üstünde. Alttaki
|
||||||
<div className="mt-6">
|
kart kalır. */}
|
||||||
<CtaSiraForm varyant="kompakt" konum="ust" baslik={ctaBaslik} />
|
<div className="mt-8">
|
||||||
</div>
|
<CtaSiraForm varyant="kompakt" konum="ust" baslik={ctaBaslik} />
|
||||||
|
|
||||||
<section aria-labelledby="universite-programlari" className="mt-8">
|
|
||||||
<div className="mb-4 flex items-end justify-between gap-4">
|
|
||||||
<h2
|
|
||||||
id="universite-programlari"
|
|
||||||
className="font-heading text-xl font-bold text-foreground sm:text-2xl"
|
|
||||||
>
|
|
||||||
Üniversite programları
|
|
||||||
</h2>
|
|
||||||
<span className="shrink-0 text-sm text-muted-foreground">
|
|
||||||
{sayi(tumProgramlar.length)} program
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<BolumProgramListesi
|
{/* Sayfada en çok dört özet sayı (DESIGN.md §1.1, §4.8). Derin
|
||||||
programlar={tumProgramlar.map((program) => ({
|
sayfalarda tekrar edilmez — üniversite sayfasıyla aynı. */}
|
||||||
...program,
|
{ilkSayfa ? (
|
||||||
universiteSlug: uniSlugFromAd(program.universite),
|
<dl className="mt-8 grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||||
}))}
|
<OzetKart
|
||||||
bazAd={bolum.ad}
|
ikon={LayoutList}
|
||||||
tabanYol={`/bolum/${slug}`}
|
etiket="Program"
|
||||||
sayfa={sayfa}
|
deger={sayi(stats.toplam)}
|
||||||
/>
|
alt={`${sayi(stats.ilSayisi)} ilde`}
|
||||||
<VeriKaynagi className="mt-6" />
|
/>
|
||||||
</section>
|
<OzetKart
|
||||||
|
ikon={Landmark}
|
||||||
|
etiket="Devlet"
|
||||||
|
deger={sayi(stats.devlet)}
|
||||||
|
alt={
|
||||||
|
devletUni.size > 0
|
||||||
|
? `${sayi(devletUni.size)} üniversitede`
|
||||||
|
: "bu bölümde yok"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<OzetKart
|
||||||
|
ikon={Building2}
|
||||||
|
etiket="Vakıf"
|
||||||
|
deger={sayi(vakifProgram)}
|
||||||
|
alt={
|
||||||
|
vakifUni.size > 0
|
||||||
|
? `${sayi(vakifUni.size)} üniversitede`
|
||||||
|
: "bu bölümde yok"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<OzetKart
|
||||||
|
ikon={Target}
|
||||||
|
etiket="En iyi sıralama"
|
||||||
|
{...(stats.minSira != null
|
||||||
|
? kartSayisi(stats.minSira)
|
||||||
|
: { deger: "—" })}
|
||||||
|
alt={`${SON_YIL} taban sırası`}
|
||||||
|
/>
|
||||||
|
</dl>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{/* Üniversite sayfasıyla aynı konum ve aynı kart: liste + sayfalama altı */}
|
<PagePixelDivider seed={83} className="mx-auto mt-12 lg:mx-0" />
|
||||||
<div className="mt-10">
|
|
||||||
<CtaSiraForm
|
<section aria-labelledby="universite-programlari" className="mt-10">
|
||||||
konum="alt"
|
<div className="text-center lg:text-left">
|
||||||
baslik={ctaBaslik}
|
<h2
|
||||||
aciklama={KATALOG_CTA_ACIKLAMA}
|
id="universite-programlari"
|
||||||
/>
|
className="font-heading text-2xl font-bold text-slate-900 sm:text-3xl"
|
||||||
</div>
|
>
|
||||||
|
Üniversite programları
|
||||||
|
</h2>
|
||||||
|
<p className="mt-2 text-base text-slate-500">
|
||||||
|
{sayi(tumProgramlar.length)} program, taban sırasına göre
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-6">
|
||||||
|
<BolumProgramListesi
|
||||||
|
programlar={tumProgramlar.map((program) => ({
|
||||||
|
...program,
|
||||||
|
universiteSlug: uniSlugFromAd(program.universite),
|
||||||
|
}))}
|
||||||
|
bazAd={bolum.ad}
|
||||||
|
tabanYol={`/bolum/${slug}`}
|
||||||
|
sayfa={sayfa}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<VeriKaynagi className="mt-6" />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Üniversite sayfasıyla aynı konum ve aynı kart: liste + sayfalama altı */}
|
||||||
|
<div className="mt-10">
|
||||||
|
<CtaSiraForm
|
||||||
|
konum="alt"
|
||||||
|
baslik={ctaBaslik}
|
||||||
|
aciklama={KATALOG_CTA_ACIKLAMA}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<SiteFooter />
|
<SiteFooter />
|
||||||
</>
|
</>
|
||||||
@@ -313,16 +444,20 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) {
|
|||||||
|
|
||||||
export function BolumIcerikSkeleton() {
|
export function BolumIcerikSkeleton() {
|
||||||
return (
|
return (
|
||||||
<main className="mx-auto w-full max-w-5xl px-4 py-10 sm:py-14">
|
<main className="mx-auto w-full max-w-5xl px-4 py-12 sm:py-16">
|
||||||
<Skeleton className="h-5 w-56" />
|
<Skeleton className="mx-auto h-5 w-56 lg:mx-0" />
|
||||||
<div className="border-b pb-7 sm:pb-9">
|
<Skeleton className="mx-auto mt-6 h-10 w-2/3 max-w-xl lg:mx-0" />
|
||||||
<Skeleton className="mt-5 h-10 w-2/3 max-w-xl" />
|
<Skeleton className="mx-auto mt-5 h-10 w-48 rounded-full lg:mx-0" />
|
||||||
<Skeleton className="mt-3 h-5 w-full max-w-2xl" />
|
<Skeleton className="mx-auto mt-6 h-5 w-full max-w-3xl lg:mx-0" />
|
||||||
|
<div className="mt-8 grid grid-cols-2 gap-4 lg:grid-cols-4">
|
||||||
|
{Array.from({ length: 4 }, (_, i) => (
|
||||||
|
<Skeleton key={i} className="h-28 w-full rounded-2xl" />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8 space-y-3">
|
<div className="mt-16 space-y-3">
|
||||||
<Skeleton className="h-7 w-64" />
|
<Skeleton className="h-7 w-64" />
|
||||||
{Array.from({ length: 5 }, (_, i) => (
|
{Array.from({ length: 5 }, (_, i) => (
|
||||||
<Skeleton key={i} className="h-12 w-full" />
|
<Skeleton key={i} className="h-20 w-full rounded-2xl" />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ function BolumLinkleri({ bolumler }: { bolumler: BolumOzet[] }) {
|
|||||||
<li key={b.slug}>
|
<li key={b.slug}>
|
||||||
<Link
|
<Link
|
||||||
href={`/bolum/${b.slug}`}
|
href={`/bolum/${b.slug}`}
|
||||||
className="group inline-flex items-baseline gap-2 text-sm text-slate-700 hover:text-primary"
|
className="group inline-flex items-baseline gap-2 text-sm text-slate-700 transition-colors hover:text-orange-500"
|
||||||
>
|
>
|
||||||
<span className="group-hover:underline">{b.ad}</span>
|
<span className="group-hover:underline">{b.ad}</span>
|
||||||
<span className="text-xs text-slate-400">
|
<span className="text-xs text-slate-400">
|
||||||
|
|||||||
@@ -12,6 +12,20 @@ import {
|
|||||||
ProgramSiraGecmisi,
|
ProgramSiraGecmisi,
|
||||||
ProgramTabanTrendi,
|
ProgramTabanTrendi,
|
||||||
} from "@/components/program-liste-verileri";
|
} from "@/components/program-liste-verileri";
|
||||||
|
import {
|
||||||
|
SATIR_AD,
|
||||||
|
SATIR_AD_DUZ,
|
||||||
|
SATIR_AD_HUCRE,
|
||||||
|
SATIR_ALT_AD,
|
||||||
|
SATIR_BASLIK,
|
||||||
|
SATIR_BUTON_BASLIK,
|
||||||
|
SATIR_BUTON_HUCRE,
|
||||||
|
SATIR_BUTON_KABI,
|
||||||
|
SATIR_CIP,
|
||||||
|
SATIR_GRUP,
|
||||||
|
SATIR_META,
|
||||||
|
SATIR_MOBIL_SAYI,
|
||||||
|
} from "@/components/liste-satiri";
|
||||||
import { ProgramEkleButonu } from "@/features/liste/components/program-ekle-butonu";
|
import { ProgramEkleButonu } from "@/features/liste/components/program-ekle-butonu";
|
||||||
import { ProgramNetleriButonu } from "./program-netleri";
|
import { ProgramNetleriButonu } from "./program-netleri";
|
||||||
import { UniLogo } from "@/components/uni-logo";
|
import { UniLogo } from "@/components/uni-logo";
|
||||||
@@ -58,7 +72,7 @@ function SiralamaDugmesi({
|
|||||||
aktif && yon === "artan" ? "azalan" : "artan"
|
aktif && yon === "artan" ? "azalan" : "artan"
|
||||||
} sırala`}
|
} sırala`}
|
||||||
aria-pressed={aktif}
|
aria-pressed={aktif}
|
||||||
className="inline-flex min-h-11 cursor-pointer items-center gap-1 rounded-full px-1.5 text-xs text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
className="inline-flex min-h-11 cursor-pointer items-center gap-1 rounded-full px-1.5 text-sm font-semibold text-slate-500 transition-colors hover:text-orange-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||||
>
|
>
|
||||||
<span>{etiket}</span>
|
<span>{etiket}</span>
|
||||||
<span className="flex flex-col -space-y-1" aria-hidden>
|
<span className="flex flex-col -space-y-1" aria-hidden>
|
||||||
@@ -66,7 +80,7 @@ function SiralamaDugmesi({
|
|||||||
className={`size-3 ${
|
className={`size-3 ${
|
||||||
aktif && yon === "artan"
|
aktif && yon === "artan"
|
||||||
? "text-primary"
|
? "text-primary"
|
||||||
: "text-muted-foreground/40"
|
: "text-slate-300"
|
||||||
}`}
|
}`}
|
||||||
strokeWidth={aktif && yon === "artan" ? 3 : 2}
|
strokeWidth={aktif && yon === "artan" ? 3 : 2}
|
||||||
/>
|
/>
|
||||||
@@ -74,7 +88,7 @@ function SiralamaDugmesi({
|
|||||||
className={`size-3 ${
|
className={`size-3 ${
|
||||||
aktif && yon === "azalan"
|
aktif && yon === "azalan"
|
||||||
? "text-primary"
|
? "text-primary"
|
||||||
: "text-muted-foreground/40"
|
: "text-slate-300"
|
||||||
}`}
|
}`}
|
||||||
strokeWidth={aktif && yon === "azalan" ? 3 : 2}
|
strokeWidth={aktif && yon === "azalan" ? 3 : 2}
|
||||||
/>
|
/>
|
||||||
@@ -154,14 +168,21 @@ export function BolumProgramListesi({
|
|||||||
ref={listeRef}
|
ref={listeRef}
|
||||||
className="scroll-mt-24 overflow-hidden rounded-2xl border border-slate-200 bg-white"
|
className="scroll-mt-24 overflow-hidden rounded-2xl border border-slate-200 bg-white"
|
||||||
>
|
>
|
||||||
|
{/* Şişko ve sade (DESIGN.md §4.15): üniversite adı kesilmez, sarar;
|
||||||
|
satır butonları 44 px. 375 px'te yalnız ad + butonlar sütun
|
||||||
|
olarak kalır; taban sıra adın altındaki satıra iner. Sıralama
|
||||||
|
düğmeleri sütun başlıklarıyla birlikte sm'den itibaren görünür —
|
||||||
|
telefonda liste zaten taban sırasına göre dizili. */}
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow className="hover:bg-transparent">
|
||||||
<TableHead>Üniversite</TableHead>
|
<TableHead className={`${SATIR_BASLIK} pl-4 sm:pl-5`}>
|
||||||
<TableHead className="hidden text-right md:table-cell">
|
Üniversite
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className={`hidden text-right lg:table-cell ${SATIR_BASLIK}`}>
|
||||||
Son {VERI_YIL_SAYISI} yıl
|
Son {VERI_YIL_SAYISI} yıl
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="hidden text-right lg:table-cell">
|
<TableHead className="hidden h-12 text-right lg:table-cell">
|
||||||
<SiralamaDugmesi
|
<SiralamaDugmesi
|
||||||
alan="puanSon"
|
alan="puanSon"
|
||||||
aktifAlan={aktifAlan}
|
aktifAlan={aktifAlan}
|
||||||
@@ -169,7 +190,7 @@ export function BolumProgramListesi({
|
|||||||
onSirala={sirala}
|
onSirala={sirala}
|
||||||
/>
|
/>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="hidden text-right sm:table-cell">
|
<TableHead className="hidden h-12 text-right md:table-cell">
|
||||||
<SiralamaDugmesi
|
<SiralamaDugmesi
|
||||||
alan="kontenjanSon"
|
alan="kontenjanSon"
|
||||||
aktifAlan={aktifAlan}
|
aktifAlan={aktifAlan}
|
||||||
@@ -177,7 +198,7 @@ export function BolumProgramListesi({
|
|||||||
onSirala={sirala}
|
onSirala={sirala}
|
||||||
/>
|
/>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="text-right">
|
<TableHead className="hidden h-12 text-right sm:table-cell">
|
||||||
<SiralamaDugmesi
|
<SiralamaDugmesi
|
||||||
alan="siraSon"
|
alan="siraSon"
|
||||||
aktifAlan={aktifAlan}
|
aktifAlan={aktifAlan}
|
||||||
@@ -185,7 +206,7 @@ export function BolumProgramListesi({
|
|||||||
onSirala={sirala}
|
onSirala={sirala}
|
||||||
/>
|
/>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="w-24 text-right">
|
<TableHead className={SATIR_BUTON_BASLIK}>
|
||||||
<span className="sr-only">Netler ve listeye ekle</span>
|
<span className="sr-only">Netler ve listeye ekle</span>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -194,84 +215,87 @@ export function BolumProgramListesi({
|
|||||||
{gosterilenProgramlar.map((p) => {
|
{gosterilenProgramlar.map((p) => {
|
||||||
const uniAd = trBaslikDuzeni(p.universite);
|
const uniAd = trBaslikDuzeni(p.universite);
|
||||||
const varyant = p.isim.trim() !== bazAd ? p.isim.trim() : null;
|
const varyant = p.isim.trim() !== bazAd ? p.isim.trim() : null;
|
||||||
const devlet = p.unitur === "DEVLET";
|
const tabanMetni =
|
||||||
|
p.siraSon != null ? `~${sayi(p.siraSon)}.` : "—";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableRow key={p.id} className="kt-cv-satir">
|
<TableRow key={p.id} className="kt-cv-satir">
|
||||||
<TableCell className="max-w-0 w-full">
|
<TableCell className={SATIR_AD_HUCRE}>
|
||||||
<span className="flex min-w-0 items-center gap-2.5">
|
<span className="flex items-start gap-3 sm:items-center">
|
||||||
{p.universiteSlug ? (
|
{p.universiteSlug ? (
|
||||||
<UniLogo slug={p.universiteSlug} ad={uniAd} boy="sm" />
|
<UniLogo slug={p.universiteSlug} ad={uniAd} boy="sm" />
|
||||||
) : null}
|
) : null}
|
||||||
<span className="min-w-0 flex-1">
|
<span className="min-w-0 flex-1">
|
||||||
{p.universiteSlug ? (
|
<span className={SATIR_GRUP}>
|
||||||
<Link
|
{p.universiteSlug ? (
|
||||||
href={`/universite/${p.universiteSlug}`}
|
<Link
|
||||||
className="block truncate text-sm font-semibold hover:text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
href={`/universite/${p.universiteSlug}`}
|
||||||
>
|
className={SATIR_AD}
|
||||||
{uniAd}
|
|
||||||
</Link>
|
|
||||||
) : (
|
|
||||||
<span className="block truncate text-sm font-semibold">
|
|
||||||
{uniAd}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
<span className="mt-0.5 flex min-w-0 items-center gap-1.5 text-xs text-slate-500">
|
|
||||||
{p.unitur ? (
|
|
||||||
<span
|
|
||||||
className={`shrink-0 rounded px-1 py-px text-[10px] font-medium leading-4 ${
|
|
||||||
devlet
|
|
||||||
? "bg-slate-100 text-slate-600"
|
|
||||||
: "bg-violet-50 text-violet-600"
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
{UNITUR_ETIKET[p.unitur]?.cip ??
|
{uniAd}
|
||||||
trBaslikDuzeni(p.unitur)}
|
</Link>
|
||||||
|
) : (
|
||||||
|
<span className={SATIR_AD_DUZ}>
|
||||||
|
{uniAd}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
)}
|
||||||
<span className="truncate">
|
<span className={SATIR_META}>
|
||||||
{p.il ? trBaslikDuzeni(p.il) : null}
|
{p.unitur ? (
|
||||||
{varyant ? `${p.il ? " · " : ""}${varyant}` : ""}
|
<span className={SATIR_CIP}>
|
||||||
|
{UNITUR_ETIKET[p.unitur]?.cip ??
|
||||||
|
trBaslikDuzeni(p.unitur)}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{p.il ? (
|
||||||
|
<span className="shrink-0">
|
||||||
|
{trBaslikDuzeni(p.il)}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{p.sure ? (
|
||||||
|
<span className="shrink-0">{p.sure} yıl</span>
|
||||||
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
{p.sure ? (
|
</span>
|
||||||
<span className="shrink-0 text-slate-400">
|
{varyant ? (
|
||||||
· {p.sure} yıl
|
<span className={SATIR_ALT_AD}>
|
||||||
</span>
|
{varyant}
|
||||||
) : null}
|
</span>
|
||||||
|
) : null}
|
||||||
|
<span className={SATIR_MOBIL_SAYI}>
|
||||||
|
Taban sıra
|
||||||
|
<span className="font-semibold tabular-nums text-slate-900">
|
||||||
|
{tabanMetni}
|
||||||
|
</span>
|
||||||
|
<ProgramTabanTrendi program={p} />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="hidden text-right text-xs md:table-cell">
|
<TableCell className="hidden text-right text-sm lg:table-cell">
|
||||||
<ProgramSiraGecmisi program={p} />
|
<ProgramSiraGecmisi program={p} />
|
||||||
</TableCell>
|
</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 != null
|
||||||
? p.puanSon.toFixed(2).replace(".", ",")
|
? p.puanSon.toFixed(2).replace(".", ",")
|
||||||
: "—"}
|
: "—"}
|
||||||
</TableCell>
|
</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` : "—"}
|
{p.kontenjanSon != null ? `${sayi(p.kontenjanSon)} kişi` : "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right text-xs tabular-nums text-slate-500">
|
<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">
|
<span className="inline-flex items-center gap-1.5">
|
||||||
<ProgramTabanTrendi program={p} />
|
<ProgramTabanTrendi program={p} />
|
||||||
{p.siraSon != null ? `~${sayi(p.siraSon)}.` : "—"}
|
{tabanMetni}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="pr-3 text-right">
|
<TableCell className={SATIR_BUTON_HUCRE}>
|
||||||
<span className="inline-flex items-center gap-1.5">
|
<span className={SATIR_BUTON_KABI}>
|
||||||
<ProgramNetleriButonu
|
<ProgramNetleriButonu
|
||||||
programId={p.id}
|
programId={p.id}
|
||||||
isim={p.isim.trim()}
|
isim={p.isim.trim()}
|
||||||
universite={uniAd}
|
universite={uniAd}
|
||||||
className="size-8 min-h-8 min-w-8"
|
|
||||||
/>
|
|
||||||
<ProgramEkleButonu
|
|
||||||
program={p}
|
|
||||||
kaynak="bolum"
|
|
||||||
className="size-8 min-h-8 min-w-8"
|
|
||||||
/>
|
/>
|
||||||
|
<ProgramEkleButonu program={p} kaynak="bolum" />
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -355,8 +355,8 @@ export function KatalogArama({ rehberler }: { rehberler: RehberAramaKaydi[] }) {
|
|||||||
· {PUAN_TURU_ETIKET[profil.tur] ?? profil.tur}
|
· {PUAN_TURU_ETIKET[profil.tur] ?? profil.tur}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="inline-flex shrink-0 items-center gap-0.5 rounded-full bg-slate-100 px-1.5 py-0.5 text-[10px] font-medium text-slate-500 sm:text-xs">
|
<span className="inline-flex shrink-0 items-center gap-1 rounded-full bg-slate-100 px-2 py-0.5 text-xs font-medium text-slate-500">
|
||||||
<SquarePen className="size-2.5 sm:size-3" aria-hidden />
|
<SquarePen className="size-3" aria-hidden />
|
||||||
Değiştir
|
Değiştir
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -564,7 +564,7 @@ export function KatalogArama({ rehberler }: { rehberler: RehberAramaKaydi[] }) {
|
|||||||
function BaslangicIcerigi({ onSec }: { onSec: () => void }) {
|
function BaslangicIcerigi({ onSec }: { onSec: () => void }) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<p className="px-1 pb-2.5 text-[11px] font-semibold uppercase tracking-[0.14em] text-slate-400">
|
<p className="px-1 pb-2.5 text-sm font-semibold text-slate-700">
|
||||||
Keşfet
|
Keşfet
|
||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-3 gap-2">
|
<div className="grid grid-cols-3 gap-2">
|
||||||
@@ -677,11 +677,11 @@ function KesfetLinki({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<span className="min-w-0 px-2.5 pb-3 pt-2 text-center sm:px-3.5 sm:pb-3.5 sm:pt-2.5 sm:text-left">
|
<span className="min-w-0 px-2.5 pb-3 pt-2 text-center sm:px-3.5 sm:pb-3.5 sm:pt-2.5 sm:text-left">
|
||||||
<span className="block text-[12px] font-semibold leading-tight tracking-tight text-slate-900 sm:text-sm">
|
<span className="block text-sm font-semibold leading-tight tracking-tight text-slate-900">
|
||||||
<span className="sm:hidden">{kisaBaslik}</span>
|
<span className="sm:hidden">{kisaBaslik}</span>
|
||||||
<span className="hidden sm:inline">{baslik}</span>
|
<span className="hidden sm:inline">{baslik}</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="mt-0.5 block text-[10px] leading-snug text-slate-500 sm:text-xs">
|
<span className="mt-0.5 block text-xs leading-snug text-slate-500">
|
||||||
{aciklama}
|
{aciklama}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -700,7 +700,7 @@ function SonucGrubu({
|
|||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<h3 className="mb-2 flex items-center gap-2 px-2 text-xs font-semibold uppercase tracking-wider text-slate-400">
|
<h3 className="mb-2 flex items-center gap-2 px-2 text-sm font-semibold text-slate-700">
|
||||||
{ikon}
|
{ikon}
|
||||||
{baslik}
|
{baslik}
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
// yok: YÖK Atlas 2026 SPA'sına geçerken cinsiyet panelini yayından kaldırdı
|
// yok: YÖK Atlas 2026 SPA'sına geçerken cinsiyet panelini yayından kaldırdı
|
||||||
// (bkz. scripts/detay.ts).
|
// (bkz. scripts/detay.ts).
|
||||||
|
|
||||||
|
import { SATIR_DUGME_BOYU } from "@/components/liste-satiri";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { ListChecks } from "lucide-react";
|
import { ListChecks } from "lucide-react";
|
||||||
import type { ProgramNetSatiri } from "@/types/yokatlas";
|
import type { ProgramNetSatiri } from "@/types/yokatlas";
|
||||||
@@ -96,7 +97,7 @@ export function ProgramNetleriButonu({
|
|||||||
type="button"
|
type="button"
|
||||||
title="Son yerleşenin netleri"
|
title="Son yerleşenin netleri"
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
className={`inline-flex size-11 min-h-11 min-w-11 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-white text-slate-500 transition-[background-color,border-color,color,transform] duration-150 hover:border-primary hover:bg-primary/5 hover:text-primary active:scale-[0.92] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${className}`}
|
className={`inline-flex ${SATIR_DUGME_BOYU} cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-white text-slate-500 transition-[background-color,border-color,color,transform] duration-150 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500 active:scale-[0.92] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${className}`}
|
||||||
>
|
>
|
||||||
<ListChecks className="size-4" aria-hidden />
|
<ListChecks className="size-4" aria-hidden />
|
||||||
<span className="sr-only">{isim} — son yerleşenin netleri</span>
|
<span className="sr-only">{isim} — son yerleşenin netleri</span>
|
||||||
|
|||||||
@@ -95,7 +95,9 @@ export function Sayfalama({
|
|||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
|
|
||||||
// 7'den çok sayfada pencere: 1 … (c-1) c (c+1) … son
|
// 7'den çok sayfada pencere: 1 … (c-1) c (c+1) … son. Telefonda
|
||||||
|
// (sm altı) tek satır kalsın diye yalnız Önceki · aktif · Sonraki görünür;
|
||||||
|
// diğer numaralar HTML'de durur (tarayıcı botları tüm sayfaları görür).
|
||||||
const numaralar: (number | "...")[] = [];
|
const numaralar: (number | "...")[] = [];
|
||||||
if (toplamSayfa <= 7) {
|
if (toplamSayfa <= 7) {
|
||||||
for (let n = 1; n <= toplamSayfa; n++) numaralar.push(n);
|
for (let n = 1; n <= toplamSayfa; n++) numaralar.push(n);
|
||||||
@@ -116,12 +118,12 @@ export function Sayfalama({
|
|||||||
return (
|
return (
|
||||||
<nav
|
<nav
|
||||||
aria-label="Sayfalama"
|
aria-label="Sayfalama"
|
||||||
className="mt-6 flex flex-wrap items-center justify-center gap-1.5"
|
className="mt-8 flex flex-wrap items-center justify-center gap-2"
|
||||||
>
|
>
|
||||||
{sayfa > 1
|
{sayfa > 1
|
||||||
? gecis(
|
? gecis(
|
||||||
sayfa - 1,
|
sayfa - 1,
|
||||||
"inline-flex h-9 items-center gap-1 rounded-full border border-slate-200 bg-white px-3.5 text-sm text-slate-700 hover:border-primary/40 hover:text-primary",
|
"inline-flex h-11 items-center gap-1 rounded-full border border-slate-200 bg-white px-4 text-sm font-semibold text-slate-700 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]",
|
||||||
<>
|
<>
|
||||||
<ChevronLeft className="size-4" aria-hidden />
|
<ChevronLeft className="size-4" aria-hidden />
|
||||||
Önceki
|
Önceki
|
||||||
@@ -133,7 +135,7 @@ export function Sayfalama({
|
|||||||
n === "..." ? (
|
n === "..." ? (
|
||||||
<span
|
<span
|
||||||
key={`bosluk-${i}`}
|
key={`bosluk-${i}`}
|
||||||
className="px-1.5 text-sm text-slate-400"
|
className="hidden px-1.5 text-sm text-slate-400 sm:inline"
|
||||||
aria-hidden
|
aria-hidden
|
||||||
>
|
>
|
||||||
…
|
…
|
||||||
@@ -142,8 +144,8 @@ export function Sayfalama({
|
|||||||
gecis(
|
gecis(
|
||||||
n,
|
n,
|
||||||
n === sayfa
|
n === sayfa
|
||||||
? "inline-flex h-9 min-w-9 items-center justify-center rounded-full bg-primary px-2 text-sm font-semibold text-primary-foreground"
|
? "inline-flex h-11 min-w-11 items-center justify-center rounded-full bg-primary px-2 text-sm font-semibold text-primary-foreground"
|
||||||
: "inline-flex h-9 min-w-9 items-center justify-center rounded-full border border-slate-200 bg-white px-2 text-sm text-slate-700 hover:border-primary/40 hover:text-primary",
|
: "hidden h-11 min-w-11 items-center justify-center rounded-full border border-slate-200 bg-white px-2 text-sm font-semibold text-slate-700 sm:inline-flex 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]",
|
||||||
n,
|
n,
|
||||||
{ aktif: n === sayfa },
|
{ aktif: n === sayfa },
|
||||||
)
|
)
|
||||||
@@ -152,7 +154,7 @@ export function Sayfalama({
|
|||||||
{sayfa < toplamSayfa
|
{sayfa < toplamSayfa
|
||||||
? gecis(
|
? gecis(
|
||||||
sayfa + 1,
|
sayfa + 1,
|
||||||
"inline-flex h-9 items-center gap-1 rounded-full border border-slate-200 bg-white px-3.5 text-sm text-slate-700 hover:border-primary/40 hover:text-primary",
|
"inline-flex h-11 items-center gap-1 rounded-full border border-slate-200 bg-white px-4 text-sm font-semibold text-slate-700 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]",
|
||||||
<>
|
<>
|
||||||
Sonraki
|
Sonraki
|
||||||
<ChevronRight className="size-4" aria-hidden />
|
<ChevronRight className="size-4" aria-hidden />
|
||||||
|
|||||||
@@ -73,18 +73,18 @@ export function TurkiyeSiralamasi() {
|
|||||||
<li key={u.slug}>
|
<li key={u.slug}>
|
||||||
<Link
|
<Link
|
||||||
href={`/universite/${u.slug}`}
|
href={`/universite/${u.slug}`}
|
||||||
className="group flex items-center gap-3 px-3 py-3 hover:bg-slate-50 sm:gap-4 sm:px-5"
|
className="group flex items-center gap-3 px-3 py-4 transition-colors duration-150 hover:bg-orange-50/60 sm:gap-4 sm:px-5"
|
||||||
>
|
>
|
||||||
<span className="w-9 shrink-0 text-right font-heading text-lg font-bold tabular-nums text-slate-900 sm:w-11 sm:text-xl">
|
<span className="w-9 shrink-0 text-right font-heading text-lg font-bold text-slate-900 sm:w-11 sm:text-xl">
|
||||||
{u.sira}
|
{u.sira}
|
||||||
</span>
|
</span>
|
||||||
<UniLogo slug={u.slug} ad={ad} boy="sm" />
|
<UniLogo slug={u.slug} ad={ad} boy="sm" />
|
||||||
<span className="min-w-0 flex-1">
|
<span className="min-w-0 flex-1">
|
||||||
<span className="block truncate text-sm font-medium text-slate-800 group-hover:text-primary group-hover:underline">
|
<span className="block text-base font-semibold leading-snug text-slate-900 transition-colors group-hover:text-primary group-hover:underline">
|
||||||
{ad}
|
{ad}
|
||||||
</span>
|
</span>
|
||||||
{alt ? (
|
{alt ? (
|
||||||
<span className="block text-xs text-slate-400">{alt}</span>
|
<span className="mt-0.5 block text-sm text-slate-500">{alt}</span>
|
||||||
) : null}
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
<span className="hidden w-40 shrink-0 items-center gap-3 sm:flex">
|
<span className="hidden w-40 shrink-0 items-center gap-3 sm:flex">
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ import {
|
|||||||
Target,
|
Target,
|
||||||
Users,
|
Users,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import type { LucideIcon } from "lucide-react";
|
|
||||||
import type { ReactNode } from "react";
|
|
||||||
import {
|
import {
|
||||||
getAllUniversiteler,
|
getAllUniversiteler,
|
||||||
getAllUniversiteSlugs,
|
getAllUniversiteSlugs,
|
||||||
@@ -55,6 +53,7 @@ import { UniLogo } from "@/components/uni-logo";
|
|||||||
import { UrapLogo } from "./urap-logo";
|
import { UrapLogo } from "./urap-logo";
|
||||||
import { AkademikAyrinti } from "./akademik-ayrinti";
|
import { AkademikAyrinti } from "./akademik-ayrinti";
|
||||||
import { NavBaslik } from "./nav-baslik";
|
import { NavBaslik } from "./nav-baslik";
|
||||||
|
import { OzetKart } from "@/components/ozet-kart";
|
||||||
import { uniLogoYolu } from "@/lib/uni-logolar";
|
import { uniLogoYolu } from "@/lib/uni-logolar";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
import type { Program } from "@/types/yokatlas";
|
import type { Program } from "@/types/yokatlas";
|
||||||
@@ -313,61 +312,6 @@ function AkademikGostergeler({ uni }: { uni: UniDetay }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Ö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
|
* 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
|
* yerini gösteren piksel şeridi (PixelSira) durur. Kartın tamamı sıralama
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ function UniLinkleri({ uniler }: { uniler: UniOzet[] }) {
|
|||||||
<li key={u.slug}>
|
<li key={u.slug}>
|
||||||
<Link
|
<Link
|
||||||
href={`/universite/${u.slug}`}
|
href={`/universite/${u.slug}`}
|
||||||
className="group flex items-center gap-3 text-sm text-slate-700 hover:text-primary"
|
className="group flex items-center gap-3 text-sm text-slate-700 transition-colors hover:text-orange-500"
|
||||||
>
|
>
|
||||||
<UniLogo slug={u.slug} ad={u.ad} boy="sm" />
|
<UniLogo slug={u.slug} ad={u.ad} boy="sm" />
|
||||||
<span className="min-w-0">
|
<span className="min-w-0">
|
||||||
|
|||||||
@@ -8,6 +8,19 @@ import {
|
|||||||
ProgramTabanTrendi,
|
ProgramTabanTrendi,
|
||||||
programEtkinSira,
|
programEtkinSira,
|
||||||
} from "@/components/program-liste-verileri";
|
} from "@/components/program-liste-verileri";
|
||||||
|
import {
|
||||||
|
SATIR_AD,
|
||||||
|
SATIR_AD_DUZ,
|
||||||
|
SATIR_AD_HUCRE,
|
||||||
|
SATIR_BASLIK,
|
||||||
|
SATIR_BUTON_BASLIK,
|
||||||
|
SATIR_BUTON_HUCRE,
|
||||||
|
SATIR_BUTON_KABI,
|
||||||
|
SATIR_CIP,
|
||||||
|
SATIR_GRUP,
|
||||||
|
SATIR_META,
|
||||||
|
SATIR_MOBIL_SAYI,
|
||||||
|
} from "@/components/liste-satiri";
|
||||||
import { ProgramEkleButonu } from "@/features/liste/components/program-ekle-butonu";
|
import { ProgramEkleButonu } from "@/features/liste/components/program-ekle-butonu";
|
||||||
import { ProgramNetleriButonu } from "./program-netleri";
|
import { ProgramNetleriButonu } from "./program-netleri";
|
||||||
import {
|
import {
|
||||||
@@ -50,22 +63,22 @@ export function UniversiteProgramTablosu({
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow className="hover:bg-transparent">
|
<TableRow className="hover:bg-transparent">
|
||||||
<TableHead className="h-12 pl-4 text-sm font-semibold text-slate-500 sm:pl-5">
|
<TableHead className={`${SATIR_BASLIK} pl-4 sm:pl-5`}>
|
||||||
Program
|
Program
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="hidden h-12 text-right text-sm font-semibold text-slate-500 lg:table-cell">
|
<TableHead className={`hidden text-right lg:table-cell ${SATIR_BASLIK}`}>
|
||||||
Son {VERI_YIL_SAYISI} yıl
|
Son {VERI_YIL_SAYISI} yıl
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="hidden h-12 text-right text-sm font-semibold text-slate-500 lg:table-cell">
|
<TableHead className={`hidden text-right lg:table-cell ${SATIR_BASLIK}`}>
|
||||||
{SON_YIL} puan
|
{SON_YIL} puan
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="hidden h-12 text-right text-sm font-semibold text-slate-500 md:table-cell">
|
<TableHead className={`hidden text-right md:table-cell ${SATIR_BASLIK}`}>
|
||||||
Kontenjan
|
Kontenjan
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="hidden h-12 text-right text-sm font-semibold text-slate-500 sm:table-cell">
|
<TableHead className={`hidden text-right sm:table-cell ${SATIR_BASLIK}`}>
|
||||||
Taban sıra
|
Taban sıra
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="h-12 w-28 text-right">
|
<TableHead className={SATIR_BUTON_BASLIK}>
|
||||||
<span className="sr-only">Netler ve listeye ekle</span>
|
<span className="sr-only">Netler ve listeye ekle</span>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -76,28 +89,30 @@ export function UniversiteProgramTablosu({
|
|||||||
const tabanMetni = taban != null ? `~${sayi(taban)}.` : "—";
|
const tabanMetni = taban != null ? `~${sayi(taban)}.` : "—";
|
||||||
return (
|
return (
|
||||||
<TableRow key={p.id} className="kt-cv-satir">
|
<TableRow key={p.id} className="kt-cv-satir">
|
||||||
<TableCell className="w-full whitespace-normal py-4 pl-4 sm:pl-5">
|
<TableCell className={SATIR_AD_HUCRE}>
|
||||||
{p.bolumSlug ? (
|
<span className={SATIR_GRUP}>
|
||||||
<Link
|
{p.bolumSlug ? (
|
||||||
href={`/bolum/${p.bolumSlug}`}
|
<Link
|
||||||
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"
|
href={`/bolum/${p.bolumSlug}`}
|
||||||
>
|
className={SATIR_AD}
|
||||||
{p.isim.trim()}
|
>
|
||||||
</Link>
|
{p.isim.trim()}
|
||||||
) : (
|
</Link>
|
||||||
<span className="block text-base font-semibold leading-snug text-slate-900">
|
) : (
|
||||||
{p.isim.trim()}
|
<span className={SATIR_AD_DUZ}>
|
||||||
|
{p.isim.trim()}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<span className={SATIR_META}>
|
||||||
|
<span className={SATIR_CIP}>
|
||||||
|
{TUR_ETIKET[p.tur] ?? p.tur}
|
||||||
|
</span>
|
||||||
|
{p.sure ? (
|
||||||
|
<span className="shrink-0">{p.sure} yıl</span>
|
||||||
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
)}
|
|
||||||
<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>
|
||||||
<span className="mt-1.5 flex items-center gap-1.5 text-sm text-slate-500 sm:hidden">
|
<span className={SATIR_MOBIL_SAYI}>
|
||||||
Taban sıra
|
Taban sıra
|
||||||
<span className="font-semibold tabular-nums text-slate-900">
|
<span className="font-semibold tabular-nums text-slate-900">
|
||||||
{tabanMetni}
|
{tabanMetni}
|
||||||
@@ -124,8 +139,8 @@ export function UniversiteProgramTablosu({
|
|||||||
{tabanMetni}
|
{tabanMetni}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="py-4 pr-4 text-right sm:pr-5">
|
<TableCell className={SATIR_BUTON_HUCRE}>
|
||||||
<span className="inline-flex items-center gap-2">
|
<span className={SATIR_BUTON_KABI}>
|
||||||
<ProgramNetleriButonu
|
<ProgramNetleriButonu
|
||||||
programId={p.id}
|
programId={p.id}
|
||||||
isim={p.isim.trim()}
|
isim={p.isim.trim()}
|
||||||
|
|||||||
@@ -58,18 +58,20 @@ export function KaydetBanner() {
|
|||||||
<div className="pointer-events-none fixed inset-x-0 bottom-4 z-40 px-4">
|
<div className="pointer-events-none fixed inset-x-0 bottom-4 z-40 px-4">
|
||||||
<div
|
<div
|
||||||
ref={gorunumRef}
|
ref={gorunumRef}
|
||||||
className="pointer-events-auto mx-auto flex max-w-xl items-center gap-4 rounded-2xl border border-slate-200 bg-white p-4 shadow-xl"
|
// Telefonda metin kendi satırında, düğme altında tam genişlik:
|
||||||
|
// yan yana dizince başlık kelime kelime kırılıyordu (375 px).
|
||||||
|
className="pointer-events-auto relative mx-auto flex max-w-xl flex-wrap items-center gap-3 rounded-2xl border border-slate-200 bg-white p-4 pr-14 shadow-xl sm:flex-nowrap sm:gap-4"
|
||||||
>
|
>
|
||||||
{/* Kullanıcının o anki işletim sisteminin logosu: "bu tarayıcıda"
|
{/* Kullanıcının o anki işletim sisteminin logosu: "bu tarayıcıda"
|
||||||
mesajını somutlaştırır */}
|
mesajını somutlaştırır */}
|
||||||
<div className="flex size-10 shrink-0 items-center justify-center rounded-xl border border-slate-200 bg-slate-50 text-slate-700">
|
<div className="flex size-10 shrink-0 items-center justify-center rounded-xl border border-slate-200 bg-slate-50 text-slate-700">
|
||||||
<IsletimSistemiLogosu className="size-5" />
|
<IsletimSistemiLogosu className="size-5" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="font-heading text-sm font-bold text-slate-900">
|
<p className="font-heading text-base font-bold text-slate-900">
|
||||||
Listen yalnızca bu tarayıcıda
|
Listen yalnızca bu tarayıcıda
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-slate-500">
|
<p className="text-sm leading-snug text-slate-500">
|
||||||
Giriş yap; {liste.length} seçimin hesabında saklansın, her
|
Giriş yap; {liste.length} seçimin hesabında saklansın, her
|
||||||
cihazdan aç{" "}
|
cihazdan aç{" "}
|
||||||
<CihazCifti className="ml-0.5 align-[-3px] text-slate-400" />
|
<CihazCifti className="ml-0.5 align-[-3px] text-slate-400" />
|
||||||
@@ -77,8 +79,7 @@ export function KaydetBanner() {
|
|||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
size="sm"
|
className="h-11 w-full cursor-pointer bg-orange-500 px-5 text-white transition-[background-color,transform] duration-200 hover:bg-orange-600 active:scale-[0.97] sm:w-auto"
|
||||||
className="cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href={girisUrl}
|
href={girisUrl}
|
||||||
@@ -99,7 +100,7 @@ export function KaydetBanner() {
|
|||||||
sessionStorage.setItem(KAPATMA_KEY, "1");
|
sessionStorage.setItem(KAPATMA_KEY, "1");
|
||||||
} catch {}
|
} catch {}
|
||||||
}}
|
}}
|
||||||
className="cursor-pointer rounded-full p-1 text-slate-400 transition-colors duration-200 hover:bg-slate-100 hover:text-slate-600"
|
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 />
|
<X className="size-4" aria-hidden />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -12,10 +12,12 @@
|
|||||||
// sadeleşir; X butonu her durumda kalır.
|
// sadeleşir; X butonu her durumda kalır.
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { toast } from "sonner";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { PixelField } from "@/components/pixel-decor";
|
import { PikselListe } from "@/components/piksel-liste";
|
||||||
import { UniLogo } from "@/components/uni-logo";
|
import { UniLogo } from "@/components/uni-logo";
|
||||||
|
import { RiskEtiketi } from "@/components/risk-etiketi";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Drawer,
|
Drawer,
|
||||||
@@ -27,6 +29,8 @@ import {
|
|||||||
} from "@/components/ui/drawer";
|
} from "@/components/ui/drawer";
|
||||||
import {
|
import {
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
|
Check,
|
||||||
|
CircleMinus,
|
||||||
GripVertical,
|
GripVertical,
|
||||||
ListChecks,
|
ListChecks,
|
||||||
Plus,
|
Plus,
|
||||||
@@ -42,8 +46,8 @@ import {
|
|||||||
useTransform,
|
useTransform,
|
||||||
type PanInfo,
|
type PanInfo,
|
||||||
} from "motion/react";
|
} from "motion/react";
|
||||||
import { RISK_ETIKET, type RiskSeviyesi } from "@/lib/risk";
|
import { bolumBazAdi, trBaslikDuzeni, turkishSlugify } from "@/lib/slug";
|
||||||
import { bolumBazAdi, turkishSlugify } from "@/lib/slug";
|
import { SATIR_AD, SATIR_IKINCIL_LINK } from "@/components/liste-satiri";
|
||||||
import { olay } from "@/lib/analitik";
|
import { olay } from "@/lib/analitik";
|
||||||
import { CihazCifti, IsletimSistemiLogosu } from "./cihaz-ikonlari";
|
import { CihazCifti, IsletimSistemiLogosu } from "./cihaz-ikonlari";
|
||||||
import {
|
import {
|
||||||
@@ -58,6 +62,7 @@ import {
|
|||||||
cekmeceAc,
|
cekmeceAc,
|
||||||
cekmeceKapat,
|
cekmeceKapat,
|
||||||
cikar,
|
cikar,
|
||||||
|
listeOku,
|
||||||
temizle,
|
temizle,
|
||||||
useCekmeceAcik,
|
useCekmeceAcik,
|
||||||
useManuelListe,
|
useManuelListe,
|
||||||
@@ -77,12 +82,35 @@ function uniHref(ad: string): string {
|
|||||||
return `/universite/${turkishSlugify(ad.replace(/\s*\([^)]*\)\s*$/, "").trim())}`;
|
return `/universite/${turkishSlugify(ad.replace(/\s*\([^)]*\)\s*$/, "").trim())}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Risk renk dili ürünün geri kalanıyla birebir aynı (bkz. rapor-listesi)
|
/**
|
||||||
const RISK_NOKTA: Record<RiskSeviyesi, string> = {
|
* Geri alınabilir silme (Vercel arayüz ilkeleri: yıkıcı işlem ya onay ya
|
||||||
guvenli: "bg-emerald-500",
|
* geri al penceresi ister; Apple: beklenen işlemde uyarı penceresi açma).
|
||||||
"az-riskli": "bg-amber-500",
|
* Çıkan tercihler eski sıralarına geri konur; arada eklenenler kaybolmaz.
|
||||||
riskli: "bg-red-500",
|
*/
|
||||||
};
|
function geriAlinabilirCikar(ids: string[], baslik: string) {
|
||||||
|
const onceki = listeOku();
|
||||||
|
const cikanlar = onceki
|
||||||
|
.map((t, sira) => ({ t, sira }))
|
||||||
|
.filter(({ t }) => ids.includes(t.id));
|
||||||
|
if (cikanlar.length === 0) return;
|
||||||
|
if (ids.length === onceki.length) temizle();
|
||||||
|
else for (const id of ids) cikar(id);
|
||||||
|
toast(baslik, {
|
||||||
|
duration: 8000,
|
||||||
|
action: {
|
||||||
|
label: "Geri al",
|
||||||
|
onClick: () => {
|
||||||
|
const simdi = listeOku().filter(
|
||||||
|
(t) => !cikanlar.some((c) => c.t.id === t.id),
|
||||||
|
);
|
||||||
|
for (const { t, sira } of cikanlar) {
|
||||||
|
simdi.splice(Math.min(sira, simdi.length), 0, t);
|
||||||
|
}
|
||||||
|
yenidenSirala(simdi.slice(0, MANUEL_LISTE_MAX));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function ListeCekmecesi() {
|
export function ListeCekmecesi() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -94,7 +122,13 @@ export function ListeCekmecesi() {
|
|||||||
// drawer içinde erken ölçüm satırları kaydırıyordu
|
// drawer içinde erken ölçüm satırları kaydırıyordu
|
||||||
const [oturdu, setOturdu] = useState(false);
|
const [oturdu, setOturdu] = useState(false);
|
||||||
|
|
||||||
const doluluk = Math.round((liste.length / MANUEL_LISTE_MAX) * 100);
|
// Risk dağılımı: riskliden güvenliye, sıfır olanlar yazılmaz
|
||||||
|
const riskSayilari = (["riskli", "az-riskli", "guvenli"] as const)
|
||||||
|
.map((r) => [r, liste.filter((t) => t.risk === r).length] as const)
|
||||||
|
.filter(([, adet]) => adet > 0);
|
||||||
|
const riskBilinen = liste.filter((t) => t.risk != null).length;
|
||||||
|
const guvenliYok =
|
||||||
|
riskBilinen >= 6 && !liste.some((t) => t.risk === "guvenli");
|
||||||
// Profilsiz + ile eklenenlerin riski henüz hesaplanmamıştır; alt kart
|
// Profilsiz + ile eklenenlerin riski henüz hesaplanmamıştır; alt kart
|
||||||
// olmayan bir analizi "hazır" diye duyurmasın. Profil varken null risk,
|
// olmayan bir analizi "hazır" diye duyurmasın. Profil varken null risk,
|
||||||
// programın taban sırası olmadığındandır — sıra istemek yanlış olur.
|
// programın taban sırası olmadığındandır — sıra istemek yanlış olur.
|
||||||
@@ -123,71 +157,99 @@ export function ListeCekmecesi() {
|
|||||||
<DrawerContent
|
<DrawerContent
|
||||||
className="min-h-0 max-h-[92dvh] bg-white sm:mx-auto sm:max-w-2xl sm:border-x sm:border-slate-200 sm:shadow-2xl"
|
className="min-h-0 max-h-[92dvh] bg-white sm:mx-auto sm:max-w-2xl sm:border-x sm:border-slate-200 sm:shadow-2xl"
|
||||||
>
|
>
|
||||||
<DrawerHeader className="border-b border-slate-100 text-left md:text-left">
|
{/* Alt sayfa başlığı (DESIGN.md §4.10): ortalı başlık + bağlam;
|
||||||
<div className="flex items-center justify-between gap-3">
|
çarpı §4.3 kanonik sınıf setinde, temizle düğmesi aynı ölçüde
|
||||||
<DrawerTitle className="flex items-center gap-2 font-heading text-lg font-bold">
|
solda. */}
|
||||||
<ListChecks className="size-5 text-primary" aria-hidden />
|
<DrawerHeader className="relative border-b border-slate-100 px-4 pt-5 pb-4 text-center md:text-center">
|
||||||
Kendi Listem
|
{liste.length > 0 ? (
|
||||||
<span className="font-heading text-sm font-bold tabular-nums text-slate-400">
|
<button
|
||||||
{liste.length}/{MANUEL_LISTE_MAX}
|
type="button"
|
||||||
</span>
|
onClick={() =>
|
||||||
</DrawerTitle>
|
geriAlinabilirCikar(
|
||||||
<div className="flex items-center gap-1">
|
liste.map((t) => t.id),
|
||||||
{liste.length > 0 ? (
|
`Listen temizlendi · ${liste.length} program çıkarıldı`,
|
||||||
<button
|
)
|
||||||
type="button"
|
}
|
||||||
onClick={temizle}
|
title="Listeyi temizle"
|
||||||
title="Listeyi temizle"
|
className="absolute top-3 left-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]"
|
||||||
className="cursor-pointer rounded-full p-2 text-slate-400 transition-colors duration-200 hover:bg-slate-100 hover:text-red-500"
|
>
|
||||||
>
|
<Trash2 className="size-4" aria-hidden />
|
||||||
<Trash2 className="size-4" aria-hidden />
|
<span className="sr-only">Listeyi temizle</span>
|
||||||
<span className="sr-only">Listeyi temizle</span>
|
</button>
|
||||||
</button>
|
) : null}
|
||||||
|
<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>
|
||||||
|
<DrawerTitle className="px-10 text-center font-heading text-2xl font-bold text-slate-900">
|
||||||
|
Kendi Listem
|
||||||
|
</DrawerTitle>
|
||||||
|
{/* Durum satırı: solda listenin piksel hâli (ilerleme çubuğunun
|
||||||
|
yerine), sağda sayı ve risk dağılımı — dikine uzamasın diye
|
||||||
|
yan yana (DESIGN.md §5.3). Durum ekran okuyucuya duyurulur. */}
|
||||||
|
<div className="mt-3 flex items-center justify-center gap-3">
|
||||||
|
<div
|
||||||
|
className="w-40 shrink-0 rounded-xl bg-slate-950 px-2.5 py-2"
|
||||||
|
role="progressbar"
|
||||||
|
aria-valuenow={liste.length}
|
||||||
|
aria-valuemin={0}
|
||||||
|
aria-valuemax={MANUEL_LISTE_MAX}
|
||||||
|
aria-label="Liste doluluğu"
|
||||||
|
>
|
||||||
|
<PikselListe
|
||||||
|
gercek={liste.map((t) =>
|
||||||
|
t.risk === "riskli"
|
||||||
|
? "hayal"
|
||||||
|
: t.risk === "az-riskli"
|
||||||
|
? "dengeli"
|
||||||
|
: t.risk === "guvenli"
|
||||||
|
? "guvenli"
|
||||||
|
: "belirsiz",
|
||||||
|
)}
|
||||||
|
sutun={12}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="min-w-0 text-left">
|
||||||
|
<p role="status" className="text-sm text-slate-500">
|
||||||
|
{liste.length === MANUEL_LISTE_MAX ? (
|
||||||
|
// Nadir, hak edilmiş an: küçük giriş (Emil: scale 0.95 + opaklık,
|
||||||
|
// güçlü ease-out, <300 ms). Hareketi azalt'ta yalnız opaklık.
|
||||||
|
<span className="inline-flex items-center gap-1.5 font-semibold text-primary transition-[opacity,transform] duration-300 ease-[cubic-bezier(0.23,1,0.32,1)] starting:scale-95 starting:opacity-0 motion-reduce:starting:scale-100">
|
||||||
|
<Check className="size-4" aria-hidden />
|
||||||
|
Listen tamam · {liste.length} / {MANUEL_LISTE_MAX}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{liste.length} / {MANUEL_LISTE_MAX} tercih
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
{riskSayilari.length > 0 ? (
|
||||||
|
<p className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1">
|
||||||
|
{riskSayilari.map(([seviye, adet]) => (
|
||||||
|
<span key={seviye} className="inline-flex items-center gap-1.5">
|
||||||
|
<RiskEtiketi risk={seviye} />
|
||||||
|
<span className="text-sm font-semibold text-slate-900">
|
||||||
|
{adet}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
<DrawerClose className="cursor-pointer rounded-full p-2 text-slate-400 transition-colors duration-200 hover:bg-slate-100 hover:text-slate-600">
|
|
||||||
<X className="size-4" aria-hidden />
|
|
||||||
<span className="sr-only">Kapat</span>
|
|
||||||
</DrawerClose>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
{/* Denge ipucu: 24'lük iskeletin mantığı (alt sıralarda güvenli
|
||||||
className="mt-3 h-1.5 overflow-hidden rounded-full bg-slate-100"
|
programlar). Yalnız riskleri bilinen ve en az 6 tercihli
|
||||||
role="progressbar"
|
listede; vaat değil, gerekçe. */}
|
||||||
aria-valuenow={liste.length}
|
{guvenliYok ? (
|
||||||
aria-valuemin={0}
|
<p className="mx-auto mt-2 max-w-sm text-center text-sm leading-relaxed text-slate-600">
|
||||||
aria-valuemax={MANUEL_LISTE_MAX}
|
Güvenli yok; alta birkaç tane eklemek riski azaltır.
|
||||||
aria-label="Liste doluluğu"
|
</p>
|
||||||
>
|
) : null}
|
||||||
<motion.div
|
|
||||||
className="h-full rounded-full bg-primary"
|
|
||||||
animate={{ width: `${doluluk}%` }}
|
|
||||||
transition={{ type: "spring", duration: 0.5, bounce: 0 }}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
className="mt-2.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-[11px] text-slate-500"
|
|
||||||
aria-label="Yerleşme riski renkleri"
|
|
||||||
>
|
|
||||||
{(
|
|
||||||
[
|
|
||||||
["guvenli", "yeşil güvenli"],
|
|
||||||
["az-riskli", "turuncu az riskli"],
|
|
||||||
["riskli", "kırmızı riskli"],
|
|
||||||
] as const
|
|
||||||
).map(([seviye, aciklama]) => (
|
|
||||||
<li key={seviye} className="inline-flex items-center gap-1.5">
|
|
||||||
<span
|
|
||||||
className={`size-2 shrink-0 rounded-full ${RISK_NOKTA[seviye]}`}
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
<span>{aciklama}</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</DrawerHeader>
|
</DrawerHeader>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto px-4 py-4"
|
className="min-h-0 flex-1 overflow-x-hidden overflow-y-auto overscroll-contain px-4 py-2"
|
||||||
>
|
>
|
||||||
{liste.length === 0 ? (
|
{liste.length === 0 ? (
|
||||||
<div className="relative flex flex-col items-center justify-center gap-2 overflow-hidden px-4 py-16 text-center">
|
<div className="relative flex flex-col items-center justify-center gap-2 overflow-hidden px-4 py-16 text-center">
|
||||||
@@ -196,10 +258,10 @@ export function ListeCekmecesi() {
|
|||||||
strokeWidth={1.25}
|
strokeWidth={1.25}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
<p className="relative font-heading text-sm font-bold text-slate-700">
|
<p className="relative font-heading text-lg font-bold text-slate-900">
|
||||||
Henüz program seçmedin
|
Henüz program seçmedin
|
||||||
</p>
|
</p>
|
||||||
<p className="relative text-xs leading-relaxed text-slate-500">
|
<p className="relative text-sm leading-relaxed text-slate-500">
|
||||||
Sonuç sayfasından program ekleyerek listeni doldurmaya başla.
|
Sonuç sayfasından program ekleyerek listeni doldurmaya başla.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -228,29 +290,32 @@ export function ListeCekmecesi() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={programaEkleGit}
|
onClick={programaEkleGit}
|
||||||
className={`flex h-12 w-full cursor-pointer items-center justify-center rounded-full border border-dashed border-slate-300 text-slate-400 transition-colors duration-200 hover:border-primary/50 hover:bg-primary/5 hover:text-primary active:scale-[0.98] ${
|
className={`flex h-12 w-full cursor-pointer items-center justify-center gap-2 rounded-full border border-dashed border-slate-300 text-sm font-semibold text-slate-500 transition-[background-color,border-color,color,transform] duration-150 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500 active:scale-[0.97] ${
|
||||||
liste.length > 0 ? "mt-2" : "mt-1"
|
liste.length > 0 ? "mt-3" : "mt-1"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Plus className="size-5" aria-hidden />
|
<Plus className="size-5" aria-hidden />
|
||||||
<span className="sr-only">Sonuç sayfasından program ekle</span>
|
Program ekle
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DrawerFooter className="border-t border-slate-100 pt-4 pb-[max(1rem,env(safe-area-inset-bottom))]">
|
<DrawerFooter className="border-t border-slate-100 pt-3 pb-[max(1rem,env(safe-area-inset-bottom))]">
|
||||||
{/* C2 kalıcı satır: girişsiz kullanıcıya listenin uçucu olduğu
|
{/* C2 kalıcı satır: girişsiz kullanıcıya listenin uçucu olduğu
|
||||||
hatırlatılır; giriş doğal "kaydet" gerekçesi olarak sunulur. */}
|
hatırlatılır; giriş doğal "kaydet" gerekçesi olarak sunulur. */}
|
||||||
{girisli === false ? (
|
{girisli === false ? (
|
||||||
<div className="mb-3 flex items-center justify-between gap-3 rounded-xl border border-slate-200 bg-slate-50 px-4 py-3 text-left">
|
<div className="mb-2 flex flex-wrap items-center justify-between gap-x-3 px-1 text-left">
|
||||||
<p className="min-w-0 text-xs leading-relaxed text-slate-600">
|
<p className="min-w-0 text-sm leading-relaxed text-slate-600">
|
||||||
{/* O anki işletim sisteminin logosu — "bu tarayıcıda"yı somutlaştırır */}
|
{/* O anki işletim sisteminin logosu — "bu tarayıcıda"yı somutlaştırır */}
|
||||||
<IsletimSistemiLogosu className="mr-1.5 inline size-3.5 align-[-3px] text-slate-700" />
|
<IsletimSistemiLogosu className="mr-1.5 inline size-4 align-[-3px] text-slate-700" />
|
||||||
<span className="font-semibold text-slate-800">
|
<span className="font-semibold text-slate-800">
|
||||||
Listen yalnızca bu tarayıcıda.
|
Listen yalnızca bu tarayıcıda.
|
||||||
</span>{" "}
|
</span>
|
||||||
Giriş yap; her cihazdan eriş{" "}
|
<span className="hidden sm:inline">
|
||||||
<CihazCifti className="align-[-3px] text-slate-400" />
|
{" "}
|
||||||
|
Giriş yap; her cihazdan eriş{" "}
|
||||||
|
<CihazCifti className="align-[-3px] text-slate-400" />
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
<Link
|
<Link
|
||||||
href={girisUrl}
|
href={girisUrl}
|
||||||
@@ -258,27 +323,26 @@ export function ListeCekmecesi() {
|
|||||||
cekmeceKapat();
|
cekmeceKapat();
|
||||||
olay("giris_cta_tiklandi", { kaynak: "liste_cekmecesi" });
|
olay("giris_cta_tiklandi", { kaynak: "liste_cekmecesi" });
|
||||||
}}
|
}}
|
||||||
className="inline-flex shrink-0 items-center gap-1 text-xs font-semibold text-primary transition-colors duration-200 hover:text-primary/80"
|
className="inline-flex min-h-11 shrink-0 items-center gap-1 text-sm font-semibold text-primary transition-colors duration-150 hover:text-orange-500"
|
||||||
>
|
>
|
||||||
Giriş yap, kaydet
|
Giriş yap, kaydet
|
||||||
<ArrowRight className="size-3.5" aria-hidden />
|
<ArrowRight className="size-4" aria-hidden />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{/* Ana sayfadaki final CTA panelinin kompakt hâli: koyu primary
|
{/* Ana sayfadaki final CTA panelinin kompakt hâli: koyu primary
|
||||||
zemin + kenarlara doğru yoğunlaşan piksel dokusu (PixelField) */}
|
zemin + kenarlara doğru yoğunlaşan piksel dokusu (PixelField) */}
|
||||||
<div className="relative isolate flex items-center justify-between gap-4 overflow-hidden rounded-2xl bg-primary px-5 py-5 text-left text-white">
|
{/* Telefonda alt alta (başlık tek satır, buton tam genişlik):
|
||||||
<div
|
yan yana dizince başlık üç satıra kırılıp listenin yerini
|
||||||
aria-hidden
|
yiyordu (24 Eyl 2026, 375 px ölçümü). */}
|
||||||
className="pointer-events-none absolute inset-x-0 -inset-y-6 -z-10 text-white"
|
{/* Funnel yüzeyi (DESIGN.md §10.8): arama penceresindeki kartın
|
||||||
>
|
koyu zemini; listenin piksel hâli başlıkta. */}
|
||||||
<PixelField seed={8} />
|
<div className="flex flex-col gap-3 rounded-2xl border border-blue-400/20 bg-slate-950 px-5 py-4 text-left text-white sm:flex-row sm:items-center sm:justify-between sm:gap-4 sm:py-5">
|
||||||
</div>
|
<div className="min-w-0 sm:order-1 sm:flex-1">
|
||||||
<div className="min-w-0">
|
<p className="font-heading text-xl font-bold sm:text-2xl">
|
||||||
<p className="font-heading text-2xl font-bold">
|
|
||||||
{riskEksik ? "Risk analizi sıralamanı bekliyor" : "Risk analizini biz yaptık"}
|
{riskEksik ? "Risk analizi sıralamanı bekliyor" : "Risk analizini biz yaptık"}
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 truncate text-sm leading-relaxed text-white">
|
<p className="mt-1 hidden truncate text-sm leading-relaxed text-slate-300 sm:block">
|
||||||
{riskEksik
|
{riskEksik
|
||||||
? "Riskleri gösterelim; istersen 24'lük listeni de kur (3 kredi)."
|
? "Riskleri gösterelim; istersen 24'lük listeni de kur (3 kredi)."
|
||||||
: "Her seçimin yerleşme riski hazır — son kararı sen ver."}
|
: "Her seçimin yerleşme riski hazır — son kararı sen ver."}
|
||||||
@@ -287,7 +351,7 @@ export function ListeCekmecesi() {
|
|||||||
{riskEksik ? (
|
{riskEksik ? (
|
||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
className="shrink-0 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
className="h-12 w-full shrink-0 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600 active:scale-[0.97] sm:order-3 sm:w-auto"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
cekmeceKapat();
|
cekmeceKapat();
|
||||||
// Hedef "sihirbaz": kapı kapanınca akış /sonuc'ta devam
|
// Hedef "sihirbaz": kapı kapanınca akış /sonuc'ta devam
|
||||||
@@ -303,7 +367,7 @@ export function ListeCekmecesi() {
|
|||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
size="lg"
|
size="lg"
|
||||||
className="shrink-0 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
className="h-12 w-full shrink-0 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600 active:scale-[0.97] sm:order-3 sm:w-auto"
|
||||||
>
|
>
|
||||||
<Link href="/listem" onClick={cekmeceKapat}>
|
<Link href="/listem" onClick={cekmeceKapat}>
|
||||||
Listemde aç
|
Listemde aç
|
||||||
@@ -341,7 +405,7 @@ function CekmeceSatiri({
|
|||||||
info.offset.x > SIL_ESIK ||
|
info.offset.x > SIL_ESIK ||
|
||||||
(info.offset.x > 32 && info.velocity.x > 500)
|
(info.offset.x > 32 && info.velocity.x > 500)
|
||||||
) {
|
) {
|
||||||
cikar(tercih.id);
|
geriAlinabilirCikar([tercih.id], `${tercih.isim} çıkarıldı`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -358,12 +422,12 @@ function CekmeceSatiri({
|
|||||||
exit={{ opacity: 0, x: 96, transition: { duration: 0.18 } }}
|
exit={{ opacity: 0, x: 96, transition: { duration: 0.18 } }}
|
||||||
transition={{ type: "spring", duration: 0.4, bounce: 0 }}
|
transition={{ type: "spring", duration: 0.4, bounce: 0 }}
|
||||||
whileDrag={{ scale: 1.03, zIndex: 10 }}
|
whileDrag={{ scale: 1.03, zIndex: 10 }}
|
||||||
className="relative mb-2 select-none last:mb-0"
|
className="relative select-none border-b border-slate-100 last:border-b-0"
|
||||||
>
|
>
|
||||||
{/* Sil bandı: kart sağa kaydıkça altından görünür */}
|
{/* Sil bandı: kart sağa kaydıkça altından görünür */}
|
||||||
<motion.div
|
<motion.div
|
||||||
style={{ opacity: silGorunur }}
|
style={{ opacity: silGorunur }}
|
||||||
className="absolute inset-0 flex items-center rounded-xl bg-red-500 pl-4"
|
className="absolute inset-0 flex items-center bg-red-500 pl-4"
|
||||||
aria-hidden
|
aria-hidden
|
||||||
>
|
>
|
||||||
<Trash2 className="size-4 text-white" />
|
<Trash2 className="size-4 text-white" />
|
||||||
@@ -377,7 +441,7 @@ function CekmeceSatiri({
|
|||||||
dragElastic={{ left: 0, right: 0.7 }}
|
dragElastic={{ left: 0, right: 0.7 }}
|
||||||
dragSnapToOrigin
|
dragSnapToOrigin
|
||||||
onDragEnd={kaydirinca}
|
onDragEnd={kaydirinca}
|
||||||
className="relative flex items-center gap-2.5 rounded-xl border border-slate-200 bg-white px-3 py-2.5"
|
className="relative flex items-center gap-3 bg-white py-3"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -385,58 +449,60 @@ function CekmeceSatiri({
|
|||||||
e.stopPropagation(); // yatay kaydırmayı tetiklemesin
|
e.stopPropagation(); // yatay kaydırmayı tetiklemesin
|
||||||
siraKontrol.start(e);
|
siraKontrol.start(e);
|
||||||
}}
|
}}
|
||||||
className="-m-1 shrink-0 cursor-grab touch-none rounded p-1 text-slate-300 active:cursor-grabbing"
|
className="-ml-1 flex h-11 w-7 shrink-0 cursor-grab touch-none items-center justify-center rounded-lg text-slate-300 transition-colors hover:text-slate-500 active:cursor-grabbing"
|
||||||
>
|
>
|
||||||
<GripVertical className="size-4" aria-hidden />
|
<GripVertical className="size-5" aria-hidden />
|
||||||
<span className="sr-only">{tercih.isim} sırasını değiştir</span>
|
<span className="sr-only">{tercih.isim} sırasını değiştir</span>
|
||||||
</button>
|
</button>
|
||||||
<span className="flex size-6 shrink-0 items-center justify-center rounded-full bg-slate-100 font-heading text-[11px] font-bold tabular-nums">
|
<span className="flex size-8 shrink-0 items-center justify-center rounded-full bg-slate-100 font-heading text-sm font-bold text-slate-900">
|
||||||
{sira}
|
{sira}
|
||||||
</span>
|
</span>
|
||||||
<UniLogo ad={tercih.universite} boy="xs" />
|
|
||||||
<span className="min-w-0 flex-1">
|
<span className="min-w-0 flex-1">
|
||||||
<Link
|
<Link
|
||||||
href={bolumHref(tercih.isim)}
|
href={bolumHref(tercih.isim)}
|
||||||
onClick={cekmeceKapat}
|
onClick={cekmeceKapat}
|
||||||
onPointerDown={(e) => e.stopPropagation()} // kaydırmayı tetiklemesin
|
onPointerDown={(e) => e.stopPropagation()} // kaydırmayı tetiklemesin
|
||||||
className="block truncate text-sm font-semibold hover:text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
className={SATIR_AD}
|
||||||
>
|
>
|
||||||
{tercih.isim}
|
{tercih.isim}
|
||||||
</Link>
|
</Link>
|
||||||
<span className="block truncate text-xs text-slate-500">
|
<span className="mt-1 flex items-center gap-2 text-sm leading-snug text-slate-600">
|
||||||
|
<UniLogo ad={tercih.universite} boy="xs" />
|
||||||
<Link
|
<Link
|
||||||
href={uniHref(tercih.universite)}
|
href={uniHref(tercih.universite)}
|
||||||
onClick={cekmeceKapat}
|
onClick={cekmeceKapat}
|
||||||
onPointerDown={(e) => e.stopPropagation()} // kaydırmayı tetiklemesin
|
onPointerDown={(e) => e.stopPropagation()} // kaydırmayı tetiklemesin
|
||||||
title={`${tercih.universite} taban puanları sayfası`}
|
title={`${tercih.universite} taban puanları sayfası`}
|
||||||
className="hover:text-primary hover:underline"
|
className={`min-w-0 ${SATIR_IKINCIL_LINK}`}
|
||||||
>
|
>
|
||||||
{tercih.universite}
|
{trBaslikDuzeni(tercih.universite)}
|
||||||
</Link>
|
</Link>
|
||||||
{tercih.il ? ` · ${tercih.il.toLocaleLowerCase("tr-TR")}` : ""}
|
|
||||||
</span>
|
</span>
|
||||||
|
{tercih.risk || tercih.efektifSira != null ? (
|
||||||
|
<span className="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-slate-500">
|
||||||
|
{tercih.risk ? <RiskEtiketi risk={tercih.risk} /> : null}
|
||||||
|
{tercih.efektifSira != null ? (
|
||||||
|
<span className="inline-flex items-center gap-1.5">
|
||||||
|
Taban sıra
|
||||||
|
<span className="font-semibold tabular-nums text-slate-900">
|
||||||
|
~{tercih.efektifSira.toLocaleString("tr-TR")}.
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
</span>
|
</span>
|
||||||
{tercih.efektifSira != null ? (
|
|
||||||
<span className="shrink-0 text-xs tabular-nums text-slate-500">
|
|
||||||
~{tercih.efektifSira.toLocaleString("tr-TR")}.
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
{tercih.risk ? (
|
|
||||||
<span className="inline-flex shrink-0 items-center gap-1.5 text-xs font-medium">
|
|
||||||
<span
|
|
||||||
className={`size-2.5 shrink-0 rounded-full ${RISK_NOKTA[tercih.risk]}`}
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
{RISK_ETIKET[tercih.risk]}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => cikar(tercih.id)}
|
onClick={() =>
|
||||||
|
geriAlinabilirCikar([tercih.id], `${tercih.isim} çıkarıldı`)
|
||||||
|
}
|
||||||
onPointerDown={(e) => e.stopPropagation()} // kaydırmayı tetiklemesin
|
onPointerDown={(e) => e.stopPropagation()} // kaydırmayı tetiklemesin
|
||||||
className="shrink-0 cursor-pointer rounded-full p-1.5 text-slate-300 transition-colors duration-200 hover:bg-slate-100 hover:text-red-500"
|
// Satırdan çıkarma "−" ile, çerçevesiz: kapatma çarpısıyla (§4.3)
|
||||||
|
// karışmasın diye çarpı yalnız çekmecenin kendisini kapatır.
|
||||||
|
className="inline-flex size-11 shrink-0 cursor-pointer items-center justify-center rounded-full text-slate-400 transition-[background-color,color,transform] duration-150 hover:bg-orange-50/60 hover:text-orange-500 active:scale-[0.92]"
|
||||||
>
|
>
|
||||||
<X className="size-3.5" aria-hidden />
|
<CircleMinus className="size-5" aria-hidden />
|
||||||
<span className="sr-only">{tercih.isim} listeden çıkar</span>
|
<span className="sr-only">{tercih.isim} listeden çıkar</span>
|
||||||
</button>
|
</button>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
55
src/features/liste/components/liste-durumu-satiri.tsx
Normal file
55
src/features/liste/components/liste-durumu-satiri.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { ArrowRight, Plus } from "lucide-react";
|
||||||
|
import {
|
||||||
|
MANUEL_LISTE_MAX,
|
||||||
|
cekmeceAc,
|
||||||
|
useListeHidreMi,
|
||||||
|
useManuelListe,
|
||||||
|
} from "../liste-store";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* /sonuc başlığının altındaki "listen nerede?" satırı (DESIGN.md §10):
|
||||||
|
* öğrenci kendi kurduğu listenin durumunu sayıyla görür (hedef eğimi:
|
||||||
|
* "5/24"), tek dokunuşla çekmeceyi açar. Liste boşsa ne yapılacağını söyler.
|
||||||
|
* Liste localStorage'da yaşadığı için hidrasyondan önce yer tutucu çizilir,
|
||||||
|
* satır zıplamasın.
|
||||||
|
*/
|
||||||
|
export function ListeDurumuSatiri() {
|
||||||
|
const hidre = useListeHidreMi();
|
||||||
|
const liste = useManuelListe();
|
||||||
|
|
||||||
|
if (!hidre) return <div className="mt-5 h-11" aria-hidden />;
|
||||||
|
|
||||||
|
if (liste.length === 0) {
|
||||||
|
return (
|
||||||
|
<p className="mx-auto mt-5 max-w-sm text-sm leading-relaxed text-slate-500 lg:mx-0 lg:max-w-none">
|
||||||
|
<span className="mr-1.5 inline-flex size-6 items-center justify-center rounded-full border border-slate-200 bg-white align-middle text-slate-500">
|
||||||
|
<Plus className="size-3.5" aria-hidden />
|
||||||
|
<span className="sr-only">Artı düğmesi</span>
|
||||||
|
</span>
|
||||||
|
ile beğendiğin programı listene ekle; sırasını sonra sen belirlersin.
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const kalan = MANUEL_LISTE_MAX - liste.length;
|
||||||
|
return (
|
||||||
|
<div className="mt-5 flex justify-center lg:justify-start">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={cekmeceAc}
|
||||||
|
className="group inline-flex h-11 cursor-pointer items-center gap-2 rounded-full border border-slate-200 bg-white px-4 text-sm font-semibold text-slate-800 transition-[background-color,border-color,color,transform] duration-150 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500 active:scale-[0.97]"
|
||||||
|
>
|
||||||
|
Listende {liste.length}/{MANUEL_LISTE_MAX} program
|
||||||
|
<span className="font-medium text-slate-500 group-hover:text-orange-500">
|
||||||
|
{kalan > 0 ? `· ${kalan} yer kaldı` : "· liste tamam"}
|
||||||
|
</span>
|
||||||
|
<ArrowRight
|
||||||
|
className="size-4 transition-transform duration-150 group-hover:translate-x-0.5"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -46,7 +46,7 @@ export function ListemButonu() {
|
|||||||
initial={azMotion ? false : { scale: 0.6 }}
|
initial={azMotion ? false : { scale: 0.6 }}
|
||||||
animate={{ scale: 1 }}
|
animate={{ scale: 1 }}
|
||||||
transition={{ type: "spring", duration: 0.4, bounce: 0.45 }}
|
transition={{ type: "spring", duration: 0.4, bounce: 0.45 }}
|
||||||
className="flex size-5 items-center justify-center rounded-full bg-orange-500 font-heading text-[11px] font-bold tabular-nums text-white"
|
className="flex h-6 min-w-6 items-center justify-center rounded-full bg-orange-500 px-1.5 font-heading text-xs font-bold text-white"
|
||||||
>
|
>
|
||||||
{liste.length}
|
{liste.length}
|
||||||
</motion.span>
|
</motion.span>
|
||||||
|
|||||||
@@ -20,7 +20,13 @@ const RISK_AGIRLIK: Record<RiskSeviyesi, number> = {
|
|||||||
riskli: 2,
|
riskli: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function ManuelHarita({ yerAyir = false }: { yerAyir?: boolean }) {
|
export function ManuelHarita({
|
||||||
|
yerAyir = false,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
yerAyir?: boolean;
|
||||||
|
className?: string;
|
||||||
|
}) {
|
||||||
const liste = useManuelListe();
|
const liste = useManuelListe();
|
||||||
const hidre = useListeHidreMi();
|
const hidre = useListeHidreMi();
|
||||||
const [seciliIl, setSeciliIl] = useState<string | null>(null);
|
const [seciliIl, setSeciliIl] = useState<string | null>(null);
|
||||||
@@ -85,10 +91,10 @@ export function ManuelHarita({ yerAyir = false }: { yerAyir?: boolean }) {
|
|||||||
// durumunda boş harita ekranda kalmaz.
|
// durumunda boş harita ekranda kalmaz.
|
||||||
if (liste.length === 0 && (hidre || !yerAyir)) return null;
|
if (liste.length === 0 && (hidre || !yerAyir)) return null;
|
||||||
|
|
||||||
// Margin yok: harita /sonuc'ta main'in İLK çocuğudur, üst boşluğunu
|
// Üst boşluk çağırandan gelir: /sonuc'ta harita sonuç başlığının altında
|
||||||
// main'in py-12'sinden alır (bkz. sonuc/page.tsx).
|
// durur (bkz. sonuc/page.tsx). Harita çizilmezse boşluk da oluşmaz.
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className={className}>
|
||||||
<TercihHaritasiLazy
|
<TercihHaritasiLazy
|
||||||
pinler={pinler}
|
pinler={pinler}
|
||||||
seciliIl={seciliIl}
|
seciliIl={seciliIl}
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { SATIR_DUGME_BOYU } from "@/components/liste-satiri";
|
||||||
import { ArrowRight, Check, Plus } from "lucide-react";
|
import { ArrowRight, Check, Plus } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import type { Program } from "@/types/yokatlas";
|
import type { Program } from "@/types/yokatlas";
|
||||||
import { olay } from "@/lib/analitik";
|
import { olay } from "@/lib/analitik";
|
||||||
import {
|
import {
|
||||||
MANUEL_LISTE_MAX,
|
MANUEL_LISTE_MAX,
|
||||||
|
cekmeceAc,
|
||||||
|
listeOku,
|
||||||
useListeDoluMu,
|
useListeDoluMu,
|
||||||
useListedeMi,
|
useListedeMi,
|
||||||
type ManuelKaynak,
|
type ManuelKaynak,
|
||||||
@@ -53,7 +56,11 @@ export function ProgramEkleButonu({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const sonuc = programEkleIste({ program, kaynak, adaySira, adayTur });
|
const sonuc = programEkleIste({ program, kaynak, adaySira, adayTur });
|
||||||
if (sonuc.doldu) {
|
if (sonuc.doldu) {
|
||||||
toast.info(`Listen ${MANUEL_LISTE_MAX} tercihe ulaştı.`);
|
// Ne olduğunu ve ne yapılacağını söyle (DESIGN.md §10)
|
||||||
|
toast.info(`Listen dolu (${MANUEL_LISTE_MAX}/${MANUEL_LISTE_MAX})`, {
|
||||||
|
description: "Yeni program eklemek için listeden birini çıkar.",
|
||||||
|
action: { label: "Listeyi aç", onClick: cekmeceAc },
|
||||||
|
});
|
||||||
} else if (sonuc.zatenVar) {
|
} else if (sonuc.zatenVar) {
|
||||||
toast.info("Bu program zaten seçtiklerinde.");
|
toast.info("Bu program zaten seçtiklerinde.");
|
||||||
} else if (sonuc.eklendi) {
|
} else if (sonuc.eklendi) {
|
||||||
@@ -62,11 +69,27 @@ export function ProgramEkleButonu({
|
|||||||
yol: "dogrudan",
|
yol: "dogrudan",
|
||||||
profilli: sonuc.profilsiz ? "hayir" : "evet",
|
profilli: sonuc.profilsiz ? "hayir" : "evet",
|
||||||
});
|
});
|
||||||
|
// Kilometre taşları (DESIGN.md §10): ilk program ve dolu liste bir kez,
|
||||||
|
// sakin bir onayla söylenir; aradaki eklemeler sessiz kalır (düğme ✓
|
||||||
|
// olur, navbar sayacı artar). Profilsiz eklemede aşağıdaki toast
|
||||||
|
// zaten açıklama taşıyor.
|
||||||
|
const adet = listeOku().length;
|
||||||
|
if (!sonuc.profilsiz && adet === 1) {
|
||||||
|
toast.success(`İlk programın listende · 1/${MANUEL_LISTE_MAX}`, {
|
||||||
|
description: "Beğendiklerini ekledikçe sırasını çekmeceden değiştirebilirsin.",
|
||||||
|
action: { label: "Listeyi aç", onClick: cekmeceAc },
|
||||||
|
});
|
||||||
|
} else if (!sonuc.profilsiz && adet === MANUEL_LISTE_MAX) {
|
||||||
|
toast.success(`Listen tamam · ${MANUEL_LISTE_MAX}/${MANUEL_LISTE_MAX}`, {
|
||||||
|
description: "Şimdi sırasına bir göz at: en çok istediğin en üstte olsun.",
|
||||||
|
action: { label: "Listeyi aç", onClick: cekmeceAc },
|
||||||
|
});
|
||||||
|
}
|
||||||
if (sonuc.profilsiz) {
|
if (sonuc.profilsiz) {
|
||||||
// Önce ver, sonra iste: program eklendi; sıralama sorusu değer
|
// Önce ver, sonra iste: program eklendi; sıralama sorusu değer
|
||||||
// karşılığında gelir. Sabit id → art arda +'larda toast yığılmaz,
|
// karşılığında gelir. Sabit id → art arda +'larda toast yığılmaz,
|
||||||
// aynı toast tazelenir.
|
// aynı toast tazelenir.
|
||||||
toast.success("Seçtiklerine eklendi", {
|
toast.success(`Listene eklendi · ${adet}/${MANUEL_LISTE_MAX}`, {
|
||||||
id: PROFILSIZ_EKLEME_TOAST_ID,
|
id: PROFILSIZ_EKLEME_TOAST_ID,
|
||||||
duration: 8000,
|
duration: 8000,
|
||||||
// Vaat, akışın gerçek sonucuna eşit olmalı: bu kapı artık yalnız
|
// Vaat, akışın gerçek sonucuna eşit olmalı: bu kapı artık yalnız
|
||||||
@@ -108,10 +131,10 @@ export function ProgramEkleButonu({
|
|||||||
? `Seçtiklerin dolu (${MANUEL_LISTE_MAX}/${MANUEL_LISTE_MAX})`
|
? `Seçtiklerin dolu (${MANUEL_LISTE_MAX}/${MANUEL_LISTE_MAX})`
|
||||||
: "Seçtiklerime ekle"
|
: "Seçtiklerime ekle"
|
||||||
}
|
}
|
||||||
className={`inline-flex size-11 min-h-11 min-w-11 cursor-pointer items-center justify-center rounded-full border transition-[background-color,border-color,color,transform] duration-150 active:scale-[0.92] disabled:cursor-default focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
|
className={`inline-flex ${SATIR_DUGME_BOYU} cursor-pointer items-center justify-center rounded-full border transition-[background-color,border-color,color,transform] duration-150 active:scale-[0.92] disabled:cursor-default focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring ${
|
||||||
listede
|
listede
|
||||||
? "border-emerald-200 bg-emerald-50 text-emerald-600"
|
? "border-emerald-200 bg-emerald-50 text-emerald-600"
|
||||||
: "border-slate-200 bg-white text-slate-500 hover:border-primary hover:bg-primary/5 hover:text-primary disabled:opacity-40"
|
: "border-slate-200 bg-white text-slate-500 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500 disabled:opacity-40"
|
||||||
} ${className}`}
|
} ${className}`}
|
||||||
>
|
>
|
||||||
{listede ? (
|
{listede ? (
|
||||||
|
|||||||
@@ -20,13 +20,8 @@ import type {
|
|||||||
PuanTuruKey,
|
PuanTuruKey,
|
||||||
RankResultsListe,
|
RankResultsListe,
|
||||||
} from "@/types/yokatlas";
|
} from "@/types/yokatlas";
|
||||||
import {
|
import { DILIM_ETIKET, riskHesapla } from "@/lib/risk";
|
||||||
DILIM_ETIKET,
|
import { bolumSayfaSlug, trBaslikDuzeni, uniSayfaSlug } from "@/lib/slug";
|
||||||
RISK_ETIKET,
|
|
||||||
riskHesapla,
|
|
||||||
type RiskSeviyesi,
|
|
||||||
} from "@/lib/risk";
|
|
||||||
import { bolumSayfaSlug, uniSayfaSlug } from "@/lib/slug";
|
|
||||||
import { VERI_YIL_SAYISI } from "@/lib/veri-yillari";
|
import { VERI_YIL_SAYISI } from "@/lib/veri-yillari";
|
||||||
import {
|
import {
|
||||||
ProgramSiraGecmisi,
|
ProgramSiraGecmisi,
|
||||||
@@ -46,6 +41,22 @@ import { ProgramEkleButonu } from "@/features/liste/components/program-ekle-buto
|
|||||||
import { preloadTercihHaritasi } from "@/features/liste/components/tercih-haritasi-lazy";
|
import { preloadTercihHaritasi } from "@/features/liste/components/tercih-haritasi-lazy";
|
||||||
import { ProgramNetleriButonu } from "@/features/katalog/components/program-netleri";
|
import { ProgramNetleriButonu } from "@/features/katalog/components/program-netleri";
|
||||||
import { UniLogo } from "@/components/uni-logo";
|
import { UniLogo } from "@/components/uni-logo";
|
||||||
|
import { RiskEtiketi } from "@/components/risk-etiketi";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
SATIR_AD,
|
||||||
|
SATIR_AD_HUCRE,
|
||||||
|
SATIR_ALT_AD,
|
||||||
|
SATIR_BASLIK,
|
||||||
|
SATIR_BUTON_BASLIK,
|
||||||
|
SATIR_BUTON_HUCRE,
|
||||||
|
SATIR_BUTON_KABI,
|
||||||
|
SATIR_CIP,
|
||||||
|
SATIR_GRUP,
|
||||||
|
SATIR_IKINCIL_LINK,
|
||||||
|
SATIR_META,
|
||||||
|
SATIR_MOBIL_SAYI,
|
||||||
|
} from "@/components/liste-satiri";
|
||||||
|
|
||||||
const DILIMLER: {
|
const DILIMLER: {
|
||||||
key: DilimKey;
|
key: DilimKey;
|
||||||
@@ -78,15 +89,6 @@ const DILIMLER: {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Risk renk dili: yeşil = güvenli, sarı = az riskli, kırmızı = riskli. Nokta
|
|
||||||
// hiçbir yerde tek başına durmaz; yanında RISK_ETIKET'ten gelen ad her ekran
|
|
||||||
// genişliğinde yazılı durur (renk tek başına anlam taşımaz).
|
|
||||||
const RISK_NOKTA: Record<RiskSeviyesi, string> = {
|
|
||||||
guvenli: "bg-emerald-500",
|
|
||||||
"az-riskli": "bg-amber-500",
|
|
||||||
riskli: "bg-red-500",
|
|
||||||
};
|
|
||||||
|
|
||||||
const BOS_EKSTRA: Record<DilimKey, ProgramListeSatiri[]> = {
|
const BOS_EKSTRA: Record<DilimKey, ProgramListeSatiri[]> = {
|
||||||
hayal: [],
|
hayal: [],
|
||||||
dengeli: [],
|
dengeli: [],
|
||||||
@@ -195,14 +197,17 @@ export function ProgramTablosu({
|
|||||||
navbar'daki sıralama rozetinden değiştirilir. Aktif hap, nav'daki
|
navbar'daki sıralama rozetinden değiştirilir. Aktif hap, nav'daki
|
||||||
"Giriş yap" butonuyla aynı dilde (koyu, rounded-full) ve layoutId
|
"Giriş yap" butonuyla aynı dilde (koyu, rounded-full) ve layoutId
|
||||||
ile sekmeler arasında kayarak taşınır. */}
|
ile sekmeler arasında kayarak taşınır. */}
|
||||||
<TabsList variant="pill" className="self-center">
|
<TabsList
|
||||||
|
variant="pill"
|
||||||
|
className="grid w-full grid-cols-3 self-center sm:inline-flex sm:w-auto"
|
||||||
|
>
|
||||||
{DILIMLER.map((d) => {
|
{DILIMLER.map((d) => {
|
||||||
const aktif = aktifDilim === d.key;
|
const aktif = aktifDilim === d.key;
|
||||||
return (
|
return (
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
key={d.key}
|
key={d.key}
|
||||||
value={d.key}
|
value={d.key}
|
||||||
className="relative cursor-pointer px-3 py-1.5 transition-colors duration-200 hover:bg-slate-100 sm:px-4 sm:py-2"
|
className="relative min-h-11 cursor-pointer px-2 py-1.5 text-sm font-semibold transition-colors duration-200 hover:text-orange-500 data-active:hover:text-white group-data-[variant=pill]/tabs-list:h-auto sm:px-4"
|
||||||
>
|
>
|
||||||
{aktif ? (
|
{aktif ? (
|
||||||
<motion.span
|
<motion.span
|
||||||
@@ -211,18 +216,22 @@ export function ProgramTablosu({
|
|||||||
transition={
|
transition={
|
||||||
hareketAzalt
|
hareketAzalt
|
||||||
? { duration: 0 }
|
? { duration: 0 }
|
||||||
: { type: "spring", duration: 0.4, bounce: 0.15 }
|
: { type: "spring", duration: 0.35, bounce: 0 }
|
||||||
}
|
}
|
||||||
aria-hidden
|
aria-hidden
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
<span className="relative z-10 inline-flex items-center gap-1.5">
|
<span className="relative z-10 inline-flex flex-col items-center gap-0.5 sm:flex-row sm:gap-1.5">
|
||||||
<d.icon
|
<span className="inline-flex items-center gap-1.5">
|
||||||
className={`size-3.5 ${aktif ? d.renkAktif : d.renk}`}
|
<d.icon
|
||||||
aria-hidden
|
className={`size-4 ${aktif ? d.renkAktif : d.renk}`}
|
||||||
/>
|
aria-hidden
|
||||||
{d.etiket}
|
/>
|
||||||
<span className="text-xs tabular-nums text-slate-400">
|
{d.etiket}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className={`text-sm font-medium tabular-nums ${aktif ? "text-white/70" : "text-slate-400"}`}
|
||||||
|
>
|
||||||
{sonuclar.toplam[d.key].toLocaleString("tr-TR")}
|
{sonuclar.toplam[d.key].toLocaleString("tr-TR")}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
@@ -236,7 +245,7 @@ export function ProgramTablosu({
|
|||||||
const toplam = sonuclar.toplam[d.key];
|
const toplam = sonuclar.toplam[d.key];
|
||||||
return (
|
return (
|
||||||
<TabsContent key={d.key} value={d.key}>
|
<TabsContent key={d.key} value={d.key}>
|
||||||
<div className="mt-3 overflow-hidden rounded-2xl border border-slate-200 bg-white">
|
<div className="mt-4 overflow-hidden rounded-2xl border border-slate-200 bg-white">
|
||||||
{satirlar.length === 0 ? (
|
{satirlar.length === 0 ? (
|
||||||
<p className="px-5 py-8 text-center text-sm text-slate-500">
|
<p className="px-5 py-8 text-center text-sm text-slate-500">
|
||||||
Bu dilimde filtrene uyan program bulunamadı.
|
Bu dilimde filtrene uyan program bulunamadı.
|
||||||
@@ -244,17 +253,31 @@ export function ProgramTablosu({
|
|||||||
) : (
|
) : (
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow className="hover:bg-transparent">
|
||||||
<TableHead className="w-8 pl-4 pr-0">Risk</TableHead>
|
<TableHead
|
||||||
<TableHead>Program</TableHead>
|
className={`hidden w-28 pl-4 pr-0 sm:table-cell sm:pl-5 ${SATIR_BASLIK}`}
|
||||||
<TableHead className="hidden text-right md:table-cell">
|
>
|
||||||
|
Risk
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className={`pl-4 ${SATIR_BASLIK}`}>
|
||||||
|
Program
|
||||||
|
</TableHead>
|
||||||
|
<TableHead
|
||||||
|
className={`hidden text-right lg:table-cell ${SATIR_BASLIK}`}
|
||||||
|
>
|
||||||
Son {VERI_YIL_SAYISI} yıl
|
Son {VERI_YIL_SAYISI} yıl
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="hidden text-right sm:table-cell">
|
<TableHead
|
||||||
|
className={`hidden text-right md:table-cell ${SATIR_BASLIK}`}
|
||||||
|
>
|
||||||
Kontenjan
|
Kontenjan
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="text-right">Taban sıra</TableHead>
|
<TableHead
|
||||||
<TableHead className="w-24 text-right">
|
className={`hidden text-right sm:table-cell ${SATIR_BASLIK}`}
|
||||||
|
>
|
||||||
|
Taban sıra
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className={SATIR_BUTON_BASLIK}>
|
||||||
<span className="sr-only">Netler ve listeye ekle</span>
|
<span className="sr-only">Netler ve listeye ekle</span>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -273,17 +296,19 @@ export function ProgramTablosu({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{satirlar.length < toplam ? (
|
{satirlar.length < toplam ? (
|
||||||
<div className="mt-3 flex flex-col items-center gap-1">
|
<div className="mt-4 flex flex-col items-center gap-2">
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
size="lg"
|
||||||
|
variant="outline"
|
||||||
disabled={yuklenen === d.key}
|
disabled={yuklenen === d.key}
|
||||||
onClick={() => dahaFazlaYukle(d.key, satirlar.length)}
|
onClick={() => dahaFazlaYukle(d.key, satirlar.length)}
|
||||||
className="inline-flex cursor-pointer items-center gap-1.5 rounded-full border border-slate-200 bg-white px-4 py-2 text-sm font-medium text-slate-700 transition-colors duration-150 hover:border-slate-300 hover:bg-slate-50 disabled:cursor-default disabled:opacity-60"
|
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]"
|
||||||
>
|
>
|
||||||
<ChevronDown className="size-4" aria-hidden />
|
|
||||||
{yuklenen === d.key ? "Yükleniyor…" : "Daha fazla göster"}
|
{yuklenen === d.key ? "Yükleniyor…" : "Daha fazla göster"}
|
||||||
</button>
|
<ChevronDown className="size-4" aria-hidden />
|
||||||
<span className="text-xs tabular-nums text-slate-400">
|
</Button>
|
||||||
|
<span className="text-sm tabular-nums text-slate-500">
|
||||||
{satirlar.length.toLocaleString("tr-TR")} /{" "}
|
{satirlar.length.toLocaleString("tr-TR")} /{" "}
|
||||||
{toplam.toLocaleString("tr-TR")} program
|
{toplam.toLocaleString("tr-TR")} program
|
||||||
</span>
|
</span>
|
||||||
@@ -309,65 +334,76 @@ function ProgramSatiri({
|
|||||||
const taban = programEtkinSira(p);
|
const taban = programEtkinSira(p);
|
||||||
const risk = riskHesapla(taban, adaySira);
|
const risk = riskHesapla(taban, adaySira);
|
||||||
const devlet = p.unitur === "DEVLET";
|
const devlet = p.unitur === "DEVLET";
|
||||||
|
const tabanMetni = taban != null ? `~${taban.toLocaleString("tr-TR")}.` : "—";
|
||||||
|
const bosKaldi =
|
||||||
|
p.kontenjanSon != null &&
|
||||||
|
p.yerlesenSon != null &&
|
||||||
|
p.yerlesenSon < p.kontenjanSon;
|
||||||
|
|
||||||
|
// Şişko ve sade (DESIGN.md §4.15): program adı kesilmez, üniversite adı
|
||||||
|
// altında ayrı satır; telefonda risk ve taban sıra adın altına iner,
|
||||||
|
// butonlar alt alta 44 px.
|
||||||
return (
|
return (
|
||||||
<TableRow className="kt-cv-satir">
|
<TableRow className="kt-cv-satir">
|
||||||
<TableCell className="pl-4 pr-0 text-xs font-medium">
|
<TableCell className="hidden pl-4 pr-0 sm:table-cell sm:pl-5">
|
||||||
{risk ? (
|
{risk ? <RiskEtiketi risk={risk} /> : null}
|
||||||
<span className="inline-flex items-center gap-1.5">
|
|
||||||
<span
|
|
||||||
className={`size-2.5 shrink-0 rounded-full ${RISK_NOKTA[risk]}`}
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
{RISK_ETIKET[risk]}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="max-w-0 w-full">
|
<TableCell className={SATIR_AD_HUCRE}>
|
||||||
<span className="flex min-w-0 items-center gap-2.5">
|
<span className="flex items-start gap-3 sm:items-center">
|
||||||
<UniLogo ad={p.universite} boy="sm" />
|
<UniLogo ad={p.universite} boy="sm" />
|
||||||
<span className="min-w-0 flex-1">
|
<span className="min-w-0 flex-1">
|
||||||
<Link
|
<Link
|
||||||
href={`/bolum/${bolumSayfaSlug(p.isim)}`}
|
href={`/bolum/${bolumSayfaSlug(p.isim)}`}
|
||||||
className="block truncate text-sm font-semibold hover:text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
className={SATIR_AD}
|
||||||
title={`${p.isim} bölüm sayfası`}
|
title={`${p.isim} bölüm sayfası`}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{p.isim}
|
{p.isim}
|
||||||
</Link>
|
</Link>
|
||||||
<span className="mt-0.5 flex items-center gap-1.5 text-xs text-slate-500">
|
<span className={SATIR_GRUP}>
|
||||||
<span
|
<span className={SATIR_ALT_AD}>
|
||||||
className={`shrink-0 rounded px-1 py-px text-[10px] font-medium leading-4 ${
|
|
||||||
devlet
|
|
||||||
? "bg-slate-100 text-slate-600"
|
|
||||||
: "bg-violet-50 text-violet-600"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{devlet ? "Devlet" : "Vakıf"}
|
|
||||||
</span>
|
|
||||||
<span className="truncate">
|
|
||||||
<Link
|
<Link
|
||||||
href={`/universite/${uniSayfaSlug(p.universite)}`}
|
href={`/universite/${uniSayfaSlug(p.universite)}`}
|
||||||
className="hover:text-primary hover:underline"
|
className={SATIR_IKINCIL_LINK}
|
||||||
title={`${p.universite} taban puanları sayfası`}
|
title={`${p.universite} taban puanları sayfası`}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{p.universite}
|
{trBaslikDuzeni(p.universite)}
|
||||||
</Link>
|
</Link>
|
||||||
{p.il ? ` · ${p.il.toLocaleLowerCase("tr-TR")}` : ""}
|
|
||||||
</span>
|
</span>
|
||||||
{p.sure ? (
|
<span className={SATIR_META}>
|
||||||
<span className="shrink-0 text-slate-400">· {p.sure} yıl</span>
|
<span className={SATIR_CIP}>{devlet ? "Devlet" : "Vakıf"}</span>
|
||||||
) : null}
|
{p.il ? (
|
||||||
|
<span className="shrink-0">{trBaslikDuzeni(p.il)}</span>
|
||||||
|
) : null}
|
||||||
|
{p.sure ? (
|
||||||
|
<span className="shrink-0">{p.sure} yıl</span>
|
||||||
|
) : null}
|
||||||
|
{bosKaldi ? (
|
||||||
|
<span className="shrink-0 rounded-full bg-amber-50 px-2.5 py-0.5 text-xs font-semibold text-amber-700 sm:hidden">
|
||||||
|
Boş kaldı
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className={SATIR_MOBIL_SAYI}>
|
||||||
|
{risk ? <RiskEtiketi risk={risk} /> : null}
|
||||||
|
<span className="inline-flex items-center gap-1.5">
|
||||||
|
Taban sıra
|
||||||
|
<span className="font-semibold tabular-nums text-slate-900">
|
||||||
|
{tabanMetni}
|
||||||
|
</span>
|
||||||
|
<ProgramTabanTrendi program={p} />
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="hidden text-right text-xs md:table-cell">
|
<TableCell className="hidden text-right text-sm lg:table-cell">
|
||||||
<ProgramSiraGecmisi program={p} />
|
<ProgramSiraGecmisi program={p} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell
|
<TableCell
|
||||||
className="hidden text-right text-xs tabular-nums text-slate-500 sm:table-cell"
|
className="hidden text-right text-sm tabular-nums text-slate-600 md:table-cell"
|
||||||
title={
|
title={
|
||||||
p.kontenjanSon != null && p.yerlesenSon != null
|
p.kontenjanSon != null && p.yerlesenSon != null
|
||||||
? `${p.kontenjanSon} kontenjan, ${p.yerlesenSon} yerleşen`
|
? `${p.kontenjanSon} kontenjan, ${p.yerlesenSon} yerleşen`
|
||||||
@@ -375,38 +411,36 @@ function ProgramSatiri({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{p.kontenjanSon != null ? (
|
{p.kontenjanSon != null ? (
|
||||||
<span className="inline-flex items-center gap-1.5">
|
<span className="inline-flex flex-col items-end gap-1">
|
||||||
{p.yerlesenSon != null && p.yerlesenSon < p.kontenjanSon ? (
|
{p.kontenjanSon.toLocaleString("tr-TR")} kişi
|
||||||
<span className="rounded bg-amber-50 px-1 py-px text-[10px] font-medium leading-4 text-amber-600">
|
{bosKaldi ? (
|
||||||
boş kaldı
|
<span className="rounded-full bg-amber-50 px-2.5 py-0.5 text-xs font-semibold text-amber-700">
|
||||||
|
Boş kaldı
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
{p.kontenjanSon.toLocaleString("tr-TR")} kişi
|
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
"—"
|
"—"
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right text-xs tabular-nums text-slate-500">
|
<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">
|
<span className="inline-flex items-center gap-1.5">
|
||||||
<ProgramTabanTrendi program={p} />
|
<ProgramTabanTrendi program={p} />
|
||||||
{taban != null ? `~${taban.toLocaleString("tr-TR")}.` : "—"}
|
{tabanMetni}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="pr-4 text-right">
|
<TableCell className={SATIR_BUTON_HUCRE}>
|
||||||
<span className="inline-flex items-center gap-1.5">
|
<span className={SATIR_BUTON_KABI}>
|
||||||
<ProgramNetleriButonu
|
<ProgramNetleriButonu
|
||||||
programId={p.id}
|
programId={p.id}
|
||||||
isim={p.isim}
|
isim={p.isim}
|
||||||
universite={p.universite}
|
universite={p.universite}
|
||||||
className="size-8 min-h-8 min-w-8"
|
|
||||||
/>
|
/>
|
||||||
<ProgramEkleButonu
|
<ProgramEkleButonu
|
||||||
program={p}
|
program={p}
|
||||||
kaynak="sonuc"
|
kaynak="sonuc"
|
||||||
adaySira={adaySira}
|
adaySira={adaySira}
|
||||||
adayTur={tur}
|
adayTur={tur}
|
||||||
className="size-8 min-h-8 min-w-8"
|
|
||||||
/>
|
/>
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@@ -1,8 +1,69 @@
|
|||||||
|
import { cache } from "react";
|
||||||
import { siraylaProgramAra } from "../liste-queries";
|
import { siraylaProgramAra } from "../liste-queries";
|
||||||
import { ProgramTablosu } from "./program-tablosu";
|
import { ProgramTablosu } from "./program-tablosu";
|
||||||
|
import { ListeDurumuSatiri } from "./liste-durumu-satiri";
|
||||||
import { Skeleton } from "@/components/ui/skeleton";
|
import { Skeleton } from "@/components/ui/skeleton";
|
||||||
|
import { PUAN_TURU_ETIKET } from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||||
|
import { trBaslikDuzeni } from "@/lib/slug";
|
||||||
import type { PuanTuruKey, UniturGrubu } from "@/types/yokatlas";
|
import type { PuanTuruKey, UniturGrubu } from "@/types/yokatlas";
|
||||||
|
|
||||||
|
const sayi = (n: number) => n.toLocaleString("tr-TR");
|
||||||
|
|
||||||
|
// Başlık ve tablo aynı isteği okur; cache tek istek içinde bir kez sorgular.
|
||||||
|
// Anahtar ilkel olsun diye iller birleştirilmiş metin olarak geçer.
|
||||||
|
const sonuclariOku = cache(
|
||||||
|
(sira: number, tur: PuanTuruKey, illerAnahtari: string, tip?: UniturGrubu) =>
|
||||||
|
siraylaProgramAra(sira, tur, {
|
||||||
|
iller: illerAnahtari ? illerAnahtari.split("|") : [],
|
||||||
|
uniturGrubu: tip,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* /sonuc'un tek cümlelik cevabı (DESIGN.md §10): sayfanın h1'i, öğrencinin
|
||||||
|
* sırası ve kaç programın açık olduğu. Tadımlık ve tablo bunun altında.
|
||||||
|
* Geniş ekran altında ortalı (§5.2).
|
||||||
|
*/
|
||||||
|
export function SonucBasligi({
|
||||||
|
sira,
|
||||||
|
tur,
|
||||||
|
iller,
|
||||||
|
uniturGrubu,
|
||||||
|
}: {
|
||||||
|
sira: number;
|
||||||
|
tur: PuanTuruKey;
|
||||||
|
iller?: string[];
|
||||||
|
uniturGrubu?: UniturGrubu;
|
||||||
|
}) {
|
||||||
|
const { toplam } = sonuclariOku(sira, tur, (iller ?? []).join("|"), uniturGrubu);
|
||||||
|
const acik = toplam.hayal + toplam.dengeli + toplam.garanti;
|
||||||
|
const filtre = [
|
||||||
|
...(iller ?? []).map(trBaslikDuzeni),
|
||||||
|
...(uniturGrubu === "devlet"
|
||||||
|
? ["yalnız devlet"]
|
||||||
|
: uniturGrubu === "vakif"
|
||||||
|
? ["yalnız vakıf"]
|
||||||
|
: []),
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<header className="text-center lg:text-left">
|
||||||
|
<h1 className="font-heading text-3xl font-bold leading-tight text-balance text-slate-900 sm:text-5xl">
|
||||||
|
Sıralamanla açılan programlar
|
||||||
|
</h1>
|
||||||
|
<p className="mx-auto mt-4 max-w-2xl text-base leading-relaxed text-slate-600 sm:text-lg lg:mx-0">
|
||||||
|
<strong className="font-semibold text-slate-900">{sayi(sira)}.</strong>{" "}
|
||||||
|
sıradasın ({PUAN_TURU_ETIKET[tur] ?? tur}).{" "}
|
||||||
|
{filtre.length > 0 ? `Seçimlerine göre (${filtre.join(", ")}) ` : ""}
|
||||||
|
<strong className="font-semibold text-slate-900">
|
||||||
|
{sayi(acik)} program
|
||||||
|
</strong>{" "}
|
||||||
|
sana açık; aşağıda riskine göre ayrıldı.
|
||||||
|
</p>
|
||||||
|
<ListeDurumuSatiri />
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// /sonuc'un ham tablosunun sunucu sarmalayıcısı: okuma burada yaşar, sayfa
|
// /sonuc'un ham tablosunun sunucu sarmalayıcısı: okuma burada yaşar, sayfa
|
||||||
// yalnızca kompoze eder.
|
// yalnızca kompoze eder.
|
||||||
export function SonucProgramTablosu({
|
export function SonucProgramTablosu({
|
||||||
@@ -16,7 +77,7 @@ export function SonucProgramTablosu({
|
|||||||
iller?: string[];
|
iller?: string[];
|
||||||
uniturGrubu?: UniturGrubu;
|
uniturGrubu?: UniturGrubu;
|
||||||
}) {
|
}) {
|
||||||
const sonuclar = siraylaProgramAra(sira, tur, { iller, uniturGrubu });
|
const sonuclar = sonuclariOku(sira, tur, (iller ?? []).join("|"), uniturGrubu);
|
||||||
return (
|
return (
|
||||||
<ProgramTablosu
|
<ProgramTablosu
|
||||||
sonuclar={sonuclar}
|
sonuclar={sonuclar}
|
||||||
|
|||||||
@@ -513,10 +513,10 @@ export function TercihHaritasi({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onTercihSec ? () => onTercihSec(sira) : undefined}
|
onClick={onTercihSec ? () => onTercihSec(sira) : undefined}
|
||||||
className="flex cursor-pointer items-center gap-2 rounded-full border border-slate-200 bg-white py-1.5 pl-1.5 pr-3 text-xs transition-colors duration-200 hover:border-slate-300 hover:bg-slate-50"
|
className="flex min-h-11 cursor-pointer items-center gap-2 rounded-full border border-slate-200 bg-white py-1.5 pl-1.5 pr-3.5 text-sm transition-[background-color,border-color,transform] duration-150 hover:border-orange-500 hover:bg-orange-50/60 active:scale-[0.97]"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className="flex size-5 items-center justify-center rounded-full font-heading text-[10px] font-bold text-white"
|
className="flex size-7 shrink-0 items-center justify-center rounded-full font-heading text-xs font-bold text-white"
|
||||||
style={{ backgroundColor: RISK_RENK[pin.risk] }}
|
style={{ backgroundColor: RISK_RENK[pin.risk] }}
|
||||||
>
|
>
|
||||||
{sira}
|
{sira}
|
||||||
@@ -543,7 +543,7 @@ export function TercihHaritasi({
|
|||||||
{bolum || kisaUniAdi(pin.universite)}
|
{bolum || kisaUniAdi(pin.universite)}
|
||||||
</span>
|
</span>
|
||||||
{bolum ? (
|
{bolum ? (
|
||||||
<span className="max-w-48 truncate text-[10px] text-slate-500">
|
<span className="max-w-48 truncate text-xs text-slate-500">
|
||||||
{kisaUniAdi(pin.universite)}
|
{kisaUniAdi(pin.universite)}
|
||||||
</span>
|
</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { useReducedMotion } from "motion/react";
|
|||||||
import useMeasure from "react-use-measure";
|
import useMeasure from "react-use-measure";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { TransitionPanel } from "@/components/core/transition-panel";
|
import { TransitionPanel } from "@/components/core/transition-panel";
|
||||||
|
import { SectionEyebrow } from "@/components/pixel-decor";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
@@ -295,11 +296,9 @@ function KapıIcerik({
|
|||||||
<div ref={panelRef} className="flex flex-col gap-5 px-px">
|
<div ref={panelRef} className="flex flex-col gap-5 px-px">
|
||||||
<div>
|
<div>
|
||||||
{duzenle ? (
|
{duzenle ? (
|
||||||
<p className="text-xs font-semibold uppercase tracking-wide text-primary">
|
<SectionEyebrow>Tercihlerini güncelle</SectionEyebrow>
|
||||||
Tercihlerini güncelle
|
|
||||||
</p>
|
|
||||||
) : null}
|
) : null}
|
||||||
<h2 className="mt-2 font-heading text-2xl font-bold text-slate-900 first:mt-0">
|
<h2 className="mt-3 font-heading text-2xl font-bold text-slate-900 first:mt-0">
|
||||||
{duzenle
|
{duzenle
|
||||||
? "Seçimlerini değiştir"
|
? "Seçimlerini değiştir"
|
||||||
: "Tercihine ışık tutabilmemiz için önce ne istediğini öğrenelim"}
|
: "Tercihine ışık tutabilmemiz için önce ne istediğini öğrenelim"}
|
||||||
@@ -348,11 +347,9 @@ function KapıIcerik({
|
|||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
{duzenle ? (
|
{duzenle ? (
|
||||||
<p className="text-xs font-semibold uppercase tracking-wide text-primary">
|
<SectionEyebrow>Sıralama ve tercihler</SectionEyebrow>
|
||||||
Sıralama ve tercihler
|
|
||||||
</p>
|
|
||||||
) : null}
|
) : null}
|
||||||
<h2 className="mt-2 font-heading text-xl font-bold text-slate-900 first:mt-0">
|
<h2 className="mt-3 font-heading text-xl font-bold text-slate-900 first:mt-0">
|
||||||
{duzenle
|
{duzenle
|
||||||
? "Yeni sıralaman nedir?"
|
? "Yeni sıralaman nedir?"
|
||||||
: "Başarı sıralaman nedir?"}
|
: "Başarı sıralaman nedir?"}
|
||||||
@@ -366,7 +363,7 @@ function KapıIcerik({
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<label
|
<label
|
||||||
htmlFor="kapisi-siralama"
|
htmlFor="kapisi-siralama"
|
||||||
className="text-xs font-semibold uppercase tracking-wide text-slate-400"
|
className="text-sm font-semibold text-slate-700"
|
||||||
>
|
>
|
||||||
Başarı sıralaması ve puan türü
|
Başarı sıralaması ve puan türü
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@@ -2,13 +2,26 @@ import { CheckCircle2, X } from "lucide-react";
|
|||||||
|
|
||||||
// Ana sayfadaki karşılaştırma tablosunun hücresi: metin değeri düz yazılır,
|
// Ana sayfadaki karşılaştırma tablosunun hücresi: metin değeri düz yazılır,
|
||||||
// boolean değer Var/Yok ikonuna çevrilir.
|
// boolean değer Var/Yok ikonuna çevrilir.
|
||||||
export function ComparisonCell({ value }: { value: boolean | string }) {
|
export function ComparisonCell({
|
||||||
|
value,
|
||||||
|
vurgu = false,
|
||||||
|
}: {
|
||||||
|
value: boolean | string;
|
||||||
|
/** KolayTercih sütunu: metin koyu ve kalın */
|
||||||
|
vurgu?: boolean;
|
||||||
|
}) {
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
return <span className="text-sm font-medium">{value}</span>;
|
return (
|
||||||
|
<span
|
||||||
|
className={`block text-sm leading-snug ${vurgu ? "font-semibold text-slate-900" : "font-medium text-slate-600"}`}
|
||||||
|
>
|
||||||
|
{value}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return value ? (
|
return value ? (
|
||||||
<CheckCircle2 className="mx-auto size-5 text-primary" aria-label="Var" />
|
<CheckCircle2 className="mx-auto size-6 text-primary" aria-label="Var" />
|
||||||
) : (
|
) : (
|
||||||
<X className="mx-auto size-5 text-muted-foreground/50" aria-label="Yok" />
|
<X className="mx-auto size-5 text-slate-300" aria-label="Yok" />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ export function SosyalKanitGovde({ ilk }: { ilk: SosyalKanitVerisi }) {
|
|||||||
className="rounded-xl border border-slate-200/70 bg-slate-50/80 px-4 py-3.5"
|
className="rounded-xl border border-slate-200/70 bg-slate-50/80 px-4 py-3.5"
|
||||||
>
|
>
|
||||||
<p className="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
<p className="flex flex-wrap items-baseline gap-x-2 gap-y-0.5">
|
||||||
<span className="font-heading text-3xl font-bold tabular-nums leading-none text-slate-900">
|
<span className="font-heading text-3xl font-bold leading-none text-slate-900">
|
||||||
{sayi(gosterilenListe)}
|
{sayi(gosterilenListe)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-sm leading-snug text-slate-600">
|
<span className="text-sm leading-snug text-slate-600">
|
||||||
|
|||||||
@@ -304,8 +304,8 @@ export function ListeUretici({
|
|||||||
onClick={() => profil && void uret(profil)}
|
onClick={() => profil && void uret(profil)}
|
||||||
className="mt-6 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
className="mt-6 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||||
>
|
>
|
||||||
<RefreshCcw className="size-4" aria-hidden />
|
|
||||||
Tekrar dene
|
Tekrar dene
|
||||||
|
<RefreshCcw className="size-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
{profil ? (
|
{profil ? (
|
||||||
<p className="mt-4 text-xs text-slate-500">
|
<p className="mt-4 text-xs text-slate-500">
|
||||||
@@ -358,7 +358,7 @@ function SunucuYokEkrani() {
|
|||||||
disabled={deniyor}
|
disabled={deniyor}
|
||||||
className="mt-6 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
className="mt-6 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
||||||
>
|
>
|
||||||
{deniyor ? "Kontrol ediliyor" : "Listemi aç"}
|
{deniyor ? "Kontrol ediliyor…" : "Listemi aç"}
|
||||||
<ArrowRight className="size-4" aria-hidden />
|
<ArrowRight className="size-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
<p aria-live="polite" className="mt-4 text-xs text-slate-500">
|
<p aria-live="polite" className="mt-4 text-xs text-slate-500">
|
||||||
@@ -401,7 +401,7 @@ function UretimGovdesi({ uzunSurdu }: { uzunSurdu: boolean }) {
|
|||||||
return (
|
return (
|
||||||
<section aria-live="polite">
|
<section aria-live="polite">
|
||||||
<header className="flex flex-col items-center gap-1.5 text-center">
|
<header className="flex flex-col items-center gap-1.5 text-center">
|
||||||
<h1 className="font-heading text-xl font-bold sm:text-2xl">
|
<h1 className="font-heading text-2xl font-bold text-slate-900 sm:text-3xl">
|
||||||
Listen hazırlanıyor
|
Listen hazırlanıyor
|
||||||
</h1>
|
</h1>
|
||||||
{uzunSurdu ? (
|
{uzunSurdu ? (
|
||||||
@@ -426,15 +426,15 @@ function UretimGovdesi({ uzunSurdu }: { uzunSurdu: boolean }) {
|
|||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-slate-400">
|
<p className="text-sm font-medium text-slate-600">
|
||||||
Bu işlem yarım dakika kadar sürebilir, sayfayı kapatma.
|
Yarım dakika kadar sürebilir; bu sayfayı kapatma.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* Nihai sayfanın (listem-govde) grid'iyle aynı düzen: liste solda,
|
{/* Nihai sayfanın (listem-govde) grid'iyle aynı düzen: liste solda,
|
||||||
danışman sağda — üretim biter bitmez ekran aynı yerlere oturur. */}
|
danışman sağda — üretim biter bitmez ekran aynı yerlere oturur. */}
|
||||||
<div className="mt-8 grid items-start gap-6 lg:grid-cols-[minmax(0,1fr)_380px] xl:grid-cols-[minmax(0,1fr)_440px]">
|
<div className="mt-8 grid grid-cols-[minmax(0,1fr)] items-start gap-6 lg:grid-cols-[minmax(0,1fr)_380px] xl:grid-cols-[minmax(0,1fr)_440px]">
|
||||||
{/* Gelmekte olan tercih listesinin iskeleti */}
|
{/* Gelmekte olan tercih listesinin iskeleti */}
|
||||||
<div className="flex min-w-0 flex-col gap-2 rounded-2xl border border-slate-200 bg-white p-6 sm:p-8">
|
<div className="flex min-w-0 flex-col gap-2 rounded-2xl border border-slate-200 bg-white p-6 sm:p-8">
|
||||||
<div className="flex items-center justify-between pb-2">
|
<div className="flex items-center justify-between pb-2">
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ import {
|
|||||||
} from "@/features/liste/hooks/use-tercih-profili";
|
} from "@/features/liste/hooks/use-tercih-profili";
|
||||||
import { UNIVERSITE_TIPI_ETIKET } from "@/features/sihirbaz/sihirbaz-sabitler";
|
import { UNIVERSITE_TIPI_ETIKET } from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||||
import { PUAN_TURLERI } from "@/types/yokatlas";
|
import { PUAN_TURLERI } from "@/types/yokatlas";
|
||||||
|
import { DILIM_ETIKET } from "@/lib/risk";
|
||||||
|
|
||||||
|
// Başlık satırındaki ikincil düğmeler: 44 px, hover tek turuncu (§4.1, §2.1)
|
||||||
|
const IKINCIL_BUTON =
|
||||||
|
"h-11 cursor-pointer px-4 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]";
|
||||||
|
|
||||||
export function ListemGovde({
|
export function ListemGovde({
|
||||||
rapor,
|
rapor,
|
||||||
@@ -94,6 +99,10 @@ export function ListemGovde({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const acikSatir = kilitliBaslangic ?? rapor.tercihler.length;
|
const acikSatir = kilitliBaslangic ?? rapor.tercihler.length;
|
||||||
|
const dilimOzeti = useMemo(() => {
|
||||||
|
const say = (d: string) => rapor.tercihler.filter((t) => t.dilim === d).length;
|
||||||
|
return `${rapor.tercihler.length} tercih: ${say("hayal")} ${DILIM_ETIKET.hayal.toLocaleLowerCase("tr-TR")} · ${say("dengeli")} ${DILIM_ETIKET.dengeli.toLocaleLowerCase("tr-TR")} · ${say("garanti")} ${DILIM_ETIKET.garanti.toLocaleLowerCase("tr-TR")}`;
|
||||||
|
}, [rapor]);
|
||||||
|
|
||||||
// Profil client'ta (localStorage) çözülür: ilk render'da null döner, yani
|
// Profil client'ta (localStorage) çözülür: ilk render'da null döner, yani
|
||||||
// SSR ile aynı "Tercihlerimi düzenle" hâli basılır — hidrasyon uyuşmazlığı
|
// SSR ile aynı "Tercihlerimi düzenle" hâli basılır — hidrasyon uyuşmazlığı
|
||||||
@@ -148,7 +157,7 @@ export function ListemGovde({
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-8 grid items-start gap-6 lg:grid-cols-[minmax(0,1fr)_380px] xl:grid-cols-[minmax(0,1fr)_440px]">
|
<div className="mt-8 grid grid-cols-[minmax(0,1fr)] items-start gap-6 lg:grid-cols-[minmax(0,1fr)_380px] xl:grid-cols-[minmax(0,1fr)_440px]">
|
||||||
<div className="flex min-w-0 flex-col gap-6">
|
<div className="flex min-w-0 flex-col gap-6">
|
||||||
{/* Harita: listenin coğrafyası — ile dokun, üniversiteleri gör */}
|
{/* Harita: listenin coğrafyası — ile dokun, üniversiteleri gör */}
|
||||||
<section
|
<section
|
||||||
@@ -181,11 +190,21 @@ export function ListemGovde({
|
|||||||
aksiyonları bu kutunun kendi başlık satırında yaşar. */}
|
aksiyonları bu kutunun kendi başlık satırında yaşar. */}
|
||||||
<section className={`min-w-0${kilitAcilisi ? " kt-unblur" : ""}`}>
|
<section className={`min-w-0${kilitAcilisi ? " kt-unblur" : ""}`}>
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3 pb-1">
|
<div className="flex flex-wrap items-center justify-between gap-3 pb-1">
|
||||||
<h1 className="min-w-0 font-heading text-xl font-bold tracking-tight sm:text-2xl">
|
{/* Sayfa nerede olduğunu söyler (DESIGN.md §10.1): başlık yer,
|
||||||
{hasPaket
|
altındaki satır listenin sayılarla özeti. */}
|
||||||
? "Yapay Zeka tercih listen"
|
<div className="min-w-0">
|
||||||
: "Veriyi ücretsiz incele. Kararı Yapay Zeka ile netleştir."}
|
<h1 className="font-heading text-2xl font-bold text-balance text-slate-900 sm:text-3xl">
|
||||||
</h1>
|
Tercih listen
|
||||||
|
</h1>
|
||||||
|
<p className="mt-1 text-base text-slate-600">
|
||||||
|
{dilimOzeti}
|
||||||
|
{hasPaket ? null : (
|
||||||
|
<span className="block text-sm text-slate-500">
|
||||||
|
Veriyi ücretsiz incele, kararı Yapay Zeka ile netleştir.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div className="flex shrink-0 flex-wrap gap-2">
|
<div className="flex shrink-0 flex-wrap gap-2">
|
||||||
{/* Tercih düzenleme her kullanıcıya açık: kapı (layout'ta monte
|
{/* Tercih düzenleme her kullanıcıya açık: kapı (layout'ta monte
|
||||||
TercihProfiliKapisi) profili günceller, /listem'e döner.
|
TercihProfiliKapisi) profili günceller, /listem'e döner.
|
||||||
@@ -200,9 +219,8 @@ export function ListemGovde({
|
|||||||
{farkVar ? (
|
{farkVar ? (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="sm"
|
|
||||||
onClick={() => setFarkModali(true)}
|
onClick={() => setFarkModali(true)}
|
||||||
className="cursor-pointer transition-[background-color,transform] duration-200 active:scale-[0.97]"
|
className="h-11 cursor-pointer px-4 transition-[background-color,transform] duration-200 active:scale-[0.97]"
|
||||||
>
|
>
|
||||||
<RefreshCcw className="size-4" aria-hidden />
|
<RefreshCcw className="size-4" aria-hidden />
|
||||||
Tercihlerin değişmiş
|
Tercihlerin değişmiş
|
||||||
@@ -211,9 +229,8 @@ export function ListemGovde({
|
|||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
|
||||||
onClick={profilDuzenlemeyiAc}
|
onClick={profilDuzenlemeyiAc}
|
||||||
className="cursor-pointer"
|
className={IKINCIL_BUTON}
|
||||||
>
|
>
|
||||||
<SlidersHorizontal className="size-4" aria-hidden />
|
<SlidersHorizontal className="size-4" aria-hidden />
|
||||||
Tercihlerimi düzenle
|
Tercihlerimi düzenle
|
||||||
@@ -224,8 +241,7 @@ export function ListemGovde({
|
|||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
className={IKINCIL_BUTON}
|
||||||
className="cursor-pointer"
|
|
||||||
>
|
>
|
||||||
<Link href="#revizyon">
|
<Link href="#revizyon">
|
||||||
<PencilLine className="size-4" aria-hidden />
|
<PencilLine className="size-4" aria-hidden />
|
||||||
@@ -235,8 +251,7 @@ export function ListemGovde({
|
|||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
className={IKINCIL_BUTON}
|
||||||
className="cursor-pointer"
|
|
||||||
>
|
>
|
||||||
<Link href="/rapor/yazdir" target="_blank">
|
<Link href="/rapor/yazdir" target="_blank">
|
||||||
<FileText className="size-4" aria-hidden />
|
<FileText className="size-4" aria-hidden />
|
||||||
@@ -372,7 +387,7 @@ export function ListemGovde({
|
|||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className="mt-4 flex items-center justify-center gap-4 text-[11px] text-slate-500">
|
<div className="mt-4 flex flex-wrap items-center justify-center gap-x-4 gap-y-1 text-xs text-slate-500">
|
||||||
<span className="inline-flex items-center gap-1">
|
<span className="inline-flex items-center gap-1">
|
||||||
<ShieldCheck className="size-3.5" aria-hidden />
|
<ShieldCheck className="size-3.5" aria-hidden />
|
||||||
iyzico güvenli ödeme
|
iyzico güvenli ödeme
|
||||||
|
|||||||
@@ -189,7 +189,7 @@ export function ListemIcerikSkeleton() {
|
|||||||
<Skeleton className="h-6 w-40 rounded-full" />
|
<Skeleton className="h-6 w-40 rounded-full" />
|
||||||
<Skeleton className="mt-4 h-9 w-3/4 max-w-xl" />
|
<Skeleton className="mt-4 h-9 w-3/4 max-w-xl" />
|
||||||
<Skeleton className="mt-3 h-5 w-2/3 max-w-lg" />
|
<Skeleton className="mt-3 h-5 w-2/3 max-w-lg" />
|
||||||
<div className="mt-8 grid items-start gap-6 lg:grid-cols-[minmax(0,1fr)_380px] xl:grid-cols-[minmax(0,1fr)_440px]">
|
<div className="mt-8 grid grid-cols-[minmax(0,1fr)] items-start gap-6 lg:grid-cols-[minmax(0,1fr)_380px] xl:grid-cols-[minmax(0,1fr)_440px]">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{Array.from({ length: 10 }).map((_, i) => (
|
{Array.from({ length: 10 }).map((_, i) => (
|
||||||
<Skeleton key={i} className="h-16 w-full rounded-xl" />
|
<Skeleton key={i} className="h-16 w-full rounded-xl" />
|
||||||
|
|||||||
@@ -26,12 +26,28 @@ import {
|
|||||||
TableHeader,
|
TableHeader,
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { bolumSayfaSlug, uniSayfaSlug } from "@/lib/slug";
|
import { bolumSayfaSlug, trBaslikDuzeni, uniSayfaSlug } from "@/lib/slug";
|
||||||
|
import { UNITUR_ETIKET } from "@/lib/unitur";
|
||||||
|
import { RiskEtiketi } from "@/components/risk-etiketi";
|
||||||
|
import {
|
||||||
|
SATIR_AD,
|
||||||
|
SATIR_AD_DUZ,
|
||||||
|
SATIR_AD_HUCRE,
|
||||||
|
SATIR_ALT_AD,
|
||||||
|
SATIR_BASLIK,
|
||||||
|
SATIR_BUTON_BASLIK,
|
||||||
|
SATIR_BUTON_HUCRE,
|
||||||
|
SATIR_CIP,
|
||||||
|
SATIR_DUGME_BOYU,
|
||||||
|
SATIR_GRUP,
|
||||||
|
SATIR_IKINCIL_LINK,
|
||||||
|
SATIR_META,
|
||||||
|
SATIR_MOBIL_SAYI,
|
||||||
|
} from "@/components/liste-satiri";
|
||||||
import { VERI_YIL_SAYISI } from "@/lib/veri-yillari";
|
import { VERI_YIL_SAYISI } from "@/lib/veri-yillari";
|
||||||
import type { RaporSonuc } from "@/features/rapor/types/rapor";
|
import type { RaporSonuc } from "@/features/rapor/types/rapor";
|
||||||
import {
|
import {
|
||||||
DILIM_ETIKET,
|
DILIM_ETIKET,
|
||||||
RISK_ETIKET,
|
|
||||||
dilimdenFarkliRiskEtiketi,
|
dilimdenFarkliRiskEtiketi,
|
||||||
dilimdenRisk,
|
dilimdenRisk,
|
||||||
riskHesapla,
|
riskHesapla,
|
||||||
@@ -39,13 +55,11 @@ import {
|
|||||||
} from "@/lib/risk";
|
} from "@/lib/risk";
|
||||||
import type { Dilim } from "@/lib/rapor-havuzu";
|
import type { Dilim } from "@/lib/rapor-havuzu";
|
||||||
|
|
||||||
// Risk renk dili: yeşil = güvenli, sarı = az riskli, kırmızı = riskli.
|
// Detay başlığındaki dilim ikonu; risk etiketi RiskEtiketi'nden (§4.5).
|
||||||
// Nokta hiçbir yerde tek başına durmaz; yanındaki etiket her ekran
|
const RISK_STIL: Record<RiskSeviyesi, { icon: typeof Rocket }> = {
|
||||||
// genişliğinde yazılı durur (renk tek başına anlam taşımaz).
|
riskli: { icon: Rocket },
|
||||||
const RISK_STIL: Record<RiskSeviyesi, { nokta: string; icon: typeof Rocket }> = {
|
"az-riskli": { icon: Scale },
|
||||||
riskli: { nokta: "bg-red-500", icon: Rocket },
|
guvenli: { icon: ShieldCheck },
|
||||||
"az-riskli": { nokta: "bg-amber-500", icon: Scale },
|
|
||||||
guvenli: { nokta: "bg-emerald-500", icon: ShieldCheck },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Sütun sayısı: # · Program · Son N yıl · Risk · Taban sıra · detay oku
|
// Sütun sayısı: # · Program · Son N yıl · Risk · Taban sıra · detay oku
|
||||||
@@ -78,26 +92,28 @@ export function RaporSatirDetayi({
|
|||||||
const farkliRisk = dilimdenFarkliRiskEtiketi(dilim, risk);
|
const farkliRisk = dilimdenFarkliRiskEtiketi(dilim, risk);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mb-2 flex flex-wrap items-center gap-2 text-xs text-slate-500">
|
<div className="mb-3 flex flex-wrap items-center gap-2 text-sm text-slate-500">
|
||||||
<stil.icon className="size-3.5" aria-hidden />
|
<stil.icon className="size-4" aria-hidden />
|
||||||
<span className="font-medium">
|
<span className="font-semibold text-slate-700">
|
||||||
{DILIM_ETIKET[dilim as Dilim] ?? dilim}
|
{DILIM_ETIKET[dilim as Dilim] ?? dilim}
|
||||||
{farkliRisk ? ` · ${farkliRisk}` : null}
|
{farkliRisk ? ` · ${farkliRisk}` : null}
|
||||||
</span>
|
</span>
|
||||||
{unitur ? <span>· {unitur}</span> : null}
|
{unitur ? (
|
||||||
|
<span>· {UNITUR_ETIKET[unitur]?.cip ?? unitur}</span>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<dl className="grid gap-2 text-sm sm:grid-cols-2">
|
<dl className="grid gap-3 text-base leading-relaxed sm:grid-cols-2">
|
||||||
<div className="rounded-lg border border-slate-200 bg-white p-3">
|
<div className="rounded-xl border border-slate-200 bg-white p-4">
|
||||||
<dt className="text-xs font-semibold text-slate-500">
|
<dt className="text-sm font-semibold text-slate-900">
|
||||||
Neden listede?
|
Neden listede?
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="mt-1 text-slate-700">{gerekce}</dd>
|
<dd className="mt-1 text-slate-600">{gerekce}</dd>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border border-slate-200 bg-white p-3">
|
<div className="rounded-xl border border-slate-200 bg-white p-4">
|
||||||
<dt className="text-xs font-semibold text-slate-500">
|
<dt className="text-sm font-semibold text-slate-900">
|
||||||
Risk ve Yapay Zeka yorumu
|
Risk ve Yapay Zeka yorumu
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="mt-1 text-slate-700">
|
<dd className="mt-1 text-slate-600">
|
||||||
{riskNotu} {trendOzeti}
|
{riskNotu} {trendOzeti}
|
||||||
</dd>
|
</dd>
|
||||||
</div>
|
</div>
|
||||||
@@ -181,11 +197,19 @@ export function RaporListesi({
|
|||||||
const risk =
|
const risk =
|
||||||
(adaySira != null ? riskHesapla(p?.efektifSira, adaySira) : null) ??
|
(adaySira != null ? riskHesapla(p?.efektifSira, adaySira) : null) ??
|
||||||
dilimdenRisk(t.dilim);
|
dilimdenRisk(t.dilim);
|
||||||
const stil = RISK_STIL[risk];
|
|
||||||
const seri = raporSiraSerisi(p?.siraGecmisi, p?.ilkYil);
|
const seri = raporSiraSerisi(p?.siraGecmisi, p?.ilkYil);
|
||||||
const taban = p?.efektifSira ?? programEtkinSira(seri);
|
const taban = p?.efektifSira ?? programEtkinSira(seri);
|
||||||
const acikMi = acik.has(t.sira);
|
const acikMi = acik.has(t.sira);
|
||||||
const devlet = p?.unitur === "DEVLET";
|
const devlet = p?.unitur === "DEVLET";
|
||||||
|
const tabanMetni = taban != null ? `~${taban.toLocaleString("tr-TR")}.` : "—";
|
||||||
|
const riskHucresi = analizKilitli ? (
|
||||||
|
<span className="inline-flex items-center gap-1.5 text-xs font-semibold text-slate-500">
|
||||||
|
<Lock className="size-3.5 shrink-0 text-slate-400" aria-hidden />
|
||||||
|
Pakette
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<RiskEtiketi risk={risk} />
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={t.sira}>
|
<Fragment key={t.sira}>
|
||||||
@@ -197,92 +221,88 @@ export function RaporListesi({
|
|||||||
onClick={() => tiklandi(t.sira)}
|
onClick={() => tiklandi(t.sira)}
|
||||||
className="kt-cv-satir cursor-pointer"
|
className="kt-cv-satir cursor-pointer"
|
||||||
>
|
>
|
||||||
<TableCell className="pl-4 pr-0">
|
<TableCell className="pl-4 pr-0 align-top sm:pl-5 sm:align-middle">
|
||||||
<span className="flex size-7 items-center justify-center rounded-full bg-slate-100 font-heading text-xs font-bold">
|
<span className="mt-4 flex size-8 items-center justify-center rounded-full bg-slate-100 font-heading text-sm font-bold text-slate-900 sm:mt-0">
|
||||||
{t.sira}
|
{t.sira}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="max-w-0 w-full">
|
<TableCell className={SATIR_AD_HUCRE}>
|
||||||
<span className="flex min-w-0 items-center gap-2.5">
|
<span className="flex items-start gap-3 sm:items-center">
|
||||||
{p?.universite ? <UniLogo ad={p.universite} boy="sm" /> : null}
|
{/* Telefonda logo yok: sıra numarası satırı zaten çapalıyor,
|
||||||
|
logo adı üç satıra sıkıştırıyordu (375 px ölçümü). */}
|
||||||
|
{p?.universite ? (
|
||||||
|
<span className="hidden sm:block">
|
||||||
|
<UniLogo ad={p.universite} boy="sm" />
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
<span className="min-w-0 flex-1">
|
<span className="min-w-0 flex-1">
|
||||||
{p ? (
|
{p ? (
|
||||||
<Link
|
<Link
|
||||||
href={`/bolum/${bolumSayfaSlug(p.isim)}`}
|
href={`/bolum/${bolumSayfaSlug(p.isim)}`}
|
||||||
className="block truncate text-sm font-semibold hover:text-primary hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
className={SATIR_AD}
|
||||||
title={`${p.isim} bölüm sayfası`}
|
title={`${p.isim} bölüm sayfası`}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{p.isim}
|
{p.isim}
|
||||||
</Link>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<span className="block truncate text-sm font-semibold">
|
<span className={SATIR_AD_DUZ}>{t.programId}</span>
|
||||||
{t.programId}
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
<span className="mt-0.5 flex items-center gap-1.5 text-xs text-slate-500">
|
<span className={SATIR_GRUP}>
|
||||||
{p?.unitur ? (
|
{p?.universite ? (
|
||||||
<span
|
<span className={SATIR_ALT_AD}>
|
||||||
className={`shrink-0 rounded px-1 py-px text-[10px] font-medium leading-4 ${
|
|
||||||
devlet
|
|
||||||
? "bg-slate-100 text-slate-600"
|
|
||||||
: "bg-violet-50 text-violet-600"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{devlet ? "Devlet" : "Vakıf"}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
<span className="truncate">
|
|
||||||
{p?.universite ? (
|
|
||||||
<Link
|
<Link
|
||||||
href={`/universite/${uniSayfaSlug(p.universite)}`}
|
href={`/universite/${uniSayfaSlug(p.universite)}`}
|
||||||
className="hover:text-primary hover:underline"
|
className={SATIR_IKINCIL_LINK}
|
||||||
title={`${p.universite} taban puanları sayfası`}
|
title={`${p.universite} taban puanları sayfası`}
|
||||||
onClick={(e) => e.stopPropagation()}
|
onClick={(e) => e.stopPropagation()}
|
||||||
>
|
>
|
||||||
{p.universite}
|
{trBaslikDuzeni(p.universite)}
|
||||||
</Link>
|
</Link>
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
<span className={SATIR_META}>
|
||||||
|
{p?.unitur ? (
|
||||||
|
<span className={SATIR_CIP}>{devlet ? "Devlet" : "Vakıf"}</span>
|
||||||
) : null}
|
) : null}
|
||||||
{p?.il ? ` · ${p.il.toLocaleLowerCase("tr-TR")}` : ""}
|
{p?.il ? (
|
||||||
|
<span className="shrink-0">{trBaslikDuzeni(p.il)}</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
<span className={SATIR_MOBIL_SAYI}>
|
||||||
|
{riskHucresi}
|
||||||
|
<span className="inline-flex items-center gap-1.5">
|
||||||
|
Taban sıra
|
||||||
|
<span className="font-semibold tabular-nums text-slate-900">
|
||||||
|
{tabanMetni}
|
||||||
|
</span>
|
||||||
|
<ProgramTabanTrendi program={seri} />
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="hidden text-right text-xs md:table-cell">
|
<TableCell className="hidden text-right text-sm lg:table-cell">
|
||||||
<ProgramSiraGecmisi program={seri} />
|
<ProgramSiraGecmisi program={seri} />
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-xs font-medium">
|
<TableCell className="hidden sm:table-cell">{riskHucresi}</TableCell>
|
||||||
{analizKilitli ? (
|
<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 text-slate-500">
|
<span className="inline-flex items-center gap-1.5">
|
||||||
<Lock className="size-3.5 shrink-0 text-slate-400" aria-hidden />
|
|
||||||
<span className="sr-only sm:not-sr-only">Pakette</span>
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span className="inline-flex items-center gap-1.5">
|
|
||||||
<span
|
|
||||||
className={`size-2.5 shrink-0 rounded-full ${stil.nokta}`}
|
|
||||||
aria-hidden
|
|
||||||
/>
|
|
||||||
<span>{RISK_ETIKET[risk]}</span>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right text-xs tabular-nums text-slate-500">
|
|
||||||
<span className="inline-flex items-center gap-1">
|
|
||||||
<ProgramTabanTrendi program={seri} />
|
<ProgramTabanTrendi program={seri} />
|
||||||
{taban != null ? `~${taban.toLocaleString("tr-TR")}.` : "—"}
|
{tabanMetni}
|
||||||
</span>
|
</span>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="pr-4 text-right">
|
<TableCell className={SATIR_BUTON_HUCRE}>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-expanded={acikMi}
|
aria-expanded={acikMi}
|
||||||
|
aria-controls={`rapor-satir-${t.sira}`}
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
tiklandi(t.sira);
|
tiklandi(t.sira);
|
||||||
}}
|
}}
|
||||||
className="inline-flex size-8 min-h-8 min-w-8 cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-white text-slate-500 transition-[background-color,border-color,color,transform] duration-150 hover:border-primary hover:bg-primary/5 hover:text-primary active:scale-[0.92] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
className={`inline-flex ${SATIR_DUGME_BOYU} cursor-pointer items-center justify-center rounded-full border border-slate-200 bg-white text-slate-500 transition-[background-color,border-color,color,transform] duration-150 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500 active:scale-[0.92] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring`}
|
||||||
>
|
>
|
||||||
<ChevronDown
|
<ChevronDown
|
||||||
className={`size-4 transition-transform duration-200 ${acikMi ? "rotate-180" : ""}`}
|
className={`size-4 transition-transform duration-200 ${acikMi ? "rotate-180" : ""}`}
|
||||||
@@ -296,24 +316,25 @@ export function RaporListesi({
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
|
|
||||||
{acikMi ? (
|
{acikMi ? (
|
||||||
<TableRow className="hover:bg-transparent">
|
<TableRow id={`rapor-satir-${t.sira}`} className="hover:bg-transparent">
|
||||||
<TableCell
|
<TableCell
|
||||||
colSpan={SUTUN_SAYISI}
|
colSpan={SUTUN_SAYISI}
|
||||||
className="whitespace-normal bg-slate-50/60 px-4 py-4"
|
className="whitespace-normal bg-slate-50 px-4 py-5 sm:px-5"
|
||||||
>
|
>
|
||||||
{analizKilitli ? (
|
{analizKilitli ? (
|
||||||
<>
|
<>
|
||||||
<div className="mb-2 flex flex-wrap items-center gap-2 text-xs text-slate-500">
|
<div className="mb-3 flex flex-wrap items-center gap-2 text-sm text-slate-500">
|
||||||
<Lock className="size-3.5" aria-hidden />
|
<Lock className="size-4" aria-hidden />
|
||||||
<span className="font-medium">Kişisel analiz pakette</span>
|
<span className="font-semibold text-slate-700">
|
||||||
{p?.unitur ? <span>· {p.unitur}</span> : null}
|
Kişisel analiz pakette
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-lg border border-dashed border-slate-300 bg-white p-3">
|
<div className="rounded-xl border border-dashed border-slate-300 bg-white p-4">
|
||||||
<p className="flex items-center gap-1.5 text-xs font-semibold text-slate-700">
|
<p className="flex items-center gap-1.5 text-sm font-semibold text-slate-900">
|
||||||
<Lock className="size-3.5" aria-hidden />
|
<Lock className="size-4" aria-hidden />
|
||||||
Yapay Zeka karar desteği
|
Yapay Zeka karar desteği
|
||||||
</p>
|
</p>
|
||||||
<p className="mt-1 text-sm text-slate-600">
|
<p className="mt-1 text-base leading-relaxed text-slate-600">
|
||||||
Bu bölümün neden listende olduğunu ve sıralamana özel
|
Bu bölümün neden listende olduğunu ve sıralamana özel
|
||||||
yerleşme riskini paketle gör.
|
yerleşme riskini paketle gör.
|
||||||
</p>
|
</p>
|
||||||
@@ -333,9 +354,9 @@ export function RaporListesi({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onHaritadaGor(p.il, t.sira)}
|
onClick={() => onHaritadaGor(p.il, t.sira)}
|
||||||
className="mt-3 inline-flex cursor-pointer items-center gap-1.5 text-xs font-medium text-primary transition-colors duration-200 hover:text-primary/80"
|
className="mt-2 inline-flex min-h-11 cursor-pointer items-center gap-1.5 text-sm font-semibold text-primary transition-colors duration-150 hover:text-orange-500"
|
||||||
>
|
>
|
||||||
<MapPin className="size-3.5" aria-hidden />
|
<MapPin className="size-4" aria-hidden />
|
||||||
Haritada gör
|
Haritada gör
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -349,13 +370,13 @@ export function RaporListesi({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{kilitIdx < rapor.tercihler.length ? (
|
{kilitIdx < rapor.tercihler.length ? (
|
||||||
<p className="text-right text-xs font-medium text-emerald-700">
|
<p className="text-center text-sm font-semibold text-emerald-700 sm:text-right">
|
||||||
Programlar ve ham veriler ücretsiz
|
Programlar ve ham veriler ücretsiz
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{rapor.kapsam?.uyarlandi ? (
|
{rapor.kapsam?.uyarlandi ? (
|
||||||
<p className="mt-3 rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900">
|
<p className="mt-3 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm leading-relaxed text-amber-900">
|
||||||
Tercihlerine göre {rapor.kapsam.toplam} uygun program bulundu; listen{" "}
|
Tercihlerine göre {rapor.kapsam.toplam} uygun program bulundu; listen{" "}
|
||||||
{rapor.kapsam.dagilim.hayal} hayal · {rapor.kapsam.dagilim.dengeli}{" "}
|
{rapor.kapsam.dagilim.hayal} hayal · {rapor.kapsam.dagilim.dengeli}{" "}
|
||||||
dengeli · {rapor.kapsam.dagilim.garanti} güvenli olarak kuruldu. Daha
|
dengeli · {rapor.kapsam.dagilim.garanti} güvenli olarak kuruldu. Daha
|
||||||
@@ -364,27 +385,31 @@ export function RaporListesi({
|
|||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{tekIl ? (
|
{tekIl ? (
|
||||||
<p className="mt-3 rounded-lg border border-amber-200 bg-amber-50 px-3 py-2 text-xs text-amber-900">
|
<p className="mt-3 rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm leading-relaxed text-amber-900">
|
||||||
Listenin {rapor.tercihler.length} tercihi de tek şehirde (
|
Listenin {rapor.tercihler.length} tercihi de tek şehirde (
|
||||||
{tekIl.toLocaleLowerCase("tr-TR")}). Boşta kalma riskini azaltmak için
|
{trBaslikDuzeni(tekIl)}). Boşta kalma riskini azaltmak için
|
||||||
birkaç farklı şehir eklemeyi düşünebilirsin.
|
birkaç farklı şehir eklemeyi düşünebilirsin.
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="mt-3 overflow-hidden rounded-2xl border border-slate-200 bg-white">
|
<div className="mt-4 overflow-hidden rounded-2xl border border-slate-200 bg-white">
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow className="hover:bg-transparent">
|
||||||
<TableHead className="w-8 pl-4 pr-0">#</TableHead>
|
<TableHead className={`w-12 pl-4 pr-0 sm:pl-5 ${SATIR_BASLIK}`}>
|
||||||
<TableHead>Program</TableHead>
|
#
|
||||||
<TableHead className="hidden text-right md:table-cell">
|
</TableHead>
|
||||||
|
<TableHead className={`pl-4 ${SATIR_BASLIK}`}>Program</TableHead>
|
||||||
|
<TableHead className={`hidden text-right lg:table-cell ${SATIR_BASLIK}`}>
|
||||||
Son {yilSayisi} yıl
|
Son {yilSayisi} yıl
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead>
|
<TableHead className={`hidden sm:table-cell ${SATIR_BASLIK}`}>
|
||||||
<span className="hidden sm:inline">Yapay Zeka </span>Risk
|
Yapay Zeka riski
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableHead className="text-right">Taban sıra</TableHead>
|
<TableHead className={`hidden text-right sm:table-cell ${SATIR_BASLIK}`}>
|
||||||
<TableHead className="w-12 pr-4 text-right">
|
Taban sıra
|
||||||
|
</TableHead>
|
||||||
|
<TableHead className={SATIR_BUTON_BASLIK}>
|
||||||
<span className="sr-only">Yapay Zeka değerlendirmesi</span>
|
<span className="sr-only">Yapay Zeka değerlendirmesi</span>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export async function RaporYazdir() {
|
|||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<footer className="mt-6 border-t border-slate-300 pt-3 text-[10px] leading-relaxed text-slate-500">
|
<footer className="mt-6 border-t border-slate-300 pt-3 text-xs leading-relaxed text-slate-500">
|
||||||
<p>
|
<p>
|
||||||
Bu rapor KolayTercih tarafından resmî YÖK Atlas verisi ({veriAraligi}{" "}
|
Bu rapor KolayTercih tarafından resmî YÖK Atlas verisi ({veriAraligi}{" "}
|
||||||
taban sıralamaları, kontenjan ve yerleşme istatistikleri) kullanılarak
|
taban sıralamaları, kontenjan ve yerleşme istatistikleri) kullanılarak
|
||||||
|
|||||||
@@ -24,7 +24,14 @@ import {
|
|||||||
import { useGorunumOlayi } from "@/components/use-gorunum-olayi";
|
import { useGorunumOlayi } from "@/components/use-gorunum-olayi";
|
||||||
import { olay, siraKovasi } from "@/lib/analitik";
|
import { olay, siraKovasi } from "@/lib/analitik";
|
||||||
import { dilimdenRisk, riskHesapla } from "@/lib/risk";
|
import { dilimdenRisk, riskHesapla } from "@/lib/risk";
|
||||||
import { turkishSlugify } from "@/lib/slug";
|
import { trBaslikDuzeni, turkishSlugify } from "@/lib/slug";
|
||||||
|
import {
|
||||||
|
SATIR_ALT_AD,
|
||||||
|
SATIR_AD_DUZ,
|
||||||
|
SATIR_CIP,
|
||||||
|
SATIR_GRUP,
|
||||||
|
SATIR_META,
|
||||||
|
} from "@/components/liste-satiri";
|
||||||
import { listemUretHref, PROFIL_DEGISTI_EVENT } from "@/features/sihirbaz/sihirbaz-sabitler";
|
import { listemUretHref, PROFIL_DEGISTI_EVENT } from "@/features/sihirbaz/sihirbaz-sabitler";
|
||||||
import { tercihProfiliOku } from "@/features/sihirbaz/sihirbaz-profil";
|
import { tercihProfiliOku } from "@/features/sihirbaz/sihirbaz-profil";
|
||||||
import type { TadimlikSatir, TadimlikTip } from "@/lib/tadimlik-havuzu";
|
import type { TadimlikSatir, TadimlikTip } from "@/lib/tadimlik-havuzu";
|
||||||
@@ -120,14 +127,11 @@ export function TadimlikSatiri({
|
|||||||
const girisUrl = `/giris?callback=${encodeURIComponent(listemUretHref("tadimlik"))}`;
|
const girisUrl = `/giris?callback=${encodeURIComponent(listemUretHref("tadimlik"))}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// first:mt-0 — kayıtlı listesi olan ziyaretçide /sonuc tadımlığı haritanın
|
// /sonuc'ta artık ilk öğe sonuç başlığı (h1); tadımlık onun altında
|
||||||
// ve tablonun ALTINA koyar (sonuc/page.tsx), orada bölüm ritmini veren
|
// (listesi olanda haritanın ve tablonun altında) mt-12 ile durur.
|
||||||
// mt-16'yı alır. Liste yoksa tadımlık main'in ilk DOM çocuğudur (JSX
|
|
||||||
// yorumları düğüm üretmez) ve main'in py-12'si tek başına yeterli üst
|
|
||||||
// boşluğu verir.
|
|
||||||
// Tadımlığın render edilmediği durumlarda (girişli kullanıcı, boş havuz)
|
// Tadımlığın render edilmediği durumlarda (girişli kullanıcı, boş havuz)
|
||||||
// bu bileşen hiç dönmediği için kural ilk gerçek bölüme geçer.
|
// bu bileşen hiç dönmez.
|
||||||
<section ref={gorunumRef} className="mt-16 min-w-0 first:mt-0">
|
<section ref={gorunumRef} className="mt-12 min-w-0">
|
||||||
<div className="flex flex-col items-center text-center">
|
<div className="flex flex-col items-center text-center">
|
||||||
<SectionEyebrow>Yapay zekâdan örnek satır</SectionEyebrow>
|
<SectionEyebrow>Yapay zekâdan örnek satır</SectionEyebrow>
|
||||||
<h2 className="mt-4 font-heading text-2xl font-bold sm:text-3xl">
|
<h2 className="mt-4 font-heading text-2xl font-bold sm:text-3xl">
|
||||||
@@ -147,40 +151,36 @@ export function TadimlikSatiri({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-6 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm sm:p-6">
|
<div className="mt-6 rounded-2xl border border-slate-200 bg-white p-4 shadow-sm sm:p-6">
|
||||||
<div className="flex min-w-0 flex-wrap items-center gap-2.5">
|
{/* Rapor listesinin satırıyla aynı düzen (§4.15): ad sarar,
|
||||||
|
üniversite adı altında, taban sıra sağda iri. */}
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
<UniLogo ad={p.universite} boy="sm" />
|
<UniLogo ad={p.universite} boy="sm" />
|
||||||
<span className="min-w-0 flex-1">
|
<span className="min-w-0 flex-1">
|
||||||
<span className="block truncate text-sm font-semibold">
|
<span className={SATIR_AD_DUZ}>{p.isim}</span>
|
||||||
{p.isim}
|
<span className={SATIR_GRUP}>
|
||||||
</span>
|
<span className={SATIR_ALT_AD}>
|
||||||
<span className="mt-0.5 flex items-center gap-1.5 text-xs text-slate-500">
|
{trBaslikDuzeni(p.universite)}
|
||||||
{p.unitur ? (
|
</span>
|
||||||
<span
|
<span className={SATIR_META}>
|
||||||
className={`shrink-0 rounded px-1 py-px text-[10px] font-medium leading-4 ${
|
{p.unitur ? (
|
||||||
devlet
|
<span className={SATIR_CIP}>{devlet ? "Devlet" : "Vakıf"}</span>
|
||||||
? "bg-slate-100 text-slate-600"
|
) : null}
|
||||||
: "bg-violet-50 text-violet-600"
|
{p.il ? (
|
||||||
}`}
|
<span className="shrink-0">{trBaslikDuzeni(p.il)}</span>
|
||||||
>
|
) : null}
|
||||||
{devlet ? "Devlet" : "Vakıf"}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
<span className="truncate">
|
|
||||||
{p.universite}
|
|
||||||
{p.il ? ` · ${p.il.toLocaleLowerCase("tr-TR")}` : ""}
|
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="hidden text-right text-xs md:inline-flex">
|
<span className="hidden text-right md:inline-flex">
|
||||||
<ProgramSiraGecmisi program={seri} />
|
<ProgramSiraGecmisi program={seri} />
|
||||||
</span>
|
</span>
|
||||||
<span className="inline-flex items-center gap-1 text-xs tabular-nums text-slate-500">
|
<span className="inline-flex shrink-0 items-center gap-1.5 text-base font-semibold tabular-nums text-slate-900">
|
||||||
<ProgramTabanTrendi program={seri} />
|
<ProgramTabanTrendi program={seri} />
|
||||||
{taban != null ? `~${taban.toLocaleString("tr-TR")}.` : "—"}
|
{taban != null ? `~${taban.toLocaleString("tr-TR")}.` : "—"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 rounded-lg bg-slate-50/60 px-4 py-4">
|
<div className="mt-4 rounded-xl bg-slate-50 px-4 py-5">
|
||||||
<RaporSatirDetayi
|
<RaporSatirDetayi
|
||||||
dilim={satir.dilim}
|
dilim={satir.dilim}
|
||||||
risk={risk}
|
risk={risk}
|
||||||
@@ -201,7 +201,7 @@ export function TadimlikSatiri({
|
|||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
asChild
|
asChild
|
||||||
className="h-11 w-full shrink-0 cursor-pointer bg-orange-500 text-white transition-[background-color,transform] duration-200 active:scale-[0.97] hover:bg-orange-600 sm:w-auto"
|
className="h-12 w-full shrink-0 cursor-pointer bg-orange-500 text-white transition-[background-color,transform] duration-200 active:scale-[0.97] hover:bg-orange-600 sm:w-auto"
|
||||||
>
|
>
|
||||||
<Link
|
<Link
|
||||||
href={girisUrl}
|
href={girisUrl}
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ export function TercihDegistiModali({
|
|||||||
key={satir.alan}
|
key={satir.alan}
|
||||||
className={i > 0 ? "border-t border-slate-200" : undefined}
|
className={i > 0 ? "border-t border-slate-200" : undefined}
|
||||||
>
|
>
|
||||||
<dt className="bg-slate-50 px-3 py-1.5 text-[11px] font-semibold uppercase tracking-wide text-slate-500">
|
<dt className="bg-slate-50 px-3 py-2 text-sm font-semibold text-slate-700">
|
||||||
{satir.etiket}
|
{satir.etiket}
|
||||||
</dt>
|
</dt>
|
||||||
<dd className="grid grid-cols-2 divide-x divide-slate-200">
|
<dd className="grid grid-cols-2 divide-x divide-slate-200">
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { olay, siraKovasi } from "@/lib/analitik";
|
import { olay, siraKovasi } from "@/lib/analitik";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { PikselCtaYuzeyi } from "@/components/piksel-cta-yuzeyi";
|
||||||
import { onbellekliJsonGetir } from "@/lib/istek-cache";
|
import { onbellekliJsonGetir } from "@/lib/istek-cache";
|
||||||
import { useTercihProfili } from "@/features/liste/hooks/use-tercih-profili";
|
import { useTercihProfili } from "@/features/liste/hooks/use-tercih-profili";
|
||||||
import type { SihirbazFacetleri } from "@/types/yokatlas";
|
import type { SihirbazFacetleri } from "@/types/yokatlas";
|
||||||
@@ -61,13 +62,35 @@ function turEtiketi(tur: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** "Sayısal · 100.000. sıra" — kayıtlı profilin kısa özeti. */
|
/** "Sayısal · 100.000. sıra" — kayıtlı profilin kısa özeti. */
|
||||||
function KayitliSira({ sira, tur }: { sira: number; tur: string }) {
|
function KayitliSira({
|
||||||
|
sira,
|
||||||
|
tur,
|
||||||
|
koyu = false,
|
||||||
|
}: {
|
||||||
|
sira: number;
|
||||||
|
tur: string;
|
||||||
|
/** Koyu funnel yüzeyinde (PikselCtaYuzeyi) */
|
||||||
|
koyu?: boolean;
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<span className="inline-flex items-center gap-1.5 text-sm text-slate-600">
|
<span
|
||||||
<Trophy className="size-4 shrink-0 text-primary" aria-hidden />
|
className={cn(
|
||||||
|
"inline-flex items-center gap-1.5 text-sm",
|
||||||
|
koyu ? "text-slate-300" : "text-slate-600",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Trophy
|
||||||
|
className={cn("size-4 shrink-0", koyu ? "text-orange-500" : "text-primary")}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
<span>
|
<span>
|
||||||
Sıralaman kayıtlı:{" "}
|
Sıralaman kayıtlı:{" "}
|
||||||
<strong className="font-semibold tabular-nums text-slate-900">
|
<strong
|
||||||
|
className={cn(
|
||||||
|
"font-semibold tabular-nums",
|
||||||
|
koyu ? "text-white" : "text-slate-900",
|
||||||
|
)}
|
||||||
|
>
|
||||||
{sira.toLocaleString("tr-TR")}.
|
{sira.toLocaleString("tr-TR")}.
|
||||||
</strong>{" "}
|
</strong>{" "}
|
||||||
sıra · {turEtiketi(tur)}
|
sıra · {turEtiketi(tur)}
|
||||||
@@ -424,54 +447,58 @@ export function CtaSiraForm({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Kart varyantı sayfa sonundaki funnel CTA'sıdır: arama kartının koyu
|
||||||
|
// yüzeyi + 24'lük piksel liste (DESIGN.md §10.8).
|
||||||
if (profil) {
|
if (profil) {
|
||||||
return (
|
return (
|
||||||
<section className="flex flex-col items-center rounded-2xl border border-primary/20 bg-primary/5 px-6 py-8 text-center sm:px-10 sm:py-10">
|
<PikselCtaYuzeyi metinOfseti={5}>
|
||||||
<h2 className="font-heading text-xl font-bold text-balance text-slate-900 sm:text-2xl">
|
<h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl">
|
||||||
{baslik ?? "Sıralamana yakın programlar hangileri? Saniyede gör."}
|
{baslik ?? "Sıralamana yakın programlar hangileri? Saniyede gör."}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="mt-3">
|
<div className="mt-3">
|
||||||
<KayitliSira sira={profil.sira} tur={profil.tur} />
|
<KayitliSira sira={profil.sira} tur={profil.tur} koyu />
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 max-w-xl text-sm leading-6 text-slate-600">
|
<p className="mt-2 max-w-xl text-base leading-relaxed text-slate-300">
|
||||||
Gerçek YÖK Atlas verisiyle hayal, dengeli ve güvenli dilimlerindeki
|
Gerçek YÖK Atlas verisiyle hayal, dengeli ve güvenli dilimlerindeki
|
||||||
programların hazır.
|
programların hazır.
|
||||||
</p>
|
</p>
|
||||||
<PagePixelDivider seed={67} className="mt-6" />
|
<Button
|
||||||
<Button asChild size="lg" className={cn(PROFILLI_CTA_SINIF, "mt-6")}>
|
asChild
|
||||||
|
size="lg"
|
||||||
|
className={cn(PROFILLI_CTA_SINIF, "mt-6 w-full active:scale-[0.97] sm:w-auto")}
|
||||||
|
>
|
||||||
<Link href={sonucHref()}>
|
<Link href={sonucHref()}>
|
||||||
Sıralamana uygun programları gör
|
Sıralamana uygun programları gör
|
||||||
<ArrowRight className="size-4" aria-hidden />
|
<ArrowRight className="size-4" aria-hidden />
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</section>
|
</PikselCtaYuzeyi>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<div ref={formRef}>
|
||||||
ref={formRef}
|
<PikselCtaYuzeyi metinOfseti={9}>
|
||||||
className="rounded-2xl border border-primary/20 bg-primary/5 p-6 sm:p-8"
|
<h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl">
|
||||||
>
|
{baslik ?? "Sıralamana yakın programlar hangileri? Saniyede gör."}
|
||||||
<h2 className="font-heading text-xl font-bold text-slate-900">
|
</h2>
|
||||||
{baslik ?? "Sıralamana yakın programlar hangileri? Saniyede gör."}
|
<p className="mt-3 max-w-xl text-base leading-relaxed text-slate-300">
|
||||||
</h2>
|
{aciklama ??
|
||||||
<p className="mt-2 text-sm leading-6 text-slate-600">
|
"YKS başarı sıralamanı gir; gerçek YÖK Atlas verisiyle sıralamana uygun hayal, dengeli ve güvenli dilimlerindeki programları anında listele. Ücretsiz, üyelik gerekmez."}
|
||||||
{aciklama ??
|
</p>
|
||||||
"YKS başarı sıralamanı gir; gerçek YÖK Atlas verisiyle sıralamana uygun hayal, dengeli ve güvenli dilimlerindeki programları anında listele. Ücretsiz, üyelik gerekmez."}
|
<Button
|
||||||
</p>
|
type="button"
|
||||||
<Button
|
size="lg"
|
||||||
type="button"
|
onClick={() => setAcik(true)}
|
||||||
onClick={() => setAcik(true)}
|
onPointerEnter={preloadSihirbazAdimlar}
|
||||||
onPointerEnter={preloadSihirbazAdimlar}
|
onFocus={preloadSihirbazAdimlar}
|
||||||
onFocus={preloadSihirbazAdimlar}
|
className="mt-6 h-12 w-full cursor-pointer bg-orange-500 px-6 text-white transition-[background-color,transform] duration-200 hover:bg-orange-600 active:scale-[0.97] sm:w-auto"
|
||||||
className="mt-4 h-11 cursor-pointer bg-orange-500 px-6 text-sm font-semibold text-white transition-colors duration-200 hover:bg-orange-600"
|
>
|
||||||
>
|
Sıralamanı gir, programları gör
|
||||||
Sıralamanı gir, programları gör
|
<ArrowRight className="size-4" aria-hidden />
|
||||||
<ArrowRight className="size-4" aria-hidden />
|
</Button>
|
||||||
</Button>
|
</PikselCtaYuzeyi>
|
||||||
|
|
||||||
{cekmece}
|
{cekmece}
|
||||||
</section>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,13 +28,18 @@ export function Cip({
|
|||||||
onClick,
|
onClick,
|
||||||
children,
|
children,
|
||||||
kucuk = false,
|
kucuk = false,
|
||||||
|
className = "",
|
||||||
}: {
|
}: {
|
||||||
secili: boolean;
|
secili: boolean;
|
||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
kucuk?: boolean;
|
kucuk?: boolean;
|
||||||
|
className?: string;
|
||||||
}) {
|
}) {
|
||||||
const boyut = kucuk ? "px-2.5 py-1 text-xs" : "px-3.5 py-2 text-sm";
|
// Şişko ve sade (DESIGN.md §1.1): çip 44 px dokunma hedefi; il
|
||||||
|
// çipleri (kucuk) çok sayıda olduğu için 40 px. Hover §2.1 tek turuncu.
|
||||||
|
const boyut = kucuk ? "h-10 px-3.5 text-sm" : "h-11 px-4 text-sm";
|
||||||
|
const temel = `inline-flex cursor-pointer items-center rounded-full border font-semibold transition-[background-color,border-color,color,transform] duration-150 active:scale-[0.97] ${boyut} ${className}`;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -42,8 +47,8 @@ export function Cip({
|
|||||||
aria-pressed={secili}
|
aria-pressed={secili}
|
||||||
className={
|
className={
|
||||||
secili
|
secili
|
||||||
? `cursor-pointer rounded-full border border-primary bg-primary font-medium text-white transition-colors duration-200 ${boyut}`
|
? `${temel} border-primary bg-primary text-white`
|
||||||
: `cursor-pointer rounded-full border border-slate-200 bg-white text-slate-700 transition-colors duration-200 hover:border-slate-300 hover:bg-slate-50 ${boyut}`
|
: `${temel} border-slate-200 bg-white text-slate-700 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500`
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
@@ -51,6 +56,8 @@ export function Cip({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ADIM_ADLARI = ["Alan", "Şehir", "Öncelik"] as const;
|
||||||
|
|
||||||
// Haritada karşılığı olan iller (KKTC/yurtdışı kampüsler çip olarak kalır)
|
// Haritada karşılığı olan iller (KKTC/yurtdışı kampüsler çip olarak kalır)
|
||||||
const HARITA_ILLERI = new Set(cities.map((c) => normalizeIlAdi(c.name)));
|
const HARITA_ILLERI = new Set(cities.map((c) => normalizeIlAdi(c.name)));
|
||||||
|
|
||||||
@@ -232,22 +239,40 @@ export function SihirbazAdimlar({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
<div className="flex items-center gap-1.5">
|
{/* Adım göstergesi (DESIGN.md §10): üç parçalı çubuk + adların
|
||||||
{[0, 1, 2].map((i) => (
|
kendisi; öğrenci nerede olduğunu ve ne kaldığını görür. Değişim
|
||||||
<span
|
ekran okuyucuya duyurulur (WCAG 4.1.3). */}
|
||||||
key={i}
|
<div>
|
||||||
className={
|
<ol className="grid grid-cols-3 gap-2" aria-label="Sihirbaz adımları">
|
||||||
i === adim
|
{ADIM_ADLARI.map((ad, i) => (
|
||||||
? "h-1.5 w-6 rounded-full bg-primary transition-[width,background-color] duration-200 ease-out"
|
<li key={ad} className="flex flex-col gap-1.5">
|
||||||
: i < adim
|
{/* Dolum transform ile (yalnız transform/opaklık canlandırılır);
|
||||||
? "h-1.5 w-3 rounded-full bg-primary/50 transition-[width,background-color] duration-200 ease-out"
|
soldan dolar, güçlü ease-out 300 ms. */}
|
||||||
: "h-1.5 w-1.5 rounded-full bg-slate-200 transition-[width,background-color] duration-200 ease-out"
|
<span className="h-1.5 overflow-hidden rounded-full bg-slate-200" aria-hidden>
|
||||||
}
|
<span
|
||||||
/>
|
className={`block h-full origin-left rounded-full bg-primary transition-transform duration-300 ease-[cubic-bezier(0.23,1,0.32,1)] motion-reduce:transition-none ${
|
||||||
))}
|
i <= adim ? "scale-x-100" : "scale-x-0"
|
||||||
<span className="ml-2 text-xs font-medium text-slate-400">
|
}`}
|
||||||
Adım {adim + 1}/3
|
/>
|
||||||
</span>
|
</span>
|
||||||
|
<span
|
||||||
|
className={`text-sm ${
|
||||||
|
i === adim
|
||||||
|
? "font-semibold text-slate-900"
|
||||||
|
: i < adim
|
||||||
|
? "font-medium text-slate-600"
|
||||||
|
: "text-slate-400"
|
||||||
|
}`}
|
||||||
|
aria-current={i === adim ? "step" : undefined}
|
||||||
|
>
|
||||||
|
{ad}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ol>
|
||||||
|
<p role="status" className="sr-only">
|
||||||
|
Adım {adim + 1} / 3: {ADIM_ADLARI[adim]}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TransitionPanel
|
<TransitionPanel
|
||||||
@@ -293,7 +318,7 @@ export function SihirbazAdimlar({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div ref={panelRef} className="px-px pb-2">
|
<div ref={panelRef} className="px-px pb-2">
|
||||||
<h3 className="font-heading text-lg font-bold">
|
<h3 className="font-heading text-xl font-bold text-slate-900">
|
||||||
Hangi alanlara ilgi duyuyorsun?
|
Hangi alanlara ilgi duyuyorsun?
|
||||||
</h3>
|
</h3>
|
||||||
<p className="mt-1 text-sm text-slate-500">
|
<p className="mt-1 text-sm text-slate-500">
|
||||||
@@ -307,20 +332,20 @@ export function SihirbazAdimlar({
|
|||||||
onClick={() => listeDegistir(kategoriler, setKategoriler, k.ad)}
|
onClick={() => listeDegistir(kategoriler, setKategoriler, k.ad)}
|
||||||
>
|
>
|
||||||
{k.ad}
|
{k.ad}
|
||||||
<span className="ml-1.5 text-xs opacity-70">{k.adet}</span>
|
<span className="ml-1.5 text-xs font-medium opacity-70">{k.adet}</span>
|
||||||
</Cip>
|
</Cip>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref={panelRef}>
|
<div ref={panelRef}>
|
||||||
<h3 className="font-heading text-lg font-bold">
|
<h3 className="font-heading text-xl font-bold text-slate-900">
|
||||||
Nerede okumak istersin?
|
Nerede okumak istersin?
|
||||||
</h3>
|
</h3>
|
||||||
<p className="mt-1 text-sm text-slate-500">
|
<p className="mt-1 text-sm text-slate-500">
|
||||||
En fazla 5 il seçebilirsin. İstersen boş bırak, fark etmez.
|
En fazla 5 il seçebilirsin. İstersen boş bırak, fark etmez.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-4 flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3.5 py-2 transition-colors focus-within:border-slate-300">
|
<div className="mt-4 flex h-11 items-center gap-2 rounded-full border border-slate-200 bg-white px-4 transition-colors focus-within:border-slate-300">
|
||||||
<Search className="size-4 shrink-0 text-slate-400" aria-hidden />
|
<Search className="size-4 shrink-0 text-slate-400" aria-hidden />
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
@@ -329,14 +354,14 @@ export function SihirbazAdimlar({
|
|||||||
placeholder="İl ara..."
|
placeholder="İl ara..."
|
||||||
aria-label="İl ara"
|
aria-label="İl ara"
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
className="min-w-0 flex-1 bg-transparent text-sm text-slate-950 outline-none placeholder:text-slate-400"
|
className="min-w-0 flex-1 bg-transparent text-base text-slate-950 outline-none placeholder:text-slate-400"
|
||||||
/>
|
/>
|
||||||
{ilArama ? (
|
{ilArama ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setIlArama("")}
|
onClick={() => setIlArama("")}
|
||||||
aria-label="Aramayı temizle"
|
aria-label="Aramayı temizle"
|
||||||
className="flex size-5 shrink-0 cursor-pointer items-center justify-center rounded-full text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700"
|
className="-mr-2 flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-full text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700"
|
||||||
>
|
>
|
||||||
<X className="size-3.5" aria-hidden />
|
<X className="size-3.5" aria-hidden />
|
||||||
</button>
|
</button>
|
||||||
@@ -350,7 +375,7 @@ export function SihirbazAdimlar({
|
|||||||
yalnızca görünümü daraltır, seçimleri etkilemez ve kırpmayı
|
yalnızca görünümü daraltır, seçimleri etkilemez ve kırpmayı
|
||||||
devre dışı bırakır. */}
|
devre dışı bırakır. */}
|
||||||
<div
|
<div
|
||||||
className={`mt-3 flex max-h-56 flex-wrap gap-1.5 overflow-y-auto sm:max-h-none sm:overflow-visible ${
|
className={`mt-3 flex max-h-64 flex-wrap gap-2 overflow-y-auto sm:max-h-none sm:overflow-visible ${
|
||||||
facetYukleniyor ? "animate-pulse opacity-60" : ""
|
facetYukleniyor ? "animate-pulse opacity-60" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -362,14 +387,14 @@ export function SihirbazAdimlar({
|
|||||||
onClick={() => listeDegistir(iller, setIller, i.il, 5)}
|
onClick={() => listeDegistir(iller, setIller, i.il, 5)}
|
||||||
>
|
>
|
||||||
{trBaslikDuzeni(i.il)}
|
{trBaslikDuzeni(i.il)}
|
||||||
<span className="ml-1 text-[10px] opacity-70">{i.adet}</span>
|
<span className="ml-1.5 text-xs font-medium opacity-70">{i.adet}</span>
|
||||||
</Cip>
|
</Cip>
|
||||||
))}
|
))}
|
||||||
{ilListesiKirpik ? (
|
{ilListesiKirpik ? (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setTumIllerAcik(true)}
|
onClick={() => setTumIllerAcik(true)}
|
||||||
className="cursor-pointer rounded-full border border-dashed border-slate-300 bg-white px-2.5 py-1 text-xs font-medium text-slate-500 transition-colors duration-200 hover:border-slate-400 hover:bg-slate-50 hover:text-slate-700"
|
className="inline-flex h-10 cursor-pointer items-center rounded-full border border-dashed border-slate-300 bg-white px-3.5 text-sm font-semibold text-slate-500 transition-[background-color,border-color,color,transform] duration-150 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500 active:scale-[0.97]"
|
||||||
>
|
>
|
||||||
+{gorunenTurkiyeIlleri.length - IL_ON_IZLEME} il daha
|
+{gorunenTurkiyeIlleri.length - IL_ON_IZLEME} il daha
|
||||||
</button>
|
</button>
|
||||||
@@ -381,7 +406,7 @@ export function SihirbazAdimlar({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setTumIllerAcik(false)}
|
onClick={() => setTumIllerAcik(false)}
|
||||||
className="cursor-pointer rounded-full border border-dashed border-slate-300 bg-white px-2.5 py-1 text-xs font-medium text-slate-500 transition-colors duration-200 hover:border-slate-400 hover:bg-slate-50 hover:text-slate-700"
|
className="inline-flex h-10 cursor-pointer items-center rounded-full border border-dashed border-slate-300 bg-white px-3.5 text-sm font-semibold text-slate-500 transition-[background-color,border-color,color,transform] duration-150 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500 active:scale-[0.97]"
|
||||||
>
|
>
|
||||||
Daha az göster
|
Daha az göster
|
||||||
</button>
|
</button>
|
||||||
@@ -398,11 +423,11 @@ export function SihirbazAdimlar({
|
|||||||
|
|
||||||
{gorunenDisIller.length > 0 ? (
|
{gorunenDisIller.length > 0 ? (
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<p className="mb-2 text-xs font-semibold tracking-wide text-slate-400 uppercase">
|
<p className="mb-2 text-sm font-semibold text-slate-700">
|
||||||
KKTC & Yurt Dışı
|
KKTC & Yurt Dışı
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
className={`flex flex-wrap gap-1.5 ${
|
className={`flex flex-wrap gap-2 ${
|
||||||
facetYukleniyor ? "animate-pulse opacity-60" : ""
|
facetYukleniyor ? "animate-pulse opacity-60" : ""
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
@@ -414,7 +439,7 @@ export function SihirbazAdimlar({
|
|||||||
onClick={() => listeDegistir(iller, setIller, i.il, 5)}
|
onClick={() => listeDegistir(iller, setIller, i.il, 5)}
|
||||||
>
|
>
|
||||||
{trBaslikDuzeni(i.il)}
|
{trBaslikDuzeni(i.il)}
|
||||||
<span className="ml-1 text-[10px] opacity-70">{i.adet}</span>
|
<span className="ml-1.5 text-xs font-medium opacity-70">{i.adet}</span>
|
||||||
</Cip>
|
</Cip>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -423,7 +448,7 @@ export function SihirbazAdimlar({
|
|||||||
|
|
||||||
{iller.length > 0 ? (
|
{iller.length > 0 ? (
|
||||||
<div className="mt-3 flex flex-wrap items-center gap-2">
|
<div className="mt-3 flex flex-wrap items-center gap-2">
|
||||||
<span className="text-xs font-semibold text-slate-400">
|
<span className="text-sm font-semibold text-slate-700">
|
||||||
Seçtiklerin:
|
Seçtiklerin:
|
||||||
</span>
|
</span>
|
||||||
{iller.map((il) => (
|
{iller.map((il) => (
|
||||||
@@ -431,10 +456,10 @@ export function SihirbazAdimlar({
|
|||||||
key={il}
|
key={il}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => listeDegistir(iller, setIller, il)}
|
onClick={() => listeDegistir(iller, setIller, il)}
|
||||||
className="inline-flex cursor-pointer items-center gap-1 rounded-full bg-orange-500 px-3 py-1.5 text-xs font-medium text-white transition-colors duration-200 hover:bg-orange-600"
|
className="inline-flex h-10 cursor-pointer items-center gap-1.5 rounded-full bg-primary px-3.5 text-sm font-semibold text-white transition-[background-color,transform] duration-150 hover:bg-primary/90 active:scale-[0.97]"
|
||||||
>
|
>
|
||||||
{trBaslikDuzeni(il)}
|
{trBaslikDuzeni(il)}
|
||||||
<X className="size-3" aria-hidden />
|
<X className="size-4" aria-hidden />
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -449,9 +474,9 @@ export function SihirbazAdimlar({
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div ref={panelRef} className="space-y-5">
|
<div ref={panelRef} className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-heading text-lg font-bold">
|
<h3 className="font-heading text-xl font-bold text-slate-900">
|
||||||
Üniversite tipi & öncelikler
|
Üniversite tipi & öncelikler
|
||||||
</h3>
|
</h3>
|
||||||
<p className="mt-1 text-sm text-slate-500">
|
<p className="mt-1 text-sm text-slate-500">
|
||||||
@@ -459,7 +484,7 @@ export function SihirbazAdimlar({
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-2 text-xs font-semibold tracking-wide text-slate-400 uppercase">
|
<p className="mb-2 text-sm font-semibold text-slate-700">
|
||||||
Üniversite tipi
|
Üniversite tipi
|
||||||
</p>
|
</p>
|
||||||
<div
|
<div
|
||||||
@@ -478,7 +503,7 @@ export function SihirbazAdimlar({
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
{UNIVERSITE_TIPI_ETIKET[u.grup]}
|
{UNIVERSITE_TIPI_ETIKET[u.grup]}
|
||||||
<span className="ml-1.5 text-xs opacity-70">{u.adet}</span>
|
<span className="ml-1.5 text-xs font-medium opacity-70">{u.adet}</span>
|
||||||
</Cip>
|
</Cip>
|
||||||
))}
|
))}
|
||||||
<Cip
|
<Cip
|
||||||
@@ -490,13 +515,16 @@ export function SihirbazAdimlar({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-2 text-xs font-semibold tracking-wide text-slate-400 uppercase">
|
<p className="mb-2 text-sm font-semibold text-slate-700">
|
||||||
Senin için önemli olan
|
Senin için önemli olan
|
||||||
</p>
|
</p>
|
||||||
<div className="flex flex-wrap gap-2">
|
{/* Telefonda iki sütun, uzun ad iki satıra sarar: tek sütunda
|
||||||
|
yedi çip 44 px'le pencereyi 812 px'in dışına itiyordu. */}
|
||||||
|
<div className="grid grid-cols-2 gap-2 sm:flex sm:flex-wrap">
|
||||||
{ONCELIKLER.map((o) => (
|
{ONCELIKLER.map((o) => (
|
||||||
<Cip
|
<Cip
|
||||||
key={o}
|
key={o}
|
||||||
|
className="h-auto min-h-11 justify-center px-3 py-1.5 text-center leading-tight sm:h-11 sm:px-4 sm:py-0"
|
||||||
secili={oncelikler.includes(o)}
|
secili={oncelikler.includes(o)}
|
||||||
onClick={() => listeDegistir(oncelikler, setOncelikler, o)}
|
onClick={() => listeDegistir(oncelikler, setOncelikler, o)}
|
||||||
>
|
>
|
||||||
@@ -504,7 +532,7 @@ export function SihirbazAdimlar({
|
|||||||
</Cip>
|
</Cip>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-2 text-xs text-slate-500">
|
<p className="mt-2 text-sm text-slate-500">
|
||||||
Öncelikler yapay zekânın gerekçelerine ve program seçimine yön
|
Öncelikler yapay zekânın gerekçelerine ve program seçimine yön
|
||||||
verir; listeyi daraltmaz.
|
verir; listeyi daraltmaz.
|
||||||
</p>
|
</p>
|
||||||
@@ -512,13 +540,13 @@ export function SihirbazAdimlar({
|
|||||||
</div>
|
</div>
|
||||||
</TransitionPanel>
|
</TransitionPanel>
|
||||||
|
|
||||||
<div className="flex items-center justify-between border-t border-slate-100 pt-4">
|
<div className="flex items-center justify-between gap-2 border-t border-slate-100 pt-4">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
onClick={() => adimaGit(adim - 1)}
|
onClick={() => adimaGit(adim - 1)}
|
||||||
disabled={adim === 0}
|
disabled={adim === 0}
|
||||||
className={adim === 0 ? "invisible" : "cursor-pointer"}
|
className={adim === 0 ? "invisible" : "h-11 cursor-pointer px-3"}
|
||||||
>
|
>
|
||||||
<ArrowLeft className="size-4" aria-hidden />
|
<ArrowLeft className="size-4" aria-hidden />
|
||||||
Geri
|
Geri
|
||||||
@@ -528,7 +556,7 @@ export function SihirbazAdimlar({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => adimaGit(adim + 1)}
|
onClick={() => adimaGit(adim + 1)}
|
||||||
disabled={!adimGecerli}
|
disabled={!adimGecerli}
|
||||||
className="cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
className="h-11 cursor-pointer bg-orange-500 px-5 text-white transition-[background-color,transform] duration-200 hover:bg-orange-600 active:scale-[0.97]"
|
||||||
>
|
>
|
||||||
Devam
|
Devam
|
||||||
<ArrowRight className="size-4" aria-hidden />
|
<ArrowRight className="size-4" aria-hidden />
|
||||||
@@ -546,7 +574,7 @@ export function SihirbazAdimlar({
|
|||||||
onTamamla({ kategoriler, iller, universiteTipi, oncelikler });
|
onTamamla({ kategoriler, iller, universiteTipi, oncelikler });
|
||||||
}}
|
}}
|
||||||
disabled={kategoriler.length === 0}
|
disabled={kategoriler.length === 0}
|
||||||
className="cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600"
|
className="h-11 cursor-pointer bg-orange-500 px-5 text-white transition-[background-color,transform] duration-200 hover:bg-orange-600 active:scale-[0.97]"
|
||||||
>
|
>
|
||||||
{sonButonEtiketi}
|
{sonButonEtiketi}
|
||||||
<ArrowRight className="size-4" aria-hidden />
|
<ArrowRight className="size-4" aria-hidden />
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { ArrowRight } from "lucide-react";
|
|||||||
import { animate } from "motion/mini";
|
import { animate } from "motion/mini";
|
||||||
import { useReducedMotion } from "motion/react";
|
import { useReducedMotion } from "motion/react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { PixelField } from "@/components/pixel-decor";
|
import { PikselCtaYuzeyi } from "@/components/piksel-cta-yuzeyi";
|
||||||
import { useGorunumOlayi } from "@/components/use-gorunum-olayi";
|
import { useGorunumOlayi } from "@/components/use-gorunum-olayi";
|
||||||
import { olay, olayBirKez, siraKovasi } from "@/lib/analitik";
|
import { olay, olayBirKez, siraKovasi } from "@/lib/analitik";
|
||||||
import type { SihirbazFacetleri } from "@/types/yokatlas";
|
import type { SihirbazFacetleri } from "@/types/yokatlas";
|
||||||
@@ -279,70 +279,57 @@ export function SihirbazBolumu({
|
|||||||
</Button>
|
</Button>
|
||||||
</section>
|
</section>
|
||||||
) : girisBekliyor ? (
|
) : girisBekliyor ? (
|
||||||
<section
|
<div ref={hazirPanelRef} className="mt-16 min-w-0">
|
||||||
ref={hazirPanelRef}
|
<PikselCtaYuzeyi metinOfseti={3}>
|
||||||
className="relative isolate mt-16 min-w-0 overflow-hidden rounded-2xl bg-primary px-5 py-8 text-center text-white sm:p-12"
|
<h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl">
|
||||||
>
|
Seçimlerin hazır — Yapay Zeka listen bir adım uzakta
|
||||||
<div
|
</h2>
|
||||||
aria-hidden
|
<p className="mt-3 max-w-xl text-base leading-relaxed text-slate-300">
|
||||||
className="pointer-events-none absolute inset-x-0 -inset-y-10 -z-10 text-white"
|
Yukarıdaki tablo her zaman ücretsiz. 5 deneme kredinle 24
|
||||||
>
|
tercihlik Yapay Zeka listeni de kurabilirsin: ücretsiz giriş yap,
|
||||||
<PixelField seed={13} />
|
seçimlerin kayıtlı — listen otomatik kurulsun (üretim 3 kredi,
|
||||||
</div>
|
kalan 2 kredinle danışmana soru sorarsın).
|
||||||
<h2 className="relative font-heading text-2xl font-bold sm:text-3xl">
|
</p>
|
||||||
Seçimlerin hazır — Yapay Zeka listen bir adım uzakta
|
<Button
|
||||||
</h2>
|
asChild
|
||||||
<p className="relative mx-auto mt-3 max-w-xl text-white/90">
|
size="lg"
|
||||||
Yukarıdaki tablo her zaman ücretsiz. 5 deneme kredinle 24
|
className="mt-6 h-auto min-h-12 w-full cursor-pointer whitespace-normal bg-orange-500 px-4 py-3 text-white transition-[background-color,transform] duration-200 hover:bg-orange-600 active:scale-[0.97] sm:w-auto sm:px-8"
|
||||||
tercihlik Yapay Zeka listeni de kurabilirsin: ücretsiz giriş yap,
|
|
||||||
seçimlerin kayıtlı — listen otomatik kurulsun (üretim 3
|
|
||||||
kredi, kalan 2 kredinle danışmana soru sorarsın).
|
|
||||||
</p>
|
|
||||||
<Button
|
|
||||||
asChild
|
|
||||||
size="lg"
|
|
||||||
className="relative mt-6 h-auto min-h-12 w-full cursor-pointer whitespace-normal bg-orange-500 px-4 py-3 text-white transition-colors duration-200 hover:bg-orange-600 sm:w-auto sm:px-8"
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
href={girisUrl}
|
|
||||||
onClick={() =>
|
|
||||||
olay("giris_cta_tiklandi", { kaynak: "hazir_panel" })
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
5 deneme kredinle Yapay Zeka listeni kur — giriş yap
|
<Link
|
||||||
<ArrowRight className="size-4" aria-hidden />
|
href={girisUrl}
|
||||||
</Link>
|
onClick={() =>
|
||||||
</Button>
|
olay("giris_cta_tiklandi", { kaynak: "hazir_panel" })
|
||||||
<p className="relative mt-3 text-xs text-white/70">
|
}
|
||||||
Kredi kartı gerekmez · Google ya da e-posta ile 30 saniyede
|
>
|
||||||
</p>
|
5 deneme kredinle Yapay Zeka listeni kur — giriş yap
|
||||||
</section>
|
<ArrowRight className="size-4" aria-hidden />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
<p className="mt-3 text-sm text-slate-400">
|
||||||
|
Kredi kartı gerekmez · Google ya da e-posta ile 30 saniyede
|
||||||
|
</p>
|
||||||
|
</PikselCtaYuzeyi>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<section className="relative isolate mt-16 min-w-0 overflow-hidden rounded-2xl bg-primary px-5 py-8 text-center text-white sm:p-12">
|
<PikselCtaYuzeyi className="mt-16 min-w-0" metinOfseti={11}>
|
||||||
<div
|
<h2 className="font-heading text-2xl font-bold text-balance sm:text-3xl">
|
||||||
aria-hidden
|
|
||||||
className="pointer-events-none absolute inset-x-0 -inset-y-10 -z-10 text-white"
|
|
||||||
>
|
|
||||||
<PixelField seed={19} />
|
|
||||||
</div>
|
|
||||||
<h2 className="relative font-heading text-2xl font-bold sm:text-3xl">
|
|
||||||
24 tercihi tek tek elemek yerine, yapay zekâ kursun
|
24 tercihi tek tek elemek yerine, yapay zekâ kursun
|
||||||
</h2>
|
</h2>
|
||||||
<p className="relative mx-auto mt-3 max-w-xl text-white/90">
|
<p className="mt-3 max-w-xl text-base leading-relaxed text-slate-300">
|
||||||
Tablodan elle eklemek her zaman serbest. Ama üç adımda ilgi
|
Tablodan elle eklemek her zaman serbest. Ama üç adımda ilgi
|
||||||
alanlarını ve şehirlerini söylersen, yapay zekâ hayal, dengeli
|
alanlarını ve şehirlerini söylersen, yapay zekâ hayal, dengeli ve
|
||||||
ve güvenli dilimlerine dağıtılmış 24 tercihi — her satırı
|
güvenli dilimlerine dağıtılmış 24 tercihi — her satırı yerleşme
|
||||||
yerleşme riski ve gerekçesiyle — senin yerine dizer.
|
riski ve gerekçesiyle — senin yerine dizer.
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
size="lg"
|
size="lg"
|
||||||
onClick={sihirbaziBaslat}
|
onClick={sihirbaziBaslat}
|
||||||
className="relative mt-6 h-auto min-h-12 w-full cursor-pointer whitespace-normal bg-orange-500 px-4 py-3 text-white transition-colors duration-200 hover:bg-orange-600 sm:w-auto sm:px-8"
|
className="mt-6 h-auto min-h-12 w-full cursor-pointer whitespace-normal bg-orange-500 px-4 py-3 text-white transition-[background-color,transform] duration-200 hover:bg-orange-600 active:scale-[0.97] sm:w-auto sm:px-8"
|
||||||
>
|
>
|
||||||
Yapay Zeka listemi kur
|
Yapay Zeka listemi kur
|
||||||
<ArrowRight className="size-4" aria-hidden />
|
<ArrowRight className="size-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
</section>
|
</PikselCtaYuzeyi>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{monteEdildi ? (
|
{monteEdildi ? (
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { ArrowLeft, Trophy } from "lucide-react";
|
import { ArrowLeft, ArrowRight } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { PagePixelDivider } from "@/components/pixel-decor";
|
import { PagePixelDivider } from "@/components/pixel-decor";
|
||||||
import { profilCerezineYaz } from "@/features/sihirbaz/sihirbaz-profil";
|
import { profilCerezineYaz } from "@/features/sihirbaz/sihirbaz-profil";
|
||||||
@@ -34,19 +34,21 @@ export function SiraGerekli() {
|
|||||||
if (!profilYuklendi || profil) {
|
if (!profilYuklendi || profil) {
|
||||||
return (
|
return (
|
||||||
<main className="mx-auto flex max-w-2xl flex-col items-center px-4 py-24 text-center">
|
<main className="mx-auto flex max-w-2xl flex-col items-center px-4 py-24 text-center">
|
||||||
<p className="text-sm text-slate-500">Sıralaman kontrol ediliyor…</p>
|
<p role="status" className="text-base text-slate-500">
|
||||||
|
Sıralaman kontrol ediliyor…
|
||||||
|
</p>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="mx-auto flex max-w-2xl flex-col items-center px-4 py-24 text-center">
|
<main className="mx-auto flex max-w-2xl flex-col items-center px-4 py-24 text-center">
|
||||||
<h1 className="font-heading text-2xl font-bold">
|
<h1 className="font-heading text-3xl font-bold text-balance text-slate-900 sm:text-4xl">
|
||||||
Geçerli bir sıralama gerekli
|
Önce sıralamanı gir
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-3 text-slate-600">
|
<p className="mt-4 max-w-md text-base leading-relaxed text-slate-600 sm:text-lg">
|
||||||
Listeni kurabilmek için YKS başarı sıralamanı gir. Kaydettiğin sıra tüm
|
YKS başarı sıralamanı girince hangi programların sana açık olduğunu
|
||||||
uygulamada kullanılır.
|
hemen görürsün. Bir kez girmen yeter; her sayfada kullanılır.
|
||||||
</p>
|
</p>
|
||||||
<PagePixelDivider seed={59} className="mx-auto mt-6" />
|
<PagePixelDivider seed={59} className="mx-auto mt-6" />
|
||||||
<div className="mt-6 flex flex-wrap items-center justify-center gap-3">
|
<div className="mt-6 flex flex-wrap items-center justify-center gap-3">
|
||||||
@@ -56,12 +58,18 @@ export function SiraGerekli() {
|
|||||||
const { href } = sonucaGitIste();
|
const { href } = sonucaGitIste();
|
||||||
if (href) router.push(href);
|
if (href) router.push(href);
|
||||||
}}
|
}}
|
||||||
className="cursor-pointer bg-orange-500 text-white hover:bg-orange-600"
|
size="lg"
|
||||||
|
className="cursor-pointer bg-orange-500 text-white transition-[background-color,transform] duration-200 hover:bg-orange-600 active:scale-[0.97]"
|
||||||
>
|
>
|
||||||
<Trophy className="size-4" aria-hidden />
|
|
||||||
Sıralamamı gir
|
Sıralamamı gir
|
||||||
|
<ArrowRight className="size-4" aria-hidden />
|
||||||
</Button>
|
</Button>
|
||||||
<Button asChild variant="outline" className="cursor-pointer">
|
<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="/">
|
<Link href="/">
|
||||||
<ArrowLeft className="size-4" aria-hidden />
|
<ArrowLeft className="size-4" aria-hidden />
|
||||||
Ana sayfaya dön
|
Ana sayfaya dön
|
||||||
|
|||||||
36
src/lib/funnel-metinleri.ts
Normal file
36
src/lib/funnel-metinleri.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/**
|
||||||
|
* CTA yüzeylerinde yazılıp silinen kısa satırlar (arama penceresindeki
|
||||||
|
* kart ve PikselCtaYuzeyi). "use client" yok: sunucu da okuyabilsin (§9.3).
|
||||||
|
*/
|
||||||
|
export const FUNNEL_METINLERI: readonly string[] = [
|
||||||
|
"Sıralamana en uygun listeyi birlikte kuralım.",
|
||||||
|
"Geleceğin 24 satırlık bir plana sığar.",
|
||||||
|
"Gerçek YÖK Atlas verisiyle güvenle seç.",
|
||||||
|
"Her tercihinin bir nedeni olsun.",
|
||||||
|
"Riskleri biz hesaplayalım, kararı sen ver.",
|
||||||
|
"Sana uygun bölümler bir adım uzağında.",
|
||||||
|
"Dengeli bir liste, rahat bir tercih dönemi.",
|
||||||
|
"Tercihlerini şansa bırakma.",
|
||||||
|
"Doğru strateji, doğru yerleşme.",
|
||||||
|
"Hayalindeki bölüm için doğru plan.",
|
||||||
|
"Verilerle konuşan bir tercih listesi.",
|
||||||
|
"Üç adımda sana özel 24 tercih.",
|
||||||
|
"Tercih dönemini stressiz geçir.",
|
||||||
|
"Geçen yılın verisi, senin avantajın.",
|
||||||
|
"Güvenli ve iddialı tercihleri dengele.",
|
||||||
|
"Sıralaman ne söylüyor? Birlikte bakalım.",
|
||||||
|
"Her satırı senin için hesaplıyoruz.",
|
||||||
|
"Akıllı tercih, içi rahat bir bekleyiş.",
|
||||||
|
"Yerleşme riskini görerek seç.",
|
||||||
|
"Sana özel liste birkaç dakikada hazır.",
|
||||||
|
"İlgi alanların ve şehrin, tek listede.",
|
||||||
|
"Boş kalan tercih, kaçan fırsattır.",
|
||||||
|
"Uzman işi bir liste, senin onayınla.",
|
||||||
|
"Kararını veriye dayandır.",
|
||||||
|
"Doğru tercih, doğru bilgiyle başlar.",
|
||||||
|
"Listeni bugün kur, için rahat olsun.",
|
||||||
|
"Yerleşme riskin renklerle önünde.",
|
||||||
|
"En önemli 24 kararını birlikte verelim.",
|
||||||
|
"Senin sıralaman, sana özel plan.",
|
||||||
|
"İyi bir liste, iyi bir başlangıçtır.",
|
||||||
|
];
|
||||||
Reference in New Issue
Block a user