diff --git a/.claude/skills/better-ui/SKILL.md b/.claude/skills/better-ui/SKILL.md new file mode 100644 index 0000000..55d8fda --- /dev/null +++ b/.claude/skills/better-ui/SKILL.md @@ -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. diff --git a/docs/tasarim/2026-09-25-samimi-arayuz-arastirmasi.md b/docs/tasarim/2026-09-25-samimi-arayuz-arastirmasi.md new file mode 100644 index 0000000..fb6ca3b --- /dev/null +++ b/docs/tasarim/2026-09-25-samimi-arayuz-arastirmasi.md @@ -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 diff --git a/docs/tasarim/DESIGN.md b/docs/tasarim/DESIGN.md index 25187ed..3268a68 100644 --- a/docs/tasarim/DESIGN.md +++ b/docs/tasarim/DESIGN.md @@ -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). - 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 `` kullanır; elle yazılmaz. Nokta + metin: `` yanında `text-xs font-semibold text-emerald-600`; metin `RISK_ETIKET`'ten. Renk tek başına asla. ### 4.6 Form @@ -222,7 +223,7 @@ shadcn `Input`, `Select`, `Textarea`, `Slider`. Etiket her alanda; hata mesajı ### 4.7 İkonlar Yalnızca `lucide-react`. Boyut: `size-4` buton/satır içi (varsayılan), `size-5` ikon kutusu, `size-3.5` küçük çip, `size-6` üstü yalnızca boş durum. Dekoratif ikon `aria-hidden`; tek başına ikon buton `aria-label` ya da `sr-only` metin taşır. Emoji ikon olarak kullanılmaz. -### 4.8 Özet kartı — `OzetKart`, `universite-icerik.tsx` +### 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 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`. - `dl` anlamı için `dt` (başlık) DOM'da önce gelir, görsel sıra `order-*` ile kurulur. - Telefonda kart iç genişliği ~131 px: en uzun sayı ("13.889", 36 px'te ~90 px) + 32 px ikon - sığar. Daha uzun sayı gelecekse önce ölç (§9.2), sonra yaz. + 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 (`after:absolute after:inset-0` gerilmiş link), iç kaynak linki `relative z-10` ile üstte 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). - 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). +- **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 ç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. @@ -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 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`. `truncate` ile "İngiliz Dili ve E…" yazılmaz. - 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` içinde değer `font-semibold text-slate-900`. - Tür çipi: `rounded-full bg-slate-100 px-2.5 py-0.5 text-xs font-semibold text-slate-700`. -- Satır butonları varsayılan `size-11` (44 px); `size-8` gibi küçültme verilmez. +- 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`. ## 5. Yerleşim @@ -378,6 +410,22 @@ kartlarla aynı hizada: yorar), form alanları, uzun rehber metni. - 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 - İ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 @@ -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ı `pnpm tasarim:denetim` bu dilin eski izlerini **uyarı** olarak listeler (lint'i kırmaz, dönüşüm listesidir): `minik-yazi` (10–11 px), `buyuk-harf-etiket` (uppercase + tracking-wide), -`mavi-hover` (§2.1; liste satırı adı istisna), `iri-sayi-tabular` (Cal Sans + tabular-nums). +`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. +**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 `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. + +## 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. diff --git a/scripts/tasarim-denetim.ts b/scripts/tasarim-denetim.ts index 810ead0..4875bc7 100644 --- a/scripts/tasarim-denetim.ts +++ b/scripts/tasarim-denetim.ts @@ -191,7 +191,21 @@ const KURALLAR: Kural[] = [ ad: "mavi-hover", seviye: "uyari", mesaj: "Hover rengi tek turuncu (orange-500); liste satırındaki ad linkiyse istisna, mavi kalır (DESIGN.md §2.1)", - bul: regexKurali(/\b(?:group-)?hover:(?:text|border|shadow)-primary\b/), + // §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", @@ -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)", 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", seviye: "uyari", diff --git a/src/app/globals.css b/src/app/globals.css index 6cee40b..2ba1976 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -303,3 +303,21 @@ 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); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 8f6d541..dcc4338 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -82,6 +82,14 @@ export default function RootLayout({ className={`${calSans.variable} ${clarityCity.variable} ${geistMono.variable} ${bricolage.variable} antialiased`} > + {/* 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. */} + + İçeriğe geç + {/* Site geneli progressive blur — üst ve alt. Stil globals.css'te (.kt-sayfa-blur): iOS/iPadOS'ta fixed + backdrop-filter katmanı @@ -92,7 +100,11 @@ export default function RootLayout({ -
+
{children}
{/* Manuel 24'lük liste çekmecesi + "+" uçuş katmanı (navbar'dan açılır) */} diff --git a/src/app/page.tsx b/src/app/page.tsx index 90ed73b..0439e78 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -25,9 +25,9 @@ import { TurkeyPixelMap } from "@/features/pazarlama/components/turkey-pixel-map import { PixelCorner, PixelDivider, - PixelField, SectionEyebrow, } from "@/components/pixel-decor"; +import { PikselCtaYuzeyi } from "@/components/piksel-cta-yuzeyi"; import { MAX_REVIZYON, URUNLER } from "@/lib/credits"; import { VERI_YIL_SAYISI } from "@/lib/veri-kapsami"; import { @@ -282,7 +282,7 @@ export default function Home() { {problems.map((p, i) => ( - +
@@ -340,7 +340,7 @@ export default function Home() { {steps.map((s, i) => ( -
+
{s.step} @@ -372,9 +372,13 @@ export default function Home() { }, { value: "%100", label: "Resmî YÖK Atlas kaynağı" }, ].map((stat) => ( -
-
{stat.label}
-
+ // Sayı kahramandır (DESIGN.md §1.1): iri sayı üstte, etiket + // altında; dt anlam için DOM'da önce, görsel sıra order ile. +
+
+ {stat.label} +
+
{stat.value}
@@ -386,7 +390,7 @@ export default function Home() {
-

+

Dürüstlük notu

@@ -425,45 +429,93 @@ export default function Home() {

+ {/* 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. */} - - - - - Özellik - - Ücretsiz robotlar ve sohbet botları - - - İnsan danışman - - - KolayTercih - - - - + +
+
+ Robotlar ve botlar + İnsan danışman + + KolayTercih + +
+
    {comparison.map((row) => ( - - +
  • +

    {row.feature} - - - - - - - - - - - +

    +
    +
    +
    Robotlar ve botlar
    +
    + +
    +
    +
    +
    İnsan danışman
    +
    + +
    +
    +
    +
    KolayTercih
    +
    + +
    +
    +
    +
  • ))} - -
+ +
+ +
+ + + + + Özellik + + + Robotlar ve sohbet botları + + + İnsan danışman + + + KolayTercih + + + + + {comparison.map((row) => ( + + + {row.feature} + + + + + + + + + + + + ))} + +
+
@@ -629,28 +681,14 @@ export default function Home() {
-
- {/* Hero'nun piksel dokusu koyu zeminde: ortası boş, kenarlara doğru yoğun */} - {/* Piksel zemin panel içinde geride kalır; taşma payı için dikeyde büyütülür */} -
- - - -
-
- -

- Tercih listen, pişmanlık listesi olmasın -

- -
-
+ {/* Funnel CTA yüzeyi (DESIGN.md §10.8): köşede kümelenen + piksel zemini yerine 24'lük listenin interaktif kreatifi */} + +

+ Tercih listen, pişmanlık listesi olmasın +

+ +
diff --git a/src/app/rehber/[slug]/page.tsx b/src/app/rehber/[slug]/page.tsx index eb6ec53..f287e7c 100644 --- a/src/app/rehber/[slug]/page.tsx +++ b/src/app/rehber/[slug]/page.tsx @@ -29,13 +29,19 @@ export default function RehberYaziPage({
diff --git a/src/app/sonuc/page.tsx b/src/app/sonuc/page.tsx index 2cc4b0b..10a57a8 100644 --- a/src/app/sonuc/page.tsx +++ b/src/app/sonuc/page.tsx @@ -7,6 +7,7 @@ import { PROFIL_CEREZ_ADI, } from "@/features/sihirbaz/sihirbaz-sabitler"; import { + SonucBasligi, SonucProgramTablosu, SonucProgramTablosuSkeleton, } 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 kutunun yalnızca İÇİ dolar. Haritanın yerini bir daha aşağı almadan önce bu mekanizmayı kontrol et. */} - + {/* 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. */} + + + {/* Girişsiz tadımlık: gerçek rapor satırı formatında tek örnek. Havuzda içerik yoksa bileşen hiç render edilmez. Listesi diff --git a/src/components/arama-funnel-karti.tsx b/src/components/arama-funnel-karti.tsx index 9979195..2ca1dae 100644 --- a/src/components/arama-funnel-karti.tsx +++ b/src/components/arama-funnel-karti.tsx @@ -9,6 +9,7 @@ import { ListChecks, } from "lucide-react"; import { Button } from "@/components/ui/button"; +import { FUNNEL_METINLERI } from "@/lib/funnel-metinleri"; import { TypingAnimation } from "@/components/ui/typing-animation"; import { SIHIRBAZ_AC_EVENT, sonucHref } from "@/features/sihirbaz/sihirbaz-sabitler"; import { @@ -16,39 +17,6 @@ import { useTercihProfili, } 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. */ function karistir(dizi: readonly T[]): T[] { const kopya = [...dizi]; @@ -102,7 +70,7 @@ export function AramaFunnelKarti({ onSec }: { onSec: () => void }) { return (
diff --git a/src/components/liste-satiri.ts b/src/components/liste-satiri.ts new file mode 100644 index 0000000..3452bb8 --- /dev/null +++ b/src/components/liste-satiri.ts @@ -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"; diff --git a/src/components/ozet-kart.tsx b/src/components/ozet-kart.tsx new file mode 100644 index 0000000..4a3ec4d --- /dev/null +++ b/src/components/ozet-kart.tsx @@ -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 `
` 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 ( +
+
+ {etiket} +
+
+ + + {deger} + + {birim ? ( + + {birim} + + ) : null} + + + + +
+
{alt}
+ {ek ?
{ek}
: null} +
+ ); +} diff --git a/src/components/piksel-cta-yuzeyi.tsx b/src/components/piksel-cta-yuzeyi.tsx new file mode 100644 index 0000000..c9d75ad --- /dev/null +++ b/src/components/piksel-cta-yuzeyi.tsx @@ -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(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 ( +
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, + )} + > +
+
+ {children} + {kayanMetin ? ( + + ) : null} +
+
+ + +
+
+
+ ); +} diff --git a/src/components/piksel-liste.tsx b/src/components/piksel-liste.tsx new file mode 100644 index 0000000..33ab0a9 --- /dev/null +++ b/src/components/piksel-liste.tsx @@ -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 = { + 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(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) => { + 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 ( + + ); +} + +/** 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 => ( + + + {sayi} {etiket.toLocaleLowerCase("tr-TR")} + + ); + return ( +

+ {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)} +

+ ); +} diff --git a/src/components/risk-etiketi.tsx b/src/components/risk-etiketi.tsx new file mode 100644 index 0000000..0398422 --- /dev/null +++ b/src/components/risk-etiketi.tsx @@ -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 = { + guvenli: "bg-emerald-500", + "az-riskli": "bg-amber-500", + riskli: "bg-red-500", +}; + +const RISK_METIN: Record = { + guvenli: "text-emerald-600", + "az-riskli": "text-amber-600", + riskli: "text-red-600", +}; + +export function RiskEtiketi({ risk }: { risk: RiskSeviyesi }) { + return ( + + + {RISK_ETIKET[risk]} + + ); +} diff --git a/src/components/site-footer.tsx b/src/components/site-footer.tsx index 6e85ab0..81b4264 100644 --- a/src/components/site-footer.tsx +++ b/src/components/site-footer.tsx @@ -18,7 +18,7 @@ export function SiteFooter() {
- {/* 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 + (logosu yok — DESIGN.md §4.14). Geniş ekran (lg) altında başlık + bloğu ortalı (§5.2). */} +

+ className="mt-6 text-center font-heading text-3xl font-bold leading-tight text-slate-900 sm:text-5xl lg:text-left" + > {bolum.ad} {ilkSayfa ? "" : ` — Sayfa ${sayfa}`} {!ilkSayfa && aralik ? ( <> {" "} - + {aralik.charAt(0).toLocaleUpperCase("tr-TR") + aralik.slice(1)} ) : null}

+ + {duzeyler.length > 0 || puanTurleri.length > 0 ? ( +

+ {duzeyler.map((d) => ( + + + {d} + + ))} + {puanTurleri.map((t) => ( + + + {t} + + ))} +

+ ) : null} + {ilkSayfa ? ( -

- {sayi(tumProgramlar.length)} üniversite programının {SON_YIL} taban - puanlarını, başarı sıralamalarını ve kontenjanlarını incele. +

+ {bolum.ad} bölümünü açan {sayi(tumProgramlar.length)} üniversite + programının {SON_YIL} taban puanlarını, başarı sıralamalarını ve + kontenjanlarını incele.{" "} + + {TERCIH_YILI} tabanları, yerleştirme sonuçları açıklandığında + netleşir. +

) : ( -

+

{bolum.ad} taban puanları tablosunun {sayfa}. sayfası ( {sayi(tumProgramlar.length)} programın{" "} {sayi((sayfa - 1) * SAYFA_BOYU + 1)}– @@ -257,54 +343,99 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) { {aralik ? `: ${aralik}` : ""}. Listenin başı{" "} ilk sayfada .

)} -
- {/* Tek sayfada çıkan ziyaretçi alttaki karta inmiyor: kompakt sıra girişi - başlık bloğunun hemen altında, listenin üstünde. Alttaki kart kalır. */} -
- -
- -
-
-

- Üniversite programları -

- - {sayi(tumProgramlar.length)} program - + {/* Tek sayfada çıkan ziyaretçi alttaki karta inmiyor: kompakt sıra + girişi özet paragrafın hemen altında, listenin üstünde. Alttaki + kart kalır. */} +
+
- ({ - ...program, - universiteSlug: uniSlugFromAd(program.universite), - }))} - bazAd={bolum.ad} - tabanYol={`/bolum/${slug}`} - sayfa={sayfa} - /> - -
+ {/* Sayfada en çok dört özet sayı (DESIGN.md §1.1, §4.8). Derin + sayfalarda tekrar edilmez — üniversite sayfasıyla aynı. */} + {ilkSayfa ? ( +
+ + 0 + ? `${sayi(devletUni.size)} üniversitede` + : "bu bölümde yok" + } + /> + 0 + ? `${sayi(vakifUni.size)} üniversitede` + : "bu bölümde yok" + } + /> + +
+ ) : null} - {/* Üniversite sayfasıyla aynı konum ve aynı kart: liste + sayfalama altı */} -
- -
+ + +
+
+

+ Üniversite programları +

+

+ {sayi(tumProgramlar.length)} program, taban sırasına göre +

+
+ +
+ ({ + ...program, + universiteSlug: uniSlugFromAd(program.universite), + }))} + bazAd={bolum.ad} + tabanYol={`/bolum/${slug}`} + sayfa={sayfa} + /> +
+ +
+ + {/* Üniversite sayfasıyla aynı konum ve aynı kart: liste + sayfalama altı */} +
+ +
@@ -313,16 +444,20 @@ export function BolumIcerik({ slug, sayfa }: { slug: string; sayfa: number }) { export function BolumIcerikSkeleton() { return ( -
- -
- - +
+ + + + +
+ {Array.from({ length: 4 }, (_, i) => ( + + ))}
-
+
{Array.from({ length: 5 }, (_, i) => ( - + ))}
diff --git a/src/features/katalog/components/bolum-listesi.tsx b/src/features/katalog/components/bolum-listesi.tsx index bb6af6f..3096042 100644 --- a/src/features/katalog/components/bolum-listesi.tsx +++ b/src/features/katalog/components/bolum-listesi.tsx @@ -15,7 +15,7 @@ function BolumLinkleri({ bolumler }: { bolumler: BolumOzet[] }) {
  • {b.ad} diff --git a/src/features/katalog/components/bolum-program-listesi.tsx b/src/features/katalog/components/bolum-program-listesi.tsx index 76a0d0c..0321d80 100644 --- a/src/features/katalog/components/bolum-program-listesi.tsx +++ b/src/features/katalog/components/bolum-program-listesi.tsx @@ -12,6 +12,20 @@ import { ProgramSiraGecmisi, ProgramTabanTrendi, } 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 { ProgramNetleriButonu } from "./program-netleri"; import { UniLogo } from "@/components/uni-logo"; @@ -58,7 +72,7 @@ function SiralamaDugmesi({ aktif && yon === "artan" ? "azalan" : "artan" } sırala`} 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" > {etiket} @@ -66,7 +80,7 @@ function SiralamaDugmesi({ className={`size-3 ${ aktif && yon === "artan" ? "text-primary" - : "text-muted-foreground/40" + : "text-slate-300" }`} strokeWidth={aktif && yon === "artan" ? 3 : 2} /> @@ -74,7 +88,7 @@ function SiralamaDugmesi({ className={`size-3 ${ aktif && yon === "azalan" ? "text-primary" - : "text-muted-foreground/40" + : "text-slate-300" }`} strokeWidth={aktif && yon === "azalan" ? 3 : 2} /> @@ -154,14 +168,21 @@ export function BolumProgramListesi({ ref={listeRef} 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. */} - - Üniversite - + + + Üniversite + + Son {VERI_YIL_SAYISI} yıl - + - + - + - + Netler ve listeye ekle @@ -194,84 +215,87 @@ export function BolumProgramListesi({ {gosterilenProgramlar.map((p) => { const uniAd = trBaslikDuzeni(p.universite); const varyant = p.isim.trim() !== bazAd ? p.isim.trim() : null; - const devlet = p.unitur === "DEVLET"; + const tabanMetni = + p.siraSon != null ? `~${sayi(p.siraSon)}.` : "—"; return ( - - + + {p.universiteSlug ? ( ) : null} - {p.universiteSlug ? ( - - {uniAd} - - ) : ( - - {uniAd} - - )} - - {p.unitur ? ( - + {p.universiteSlug ? ( + - {UNITUR_ETIKET[p.unitur]?.cip ?? - trBaslikDuzeni(p.unitur)} + {uniAd} + + ) : ( + + {uniAd} - ) : null} - - {p.il ? trBaslikDuzeni(p.il) : null} - {varyant ? `${p.il ? " · " : ""}${varyant}` : ""} + )} + + {p.unitur ? ( + + {UNITUR_ETIKET[p.unitur]?.cip ?? + trBaslikDuzeni(p.unitur)} + + ) : null} + {p.il ? ( + + {trBaslikDuzeni(p.il)} + + ) : null} + {p.sure ? ( + {p.sure} yıl + ) : null} - {p.sure ? ( - - · {p.sure} yıl - - ) : null} + + {varyant ? ( + + {varyant} + + ) : null} + + Taban sıra + + {tabanMetni} + + - + - + {p.puanSon != null ? p.puanSon.toFixed(2).replace(".", ",") : "—"} - + {p.kontenjanSon != null ? `${sayi(p.kontenjanSon)} kişi` : "—"} - - + + - {p.siraSon != null ? `~${sayi(p.siraSon)}.` : "—"} + {tabanMetni} - - + + - + diff --git a/src/features/katalog/components/katalog-arama.tsx b/src/features/katalog/components/katalog-arama.tsx index e2a7ab1..09cf914 100644 --- a/src/features/katalog/components/katalog-arama.tsx +++ b/src/features/katalog/components/katalog-arama.tsx @@ -355,8 +355,8 @@ export function KatalogArama({ rehberler }: { rehberler: RehberAramaKaydi[] }) { · {PUAN_TURU_ETIKET[profil.tur] ?? profil.tur} - - + + Değiştir @@ -564,7 +564,7 @@ export function KatalogArama({ rehberler }: { rehberler: RehberAramaKaydi[] }) { function BaslangicIcerigi({ onSec }: { onSec: () => void }) { return (
    -

    +

    Keşfet

    @@ -677,11 +677,11 @@ function KesfetLinki({
    - + {kisaBaslik} {baslik} - + {aciklama} @@ -700,7 +700,7 @@ function SonucGrubu({ }) { return (
    -

    +

    {ikon} {baslik}

    diff --git a/src/features/katalog/components/program-netleri.tsx b/src/features/katalog/components/program-netleri.tsx index 02570ad..e013841 100644 --- a/src/features/katalog/components/program-netleri.tsx +++ b/src/features/katalog/components/program-netleri.tsx @@ -6,6 +6,7 @@ // yok: YÖK Atlas 2026 SPA'sına geçerken cinsiyet panelini yayından kaldırdı // (bkz. scripts/detay.ts). +import { SATIR_DUGME_BOYU } from "@/components/liste-satiri"; import { useState } from "react"; import { ListChecks } from "lucide-react"; import type { ProgramNetSatiri } from "@/types/yokatlas"; @@ -96,7 +97,7 @@ export function ProgramNetleriButonu({ type="button" title="Son yerleşenin netleri" 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}`} > {isim} — son yerleşenin netleri diff --git a/src/features/katalog/components/sayfalama.tsx b/src/features/katalog/components/sayfalama.tsx index bf2c546..8b2a033 100644 --- a/src/features/katalog/components/sayfalama.tsx +++ b/src/features/katalog/components/sayfalama.tsx @@ -95,7 +95,9 @@ export function Sayfalama({ ); - // 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 | "...")[] = []; if (toplamSayfa <= 7) { for (let n = 1; n <= toplamSayfa; n++) numaralar.push(n); @@ -116,12 +118,12 @@ export function Sayfalama({ return (
    - + Program - + Son {VERI_YIL_SAYISI} yıl - + {SON_YIL} puan - + Kontenjan - + Taban sıra - + Netler ve listeye ekle @@ -76,28 +89,30 @@ export function UniversiteProgramTablosu({ const tabanMetni = taban != null ? `~${sayi(taban)}.` : "—"; return ( - - {p.bolumSlug ? ( - - {p.isim.trim()} - - ) : ( - - {p.isim.trim()} + + + {p.bolumSlug ? ( + + {p.isim.trim()} + + ) : ( + + {p.isim.trim()} + + )} + + + {TUR_ETIKET[p.tur] ?? p.tur} + + {p.sure ? ( + {p.sure} yıl + ) : null} - )} - - - {TUR_ETIKET[p.tur] ?? p.tur} - - {p.sure ? ( - {p.sure} yıl - ) : null} - + Taban sıra {tabanMetni} @@ -124,8 +139,8 @@ export function UniversiteProgramTablosu({ {tabanMetni} - - + +
    {/* Kullanıcının o anki işletim sisteminin logosu: "bu tarayıcıda" mesajını somutlaştırır */}
    -
    -

    +

    +

    Listen yalnızca bu tarayıcıda

    -

    +

    Giriş yap; {liste.length} seçimin hesabında saklansın, her cihazdan aç{" "} @@ -77,8 +79,7 @@ export function KaydetBanner() {

    diff --git a/src/features/liste/components/liste-cekmecesi.tsx b/src/features/liste/components/liste-cekmecesi.tsx index adc8e96..c2d6ea5 100644 --- a/src/features/liste/components/liste-cekmecesi.tsx +++ b/src/features/liste/components/liste-cekmecesi.tsx @@ -12,10 +12,12 @@ // sadeleşir; X butonu her durumda kalır. import { useState } from "react"; +import { toast } from "sonner"; import Link from "next/link"; import { useRouter } from "next/navigation"; -import { PixelField } from "@/components/pixel-decor"; +import { PikselListe } from "@/components/piksel-liste"; import { UniLogo } from "@/components/uni-logo"; +import { RiskEtiketi } from "@/components/risk-etiketi"; import { Button } from "@/components/ui/button"; import { Drawer, @@ -27,6 +29,8 @@ import { } from "@/components/ui/drawer"; import { ArrowRight, + Check, + CircleMinus, GripVertical, ListChecks, Plus, @@ -42,8 +46,8 @@ import { useTransform, type PanInfo, } from "motion/react"; -import { RISK_ETIKET, type RiskSeviyesi } from "@/lib/risk"; -import { bolumBazAdi, turkishSlugify } from "@/lib/slug"; +import { bolumBazAdi, trBaslikDuzeni, turkishSlugify } from "@/lib/slug"; +import { SATIR_AD, SATIR_IKINCIL_LINK } from "@/components/liste-satiri"; import { olay } from "@/lib/analitik"; import { CihazCifti, IsletimSistemiLogosu } from "./cihaz-ikonlari"; import { @@ -58,6 +62,7 @@ import { cekmeceAc, cekmeceKapat, cikar, + listeOku, temizle, useCekmeceAcik, useManuelListe, @@ -77,12 +82,35 @@ function uniHref(ad: string): string { 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 = { - guvenli: "bg-emerald-500", - "az-riskli": "bg-amber-500", - riskli: "bg-red-500", -}; +/** + * Geri alınabilir silme (Vercel arayüz ilkeleri: yıkıcı işlem ya onay ya + * geri al penceresi ister; Apple: beklenen işlemde uyarı penceresi açma). + * Çıkan tercihler eski sıralarına geri konur; arada eklenenler kaybolmaz. + */ +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() { const router = useRouter(); @@ -94,7 +122,13 @@ export function ListeCekmecesi() { // drawer içinde erken ölçüm satırları kaydırıyordu 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 // 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. @@ -123,71 +157,99 @@ export function ListeCekmecesi() { - -
    - - - Kendi Listem - - {liste.length}/{MANUEL_LISTE_MAX} - - -
    - {liste.length > 0 ? ( - + {/* Alt sayfa başlığı (DESIGN.md §4.10): ortalı başlık + bağlam; + çarpı §4.3 kanonik sınıf setinde, temizle düğmesi aynı ölçüde + solda. */} + + {liste.length > 0 ? ( + + ) : null} + + + Kapat + + + Kendi Listem + + {/* 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. */} +
    +
    + + t.risk === "riskli" + ? "hayal" + : t.risk === "az-riskli" + ? "dengeli" + : t.risk === "guvenli" + ? "guvenli" + : "belirsiz", + )} + sutun={12} + /> +
    +
    +

    + {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. + + + Listen tamam · {liste.length} / {MANUEL_LISTE_MAX} + + ) : ( + <> + {liste.length} / {MANUEL_LISTE_MAX} tercih + + )} +

    + {riskSayilari.length > 0 ? ( +

    + {riskSayilari.map(([seviye, adet]) => ( + + + + {adet} + + + ))} +

    ) : null} - - - Kapat -
    -
    - -
    -
      - {( - [ - ["guvenli", "yeşil güvenli"], - ["az-riskli", "turuncu az riskli"], - ["riskli", "kırmızı riskli"], - ] as const - ).map(([seviye, aciklama]) => ( -
    • - - {aciklama} -
    • - ))} -
    + {/* Denge ipucu: 24'lük iskeletin mantığı (alt sıralarda güvenli + programlar). Yalnız riskleri bilinen ve en az 6 tercihli + listede; vaat değil, gerekçe. */} + {guvenliYok ? ( +

    + Güvenli yok; alta birkaç tane eklemek riski azaltır. +

    + ) : null}
    {liste.length === 0 ? (
    @@ -196,10 +258,10 @@ export function ListeCekmecesi() { strokeWidth={1.25} aria-hidden /> -

    +

    Henüz program seçmedin

    -

    +

    Sonuç sayfasından program ekleyerek listeni doldurmaya başla.

    @@ -228,29 +290,32 @@ export function ListeCekmecesi() { ) : null}
    - + {/* 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. */} {girisli === false ? ( -
    -

    +

    +

    {/* O anki işletim sisteminin logosu — "bu tarayıcıda"yı somutlaştırır */} - + Listen yalnızca bu tarayıcıda. - {" "} - Giriş yap; her cihazdan eriş{" "} - + + + {" "} + Giriş yap; her cihazdan eriş{" "} + +

    Giriş yap, kaydet - +
    ) : null} {/* Ana sayfadaki final CTA panelinin kompakt hâli: koyu primary zemin + kenarlara doğru yoğunlaşan piksel dokusu (PixelField) */} -
    -
    - -
    -
    -

    + {/* Telefonda alt alta (başlık tek satır, buton tam genişlik): + yan yana dizince başlık üç satıra kırılıp listenin yerini + yiyordu (24 Eyl 2026, 375 px ölçümü). */} + {/* Funnel yüzeyi (DESIGN.md §10.8): arama penceresindeki kartın + koyu zemini; listenin piksel hâli başlıkta. */} +

    +
    +

    {riskEksik ? "Risk analizi sıralamanı bekliyor" : "Risk analizini biz yaptık"}

    -

    +

    {riskEksik ? "Riskleri gösterelim; istersen 24'lük listeni de kur (3 kredi)." : "Her seçimin yerleşme riski hazır — son kararı sen ver."} @@ -287,7 +351,7 @@ export function ListeCekmecesi() { {riskEksik ? ( - + {sira} - 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} - + + e.stopPropagation()} // kaydırmayı tetiklemesin title={`${tercih.universite} taban puanları sayfası`} - className="hover:text-primary hover:underline" + className={`min-w-0 ${SATIR_IKINCIL_LINK}`} > - {tercih.universite} + {trBaslikDuzeni(tercih.universite)} - {tercih.il ? ` · ${tercih.il.toLocaleLowerCase("tr-TR")}` : ""} + {tercih.risk || tercih.efektifSira != null ? ( + + {tercih.risk ? : null} + {tercih.efektifSira != null ? ( + + Taban sıra + + ~{tercih.efektifSira.toLocaleString("tr-TR")}. + + + ) : null} + + ) : null} - {tercih.efektifSira != null ? ( - - ~{tercih.efektifSira.toLocaleString("tr-TR")}. - - ) : null} - {tercih.risk ? ( - - - {RISK_ETIKET[tercih.risk]} - - ) : null} diff --git a/src/features/liste/components/liste-durumu-satiri.tsx b/src/features/liste/components/liste-durumu-satiri.tsx new file mode 100644 index 0000000..51c71b4 --- /dev/null +++ b/src/features/liste/components/liste-durumu-satiri.tsx @@ -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

    ; + + if (liste.length === 0) { + return ( +

    + + + Artı düğmesi + + ile beğendiğin programı listene ekle; sırasını sonra sen belirlersin. +

    + ); + } + + const kalan = MANUEL_LISTE_MAX - liste.length; + return ( +
    + +
    + ); +} diff --git a/src/features/liste/components/listem-butonu.tsx b/src/features/liste/components/listem-butonu.tsx index e11c60a..ebf079c 100644 --- a/src/features/liste/components/listem-butonu.tsx +++ b/src/features/liste/components/listem-butonu.tsx @@ -46,7 +46,7 @@ export function ListemButonu() { initial={azMotion ? false : { scale: 0.6 }} animate={{ scale: 1 }} 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} diff --git a/src/features/liste/components/manuel-harita.tsx b/src/features/liste/components/manuel-harita.tsx index 007a867..8b2f810 100644 --- a/src/features/liste/components/manuel-harita.tsx +++ b/src/features/liste/components/manuel-harita.tsx @@ -20,7 +20,13 @@ const RISK_AGIRLIK: Record = { riskli: 2, }; -export function ManuelHarita({ yerAyir = false }: { yerAyir?: boolean }) { +export function ManuelHarita({ + yerAyir = false, + className, +}: { + yerAyir?: boolean; + className?: string; +}) { const liste = useManuelListe(); const hidre = useListeHidreMi(); const [seciliIl, setSeciliIl] = useState(null); @@ -85,10 +91,10 @@ export function ManuelHarita({ yerAyir = false }: { yerAyir?: boolean }) { // durumunda boş harita ekranda kalmaz. if (liste.length === 0 && (hidre || !yerAyir)) return null; - // Margin yok: harita /sonuc'ta main'in İLK çocuğudur, üst boşluğunu - // main'in py-12'sinden alır (bkz. sonuc/page.tsx). + // Üst boşluk çağırandan gelir: /sonuc'ta harita sonuç başlığının altında + // durur (bkz. sonuc/page.tsx). Harita çizilmezse boşluk da oluşmaz. return ( -
    +
    {listede ? ( diff --git a/src/features/liste/components/program-tablosu.tsx b/src/features/liste/components/program-tablosu.tsx index 8a6c5ea..5041ac9 100644 --- a/src/features/liste/components/program-tablosu.tsx +++ b/src/features/liste/components/program-tablosu.tsx @@ -20,13 +20,8 @@ import type { PuanTuruKey, RankResultsListe, } from "@/types/yokatlas"; -import { - DILIM_ETIKET, - RISK_ETIKET, - riskHesapla, - type RiskSeviyesi, -} from "@/lib/risk"; -import { bolumSayfaSlug, uniSayfaSlug } from "@/lib/slug"; +import { DILIM_ETIKET, riskHesapla } from "@/lib/risk"; +import { bolumSayfaSlug, trBaslikDuzeni, uniSayfaSlug } from "@/lib/slug"; import { VERI_YIL_SAYISI } from "@/lib/veri-yillari"; import { ProgramSiraGecmisi, @@ -46,6 +41,22 @@ import { ProgramEkleButonu } from "@/features/liste/components/program-ekle-buto import { preloadTercihHaritasi } from "@/features/liste/components/tercih-haritasi-lazy"; import { ProgramNetleriButonu } from "@/features/katalog/components/program-netleri"; 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: { 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 = { - guvenli: "bg-emerald-500", - "az-riskli": "bg-amber-500", - riskli: "bg-red-500", -}; - const BOS_EKSTRA: Record = { hayal: [], dengeli: [], @@ -195,14 +197,17 @@ export function ProgramTablosu({ navbar'daki sıralama rozetinden değiştirilir. Aktif hap, nav'daki "Giriş yap" butonuyla aynı dilde (koyu, rounded-full) ve layoutId ile sekmeler arasında kayarak taşınır. */} - + {DILIMLER.map((d) => { const aktif = aktifDilim === d.key; return ( {aktif ? ( ) : null} - - - {d.etiket} - + + + + {d.etiket} + + {sonuclar.toplam[d.key].toLocaleString("tr-TR")} @@ -236,7 +245,7 @@ export function ProgramTablosu({ const toplam = sonuclar.toplam[d.key]; return ( -
    +
    {satirlar.length === 0 ? (

    Bu dilimde filtrene uyan program bulunamadı. @@ -244,17 +253,31 @@ export function ProgramTablosu({ ) : (

    - - Risk - Program - + + + Risk + + + Program + + Son {VERI_YIL_SAYISI} yıl - + Kontenjan - Taban sıra - + + Taban sıra + + Netler ve listeye ekle @@ -273,17 +296,19 @@ export function ProgramTablosu({ )} {satirlar.length < toplam ? ( -
    - - + + + {satirlar.length.toLocaleString("tr-TR")} /{" "} {toplam.toLocaleString("tr-TR")} program @@ -309,65 +334,76 @@ function ProgramSatiri({ const taban = programEtkinSira(p); const risk = riskHesapla(taban, adaySira); 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 ( - - {risk ? ( - - - {RISK_ETIKET[risk]} - - ) : null} + + {risk ? : null} - - + + e.stopPropagation()} > {p.isim} - - - {devlet ? "Devlet" : "Vakıf"} - - + + e.stopPropagation()} > - {p.universite} + {trBaslikDuzeni(p.universite)} - {p.il ? ` · ${p.il.toLocaleLowerCase("tr-TR")}` : ""} - {p.sure ? ( - · {p.sure} yıl - ) : null} + + {devlet ? "Devlet" : "Vakıf"} + {p.il ? ( + {trBaslikDuzeni(p.il)} + ) : null} + {p.sure ? ( + {p.sure} yıl + ) : null} + {bosKaldi ? ( + + Boş kaldı + + ) : null} + + + + {risk ? : null} + + Taban sıra + + {tabanMetni} + + + - + {p.kontenjanSon != null ? ( - - {p.yerlesenSon != null && p.yerlesenSon < p.kontenjanSon ? ( - - boş kaldı + + {p.kontenjanSon.toLocaleString("tr-TR")} kişi + {bosKaldi ? ( + + Boş kaldı ) : null} - {p.kontenjanSon.toLocaleString("tr-TR")} kişi ) : ( "—" )} - - + + - {taban != null ? `~${taban.toLocaleString("tr-TR")}.` : "—"} + {tabanMetni} - - + + diff --git a/src/features/liste/components/sonuc-program-tablosu.tsx b/src/features/liste/components/sonuc-program-tablosu.tsx index b34ca89..e9a5dd4 100644 --- a/src/features/liste/components/sonuc-program-tablosu.tsx +++ b/src/features/liste/components/sonuc-program-tablosu.tsx @@ -1,8 +1,69 @@ +import { cache } from "react"; import { siraylaProgramAra } from "../liste-queries"; import { ProgramTablosu } from "./program-tablosu"; +import { ListeDurumuSatiri } from "./liste-durumu-satiri"; 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"; +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 ( +
    +

    + Sıralamanla açılan programlar +

    +

    + {sayi(sira)}.{" "} + sıradasın ({PUAN_TURU_ETIKET[tur] ?? tur}).{" "} + {filtre.length > 0 ? `Seçimlerine göre (${filtre.join(", ")}) ` : ""} + + {sayi(acik)} program + {" "} + sana açık; aşağıda riskine göre ayrıldı. +

    + +
    + ); +} + // /sonuc'un ham tablosunun sunucu sarmalayıcısı: okuma burada yaşar, sayfa // yalnızca kompoze eder. export function SonucProgramTablosu({ @@ -16,7 +77,7 @@ export function SonucProgramTablosu({ iller?: string[]; uniturGrubu?: UniturGrubu; }) { - const sonuclar = siraylaProgramAra(sira, tur, { iller, uniturGrubu }); + const sonuclar = sonuclariOku(sira, tur, (iller ?? []).join("|"), uniturGrubu); return ( 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]" > {sira} @@ -543,7 +543,7 @@ export function TercihHaritasi({ {bolum || kisaUniAdi(pin.universite)} {bolum ? ( - + {kisaUniAdi(pin.universite)} ) : null} diff --git a/src/features/liste/components/tercih-profili-kapisi.tsx b/src/features/liste/components/tercih-profili-kapisi.tsx index 7ab9ce9..1c1d891 100644 --- a/src/features/liste/components/tercih-profili-kapisi.tsx +++ b/src/features/liste/components/tercih-profili-kapisi.tsx @@ -7,6 +7,7 @@ import { useReducedMotion } from "motion/react"; import useMeasure from "react-use-measure"; import { toast } from "sonner"; import { TransitionPanel } from "@/components/core/transition-panel"; +import { SectionEyebrow } from "@/components/pixel-decor"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { @@ -295,11 +296,9 @@ function KapıIcerik({
    {duzenle ? ( -

    - Tercihlerini güncelle -

    + Tercihlerini güncelle ) : null} -

    +

    {duzenle ? "Seçimlerini değiştir" : "Tercihine ışık tutabilmemiz için önce ne istediğini öğrenelim"} @@ -348,11 +347,9 @@ function KapıIcerik({ >
    {duzenle ? ( -

    - Sıralama ve tercihler -

    + Sıralama ve tercihler ) : null} -

    +

    {duzenle ? "Yeni sıralaman nedir?" : "Başarı sıralaman nedir?"} @@ -366,7 +363,7 @@ function KapıIcerik({
    diff --git a/src/features/pazarlama/components/landing-parcalari.tsx b/src/features/pazarlama/components/landing-parcalari.tsx index f49b28e..66e7412 100644 --- a/src/features/pazarlama/components/landing-parcalari.tsx +++ b/src/features/pazarlama/components/landing-parcalari.tsx @@ -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, // 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") { - return {value}; + return ( + + {value} + + ); } return value ? ( - + ) : ( - + ); } diff --git a/src/features/pazarlama/components/sosyal-kanit-govde.tsx b/src/features/pazarlama/components/sosyal-kanit-govde.tsx index 5739a0d..d42f2f6 100644 --- a/src/features/pazarlama/components/sosyal-kanit-govde.tsx +++ b/src/features/pazarlama/components/sosyal-kanit-govde.tsx @@ -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" >

    - + {sayi(gosterilenListe)} diff --git a/src/features/rapor/components/liste-uretici.tsx b/src/features/rapor/components/liste-uretici.tsx index 39f6bef..8f62125 100644 --- a/src/features/rapor/components/liste-uretici.tsx +++ b/src/features/rapor/components/liste-uretici.tsx @@ -304,8 +304,8 @@ export function ListeUretici({ onClick={() => profil && void uret(profil)} className="mt-6 cursor-pointer bg-orange-500 text-white transition-colors duration-200 hover:bg-orange-600" > - Tekrar dene + {profil ? (

    @@ -358,7 +358,7 @@ function SunucuYokEkrani() { disabled={deniyor} 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ç"}

    @@ -401,7 +401,7 @@ function UretimGovdesi({ uzunSurdu }: { uzunSurdu: boolean }) { return (

    -

    +

    Listen hazırlanıyor

    {uzunSurdu ? ( @@ -426,15 +426,15 @@ function UretimGovdesi({ uzunSurdu }: { uzunSurdu: boolean }) { ) : ( -

    - Bu işlem yarım dakika kadar sürebilir, sayfayı kapatma. +

    + Yarım dakika kadar sürebilir; bu sayfayı kapatma.

    )}
    {/* Nihai sayfanın (listem-govde) grid'iyle aynı düzen: liste solda, danışman sağda — üretim biter bitmez ekran aynı yerlere oturur. */} -
    +
    {/* Gelmekte olan tercih listesinin iskeleti */}
    diff --git a/src/features/rapor/components/listem-govde.tsx b/src/features/rapor/components/listem-govde.tsx index 28feb08..f2a6076 100644 --- a/src/features/rapor/components/listem-govde.tsx +++ b/src/features/rapor/components/listem-govde.tsx @@ -39,6 +39,11 @@ import { } from "@/features/liste/hooks/use-tercih-profili"; import { UNIVERSITE_TIPI_ETIKET } from "@/features/sihirbaz/sihirbaz-sabitler"; 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({ rapor, @@ -94,6 +99,10 @@ export function ListemGovde({ ); 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 // SSR ile aynı "Tercihlerimi düzenle" hâli basılır — hidrasyon uyuşmazlığı @@ -148,7 +157,7 @@ export function ListemGovde({ ); return ( -
    +
    {/* Harita: listenin coğrafyası — ile dokun, üniversiteleri gör */}
    -

    - {hasPaket - ? "Yapay Zeka tercih listen" - : "Veriyi ücretsiz incele. Kararı Yapay Zeka ile netleştir."} -

    + {/* Sayfa nerede olduğunu söyler (DESIGN.md §10.1): başlık yer, + altındaki satır listenin sayılarla özeti. */} +
    +

    + Tercih listen +

    +

    + {dilimOzeti} + {hasPaket ? null : ( + + Veriyi ücretsiz incele, kararı Yapay Zeka ile netleştir. + + )} +

    +
    {/* Tercih düzenleme her kullanıcıya açık: kapı (layout'ta monte TercihProfiliKapisi) profili günceller, /listem'e döner. @@ -200,9 +219,8 @@ export function ListemGovde({ {farkVar ? ( -
    +
    iyzico güvenli ödeme diff --git a/src/features/rapor/components/listem-icerik.tsx b/src/features/rapor/components/listem-icerik.tsx index f593730..f0b0a41 100644 --- a/src/features/rapor/components/listem-icerik.tsx +++ b/src/features/rapor/components/listem-icerik.tsx @@ -189,7 +189,7 @@ export function ListemIcerikSkeleton() { -
    +
    {Array.from({ length: 10 }).map((_, i) => ( diff --git a/src/features/rapor/components/rapor-listesi.tsx b/src/features/rapor/components/rapor-listesi.tsx index 16db174..fbd4a41 100644 --- a/src/features/rapor/components/rapor-listesi.tsx +++ b/src/features/rapor/components/rapor-listesi.tsx @@ -26,12 +26,28 @@ import { TableHeader, TableRow, } 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 type { RaporSonuc } from "@/features/rapor/types/rapor"; import { DILIM_ETIKET, - RISK_ETIKET, dilimdenFarkliRiskEtiketi, dilimdenRisk, riskHesapla, @@ -39,13 +55,11 @@ import { } from "@/lib/risk"; import type { Dilim } from "@/lib/rapor-havuzu"; -// Risk renk dili: yeşil = güvenli, sarı = az riskli, kırmızı = riskli. -// Nokta hiçbir yerde tek başına durmaz; yanındaki etiket her ekran -// genişliğinde yazılı durur (renk tek başına anlam taşımaz). -const RISK_STIL: Record = { - riskli: { nokta: "bg-red-500", icon: Rocket }, - "az-riskli": { nokta: "bg-amber-500", icon: Scale }, - guvenli: { nokta: "bg-emerald-500", icon: ShieldCheck }, +// Detay başlığındaki dilim ikonu; risk etiketi RiskEtiketi'nden (§4.5). +const RISK_STIL: Record = { + riskli: { icon: Rocket }, + "az-riskli": { icon: Scale }, + guvenli: { icon: ShieldCheck }, }; // 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); return ( <> -
    - - +
    + + {DILIM_ETIKET[dilim as Dilim] ?? dilim} {farkliRisk ? ` · ${farkliRisk}` : null} - {unitur ? · {unitur} : null} + {unitur ? ( + · {UNITUR_ETIKET[unitur]?.cip ?? unitur} + ) : null}
    -
    -
    -
    +
    +
    +
    Neden listede?
    -
    {gerekce}
    +
    {gerekce}
    -
    -
    +
    +
    Risk ve Yapay Zeka yorumu
    -
    +
    {riskNotu} {trendOzeti}
    @@ -181,11 +197,19 @@ export function RaporListesi({ const risk = (adaySira != null ? riskHesapla(p?.efektifSira, adaySira) : null) ?? dilimdenRisk(t.dilim); - const stil = RISK_STIL[risk]; const seri = raporSiraSerisi(p?.siraGecmisi, p?.ilkYil); const taban = p?.efektifSira ?? programEtkinSira(seri); const acikMi = acik.has(t.sira); const devlet = p?.unitur === "DEVLET"; + const tabanMetni = taban != null ? `~${taban.toLocaleString("tr-TR")}.` : "—"; + const riskHucresi = analizKilitli ? ( + + + Pakette + + ) : ( + + ); return ( @@ -197,92 +221,88 @@ export function RaporListesi({ onClick={() => tiklandi(t.sira)} className="kt-cv-satir cursor-pointer" > - - + + {t.sira} - - - {p?.universite ? : 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 ? ( + + + + ) : null} {p ? ( e.stopPropagation()} > {p.isim} ) : ( - - {t.programId} - + {t.programId} )} - - {p?.unitur ? ( - - {devlet ? "Devlet" : "Vakıf"} - - ) : null} - - {p?.universite ? ( + + {p?.universite ? ( + e.stopPropagation()} > - {p.universite} + {trBaslikDuzeni(p.universite)} + + ) : null} + + {p?.unitur ? ( + {devlet ? "Devlet" : "Vakıf"} ) : null} - {p?.il ? ` · ${p.il.toLocaleLowerCase("tr-TR")}` : ""} + {p?.il ? ( + {trBaslikDuzeni(p.il)} + ) : null} + + + + {riskHucresi} + + Taban sıra + + {tabanMetni} + + - + - - {analizKilitli ? ( - - - Pakette - - ) : ( - - - {RISK_ETIKET[risk]} - - )} - - - + {riskHucresi} + + - {taban != null ? `~${taban.toLocaleString("tr-TR")}.` : "—"} + {tabanMetni} - + ) : null} @@ -349,13 +370,13 @@ export function RaporListesi({ return (
    {kilitIdx < rapor.tercihler.length ? ( -

    +

    Programlar ve ham veriler ücretsiz

    ) : null} {rapor.kapsam?.uyarlandi ? ( -

    +

    Tercihlerine göre {rapor.kapsam.toplam} uygun program bulundu; listen{" "} {rapor.kapsam.dagilim.hayal} hayal · {rapor.kapsam.dagilim.dengeli}{" "} dengeli · {rapor.kapsam.dagilim.garanti} güvenli olarak kuruldu. Daha @@ -364,27 +385,31 @@ export function RaporListesi({ ) : null} {tekIl ? ( -

    +

    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.

    ) : null} -
    +

    - - # - Program - + + + # + + Program + Son {yilSayisi} yıl - - Yapay Zeka Risk + + Yapay Zeka riski - Taban sıra - + + Taban sıra + + Yapay Zeka değerlendirmesi diff --git a/src/features/rapor/components/rapor-yazdir.tsx b/src/features/rapor/components/rapor-yazdir.tsx index 10fc8c0..df81b29 100644 --- a/src/features/rapor/components/rapor-yazdir.tsx +++ b/src/features/rapor/components/rapor-yazdir.tsx @@ -122,7 +122,7 @@ export async function RaporYazdir() {
    -
    +

    Bu rapor KolayTercih tarafından resmî YÖK Atlas verisi ({veriAraligi}{" "} taban sıralamaları, kontenjan ve yerleşme istatistikleri) kullanılarak diff --git a/src/features/rapor/components/tadimlik-satiri.tsx b/src/features/rapor/components/tadimlik-satiri.tsx index 1f7e8cf..944fb59 100644 --- a/src/features/rapor/components/tadimlik-satiri.tsx +++ b/src/features/rapor/components/tadimlik-satiri.tsx @@ -24,7 +24,14 @@ import { import { useGorunumOlayi } from "@/components/use-gorunum-olayi"; import { olay, siraKovasi } from "@/lib/analitik"; 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 { tercihProfiliOku } from "@/features/sihirbaz/sihirbaz-profil"; import type { TadimlikSatir, TadimlikTip } from "@/lib/tadimlik-havuzu"; @@ -120,14 +127,11 @@ export function TadimlikSatiri({ const girisUrl = `/giris?callback=${encodeURIComponent(listemUretHref("tadimlik"))}`; return ( - // first:mt-0 — kayıtlı listesi olan ziyaretçide /sonuc tadımlığı haritanın - // ve tablonun ALTINA koyar (sonuc/page.tsx), orada bölüm ritmini veren - // 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. + // /sonuc'ta artık ilk öğe sonuç başlığı (h1); tadımlık onun altında + // (listesi olanda haritanın ve tablonun altında) mt-12 ile durur. // 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. +
    Yapay zekâdan örnek satır

    @@ -147,40 +151,36 @@ export function TadimlikSatiri({

    -
    + {/* Rapor listesinin satırıyla aynı düzen (§4.15): ad sarar, + üniversite adı altında, taban sıra sağda iri. */} +
    - - {p.isim} - - - {p.unitur ? ( - - {devlet ? "Devlet" : "Vakıf"} - - ) : null} - - {p.universite} - {p.il ? ` · ${p.il.toLocaleLowerCase("tr-TR")}` : ""} + {p.isim} + + + {trBaslikDuzeni(p.universite)} + + + {p.unitur ? ( + {devlet ? "Devlet" : "Vakıf"} + ) : null} + {p.il ? ( + {trBaslikDuzeni(p.il)} + ) : null} - + - + {taban != null ? `~${taban.toLocaleString("tr-TR")}.` : "—"}
    -
    +
    -
    + ); } return ( -
    -

    - {baslik ?? "Sıralamana yakın programlar hangileri? Saniyede gör."} -

    -

    - {aciklama ?? - "YKS başarı sıralamanı gir; gerçek YÖK Atlas verisiyle sıralamana uygun hayal, dengeli ve güvenli dilimlerindeki programları anında listele. Ücretsiz, üyelik gerekmez."} -

    - - +
    + +

    + {baslik ?? "Sıralamana yakın programlar hangileri? Saniyede gör."} +

    +

    + {aciklama ?? + "YKS başarı sıralamanı gir; gerçek YÖK Atlas verisiyle sıralamana uygun hayal, dengeli ve güvenli dilimlerindeki programları anında listele. Ücretsiz, üyelik gerekmez."} +

    + +
    {cekmece} -
    +
  • ); } diff --git a/src/features/sihirbaz/components/sihirbaz-adimlar.tsx b/src/features/sihirbaz/components/sihirbaz-adimlar.tsx index 71ca9fd..d1220cd 100644 --- a/src/features/sihirbaz/components/sihirbaz-adimlar.tsx +++ b/src/features/sihirbaz/components/sihirbaz-adimlar.tsx @@ -28,13 +28,18 @@ export function Cip({ onClick, children, kucuk = false, + className = "", }: { secili: boolean; onClick: () => void; children: React.ReactNode; 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 ( @@ -350,7 +375,7 @@ export function SihirbazAdimlar({ yalnızca görünümü daraltır, seçimleri etkilemez ve kırpmayı devre dışı bırakır. */}
    @@ -362,14 +387,14 @@ export function SihirbazAdimlar({ onClick={() => listeDegistir(iller, setIller, i.il, 5)} > {trBaslikDuzeni(i.il)} - {i.adet} + {i.adet} ))} {ilListesiKirpik ? ( @@ -381,7 +406,7 @@ export function SihirbazAdimlar({ @@ -398,11 +423,11 @@ export function SihirbazAdimlar({ {gorunenDisIller.length > 0 ? (
    -

    +

    KKTC & Yurt Dışı

    @@ -414,7 +439,7 @@ export function SihirbazAdimlar({ onClick={() => listeDegistir(iller, setIller, i.il, 5)} > {trBaslikDuzeni(i.il)} - {i.adet} + {i.adet} ))}
    @@ -423,7 +448,7 @@ export function SihirbazAdimlar({ {iller.length > 0 ? (
    - + Seçtiklerin: {iller.map((il) => ( @@ -431,10 +456,10 @@ export function SihirbazAdimlar({ key={il} type="button" 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)} - + ))}
    @@ -449,9 +474,9 @@ export function SihirbazAdimlar({ />
    -
    +
    -

    +

    Üniversite tipi & öncelikler

    @@ -459,7 +484,7 @@ export function SihirbazAdimlar({

    -

    +

    Üniversite tipi

    {UNIVERSITE_TIPI_ETIKET[u.grup]} - {u.adet} + {u.adet} ))}
    -

    +

    Senin için önemli olan

    -
    + {/* 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. */} +
    {ONCELIKLER.map((o) => ( listeDegistir(oncelikler, setOncelikler, o)} > @@ -504,7 +532,7 @@ export function SihirbazAdimlar({ ))}
    -

    +

    Öncelikler yapay zekânın gerekçelerine ve program seçimine yön verir; listeyi daraltmaz.

    @@ -512,13 +540,13 @@ export function SihirbazAdimlar({
    -
    +
    ) : girisBekliyor ? ( -
    -
    - -
    -

    - Seçimlerin hazır — Yapay Zeka listen bir adım uzakta -

    -

    - Yukarıdaki tablo her zaman ücretsiz. 5 deneme kredinle 24 - 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). -

    - -

    - Kredi kartı gerekmez · Google ya da e-posta ile 30 saniyede -

    -
    + + olay("giris_cta_tiklandi", { kaynak: "hazir_panel" }) + } + > + 5 deneme kredinle Yapay Zeka listeni kur — giriş yap + + + +

    + Kredi kartı gerekmez · Google ya da e-posta ile 30 saniyede +

    + +
    ) : ( -
    -
    - -
    -

    + +

    24 tercihi tek tek elemek yerine, yapay zekâ kursun

    -

    +

    Tablodan elle eklemek her zaman serbest. Ama üç adımda ilgi - alanlarını ve şehirlerini söylersen, yapay zekâ hayal, dengeli - ve güvenli dilimlerine dağıtılmış 24 tercihi — her satırı - yerleşme riski ve gerekçesiyle — senin yerine dizer. + alanlarını ve şehirlerini söylersen, yapay zekâ hayal, dengeli ve + güvenli dilimlerine dağıtılmış 24 tercihi — her satırı yerleşme + riski ve gerekçesiyle — senin yerine dizer.

    -

    + )} {monteEdildi ? ( diff --git a/src/features/sihirbaz/components/sira-gerekli.tsx b/src/features/sihirbaz/components/sira-gerekli.tsx index 4ad42e9..bd827e9 100644 --- a/src/features/sihirbaz/components/sira-gerekli.tsx +++ b/src/features/sihirbaz/components/sira-gerekli.tsx @@ -7,7 +7,7 @@ import { useEffect } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; -import { ArrowLeft, Trophy } from "lucide-react"; +import { ArrowLeft, ArrowRight } from "lucide-react"; import { Button } from "@/components/ui/button"; import { PagePixelDivider } from "@/components/pixel-decor"; import { profilCerezineYaz } from "@/features/sihirbaz/sihirbaz-profil"; @@ -34,19 +34,21 @@ export function SiraGerekli() { if (!profilYuklendi || profil) { return (
    -

    Sıralaman kontrol ediliyor…

    +

    + Sıralaman kontrol ediliyor… +

    ); } return (
    -

    - Geçerli bir sıralama gerekli +

    + Önce sıralamanı gir

    -

    - Listeni kurabilmek için YKS başarı sıralamanı gir. Kaydettiğin sıra tüm - uygulamada kullanılır. +

    + YKS başarı sıralamanı girince hangi programların sana açık olduğunu + hemen görürsün. Bir kez girmen yeter; her sayfada kullanılır.

    @@ -56,12 +58,18 @@ export function SiraGerekli() { const { href } = sonucaGitIste(); 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]" > - Sıralamamı gir + -