I see this error with the app, reported by NextJS, please fix it. The er
This commit is contained in:
parent
128334e790
commit
28430f236e
|
|
@ -39,6 +39,7 @@
|
||||||
"genkit": "^1.8.0",
|
"genkit": "^1.8.0",
|
||||||
"lucide-react": "^0.475.0",
|
"lucide-react": "^0.475.0",
|
||||||
"next": "15.3.3",
|
"next": "15.3.3",
|
||||||
|
"next-international": "^1.3.0",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
|
@ -6935,6 +6936,12 @@
|
||||||
"node": ">=8"
|
"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": {
|
"node_modules/internmap": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
"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": {
|
"node_modules/next-themes": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz",
|
||||||
|
|
@ -8902,6 +8920,12 @@
|
||||||
"node": ">= 0.8.0"
|
"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": {
|
"node_modules/set-function-length": {
|
||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||||
|
|
|
||||||
|
|
@ -44,6 +44,7 @@
|
||||||
"lucide-react": "^0.475.0",
|
"lucide-react": "^0.475.0",
|
||||||
"next": "15.3.3",
|
"next": "15.3.3",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
|
"next-international": "^1.3.0",
|
||||||
"patch-package": "^8.0.0",
|
"patch-package": "^8.0.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-day-picker": "^8.10.1",
|
"react-day-picker": "^8.10.1",
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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 }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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} →
|
||||||
|
</Link>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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" />;
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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 }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -2,37 +2,49 @@
|
||||||
|
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
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 { Button } from '@/components/ui/button';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { useToast } from "@/hooks/use-toast";
|
import { useToast } from "@/hooks/use-toast";
|
||||||
|
import { useI18n, useCurrentLocale } from '@/locales/client';
|
||||||
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 },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function DashboardSidebar() {
|
export default function DashboardSidebar() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const t = useI18n();
|
||||||
|
const locale = useCurrentLocale();
|
||||||
const { toast } = useToast();
|
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 = () => {
|
const handleLogout = () => {
|
||||||
localStorage.removeItem('isToyShareAuthenticated'); // Mock logout
|
localStorage.removeItem('isToyShareAuthenticated');
|
||||||
toast({ description: "You have been logged out." });
|
toast({ description: "You have been logged out." }); // Translate if needed
|
||||||
router.push('/');
|
router.push(`/${locale}/`); // Redirect to localized home page
|
||||||
};
|
};
|
||||||
|
|
||||||
const NavLink = ({ href, label, icon: Icon }: typeof sidebarNavItems[0]) => {
|
const NavLink = ({ href, label, icon: Icon }: typeof sidebarNavItems[0] & {icon: React.ElementType}) => {
|
||||||
const isActive = pathname === href || (href !== '/dashboard' && pathname.startsWith(href));
|
// 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 (
|
return (
|
||||||
<Link href={href} passHref>
|
<Link href={href} passHref>
|
||||||
<Button
|
<Button
|
||||||
|
|
@ -53,18 +65,18 @@ export default function DashboardSidebar() {
|
||||||
<ToyBrick className="h-7 w-7" />
|
<ToyBrick className="h-7 w-7" />
|
||||||
<h2 className="text-xl font-headline font-bold">ToyShare</h2>
|
<h2 className="text-xl font-headline font-bold">ToyShare</h2>
|
||||||
</Link>
|
</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>
|
</div>
|
||||||
|
|
||||||
<nav className="flex-grow space-y-1">
|
<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) => (
|
{sidebarNavItems.map((item) => (
|
||||||
<NavLink key={item.href} {...item} />
|
<NavLink key={item.href} {...item} />
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<Separator className="my-4" />
|
<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) => (
|
{accountNavItems.map((item) => (
|
||||||
<NavLink key={item.href} {...item} />
|
<NavLink key={item.href} {...item} />
|
||||||
))}
|
))}
|
||||||
|
|
@ -74,8 +86,8 @@ export default function DashboardSidebar() {
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button variant="ghost" className="w-full justify-start text-red-600 hover:bg-red-100 hover:text-red-700" onClick={handleLogout}>
|
<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" />
|
<LogOutIcon className="mr-3 h-5 w-5" /> {/* Changed to LogOutIcon */}
|
||||||
Logout
|
{t('dashboard.sidebar.logout')}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
return (
|
||||||
<footer className="bg-muted/50 border-t border-border py-8 text-center text-muted-foreground">
|
<footer className="bg-muted/50 border-t border-border py-8 text-center text-muted-foreground">
|
||||||
<div className="container mx-auto px-4">
|
<div className="container mx-auto px-4">
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
© {new Date().getFullYear()} ToyShare. All rights reserved.
|
{t('footer.copy', { year })}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs mt-1">
|
<p className="text-xs mt-1">
|
||||||
Sharing happiness, one toy at a time.
|
{t('footer.tagline')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
||||||
|
|
@ -14,22 +14,30 @@ import {
|
||||||
} from "@/components/ui/dropdown-menu";
|
} from "@/components/ui/dropdown-menu";
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { ThemeToggleButton } from '@/components/ui/theme-toggle';
|
import { ThemeToggleButton } from '@/components/ui/theme-toggle';
|
||||||
|
import LanguageSwitcher from './LanguageSwitcher';
|
||||||
|
import { useI18n, useCurrentLocale } from '@/locales/client';
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
const t = useI18n();
|
||||||
|
const locale = useCurrentLocale();
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const authStatus = localStorage.getItem('isToyShareAuthenticated');
|
const authStatus = localStorage.getItem('isToyShareAuthenticated');
|
||||||
setIsAuthenticated(authStatus === 'true');
|
setIsAuthenticated(authStatus === 'true');
|
||||||
}, [pathname]);
|
}, [pathname]); // Listen to pathname changes to re-check auth if needed after navigation
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
localStorage.removeItem('isToyShareAuthenticated');
|
localStorage.removeItem('isToyShareAuthenticated');
|
||||||
setIsAuthenticated(false);
|
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 (
|
return (
|
||||||
<header className="bg-card border-b border-border shadow-sm sticky top-0 z-50">
|
<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>
|
<h1 className="text-2xl font-headline font-bold">ToyShare</h1>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-1 sm:gap-2">
|
||||||
<nav 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="/" passHref>
|
||||||
<Button variant={pathname === '/' ? 'secondary' : 'ghost'} size="sm" className="px-2 sm:px-3">
|
<Button variant={cleanPathname === '/' ? 'secondary' : 'ghost'} size="sm" className="px-2 sm:px-3">
|
||||||
Browse Toys
|
{t('header.browse_toys')}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
{isAuthenticated ? (
|
{isAuthenticated ? (
|
||||||
|
|
@ -54,45 +62,47 @@ export default function Header() {
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
<DropdownMenuLabel>{t('header.my_account')}</DropdownMenuLabel>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link href="/dashboard">
|
<Link href="/dashboard">
|
||||||
<LayoutDashboard className="mr-2 h-4 w-4" />
|
<LayoutDashboard className="mr-2 h-4 w-4" />
|
||||||
Dashboard
|
{t('header.dashboard')}
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem asChild>
|
<DropdownMenuItem asChild>
|
||||||
<Link href="/dashboard/profile">
|
<Link href="/dashboard/profile">
|
||||||
<UserCircle2 className="mr-2 h-4 w-4" />
|
<UserCircle2 className="mr-2 h-4 w-4" />
|
||||||
Profile
|
{t('header.profile')}
|
||||||
</Link>
|
</Link>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem onClick={handleLogout}>
|
<DropdownMenuItem onClick={handleLogout}>
|
||||||
<LogIn className="mr-2 h-4 w-4" />
|
<LogIn className="mr-2 h-4 w-4" /> {/* Using LogIn icon for logout action for consistency */}
|
||||||
Logout
|
{t('header.logout')}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Link href="/login" passHref>
|
<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 className="mr-2 h-4 w-4" />
|
||||||
Login
|
{t('header.login')}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/register" passHref>
|
<Link href="/register" passHref>
|
||||||
<Button variant="default" size="sm" className="px-2 sm:px-3">
|
<Button variant="default" size="sm" className="px-2 sm:px-3">
|
||||||
<UserPlus className="mr-2 h-4 w-4" />
|
<UserPlus className="mr-2 h-4 w-4" />
|
||||||
Register
|
{t('header.register')}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</nav>
|
</nav>
|
||||||
|
<LanguageSwitcher />
|
||||||
<ThemeToggleButton />
|
<ThemeToggleButton />
|
||||||
|
{/* Mobile menu trigger could be added here if needed */}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -10,28 +10,31 @@ import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
import { Checkbox } from '@/components/ui/checkbox';
|
||||||
import { useToast } from '@/hooks/use-toast';
|
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 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 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;
|
const daysOfWeek = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] as const;
|
||||||
|
|
||||||
|
|
||||||
interface AddToyFormProps {
|
interface AddToyFormProps {
|
||||||
initialData?: Partial<Toy>; // For editing existing toys
|
initialData?: Partial<Toy>;
|
||||||
isEditMode?: boolean;
|
isEditMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AddToyForm({ initialData, isEditMode = false }: AddToyFormProps) {
|
export default function AddToyForm({ initialData, isEditMode = false }: AddToyFormProps) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const t = useI18n();
|
||||||
|
const locale = useCurrentLocale();
|
||||||
|
|
||||||
const [name, setName] = useState(initialData?.name || '');
|
const [name, setName] = useState(initialData?.name || '');
|
||||||
const [description, setDescription] = useState(initialData?.description || '');
|
const [description, setDescription] = useState(initialData?.description || '');
|
||||||
const [category, setCategory] = useState(initialData?.category || '');
|
const [category, setCategory] = useState(initialData?.category || '');
|
||||||
const [pricePerDay, setPricePerDay] = useState(initialData?.pricePerDay?.toString() || '0');
|
const [pricePerDay, setPricePerDay] = useState(initialData?.pricePerDay?.toString() || '0');
|
||||||
const [location, setLocation] = useState(initialData?.location || '');
|
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']>(
|
const [availability, setAvailability] = useState<Toy['availability']>(
|
||||||
initialData?.availability || {
|
initialData?.availability || {
|
||||||
monday: true, tuesday: true, wednesday: true, thursday: true, friday: true, saturday: false, sunday: false
|
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) {
|
if (images.length > 1) {
|
||||||
setImages(images.filter((_, i) => i !== index));
|
setImages(images.filter((_, i) => i !== index));
|
||||||
} else {
|
} 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();
|
e.preventDefault();
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
// Form validation (basic example)
|
|
||||||
if (!name || !description || !category) {
|
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);
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -73,21 +75,18 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
|
||||||
name, description, category,
|
name, description, category,
|
||||||
pricePerDay: parseFloat(pricePerDay) || 0,
|
pricePerDay: parseFloat(pricePerDay) || 0,
|
||||||
location,
|
location,
|
||||||
images: images.filter(img => img.trim() !== ''), // Filter out empty image URLs
|
images: images.filter(img => img.trim() !== ''),
|
||||||
availability,
|
availability,
|
||||||
// ownerId and id would be set by backend
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Mock API call
|
|
||||||
console.log("Submitting toy data:", toyData);
|
console.log("Submitting toy data:", toyData);
|
||||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: isEditMode ? "Toy Updated!" : "Toy Added!",
|
title: isEditMode ? "Toy Updated!" : "Toy Added!", // Translate
|
||||||
description: `${name} has been successfully ${isEditMode ? 'updated' : 'listed'}.`,
|
description: `${name} has been successfully ${isEditMode ? 'updated' : 'listed'}.`, // Translate
|
||||||
});
|
});
|
||||||
router.push('/dashboard/my-toys'); // Redirect after success
|
router.push(`/${locale}/dashboard/my-toys`);
|
||||||
// Optionally, could clear form or reset state here
|
|
||||||
setIsLoading(false);
|
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">
|
<div className="mx-auto bg-primary text-primary-foreground rounded-full p-3 w-fit mb-4">
|
||||||
<ToyBrick className="h-8 w-8" />
|
<ToyBrick className="h-8 w-8" />
|
||||||
</div>
|
</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>
|
<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>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form onSubmit={handleSubmit} className="space-y-8">
|
<form onSubmit={handleSubmit} className="space-y-8">
|
||||||
{/* Toy Name */}
|
|
||||||
<div className="space-y-2">
|
<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} />
|
<Input id="name" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g., Red Racing Car" required disabled={isLoading} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Description */}
|
|
||||||
<div className="space-y-2">
|
<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} />
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* Category */}
|
|
||||||
<div className="space-y-2">
|
<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}>
|
<Select value={category} onValueChange={setCategory} required disabled={isLoading}>
|
||||||
<SelectTrigger id="category">
|
<SelectTrigger id="category">
|
||||||
<SelectValue placeholder="Select a category" />
|
<SelectValue placeholder={t('add_toy_form.select_category_placeholder')} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{toyCategories.map(cat => <SelectItem key={cat} value={cat}>{cat}</SelectItem>)}
|
{toyCategories.map(cat => <SelectItem key={cat} value={cat}>{cat}</SelectItem>)}
|
||||||
|
|
@ -129,22 +125,19 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Price per Day */}
|
|
||||||
<div className="space-y-2">
|
<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} />
|
<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>
|
</div>
|
||||||
|
|
||||||
{/* Location */}
|
|
||||||
<div className="space-y-2">
|
<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} />
|
<Input id="location" value={location} onChange={(e) => setLocation(e.target.value)} placeholder="e.g., Springfield Park Area" disabled={isLoading} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Image URLs */}
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Label className="text-base">Toy Images (URLs)</Label>
|
<Label className="text-base">{t('add_toy_form.images_label')}</Label>
|
||||||
<p className="text-sm text-muted-foreground">Enter direct URLs to your toy images. Add up to 5 images.</p>
|
<p className="text-sm text-muted-foreground">{t('add_toy_form.images_description')}</p>
|
||||||
{images.map((imgUrl, index) => (
|
{images.map((imgUrl, index) => (
|
||||||
<div key={index} className="flex items-center gap-2">
|
<div key={index} className="flex items-center gap-2">
|
||||||
<Input
|
<Input
|
||||||
|
|
@ -163,14 +156,13 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
|
||||||
))}
|
))}
|
||||||
{images.length < 5 && (
|
{images.length < 5 && (
|
||||||
<Button type="button" variant="outline" onClick={addImageField} disabled={isLoading}>
|
<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>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Availability */}
|
|
||||||
<div className="space-y-3">
|
<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">
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
|
||||||
{daysOfWeek.map(day => (
|
{daysOfWeek.map(day => (
|
||||||
<div key={day} className="flex items-center space-x-2">
|
<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">
|
<CardFooter className="p-0 pt-6">
|
||||||
<Button type="submit" className="w-full" size="lg" disabled={isLoading}>
|
<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" />
|
<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>
|
</Button>
|
||||||
|
|
@ -201,16 +193,3 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
|
||||||
</Card>
|
</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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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'),
|
||||||
|
});
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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'),
|
||||||
|
});
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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).*)'],
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue