add toy rental history in user dashboard

This commit is contained in:
Indigo Tang 2025-06-09 04:38:59 +00:00
parent 9d8c9856e0
commit 8ba7f4cbed
6 changed files with 208 additions and 19 deletions

View File

@ -0,0 +1,115 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { History, ToyBrick } from "lucide-react";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import Image from "next/image";
import type { RentalHistoryEntry } from "@/types";
import { mockRentalHistory } from "@/lib/mockData";
import { getI18n } from "@/locales/server";
import type { Locale } from "@/locales/server";
import { Badge } from "@/components/ui/badge";
import { format } from 'date-fns';
const currentUserId = 'user1'; // Assume this is the logged-in user's ID
export default async function RentalHistoryPage({ params }: { params: { locale: Locale } }) {
const t = await getI18n();
const locale = params.locale;
// Filter rental history for the current user
const userRentalHistory = mockRentalHistory.filter(entry => entry.userId === currentUserId);
return (
<div className="space-y-8">
<div>
<h1 className="text-3xl font-bold font-headline text-primary">{t('dashboard.rental_history.title')}</h1>
<p className="text-muted-foreground">{t('dashboard.rental_history.description')}</p>
</div>
{userRentalHistory.length === 0 ? (
<Card className="text-center py-12 shadow-md">
<CardHeader>
<History className="h-16 w-16 mx-auto text-muted-foreground mb-4" />
<CardTitle>{t('dashboard.rental_history.no_history_title')}</CardTitle>
<CardDescription>{t('dashboard.rental_history.no_history_description')}</CardDescription>
</CardHeader>
<CardContent>
<Link href={`/${locale}/`} passHref>
<Button size="lg">
<ToyBrick className="mr-2 h-5 w-5" />
{t('dashboard.rental_history.browse_toys_button')}
</Button>
</Link>
</CardContent>
</Card>
) : (
<div className="space-y-6">
{userRentalHistory.map(entry => (
<RentalHistoryItemCard key={entry.id} item={entry} t={t} locale={locale} />
))}
</div>
)}
</div>
);
}
interface RentalHistoryItemCardProps {
item: RentalHistoryEntry;
t: (key: string, params?: Record<string, string | number | Date>) => string;
locale: Locale;
}
function RentalHistoryItemCard({ item, t, locale }: RentalHistoryItemCardProps) {
const placeholderHint = item.dataAiHint || item.toy.category.toLowerCase() || "toy";
const formattedStartDate = format(new Date(item.rentalStartDate), 'PP');
const formattedEndDate = format(new Date(item.rentalEndDate), 'PP');
const getStatusTextKey = (status: RentalHistoryEntry['status']) => {
if (status === 'Completed') return 'rental_history_card.status_completed';
if (status === 'Returned') return 'rental_history_card.status_returned';
return '';
}
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/4 lg:w-1/5 relative aspect-video md:aspect-[4/3]">
<Image
src={item.toy.images[0] || 'https://placehold.co/200x150.png'}
alt={item.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 mb-2">
<CardTitle className="text-xl font-headline">{item.toy.name}</CardTitle>
<Badge variant={item.status === 'Completed' ? 'default' : 'secondary'}>{t(getStatusTextKey(item.status))}</Badge>
</div>
<p className="text-sm text-muted-foreground">
{t('rental_history_card.rented_from')}: <span className="font-medium text-foreground">{item.toy.ownerName}</span>
</p>
<p className="text-sm text-muted-foreground">
{t('rental_history_card.rental_period')}: <span className="font-medium text-foreground">{formattedStartDate} - {formattedEndDate}</span>
</p>
<p className="text-sm text-muted-foreground">
{t('rental_history_card.cost')}: <span className="font-medium text-foreground">${item.totalCost.toFixed(2)}</span>
</p>
<p className="text-sm text-muted-foreground">
{t('rental_history_card.status')}: <span className="font-medium text-foreground">{t(getStatusTextKey(item.status))}</span>
</p>
<div className="mt-4">
<Link href={`/${locale}/toys/${item.toy.id}`} passHref>
<Button variant="outline" size="sm">{t('rental_history_card.view_toy_button')}</Button>
</Link>
</div>
</div>
</div>
</Card>
);
}

View File

@ -2,7 +2,7 @@
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, Settings, ShoppingBag, LogOutIcon } from 'lucide-react'; // Changed LogOut to LogOutIcon import { Home, ToyBrick, PlusCircle, ListOrdered, Settings, ShoppingBag, LogOutIcon, History } from 'lucide-react';
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';
@ -22,6 +22,7 @@ export default function DashboardSidebar() {
{ href: '/dashboard/my-toys/add', label: t('dashboard.sidebar.add_new_toy'), icon: PlusCircle }, { 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/rentals', label: t('dashboard.sidebar.my_rentals'), icon: ShoppingBag },
{ href: '/dashboard/requests', label: t('dashboard.sidebar.rental_requests'), icon: ListOrdered }, { href: '/dashboard/requests', label: t('dashboard.sidebar.rental_requests'), icon: ListOrdered },
{ href: '/dashboard/rental-history', label: t('dashboard.sidebar.rental_history'), icon: History },
]; ];
const accountNavItems = [ const accountNavItems = [
@ -30,12 +31,11 @@ export default function DashboardSidebar() {
const handleLogout = () => { const handleLogout = () => {
localStorage.removeItem('isToyShareAuthenticated'); localStorage.removeItem('isToyShareAuthenticated');
toast({ description: "You have been logged out." }); // Translate if needed toast({ description: "You have been logged out." });
router.push(`/${locale}/`); // Redirect to localized home page router.push(`/${locale}/`);
}; };
const NavLink = ({ href, label, icon: Icon }: typeof sidebarNavItems[0] & {icon: React.ElementType}) => { const NavLink = ({ href, label, icon: Icon }: typeof sidebarNavItems[0] & {icon: React.ElementType}) => {
// For active link comparison, remove locale prefix from pathname
const cleanPathname = pathname.startsWith(`/${locale}`) const cleanPathname = pathname.startsWith(`/${locale}`)
? pathname.substring(`/${locale}`.length) || '/' ? pathname.substring(`/${locale}`.length) || '/'
: pathname; : pathname;
@ -46,7 +46,7 @@ export default function DashboardSidebar() {
const isActive = cleanPathname === cleanHref || (cleanHref !== '/dashboard' && cleanPathname.startsWith(cleanHref)); const isActive = cleanPathname === cleanHref || (cleanHref !== '/dashboard' && cleanPathname.startsWith(cleanHref));
return ( return (
<Link href={href} passHref> <Link href={`/${locale}${href}`} passHref>
<Button <Button
variant={isActive ? 'secondary' : 'ghost'} variant={isActive ? 'secondary' : 'ghost'}
className={cn('w-full justify-start', isActive && 'font-semibold')} className={cn('w-full justify-start', isActive && 'font-semibold')}
@ -61,7 +61,7 @@ export default function DashboardSidebar() {
return ( return (
<aside className="w-64 min-h-full bg-card border-r border-border p-4 flex flex-col shadow-md"> <aside className="w-64 min-h-full bg-card border-r border-border p-4 flex flex-col shadow-md">
<div className="mb-6"> <div className="mb-6">
<Link href="/" className="flex items-center gap-2 text-primary mb-2"> <Link href={`/${locale}/`} className="flex items-center gap-2 text-primary mb-2">
<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>
@ -86,7 +86,7 @@ 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}>
<LogOutIcon className="mr-3 h-5 w-5" /> {/* Changed to LogOutIcon */} <LogOutIcon className="mr-3 h-5 w-5" />
{t('dashboard.sidebar.logout')} {t('dashboard.sidebar.logout')}
</Button> </Button>
</div> </div>

View File

@ -1,4 +1,4 @@
import type { Toy } from '@/types'; import type { Toy, RentalHistoryEntry } from '@/types';
export const mockToys: Toy[] = [ export const mockToys: Toy[] = [
{ {
@ -47,7 +47,7 @@ export const mockToys: Toy[] = [
category: 'Plush Toys', category: 'Plush Toys',
images: ['https://placehold.co/600x400.png?text=Teddy+Bear'], images: ['https://placehold.co/600x400.png?text=Teddy+Bear'],
availability: { monday: true, tuesday: true, wednesday: true, thursday: true, friday: true, saturday: true, sunday: true }, availability: { monday: true, tuesday: true, wednesday: true, thursday: true, friday: true, saturday: true, sunday: true },
ownerName: 'David Copperfield', ownerName: 'Alice Wonderland', // Changed owner for variety, was David Copperfield
ownerId: 'user1', ownerId: 'user1',
pricePerDay: 3, pricePerDay: 3,
location: 'Springfield Gardens', location: 'Springfield Gardens',
@ -60,7 +60,7 @@ export const mockToys: Toy[] = [
category: 'Musical', category: 'Musical',
images: ['https://placehold.co/600x400.png?text=Kids+Guitar'], images: ['https://placehold.co/600x400.png?text=Kids+Guitar'],
availability: { monday: true, tuesday: false, wednesday: true, thursday: false, friday: true, saturday: true, sunday: true }, availability: { monday: true, tuesday: false, wednesday: true, thursday: false, friday: true, saturday: true, sunday: true },
ownerName: 'Eve Adamson', ownerName: 'Bob The Builder', // Changed owner, was Eve Adamson
ownerId: 'user2', ownerId: 'user2',
pricePerDay: 10, pricePerDay: 10,
location: 'Willow Creek', location: 'Willow Creek',
@ -73,7 +73,7 @@ export const mockToys: Toy[] = [
category: 'Outdoor', category: 'Outdoor',
images: ['https://placehold.co/600x400.png?text=Sports+Kit'], images: ['https://placehold.co/600x400.png?text=Sports+Kit'],
availability: { monday: true, tuesday: true, wednesday: true, thursday: true, friday: true, saturday: true, sunday: true }, availability: { monday: true, tuesday: true, wednesday: true, thursday: true, friday: true, saturday: true, sunday: true },
ownerName: 'Frank Castle', ownerName: 'Carol Danvers', // Changed owner, was Frank Castle
ownerId: 'user3', ownerId: 'user3',
pricePerDay: 6, pricePerDay: 6,
location: 'Metro City', location: 'Metro City',
@ -81,13 +81,47 @@ export const mockToys: Toy[] = [
} }
]; ];
// Add dataAiHint to mockToys where Toy might have it. export const mockRentalHistory: RentalHistoryEntry[] = [
{
id: 'hist1',
userId: 'user1',
toy: mockToys[2], // Interactive Learning Tablet from Carol Danvers (user3)
rentalStartDate: '2024-05-01',
rentalEndDate: '2024-05-07',
totalCost: mockToys[2].pricePerDay! * 7,
status: 'Completed',
dataAiHint: mockToys[2].category.toLowerCase(),
},
{
id: 'hist2',
userId: 'user1',
toy: mockToys[5], // Outdoor Sports Kit from Carol Danvers (user3)
rentalStartDate: '2024-06-10',
rentalEndDate: '2024-06-15',
totalCost: mockToys[5].pricePerDay! * 5,
status: 'Returned',
dataAiHint: mockToys[5].category.toLowerCase(),
},
{
id: 'hist3',
userId: 'user2', // Different user
toy: mockToys[0], // Building Blocks from Alice Wonderland (user1)
rentalStartDate: '2024-07-01',
rentalEndDate: '2024-07-10',
totalCost: mockToys[0].pricePerDay! * 10,
status: 'Completed',
dataAiHint: mockToys[0].category.toLowerCase(),
}
];
mockToys.forEach(toy => { mockToys.forEach(toy => {
if ('dataAiHint' in toy && toy.images.length > 0) { if (!toy.dataAiHint) {
// This is a bit of a hack since the Toy interface doesn't have dataAiHint toy.dataAiHint = toy.category.toLowerCase().split(' ')[0];
// and images don't store this attribute directly. }
// In a real scenario, this would be part of the image object or derived. });
// For now, we'll assume the first image can have this hint if the toy object does.
// This is primarily for satisfying the placeholder image hint requirement in the prompt. mockRentalHistory.forEach(entry => {
if (!entry.dataAiHint) {
entry.dataAiHint = entry.toy.category.toLowerCase().split(' ')[0];
} }
}); });

View File

@ -53,6 +53,7 @@ export default {
'dashboard.sidebar.add_new_toy': 'Add New Toy', 'dashboard.sidebar.add_new_toy': 'Add New Toy',
'dashboard.sidebar.my_rentals': 'My Rentals', 'dashboard.sidebar.my_rentals': 'My Rentals',
'dashboard.sidebar.rental_requests': 'Rental Requests', 'dashboard.sidebar.rental_requests': 'Rental Requests',
'dashboard.sidebar.rental_history': 'Rental History',
'dashboard.sidebar.account': 'Account', 'dashboard.sidebar.account': 'Account',
'dashboard.sidebar.profile_settings': 'Profile Settings', 'dashboard.sidebar.profile_settings': 'Profile Settings',
'dashboard.sidebar.logout': 'Logout', 'dashboard.sidebar.logout': 'Logout',
@ -216,4 +217,17 @@ export default {
'admin.toys.table_header_actions': 'Actions', 'admin.toys.table_header_actions': 'Actions',
'admin.toys.edit_button': 'Edit Toy', 'admin.toys.edit_button': 'Edit Toy',
'admin.toys.no_toys_found': 'No toys found.', 'admin.toys.no_toys_found': 'No toys found.',
'dashboard.rental_history.title': 'My Rental History',
'dashboard.rental_history.description': 'View your past toy rentals.',
'dashboard.rental_history.no_history_title': 'No Rental History Yet',
'dashboard.rental_history.no_history_description': 'Once you rent and return toys, they will appear here.',
'dashboard.rental_history.browse_toys_button': 'Browse Toys to Rent',
'rental_history_card.rented_from': 'Rented from',
'rental_history_card.rental_period': 'Rental Period',
'rental_history_card.status': 'Status',
'rental_history_card.cost': 'Total Cost',
'rental_history_card.view_toy_button': 'View Toy',
'rental_history_card.status_completed': 'Completed',
'rental_history_card.status_returned': 'Returned',
} as const; } as const;

View File

@ -53,6 +53,7 @@ export default {
'dashboard.sidebar.add_new_toy': '新增玩具', 'dashboard.sidebar.add_new_toy': '新增玩具',
'dashboard.sidebar.my_rentals': '我的租借', 'dashboard.sidebar.my_rentals': '我的租借',
'dashboard.sidebar.rental_requests': '租借請求', 'dashboard.sidebar.rental_requests': '租借請求',
'dashboard.sidebar.rental_history': '租借歷史',
'dashboard.sidebar.account': '帳戶', 'dashboard.sidebar.account': '帳戶',
'dashboard.sidebar.profile_settings': '個人資料設定', 'dashboard.sidebar.profile_settings': '個人資料設定',
'dashboard.sidebar.logout': '登出', 'dashboard.sidebar.logout': '登出',
@ -141,7 +142,7 @@ export default {
'dashboard.requests.description': '管理您玩具的租借請求。', 'dashboard.requests.description': '管理您玩具的租借請求。',
'dashboard.requests.no_requests_title': '沒有租借請求', 'dashboard.requests.no_requests_title': '沒有租借請求',
'dashboard.requests.no_requests_description': '您目前沒有任何待處理的玩具租借請求。', 'dashboard.requests.no_requests_description': '您目前沒有任何待處理的玩具租借請求。',
'dashboard.requests.no_requests_content': '當有人請求租借您的玩具時,請求將會出現在這裡。', 'dashboard.requests.no_requests_content': '当有人请求租用您的玩具时,它会显示在此处。',
'dashboard.requests.requested_by': '請求者', 'dashboard.requests.requested_by': '請求者',
'dashboard.requests.dates': '日期', 'dashboard.requests.dates': '日期',
'dashboard.requests.message': '訊息', 'dashboard.requests.message': '訊息',
@ -216,4 +217,17 @@ export default {
'admin.toys.table_header_actions': '操作', 'admin.toys.table_header_actions': '操作',
'admin.toys.edit_button': '編輯玩具', 'admin.toys.edit_button': '編輯玩具',
'admin.toys.no_toys_found': '找不到玩具。', 'admin.toys.no_toys_found': '找不到玩具。',
'dashboard.rental_history.title': '我的租借歷史',
'dashboard.rental_history.description': '查看您過去的玩具租借記錄。',
'dashboard.rental_history.no_history_title': '尚無租借歷史',
'dashboard.rental_history.no_history_description': '當您租借並歸還玩具後,它們將會出現在這裡。',
'dashboard.rental_history.browse_toys_button': '瀏覽可租借的玩具',
'rental_history_card.rented_from': '租借自',
'rental_history_card.rental_period': '租借期間',
'rental_history_card.status': '狀態',
'rental_history_card.cost': '總費用',
'rental_history_card.view_toy_button': '查看玩具',
'rental_history_card.status_completed': '已完成',
'rental_history_card.status_returned': '已歸還',
} as const; } as const;

View File

@ -18,6 +18,7 @@ export interface Toy {
ownerId: string; ownerId: string;
pricePerDay?: number; // Optional daily rental price pricePerDay?: number; // Optional daily rental price
location?: string; // Optional, e.g., "City, State" or "Neighborhood" location?: string; // Optional, e.g., "City, State" or "Neighborhood"
dataAiHint?: string; // For placeholder image keyword hint
} }
export interface User { export interface User {
@ -33,3 +34,14 @@ export interface DailyAvailability {
isAvailable: boolean; isAvailable: boolean;
bookedBy?: string; // User ID of the renter if booked bookedBy?: string; // User ID of the renter if booked
} }
export interface RentalHistoryEntry {
id: string;
userId: string; // ID of the user who rented
toy: Toy;
rentalStartDate: string; // ISO date string
rentalEndDate: string; // ISO date string
totalCost: number;
status: 'Completed' | 'Returned'; // Example statuses
dataAiHint?: string;
}