toy availability change to assign date range for rental, toy page show c
This commit is contained in:
parent
d45edc5f88
commit
e52d2d2977
|
|
@ -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 =>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -180,23 +173,6 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
|
|||
)}
|
||||
</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}>
|
||||
{isLoading ? (isEditMode ? t('add_toy_form.saving_button') : t('add_toy_form.listing_button')) : (
|
||||
|
|
@ -212,5 +188,3 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
|
|||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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': '沒有進行中的租借',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue