feat(arayüz): Tailwind + shadcn/ui tasarım sistemi, tema ve biçimlendirme
- Tailwind v4 + shadcn/ui (Radix), Inter yazı tipi (yerel) - Tasarım token'ları: 4 ana grup (renk körlüğüne uygun) ve durum renkleri, WCAG AA - Aydınlık / Karanlık / Sistem teması (ilk açılışta parlama yok) - lib/format.ts: para (1.250,00 ₺), miktar, yüzde, tarih (GG.AA.YYYY), tutar ayrıştırma - Vitest: 22 biçimlendirme testi; npm test artık frontend'i de çalıştırır - Butonlar mobil dokunma alanına (≥44 px) göre büyütüldü Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Tüm sayı / para / tarih biçimlendirmesi buradan yapılır (KAKEIBO_PROMPT §9).
|
||||
*
|
||||
* - Para: backend'den **kuruş cinsinden tamsayı** gelir → `1.250,00 ₺`
|
||||
* (para birimi rakamın sağında, arada bir boşluk)
|
||||
* - Miktar (altın, döviz): milyonda bir birim (`_e6`) → `12,50 gr`
|
||||
* - Tarih: `GG.AA.YYYY`
|
||||
*/
|
||||
|
||||
export type CurrencyCode = 'TRY' | 'USD' | 'EUR' | 'XAU_GRAM'
|
||||
|
||||
const CURRENCY_SUFFIX: Record<CurrencyCode, string> = {
|
||||
TRY: '₺',
|
||||
USD: '$',
|
||||
EUR: '€',
|
||||
XAU_GRAM: 'gr',
|
||||
}
|
||||
|
||||
const numberFormats = new Map<number, Intl.NumberFormat>()
|
||||
function numberFormat(decimals: number): Intl.NumberFormat {
|
||||
let f = numberFormats.get(decimals)
|
||||
if (!f) {
|
||||
f = new Intl.NumberFormat('tr-TR', {
|
||||
minimumFractionDigits: decimals,
|
||||
maximumFractionDigits: decimals,
|
||||
})
|
||||
numberFormats.set(decimals, f)
|
||||
}
|
||||
return f
|
||||
}
|
||||
|
||||
/** Ondalıklı sayıyı Türkçe biçimler: `1250.5` → `1.250,50` */
|
||||
export function formatNumber(value: number, decimals = 2): string {
|
||||
return numberFormat(decimals).format(value)
|
||||
}
|
||||
|
||||
/** Kuruş → `1.250,00 ₺`. `signed: true` ise pozitiflere `+` eklenir. */
|
||||
export function formatMoney(
|
||||
kurus: number,
|
||||
currency: CurrencyCode = 'TRY',
|
||||
options: { signed?: boolean } = {},
|
||||
): string {
|
||||
const text = `${formatNumber(kurus / 100)} ${CURRENCY_SUFFIX[currency]}`
|
||||
return options.signed && kurus > 0 ? `+${text}` : text
|
||||
}
|
||||
|
||||
/** Milyonda bir birim → `12,50 gr` (altın, döviz miktarı) */
|
||||
export function formatQuantity(e6: number, unit: string, decimals = 2): string {
|
||||
return `${formatNumber(e6 / 1_000_000, decimals)} ${unit}`
|
||||
}
|
||||
|
||||
/** Yüzde: `0.123` → `%12,3` (Türkçede % işareti başta) */
|
||||
export function formatPercent(ratio: number, decimals = 1): string {
|
||||
return `%${formatNumber(ratio * 100, decimals)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Kullanıcının yazdığı tutarı kuruşa çevirir. Geçersizse `null`.
|
||||
*
|
||||
* Kabul edilenler: `1250` · `1.250` · `1250,5` · `1.250,50` · `1250.50` (telefon klavyesi)
|
||||
* Kural: virgül varsa ondalık ayırıcıdır, noktalar binliktir.
|
||||
* Virgül yoksa ve son noktadan sonra 1–2 rakam varsa o nokta ondalıktır.
|
||||
* Float hatası olmasın diye hesap metin üzerinden yapılır.
|
||||
*/
|
||||
export function parseMoney(input: string): number | null {
|
||||
let s = input.replace(/[\s₺]/g, '')
|
||||
if (!s) return null
|
||||
|
||||
let negative = false
|
||||
if (s.startsWith('-')) {
|
||||
negative = true
|
||||
s = s.slice(1)
|
||||
}
|
||||
|
||||
let intPart: string
|
||||
let fracPart = ''
|
||||
if (s.includes(',')) {
|
||||
const parts = s.split(',')
|
||||
if (parts.length !== 2) return null
|
||||
intPart = parts[0].replace(/\./g, '')
|
||||
fracPart = parts[1]
|
||||
} else {
|
||||
const m = /^(.*)\.(\d{1,2})$/.exec(s)
|
||||
if (m && !m[1].includes('.')) {
|
||||
intPart = m[1]
|
||||
fracPart = m[2]
|
||||
} else {
|
||||
intPart = s.replace(/\./g, '')
|
||||
}
|
||||
}
|
||||
|
||||
if (intPart === '') intPart = '0'
|
||||
if (!/^\d+$/.test(intPart) || !/^\d{0,2}$/.test(fracPart)) return null
|
||||
|
||||
const kurus = Number(intPart) * 100 + Number(fracPart.padEnd(2, '0'))
|
||||
if (!Number.isSafeInteger(kurus)) return null
|
||||
return negative ? -kurus : kurus
|
||||
}
|
||||
|
||||
/** `"2026-12-09"` veya ISO zaman damgası → `09.12.2026` */
|
||||
export function formatDate(value: string | Date): string {
|
||||
if (typeof value === 'string') {
|
||||
const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(value)
|
||||
if (m) return `${m[3]}.${m[2]}.${m[1]}`
|
||||
value = new Date(value)
|
||||
}
|
||||
const dd = String(value.getDate()).padStart(2, '0')
|
||||
const mm = String(value.getMonth() + 1).padStart(2, '0')
|
||||
return `${dd}.${mm}.${value.getFullYear()}`
|
||||
}
|
||||
|
||||
/** Yerel tarih → `"2026-12-09"` (backend'in takvim tarihi biçimi) */
|
||||
export function toIsoDate(date: Date): string {
|
||||
const mm = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const dd = String(date.getDate()).padStart(2, '0')
|
||||
return `${date.getFullYear()}-${mm}-${dd}`
|
||||
}
|
||||
Reference in New Issue
Block a user