feat(CC-065): add Language & Timezone settings page (localStorage preferences)

This commit is contained in:
admin-valentin 2026-08-01 21:29:54 +00:00
parent 036ed20135
commit d2b6c04068

View 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>
);
}