add admin page to manage site title and edit user permission and profile

This commit is contained in:
Indigo Tang 2025-06-09 04:02:05 +00:00
parent 86bbba00b9
commit 739eec3227
10 changed files with 638 additions and 33 deletions

View File

@ -0,0 +1,72 @@
'use client';
import AdminSidebar from '@/components/layout/AdminSidebar';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { Loader2, ShieldOff } from 'lucide-react';
import { useCurrentLocale, useI18n } from '@/locales/client';
import { Button } from '@/components/ui/button';
import Link from 'next/link';
const ADMIN_EMAIL = 'user@example.com'; // Or 'admin@example.com' if you updated login
export default function AdminLayout({
children,
}: {
children: React.ReactNode;
}) {
const router = useRouter();
const locale = useCurrentLocale();
const t = useI18n();
const [isAuthenticating, setIsAuthenticating] = useState(true);
const [isAuthorized, setIsAuthorized] = useState(false);
useEffect(() => {
const isAuthenticated = localStorage.getItem('isToyShareAuthenticated') === 'true';
const userEmail = localStorage.getItem('userEmail');
if (!isAuthenticated) {
router.replace(`/${locale}/login?redirect=/${locale}/admin`);
} else if (userEmail !== ADMIN_EMAIL && userEmail !== 'admin@example.com') { // Allow both for flexibility
setIsAuthorized(false);
setIsAuthenticating(false);
} else {
setIsAuthorized(true);
setIsAuthenticating(false);
}
}, [router, locale]);
if (isAuthenticating) {
return (
<div className="flex justify-center items-center h-screen bg-background">
<Loader2 className="h-12 w-12 animate-spin text-primary" />
<p className="ml-4 text-lg text-muted-foreground">{t('admin.layout.loading')}</p>
</div>
);
}
if (!isAuthorized) {
return (
<div className="flex flex-col justify-center items-center h-screen bg-background text-center p-4">
<ShieldOff className="h-24 w-24 text-destructive mb-6" />
<h1 className="text-3xl font-bold text-destructive mb-3">{t('admin.layout.unauthorized_title')}</h1>
<p className="text-muted-foreground mb-8 max-w-md">{t('admin.layout.unauthorized_description')}</p>
<Link href={`/${locale}/`} passHref>
<Button variant="outline" size="lg">
{t('admin.layout.back_to_home_button')}
</Button>
</Link>
</div>
);
}
return (
<div className="flex min-h-[calc(100vh-4rem)]"> {/* Assuming header height is 4rem */}
<AdminSidebar />
<main className="flex-1 p-6 lg:p-8 bg-muted/30 overflow-auto">
{children}
</main>
</div>
);
}

View File

@ -0,0 +1,103 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { Users, ToyBrick as ToyIcon, Settings, BarChart3 } from "lucide-react";
import { getI18n } from "@/locales/server";
import type { Locale } from "@/locales/server"; // Assuming Locale type might be needed if dynamic links
// Mock data for admin overview
const adminStats = {
totalUsers: 150,
totalToys: 75,
pendingRequests: 5,
};
export default async function AdminOverviewPage({ params }: { params: { locale: Locale } }) {
const t = await getI18n();
const locale = params.locale;
return (
<div className="space-y-8">
<Card className="shadow-lg border-primary/20">
<CardHeader>
<CardTitle className="text-3xl font-headline text-primary">{t('admin.overview.title')}</CardTitle>
<CardDescription>{t('admin.overview.description')}</CardDescription>
</CardHeader>
<CardContent className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<AdminStatCard
title={t('admin.overview.total_users')}
value={adminStats.totalUsers.toString()}
icon={<Users className="h-8 w-8 text-accent" />}
actionLink={`/${locale}/admin/users`}
actionLabel={t('admin.overview.manage_users_button')}
/>
<AdminStatCard
title={t('admin.overview.total_toys')}
value={adminStats.totalToys.toString()}
icon={<ToyIcon className="h-8 w-8 text-accent" />}
actionLink={`/${locale}/admin/toys`}
actionLabel={t('admin.overview.manage_toys_button')}
/>
<AdminStatCard
title={t('admin.overview.pending_requests')} // Assuming a general count for now
value={adminStats.pendingRequests.toString()}
icon={<BarChart3 className="h-8 w-8 text-yellow-500" />} // Using BarChart for general stat
actionLink={`/${locale}/dashboard/requests`} // Link to user-facing requests for now
actionLabel={t('dashboard.overview.manage_requests')} // Re-use key if applicable
/>
</CardContent>
</Card>
<div className="grid gap-6 md:grid-cols-2">
<Card className="shadow-md">
<CardHeader>
<CardTitle className="text-xl font-headline">{t('dashboard.overview.quick_actions')}</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
<Link href={`/${locale}/admin/site-settings`} passHref>
<Button className="w-full justify-start" variant="outline">
<Settings className="mr-2 h-5 w-5" /> {t('admin.overview.go_to_site_settings_button')}
</Button>
</Link>
<Link href={`/${locale}/admin/users`} passHref>
<Button className="w-full justify-start" variant="outline">
<Users className="mr-2 h-5 w-5" /> {t('admin.overview.manage_users_button')}
</Button>
</Link>
<Link href={`/${locale}/admin/toys`} passHref>
<Button className="w-full justify-start" variant="outline">
<ToyIcon className="mr-2 h-5 w-5" /> {t('admin.overview.manage_toys_button')}
</Button>
</Link>
</CardContent>
</Card>
</div>
</div>
);
}
interface AdminStatCardProps {
title: string;
value: string;
icon: React.ReactNode;
actionLink: string;
actionLabel: string;
}
function AdminStatCard({ title, value, icon, actionLink, actionLabel }: AdminStatCardProps) {
return (
<Card className="hover:shadow-lg transition-shadow bg-card">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">{title}</CardTitle>
{icon}
</CardHeader>
<CardContent>
<div className="text-4xl font-bold text-foreground">{value}</div>
<Link href={actionLink} className="text-xs text-primary hover:underline mt-1 block">
{actionLabel} &rarr;
</Link>
</CardContent>
</Card>
);
}

View File

@ -0,0 +1,45 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { getI18n } from "@/locales/server";
import { Save } from "lucide-react";
// This is a placeholder page. Actual functionality would require state management and API calls.
export default async function AdminSiteSettingsPage() {
const t = await getI18n();
const currentSiteTitle = "ToyShare"; // Mock current site title
return (
<div className="space-y-8">
<div>
<h1 className="text-3xl font-bold font-headline text-primary">{t('admin.site_settings.title')}</h1>
<p className="text-muted-foreground">{t('admin.site_settings.description')}</p>
</div>
<Card className="shadow-md max-w-2xl">
<CardHeader>
<CardTitle className="text-xl font-headline">{t('admin.site_settings.title')}</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div className="space-y-2">
<Label htmlFor="currentTitle">{t('admin.site_settings.current_title_label')}</Label>
<Input id="currentTitle" value={currentSiteTitle} readOnly disabled className="bg-muted/50" />
</div>
<div className="space-y-2">
<Label htmlFor="newTitle">{t('admin.site_settings.new_title_label')}</Label>
<Input id="newTitle" placeholder="Enter new site title" />
</div>
</CardContent>
<CardFooter>
<Button>
<Save className="mr-2 h-4 w-4" />
{t('admin.site_settings.save_button')}
</Button>
</CardFooter>
</Card>
</div>
);
}

View File

@ -0,0 +1,78 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { mockToys } from "@/lib/mockData";
import { getI18n } from "@/locales/server";
import { Edit3 } from "lucide-react";
import Link from "next/link";
import type { Locale } from "@/locales/server";
import Image from "next/image";
export default async function AdminToyManagementPage({ params }: { params: { locale: Locale } }) {
const t = await getI18n();
const locale = params.locale;
// In a real app, you might want pagination for a large number of toys
const allToys = mockToys;
return (
<div className="space-y-8">
<div>
<h1 className="text-3xl font-bold font-headline text-primary">{t('admin.toys.title')}</h1>
<p className="text-muted-foreground">{t('admin.toys.description')}</p>
</div>
<Card className="shadow-md">
<CardHeader>
<CardTitle className="text-xl font-headline">{t('admin.toys.title')}</CardTitle>
</CardHeader>
<CardContent>
{allToys.length > 0 ? (
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-16 hidden md:table-cell">Image</TableHead>
<TableHead>{t('admin.toys.table_header_name')}</TableHead>
<TableHead>{t('admin.toys.table_header_owner')}</TableHead>
<TableHead>{t('admin.toys.table_header_category')}</TableHead>
<TableHead className="text-right">{t('admin.toys.table_header_actions')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{allToys.map((toy) => (
<TableRow key={toy.id}>
<TableCell className="hidden md:table-cell">
<Image
src={toy.images[0] || 'https://placehold.co/40x40.png'}
alt={toy.name}
width={40}
height={40}
className="rounded-sm object-cover"
data-ai-hint={toy.category.toLowerCase()}
/>
</TableCell>
<TableCell className="font-medium">{toy.name}</TableCell>
<TableCell>{toy.ownerName}</TableCell>
<TableCell>{toy.category}</TableCell>
<TableCell className="text-right">
{/* Link to the existing edit page, which uses user-level permissions.
A true admin edit might need a separate form or enhanced permissions. */}
<Link href={`/${locale}/dashboard/my-toys/edit/${toy.id}`} passHref>
<Button variant="outline" size="sm">
<Edit3 className="mr-2 h-4 w-4" />
{t('admin.toys.edit_button')}
</Button>
</Link>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : (
<p className="text-muted-foreground text-center py-4">{t('admin.toys.no_toys_found')}</p>
)}
</CardContent>
</Card>
</div>
);
}

View File

@ -0,0 +1,79 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { mockToys } from "@/lib/mockData"; // Using mockToys to derive some mock users
import { getI18n } from "@/locales/server";
import { UserCog } from "lucide-react";
import Link from "next/link";
import type { Locale } from "@/locales/server";
// Create mock users based on toy owners for demonstration
const mockUsers = Array.from(new Set(mockToys.map(toy => toy.ownerId))).map(id => {
const userToy = mockToys.find(toy => toy.ownerId === id);
return {
id,
name: userToy?.ownerName || `User ${id}`,
email: `${id}@example.com`, // Simple mock email
role: id === 'user1' || id === 'admin' ? 'Admin' : 'User', // user1 is our admin
};
});
// Add a specific admin user if not already present from mockToys
if (!mockUsers.find(u => u.email === 'admin@example.com')) {
mockUsers.push({id: 'admin-main', name: 'Main Admin', email: 'admin@example.com', role: 'Admin'});
}
if (!mockUsers.find(u => u.email === 'user@example.com')) {
mockUsers.push({id: 'user1', name: 'Alice Wonderland (User1)', email: 'user@example.com', role: 'Admin'});
}
export default async function AdminUserManagementPage({ params }: { params: { locale: Locale } }) {
const t = await getI18n();
const locale = params.locale;
return (
<div className="space-y-8">
<div>
<h1 className="text-3xl font-bold font-headline text-primary">{t('admin.users.title')}</h1>
<p className="text-muted-foreground">{t('admin.users.description')}</p>
</div>
<Card className="shadow-md">
<CardHeader>
<CardTitle className="text-xl font-headline">{t('admin.users.title')}</CardTitle>
</CardHeader>
<CardContent>
{mockUsers.length > 0 ? (
<Table>
<TableHeader>
<TableRow>
<TableHead>{t('admin.users.table_header_name')}</TableHead>
<TableHead>{t('admin.users.table_header_email')}</TableHead>
<TableHead>{t('admin.users.table_header_role')}</TableHead>
<TableHead className="text-right">{t('admin.users.table_header_actions')}</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{mockUsers.map((user) => (
<TableRow key={user.id}>
<TableCell className="font-medium">{user.name}</TableCell>
<TableCell>{user.email}</TableCell>
<TableCell>{user.role}</TableCell>
<TableCell className="text-right">
<Button variant="outline" size="sm" disabled> {/* Placeholder for future edit functionality */}
<UserCog className="mr-2 h-4 w-4" />
{t('admin.users.edit_button')}
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : (
<p className="text-muted-foreground text-center py-4">{t('admin.users.no_users_found')}</p>
)}
</CardContent>
</Card>
</div>
);
}

View File

@ -1,3 +1,4 @@
'use client';
import { useState } from 'react';
@ -28,19 +29,21 @@ export default function LoginPage() {
setIsLoading(true);
await new Promise(resolve => setTimeout(resolve, 1000));
if (email === "user@example.com" && password === "password") {
// Mock admin credentials: admin@example.com / passwordadmin
// Mock user credentials: user@example.com / password
if ((email === "user@example.com" && password === "password") || (email === "admin@example.com" && password === "passwordadmin")) {
localStorage.setItem('isToyShareAuthenticated', 'true');
localStorage.setItem('userEmail', email); // Store email for admin check
toast({
title: "Login Successful", // Consider translating toast messages too
description: "Welcome back!",
title: t('login.success_title'),
description: t('login.welcome_back_toast'),
});
// If redirectPath includes locale, use it. Otherwise, prefix with current locale.
const finalRedirect = redirectPath ? (redirectPath.startsWith(`/${locale}`) ? redirectPath : `/${locale}${redirectPath}`) : `/${locale}/dashboard`;
router.push(finalRedirect);
} else {
toast({
title: "Login Failed",
description: "Invalid email or password. (Hint: user@example.com / password)",
title: t('login.failure_title'),
description: t('login.invalid_credentials_toast_user'),
variant: "destructive",
});
}
@ -63,7 +66,7 @@ export default function LoginPage() {
return (
<>
{parts[0]}
<Link href="/register" className="text-primary font-semibold hover:underline">{parts[1]}</Link>
<Link href={`/${locale}/register`} className="text-primary font-semibold hover:underline">{parts[1]}</Link>
{parts[2]}
</>
);

View File

@ -0,0 +1,88 @@
'use client';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import { Home, Settings, Users, ToyBrick as ToyIcon, LogOut, LayoutDashboardIcon, ShieldAlert } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { Separator } from '@/components/ui/separator';
import { useToast } from "@/hooks/use-toast";
import { useI18n, useCurrentLocale } from '@/locales/client';
export default function AdminSidebar() {
const pathname = usePathname();
const router = useRouter();
const t = useI18n();
const locale = useCurrentLocale();
const { toast } = useToast();
const sidebarNavItems = [
{ href: `/${locale}/admin`, label: t('admin.sidebar.overview'), icon: Home, exact: true },
{ href: `/${locale}/admin/site-settings`, label: t('admin.sidebar.site_settings'), icon: Settings },
{ href: `/${locale}/admin/users`, label: t('admin.sidebar.user_management'), icon: Users },
{ href: `/${locale}/admin/toys`, label: t('admin.sidebar.toy_management'), icon: ToyIcon },
];
const accountNavItems = [
{ href: `/${locale}/dashboard`, label: t('admin.sidebar.back_to_dashboard'), icon: LayoutDashboardIcon },
];
const handleLogout = () => {
localStorage.removeItem('isToyShareAuthenticated');
localStorage.removeItem('userEmail');
toast({ description: t('dashboard.sidebar.logout') }); // Using existing key as it's generic
router.push(`/${locale}/`);
};
const NavLink = ({ href, label, icon: Icon, exact = false }: typeof sidebarNavItems[0] & {icon: React.ElementType, exact?: boolean}) => {
const isActive = exact ? pathname === href : pathname.startsWith(href);
return (
<Link href={href} passHref>
<Button
variant={isActive ? 'secondary' : 'ghost'}
className={cn('w-full justify-start', isActive && 'font-semibold bg-primary/10 text-primary hover:bg-primary/20')}
>
<Icon className="mr-3 h-5 w-5" />
{label}
</Button>
</Link>
);
};
return (
<aside className="w-72 min-h-full bg-card border-r border-border p-4 flex flex-col shadow-lg">
<div className="mb-6">
<Link href={`/${locale}/admin`} className="flex items-center gap-2 text-primary mb-2">
<ShieldAlert className="h-8 w-8" />
<h2 className="text-2xl font-headline font-bold">{t('admin.sidebar.title')}</h2>
</Link>
<p className="text-sm text-muted-foreground">{t('admin.sidebar.management_console')}</p>
</div>
<nav className="flex-grow space-y-1">
<p className="px-3 py-2 text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('admin.sidebar.navigation')}</p>
{sidebarNavItems.map((item) => (
<NavLink key={item.href} {...item} />
))}
<Separator className="my-4" />
<p className="px-3 py-2 text-xs font-medium text-muted-foreground uppercase tracking-wider">{t('admin.sidebar.account')}</p>
{accountNavItems.map((item) => (
<NavLink key={item.href} {...item} />
))}
</nav>
<Separator className="my-4" />
<div>
<Button variant="ghost" className="w-full justify-start text-destructive hover:bg-destructive/10 hover:text-destructive-foreground" onClick={handleLogout}>
<LogOut className="mr-3 h-5 w-5" />
{t('admin.sidebar.logout')}
</Button>
</div>
</aside>
);
}

View File

@ -1,9 +1,10 @@
'use client';
import Link from 'next/link';
import { ToyBrick, UserCircle2, LogIn, UserPlus, LayoutDashboard } from 'lucide-react';
import { ToyBrick, UserCircle2, LogIn, UserPlus, LayoutDashboard, ShieldCheck } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { usePathname } from 'next/navigation';
import { usePathname, useRouter } from 'next/navigation';
import {
DropdownMenu,
DropdownMenuContent,
@ -19,40 +20,66 @@ import { useI18n, useCurrentLocale, useChangeLocale } from '@/locales/client';
export default function Header() {
const pathname = usePathname();
const router = useRouter();
const t = useI18n();
const locale = useCurrentLocale();
const changeLocale = useChangeLocale();
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isMounted, setIsMounted] = useState(false);
const [userEmail, setUserEmail] = useState<string | null>(null);
useEffect(() => {
setIsMounted(true);
const authStatus = localStorage.getItem('isToyShareAuthenticated');
setIsAuthenticated(authStatus === 'true');
}, []); // Run only once on mount
const authStatus = localStorage.getItem('isToyShareAuthenticated') === 'true';
setIsAuthenticated(authStatus);
if (authStatus) {
setUserEmail(localStorage.getItem('userEmail'));
}
}, []);
useEffect(() => {
if (isMounted) { // Ensure this runs only on the client after mount
// This effect handles re-authentication status if localStorage changes from another tab/window
const handleStorageChange = () => {
const authStatus = localStorage.getItem('isToyShareAuthenticated') === 'true';
setIsAuthenticated(authStatus);
if (authStatus) {
setUserEmail(localStorage.getItem('userEmail'));
} else {
setUserEmail(null);
}
};
window.addEventListener('storage', handleStorageChange);
return () => {
window.removeEventListener('storage', handleStorageChange);
};
}, []);
useEffect(() => {
if (isMounted) {
const preferredLang = localStorage.getItem('userPreferredLanguage') as 'en' | 'zh-TW' | null;
if (preferredLang && preferredLang !== locale) {
// Check if the current pathname without locale prefix exists for the preferredLang
// This avoids redirect loops if a page doesn't exist in the preferredLang
const currentPathWithoutLocale = pathname.startsWith(`/${locale}`) ? pathname.substring(`/${locale}`.length) : pathname;
// It's hard to verify if `currentPathWithoutLocale` is valid for `preferredLang` without a full route list
// For simplicity, we'll directly change locale. If a 404 occurs, user can switch back.
changeLocale(preferredLang);
// Ensure the path exists before redirecting. For simplicity, assume it does.
// A more robust solution would check against a list of valid routes for the preferredLang.
router.push(`/${preferredLang}${currentPathWithoutLocale}${window.location.search}`);
}
}
}, [isMounted, locale, pathname, changeLocale]);
}, [isMounted, locale, pathname, router]);
const handleLogout = () => {
localStorage.removeItem('isToyShareAuthenticated');
localStorage.removeItem('userEmail');
setIsAuthenticated(false);
// No need to router.push here, links will use current locale, or redirect logic will apply
setUserEmail(null);
router.push(`/${locale}/`);
};
// Helper to remove locale prefix for path comparison
const isAdmin = userEmail === 'user@example.com' || userEmail === 'admin@example.com';
const cleanPathname = pathname.startsWith(`/${locale}`)
? pathname.substring(`/${locale}`.length) || '/'
: pathname;
@ -60,19 +87,19 @@ export default function Header() {
return (
<header className="bg-card border-b border-border shadow-sm sticky top-0 z-50">
<div className="container mx-auto px-4 h-16 flex items-center justify-between">
<Link href="/" className="flex items-center gap-2 text-primary hover:text-primary/80 transition-colors">
<Link href={`/${locale}/`} className="flex items-center gap-2 text-primary hover:text-primary/80 transition-colors">
<ToyBrick className="h-7 w-7" />
<h1 className="text-2xl font-headline font-bold">ToyShare</h1>
</Link>
<div className="flex items-center gap-1 sm:gap-2">
<nav className="hidden sm:flex items-center gap-1 sm:gap-2">
<Link href="/" passHref>
<Link href={`/${locale}/`} passHref>
<Button variant={cleanPathname === '/' ? 'secondary' : 'ghost'} size="sm" className="px-2 sm:px-3">
{t('header.browse_toys')}
</Button>
</Link>
{isAuthenticated ? (
{isMounted && isAuthenticated ? (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="rounded-full h-9 w-9 md:h-10 md:w-10">
@ -83,17 +110,25 @@ export default function Header() {
<DropdownMenuLabel>{t('header.my_account')}</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link href="/dashboard">
<Link href={`/${locale}/dashboard`}>
<LayoutDashboard className="mr-2 h-4 w-4" />
{t('header.dashboard')}
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="/dashboard/profile">
<Link href={`/${locale}/dashboard/profile`}>
<UserCircle2 className="mr-2 h-4 w-4" />
{t('header.profile')}
</Link>
</DropdownMenuItem>
{isAdmin && (
<DropdownMenuItem asChild>
<Link href={`/${locale}/admin`}>
<ShieldCheck className="mr-2 h-4 w-4" />
{t('header.admin_panel')}
</Link>
</DropdownMenuItem>
)}
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout}>
<LogIn className="mr-2 h-4 w-4" />
@ -101,21 +136,27 @@ export default function Header() {
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : (
) : isMounted ? (
<>
<Link href="/login" passHref>
<Link href={`/${locale}/login`} passHref>
<Button variant={cleanPathname === '/login' ? 'secondary' : 'ghost'} size="sm" className="px-2 sm:px-3">
<LogIn className="mr-2 h-4 w-4" />
{t('header.login')}
</Button>
</Link>
<Link href="/register" passHref>
<Link href={`/${locale}/register`} passHref>
<Button variant="default" size="sm" className="px-2 sm:px-3">
<UserPlus className="mr-2 h-4 w-4" />
{t('header.register')}
</Button>
</Link>
</>
) : (
// Skeleton for auth buttons during hydration
<div className="flex items-center gap-2">
<div className="h-9 w-20 bg-muted rounded-md animate-pulse"></div>
<div className="h-9 w-24 bg-muted rounded-md animate-pulse"></div>
</div>
)}
</nav>
<LanguageSwitcher />

View File

@ -7,6 +7,7 @@ export default {
'header.dashboard': 'Dashboard',
'header.profile': 'Profile',
'header.logout': 'Logout',
'header.admin_panel': 'Admin Panel',
'footer.copy': '© {year} ToyShare. All rights reserved.',
'footer.tagline': 'Sharing happiness, one toy at a time.',
'home.welcome': 'Welcome to ToyShare!',
@ -22,6 +23,11 @@ export default {
'login.loading_button': 'Logging in...',
'login.forgot_password': 'Forgot your password? <link>Reset it here</link>.',
'login.no_account': "Don't have an account? <link>Sign up now</link>",
'login.success_title': 'Login Successful',
'login.welcome_back_toast': 'Welcome back!',
'login.failure_title': 'Login Failed',
'login.invalid_credentials_toast': 'Invalid email or password.',
'login.invalid_credentials_toast_user': 'Invalid email or password. (Hint: user@example.com / password or admin@example.com / passwordadmin)',
'register.create_account': 'Create Your Account',
'register.description': 'Join ToyShare and start sharing the fun!',
'register.name_label': 'Full Name',
@ -162,6 +168,48 @@ export default {
'toy_categories.puzzles': 'Puzzles',
'toy_categories.arts_crafts': 'Arts & Crafts',
'toy_categories.building_blocks': 'Building Blocks',
'admin.layout.loading': 'Loading Admin Panel...',
'admin.layout.unauthorized_title': 'Unauthorized Access',
'admin.layout.unauthorized_description': 'You do not have permission to access this page.',
'admin.layout.back_to_home_button': 'Back to Home',
'admin.sidebar.title': 'ToyShare Admin',
'admin.sidebar.management_console': 'Management Console',
'admin.sidebar.navigation': 'Navigation',
'admin.sidebar.overview': 'Overview',
'admin.sidebar.site_settings': 'Site Settings',
'admin.sidebar.user_management': 'User Management',
'admin.sidebar.toy_management': 'Toy Management',
'admin.sidebar.account': 'Account',
'admin.sidebar.back_to_dashboard': 'Back to Dashboard',
'admin.sidebar.logout': 'Logout',
'admin.overview.title': 'Admin Overview',
'admin.overview.description': 'Welcome to the ToyShare Admin Panel.',
'admin.overview.quick_stats': 'Quick Stats',
'admin.overview.total_users': 'Total Users',
'admin.overview.total_toys': 'Total Toys',
'admin.overview.pending_requests': 'Pending Rental Requests',
'admin.overview.manage_users_button': 'Manage Users',
'admin.overview.manage_toys_button': 'Manage Toys',
'admin.overview.go_to_site_settings_button': 'Go to Site Settings',
'admin.site_settings.title': 'Site Settings',
'admin.site_settings.description': 'Manage global site settings.',
'admin.site_settings.current_title_label': 'Current Site Title',
'admin.site_settings.new_title_label': 'New Site Title',
'admin.site_settings.save_button': 'Save Site Title',
'admin.users.title': 'User Management',
'admin.users.description': 'View and manage user accounts and permissions.',
'admin.users.table_header_name': 'Name',
'admin.users.table_header_email': 'Email',
'admin.users.table_header_role': 'Role',
'admin.users.table_header_actions': 'Actions',
'admin.users.edit_button': 'Edit',
'admin.users.no_users_found': 'No users found.',
'admin.toys.title': 'Toy Management',
'admin.toys.description': 'View and manage all toy listings in the system.',
'admin.toys.table_header_name': 'Toy Name',
'admin.toys.table_header_owner': 'Owner',
'admin.toys.table_header_category': 'Category',
'admin.toys.table_header_actions': 'Actions',
'admin.toys.edit_button': 'Edit Toy',
'admin.toys.no_toys_found': 'No toys found.',
} as const;

View File

@ -7,6 +7,7 @@ export default {
'header.dashboard': '儀表板',
'header.profile': '個人資料',
'header.logout': '登出',
'header.admin_panel': '管理後台',
'footer.copy': '© {year} ToyShare. 版權所有。',
'footer.tagline': '分享快樂,從玩具開始。',
'home.welcome': '歡迎來到 ToyShare',
@ -22,6 +23,11 @@ export default {
'login.loading_button': '登入中...',
'login.forgot_password': '忘記密碼? <link>在此重設</link>。',
'login.no_account': '還沒有帳戶? <link>立即註冊</link>',
'login.success_title': '登入成功',
'login.welcome_back_toast': '歡迎回來!',
'login.failure_title': '登入失敗',
'login.invalid_credentials_toast': '無效的電子郵件或密碼。',
'login.invalid_credentials_toast_user': '無效的電子郵件或密碼。(提示: user@example.com / password 或 admin@example.com / passwordadmin)',
'register.create_account': '建立您的帳戶',
'register.description': '加入 ToyShare開始分享樂趣',
'register.name_label': '全名',
@ -162,6 +168,48 @@ export default {
'toy_categories.puzzles': '拼圖',
'toy_categories.arts_crafts': '美術勞作',
'toy_categories.building_blocks': '積木',
'admin.layout.loading': '正在載入管理後台...',
'admin.layout.unauthorized_title': '未經授權的存取',
'admin.layout.unauthorized_description': '您沒有權限存取此頁面。',
'admin.layout.back_to_home_button': '返回首頁',
'admin.sidebar.title': 'ToyShare 管理後台',
'admin.sidebar.management_console': '管理控制台',
'admin.sidebar.navigation': '導覽',
'admin.sidebar.overview': '總覽',
'admin.sidebar.site_settings': '網站設定',
'admin.sidebar.user_management': '使用者管理',
'admin.sidebar.toy_management': '玩具管理',
'admin.sidebar.account': '帳戶',
'admin.sidebar.back_to_dashboard': '返回儀表板',
'admin.sidebar.logout': '登出',
'admin.overview.title': '管理後台總覽',
'admin.overview.description': '歡迎來到 ToyShare 管理後台。',
'admin.overview.quick_stats': '快速統計',
'admin.overview.total_users': '總使用者數',
'admin.overview.total_toys': '總玩具數',
'admin.overview.pending_requests': '待處理租借請求',
'admin.overview.manage_users_button': '管理使用者',
'admin.overview.manage_toys_button': '管理玩具',
'admin.overview.go_to_site_settings_button': '前往網站設定',
'admin.site_settings.title': '網站設定',
'admin.site_settings.description': '管理全域網站設定。',
'admin.site_settings.current_title_label': '目前網站標題',
'admin.site_settings.new_title_label': '新網站標題',
'admin.site_settings.save_button': '儲存網站標題',
'admin.users.title': '使用者管理',
'admin.users.description': '查看和管理使用者帳戶及權限。',
'admin.users.table_header_name': '名稱',
'admin.users.table_header_email': '電子郵件',
'admin.users.table_header_role': '角色',
'admin.users.table_header_actions': '操作',
'admin.users.edit_button': '編輯',
'admin.users.no_users_found': '找不到使用者。',
'admin.toys.title': '玩具管理',
'admin.toys.description': '查看和管理系統中的所有玩具列表。',
'admin.toys.table_header_name': '玩具名稱',
'admin.toys.table_header_owner': '擁有者',
'admin.toys.table_header_category': '類別',
'admin.toys.table_header_actions': '操作',
'admin.toys.edit_button': '編輯玩具',
'admin.toys.no_toys_found': '找不到玩具。',
} as const;