toy availability change to assign date range for rental, toy page show c

This commit is contained in:
Indigo Tang 2025-06-09 09:20:54 +00:00
parent d45edc5f88
commit e52d2d2977
7 changed files with 86 additions and 121 deletions

View File

@ -3,15 +3,18 @@ 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 { Calendar } from '@/components/ui/calendar'; // Using ShadCN calendar
import { ArrowLeft, 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
import { getI18n, getStaticParams as getLocaleStaticParams } from '@/locales/server';
import type { Locale } from '@/locales/server';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { addDays, parseISO } from 'date-fns'; // For date manipulation
interface ToyPageProps {
params: { id: string, locale: string };
params: { id: string, locale: Locale };
}
async function getToyById(id: string): Promise<Toy | undefined> {
@ -40,6 +43,17 @@ export default async function ToyPage({ params }: ToyPageProps) {
const placeholderHint = toy.category.toLowerCase() || "toy detail";
const disabledDates = toy.unavailableRanges.map(range => {
// react-day-picker expects Date objects for ranges
const from = parseISO(range.startDate);
const to = parseISO(range.endDate);
// If 'to' is inclusive, add a day for the range. react-day-picker's 'to' is exclusive for the visual range.
// However, for disabling, if a range is "2023-08-10" to "2023-08-12", all 3 days should be disabled.
// The `disabled` prop for DayPicker usually treats `to` as inclusive.
return { from, to };
});
return (
<div className="container mx-auto py-8 px-4">
<Link href={`/${params.locale}/`} className="inline-flex items-center text-primary hover:underline mb-6 group">
@ -120,7 +134,22 @@ export default async function ToyPage({ params }: ToyPageProps) {
<Separator />
<AvailabilityCalendar availability={toy.availability} />
<Card className="shadow-md">
<CardHeader>
<CardTitle className="text-xl font-headline text-primary">{t('toy_details.availability_calendar_title')}</CardTitle>
</CardHeader>
<CardContent className="flex justify-center">
<Calendar
mode="single" // "single" is fine for display, range selection would be for booking
disabled={disabledDates} // Disable booked ranges
month={new Date()} // Show current month by default
className="rounded-md border"
/>
</CardContent>
<p className="text-xs text-muted-foreground mt-0 pb-4 text-center">
{t('toy_details.calendar_note')}
</p>
</Card>
<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')}
@ -132,7 +161,7 @@ export default async function ToyPage({ params }: ToyPageProps) {
}
export function generateStaticParams() {
const localeParams = getLocaleStaticParams(); // e.g. [{ locale: 'en' }, { locale: 'zh-TW' }]
const localeParams = getLocaleStaticParams();
const toyParams = mockToys.map((toy) => ({ id: toy.id }));
return localeParams.flatMap(lang =>

View File

@ -9,7 +9,6 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox';
import { useToast } from '@/hooks/use-toast';
import { ToyBrick, Save, PlusCircle, Trash2 } from 'lucide-react';
import type { Toy } from '@/types';
@ -30,9 +29,6 @@ const toyCategoryDefinitions = [
{ key: 'building_blocks', value: 'Building Blocks' },
];
const daysOfWeek = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] as const;
interface AddToyFormProps {
initialData?: Partial<Toy>;
isEditMode?: boolean;
@ -50,11 +46,8 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
const [pricePerDay, setPricePerDay] = useState(initialData?.pricePerDay?.toString() || '0');
const [location, setLocation] = useState(initialData?.location || '');
const [images, setImages] = useState<string[]>(initialData?.images || ['']);
const [availability, setAvailability] = useState<Toy['availability']>(
initialData?.availability || {
monday: true, tuesday: true, wednesday: true, thursday: true, friday: true, saturday: false, sunday: false
}
);
// unavailableRanges will be initialized as empty or from initialData, but not editable in this form version.
const [unavailableRanges, setUnavailableRanges] = useState<Toy['unavailableRanges']>(initialData?.unavailableRanges || []);
const [isLoading, setIsLoading] = useState(false);
const handleImageChange = (index: number, value: string) => {
@ -72,34 +65,34 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
}
};
const handleAvailabilityChange = (day: keyof Toy['availability']) => {
setAvailability(prev => ({ ...prev, [day]: !prev[day] }));
};
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
if (!name || !description || !category) {
toast({ title: "Missing Fields", description: "Please fill in all required fields.", variant: "destructive" }); // Translate
toast({ title: "Missing Fields", description: "Please fill in all required fields.", variant: "destructive" });
setIsLoading(false);
return;
}
const toyData = {
const toyData: Partial<Toy> = {
name, description, category,
pricePerDay: parseFloat(pricePerDay) || 0,
location,
images: images.filter(img => img.trim() !== ''),
availability,
unavailableRanges: initialData?.unavailableRanges || [], // Preserve existing, or empty for new
};
if (isEditMode && initialData?.id) {
toyData.id = initialData.id;
}
console.log("Submitting toy data:", toyData);
await new Promise(resolve => setTimeout(resolve, 1500));
toast({
title: isEditMode ? "Toy Updated!" : "Toy Added!", // Translate
description: `${name} has been successfully ${isEditMode ? 'updated' : 'listed'}.`, // Translate
title: isEditMode ? t('add_toy_form.edit_title_toast') : t('add_toy_form.add_title_toast'),
description: t('add_toy_form.success_description_toast', { toyName: name, action: isEditMode ? t('add_toy_form.updated_action_toast') : t('add_toy_form.listed_action_toast')})
});
router.push(`/${locale}/dashboard/my-toys`);
setIsLoading(false);
@ -137,7 +130,7 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
<SelectContent>
{toyCategoryDefinitions.map(catDef => (
<SelectItem key={catDef.key} value={catDef.value}>
{t(`toy_categories.${catDef.key}` as any)} {/* Use 'as any' if TS complains about template literal type */}
{t(`toy_categories.${catDef.key}` as any)}
</SelectItem>
))}
</SelectContent>
@ -179,23 +172,6 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
</Button>
)}
</div>
<div className="space-y-3">
<Label className="text-base">{t('add_toy_form.availability_label')}</Label>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-4">
{daysOfWeek.map(day => (
<div key={day} className="flex items-center space-x-2">
<Checkbox
id={`avail-${day}`}
checked={availability[day]}
onCheckedChange={() => handleAvailabilityChange(day as keyof Toy['availability'])}
disabled={isLoading}
/>
<Label htmlFor={`avail-${day}`} className="capitalize font-normal text-sm">{day}</Label>
</div>
))}
</div>
</div>
<CardFooter className="p-0 pt-6">
<Button type="submit" className="w-full" size="lg" disabled={isLoading}>
@ -212,5 +188,3 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
</Card>
);
}

View File

@ -1,54 +0,0 @@
import type { Toy } from '@/types';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { CheckCircle2, XCircle } from 'lucide-react';
import { cn } from '@/lib/utils';
interface AvailabilityCalendarProps {
availability: Toy['availability'];
}
const daysOfWeek = [
{ key: 'monday', label: 'Mon' },
{ key: 'tuesday', label: 'Tue' },
{ key: 'wednesday', label: 'Wed' },
{ key: 'thursday', label: 'Thu' },
{ key: 'friday', label: 'Fri' },
{ key: 'saturday', label: 'Sat' },
{ key: 'sunday', label: 'Sun' },
] as const; // Use 'as const' for stricter typing of keys
export default function AvailabilityCalendar({ availability }: AvailabilityCalendarProps) {
return (
<Card className="shadow-md">
<CardHeader>
<CardTitle className="text-xl font-headline text-primary">Weekly Availability</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-7 gap-2 text-center">
{daysOfWeek.map((day) => {
const isAvailable = availability[day.key];
return (
<div
key={day.key}
className={cn(
"p-3 rounded-md border flex flex-col items-center justify-center space-y-1",
isAvailable ? "bg-green-100 border-green-300" : "bg-red-100 border-red-300"
)}
>
<span className="font-medium text-sm text-foreground/80">{day.label}</span>
{isAvailable ? (
<CheckCircle2 className="h-6 w-6 text-green-600" />
) : (
<XCircle className="h-6 w-6 text-red-600" />
)}
</div>
);
})}
</div>
<p className="text-xs text-muted-foreground mt-4 text-center">
This calendar shows general weekly availability. Specific dates may vary.
</p>
</CardContent>
</Card>
);
}

View File

@ -1,4 +1,7 @@
import type { Toy, RentalHistoryEntry } from '@/types';
import { addDays, formatISO } from 'date-fns';
const today = new Date();
export const mockToys: Toy[] = [
{
@ -7,7 +10,10 @@ export const mockToys: Toy[] = [
description: 'A fantastic set of 100 colorful building blocks to spark creativity in young minds. Suitable for ages 3+.',
category: 'Educational',
images: ['https://placehold.co/600x400.png?text=Building+Blocks', 'https://placehold.co/600x400.png?text=Blocks+Close+Up'],
availability: { monday: true, tuesday: true, wednesday: false, thursday: true, friday: true, saturday: true, sunday: false },
unavailableRanges: [
{ startDate: formatISO(addDays(today, 5), { representation: 'date' }), endDate: formatISO(addDays(today, 7), { representation: 'date' }) },
{ startDate: formatISO(addDays(today, 15), { representation: 'date' }), endDate: formatISO(addDays(today, 16), { representation: 'date' }) },
],
ownerName: 'Alice Wonderland',
ownerId: 'user1',
pricePerDay: 5,
@ -20,7 +26,9 @@ export const mockToys: Toy[] = [
description: 'Zoom around with this super-fast remote control racing car. Features rechargeable batteries and durable design. Ages 6+.',
category: 'Vehicles',
images: ['https://placehold.co/600x400.png?text=RC+Car', 'https://placehold.co/600x400.png?text=RC+Car+Controller'],
availability: { monday: false, tuesday: true, wednesday: true, thursday: true, friday: true, saturday: true, sunday: true },
unavailableRanges: [
{ startDate: formatISO(addDays(today, 10), { representation: 'date' }), endDate: formatISO(addDays(today, 12), { representation: 'date' }) },
],
ownerName: 'Bob The Builder',
ownerId: 'user2',
pricePerDay: 8,
@ -33,7 +41,7 @@ export const mockToys: Toy[] = [
description: 'An educational tablet for kids with games, stories, and learning activities. Parent-approved content. Ages 4-7.',
category: 'Electronics',
images: ['https://placehold.co/600x400.png?text=Kids+Tablet', 'https://placehold.co/600x400.png?text=Tablet+Screen'],
availability: { monday: true, tuesday: true, wednesday: true, thursday: true, friday: true, saturday: false, sunday: false },
unavailableRanges: [],
ownerName: 'Carol Danvers',
ownerId: 'user3',
pricePerDay: 7,
@ -46,8 +54,10 @@ export const mockToys: Toy[] = [
description: 'A cuddly and soft large teddy bear, perfect for hugs and comfort. Hypoallergenic materials. All ages.',
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: 'Alice Wonderland', // Changed owner for variety, was David Copperfield
unavailableRanges: [
{ startDate: formatISO(addDays(today, 20), { representation: 'date' }), endDate: formatISO(addDays(today, 25), { representation: 'date' }) },
],
ownerName: 'Alice Wonderland',
ownerId: 'user1',
pricePerDay: 3,
location: 'Springfield Gardens',
@ -59,8 +69,8 @@ export const mockToys: Toy[] = [
description: 'A 3/4 size acoustic guitar, ideal for children starting their musical journey. Comes with a soft case and picks.',
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: 'Bob The Builder', // Changed owner, was Eve Adamson
unavailableRanges: [],
ownerName: 'Bob The Builder',
ownerId: 'user2',
pricePerDay: 10,
location: 'Willow Creek',
@ -72,8 +82,8 @@ export const mockToys: Toy[] = [
description: 'Includes a frisbee, a jump rope, and a set of cones. Perfect for outdoor fun and activities.',
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: 'Carol Danvers', // Changed owner, was Frank Castle
unavailableRanges: [],
ownerName: 'Carol Danvers',
ownerId: 'user3',
pricePerDay: 6,
location: 'Metro City',
@ -85,7 +95,7 @@ export const mockRentalHistory: RentalHistoryEntry[] = [
{
id: 'hist1',
userId: 'user1',
toy: mockToys[2], // Interactive Learning Tablet from Carol Danvers (user3)
toy: mockToys[2],
rentalStartDate: '2024-05-01',
rentalEndDate: '2024-05-07',
totalCost: mockToys[2].pricePerDay! * 7,
@ -95,7 +105,7 @@ export const mockRentalHistory: RentalHistoryEntry[] = [
{
id: 'hist2',
userId: 'user1',
toy: mockToys[5], // Outdoor Sports Kit from Carol Danvers (user3)
toy: mockToys[5],
rentalStartDate: '2024-06-10',
rentalEndDate: '2024-06-15',
totalCost: mockToys[5].pricePerDay! * 5,
@ -104,8 +114,8 @@ export const mockRentalHistory: RentalHistoryEntry[] = [
},
{
id: 'hist3',
userId: 'user2', // Different user
toy: mockToys[0], // Building Blocks from Alice Wonderland (user1)
userId: 'user2',
toy: mockToys[0],
rentalStartDate: '2024-07-01',
rentalEndDate: '2024-07-10',
totalCost: mockToys[0].pricePerDay! * 10,

View File

@ -45,6 +45,8 @@ export default {
'toy_details.price_free': 'Free',
'toy_details.price_per_day': '/day',
'toy_details.request_to_rent': 'Request to Rent',
'toy_details.availability_calendar_title': 'Availability Calendar',
'toy_details.calendar_note': 'Dates shown in gray or crossed out are unavailable.',
'dashboard.layout.loading': 'Loading Dashboard...',
'dashboard.sidebar.user_dashboard': 'User Dashboard',
'dashboard.sidebar.toy_management': 'Toy Management',
@ -131,6 +133,11 @@ export default {
'add_toy_form.list_button': 'List My Toy',
'add_toy_form.saving_button': 'Saving Changes...',
'add_toy_form.listing_button': 'Listing Toy...',
'add_toy_form.edit_title_toast': 'Toy Updated!',
'add_toy_form.add_title_toast': 'Toy Added!',
'add_toy_form.success_description_toast': '{toyName} has been successfully {action}.',
'add_toy_form.updated_action_toast': 'updated',
'add_toy_form.listed_action_toast': 'listed',
'dashboard.rentals.title': 'My Rentals',
'dashboard.rentals.description': 'Toys you are currently renting from others.',
'dashboard.rentals.no_rentals_title': 'No Active Rentals',

View File

@ -45,6 +45,8 @@ export default {
'toy_details.price_free': '免費',
'toy_details.price_per_day': '/天',
'toy_details.request_to_rent': '請求租借',
'toy_details.availability_calendar_title': '可租借日曆',
'toy_details.calendar_note': '灰色或劃掉的日期表示不可租借。',
'dashboard.layout.loading': '正在載入儀表板...',
'dashboard.sidebar.user_dashboard': '使用者儀表板',
'dashboard.sidebar.toy_management': '玩具管理',
@ -131,6 +133,11 @@ export default {
'add_toy_form.list_button': '列出我的玩具',
'add_toy_form.saving_button': '儲存變更中...',
'add_toy_form.listing_button': '列出玩具中...',
'add_toy_form.edit_title_toast': '玩具已更新!',
'add_toy_form.add_title_toast': '玩具已新增!',
'add_toy_form.success_description_toast': '{toyName} 已成功 {action}。',
'add_toy_form.updated_action_toast': '更新',
'add_toy_form.listed_action_toast': '列出',
'dashboard.rentals.title': '我的租借',
'dashboard.rentals.description': '您目前正在向他人租借的玩具。',
'dashboard.rentals.no_rentals_title': '沒有進行中的租借',

View File

@ -5,15 +5,7 @@ export interface Toy {
description: string;
category: string;
images: string[]; // Array of image URLs
availability: {
monday: boolean;
tuesday: boolean;
wednesday: boolean;
thursday: boolean;
friday: boolean;
saturday: boolean;
sunday: boolean;
};
unavailableRanges: { startDate: string; endDate: string }[]; // New field for booked/unavailable date ranges
ownerName: string; // Simplified for now
ownerId: string;
pricePerDay?: number; // Optional daily rental price
@ -36,7 +28,7 @@ export interface DailyAvailability {
}
export interface RentalHistoryEntry {
id: string;
id:string;
userId: string; // ID of the user who rented
toy: Toy;
rentalStartDate: string; // ISO date string