diff --git a/package-lock.json b/package-lock.json index 30d1f84..50deb3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,7 @@ "genkit": "^1.8.0", "lucide-react": "^0.475.0", "next": "15.3.3", + "next-international": "^1.3.0", "next-themes": "^0.3.0", "patch-package": "^8.0.0", "react": "^18.3.1", @@ -6935,6 +6936,12 @@ "node": ">=8" } }, + "node_modules/international-types": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/international-types/-/international-types-0.8.1.tgz", + "integrity": "sha512-tajBCAHo4I0LIFlmQ9ZWfjMWVyRffzuvfbXCd6ssFt5u1Zw15DN0UBpVTItXdNa1ls+cpQt3Yw8+TxsfGF8JcA==", + "license": "MIT" + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -7654,6 +7661,17 @@ } } }, + "node_modules/next-international": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/next-international/-/next-international-1.3.1.tgz", + "integrity": "sha512-ydU9jQe+4MohMWltbZae/yuWeKhmp0QKQqJNNi8WCCMwrly03qfMAHw/tWbT2qgAlG++CxF5jMXmGQZgOHeVOw==", + "license": "MIT", + "dependencies": { + "client-only": "^0.0.1", + "international-types": "^0.8.1", + "server-only": "^0.0.1" + } + }, "node_modules/next-themes": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.3.0.tgz", @@ -8902,6 +8920,12 @@ "node": ">= 0.8.0" } }, + "node_modules/server-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/server-only/-/server-only-0.0.1.tgz", + "integrity": "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", diff --git a/package.json b/package.json index 509cf85..7e18003 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "lucide-react": "^0.475.0", "next": "15.3.3", "next-themes": "^0.3.0", + "next-international": "^1.3.0", "patch-package": "^8.0.0", "react": "^18.3.1", "react-day-picker": "^8.10.1", diff --git a/src/app/[locale]/dashboard/layout.tsx b/src/app/[locale]/dashboard/layout.tsx new file mode 100644 index 0000000..7b47aaf --- /dev/null +++ b/src/app/[locale]/dashboard/layout.tsx @@ -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 ( +
+ +

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

+
+ ); + } + + return ( +
+ +
+ {children} +
+
+ ); +} diff --git a/src/app/[locale]/dashboard/my-toys/add/page.tsx b/src/app/[locale]/dashboard/my-toys/add/page.tsx new file mode 100644 index 0000000..900b3ed --- /dev/null +++ b/src/app/[locale]/dashboard/my-toys/add/page.tsx @@ -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 ( +
+ +
+ ); +} diff --git a/src/app/[locale]/dashboard/my-toys/edit/[id]/page.tsx b/src/app/[locale]/dashboard/my-toys/edit/[id]/page.tsx new file mode 100644 index 0000000..dab59f1 --- /dev/null +++ b/src/app/[locale]/dashboard/my-toys/edit/[id]/page.tsx @@ -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 | 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 ( +
+

{t('general.toy_not_found')}

+

{t('general.cannot_edit_nonexistent_toy')}

+ + + +
+ ); + } + + return ( +
+ + + {t('general.back_to_my_toys')} + + {/* AddToyForm is a client component and will handle its own translations via useI18n */} + +
+ ); +} + +// 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 })) + ); +} diff --git a/src/app/[locale]/dashboard/my-toys/page.tsx b/src/app/[locale]/dashboard/my-toys/page.tsx new file mode 100644 index 0000000..339ff4a --- /dev/null +++ b/src/app/[locale]/dashboard/my-toys/page.tsx @@ -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 ( +
+
+
+

{t('dashboard.my_toys.title')}

+

{t('dashboard.my_toys.description')}

+
+ + + +
+ + {userToys.length === 0 ? ( + + + + {t('dashboard.my_toys.no_toys_title')} + {t('dashboard.my_toys.no_toys_description')} + + + + + + + + ) : ( +
+ {userToys.map(toy => ( + + ))} +
+ )} +
+ ); +} + +interface ListedToyItemProps { + toy: Toy & {dataAiHint?: string}; + t: (key: string, params?: Record) => 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 ( + +
+
+ {toy.name} +
+
+
+
+ {toy.name} + {toy.category} +
+
+ + + + + + + +
+
+

{toy.description}

+
+ {t('toy_details.price')}: + {toy.pricePerDay !== undefined ? (toy.pricePerDay > 0 ? `$${toy.pricePerDay}${t('toy_details.price_per_day')}` : t('toy_details.price_free')) : 'Not set'} +
+
+
+
+ ); +} diff --git a/src/app/[locale]/dashboard/page.tsx b/src/app/[locale]/dashboard/page.tsx new file mode 100644 index 0000000..6644332 --- /dev/null +++ b/src/app/[locale]/dashboard/page.tsx @@ -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 ( +
+ + + {t('dashboard.overview.welcome')} + {t('dashboard.overview.description')} + + + } + actionLink="/dashboard/my-toys" + actionLabel={t('dashboard.overview.view_my_toys')} + /> + } + actionLink="/dashboard/rentals" + actionLabel={t('dashboard.overview.view_my_rentals')} + /> + } + actionLink="/dashboard/requests" + actionLabel={t('dashboard.overview.manage_requests')} + /> + + + +
+ + + {t('dashboard.overview.quick_actions')} + + + + + + + + + + + + + + {t('dashboard.overview.tips_for_sharers')} + + +
    +
  • {t('dashboard.overview.tip1')}
  • +
  • {t('dashboard.overview.tip2')}
  • +
  • {t('dashboard.overview.tip3')}
  • +
  • {t('dashboard.overview.tip4')}
  • +
+
+
+
+
+ ); +} + +interface DashboardStatCardProps { + title: string; + value: string; + icon: React.ReactNode; + actionLink: string; + actionLabel: string; +} + +function DashboardStatCard({ title, value, icon, actionLink, actionLabel }: DashboardStatCardProps) { + return ( + + + {title} + {icon} + + +
{value}
+ + {actionLabel} → + +
+
+ ); +} diff --git a/src/app/[locale]/dashboard/profile/page.tsx b/src/app/[locale]/dashboard/profile/page.tsx new file mode 100644 index 0000000..0e43ff1 --- /dev/null +++ b/src/app/[locale]/dashboard/profile/page.tsx @@ -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) => { + 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) => { + 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 ( +
+
+

{t('dashboard.profile.title')}

+

{t('dashboard.profile.description')}

+
+ + + + {t('dashboard.profile.personal_info_title')} + {t('dashboard.profile.personal_info_description')} + +
+ +
+ + + {name.split(' ').map(n => n[0]).join('').toUpperCase()} + +
+ + setAvatarUrl(e.target.value)} placeholder="https://example.com/avatar.png" disabled={isLoading} /> +
+
+ +
+
+ + setName(e.target.value)} placeholder="Your Name" disabled={isLoading} /> +
+
+ + +
+
+ +
+ +