feat: add i18n and avatar upload

This commit is contained in:
Vaka.pro
2026-04-26 22:16:59 +03:00
parent db41d4a246
commit 1b23097b18
22 changed files with 750 additions and 145 deletions

View File

@@ -2,12 +2,14 @@ import { useQuery } from '@tanstack/react-query';
import { Gift } from 'lucide-react';
import { api } from '@/lib/api';
import { FRONTEND_VERSION } from '@/lib/version';
import { useI18n } from '@/i18n/i18n';
interface VersionInfo {
backend: string;
}
export function Footer() {
const { t } = useI18n();
const { data } = useQuery({
queryKey: ['version'],
queryFn: () => api.get<VersionInfo>('/api/version'),
@@ -19,12 +21,12 @@ export function Footer() {
<div className="flex flex-col items-center justify-between gap-2 sm:flex-row">
<div className="flex items-center gap-2">
<Gift className="h-4 w-4" aria-hidden />
<span className="font-display text-sm">Family Wishlist</span>
<span className="font-display text-sm">{t('app.name')}</span>
</div>
<div className="flex items-center gap-3">
<span>frontend v{FRONTEND_VERSION}</span>
<span>{t('footer.frontend', { version: FRONTEND_VERSION })}</span>
<span className="opacity-50">·</span>
<span>backend v{data?.backend ?? '...'}</span>
<span>{t('footer.backend', { version: data?.backend ?? '...' })}</span>
</div>
</div>
</footer>

View File

@@ -1,20 +1,31 @@
import { Link, NavLink, useNavigate } from 'react-router-dom';
import { Archive, Gift, LogOut, Sparkles, Trash2, UserCog, CheckCircle2 } from 'lucide-react';
import type { ComponentType } from 'react';
import { Button } from '../ui/Button';
import { useAuthStore } from '@/features/auth/authStore';
import { cn } from '@/lib/cn';
import { useI18n, type TranslationKey } from '@/i18n/i18n';
import { LanguageSwitcher } from '../LanguageSwitcher';
type NavIcon = ComponentType<{ className?: string }>;
const links = [
{ to: '/', label: 'Active', icon: Sparkles, end: true },
{ to: '/completed', label: 'Fulfilled', icon: CheckCircle2 },
{ to: '/archive', label: 'Archive', icon: Archive },
{ to: '/trash', label: 'Trash', icon: Trash2 },
];
{ to: '/', label: 'header.active', icon: Sparkles, end: true },
{ to: '/completed', label: 'header.fulfilled', icon: CheckCircle2 },
{ to: '/archive', label: 'header.archive', icon: Archive },
{ to: '/trash', label: 'header.trash', icon: Trash2 },
] satisfies Array<{
to: string;
label: TranslationKey;
icon: NavIcon;
end?: boolean;
}>;
export function Header() {
const user = useAuthStore((s) => s.user);
const logout = useAuthStore((s) => s.logout);
const navigate = useNavigate();
const { t } = useI18n();
if (!user) return null;
@@ -26,9 +37,9 @@ export function Header() {
<Gift className="h-4 w-4" />
</span>
<div>
<div className="font-display text-lg leading-tight">Family Wishlist</div>
<div className="font-display text-lg leading-tight">{t('app.name')}</div>
<div className="text-xs text-muted">
signed in as <span className="font-medium text-ink">{user.displayName}</span>
{t('header.signedInAs', { name: user.displayName })}
</div>
</div>
</Link>
@@ -47,20 +58,21 @@ export function Header() {
}
>
<l.icon className="h-4 w-4" />
{l.label}
{t(l.label)}
</NavLink>
))}
</nav>
<div className="flex items-center gap-1">
<LanguageSwitcher />
<Button
variant="ghost"
size="sm"
onClick={() => navigate('/settings')}
title="Profile settings"
title={t('header.profileSettings')}
>
<UserCog className="h-4 w-4" />
Profile
{t('header.profile')}
</Button>
<Button
variant="ghost"
@@ -70,7 +82,7 @@ export function Header() {
}}
>
<LogOut className="h-4 w-4" />
Log out
{t('header.logout')}
</Button>
</div>
</div>

View File

@@ -1,13 +1,17 @@
import { Navigate, useLocation } from 'react-router-dom';
import type { ReactNode } from 'react';
import { useAuthStore } from '@/features/auth/authStore';
import { useI18n } from '@/i18n/i18n';
export function ProtectedRoute({ children }: { children: ReactNode }) {
const { user, status } = useAuthStore();
const location = useLocation();
const { t } = useI18n();
if (status !== 'ready') {
return (
<div className="flex min-h-[50vh] items-center justify-center text-muted">Loading...</div>
<div className="flex min-h-[50vh] items-center justify-center text-muted">
{t('protected.loading')}
</div>
);
}
if (!user) {