From 739eec32273693a6c6ee0d0ec59f92522d1a39f0 Mon Sep 17 00:00:00 2001 From: Indigo Tang Date: Mon, 9 Jun 2025 04:02:05 +0000 Subject: [PATCH] add admin page to manage site title and edit user permission and profile --- src/app/[locale]/admin/layout.tsx | 72 ++++++++++++ src/app/[locale]/admin/page.tsx | 103 ++++++++++++++++++ src/app/[locale]/admin/site-settings/page.tsx | 45 ++++++++ src/app/[locale]/admin/toys/page.tsx | 78 +++++++++++++ src/app/[locale]/admin/users/page.tsx | 79 ++++++++++++++ src/app/[locale]/login/page.tsx | 17 +-- src/components/layout/AdminSidebar.tsx | 88 +++++++++++++++ src/components/layout/Header.tsx | 85 +++++++++++---- src/locales/en.ts | 52 ++++++++- src/locales/zh-TW.ts | 52 ++++++++- 10 files changed, 638 insertions(+), 33 deletions(-) create mode 100644 src/app/[locale]/admin/layout.tsx create mode 100644 src/app/[locale]/admin/page.tsx create mode 100644 src/app/[locale]/admin/site-settings/page.tsx create mode 100644 src/app/[locale]/admin/toys/page.tsx create mode 100644 src/app/[locale]/admin/users/page.tsx create mode 100644 src/components/layout/AdminSidebar.tsx diff --git a/src/app/[locale]/admin/layout.tsx b/src/app/[locale]/admin/layout.tsx new file mode 100644 index 0000000..a15dd67 --- /dev/null +++ b/src/app/[locale]/admin/layout.tsx @@ -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 ( +
+ +

{t('admin.layout.loading')}

+
+ ); + } + + if (!isAuthorized) { + return ( +
+ +

{t('admin.layout.unauthorized_title')}

+

{t('admin.layout.unauthorized_description')}

+ + + +
+ ); + } + + return ( +
{/* Assuming header height is 4rem */} + +
+ {children} +
+
+ ); +} diff --git a/src/app/[locale]/admin/page.tsx b/src/app/[locale]/admin/page.tsx new file mode 100644 index 0000000..6a90f04 --- /dev/null +++ b/src/app/[locale]/admin/page.tsx @@ -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 ( +
+ + + {t('admin.overview.title')} + {t('admin.overview.description')} + + + } + actionLink={`/${locale}/admin/users`} + actionLabel={t('admin.overview.manage_users_button')} + /> + } + actionLink={`/${locale}/admin/toys`} + actionLabel={t('admin.overview.manage_toys_button')} + /> + } // 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 + /> + + + +
+ + + {t('dashboard.overview.quick_actions')} + + + + + + + + + + + + + +
+
+ ); +} + +interface AdminStatCardProps { + title: string; + value: string; + icon: React.ReactNode; + actionLink: string; + actionLabel: string; +} + +function AdminStatCard({ title, value, icon, actionLink, actionLabel }: AdminStatCardProps) { + return ( + + + {title} + {icon} + + +
{value}
+ + {actionLabel} → + +
+
+ ); +} diff --git a/src/app/[locale]/admin/site-settings/page.tsx b/src/app/[locale]/admin/site-settings/page.tsx new file mode 100644 index 0000000..6e67267 --- /dev/null +++ b/src/app/[locale]/admin/site-settings/page.tsx @@ -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 ( +
+
+

{t('admin.site_settings.title')}

+

{t('admin.site_settings.description')}

+
+ + + + {t('admin.site_settings.title')} + + +
+ + +
+
+ + +
+
+ + + +
+
+ ); +} diff --git a/src/app/[locale]/admin/toys/page.tsx b/src/app/[locale]/admin/toys/page.tsx new file mode 100644 index 0000000..24f971c --- /dev/null +++ b/src/app/[locale]/admin/toys/page.tsx @@ -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 ( +
+
+

{t('admin.toys.title')}

+

{t('admin.toys.description')}

+
+ + + + {t('admin.toys.title')} + + + {allToys.length > 0 ? ( + + + + Image + {t('admin.toys.table_header_name')} + {t('admin.toys.table_header_owner')} + {t('admin.toys.table_header_category')} + {t('admin.toys.table_header_actions')} + + + + {allToys.map((toy) => ( + + + + + {toy.name} + {toy.ownerName} + {toy.category} + + {/* Link to the existing edit page, which uses user-level permissions. + A true admin edit might need a separate form or enhanced permissions. */} + + + + + + ))} + +
+ ) : ( +

{t('admin.toys.no_toys_found')}

+ )} +
+
+
+ ); +} diff --git a/src/app/[locale]/admin/users/page.tsx b/src/app/[locale]/admin/users/page.tsx new file mode 100644 index 0000000..3f4a419 --- /dev/null +++ b/src/app/[locale]/admin/users/page.tsx @@ -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 ( +
+
+

{t('admin.users.title')}

+

{t('admin.users.description')}

+
+ + + + {t('admin.users.title')} + + + {mockUsers.length > 0 ? ( + + + + {t('admin.users.table_header_name')} + {t('admin.users.table_header_email')} + {t('admin.users.table_header_role')} + {t('admin.users.table_header_actions')} + + + + {mockUsers.map((user) => ( + + {user.name} + {user.email} + {user.role} + + + + + ))} + +
+ ) : ( +

{t('admin.users.no_users_found')}

+ )} +
+
+
+ ); +} diff --git a/src/app/[locale]/login/page.tsx b/src/app/[locale]/login/page.tsx index 0f3e1c5..bca822d 100644 --- a/src/app/[locale]/login/page.tsx +++ b/src/app/[locale]/login/page.tsx @@ -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]} - {parts[1]} + {parts[1]} {parts[2]} ); diff --git a/src/components/layout/AdminSidebar.tsx b/src/components/layout/AdminSidebar.tsx new file mode 100644 index 0000000..add66eb --- /dev/null +++ b/src/components/layout/AdminSidebar.tsx @@ -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 ( + + + + ); + }; + + return ( + + ); +} diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index 405f8ed..3ef2a49 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -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(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 (
- +

ToyShare

diff --git a/src/locales/en.ts b/src/locales/en.ts index 2e5a272..2cc9f86 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -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? Reset it here.', 'login.no_account': "Don't have an account? Sign up now", + '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; - - \ No newline at end of file diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts index 8ba4edb..09da941 100644 --- a/src/locales/zh-TW.ts +++ b/src/locales/zh-TW.ts @@ -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': '忘記密碼? 在此重設。', 'login.no_account': '還沒有帳戶? 立即註冊', + '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; - - \ No newline at end of file