feat(CC-065): add Language & Timezone settings page (localStorage preferences)
This commit is contained in:
parent
036ed20135
commit
d2b6c04068
1 changed files with 149 additions and 0 deletions
149
src/app/dashboard/settings/locale/page.tsx
Normal file
149
src/app/dashboard/settings/locale/page.tsx
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
const LANGUAGES = [
|
||||
{ code: 'ro', label: 'Română', flag: '🇷🇴' },
|
||||
{ code: 'en', label: 'English', flag: '🇬🇧' },
|
||||
{ code: 'de', label: 'Deutsch', flag: '🇩🇪' },
|
||||
{ code: 'es', label: 'Español', flag: '🇪🇸' },
|
||||
];
|
||||
|
||||
const TIMEZONES = [
|
||||
{ value: 'Europe/Bucharest', label: 'Europa/București (EET/EEST)' },
|
||||
{ value: 'Europe/Berlin', label: 'Europa/Berlin (CET/CEST)' },
|
||||
{ value: 'Europe/London', label: 'Europa/Londra (GMT/BST)' },
|
||||
{ value: 'America/New_York', label: 'SUA/New York (EST/EDT)' },
|
||||
{ value: 'UTC', label: 'UTC' },
|
||||
];
|
||||
|
||||
const DATE_FORMATS = [
|
||||
{ value: 'dd.MM.yyyy', label: '01.08.2026 (România)' },
|
||||
{ value: 'MM/dd/yyyy', label: '08/01/2026 (SUA)' },
|
||||
{ value: 'yyyy-MM-dd', label: '2026-08-01 (ISO)' },
|
||||
{ value: 'dd/MM/yyyy', label: '01/08/2026 (UK)' },
|
||||
];
|
||||
|
||||
const CURRENCIES = [
|
||||
{ code: 'RON', label: 'Leu românesc (RON)' },
|
||||
{ code: 'EUR', label: 'Euro (EUR)' },
|
||||
{ code: 'USD', label: 'Dolar american (USD)' },
|
||||
{ code: 'GBP', label: 'Liră sterlină (GBP)' },
|
||||
];
|
||||
|
||||
const STORAGE_KEY = 'ceo-os:locale-preferences';
|
||||
|
||||
interface LocalePrefs {
|
||||
language: string;
|
||||
timezone: string;
|
||||
dateFormat: string;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
function load(): LocalePrefs {
|
||||
try {
|
||||
const raw = localStorage.getItem(STORAGE_KEY);
|
||||
if (raw) return JSON.parse(raw);
|
||||
} catch {}
|
||||
return { language: 'ro', timezone: 'Europe/Bucharest', dateFormat: 'dd.MM.yyyy', currency: 'RON' };
|
||||
}
|
||||
|
||||
export default function LocalePage() {
|
||||
const [prefs, setPrefs] = useState<LocalePrefs>(load);
|
||||
const [saved, setSaved] = useState(false);
|
||||
|
||||
const update = (key: keyof LocalePrefs, value: string) => {
|
||||
setPrefs((p) => ({ ...p, [key]: value }));
|
||||
setSaved(false);
|
||||
};
|
||||
|
||||
const save = () => {
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(prefs));
|
||||
setSaved(true);
|
||||
setTimeout(() => setSaved(false), 2000);
|
||||
} catch {}
|
||||
};
|
||||
|
||||
const now = new Date();
|
||||
const tzDisplay = (() => {
|
||||
try { return now.toLocaleTimeString('ro-RO', { timeZone: prefs.timezone, hour: '2-digit', minute: '2-digit', timeZoneName: 'short' }); }
|
||||
catch { return '—'; }
|
||||
})();
|
||||
|
||||
return (
|
||||
<div className="max-w-2xl space-y-6 p-6">
|
||||
<div>
|
||||
<h1 className="font-display text-2xl font-semibold text-ink">Limbă & Fus Orar</h1>
|
||||
<p className="text-sm text-ink-faint mt-1">
|
||||
Preferințele de localizare sunt salvate local în browser.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
{/* Language */}
|
||||
<div className="card p-5 space-y-3">
|
||||
<h2 className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Limbă Interfață</h2>
|
||||
<div className="grid grid-cols-2 gap-2 sm:grid-cols-4">
|
||||
{LANGUAGES.map(({ code, label, flag }) => (
|
||||
<button key={code} onClick={() => update('language', code)}
|
||||
className={`flex items-center gap-2 rounded-xl border p-3 text-sm transition-colors ${
|
||||
prefs.language === code ? 'bg-primary/10 border-primary text-ink' : 'bg-card border-border hover:border-primary/40 text-ink-faint'
|
||||
}`}>
|
||||
<span className="text-base">{flag}</span>
|
||||
<span className="font-medium text-xs">{label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
{prefs.language !== 'ro' && (
|
||||
<p className="text-xs text-signal-warn">
|
||||
⚠ Interfața CEO OS este disponibilă momentan doar în Română. Traducerile vor fi adăugate în versiunile viitoare.
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Timezone */}
|
||||
<div className="card p-5 space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Fus Orar</h2>
|
||||
<span className="text-xs text-ink-faint font-mono">{tzDisplay}</span>
|
||||
</div>
|
||||
<select value={prefs.timezone} onChange={(e) => update('timezone', e.target.value)}
|
||||
className="w-full rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring">
|
||||
{TIMEZONES.map((tz) => <option key={tz.value} value={tz.value}>{tz.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Date format */}
|
||||
<div className="card p-5 space-y-3">
|
||||
<h2 className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Format Dată</h2>
|
||||
<div className="space-y-2">
|
||||
{DATE_FORMATS.map(({ value, label }) => (
|
||||
<label key={value} className={`flex items-center gap-3 rounded-lg border p-3 cursor-pointer transition-colors ${
|
||||
prefs.dateFormat === value ? 'bg-primary/5 border-primary/30' : 'bg-card border-border hover:border-primary/20'
|
||||
}`}>
|
||||
<input type="radio" name="dateFormat" value={value} checked={prefs.dateFormat === value}
|
||||
onChange={() => update('dateFormat', value)} className="shrink-0" />
|
||||
<span className="text-sm text-ink font-mono">{label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Default currency */}
|
||||
<div className="card p-5 space-y-3">
|
||||
<h2 className="text-xs font-semibold uppercase tracking-wider text-ink-faint">Monedă Implicită</h2>
|
||||
<select value={prefs.currency} onChange={(e) => update('currency', e.target.value)}
|
||||
className="w-full rounded-lg border bg-card px-3 py-2 text-sm text-ink focus:outline-none focus:ring-2 focus:ring-ring">
|
||||
{CURRENCIES.map((c) => <option key={c.code} value={c.code}>{c.label}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button onClick={save}
|
||||
className={`btn px-6 py-2 text-sm font-medium rounded-lg transition-colors ${saved ? 'bg-signal-ok text-white' : 'btn-primary'}`}>
|
||||
{saved ? '✓ Salvat' : 'Salvează preferințele'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in a new issue