Compare commits
1 Commits
bc920f6111
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b2bab50337 |
23
AGENTS.md
23
AGENTS.md
@@ -16,6 +16,27 @@ Açtığında geçerli olanlar:
|
||||
- Tarayıcıda hesap açma, şifre/kart/kimlik girme, ödeme, mesaj gönderme, yayın/post, CAPTCHA çözme, dosya indirme **yine yasak** (PROTOKOL md.5). Çerez bandı çıkarsa reddet. Kullanıcının açık oturumlarına girme.
|
||||
- Canlı siteye (`kolaytercih.com`) bakmak serbesttir ve çoğu zaman dev sunucusu açmaktan ucuzdur — önce onu dene.
|
||||
|
||||
# Sürümleme (7 Eki 2026'dan itibaren geçerli)
|
||||
|
||||
Site semantik sürüm taşır; tek kaynak `package.json` `version` alanıdır, header'daki
|
||||
rozet (`src/components/surum-rozeti.tsx`, `src/lib/surum.ts`) bunu build'den okur.
|
||||
Canlıdaki (`main`) sistem **1.0.0 Beta**'dır; `v2` dalı **2.0.0** geliştirmesidir.
|
||||
Kurallar:
|
||||
|
||||
- **Major (X.0.0):** sihirbaz/liste/ödeme akışında kullanıcının gördüğü davranış değişir,
|
||||
eski liste ya da raporlar yeni sistemle uyumsuz hâle gelir, ya da veri şeması kırılır.
|
||||
Yalnızca Bilal açar; `vN` adlı dalda biriktirilir, `main`'e merge edilince `package.json`
|
||||
`N.0.0` olur.
|
||||
- **Minor (x.Y.0):** yeni özellik, yeni ekran, yeni rehber altyapısı — geriye uyumlu.
|
||||
Özelliği `main`'e alan commit `package.json`'daki sürümü de artırır (aynı commit).
|
||||
- **Patch (x.y.Z):** hata düzeltmesi, metin/stil düzeltmesi, içerik. Düzeltme commit'i
|
||||
artırır; yalnız rehber yazısı ekleyen commit'ler sürüm artırmaz.
|
||||
- "Beta" etiketi rozette sabittir; kalkması Bilal'in kararıdır (rozet metni tek yerde,
|
||||
`surum-rozeti.tsx`).
|
||||
- Sürüm artıran commit mesajı `chore(surum): 1.1.0` ya da özelliğin mesajına `(→ 1.1.0)`
|
||||
ekiyle yazılır; `main`'e giden her push'tan sonra canlıdaki rozetin beklenen sayıyı
|
||||
gösterdiği kontrol edilir.
|
||||
|
||||
# Push öncesi build zorunlu
|
||||
|
||||
Push emri geldiğinde önce lokalde `pnpm build` başarıyla geçmeli; geçmeden asla push etme. Kritik detay: çalışma ağacı kirliyken build, commit'lenmemiş dosyaları da gördüğü için CI'da kırılacak bir commit'i yakalayamayabilir. Bu yüzden build'i **push edilecek commit'in kendisi** üzerinde doğrula (ör. geçici `git worktree add <dir> HEAD` + `node_modules` symlink'i ile). Ayrıca kısmi commit atarken değişikliğin bağımlılık kapanışını kontrol et — bir dosyanın kullandığı prop/export başka bir commit'lenmemiş dosyadan geliyorsa onları da aynı commit'e dahil et.
|
||||
@@ -30,7 +51,7 @@ Sitede açık / koyu / sistem teması var (DESIGN.md §2.3). Karanlık mod `src/
|
||||
|
||||
# Badge/eyebrow rozetleri: tek kaynak SectionEyebrow
|
||||
|
||||
Site genelindeki tüm badge/rozet/eyebrow elemanları ana sayfadaki section başlıklarının üstünde duran rozetle birebir aynı olmalı. Kanonik bileşen `src/components/pixel-decor.tsx` içindeki `SectionEyebrow`dur (piksel imzalı `PixelMark` ikonu + `inline-flex items-center gap-2 rounded-full border border-slate-200 bg-card px-3.5 py-1.5 text-xs font-semibold text-primary`; metin büyük-küçük harfli yazılır — uppercase yok). Yeni bir badge/eyebrow gerektiğinde `SectionEyebrow` kullan (`className` prop'u kenar boşluğu için var); `@/components/ui/badge`'deki `Badge` ile ya da elle yazılmış pill span'lerle yeni badge tasarlama. İnteraktif chip/buton pill'leri (filtre çipleri, nav butonları) bu kuralın dışındadır.
|
||||
Site genelindeki tüm badge/rozet/eyebrow elemanları ana sayfadaki section başlıklarının üstünde duran rozetle birebir aynı olmalı. Kanonik bileşen `src/components/pixel-decor.tsx` içindeki `SectionEyebrow`dur (piksel imzalı `PixelMark` ikonu + `inline-flex items-center gap-2 rounded-full border border-slate-200 bg-card px-3.5 py-1.5 text-xs font-semibold text-primary`; metin büyük-küçük harfli yazılır — uppercase yok). Yeni bir badge/eyebrow gerektiğinde `SectionEyebrow` kullan (`className` prop'u kenar boşluğu için var); `@/components/ui/badge`'deki `Badge` ile ya da elle yazılmış pill span'lerle yeni badge tasarlama. İnteraktif chip/buton pill'leri (filtre çipleri, nav butonları) bu kuralın dışındadır. Header'daki "Beta" sürüm rozeti (`src/components/surum-rozeti.tsx`) de dışındadır: o Arc UI Badge biçiminde, akademik gösterge morunda (`bg-indigo-500`) tek bir yerde durur; başka yere kopyalanmaz.
|
||||
|
||||
# UI tutarlılığı: ortak elemanlar tek kaynaktan
|
||||
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
},
|
||||
"registries": {
|
||||
"@skiper-ui": "https://skiper-ui.com/registry/{name}.json",
|
||||
"@magicui": "https://magicui.design/r/{name}",
|
||||
"@uiarc": "https://uiarc.dev/r/{name}.json"
|
||||
"@magicui": "https://magicui.design/r/{name}"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,6 +114,8 @@ turuncu **tek tondur: `orange-500`** — CTA butonunun turuncusuyla birebir ayn
|
||||
ton/opaklıkta bu renkler yine yasak. (`bg-violet-50 text-violet-600` tek bir mevcut durum
|
||||
çipidir; yeni yere taşıma — üniversite sayfasında "Vakıf" çipi bu yüzden hap + ikonla
|
||||
değiştirildi, §4.12.)
|
||||
İkinci istisna: üst çubuktaki "Beta" sürüm rozeti, dolu `bg-indigo-500` (§4.14, Bilal'in
|
||||
kararı, 7 Eki 2026) — gösterge karesiyle aynı mor, yine yalnız dolu zemin.
|
||||
- Gradyan metin (`bg-clip-text text-transparent`) ve parlama gölgesi (`shadow-[0_0_…]`) yok.
|
||||
- Hex/oklch değeri sınıf içine yazılmaz (`text-[#…]`); token ya da Tailwind tonu kullanılır.
|
||||
Risk renklerinin SVG karşılığı `src/lib/risk.ts` içindeki `RISK_PIN_RENK`'ten okunur.
|
||||
@@ -448,6 +450,16 @@ verilir (logo listesi istemciye inmez).
|
||||
Hareket: yay `bounce: 0, duration: 0.35`, giriş ve çıkış aynı yoldan; "hareketi azalt"ta yalnız
|
||||
opaklık.
|
||||
|
||||
**Sürüm rozeti** (`src/components/surum-rozeti.tsx`, 7 Eki 2026): logo yazısının hemen
|
||||
sağında "Beta" hapı + sürüm numarası. Hap Arc UI Badge (uiarc.dev/components/badge) `sm`
|
||||
boyunun karşılığı (yazı §3 alt sınırı gereği 11 yerine 12 px): `inline-flex h-[22px] items-center rounded-full bg-indigo-500 px-2
|
||||
text-xs leading-none font-semibold tracking-[-0.01em] text-white` — renk akademik gösterge
|
||||
karesinin moru (§2.2 istisnası), renkli zeminde yazı `font-semibold`. Sürüm hapın **içinde
|
||||
değil**, yanında: `text-xs font-medium text-slate-500 tabular-nums`, `hidden sm:inline`
|
||||
(dar ekranda yalnız hap kalır). Rozet logo yazısıyla aynı fade sınıflarını taşır; NavBaslik
|
||||
yuvası açılınca birlikte söner. Sayı `package.json`'dan gelir (AGENTS.md "Sürümleme");
|
||||
elle yazılmaz. Başka yerde tekrarlanmaz — SectionEyebrow kuralının tek istisnasıdır.
|
||||
|
||||
### 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
|
||||
@@ -686,26 +698,6 @@ genişletilebilir olmalı; UI için prompt-kit kullan."
|
||||
zaman yazılıdır (§2.2). Sohbet ilerlese de kalır. Yeni bir liste uyarısı gerekince
|
||||
`listeUyarilari`'na eklenir, sayfaya yeni kutu açılmaz.
|
||||
|
||||
### 4.21 Sayı rulosu — `SayiRulo`, `src/components/ui/sayi-rulo.tsx`
|
||||
Ekranda **değişen** sayı (üst çubuk rozeti ve kredi hapı, "Listende 5/24", çekmece sayacı,
|
||||
danışman kredi rozeti, "40 / 312 program", üst şeridin sezon geri sayımı, giriş formunun
|
||||
"Yeniden gönder (59)" sayacı, kaydet bildirimindeki seçim sayısı) rulo gibi döner:
|
||||
eklemede yukarı, çıkarmada aşağı; basamak artınca yeni sütun yayla açılır. Yazı boyu/rengi
|
||||
kapsayıcıdan gelir. Yay §6 varsayılanı; hareketi azalt'ta düz metin; ekran okuyucu düz metni
|
||||
okur. Kaynak Arc UI "Number field" (uiarc.dev, 6 Eki 2026); oradaki −/+ adım butonları ve
|
||||
sürükleme alınmadı — sıralama girişi `Input` hapı olarak kalır (§4.6), rulo yalnız gösterime
|
||||
takılır. Durağan sayıya (taban sıralaması, kontenjan) takılmaz. Geri sayım gibi basamak
|
||||
dolgusu isteyen sayaçta `enAzBasamak={2}` ("05") ve sabit `yon={-1}` (00 → 59 sarması da aşağı
|
||||
döner) verilir; sitede ikinci bir rakam kütüphanesi yoktur (`@number-flow/react` 6 Eki 2026'da
|
||||
kaldırıldı, üst şerit de bu ruloyu kullanır).
|
||||
|
||||
### 4.22 Kayan vurgulu seçim hapları — `SecimHaplari`, `src/components/ui/secim-haplari.tsx`
|
||||
Tek seçimli hap grubunda seçili vurgu yeniden boyanmaz, tek yüzey olarak seçenekten seçeneğe
|
||||
**kayar** (`layoutId`). `ayrik`: kenarlıklı ayrı haplar + mavi dolu vurgu (puan türü çipleri —
|
||||
hero ve CTA formu). `kapsul`: tek kapsülde koyu hap, yazı `text-slate-50` (tema seçici). Yay §6;
|
||||
hareketi azalt'ta vurgu yerinde belirir. Kaynak Arc UI "Chip group" (6 Eki 2026). Çok seçimli
|
||||
çipler (sihirbaz alan/il) bu desene girmez — orada her çip kendi başına yanar.
|
||||
|
||||
## 5. Yerleşim
|
||||
|
||||
### 5.1 Genel
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "kolaytercih",
|
||||
"version": "0.1.0",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
@@ -31,6 +31,7 @@
|
||||
"@ai-sdk/react": "^4.0.130",
|
||||
"@cds/city": "^1.1.0",
|
||||
"@libsql/client": "^0.17.4",
|
||||
"@number-flow/react": "^0.6.2",
|
||||
"@shadcn/helpers": "^0.2.0",
|
||||
"ai": "^7.0.127",
|
||||
"better-auth": "^1.6.23",
|
||||
|
||||
28
pnpm-lock.yaml
generated
28
pnpm-lock.yaml
generated
@@ -20,6 +20,9 @@ importers:
|
||||
'@libsql/client':
|
||||
specifier: ^0.17.4
|
||||
version: 0.17.4
|
||||
'@number-flow/react':
|
||||
specifier: ^0.6.2
|
||||
version: 0.6.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@shadcn/helpers':
|
||||
specifier: ^0.2.0
|
||||
version: 0.2.0(ai@7.0.127(zod@4.4.3))
|
||||
@@ -1286,6 +1289,12 @@ packages:
|
||||
resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
|
||||
engines: {node: '>=12.4.0'}
|
||||
|
||||
'@number-flow/react@0.6.2':
|
||||
resolution: {integrity: sha512-WjZuV4aA+vhRCgCF+adGLgFVVAJ8vdvq6EchRT2FiBIgElTXtDOA3YgkcMr9UHpvpS9v4H70AB5wZv0D9jc3QA==}
|
||||
peerDependencies:
|
||||
react: ^18 || ^19
|
||||
react-dom: ^18 || ^19
|
||||
|
||||
'@opentelemetry/semantic-conventions@1.43.0':
|
||||
resolution: {integrity: sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -3538,6 +3547,9 @@ packages:
|
||||
jiti:
|
||||
optional: true
|
||||
|
||||
esm-env@1.2.2:
|
||||
resolution: {integrity: sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==}
|
||||
|
||||
espree@10.4.0:
|
||||
resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -4680,6 +4692,9 @@ packages:
|
||||
resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
number-flow@0.6.2:
|
||||
resolution: {integrity: sha512-MCnImG4Q5vPwhSXnov56nOuyyKn6LC+Qd7II1UiKc+ACRtug5iAtn0+CwXNxM38AC5lSowEY+oYEtZX2qMnUyw==}
|
||||
|
||||
oauth-sign@0.9.0:
|
||||
resolution: {integrity: sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==}
|
||||
|
||||
@@ -6678,6 +6693,13 @@ snapshots:
|
||||
|
||||
'@nolyfill/is-core-module@1.0.39': {}
|
||||
|
||||
'@number-flow/react@0.6.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)':
|
||||
dependencies:
|
||||
esm-env: 1.2.2
|
||||
number-flow: 0.6.2
|
||||
react: 19.2.4
|
||||
react-dom: 19.2.4(react@19.2.4)
|
||||
|
||||
'@opentelemetry/semantic-conventions@1.43.0': {}
|
||||
|
||||
'@postman/form-data@3.1.1':
|
||||
@@ -9087,6 +9109,8 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
esm-env@1.2.2: {}
|
||||
|
||||
espree@10.4.0:
|
||||
dependencies:
|
||||
acorn: 8.17.0
|
||||
@@ -10411,6 +10435,10 @@ snapshots:
|
||||
path-key: 4.0.0
|
||||
unicorn-magic: 0.3.0
|
||||
|
||||
number-flow@0.6.2:
|
||||
dependencies:
|
||||
esm-env: 1.2.2
|
||||
|
||||
oauth-sign@0.9.0: {}
|
||||
|
||||
object-assign@4.1.1: {}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Suspense } from "react";
|
||||
import Link from "next/link";
|
||||
import { AnimasyonluMark } from "@/components/animasyonlu-mark";
|
||||
import { SurumRozeti } from "@/components/surum-rozeti";
|
||||
import { KatalogArama } from "@/features/katalog/components/katalog-arama";
|
||||
import { UserNav, UserNavFallback } from "@/features/kullanici/components/user-nav";
|
||||
import { getRehberAramaKayitlari } from "@/lib/rehber";
|
||||
@@ -32,6 +33,9 @@ export function SiteHeader() {
|
||||
Tercih
|
||||
</span>
|
||||
</span>
|
||||
{/* Beta rozeti + sürüm (surum-rozeti.tsx); yazı logosuyla aynı
|
||||
fade'i taşır ki NavBaslik yuvası açılınca birlikte sönsün. */}
|
||||
<SurumRozeti className="ml-2 transition-[opacity,translate] duration-300 ease-out in-data-[nav-baslik]:pointer-events-none in-data-[nav-baslik]:-translate-y-2 in-data-[nav-baslik]:opacity-0 motion-reduce:transition-opacity sm:ml-3" />
|
||||
</Link>
|
||||
<div
|
||||
id="nav-baslik-yuvasi"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { SayiRulo } from "@/components/ui/sayi-rulo";
|
||||
import { startTransition, useEffect, useState } from "react";
|
||||
import NumberFlow, { NumberFlowGroup } from "@number-flow/react";
|
||||
import { CalendarDays, Clock3 } from "lucide-react";
|
||||
import {
|
||||
BANT_ARIA,
|
||||
@@ -131,28 +131,45 @@ export function SiteTopBanner() {
|
||||
: "Geri sayım yükleniyor"
|
||||
}
|
||||
>
|
||||
{/* Rulo rakam (DESIGN.md §4.21): her birim kendi tekerleğinde
|
||||
aşağı döner; 00 → 59 sarması da aşağı (yon=-1). */}
|
||||
{BIRIMLER.map(({ anahtar, etiket }) => (
|
||||
<div className="flex items-baseline gap-0.5" key={anahtar}>
|
||||
<dt className="sr-only">{etiket}</dt>
|
||||
<dd className="font-semibold">
|
||||
{geriSayim ? (
|
||||
<SayiRulo
|
||||
deger={geriSayim[anahtar]}
|
||||
enAzBasamak={2}
|
||||
yon={-1}
|
||||
className="min-w-[2ch] justify-end"
|
||||
/>
|
||||
) : (
|
||||
"--"
|
||||
)}
|
||||
<span className="ml-0.5 text-xs font-medium opacity-70">
|
||||
{etiket}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
<NumberFlowGroup>
|
||||
{BIRIMLER.map(({ anahtar, etiket }) => (
|
||||
<div className="flex items-baseline gap-0.5" key={anahtar}>
|
||||
<dt className="sr-only">{etiket}</dt>
|
||||
<dd className="font-semibold">
|
||||
{geriSayim ? (
|
||||
<NumberFlow
|
||||
value={geriSayim[anahtar]}
|
||||
trend={-1}
|
||||
format={{
|
||||
minimumIntegerDigits: 2,
|
||||
useGrouping: false,
|
||||
}}
|
||||
transformTiming={{
|
||||
duration: 280,
|
||||
easing: "cubic-bezier(0.23, 1, 0.32, 1)",
|
||||
}}
|
||||
spinTiming={{
|
||||
duration: 280,
|
||||
easing: "cubic-bezier(0.23, 1, 0.32, 1)",
|
||||
}}
|
||||
opacityTiming={{
|
||||
duration: 180,
|
||||
easing: "ease-out",
|
||||
}}
|
||||
willChange={anahtar === "saniye"}
|
||||
className="inline-block min-w-[2ch] text-right"
|
||||
aria-hidden
|
||||
/>
|
||||
) : (
|
||||
"--"
|
||||
)}
|
||||
<span className="ml-0.5 text-xs font-medium opacity-70">
|
||||
{etiket}
|
||||
</span>
|
||||
</dd>
|
||||
</div>
|
||||
))}
|
||||
</NumberFlowGroup>
|
||||
</dl>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
26
src/components/surum-rozeti.tsx
Normal file
26
src/components/surum-rozeti.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import { SURUM } from "@/lib/surum";
|
||||
|
||||
/**
|
||||
* Header'da logonun yanındaki "Beta" rozeti + sürüm numarası (DESIGN.md
|
||||
* §4.21). Rozet Arc UI Badge (uiarc.dev/components/badge, `sm` boyu: 22px,
|
||||
* px-2, pill; yazı §3 gereği 12 px) biçiminde; rengi akademik gösterge karesinin moru
|
||||
* (`bg-indigo-500`, §2.2 istisnası). Sürüm rozetin içinde değil, yanında
|
||||
* küçük puntoyla; dar ekranda yalnız rozet kalır. Logo fade'iyle birlikte
|
||||
* söner (NavBaslik yuvası açılınca), o yüzden site-header'daki fade
|
||||
* sınıflarını aynen taşır.
|
||||
*/
|
||||
export function SurumRozeti({ className = "" }: { className?: string }) {
|
||||
return (
|
||||
<span className={`inline-flex shrink-0 items-center gap-1.5 ${className}`}>
|
||||
<span className="inline-flex h-[22px] items-center rounded-full bg-indigo-500 px-2 text-xs leading-none font-semibold tracking-[-0.01em] whitespace-nowrap text-white">
|
||||
Beta
|
||||
</span>
|
||||
<span
|
||||
className="hidden text-xs leading-none font-medium tracking-[-0.01em] text-slate-500 tabular-nums sm:inline"
|
||||
aria-label={`Sürüm ${SURUM}`}
|
||||
>
|
||||
v{SURUM}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -3,22 +3,20 @@
|
||||
import { useSyncExternalStore } from "react";
|
||||
import { useTheme } from "next-themes";
|
||||
import { Monitor, Moon, Sun } from "lucide-react";
|
||||
import { SecimHaplari } from "@/components/ui/secim-haplari";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const SECENEKLER = [
|
||||
{ deger: "light", etiket: "Açık", ikon: Sun },
|
||||
{ deger: "dark", etiket: "Koyu", ikon: Moon },
|
||||
{ deger: "system", etiket: "Sistem", ikon: Monitor },
|
||||
] as const;
|
||||
type Tema = (typeof SECENEKLER)[number]["deger"];
|
||||
|
||||
const aboneOlma = () => () => {};
|
||||
|
||||
/**
|
||||
* Tema seçici: açık / koyu / sistem (DESIGN.md §2.3). Seçili tema yalnızca
|
||||
* tarayıcıda bilinir; sunucu HTML'inde hiçbir seçenek işaretli gelmez,
|
||||
* hydration sonrası işaretlenir (yerleşim oynamaz). Koyu vurgu seçenekten
|
||||
* seçeneğe kayar (secim-haplari.tsx, kapsül varyantı).
|
||||
* hydration sonrası işaretlenir (yerleşim oynamaz).
|
||||
*/
|
||||
export function TemaSecici({ className }: { className?: string }) {
|
||||
const { theme, setTheme } = useTheme();
|
||||
@@ -27,19 +25,37 @@ export function TemaSecici({ className }: { className?: string }) {
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
const secili = tarayicida
|
||||
? (SECENEKLER.find((s) => s.deger === theme)?.deger ?? null)
|
||||
: null;
|
||||
|
||||
return (
|
||||
<SecimHaplari<Tema>
|
||||
ariaLabel="Tema"
|
||||
secenekler={SECENEKLER}
|
||||
deger={secili}
|
||||
onChange={setTheme}
|
||||
varyant="kapsul"
|
||||
boyut="md"
|
||||
className={className}
|
||||
/>
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label="Tema"
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1 rounded-full border border-slate-200 bg-card p-1",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{SECENEKLER.map(({ deger, etiket, ikon: Ikon }) => {
|
||||
const secili = tarayicida && theme === deger;
|
||||
return (
|
||||
<button
|
||||
key={deger}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={secili}
|
||||
onClick={() => setTheme(deger)}
|
||||
className={cn(
|
||||
"inline-flex h-9 cursor-pointer items-center gap-1.5 rounded-full px-3 text-sm font-semibold transition-[background-color,color,transform] duration-150 outline-none focus-visible:ring-3 focus-visible:ring-ring/50 active:scale-[0.97]",
|
||||
secili
|
||||
? "bg-slate-900 text-slate-50"
|
||||
: "text-slate-600 hover:text-orange-500",
|
||||
)}
|
||||
>
|
||||
<Ikon className="size-4" aria-hidden />
|
||||
{etiket}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
"use client";
|
||||
|
||||
// Rulo rakam (odometer): sayı değişince her basamak kendi tekerleğinde döner —
|
||||
// artarken yukarı, azalırken aşağı; 9 → 0 sarar, 10 → 9 geri döner. Basamak
|
||||
// sayısı değişince (9 → 10) yeni sütun genişliğini yayla açar, kalkan sütun
|
||||
// kapanır; sütunlar basamak değerine göre anahtarlı olduğundan birler hanesi
|
||||
// hep birler hanesi kalır.
|
||||
//
|
||||
// Kaynak: Arc UI "Number field" (uiarc.dev) rakam rulosu. Oradaki adım
|
||||
// butonları, sürükleyerek değiştirme ve sınır notu alınmadı — 6 haneli
|
||||
// sıralama girişine uymuyor; rulo yalnız EKRANDA DEĞİŞEN sayıya takılır
|
||||
// (liste sayacı gibi). Yay DESIGN.md §6 varsayılanı (sekmesiz, 0,35 s);
|
||||
// hareketi azalt'ta düz metin. Ekran okuyucu düz metni okur, rulo aria-hidden.
|
||||
|
||||
import { useLayoutEffect, useRef, useState } from "react";
|
||||
import {
|
||||
AnimatePresence,
|
||||
animate,
|
||||
motion,
|
||||
useMotionValue,
|
||||
useReducedMotion,
|
||||
useTransform,
|
||||
type MotionValue,
|
||||
type Variants,
|
||||
} from "motion/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const YAY = { type: "spring", bounce: 0, duration: 0.35 } as const;
|
||||
const RAKAMLAR = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
const BICIMLER = new Map<number, Intl.NumberFormat>();
|
||||
/** Basamak dolgusu isteyen sayaç (geri sayım "05") binlik ayracı kullanmaz. */
|
||||
function bicimAl(enAzBasamak: number): Intl.NumberFormat {
|
||||
let b = BICIMLER.get(enAzBasamak);
|
||||
if (!b) {
|
||||
b = new Intl.NumberFormat("tr-TR", {
|
||||
numberingSystem: "latn",
|
||||
minimumIntegerDigits: enAzBasamak,
|
||||
useGrouping: enAzBasamak <= 1,
|
||||
});
|
||||
BICIMLER.set(enAzBasamak, b);
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
type Parca =
|
||||
| { anahtar: string; rakam: number }
|
||||
| { anahtar: string; metin: string };
|
||||
|
||||
/** Biçimlenmiş sayıyı basamak değerine göre anahtarlı sütunlara böler. */
|
||||
function parcala(deger: number, bicim: Intl.NumberFormat): Parca[] {
|
||||
const parcalar = bicim.formatToParts(deger);
|
||||
let basamak = parcalar.reduce(
|
||||
(n, p) => n + (p.type === "integer" ? p.value.length : 0),
|
||||
0,
|
||||
);
|
||||
return parcalar.flatMap((p, i): Parca[] => {
|
||||
if (p.type === "integer")
|
||||
return [...p.value].map((c) => ({ anahtar: `b${--basamak}`, rakam: Number(c) }));
|
||||
return [
|
||||
{ anahtar: p.type === "group" ? `g${basamak}` : `${p.type}${i}`, metin: p.value },
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
// Sütun açılırken değişim yönünde yükselir, kapanırken aynı yoldan gider (§6:
|
||||
// giriş ve çıkış aynı yoldan).
|
||||
const yuva: Variants = {
|
||||
giris: (yon: number) => ({ width: 0, opacity: 0, y: `${yon * 0.3}em` }),
|
||||
merkez: { width: "auto", opacity: 1, y: 0, transition: YAY },
|
||||
cikis: (yon: number) => ({
|
||||
width: 0,
|
||||
opacity: 0,
|
||||
y: `${yon * -0.3}em`,
|
||||
transition: YAY,
|
||||
}),
|
||||
};
|
||||
|
||||
/** Tekerlekteki bir rakam: tekerleğin konumuna uzaklığı yerini ve görünürlüğünü belirler. */
|
||||
function Glif({ konum, rakam }: { konum: MotionValue<number>; rakam: number }) {
|
||||
const uzaklik = (simdiki: number) =>
|
||||
((((rakam - simdiki) % 10) + 15) % 10) - 5;
|
||||
const y = useTransform(konum, (c) => `${uzaklik(c) * 1.05}em`);
|
||||
const opacity = useTransform(konum, (c) =>
|
||||
Math.max(0, 1 - Math.abs(uzaklik(c)) ** 1.5 * 1.1),
|
||||
);
|
||||
const visibility = useTransform(konum, (c) =>
|
||||
Math.abs(uzaklik(c)) >= 1 ? "hidden" : "visible",
|
||||
);
|
||||
return (
|
||||
<motion.span
|
||||
className="absolute inset-0 flex items-center justify-center"
|
||||
style={{ y, opacity, visibility }}
|
||||
>
|
||||
{rakam}
|
||||
</motion.span>
|
||||
);
|
||||
}
|
||||
|
||||
/** Bir basamağın tekerleği: sayının gittiği yönde döner, yarım turdan uzunsa kısa yolu seçer. */
|
||||
function Tekerlek({ rakam, yon }: { rakam: number; yon: number }) {
|
||||
const konum = useMotionValue(rakam);
|
||||
const durum = useRef({ rakam, hedef: rakam });
|
||||
useLayoutEffect(() => {
|
||||
const d = durum.current;
|
||||
if (d.rakam === rakam) return;
|
||||
let fark =
|
||||
yon > 0 ? (rakam - d.rakam + 10) % 10 : -((d.rakam - rakam + 10) % 10);
|
||||
if (Math.abs(fark) > 5) fark -= Math.sign(fark) * 10;
|
||||
d.hedef += fark;
|
||||
d.rakam = rakam;
|
||||
animate(konum, d.hedef, YAY);
|
||||
}, [rakam, yon, konum]);
|
||||
return (
|
||||
<>
|
||||
<span className="invisible">0</span>
|
||||
{RAKAMLAR.map((r) => (
|
||||
<Glif key={r} konum={konum} rakam={r} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ekranda değişen bir sayı (liste sayacı gibi). Yazı boyu/rengi/ağırlığı
|
||||
* kapsayıcıdan gelir; `className` yalnız yerleşim için.
|
||||
*/
|
||||
export function SayiRulo({
|
||||
deger,
|
||||
className,
|
||||
enAzBasamak = 1,
|
||||
yon: sabitYon,
|
||||
}: {
|
||||
deger: number;
|
||||
className?: string;
|
||||
/** Basamak dolgusu: 2 → "05". Üst çubuk geri sayımı; binlik ayracı kapanır. */
|
||||
enAzBasamak?: number;
|
||||
/** Dönüş yönünü sabitler: geri sayımda 00 → 59 yukarı değil aşağı dönsün. */
|
||||
yon?: 1 | -1;
|
||||
}) {
|
||||
const azalt = useReducedMotion();
|
||||
const [iz, setIz] = useState({ deger, yon: 1 });
|
||||
if (iz.deger !== deger) setIz({ deger, yon: deger > iz.deger ? 1 : -1 });
|
||||
const yon =
|
||||
sabitYon ?? (iz.deger === deger ? iz.yon : deger > iz.deger ? 1 : -1);
|
||||
const bicim = bicimAl(enAzBasamak);
|
||||
const metin = bicim.format(deger);
|
||||
|
||||
if (azalt) return <span className={className}>{metin}</span>;
|
||||
|
||||
return (
|
||||
<span className={cn("inline-flex", className)}>
|
||||
<span className="sr-only">{metin}</span>
|
||||
<span aria-hidden className="inline-flex">
|
||||
<AnimatePresence initial={false} custom={yon}>
|
||||
{parcala(deger, bicim).map((p) => (
|
||||
<motion.span
|
||||
key={p.anahtar}
|
||||
custom={yon}
|
||||
variants={yuva}
|
||||
initial="giris"
|
||||
animate="merkez"
|
||||
exit="cikis"
|
||||
className="relative inline-block overflow-hidden"
|
||||
>
|
||||
{"rakam" in p ? <Tekerlek rakam={p.rakam} yon={yon} /> : p.metin}
|
||||
</motion.span>
|
||||
))}
|
||||
</AnimatePresence>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
"use client";
|
||||
|
||||
// Tek seçimli hap grubu (puan türü, tema): seçili vurgu ayrı ayrı yeniden
|
||||
// boyanmak yerine TEK bir yüzey olarak seçenekten seçeneğe kayar
|
||||
// (`layoutId`). Göz "hangisi seçili" sorusunu hareketin bittiği yerde okur.
|
||||
//
|
||||
// Kaynak: Arc UI "Chip group" / "Floating button group" (uiarc.dev) kayan
|
||||
// vurgu deseni; sınıf dizisi DESIGN.md hap dili. İki varyant:
|
||||
// - `ayrik`: her seçenek kendi kenarlıklı hapı, vurgu mavi dolu hap (hero ve
|
||||
// CTA formundaki puan türü çipleri).
|
||||
// - `kapsul`: tek kapsül içinde koyu hap (üst çubuk "Giriş yap" diliyle aynı,
|
||||
// tema seçici). Koyu hap yazısı `text-slate-50` (§2.3).
|
||||
// Yay §6 varsayılanı; hareketi azalt'ta vurgu kaymaz, yerinde belirir.
|
||||
|
||||
import { useId, type ComponentType } from "react";
|
||||
import { motion, useReducedMotion } from "motion/react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const YAY = { type: "spring", bounce: 0, duration: 0.35 } as const;
|
||||
|
||||
export interface SecimHapiSecenek<T extends string> {
|
||||
deger: T;
|
||||
etiket: string;
|
||||
ikon?: ComponentType<{ className?: string; "aria-hidden"?: boolean }>;
|
||||
}
|
||||
|
||||
export function SecimHaplari<T extends string>({
|
||||
secenekler,
|
||||
deger,
|
||||
onChange,
|
||||
ariaLabel,
|
||||
varyant = "ayrik",
|
||||
boyut = "sm",
|
||||
className,
|
||||
}: {
|
||||
secenekler: readonly SecimHapiSecenek<T>[];
|
||||
/** `null`: henüz seçim yok (ör. tema hidrasyondan önce) — vurgu çizilmez. */
|
||||
deger: T | null;
|
||||
onChange: (deger: T) => void;
|
||||
ariaLabel: string;
|
||||
varyant?: "ayrik" | "kapsul";
|
||||
boyut?: "sm" | "md";
|
||||
className?: string;
|
||||
}) {
|
||||
const uid = useId();
|
||||
const azalt = useReducedMotion();
|
||||
const kapsul = varyant === "kapsul";
|
||||
const yukseklik = boyut === "md" ? "h-9 px-3 text-sm" : "h-8 px-3 text-xs";
|
||||
|
||||
return (
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label={ariaLabel}
|
||||
className={cn(
|
||||
"flex flex-wrap",
|
||||
kapsul
|
||||
? "inline-flex items-center gap-1 rounded-full border border-slate-200 bg-card p-1"
|
||||
: "gap-1.5",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{secenekler.map(({ deger: d, etiket, ikon: Ikon }) => {
|
||||
const secili = deger === d;
|
||||
return (
|
||||
<button
|
||||
key={d}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={secili}
|
||||
onClick={() => onChange(d)}
|
||||
className={cn(
|
||||
"relative inline-flex cursor-pointer items-center gap-1.5 rounded-full font-semibold outline-none transition-[color,border-color,background-color,transform] duration-200 focus-visible:ring-3 focus-visible:ring-ring/50 active:scale-[0.97]",
|
||||
yukseklik,
|
||||
kapsul
|
||||
? secili
|
||||
? "text-slate-50"
|
||||
: "text-slate-600 hover:text-orange-500"
|
||||
: secili
|
||||
? "border border-transparent text-white"
|
||||
: "border border-slate-200 bg-card text-slate-600 hover:border-slate-300 hover:bg-slate-50",
|
||||
)}
|
||||
>
|
||||
{secili ? (
|
||||
<motion.span
|
||||
layoutId={`${uid}-vurgu`}
|
||||
initial={false}
|
||||
transition={azalt ? { duration: 0 } : YAY}
|
||||
className={cn(
|
||||
"absolute inset-0 rounded-full",
|
||||
kapsul ? "bg-slate-900" : "bg-primary",
|
||||
)}
|
||||
aria-hidden
|
||||
/>
|
||||
) : null}
|
||||
{Ikon ? (
|
||||
<Ikon className="relative size-4 shrink-0" aria-hidden />
|
||||
) : null}
|
||||
<span className="relative">{etiket}</span>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { SayiRulo } from "@/components/ui/sayi-rulo";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { ArrowRight, MailCheck } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
@@ -130,15 +129,11 @@ export function GirisForm({ callbackURL }: { callbackURL: string }) {
|
||||
disabled={busy !== null || bekleme > 0}
|
||||
className="cursor-pointer font-medium text-primary underline-offset-2 hover:underline disabled:cursor-default disabled:text-slate-400 disabled:no-underline"
|
||||
>
|
||||
{busy === "email" ? (
|
||||
"Gönderiliyor…"
|
||||
) : bekleme > 0 ? (
|
||||
<>
|
||||
Yeniden gönder (<SayiRulo deger={bekleme} />)
|
||||
</>
|
||||
) : (
|
||||
"Yeniden gönder"
|
||||
)}
|
||||
{busy === "email"
|
||||
? "Gönderiliyor…"
|
||||
: bekleme > 0
|
||||
? `Yeniden gönder (${bekleme})`
|
||||
: "Yeniden gönder"}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import Link from "next/link";
|
||||
import { SayiRulo } from "@/components/ui/sayi-rulo";
|
||||
import { ClipboardList, Coins } from "lucide-react";
|
||||
import { getCurrentUser } from "@/lib/session";
|
||||
import { ListemButonu } from "@/features/liste/components/listem-butonu";
|
||||
@@ -70,9 +69,7 @@ export async function UserNav() {
|
||||
title="Kredilerin — yüklemek için tıkla"
|
||||
>
|
||||
<Coins className="size-3.5 text-amber-500 sm:size-4" aria-hidden />
|
||||
<span className="tabular-nums">
|
||||
<SayiRulo deger={u.creditBalance} />
|
||||
</span>
|
||||
<span className="tabular-nums">{u.creditBalance}</span>
|
||||
{/* Etiket yalnızca md–lg arası: daha darda logo kesiliyor, lg üstünde
|
||||
ortadaki nav ile yer dar (konteyner 1152'de sabit). İkon+sayı ve
|
||||
title her genişlikte anlamı taşıyor. */}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
// hariç her yerde görünür; kapatma oturum boyunca kalıcıdır (sessionStorage).
|
||||
// Exit-intent/agresif popup YOK (PRD kuralı).
|
||||
|
||||
import { SayiRulo } from "@/components/ui/sayi-rulo";
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
@@ -73,7 +72,7 @@ export function KaydetBanner() {
|
||||
Listen yalnızca bu tarayıcıda
|
||||
</p>
|
||||
<p className="text-sm leading-snug text-slate-500">
|
||||
Giriş yap; <SayiRulo deger={liste.length} /> seçimin hesabında saklansın, her
|
||||
Giriş yap; {liste.length} seçimin hesabında saklansın, her
|
||||
cihazdan aç{" "}
|
||||
<CihazCifti className="ml-0.5 align-[-3px] text-slate-400" />
|
||||
</p>
|
||||
|
||||
@@ -15,7 +15,6 @@ import { useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { SayiRulo } from "@/components/ui/sayi-rulo";
|
||||
import { CtaBandi } from "@/components/cta-bandi";
|
||||
import { UniLogo } from "@/components/uni-logo";
|
||||
import { RiskEtiketi } from "@/components/risk-etiketi";
|
||||
@@ -199,7 +198,7 @@ export function ListeCekmecesi() {
|
||||
</span>
|
||||
) : (
|
||||
<>
|
||||
<SayiRulo deger={liste.length} /> / {MANUEL_LISTE_MAX} tercih
|
||||
{liste.length} / {MANUEL_LISTE_MAX} tercih
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { ArrowRight, Plus } from "lucide-react";
|
||||
import { SayiRulo } from "@/components/ui/sayi-rulo";
|
||||
import {
|
||||
MANUEL_LISTE_MAX,
|
||||
cekmeceAc,
|
||||
@@ -42,17 +41,9 @@ export function ListeDurumuSatiri() {
|
||||
onClick={cekmeceAc}
|
||||
className="group inline-flex h-11 cursor-pointer items-center gap-2 rounded-full border border-slate-200 bg-card px-4 text-sm font-semibold text-slate-800 transition-[background-color,border-color,color,transform] duration-150 hover:border-orange-500 hover:bg-orange-50/60 hover:text-orange-500 active:scale-[0.97]"
|
||||
>
|
||||
<span>
|
||||
Listende <SayiRulo deger={liste.length} />/{MANUEL_LISTE_MAX} program
|
||||
</span>
|
||||
Listende {liste.length}/{MANUEL_LISTE_MAX} program
|
||||
<span className="font-medium text-slate-500 group-hover:text-orange-500">
|
||||
{kalan > 0 ? (
|
||||
<>
|
||||
· <SayiRulo deger={kalan} /> yer kaldı
|
||||
</>
|
||||
) : (
|
||||
"· liste tamam"
|
||||
)}
|
||||
{kalan > 0 ? `· ${kalan} yer kaldı` : "· liste tamam"}
|
||||
</span>
|
||||
<ArrowRight
|
||||
className="size-4 transition-transform duration-150 group-hover:translate-x-0.5"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
// yaşadığı /listem sayfasına aittir — isim çakışmasın.)
|
||||
|
||||
import { ListChecks } from "lucide-react";
|
||||
import { SayiRulo } from "@/components/ui/sayi-rulo";
|
||||
import { motion, useReducedMotion } from "motion/react";
|
||||
import {
|
||||
MANUEL_LISTE_MAX,
|
||||
cekmeceDegistir,
|
||||
@@ -17,6 +17,7 @@ import { preloadListeCekmecesi } from "./liste-cekmecesi-lazy";
|
||||
export function ListemButonu() {
|
||||
const liste = useManuelListe();
|
||||
const acik = useCekmeceAcik();
|
||||
const azMotion = useReducedMotion();
|
||||
|
||||
// Liste boşken buton gürültü: tabloya (+) ulaşmamış ziyaretçiye "0/24"
|
||||
// bir şey ifade etmez. İlk program eklenince belirir, liste boşalınca gider.
|
||||
@@ -39,11 +40,16 @@ export function ListemButonu() {
|
||||
aria-label anlamı taşıyor. UserNav'daki pill'le aynı desen. */}
|
||||
<span className="hidden">Kendi Listem</span>
|
||||
{liste.length > 0 ? (
|
||||
// Rozet yerinde durur, rakam rulo gibi döner (eklemede yukarı,
|
||||
// çıkarmada aşağı) — Arc "Number field" rulosu, bkz. sayi-rulo.tsx.
|
||||
<span 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">
|
||||
<SayiRulo deger={liste.length} />
|
||||
</span>
|
||||
// key=length: her eklemede rozet yeniden doğar ve spring ile oturur
|
||||
<motion.span
|
||||
key={liste.length}
|
||||
initial={azMotion ? false : { scale: 0.6 }}
|
||||
animate={{ scale: 1 }}
|
||||
transition={{ type: "spring", duration: 0.4, bounce: 0.45 }}
|
||||
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}
|
||||
</motion.span>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
|
||||
@@ -22,7 +22,6 @@ import type {
|
||||
} from "@/types/yokatlas";
|
||||
import { DILIM_ETIKET, riskHesapla } from "@/lib/risk";
|
||||
import { bolumSayfaSlug, ilSayfaHref, trBaslikDuzeni, uniSayfaSlug } from "@/lib/slug";
|
||||
import { SayiRulo } from "@/components/ui/sayi-rulo";
|
||||
import { TurCipi } from "@/components/tur-cipi";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { VERI_YIL_SAYISI } from "@/lib/veri-yillari";
|
||||
@@ -307,7 +306,7 @@ export function ProgramTablosu({
|
||||
<ChevronDown className="size-4" aria-hidden />
|
||||
</Button>
|
||||
<span className="text-sm tabular-nums text-slate-500">
|
||||
<SayiRulo deger={satirlar.length} /> /{" "}
|
||||
{satirlar.length.toLocaleString("tr-TR")} /{" "}
|
||||
{toplam.toLocaleString("tr-TR")} program
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
} from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SayiRulo } from "@/components/ui/sayi-rulo";
|
||||
import { AnimasyonluMark } from "@/components/animasyonlu-mark";
|
||||
import { AracPilleri, aracPilleri } from "./arac-pilleri";
|
||||
import { SOHBET_MARKDOWN_BILESENLERI, baglantilariIsaretle } from "./sohbet-baglanti";
|
||||
@@ -644,7 +643,7 @@ export function SohbetClient({
|
||||
title="Kredilerin — yüklemek için tıkla"
|
||||
>
|
||||
<Coins className="size-3.5" aria-hidden />
|
||||
<SayiRulo deger={kredi} /> kredi
|
||||
{kredi} kredi
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useRouter } from "next/navigation";
|
||||
import { ArrowRight, Trophy, X } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { SecimHaplari } from "@/components/ui/secim-haplari";
|
||||
import {
|
||||
Drawer,
|
||||
DrawerClose,
|
||||
@@ -38,7 +37,6 @@ const PUAN_TURLERI_SECENEK = [
|
||||
{ deger: "dil", etiket: "Dil" },
|
||||
{ deger: "tyt", etiket: "TYT" },
|
||||
] as const;
|
||||
type PuanTuru = (typeof PUAN_TURLERI_SECENEK)[number]["deger"];
|
||||
|
||||
// Kayıtlı profilli CTA'lar /listem boş durumunun dilini taşır (bkz.
|
||||
// listem-bos-cta.tsx): büyük turuncu buton. Sınıf seti tek yerde.
|
||||
@@ -140,7 +138,7 @@ export function CtaSiraForm({
|
||||
const [acik, setAcik] = useState(false);
|
||||
const [adim, setAdim] = useState<"sira" | "sihirbaz">("sira");
|
||||
const [siralama, setSiralama] = useState("");
|
||||
const [tur, setTur] = useState<PuanTuru>("say");
|
||||
const [tur, setTur] = useState<string>("say");
|
||||
const [sira, setSira] = useState<number | null>(null);
|
||||
const [facetler, setFacetler] = useState<SihirbazFacetleri | null>(null);
|
||||
const [yukleniyor, setYukleniyor] = useState(false);
|
||||
@@ -238,14 +236,28 @@ export function CtaSiraForm({
|
||||
: "flex flex-col gap-3"
|
||||
}
|
||||
>
|
||||
{/* Seçili vurgu çipten çipe kayar (Arc kayan vurgu, secim-haplari.tsx). */}
|
||||
<SecimHaplari
|
||||
ariaLabel="Puan türü"
|
||||
secenekler={PUAN_TURLERI_SECENEK}
|
||||
deger={tur}
|
||||
onChange={setTur}
|
||||
className={cn(kompakt && "lg:shrink-0")}
|
||||
/>
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label="Puan türü"
|
||||
className={cn("flex flex-wrap gap-1.5", kompakt && "lg:shrink-0")}
|
||||
>
|
||||
{PUAN_TURLERI_SECENEK.map((t) => (
|
||||
<button
|
||||
key={t.deger}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={tur === t.deger}
|
||||
onClick={() => setTur(t.deger)}
|
||||
className={
|
||||
tur === t.deger
|
||||
? "cursor-pointer rounded-full border border-primary bg-primary px-3 py-1.5 text-xs font-medium text-white transition-colors duration-200"
|
||||
: "cursor-pointer rounded-full border border-slate-200 bg-card px-3 py-1.5 text-xs text-slate-600 transition-colors duration-200 hover:border-slate-300 hover:bg-slate-50"
|
||||
}
|
||||
>
|
||||
{t.etiket}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<label htmlFor={alanId} className="sr-only">
|
||||
YKS başarı sıralaması
|
||||
|
||||
@@ -6,7 +6,6 @@ import { useRouter } from "next/navigation";
|
||||
import { ArrowRight } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { SecimHaplari } from "@/components/ui/secim-haplari";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -36,11 +35,10 @@ const PUAN_TURLERI_SECENEK = [
|
||||
{ deger: "dil", etiket: "Dil" },
|
||||
{ deger: "tyt", etiket: "TYT" },
|
||||
] as const;
|
||||
type PuanTuru = (typeof PUAN_TURLERI_SECENEK)[number]["deger"];
|
||||
|
||||
export function HeroForm() {
|
||||
const [siralama, setSiralama] = useState("");
|
||||
const [tur, setTur] = useState<PuanTuru>("say");
|
||||
const [tur, setTur] = useState<string>("say");
|
||||
const [sira, setSira] = useState<number | null>(null);
|
||||
const [facetler, setFacetler] = useState<SihirbazFacetleri | null>(null);
|
||||
const [yukleniyor, setYukleniyor] = useState(false);
|
||||
@@ -142,14 +140,28 @@ export function HeroForm() {
|
||||
// max-w-xl: yatay dizilimde placeholder'ın tamamı inputa sığsın diye
|
||||
// (md'de buton geniş olduğundan inputa ~200px kalıyor, metin kesiliyordu).
|
||||
<div className="flex w-full max-w-xl flex-col items-center gap-3">
|
||||
{/* Seçili vurgu çipten çipe kayar (Arc kayan vurgu, secim-haplari.tsx). */}
|
||||
<SecimHaplari
|
||||
ariaLabel="Puan türü"
|
||||
secenekler={PUAN_TURLERI_SECENEK}
|
||||
deger={tur}
|
||||
onChange={setTur}
|
||||
className="justify-center"
|
||||
/>
|
||||
<div
|
||||
role="radiogroup"
|
||||
aria-label="Puan türü"
|
||||
className="flex flex-wrap justify-center gap-1.5"
|
||||
>
|
||||
{PUAN_TURLERI_SECENEK.map((t) => (
|
||||
<button
|
||||
key={t.deger}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={tur === t.deger}
|
||||
onClick={() => setTur(t.deger)}
|
||||
className={
|
||||
tur === t.deger
|
||||
? "cursor-pointer rounded-full border border-primary bg-primary px-3 py-1.5 text-xs font-medium text-white transition-colors duration-200"
|
||||
: "cursor-pointer rounded-full border border-slate-200 bg-card px-3 py-1.5 text-xs text-slate-600 transition-colors duration-200 hover:border-slate-300 hover:bg-slate-50"
|
||||
}
|
||||
>
|
||||
{t.etiket}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
|
||||
8
src/lib/surum.ts
Normal file
8
src/lib/surum.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import pkg from "../../package.json";
|
||||
|
||||
/**
|
||||
* Sitenin sürümü — tek kaynak package.json `version` alanıdır (AGENTS.md
|
||||
* "Sürümleme"). Header'daki rozet bunu okur; build'e girdiği için canlıda
|
||||
* görünen sayı her zaman deploy edilen commit'in package.json'ıdır.
|
||||
*/
|
||||
export const SURUM: string = pkg.version;
|
||||
Reference in New Issue
Block a user