add admin page to manage site title and edit user permission and profile
This commit is contained in:
parent
86bbba00b9
commit
739eec3227
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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} →
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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]}
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
||||
|
|
@ -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;
|
||||
|
||||
|
||||
Loading…
Reference in New Issue