I see this error with the app, reported by NextJS, please fix it. The er

This commit is contained in:
Indigo Tang 2025-06-09 03:21:59 +00:00
parent 128334e790
commit 28430f236e
23 changed files with 1451 additions and 87 deletions

24
package-lock.json generated
View File

@ -39,6 +39,7 @@
"genkit": "^1.8.0",
"lucide-react": "^0.475.0",
"next": "15.3.3",
"next-international": "^1.3.0",
"next-themes": "^0.3.0",
"patch-package": "^8.0.0",
"react": "^18.3.1",
@ -6935,6 +6936,12 @@
"node": ">=8"
}
},
"node_modules/international-types": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/international-types/-/international-types-0.8.1.tgz",
"integrity": "sha512-tajBCAHo4I0LIFlmQ9ZWfjMWVyRffzuvfbXCd6ssFt5u1Zw15DN0UBpVTItXdNa1ls+cpQt3Yw8+TxsfGF8JcA==",
"license": "MIT"
},
"node_modules/internmap": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
@ -7654,6 +7661,17 @@
}
}
},
"node_modules/next-international": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/next-international/-/next-international-1.3.1.tgz",
"integrity": "sha512-ydU9jQe+4MohMWltbZae/yuWeKhmp0QKQqJNNi8WCCMwrly03qfMAHw/tWbT2qgAlG++CxF5jMXmGQZgOHeVOw==",
"license": "MIT",
"dependencies": {
"client-only": "^0.0.1",
"international-types": "^0.8.1",
"server-only": "^0.0.1"
}
},
"node_modules/next-themes": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz",
@ -8902,6 +8920,12 @@
"node": ">= 0.8.0"
}
},
"node_modules/server-only": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz",
"integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==",
"license": "MIT"
},
"node_modules/set-function-length": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",

View File

@ -44,6 +44,7 @@
"lucide-react": "^0.475.0",
"next": "15.3.3",
"next-themes": "^0.3.0",
"next-international": "^1.3.0",
"patch-package": "^8.0.0",
"react": "^18.3.1",
"react-day-picker": "^8.10.1",

View File

@ -0,0 +1,46 @@
'use client';
import DashboardSidebar from '@/components/layout/DashboardSidebar';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { Loader2 } from 'lucide-react';
import { useCurrentLocale, useI18n } from '@/locales/client';
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
const router = useRouter();
const locale = useCurrentLocale();
const t = useI18n();
const [isAuthenticating, setIsAuthenticating] = useState(true);
useEffect(() => {
const isAuthenticated = localStorage.getItem('isToyShareAuthenticated') === 'true';
if (!isAuthenticated) {
router.replace(`/${locale}/login?redirect=/${locale}/dashboard`);
} else {
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('dashboard.layout.loading')}</p>
</div>
);
}
return (
<div className="flex min-h-[calc(100vh-4rem)]">
<DashboardSidebar />
<main className="flex-1 p-6 lg:p-8 bg-background overflow-auto">
{children}
</main>
</div>
);
}

View File

@ -0,0 +1,11 @@
import AddToyForm from '@/components/toys/AddToyForm';
// Note: AddToyForm is a client component and will need to use useI18n for its internal text.
// This page itself is a server component.
export default function AddNewToyPage() {
return (
<div className="space-y-8">
<AddToyForm />
</div>
);
}

View File

@ -0,0 +1,58 @@
import AddToyForm from '@/components/toys/AddToyForm';
import { mockToys } from '@/lib/mockData';
import type { Toy } from '@/types';
import { Button } from '@/components/ui/button';
import Link from 'next/link';
import { ArrowLeft } from 'lucide-react';
import { getI18n, getStaticParams as getLocaleStaticParams } from '@/locales/server';
interface EditToyPageProps {
params: { id: string, locale: string };
}
async function getToyForEdit(id: string): Promise<Partial<Toy> | undefined> {
await new Promise(resolve => setTimeout(resolve, 100));
return mockToys.find(toy => toy.id === id);
}
export default async function EditToyPage({ params }: EditToyPageProps) {
const t = await getI18n();
const toyData = await getToyForEdit(params.id);
if (!toyData) {
return (
<div className="text-center py-12">
<h1 className="text-2xl font-bold mb-4">{t('general.toy_not_found')}</h1>
<p className="text-muted-foreground mb-6">{t('general.cannot_edit_nonexistent_toy')}</p>
<Link href="/dashboard/my-toys" passHref>
<Button variant="outline">
<ArrowLeft className="mr-2 h-4 w-4" />
{t('general.back_to_my_toys')}
</Button>
</Link>
</div>
);
}
return (
<div className="space-y-8">
<Link href="/dashboard/my-toys" className="inline-flex items-center text-primary hover:underline mb-0 group">
<ArrowLeft className="mr-2 h-4 w-4 transition-transform group-hover:-translate-x-1" />
{t('general.back_to_my_toys')}
</Link>
{/* AddToyForm is a client component and will handle its own translations via useI18n */}
<AddToyForm initialData={toyData} isEditMode={true} />
</div>
);
}
// For static generation of edit pages if desired, similar to toy details page
export function generateStaticParams() {
const localeParams = getLocaleStaticParams();
const toyParams = mockToys.map((toy) => ({ id: toy.id }));
return localeParams.flatMap(lang =>
toyParams.map(toy => ({ ...lang, id: toy.id }))
);
}

View File

@ -0,0 +1,110 @@
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import ToyCard from "@/components/toys/ToyCard"; // This component would also need translation if it has text
import { mockToys } from "@/lib/mockData";
import Link from "next/link";
import { PlusCircle, Edit3, Trash2, Eye, ToyBrick as ToyBrickIcon } from "lucide-react"; // Renamed ToyBrick to avoid conflict
import Image from "next/image";
import type { Toy } from "@/types";
import { Badge } from "@/components/ui/badge";
import { getI18n } from "@/locales/server";
const currentUserId = 'user1';
const userToys = mockToys.filter(toy => toy.ownerId === currentUserId);
export default async function MyToysPage() {
const t = await getI18n();
return (
<div className="space-y-8">
<div className="flex justify-between items-center">
<div>
<h1 className="text-3xl font-bold font-headline text-primary">{t('dashboard.my_toys.title')}</h1>
<p className="text-muted-foreground">{t('dashboard.my_toys.description')}</p>
</div>
<Link href="/dashboard/my-toys/add" passHref>
<Button>
<PlusCircle className="mr-2 h-5 w-5" />
{t('dashboard.my_toys.add_new_toy_button')}
</Button>
</Link>
</div>
{userToys.length === 0 ? (
<Card className="text-center py-12 shadow-md">
<CardHeader>
<ToyBrickIcon className="h-16 w-16 mx-auto text-muted-foreground mb-4" />
<CardTitle>{t('dashboard.my_toys.no_toys_title')}</CardTitle>
<CardDescription>{t('dashboard.my_toys.no_toys_description')}</CardDescription>
</CardHeader>
<CardContent>
<Link href="/dashboard/my-toys/add" passHref>
<Button size="lg">
<PlusCircle className="mr-2 h-5 w-5" />
{t('dashboard.my_toys.add_first_toy_button')}
</Button>
</Link>
</CardContent>
</Card>
) : (
<div className="space-y-6">
{userToys.map(toy => (
<ListedToyItem key={toy.id} toy={toy} t={t} />
))}
</div>
)}
</div>
);
}
interface ListedToyItemProps {
toy: Toy & {dataAiHint?: string};
t: (key: string, params?: Record<string, string | number>) => string; // Pass t function for client components or sub-server components
}
function ListedToyItem({ toy, t }: ListedToyItemProps) {
const placeholderHint = toy.dataAiHint || toy.category.toLowerCase() || "toy";
return (
<Card className="overflow-hidden shadow-lg hover:shadow-xl transition-shadow duration-300">
<div className="flex flex-col md:flex-row">
<div className="md:w-1/3 lg:w-1/4 relative aspect-video md:aspect-auto">
<Image
src={toy.images[0] || 'https://placehold.co/300x200.png'}
alt={toy.name}
layout="fill"
objectFit="cover"
data-ai-hint={placeholderHint}
className="md:rounded-l-lg md:rounded-tr-none rounded-t-lg"
/>
</div>
<div className="flex-1 p-6">
<div className="flex justify-between items-start">
<div>
<CardTitle className="text-2xl font-headline mb-1">{toy.name}</CardTitle>
<Badge variant="outline">{toy.category}</Badge>
</div>
<div className="flex space-x-2">
<Link href={`/toys/${toy.id}`} passHref>
<Button variant="ghost" size="icon" title={t('dashboard.my_toys.view_toy_action')}>
<Eye className="h-5 w-5" />
</Button>
</Link>
<Link href={`/dashboard/my-toys/edit/${toy.id}`} passHref>
<Button variant="ghost" size="icon" title={t('dashboard.my_toys.edit_toy_action')}>
<Edit3 className="h-5 w-5" />
</Button>
</Link>
<Button variant="ghost" size="icon" className="text-destructive hover:text-destructive hover:bg-destructive/10" title={t('dashboard.my_toys.delete_toy_action')}>
<Trash2 className="h-5 w-5" />
</Button>
</div>
</div>
<p className="text-muted-foreground mt-2 text-sm line-clamp-2">{toy.description}</p>
<div className="mt-4 text-sm">
<span className="font-semibold">{t('toy_details.price')}: </span>
{toy.pricePerDay !== undefined ? (toy.pricePerDay > 0 ? `$${toy.pricePerDay}${t('toy_details.price_per_day')}` : t('toy_details.price_free')) : 'Not set'}
</div>
</div>
</div>
</Card>
);
}

View File

@ -0,0 +1,107 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { ToyBrick, PlusCircle, ListOrdered, User, ShoppingBag } from "lucide-react";
import { getI18n } from "@/locales/server";
const userStats = {
listedToys: 3,
activeRentals: 1,
pendingRequests: 2,
};
export default async function DashboardOverviewPage() {
const t = await getI18n();
return (
<div className="space-y-8">
<Card className="shadow-lg">
<CardHeader>
<CardTitle className="text-3xl font-headline text-primary">{t('dashboard.overview.welcome')}</CardTitle>
<CardDescription>{t('dashboard.overview.description')}</CardDescription>
</CardHeader>
<CardContent className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<DashboardStatCard
title={t('dashboard.overview.my_listed_toys')}
value={userStats.listedToys.toString()}
icon={<ToyBrick className="h-8 w-8 text-primary" />}
actionLink="/dashboard/my-toys"
actionLabel={t('dashboard.overview.view_my_toys')}
/>
<DashboardStatCard
title={t('dashboard.overview.toys_i_renting')}
value={userStats.activeRentals.toString()}
icon={<ShoppingBag className="h-8 w-8 text-accent" />}
actionLink="/dashboard/rentals"
actionLabel={t('dashboard.overview.view_my_rentals')}
/>
<DashboardStatCard
title={t('dashboard.overview.pending_requests')}
value={userStats.pendingRequests.toString()}
icon={<ListOrdered className="h-8 w-8 text-yellow-500" />}
actionLink="/dashboard/requests"
actionLabel={t('dashboard.overview.manage_requests')}
/>
</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="/dashboard/my-toys/add" passHref>
<Button className="w-full justify-start" variant="outline">
<PlusCircle className="mr-2 h-5 w-5" /> {t('dashboard.overview.add_new_toy_button')}
</Button>
</Link>
<Link href="/dashboard/profile" passHref>
<Button className="w-full justify-start" variant="outline">
<User className="mr-2 h-5 w-5" /> {t('dashboard.overview.update_profile_button')}
</Button>
</Link>
</CardContent>
</Card>
<Card className="shadow-md">
<CardHeader>
<CardTitle className="text-xl font-headline">{t('dashboard.overview.tips_for_sharers')}</CardTitle>
</CardHeader>
<CardContent>
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-2">
<li>{t('dashboard.overview.tip1')}</li>
<li>{t('dashboard.overview.tip2')}</li>
<li>{t('dashboard.overview.tip3')}</li>
<li>{t('dashboard.overview.tip4')}</li>
</ul>
</CardContent>
</Card>
</div>
</div>
);
}
interface DashboardStatCardProps {
title: string;
value: string;
icon: React.ReactNode;
actionLink: string;
actionLabel: string;
}
function DashboardStatCard({ title, value, icon, actionLink, actionLabel }: DashboardStatCardProps) {
return (
<Card className="hover:shadow-lg transition-shadow">
<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,160 @@
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Save } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
import { Textarea } from '@/components/ui/textarea';
import { useI18n } from '@/locales/client';
const mockUserProfile = {
name: 'Alice Wonderland',
email: 'alice@example.com',
avatarUrl: 'https://placehold.co/100x100.png?text=AW',
bio: "Lover of imaginative play and sharing joy. I have a collection of classic storybooks and dress-up costumes.",
phone: '555-123-4567',
location: 'Springfield Gardens, USA',
};
export default function ProfilePage() {
const { toast } = useToast();
const t = useI18n();
const [name, setName] = useState(mockUserProfile.name);
const [email, setEmail] = useState(mockUserProfile.email);
const [avatarUrl, setAvatarUrl] = useState(mockUserProfile.avatarUrl);
const [bio, setBio] = useState(mockUserProfile.bio);
const [phone, setPhone] = useState(mockUserProfile.phone);
const [location, setLocation] = useState(mockUserProfile.location);
const [currentPassword, setCurrentPassword] = useState('');
const [newPassword, setNewPassword] = useState('');
const [confirmNewPassword, setConfirmNewPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [isPasswordLoading, setIsPasswordLoading] = useState(false);
const handleProfileUpdate = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
await new Promise(resolve => setTimeout(resolve, 1000));
console.log("Updating profile:", { name, avatarUrl, bio, phone, location });
toast({ title: "Profile Updated", description: "Your profile information has been saved." }); // Translate
setIsLoading(false);
};
const handlePasswordChange = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (newPassword !== confirmNewPassword) {
toast({ title: "Password Mismatch", description: "New passwords do not match.", variant: "destructive" }); // Translate
return;
}
if (newPassword.length < 6) {
toast({ title: "Password Too Short", description: "Password must be at least 6 characters.", variant: "destructive" }); // Translate
return;
}
setIsPasswordLoading(true);
await new Promise(resolve => setTimeout(resolve, 1000));
console.log("Changing password");
toast({ title: "Password Updated", description: "Your password has been changed successfully." }); // Translate
setCurrentPassword('');
setNewPassword('');
setConfirmNewPassword('');
setIsPasswordLoading(false);
};
return (
<div className="space-y-8 max-w-3xl mx-auto">
<div>
<h1 className="text-3xl font-bold font-headline text-primary">{t('dashboard.profile.title')}</h1>
<p className="text-muted-foreground">{t('dashboard.profile.description')}</p>
</div>
<Card className="shadow-lg">
<CardHeader>
<CardTitle className="text-xl font-headline">{t('dashboard.profile.personal_info_title')}</CardTitle>
<CardDescription>{t('dashboard.profile.personal_info_description')}</CardDescription>
</CardHeader>
<form onSubmit={handleProfileUpdate}>
<CardContent className="space-y-6">
<div className="flex items-center space-x-4">
<Avatar className="h-20 w-20">
<AvatarImage src={avatarUrl} alt={name} data-ai-hint="user avatar"/>
<AvatarFallback>{name.split(' ').map(n => n[0]).join('').toUpperCase()}</AvatarFallback>
</Avatar>
<div className="flex-1 space-y-1">
<Label htmlFor="avatarUrl">{t('dashboard.profile.avatar_url_label')}</Label>
<Input id="avatarUrl" value={avatarUrl} onChange={(e) => setAvatarUrl(e.target.value)} placeholder="https://example.com/avatar.png" disabled={isLoading} />
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-1">
<Label htmlFor="name">{t('dashboard.profile.full_name_label')}</Label>
<Input id="name" value={name} onChange={(e) => setName(e.target.value)} placeholder="Your Name" disabled={isLoading} />
</div>
<div className="space-y-1">
<Label htmlFor="email">{t('dashboard.profile.email_label')}</Label>
<Input id="email" type="email" value={email} readOnly disabled className="bg-muted/50 cursor-not-allowed" />
</div>
</div>
<div className="space-y-1">
<Label htmlFor="bio">{t('dashboard.profile.bio_label')}</Label>
<Textarea id="bio" value={bio} onChange={(e) => setBio(e.target.value)} placeholder="Tell us a bit about yourself and your toys..." rows={3} disabled={isLoading}/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-1">
<Label htmlFor="phone">{t('dashboard.profile.phone_label')}</Label>
<Input id="phone" type="tel" value={phone} onChange={(e) => setPhone(e.target.value)} placeholder="Your Phone" disabled={isLoading} />
</div>
<div className="space-y-1">
<Label htmlFor="location">{t('dashboard.profile.location_label')}</Label>
<Input id="location" value={location} onChange={(e) => setLocation(e.target.value)} placeholder="City, Country" disabled={isLoading} />
</div>
</div>
</CardContent>
<CardFooter>
<Button type="submit" disabled={isLoading}>
<Save className="mr-2 h-4 w-4" />
{isLoading ? t('dashboard.profile.saving_button') : t('dashboard.profile.save_button')}
</Button>
</CardFooter>
</form>
</Card>
<Card className="shadow-lg">
<CardHeader>
<CardTitle className="text-xl font-headline">{t('dashboard.profile.change_password_title')}</CardTitle>
<CardDescription>{t('dashboard.profile.change_password_description')}</CardDescription>
</CardHeader>
<form onSubmit={handlePasswordChange}>
<CardContent className="space-y-4">
<div className="space-y-1">
<Label htmlFor="currentPassword">{t('dashboard.profile.current_password_label')}</Label>
<Input id="currentPassword" type="password" value={currentPassword} onChange={(e) => setCurrentPassword(e.target.value)} required disabled={isPasswordLoading} />
</div>
<div className="space-y-1">
<Label htmlFor="newPassword">{t('dashboard.profile.new_password_label')}</Label>
<Input id="newPassword" type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} required disabled={isPasswordLoading} />
</div>
<div className="space-y-1">
<Label htmlFor="confirmNewPassword">{t('dashboard.profile.confirm_new_password_label')}</Label>
<Input id="confirmNewPassword" type="password" value={confirmNewPassword} onChange={(e) => setConfirmNewPassword(e.target.value)} required disabled={isPasswordLoading} />
</div>
</CardContent>
<CardFooter>
<Button type="submit" disabled={isPasswordLoading}>
<Save className="mr-2 h-4 w-4" />
{isPasswordLoading ? t('dashboard.profile.updating_password_button') : t('dashboard.profile.update_password_button')}
</Button>
</CardFooter>
</form>
</Card>
</div>
);
}

View File

@ -0,0 +1,67 @@
import type { Metadata } from 'next';
import { PT_Sans } from 'next/font/google';
import '../globals.css'; // Adjusted path for globals.css
import { Toaster } from "@/components/ui/toaster";
import Header from '@/components/layout/Header';
import Footer from '@/components/layout/Footer';
import { cn } from '@/lib/utils';
import { ThemeProvider } from '@/components/theme-provider';
import { I18nProviderClient } from '@/locales/client';
import { getStaticParams } from '@/locales/server';
const ptSans = PT_Sans({
subsets: ['latin'],
weight: ['400', '700'],
variable: '--font-body',
});
export const metadata: Metadata = {
title: 'ToyShare - Share and Rent Toys',
description: 'A friendly platform to share and rent toys in your community.',
icons: {
icon: '/favicon.ico',
}
};
// Needed for static generation with dynamic [locale] segment
export function generateStaticParams() {
return getStaticParams();
}
export default function RootLayout({
children,
params: { locale }
}: Readonly<{
children: React.ReactNode;
params: { locale: string };
}>) {
return (
<I18nProviderClient locale={locale}>
<html lang={locale} suppressHydrationWarning>
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
<link href="https://fonts.googleapis.com/css2?family=PT+Sans:wght@400;700&display=swap" rel="stylesheet" />
</head>
<body
className={cn('min-h-screen bg-background font-body antialiased flex flex-col', ptSans.variable)}
suppressHydrationWarning={true}
>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
disableTransitionOnChange
>
<Header />
<main className="flex-grow container mx-auto px-4 py-8">
{children}
</main>
<Footer />
<Toaster />
</ThemeProvider>
</body>
</html>
</I18nProviderClient>
);
}

View File

@ -0,0 +1,130 @@
'use client';
import { useState } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { LogIn } from 'lucide-react';
import { useToast } from "@/hooks/use-toast";
import { useI18n, useCurrentLocale } from '@/locales/client';
export default function LoginPage() {
const router = useRouter();
const searchParams = useSearchParams();
const redirectPath = searchParams.get('redirect');
const { toast } = useToast();
const t = useI18n();
const locale = useCurrentLocale();
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
await new Promise(resolve => setTimeout(resolve, 1000));
if (email === "user@example.com" && password === "password") {
localStorage.setItem('isToyShareAuthenticated', 'true');
toast({
title: "Login Successful", // Consider translating toast messages too
description: "Welcome back!",
});
// 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)",
variant: "destructive",
});
}
setIsLoading(false);
};
const renderForgotPasswordLink = () => {
const parts = t('login.forgot_password').split(/<link>|<\/link>/);
return (
<>
{parts[0]}
<Link href="#" className="text-primary hover:underline">{parts[1]}</Link>
{parts[2]}
</>
);
}
const renderNoAccountLink = () => {
const parts = t('login.no_account').split(/<link>|<\/link>/);
return (
<>
{parts[0]}
<Link href="/register" className="text-primary font-semibold hover:underline">{parts[1]}</Link>
{parts[2]}
</>
);
}
return (
<div className="flex justify-center items-center min-h-[calc(100vh-12rem)] py-12">
<Card className="w-full max-w-md shadow-xl">
<CardHeader className="text-center">
<div className="mx-auto bg-primary text-primary-foreground rounded-full p-3 w-fit mb-4">
<LogIn className="h-8 w-8" />
</div>
<CardTitle className="text-3xl font-headline">{t('login.welcome_back')}</CardTitle>
<CardDescription>{t('login.description')}</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="email">{t('login.email_label')}</Label>
<Input
id="email"
type="email"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
disabled={isLoading}
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">{t('login.password_label')}</Label>
<Input
id="password"
type="password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
disabled={isLoading}
/>
</div>
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? t('login.loading_button') : t('login.submit_button')}
</Button>
</form>
</CardContent>
<CardFooter className="flex flex-col gap-4 text-center">
<p className="text-sm text-muted-foreground">
{renderForgotPasswordLink()}
</p>
<Separator />
<p className="text-sm text-muted-foreground">
{renderNoAccountLink()}
</p>
</CardFooter>
</Card>
</div>
);
}
function Separator() {
return <div className="h-px w-full bg-border my-2" />;
}

43
src/app/[locale]/page.tsx Normal file
View File

@ -0,0 +1,43 @@
import ToyList from '@/components/toys/ToyList';
import { mockToys } from '@/lib/mockData';
import { Button } from '@/components/ui/button';
import Link from 'next/link';
import { PlusCircle } from 'lucide-react';
import { getI18n } from '@/locales/server';
export default async function HomePage() {
const t = await getI18n();
return (
<div className="space-y-8">
<section className="text-center py-12 bg-gradient-to-r from-primary/10 via-background to-accent/10 rounded-lg shadow">
<h1 className="text-4xl md:text-5xl font-bold font-headline text-primary mb-4">
{t('home.welcome')}
</h1>
<p className="text-lg text-foreground/80 max-w-2xl mx-auto mb-8">
{t('home.discover')}
</p>
<div className="space-x-4">
<Link href="/#toy-listings" passHref>
<Button size="lg" variant="default" className="transition-transform transform hover:scale-105">
{t('home.explore_toys')}
</Button>
</Link>
<Link href="/dashboard/my-toys/add" passHref>
<Button size="lg" variant="outline" className="transition-transform transform hover:scale-105">
<PlusCircle className="mr-2 h-5 w-5" />
{t('home.share_your_toy')}
</Button>
</Link>
</div>
</section>
<section id="toy-listings" className="pt-8">
<h2 className="text-3xl font-bold font-headline text-center mb-8 text-primary">
{t('home.available_toys')}
</h2>
<ToyList toys={mockToys.map(toy => ({...toy, dataAiHint: toy.category.toLowerCase()}))} />
</section>
</div>
);
}

View File

@ -0,0 +1,135 @@
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { UserPlus } from 'lucide-react';
import { useToast } from "@/hooks/use-toast";
import { useI18n, useCurrentLocale } from '@/locales/client';
export default function RegisterPage() {
const router = useRouter();
const { toast } = useToast();
const t = useI18n();
const locale = useCurrentLocale();
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [isLoading, setIsLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
if (password !== confirmPassword) {
toast({
title: "Registration Error", // Translate
description: "Passwords do not match.", // Translate
variant: "destructive",
});
setIsLoading(false);
return;
}
await new Promise(resolve => setTimeout(resolve, 1000));
localStorage.setItem('isToyShareAuthenticated', 'true');
toast({
title: "Registration Successful", // Translate
description: "Your account has been created. Welcome!", // Translate
});
router.push(`/${locale}/dashboard`);
setIsLoading(false);
};
const renderHasAccountLink = () => {
const parts = t('register.has_account').split(/<link>|<\/link>/);
return (
<>
{parts[0]}
<Link href="/login" className="text-primary font-semibold hover:underline">{parts[1]}</Link>
{parts[2]}
</>
);
};
return (
<div className="flex justify-center items-center min-h-[calc(100vh-12rem)] py-12">
<Card className="w-full max-w-md shadow-xl">
<CardHeader className="text-center">
<div className="mx-auto bg-primary text-primary-foreground rounded-full p-3 w-fit mb-4">
<UserPlus className="h-8 w-8" />
</div>
<CardTitle className="text-3xl font-headline">{t('register.create_account')}</CardTitle>
<CardDescription>{t('register.description')}</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-6">
<div className="space-y-2">
<Label htmlFor="name">{t('register.name_label')}</Label>
<Input
id="name"
type="text"
placeholder="John Doe"
value={name}
onChange={(e) => setName(e.target.value)}
required
disabled={isLoading}
/>
</div>
<div className="space-y-2">
<Label htmlFor="email">{t('login.email_label')}</Label>
<Input
id="email"
type="email"
placeholder="you@example.com"
value={email}
onChange={(e) => setEmail(e.target.value)}
required
disabled={isLoading}
/>
</div>
<div className="space-y-2">
<Label htmlFor="password">{t('login.password_label')}</Label>
<Input
id="password"
type="password"
placeholder="••••••••"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
disabled={isLoading}
/>
</div>
<div className="space-y-2">
<Label htmlFor="confirmPassword">{t('register.confirm_password_label')}</Label>
<Input
id="confirmPassword"
type="password"
placeholder="••••••••"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
required
disabled={isLoading}
/>
</div>
<Button type="submit" className="w-full" disabled={isLoading}>
{isLoading ? t('register.loading_button') : t('register.submit_button')}
</Button>
</form>
</CardContent>
<CardFooter className="text-center">
<p className="text-sm text-muted-foreground w-full">
{renderHasAccountLink()}
</p>
</CardFooter>
</Card>
</div>
);
}

View File

@ -0,0 +1,138 @@
import Image from 'next/image';
import { mockToys } from '@/lib/mockData';
import type { Toy } from '@/types';
import { Button } from '@/components/ui/button';
import AvailabilityCalendar from '@/components/toys/AvailabilityCalendar';
import { ArrowLeft, CalendarDays, DollarSign, MapPin, ShoppingBag, UserCircle2 } from 'lucide-react';
import Link from 'next/link';
import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator';
import { getI18n, getStaticParams as getLocaleStaticParams } from '@/locales/server'; // Renamed to avoid conflict
interface ToyPageProps {
params: { id: string, locale: string };
}
async function getToyById(id: string): Promise<Toy | undefined> {
await new Promise(resolve => setTimeout(resolve, 200));
return mockToys.find(toy => toy.id === id);
}
export default async function ToyPage({ params }: ToyPageProps) {
const t = await getI18n();
const toy = await getToyById(params.id);
if (!toy) {
return (
<div className="text-center py-12">
<h1 className="text-2xl font-bold mb-4">{t('toy_details.toy_not_found_title')}</h1>
<p className="text-muted-foreground mb-6">{t('toy_details.toy_not_found_description')}</p>
<Link href="/" passHref>
<Button variant="outline">
<ArrowLeft className="mr-2 h-4 w-4" />
{t('toy_details.back_to_toys')}
</Button>
</Link>
</div>
);
}
const placeholderHint = toy.category.toLowerCase() || "toy detail";
return (
<div className="container mx-auto py-8 px-4">
<Link href="/" className="inline-flex items-center text-primary hover:underline mb-6 group">
<ArrowLeft className="mr-2 h-4 w-4 transition-transform group-hover:-translate-x-1" />
{t('toy_details.back_to_toys')}
</Link>
<div className="grid md:grid-cols-2 gap-8 lg:gap-12 items-start">
<div className="space-y-4">
<div className="aspect-video relative w-full rounded-lg overflow-hidden shadow-lg">
<Image
src={toy.images[0] || 'https://placehold.co/600x400.png'}
alt={toy.name}
layout="fill"
objectFit="cover"
data-ai-hint={placeholderHint + " main"}
priority
/>
</div>
{toy.images.length > 1 && (
<div className="grid grid-cols-3 gap-2">
{toy.images.slice(1, 4).map((img, index) => (
<div key={index} className="aspect-square relative w-full rounded-md overflow-hidden shadow">
<Image
src={img}
alt={`${toy.name} - image ${index + 2}`}
layout="fill"
objectFit="cover"
data-ai-hint={placeholderHint + " thumbnail"}
/>
</div>
))}
</div>
)}
</div>
<div className="space-y-6">
<Badge variant="secondary" className="text-sm">{toy.category}</Badge>
<h1 className="text-4xl font-bold font-headline text-primary">{toy.name}</h1>
<div className="text-lg text-foreground/90 leading-relaxed">
<p>{toy.description}</p>
</div>
<Separator />
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
<div className="flex items-center">
<UserCircle2 className="h-5 w-5 mr-2 text-accent" />
<div>
<span className="font-medium text-muted-foreground">{t('toy_details.owner')}: </span>
<span className="text-foreground">{toy.ownerName}</span>
</div>
</div>
{toy.location && (
<div className="flex items-center">
<MapPin className="h-5 w-5 mr-2 text-accent" />
<div>
<span className="font-medium text-muted-foreground">{t('toy_details.location')}: </span>
<span className="text-foreground">{toy.location}</span>
</div>
</div>
)}
{toy.pricePerDay !== undefined && (
<div className="flex items-center col-span-full sm:col-span-1">
<DollarSign className="h-5 w-5 mr-2 text-accent" />
<div>
<span className="font-medium text-muted-foreground">{t('toy_details.price')}: </span>
<span className="text-foreground font-semibold">
{toy.pricePerDay > 0 ? `$${toy.pricePerDay}${t('toy_details.price_per_day')}` : t('toy_details.price_free')}
</span>
</div>
</div>
)}
</div>
<Separator />
<AvailabilityCalendar availability={toy.availability} />
<Button size="lg" className="w-full mt-6 transition-transform transform hover:scale-105">
<ShoppingBag className="mr-2 h-5 w-5" /> {t('toy_details.request_to_rent')}
</Button>
</div>
</div>
</div>
);
}
export function generateStaticParams() {
const localeParams = getLocaleStaticParams(); // e.g. [{ locale: 'en' }, { locale: 'zh-TW' }]
const toyParams = mockToys.map((toy) => ({ id: toy.id }));
return localeParams.flatMap(lang =>
toyParams.map(toy => ({ ...lang, id: toy.id }))
);
}

View File

@ -2,37 +2,49 @@
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import { Home, ToyBrick, PlusCircle, ListOrdered, User, LogOut, Settings, ShoppingBag } from 'lucide-react';
import { Home, ToyBrick, PlusCircle, ListOrdered, Settings, ShoppingBag, LogOutIcon } from 'lucide-react'; // Changed LogOut to LogOutIcon
import { Button } from '@/components/ui/button';
import { cn } from '@/lib/utils';
import { Separator } from '@/components/ui/separator';
import { useToast } from "@/hooks/use-toast";
const sidebarNavItems = [
{ href: '/dashboard', label: 'Overview', icon: Home },
{ href: '/dashboard/my-toys', label: 'My Toys', icon: ToyBrick },
{ href: '/dashboard/my-toys/add', label: 'Add New Toy', icon: PlusCircle },
{ href: '/dashboard/rentals', label: 'My Rentals', icon: ShoppingBag }, // Toys I'm renting
{ href: '/dashboard/requests', label: 'Rental Requests', icon: ListOrdered }, // Requests for my toys
];
const accountNavItems = [
{ href: '/dashboard/profile', label: 'Profile Settings', icon: Settings },
];
import { useI18n, useCurrentLocale } from '@/locales/client';
export default function DashboardSidebar() {
const pathname = usePathname();
const router = useRouter();
const t = useI18n();
const locale = useCurrentLocale();
const { toast } = useToast();
const sidebarNavItems = [
{ href: '/dashboard', label: t('dashboard.sidebar.overview'), icon: Home },
{ href: '/dashboard/my-toys', label: t('dashboard.sidebar.my_toys'), icon: ToyBrick },
{ href: '/dashboard/my-toys/add', label: t('dashboard.sidebar.add_new_toy'), icon: PlusCircle },
{ href: '/dashboard/rentals', label: t('dashboard.sidebar.my_rentals'), icon: ShoppingBag },
{ href: '/dashboard/requests', label: t('dashboard.sidebar.rental_requests'), icon: ListOrdered },
];
const accountNavItems = [
{ href: '/dashboard/profile', label: t('dashboard.sidebar.profile_settings'), icon: Settings },
];
const handleLogout = () => {
localStorage.removeItem('isToyShareAuthenticated'); // Mock logout
toast({ description: "You have been logged out." });
router.push('/');
localStorage.removeItem('isToyShareAuthenticated');
toast({ description: "You have been logged out." }); // Translate if needed
router.push(`/${locale}/`); // Redirect to localized home page
};
const NavLink = ({ href, label, icon: Icon }: typeof sidebarNavItems[0]) => {
const isActive = pathname === href || (href !== '/dashboard' && pathname.startsWith(href));
const NavLink = ({ href, label, icon: Icon }: typeof sidebarNavItems[0] & {icon: React.ElementType}) => {
// For active link comparison, remove locale prefix from pathname
const cleanPathname = pathname.startsWith(`/${locale}`)
? pathname.substring(`/${locale}`.length) || '/'
: pathname;
const cleanHref = href.startsWith(`/${locale}`)
? href.substring(`/${locale}`.length) || '/'
: href;
const isActive = cleanPathname === cleanHref || (cleanHref !== '/dashboard' && cleanPathname.startsWith(cleanHref));
return (
<Link href={href} passHref>
<Button
@ -53,18 +65,18 @@ export default function DashboardSidebar() {
<ToyBrick className="h-7 w-7" />
<h2 className="text-xl font-headline font-bold">ToyShare</h2>
</Link>
<p className="text-xs text-muted-foreground">User Dashboard</p>
<p className="text-xs text-muted-foreground">{t('dashboard.sidebar.user_dashboard')}</p>
</div>
<nav className="flex-grow space-y-1">
<p className="px-3 py-2 text-xs font-medium text-muted-foreground">Toy Management</p>
<p className="px-3 py-2 text-xs font-medium text-muted-foreground">{t('dashboard.sidebar.toy_management')}</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">Account</p>
<p className="px-3 py-2 text-xs font-medium text-muted-foreground">{t('dashboard.sidebar.account')}</p>
{accountNavItems.map((item) => (
<NavLink key={item.href} {...item} />
))}
@ -74,8 +86,8 @@ export default function DashboardSidebar() {
<div>
<Button variant="ghost" className="w-full justify-start text-red-600 hover:bg-red-100 hover:text-red-700" onClick={handleLogout}>
<LogOut className="mr-3 h-5 w-5" />
Logout
<LogOutIcon className="mr-3 h-5 w-5" /> {/* Changed to LogOutIcon */}
{t('dashboard.sidebar.logout')}
</Button>
</div>
</aside>

View File

@ -1,12 +1,18 @@
export default function Footer() {
import { getCurrentLocale, getI18n } from '@/locales/server';
export default async function Footer() {
const year = new Date().getFullYear();
const t = await getI18n();
// const locale = getCurrentLocale(); // Example if needed
return (
<footer className="bg-muted/50 border-t border-border py-8 text-center text-muted-foreground">
<div className="container mx-auto px-4">
<p className="text-sm">
&copy; {new Date().getFullYear()} ToyShare. All rights reserved.
{t('footer.copy', { year })}
</p>
<p className="text-xs mt-1">
Sharing happiness, one toy at a time.
{t('footer.tagline')}
</p>
</div>
</footer>

View File

@ -14,22 +14,30 @@ import {
} from "@/components/ui/dropdown-menu";
import { useState, useEffect } from 'react';
import { ThemeToggleButton } from '@/components/ui/theme-toggle';
import LanguageSwitcher from './LanguageSwitcher';
import { useI18n, useCurrentLocale } from '@/locales/client';
export default function Header() {
const pathname = usePathname();
const t = useI18n();
const locale = useCurrentLocale();
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
const authStatus = localStorage.getItem('isToyShareAuthenticated');
setIsAuthenticated(authStatus === 'true');
}, [pathname]);
}, [pathname]); // Listen to pathname changes to re-check auth if needed after navigation
const handleLogout = () => {
localStorage.removeItem('isToyShareAuthenticated');
setIsAuthenticated(false);
// router.push('/'); // Consider redirecting if not already handled by auth checks elsewhere
// No need to router.push here, links will use current locale
};
// Helper to remove locale prefix for path comparison
const cleanPathname = pathname.startsWith(`/${locale}`)
? pathname.substring(`/${locale}`.length) || '/'
: pathname;
return (
<header className="bg-card border-b border-border shadow-sm sticky top-0 z-50">
@ -39,11 +47,11 @@ export default function Header() {
<h1 className="text-2xl font-headline font-bold">ToyShare</h1>
</Link>
<div className="flex items-center gap-2">
<nav className="flex items-center gap-1 sm:gap-2">
<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>
<Button variant={pathname === '/' ? 'secondary' : 'ghost'} size="sm" className="px-2 sm:px-3">
Browse Toys
<Button variant={cleanPathname === '/' ? 'secondary' : 'ghost'} size="sm" className="px-2 sm:px-3">
{t('header.browse_toys')}
</Button>
</Link>
{isAuthenticated ? (
@ -54,45 +62,47 @@ export default function Header() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuLabel>{t('header.my_account')}</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem asChild>
<Link href="/dashboard">
<LayoutDashboard className="mr-2 h-4 w-4" />
Dashboard
{t('header.dashboard')}
</Link>
</DropdownMenuItem>
<DropdownMenuItem asChild>
<Link href="/dashboard/profile">
<UserCircle2 className="mr-2 h-4 w-4" />
Profile
{t('header.profile')}
</Link>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleLogout}>
<LogIn className="mr-2 h-4 w-4" />
Logout
<LogIn className="mr-2 h-4 w-4" /> {/* Using LogIn icon for logout action for consistency */}
{t('header.logout')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
) : (
<>
<Link href="/login" passHref>
<Button variant={pathname === '/login' ? 'secondary' : 'ghost'} size="sm" className="px-2 sm:px-3">
<Button variant={cleanPathname === '/login' ? 'secondary' : 'ghost'} size="sm" className="px-2 sm:px-3">
<LogIn className="mr-2 h-4 w-4" />
Login
{t('header.login')}
</Button>
</Link>
<Link href="/register" passHref>
<Button variant="default" size="sm" className="px-2 sm:px-3">
<UserPlus className="mr-2 h-4 w-4" />
Register
{t('header.register')}
</Button>
</Link>
</>
)}
</nav>
<LanguageSwitcher />
<ThemeToggleButton />
{/* Mobile menu trigger could be added here if needed */}
</div>
</div>
</header>

View File

@ -0,0 +1,46 @@
'use client';
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useChangeLocale, useCurrentLocale, useI18n } from "@/locales/client";
import { Languages } from "lucide-react";
export default function LanguageSwitcher() {
const changeLocale = useChangeLocale();
const currentLocale = useCurrentLocale();
const t = useI18n();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon" className="h-9 w-9 md:h-10 md:w-10">
<Languages className="h-5 w-5" />
<span className="sr-only">{t('lang.select_language')}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>{t('lang.select_language')}</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => changeLocale('en')}
disabled={currentLocale === 'en'}
>
{t('lang.english')}
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => changeLocale('zh-TW')}
disabled={currentLocale === 'zh-TW'}
>
{t('lang.traditional_chinese')}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

View File

@ -10,28 +10,31 @@ import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox';
import { useToast } from '@/hooks/use-toast';
import { ToyBrick, UploadCloud, Save, PlusCircle, Trash2 } from 'lucide-react';
import { ToyBrick, Save, PlusCircle, Trash2 } from 'lucide-react';
import type { Toy } from '@/types';
import { useI18n, useCurrentLocale } from '@/locales/client';
const toyCategories = ["Educational", "Vehicles", "Electronics", "Plush Toys", "Musical", "Outdoor", "Board Games", "Action Figures", "Dolls", "Puzzles", "Arts & Crafts", "Building Blocks"];
const daysOfWeek = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] as const;
interface AddToyFormProps {
initialData?: Partial<Toy>; // For editing existing toys
initialData?: Partial<Toy>;
isEditMode?: boolean;
}
export default function AddToyForm({ initialData, isEditMode = false }: AddToyFormProps) {
const router = useRouter();
const { toast } = useToast();
const t = useI18n();
const locale = useCurrentLocale();
const [name, setName] = useState(initialData?.name || '');
const [description, setDescription] = useState(initialData?.description || '');
const [category, setCategory] = useState(initialData?.category || '');
const [pricePerDay, setPricePerDay] = useState(initialData?.pricePerDay?.toString() || '0');
const [location, setLocation] = useState(initialData?.location || '');
const [images, setImages] = useState<string[]>(initialData?.images || ['']); // Store image URLs
const [images, setImages] = useState<string[]>(initialData?.images || ['']);
const [availability, setAvailability] = useState<Toy['availability']>(
initialData?.availability || {
monday: true, tuesday: true, wednesday: true, thursday: true, friday: true, saturday: false, sunday: false
@ -50,7 +53,7 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
if (images.length > 1) {
setImages(images.filter((_, i) => i !== index));
} else {
setImages(['']); // Keep at least one field, but clear it
setImages(['']);
}
};
@ -62,9 +65,8 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
e.preventDefault();
setIsLoading(true);
// Form validation (basic example)
if (!name || !description || !category) {
toast({ title: "Missing Fields", description: "Please fill in all required fields.", variant: "destructive" });
toast({ title: "Missing Fields", description: "Please fill in all required fields.", variant: "destructive" }); // Translate
setIsLoading(false);
return;
}
@ -73,21 +75,18 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
name, description, category,
pricePerDay: parseFloat(pricePerDay) || 0,
location,
images: images.filter(img => img.trim() !== ''), // Filter out empty image URLs
images: images.filter(img => img.trim() !== ''),
availability,
// ownerId and id would be set by backend
};
// Mock API call
console.log("Submitting toy data:", toyData);
await new Promise(resolve => setTimeout(resolve, 1500));
toast({
title: isEditMode ? "Toy Updated!" : "Toy Added!",
description: `${name} has been successfully ${isEditMode ? 'updated' : 'listed'}.`,
title: isEditMode ? "Toy Updated!" : "Toy Added!", // Translate
description: `${name} has been successfully ${isEditMode ? 'updated' : 'listed'}.`, // Translate
});
router.push('/dashboard/my-toys'); // Redirect after success
// Optionally, could clear form or reset state here
router.push(`/${locale}/dashboard/my-toys`);
setIsLoading(false);
};
@ -97,31 +96,28 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
<div className="mx-auto bg-primary text-primary-foreground rounded-full p-3 w-fit mb-4">
<ToyBrick className="h-8 w-8" />
</div>
<CardTitle className="text-3xl font-headline">{isEditMode ? 'Edit Your Toy' : 'Share a New Toy'}</CardTitle>
<CardTitle className="text-3xl font-headline">{isEditMode ? t('add_toy_form.edit_title') : t('add_toy_form.add_title')}</CardTitle>
<CardDescription>
{isEditMode ? 'Update the details of your toy listing.' : 'Fill in the details below to list your toy for others to enjoy.'}
{isEditMode ? t('add_toy_form.edit_description') : t('add_toy_form.add_description')}
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-8">
{/* Toy Name */}
<div className="space-y-2">
<Label htmlFor="name" className="text-base">Toy Name</Label>
<Label htmlFor="name" className="text-base">{t('add_toy_form.name_label')}</Label>
<Input id="name" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g., Red Racing Car" required disabled={isLoading} />
</div>
{/* Description */}
<div className="space-y-2">
<Label htmlFor="description" className="text-base">Description</Label>
<Label htmlFor="description" className="text-base">{t('add_toy_form.description_label')}</Label>
<Textarea id="description" value={description} onChange={(e) => setDescription(e.target.value)} placeholder="Describe your toy, its condition, and any accessories." required disabled={isLoading} rows={4} />
</div>
{/* Category */}
<div className="space-y-2">
<Label htmlFor="category" className="text-base">Category</Label>
<Label htmlFor="category" className="text-base">{t('add_toy_form.category_label')}</Label>
<Select value={category} onValueChange={setCategory} required disabled={isLoading}>
<SelectTrigger id="category">
<SelectValue placeholder="Select a category" />
<SelectValue placeholder={t('add_toy_form.select_category_placeholder')} />
</SelectTrigger>
<SelectContent>
{toyCategories.map(cat => <SelectItem key={cat} value={cat}>{cat}</SelectItem>)}
@ -129,22 +125,19 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
</Select>
</div>
{/* Price per Day */}
<div className="space-y-2">
<Label htmlFor="pricePerDay" className="text-base">Rental Price per Day ($)</Label>
<Label htmlFor="pricePerDay" className="text-base">{t('add_toy_form.price_label')}</Label>
<Input id="pricePerDay" type="number" value={pricePerDay} onChange={(e) => setPricePerDay(e.target.value)} placeholder="0 for free" min="0" step="0.50" disabled={isLoading} />
</div>
{/* Location */}
<div className="space-y-2">
<Label htmlFor="location" className="text-base">Location (Optional)</Label>
<Label htmlFor="location" className="text-base">{t('add_toy_form.location_label')}</Label>
<Input id="location" value={location} onChange={(e) => setLocation(e.target.value)} placeholder="e.g., Springfield Park Area" disabled={isLoading} />
</div>
{/* Image URLs */}
<div className="space-y-4">
<Label className="text-base">Toy Images (URLs)</Label>
<p className="text-sm text-muted-foreground">Enter direct URLs to your toy images. Add up to 5 images.</p>
<Label className="text-base">{t('add_toy_form.images_label')}</Label>
<p className="text-sm text-muted-foreground">{t('add_toy_form.images_description')}</p>
{images.map((imgUrl, index) => (
<div key={index} className="flex items-center gap-2">
<Input
@ -163,14 +156,13 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
))}
{images.length < 5 && (
<Button type="button" variant="outline" onClick={addImageField} disabled={isLoading}>
<PlusCircle className="mr-2 h-4 w-4" /> Add Another Image
<PlusCircle className="mr-2 h-4 w-4" /> {t('add_toy_form.add_image_button')}
</Button>
)}
</div>
{/* Availability */}
<div className="space-y-3">
<Label className="text-base">Weekly Availability</Label>
<Label className="text-base">{t('add_toy_form.availability_label')}</Label>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
{daysOfWeek.map(day => (
<div key={day} className="flex items-center space-x-2">
@ -188,10 +180,10 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
<CardFooter className="p-0 pt-6">
<Button type="submit" className="w-full" size="lg" disabled={isLoading}>
{isLoading ? (isEditMode ? 'Saving Changes...' : 'Listing Toy...') : (
{isLoading ? (isEditMode ? t('add_toy_form.saving_button') : t('add_toy_form.listing_button')) : (
<>
<Save className="mr-2 h-5 w-5" />
{isEditMode ? 'Save Changes' : 'List My Toy'}
{isEditMode ? t('add_toy_form.save_button') : t('add_toy_form.list_button')}
</>
)}
</Button>
@ -201,16 +193,3 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
</Card>
);
}
// Icon for file upload, not used in URL version but good for future
function FileUploadIcon() {
return (
<div className="border-2 border-dashed border-border rounded-lg p-6 text-center cursor-pointer hover:border-primary transition-colors">
<UploadCloud className="mx-auto h-12 w-12 text-muted-foreground" />
<p className="mt-2 text-sm text-muted-foreground">
<span className="font-semibold text-primary">Click to upload</span> or drag and drop
</p>
<p className="text-xs text-muted-foreground">PNG, JPG, GIF up to 10MB</p>
</div>
);
}

14
src/locales/client.ts Normal file
View File

@ -0,0 +1,14 @@
'use client';
import { createI18nClient } from 'next-international/client';
export const {
useI18n,
useScopedI18n,
I18nProviderClient,
useChangeLocale,
useCurrentLocale
} = createI18nClient({
en: () => import('./en'),
'zh-TW': () => import('./zh-TW'),
});

119
src/locales/en.ts Normal file
View File

@ -0,0 +1,119 @@
export default {
'header.browse_toys': 'Browse Toys',
'header.login': 'Login',
'header.register': 'Register',
'header.my_account': 'My Account',
'header.dashboard': 'Dashboard',
'header.profile': 'Profile',
'header.logout': 'Logout',
'footer.copy': '© {year} ToyShare. All rights reserved.',
'footer.tagline': 'Sharing happiness, one toy at a time.',
'home.welcome': 'Welcome to ToyShare!',
'home.discover': 'Discover a world of fun! Share your beloved toys or find new adventures by renting from our friendly community.',
'home.explore_toys': 'Explore Toys',
'home.share_your_toy': 'Share Your Toy',
'home.available_toys': 'Available Toys',
'login.welcome_back': 'Welcome Back!',
'login.description': 'Log in to your ToyShare account to continue.',
'login.email_label': 'Email Address',
'login.password_label': 'Password',
'login.submit_button': 'Log In',
'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>",
'register.create_account': 'Create Your Account',
'register.description': 'Join ToyShare and start sharing the fun!',
'register.name_label': 'Full Name',
'register.confirm_password_label': 'Confirm Password',
'register.submit_button': 'Create Account',
'register.loading_button': 'Registering...',
'register.has_account': 'Already have an account? <link>Log in</link>',
'toy_details.back_to_toys': 'Back to All Toys',
'toy_details.toy_not_found_title': 'Toy Not Found',
'toy_details.toy_not_found_description': 'Sorry, the toy you are looking for does not exist or has been removed.',
'toy_details.owner': 'Owner',
'toy_details.location': 'Location',
'toy_details.price': 'Price',
'toy_details.price_free': 'Free',
'toy_details.price_per_day': '/day',
'toy_details.request_to_rent': 'Request to Rent',
'dashboard.layout.loading': 'Loading Dashboard...',
'dashboard.sidebar.user_dashboard': 'User Dashboard',
'dashboard.sidebar.toy_management': 'Toy Management',
'dashboard.sidebar.overview': 'Overview',
'dashboard.sidebar.my_toys': 'My Toys',
'dashboard.sidebar.add_new_toy': 'Add New Toy',
'dashboard.sidebar.my_rentals': 'My Rentals',
'dashboard.sidebar.rental_requests': 'Rental Requests',
'dashboard.sidebar.account': 'Account',
'dashboard.sidebar.profile_settings': 'Profile Settings',
'dashboard.sidebar.logout': 'Logout',
'dashboard.overview.welcome': 'Welcome to Your Dashboard!',
'dashboard.overview.description': 'Manage your toys, rentals, and account settings all in one place.',
'dashboard.overview.my_listed_toys': 'My Listed Toys',
'dashboard.overview.view_my_toys': 'View My Toys',
'dashboard.overview.toys_i_renting': "Toys I'm Renting",
'dashboard.overview.view_my_rentals': 'View My Rentals',
'dashboard.overview.pending_requests': 'Pending Requests',
'dashboard.overview.manage_requests': 'Manage Requests',
'dashboard.overview.quick_actions': 'Quick Actions',
'dashboard.overview.add_new_toy_button': 'Add a New Toy',
'dashboard.overview.update_profile_button': 'Update Profile',
'dashboard.overview.tips_for_sharers': 'Tips for Sharers',
'dashboard.overview.tip1': 'Take clear photos of your toys from multiple angles.',
'dashboard.overview.tip2': 'Write detailed and accurate descriptions.',
'dashboard.overview.tip3': 'Keep your availability calendar up-to-date.',
'dashboard.overview.tip4': 'Respond promptly to rental requests.',
'dashboard.my_toys.title': 'My Listed Toys',
'dashboard.my_toys.description': "Manage the toys you've shared with the community.",
'dashboard.my_toys.add_new_toy_button': 'Add New Toy',
'dashboard.my_toys.no_toys_title': 'No Toys Listed Yet',
'dashboard.my_toys.no_toys_description': 'Share your first toy and spread the joy!',
'dashboard.my_toys.add_first_toy_button': 'Add Your First Toy',
'dashboard.my_toys.view_toy_action': 'View Toy',
'dashboard.my_toys.edit_toy_action': 'Edit Toy',
'dashboard.my_toys.delete_toy_action': 'Delete Toy',
'dashboard.profile.title': 'Profile Settings',
'dashboard.profile.description': 'Manage your personal information and account settings.',
'dashboard.profile.personal_info_title': 'Personal Information',
'dashboard.profile.personal_info_description': 'Update your publicly visible profile information.',
'dashboard.profile.avatar_url_label': 'Avatar URL',
'dashboard.profile.full_name_label': 'Full Name',
'dashboard.profile.email_label': 'Email Address (Read-only)',
'dashboard.profile.bio_label': 'Bio',
'dashboard.profile.phone_label': 'Phone Number',
'dashboard.profile.location_label': 'Location',
'dashboard.profile.save_button': 'Save Profile Changes',
'dashboard.profile.saving_button': 'Saving Profile...',
'dashboard.profile.change_password_title': 'Change Password',
'dashboard.profile.change_password_description': 'Update your account password for security.',
'dashboard.profile.current_password_label': 'Current Password',
'dashboard.profile.new_password_label': 'New Password',
'dashboard.profile.confirm_new_password_label': 'Confirm New Password',
'dashboard.profile.update_password_button': 'Update Password',
'dashboard.profile.updating_password_button': 'Updating Password...',
'lang.english': 'English',
'lang.traditional_chinese': '繁體中文',
'lang.select_language': 'Select Language',
'general.back_to_my_toys': 'Back to My Toys',
'general.toy_not_found': 'Toy Not Found',
'general.cannot_edit_nonexistent_toy': 'Cannot edit a toy that does not exist.',
'add_toy_form.edit_title': 'Edit Your Toy',
'add_toy_form.add_title': 'Share a New Toy',
'add_toy_form.edit_description': 'Update the details of your toy listing.',
'add_toy_form.add_description': 'Fill in the details below to list your toy for others to enjoy.',
'add_toy_form.name_label': 'Toy Name',
'add_toy_form.description_label': 'Description',
'add_toy_form.category_label': 'Category',
'add_toy_form.select_category_placeholder': 'Select a category',
'add_toy_form.price_label': 'Rental Price per Day ($)',
'add_toy_form.location_label': 'Location (Optional)',
'add_toy_form.images_label': 'Toy Images (URLs)',
'add_toy_form.images_description': 'Enter direct URLs to your toy images. Add up to 5 images.',
'add_toy_form.add_image_button': 'Add Another Image',
'add_toy_form.availability_label': 'Weekly Availability',
'add_toy_form.save_button': 'Save Changes',
'add_toy_form.list_button': 'List My Toy',
'add_toy_form.saving_button': 'Saving Changes...',
'add_toy_form.listing_button': 'Listing Toy...',
} as const;

11
src/locales/server.ts Normal file
View File

@ -0,0 +1,11 @@
import { createI18nServer } from 'next-international/server';
export const {
getI18n,
getScopedI18n,
getCurrentLocale,
getStaticParams
} = createI18nServer({
en: () => import('./en'),
'zh-TW': () => import('./zh-TW'),
});

119
src/locales/zh-TW.ts Normal file
View File

@ -0,0 +1,119 @@
export default {
'header.browse_toys': '瀏覽玩具',
'header.login': '登入',
'header.register': '註冊',
'header.my_account': '我的帳戶',
'header.dashboard': '儀表板',
'header.profile': '個人資料',
'header.logout': '登出',
'footer.copy': '© {year} ToyShare. 版權所有。',
'footer.tagline': '分享快樂,從玩具開始。',
'home.welcome': '歡迎來到 ToyShare',
'home.discover': '發現充滿樂趣的世界!分享您心愛的玩具,或從我們友善的社群租借玩具,開啟新的冒險旅程。',
'home.explore_toys': '探索玩具',
'home.share_your_toy': '分享您的玩具',
'home.available_toys': '現有玩具',
'login.welcome_back': '歡迎回來!',
'login.description': '登入您的 ToyShare 帳戶以繼續。',
'login.email_label': '電子郵件地址',
'login.password_label': '密碼',
'login.submit_button': '登入',
'login.loading_button': '登入中...',
'login.forgot_password': '忘記密碼? <link>在此重設</link>。',
'login.no_account': '還沒有帳戶? <link>立即註冊</link>',
'register.create_account': '建立您的帳戶',
'register.description': '加入 ToyShare開始分享樂趣',
'register.name_label': '全名',
'register.confirm_password_label': '確認密碼',
'register.submit_button': '建立帳戶',
'register.loading_button': '註冊中...',
'register.has_account': '已經有帳戶了? <link>登入</link>',
'toy_details.back_to_toys': '返回所有玩具',
'toy_details.toy_not_found_title': '找不到玩具',
'toy_details.toy_not_found_description': '抱歉,您尋找的玩具不存在或已被移除。',
'toy_details.owner': '擁有者',
'toy_details.location': '地點',
'toy_details.price': '價格',
'toy_details.price_free': '免費',
'toy_details.price_per_day': '/天',
'toy_details.request_to_rent': '請求租借',
'dashboard.layout.loading': '正在載入儀表板...',
'dashboard.sidebar.user_dashboard': '使用者儀表板',
'dashboard.sidebar.toy_management': '玩具管理',
'dashboard.sidebar.overview': '總覽',
'dashboard.sidebar.my_toys': '我的玩具',
'dashboard.sidebar.add_new_toy': '新增玩具',
'dashboard.sidebar.my_rentals': '我的租借',
'dashboard.sidebar.rental_requests': '租借請求',
'dashboard.sidebar.account': '帳戶',
'dashboard.sidebar.profile_settings': '個人資料設定',
'dashboard.sidebar.logout': '登出',
'dashboard.overview.welcome': '歡迎來到您的儀表板!',
'dashboard.overview.description': '集中管理您的玩具、租借和帳戶設定。',
'dashboard.overview.my_listed_toys': '我列出的玩具',
'dashboard.overview.view_my_toys': '查看我的玩具',
'dashboard.overview.toys_i_renting': '我正在租借的玩具',
'dashboard.overview.view_my_rentals': '查看我的租借',
'dashboard.overview.pending_requests': '待處理請求',
'dashboard.overview.manage_requests': '管理請求',
'dashboard.overview.quick_actions': '快速操作',
'dashboard.overview.add_new_toy_button': '新增玩具',
'dashboard.overview.update_profile_button': '更新個人資料',
'dashboard.overview.tips_for_sharers': '給分享者的提示',
'dashboard.overview.tip1': '從多個角度拍攝玩具的清晰照片。',
'dashboard.overview.tip2': '撰寫詳細且準確的描述。',
'dashboard.overview.tip3': '保持您的可用性日曆最新。',
'dashboard.overview.tip4': '及時回應租借請求。',
'dashboard.my_toys.title': '我列出的玩具',
'dashboard.my_toys.description': '管理您與社群分享的玩具。',
'dashboard.my_toys.add_new_toy_button': '新增玩具',
'dashboard.my_toys.no_toys_title': '尚無列出玩具',
'dashboard.my_toys.no_toys_description': '分享您的第一個玩具,傳播歡樂!',
'dashboard.my_toys.add_first_toy_button': '新增您的第一個玩具',
'dashboard.my_toys.view_toy_action': '查看玩具',
'dashboard.my_toys.edit_toy_action': '編輯玩具',
'dashboard.my_toys.delete_toy_action': '刪除玩具',
'dashboard.profile.title': '個人資料設定',
'dashboard.profile.description': '管理您的個人資訊和帳戶設定。',
'dashboard.profile.personal_info_title': '個人資訊',
'dashboard.profile.personal_info_description': '更新您公開顯示的個人資料資訊。',
'dashboard.profile.avatar_url_label': '頭像 URL',
'dashboard.profile.full_name_label': '全名',
'dashboard.profile.email_label': '電子郵件地址 (唯讀)',
'dashboard.profile.bio_label': '簡介',
'dashboard.profile.phone_label': '電話號碼',
'dashboard.profile.location_label': '地點',
'dashboard.profile.save_button': '儲存個人資料變更',
'dashboard.profile.saving_button': '儲存個人資料中...',
'dashboard.profile.change_password_title': '更改密碼',
'dashboard.profile.change_password_description': '為了安全,更新您的帳戶密碼。',
'dashboard.profile.current_password_label': '目前密碼',
'dashboard.profile.new_password_label': '新密碼',
'dashboard.profile.confirm_new_password_label': '確認新密碼',
'dashboard.profile.update_password_button': '更新密碼',
'dashboard.profile.updating_password_button': '更新密碼中...',
'lang.english': 'English',
'lang.traditional_chinese': '繁體中文',
'lang.select_language': '選擇語言',
'general.back_to_my_toys': '返回我的玩具',
'general.toy_not_found': '找不到玩具',
'general.cannot_edit_nonexistent_toy': '無法編輯不存在的玩具。',
'add_toy_form.edit_title': '編輯您的玩具',
'add_toy_form.add_title': '分享一個新玩具',
'add_toy_form.edit_description': '更新您玩具列表的詳細資訊。',
'add_toy_form.add_description': '填寫以下詳細資訊,將您的玩具列出供他人享用。',
'add_toy_form.name_label': '玩具名稱',
'add_toy_form.description_label': '描述',
'add_toy_form.category_label': '類別',
'add_toy_form.select_category_placeholder': '選擇一個類別',
'add_toy_form.price_label': '每日租金 ($)',
'add_toy_form.location_label': '地點 (選填)',
'add_toy_form.images_label': '玩具圖片 (URL)',
'add_toy_form.images_description': '輸入玩具圖片的直接 URL。最多可新增 5 張圖片。',
'add_toy_form.add_image_button': '新增另一張圖片',
'add_toy_form.availability_label': '每週可用時間',
'add_toy_form.save_button': '儲存變更',
'add_toy_form.list_button': '列出我的玩具',
'add_toy_form.saving_button': '儲存變更中...',
'add_toy_form.listing_button': '列出玩具中...',
} as const;

18
src/middleware.ts Normal file
View File

@ -0,0 +1,18 @@
import type { NextRequest } from 'next/server';
import { createI18nMiddleware } from 'next-international/middleware';
const I18nMiddleware = createI18nMiddleware({
locales: ['en', 'zh-TW'],
defaultLocale: 'en',
urlMappingStrategy: 'rewrite', // Or 'redirect', depending on preference
// prefix: 'as-needed' // default is 'as-needed'
});
export function middleware(request: NextRequest) {
return I18nMiddleware(request);
}
export const config = {
// Matcher ignoring `/_next/` and `/api/`
matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
};