add toy rental history in user dashboard
This commit is contained in:
parent
9d8c9856e0
commit
8ba7f4cbed
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import Link from 'next/link';
|
||||
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 { cn } from '@/lib/utils';
|
||||
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/rentals', label: t('dashboard.sidebar.my_rentals'), icon: ShoppingBag },
|
||||
{ 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 = [
|
||||
|
|
@ -30,12 +31,11 @@ export default function DashboardSidebar() {
|
|||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('isToyShareAuthenticated');
|
||||
toast({ description: "You have been logged out." }); // Translate if needed
|
||||
router.push(`/${locale}/`); // Redirect to localized home page
|
||||
toast({ description: "You have been logged out." });
|
||||
router.push(`/${locale}/`);
|
||||
};
|
||||
|
||||
const NavLink = ({ href, label, icon: Icon }: typeof sidebarNavItems[0] & {icon: React.ElementType}) => {
|
||||
// For active link comparison, remove locale prefix from pathname
|
||||
const cleanPathname = pathname.startsWith(`/${locale}`)
|
||||
? pathname.substring(`/${locale}`.length) || '/'
|
||||
: pathname;
|
||||
|
|
@ -46,7 +46,7 @@ export default function DashboardSidebar() {
|
|||
const isActive = cleanPathname === cleanHref || (cleanHref !== '/dashboard' && cleanPathname.startsWith(cleanHref));
|
||||
|
||||
return (
|
||||
<Link href={href} passHref>
|
||||
<Link href={`/${locale}${href}`} passHref>
|
||||
<Button
|
||||
variant={isActive ? 'secondary' : 'ghost'}
|
||||
className={cn('w-full justify-start', isActive && 'font-semibold')}
|
||||
|
|
@ -61,7 +61,7 @@ export default function DashboardSidebar() {
|
|||
return (
|
||||
<aside className="w-64 min-h-full bg-card border-r border-border p-4 flex flex-col shadow-md">
|
||||
<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" />
|
||||
<h2 className="text-xl font-headline font-bold">ToyShare</h2>
|
||||
</Link>
|
||||
|
|
@ -86,7 +86,7 @@ export default function DashboardSidebar() {
|
|||
|
||||
<div>
|
||||
<Button variant="ghost" className="w-full justify-start text-red-600 hover:bg-red-100 hover:text-red-700" onClick={handleLogout}>
|
||||
<LogOutIcon className="mr-3 h-5 w-5" /> {/* Changed to LogOutIcon */}
|
||||
<LogOutIcon className="mr-3 h-5 w-5" />
|
||||
{t('dashboard.sidebar.logout')}
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { Toy } from '@/types';
|
||||
import type { Toy, RentalHistoryEntry } from '@/types';
|
||||
|
||||
export const mockToys: Toy[] = [
|
||||
{
|
||||
|
|
@ -47,7 +47,7 @@ export const mockToys: Toy[] = [
|
|||
category: 'Plush Toys',
|
||||
images: ['https://placehold.co/600x400.png?text=Teddy+Bear'],
|
||||
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',
|
||||
pricePerDay: 3,
|
||||
location: 'Springfield Gardens',
|
||||
|
|
@ -60,7 +60,7 @@ export const mockToys: Toy[] = [
|
|||
category: 'Musical',
|
||||
images: ['https://placehold.co/600x400.png?text=Kids+Guitar'],
|
||||
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',
|
||||
pricePerDay: 10,
|
||||
location: 'Willow Creek',
|
||||
|
|
@ -73,7 +73,7 @@ export const mockToys: Toy[] = [
|
|||
category: 'Outdoor',
|
||||
images: ['https://placehold.co/600x400.png?text=Sports+Kit'],
|
||||
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',
|
||||
pricePerDay: 6,
|
||||
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 => {
|
||||
if ('dataAiHint' in toy && toy.images.length > 0) {
|
||||
// This is a bit of a hack since the Toy interface doesn't have dataAiHint
|
||||
// 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.
|
||||
if (!toy.dataAiHint) {
|
||||
toy.dataAiHint = toy.category.toLowerCase().split(' ')[0];
|
||||
}
|
||||
});
|
||||
|
||||
mockRentalHistory.forEach(entry => {
|
||||
if (!entry.dataAiHint) {
|
||||
entry.dataAiHint = entry.toy.category.toLowerCase().split(' ')[0];
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ export default {
|
|||
'dashboard.sidebar.add_new_toy': 'Add New Toy',
|
||||
'dashboard.sidebar.my_rentals': 'My Rentals',
|
||||
'dashboard.sidebar.rental_requests': 'Rental Requests',
|
||||
'dashboard.sidebar.rental_history': 'Rental History',
|
||||
'dashboard.sidebar.account': 'Account',
|
||||
'dashboard.sidebar.profile_settings': 'Profile Settings',
|
||||
'dashboard.sidebar.logout': 'Logout',
|
||||
|
|
@ -216,4 +217,17 @@ export default {
|
|||
'admin.toys.table_header_actions': 'Actions',
|
||||
'admin.toys.edit_button': 'Edit Toy',
|
||||
'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;
|
||||
|
|
|
|||
|
|
@ -53,6 +53,7 @@ export default {
|
|||
'dashboard.sidebar.add_new_toy': '新增玩具',
|
||||
'dashboard.sidebar.my_rentals': '我的租借',
|
||||
'dashboard.sidebar.rental_requests': '租借請求',
|
||||
'dashboard.sidebar.rental_history': '租借歷史',
|
||||
'dashboard.sidebar.account': '帳戶',
|
||||
'dashboard.sidebar.profile_settings': '個人資料設定',
|
||||
'dashboard.sidebar.logout': '登出',
|
||||
|
|
@ -141,7 +142,7 @@ export default {
|
|||
'dashboard.requests.description': '管理您玩具的租借請求。',
|
||||
'dashboard.requests.no_requests_title': '沒有租借請求',
|
||||
'dashboard.requests.no_requests_description': '您目前沒有任何待處理的玩具租借請求。',
|
||||
'dashboard.requests.no_requests_content': '當有人請求租借您的玩具時,請求將會出現在這裡。',
|
||||
'dashboard.requests.no_requests_content': '当有人请求租用您的玩具时,它会显示在此处。',
|
||||
'dashboard.requests.requested_by': '請求者',
|
||||
'dashboard.requests.dates': '日期',
|
||||
'dashboard.requests.message': '訊息',
|
||||
|
|
@ -216,4 +217,17 @@ export default {
|
|||
'admin.toys.table_header_actions': '操作',
|
||||
'admin.toys.edit_button': '編輯玩具',
|
||||
'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;
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ export interface Toy {
|
|||
ownerId: string;
|
||||
pricePerDay?: number; // Optional daily rental price
|
||||
location?: string; // Optional, e.g., "City, State" or "Neighborhood"
|
||||
dataAiHint?: string; // For placeholder image keyword hint
|
||||
}
|
||||
|
||||
export interface User {
|
||||
|
|
@ -33,3 +34,14 @@ export interface DailyAvailability {
|
|||
isAvailable: boolean;
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue