feat: add i18n and avatar upload
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user