Updated app
This commit is contained in:
parent
7d3cda96fb
commit
3db4ec2066
|
|
@ -0,0 +1,19 @@
|
|||
# **App Name**: ToyShare
|
||||
|
||||
## Core Features:
|
||||
|
||||
- Toy Showcase: Display available toys in a visually appealing gallery format, showing key details.
|
||||
- User Authentication: Allow users to log in/register via the website
|
||||
- Availability Calendar: Display a simple weekly availability calendar for each toy.
|
||||
- Schedule Setup: Users can set their own rental schedule
|
||||
- Image display: Display all images of toys available for rental.
|
||||
|
||||
## Style Guidelines:
|
||||
|
||||
- Primary color: Muted blue (#6699CC), evoking trust and calmness.
|
||||
- Background color: Very light blue (#F0F8FF), creates a soft and clean backdrop.
|
||||
- Accent color: Soft green (#8FBC8F), provides a contrasting, harmonious accent.
|
||||
- Body and headline font: 'PT Sans', a humanist sans-serif for a modern yet approachable feel.
|
||||
- Use simple, outlined icons to represent toy categories and functionalities.
|
||||
- Clean, grid-based layout with ample white space to highlight toys and schedule.
|
||||
- Subtle transitions and loading animations to provide a smooth user experience.
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
'use client'; // Required for checking auth state on client
|
||||
|
||||
import DashboardSidebar from '@/components/layout/DashboardSidebar';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Loader2 } from 'lucide-react';
|
||||
|
||||
export default function DashboardLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const [isAuthenticating, setIsAuthenticating] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
// Mock authentication check
|
||||
const isAuthenticated = localStorage.getItem('isToyShareAuthenticated') === 'true';
|
||||
if (!isAuthenticated) {
|
||||
router.replace('/login?redirect=/dashboard'); // Redirect to login if not authenticated
|
||||
} else {
|
||||
setIsAuthenticating(false);
|
||||
}
|
||||
}, [router]);
|
||||
|
||||
if (isAuthenticating) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-screen bg-background">
|
||||
<Loader2 className="h-12 w-12 animate-spin text-primary" />
|
||||
<p className="ml-4 text-lg text-muted-foreground">Loading Dashboard...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[calc(100vh-4rem)]"> {/* Adjust min-height based on header height */}
|
||||
<DashboardSidebar />
|
||||
<main className="flex-1 p-6 lg:p-8 bg-background overflow-auto">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import AddToyForm from '@/components/toys/AddToyForm';
|
||||
|
||||
export default function AddNewToyPage() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Page Title can be part of the AddToyForm or here */}
|
||||
{/* <h1 className="text-3xl font-bold font-headline text-primary mb-6">Share a New Toy</h1> */}
|
||||
<AddToyForm />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
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';
|
||||
|
||||
interface EditToyPageProps {
|
||||
params: { id: string };
|
||||
}
|
||||
|
||||
// Server Component to fetch toy data (mocked for now)
|
||||
async function getToyForEdit(id: string): Promise<Partial<Toy> | undefined> {
|
||||
await new Promise(resolve => setTimeout(resolve, 100)); // Simulate fetch
|
||||
return mockToys.find(toy => toy.id === id);
|
||||
}
|
||||
|
||||
export default async function EditToyPage({ params }: EditToyPageProps) {
|
||||
const toyData = await getToyForEdit(params.id);
|
||||
|
||||
if (!toyData) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<h1 className="text-2xl font-bold mb-4">Toy Not Found</h1>
|
||||
<p className="text-muted-foreground mb-6">Cannot edit a toy that does not exist.</p>
|
||||
<Link href="/dashboard/my-toys" passHref>
|
||||
<Button variant="outline">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to My Toys
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<Link href="/dashboard/my-toys" className="inline-flex items-center text-primary hover:underline mb-0 group">
|
||||
<ArrowLeft className="mr-2 h-4 w-4 transition-transform group-hover:-translate-x-1" />
|
||||
Back to My Toys
|
||||
</Link>
|
||||
<AddToyForm initialData={toyData} isEditMode={true} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,112 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import ToyCard from "@/components/toys/ToyCard";
|
||||
import { mockToys } from "@/lib/mockData"; // Using all toys for now, filter by ownerId in real app
|
||||
import Link from "next/link";
|
||||
import { PlusCircle, Edit3, Trash2, Eye } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import type { Toy } from "@/types";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
// Assume this is the logged-in user's ID
|
||||
const currentUserId = 'user1';
|
||||
|
||||
// Filter toys by current user
|
||||
const userToys = mockToys.filter(toy => toy.ownerId === currentUserId);
|
||||
|
||||
export default function MyToysPage() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold font-headline text-primary">My Listed Toys</h1>
|
||||
<p className="text-muted-foreground">Manage the toys you've shared with the community.</p>
|
||||
</div>
|
||||
<Link href="/dashboard/my-toys/add" passHref>
|
||||
<Button>
|
||||
<PlusCircle className="mr-2 h-5 w-5" />
|
||||
Add New Toy
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{userToys.length === 0 ? (
|
||||
<Card className="text-center py-12 shadow-md">
|
||||
<CardHeader>
|
||||
<ToyBrick className="h-16 w-16 mx-auto text-muted-foreground mb-4" />
|
||||
<CardTitle>No Toys Listed Yet</CardTitle>
|
||||
<CardDescription>Share your first toy and spread the joy!</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Link href="/dashboard/my-toys/add" passHref>
|
||||
<Button size="lg">
|
||||
<PlusCircle className="mr-2 h-5 w-5" />
|
||||
Add Your First Toy
|
||||
</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{userToys.map(toy => (
|
||||
<ListedToyItem key={toy.id} toy={toy} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface ListedToyItemProps {
|
||||
toy: Toy & {dataAiHint?: string};
|
||||
}
|
||||
|
||||
function ListedToyItem({ toy }: ListedToyItemProps) {
|
||||
const placeholderHint = toy.dataAiHint || toy.category.toLowerCase() || "toy";
|
||||
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/3 lg:w-1/4 relative aspect-video md:aspect-auto">
|
||||
<Image
|
||||
src={toy.images[0] || 'https://placehold.co/300x200.png'}
|
||||
alt={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">
|
||||
<div>
|
||||
<CardTitle className="text-2xl font-headline mb-1">{toy.name}</CardTitle>
|
||||
<Badge variant="outline">{toy.category}</Badge>
|
||||
</div>
|
||||
<div className="flex space-x-2">
|
||||
<Link href={`/toys/${toy.id}`} passHref>
|
||||
<Button variant="ghost" size="icon" title="View Toy">
|
||||
<Eye className="h-5 w-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href={`/dashboard/my-toys/edit/${toy.id}`} passHref>
|
||||
<Button variant="ghost" size="icon" title="Edit Toy">
|
||||
<Edit3 className="h-5 w-5" />
|
||||
</Button>
|
||||
</Link>
|
||||
<Button variant="ghost" size="icon" className="text-destructive hover:text-destructive hover:bg-destructive/10" title="Delete Toy">
|
||||
<Trash2 className="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-2 text-sm line-clamp-2">{toy.description}</p>
|
||||
<div className="mt-4 text-sm">
|
||||
<span className="font-semibold">Price: </span>
|
||||
{toy.pricePerDay !== undefined ? (toy.pricePerDay > 0 ? `$${toy.pricePerDay}/day` : 'Free') : 'Not set'}
|
||||
</div>
|
||||
{/* Could add more stats like number of rentals, views etc. here */}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
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";
|
||||
|
||||
// Mock data for dashboard overview
|
||||
const userStats = {
|
||||
listedToys: 3,
|
||||
activeRentals: 1, // Toys I'm renting
|
||||
pendingRequests: 2, // Requests for my toys
|
||||
};
|
||||
|
||||
export default function DashboardOverviewPage() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<Card className="shadow-lg">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-3xl font-headline text-primary">Welcome to Your Dashboard!</CardTitle>
|
||||
<CardDescription>Manage your toys, rentals, and account settings all in one place.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<DashboardStatCard
|
||||
title="My Listed Toys"
|
||||
value={userStats.listedToys.toString()}
|
||||
icon={<ToyBrick className="h-8 w-8 text-primary" />}
|
||||
actionLink="/dashboard/my-toys"
|
||||
actionLabel="View My Toys"
|
||||
/>
|
||||
<DashboardStatCard
|
||||
title="Toys I'm Renting"
|
||||
value={userStats.activeRentals.toString()}
|
||||
icon={<ShoppingBag className="h-8 w-8 text-accent" />}
|
||||
actionLink="/dashboard/rentals"
|
||||
actionLabel="View My Rentals"
|
||||
/>
|
||||
<DashboardStatCard
|
||||
title="Pending Requests"
|
||||
value={userStats.pendingRequests.toString()}
|
||||
icon={<ListOrdered className="h-8 w-8 text-yellow-500" />}
|
||||
actionLink="/dashboard/requests"
|
||||
actionLabel="Manage Requests"
|
||||
/>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<Card className="shadow-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl font-headline">Quick Actions</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
<Link href="/dashboard/my-toys/add" passHref>
|
||||
<Button className="w-full justify-start" variant="outline">
|
||||
<PlusCircle className="mr-2 h-5 w-5" /> Add a New Toy
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/dashboard/profile" passHref>
|
||||
<Button className="w-full justify-start" variant="outline">
|
||||
<User className="mr-2 h-5 w-5" /> Update Profile
|
||||
</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="shadow-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl font-headline">Tips for Sharers</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ul className="list-disc list-inside text-sm text-muted-foreground space-y-2">
|
||||
<li>Take clear photos of your toys from multiple angles.</li>
|
||||
<li>Write detailed and accurate descriptions.</li>
|
||||
<li>Keep your availability calendar up-to-date.</li>
|
||||
<li>Respond promptly to rental requests.</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface DashboardStatCardProps {
|
||||
title: string;
|
||||
value: string;
|
||||
icon: React.ReactNode;
|
||||
actionLink: string;
|
||||
actionLabel: string;
|
||||
}
|
||||
|
||||
function DashboardStatCard({ title, value, icon, actionLink, actionLabel }: DashboardStatCardProps) {
|
||||
return (
|
||||
<Card className="hover:shadow-lg transition-shadow">
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">{title}</CardTitle>
|
||||
{icon}
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-4xl font-bold text-foreground">{value}</div>
|
||||
<Link href={actionLink} className="text-xs text-primary hover:underline mt-1 block">
|
||||
{actionLabel} →
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,162 @@
|
|||
'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 { UserCircle2, Mail, Phone, MapPin, Save } from 'lucide-react';
|
||||
import { useToast } from '@/hooks/use-toast';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
|
||||
// Mock user data
|
||||
const mockUserProfile = {
|
||||
name: 'Alice Wonderland',
|
||||
email: 'alice@example.com', // Usually not editable or needs verification
|
||||
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 [name, setName] = useState(mockUserProfile.name);
|
||||
const [email, setEmail] = useState(mockUserProfile.email); // For display
|
||||
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<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
// Mock API call
|
||||
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." });
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
const handlePasswordChange = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
if (newPassword !== confirmNewPassword) {
|
||||
toast({ title: "Password Mismatch", description: "New passwords do not match.", variant: "destructive" });
|
||||
return;
|
||||
}
|
||||
if (newPassword.length < 6) {
|
||||
toast({ title: "Password Too Short", description: "Password must be at least 6 characters.", variant: "destructive" });
|
||||
return;
|
||||
}
|
||||
setIsPasswordLoading(true);
|
||||
// Mock API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
console.log("Changing password");
|
||||
toast({ title: "Password Updated", description: "Your password has been changed successfully." });
|
||||
setCurrentPassword('');
|
||||
setNewPassword('');
|
||||
setConfirmNewPassword('');
|
||||
setIsPasswordLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-8 max-w-3xl mx-auto">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold font-headline text-primary">Profile Settings</h1>
|
||||
<p className="text-muted-foreground">Manage your personal information and account settings.</p>
|
||||
</div>
|
||||
|
||||
{/* Profile Information Card */}
|
||||
<Card className="shadow-lg">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl font-headline">Personal Information</CardTitle>
|
||||
<CardDescription>Update your publicly visible profile information.</CardDescription>
|
||||
</CardHeader>
|
||||
<form onSubmit={handleProfileUpdate}>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
<Avatar className="h-20 w-20">
|
||||
<AvatarImage src={avatarUrl} alt={name} data-ai-hint="user avatar"/>
|
||||
<AvatarFallback>{name.split(' ').map(n => n[0]).join('').toUpperCase()}</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="flex-1 space-y-1">
|
||||
<Label htmlFor="avatarUrl">Avatar URL</Label>
|
||||
<Input id="avatarUrl" value={avatarUrl} onChange={(e) => setAvatarUrl(e.target.value)} placeholder="https://example.com/avatar.png" disabled={isLoading} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="name">Full Name</Label>
|
||||
<Input id="name" value={name} onChange={(e) => setName(e.target.value)} placeholder="Your Name" disabled={isLoading} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="email">Email Address (Read-only)</Label>
|
||||
<Input id="email" type="email" value={email} readOnly disabled className="bg-muted/50 cursor-not-allowed" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="bio">Bio</Label>
|
||||
<Textarea id="bio" value={bio} onChange={(e) => setBio(e.target.value)} placeholder="Tell us a bit about yourself and your toys..." rows={3} disabled={isLoading}/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="phone">Phone Number</Label>
|
||||
<Input id="phone" type="tel" value={phone} onChange={(e) => setPhone(e.target.value)} placeholder="Your Phone" disabled={isLoading} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="location">Location</Label>
|
||||
<Input id="location" value={location} onChange={(e) => setLocation(e.target.value)} placeholder="City, Country" disabled={isLoading} />
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button type="submit" disabled={isLoading}>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
{isLoading ? 'Saving Profile...' : 'Save Profile Changes'}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Card>
|
||||
|
||||
{/* Change Password Card */}
|
||||
<Card className="shadow-lg">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl font-headline">Change Password</CardTitle>
|
||||
<CardDescription>Update your account password for security.</CardDescription>
|
||||
</CardHeader>
|
||||
<form onSubmit={handlePasswordChange}>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="currentPassword">Current Password</Label>
|
||||
<Input id="currentPassword" type="password" value={currentPassword} onChange={(e) => setCurrentPassword(e.target.value)} required disabled={isPasswordLoading} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="newPassword">New Password</Label>
|
||||
<Input id="newPassword" type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} required disabled={isPasswordLoading} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="confirmNewPassword">Confirm New Password</Label>
|
||||
<Input id="confirmNewPassword" type="password" value={confirmNewPassword} onChange={(e) => setConfirmNewPassword(e.target.value)} required disabled={isPasswordLoading} />
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<Button type="submit" disabled={isPasswordLoading}>
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
{isPasswordLoading ? 'Updating Password...' : 'Update Password'}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { ShoppingBag, ToyBrick } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Image from "next/image";
|
||||
import type { Toy } from "@/types";
|
||||
import { mockToys } from "@/lib/mockData"; // Using all toys for now
|
||||
|
||||
// Mock data: toys rented by the current user
|
||||
// In a real app, this would come from a database query based on rental records
|
||||
const rentedToys: (Toy & { rentalEndDate?: string, dataAiHint?: string })[] = [
|
||||
{ ...mockToys[1], rentalEndDate: "2024-08-15", dataAiHint: mockToys[1]?.category.toLowerCase() }, // Remote Control Car
|
||||
{ ...mockToys[4], rentalEndDate: "2024-09-01", dataAiHint: mockToys[4]?.category.toLowerCase() }, // Beginner Guitar
|
||||
];
|
||||
|
||||
|
||||
export default function MyRentalsPage() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold font-headline text-primary">My Rentals</h1>
|
||||
<p className="text-muted-foreground">Toys you are currently renting from others.</p>
|
||||
</div>
|
||||
|
||||
{rentedToys.length === 0 ? (
|
||||
<Card className="text-center py-12 shadow-md">
|
||||
<CardHeader>
|
||||
<ShoppingBag className="h-16 w-16 mx-auto text-muted-foreground mb-4" />
|
||||
<CardTitle>No Active Rentals</CardTitle>
|
||||
<CardDescription>You haven't rented any toys yet. Explore available toys and find your next adventure!</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Link href="/" passHref>
|
||||
<Button size="lg">
|
||||
<ToyBrick className="mr-2 h-5 w-5" />
|
||||
Browse Toys
|
||||
</Button>
|
||||
</Link>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{rentedToys.map(toy => (
|
||||
<RentalItemCard key={toy.id} toy={toy} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface RentalItemCardProps {
|
||||
toy: Toy & { rentalEndDate?: string, dataAiHint?: string };
|
||||
}
|
||||
|
||||
function RentalItemCard({ toy }: RentalItemCardProps) {
|
||||
const placeholderHint = toy.dataAiHint || toy.category.toLowerCase() || "toy";
|
||||
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/3 lg:w-1/4 relative aspect-video md:aspect-auto">
|
||||
<Image
|
||||
src={toy.images[0] || 'https://placehold.co/300x200.png'}
|
||||
alt={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">
|
||||
<CardTitle className="text-2xl font-headline mb-1">{toy.name}</CardTitle>
|
||||
<p className="text-sm text-muted-foreground mb-2">Rented from: {toy.ownerName}</p>
|
||||
{toy.rentalEndDate && (
|
||||
<p className="text-sm font-semibold text-primary mb-3">
|
||||
Rental ends: {new Date(toy.rentalEndDate).toLocaleDateString()}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-muted-foreground text-sm line-clamp-2 mb-4">{toy.description}</p>
|
||||
<div className="flex space-x-2">
|
||||
<Link href={`/toys/${toy.id}`} passHref>
|
||||
<Button variant="outline" size="sm">View Toy Details</Button>
|
||||
</Link>
|
||||
<Button variant="default" size="sm">Contact Owner</Button> {/* Mock action */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { ListOrdered, Check, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Image from "next/image";
|
||||
import type { Toy } from "@/types";
|
||||
import { mockToys } from "@/lib/mockData";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
interface RentalRequest {
|
||||
id: string;
|
||||
toy: Toy;
|
||||
requesterName: string;
|
||||
requesterId: string;
|
||||
requestedDates: string; // e.g., "Aug 5, 2024 - Aug 10, 2024"
|
||||
status: 'pending' | 'approved' | 'declined';
|
||||
message?: string;
|
||||
dataAiHint?: string;
|
||||
}
|
||||
|
||||
// Mock data: rental requests for the current user's toys
|
||||
const rentalRequests: RentalRequest[] = [
|
||||
{
|
||||
id: 'req1',
|
||||
toy: mockToys[0], // Colorful Building Blocks Set (owned by user1)
|
||||
requesterName: 'Charlie Brown',
|
||||
requesterId: 'user4',
|
||||
requestedDates: 'August 10, 2024 - August 17, 2024',
|
||||
status: 'pending',
|
||||
message: 'My son would love to play with these for his birthday week!',
|
||||
dataAiHint: mockToys[0]?.category.toLowerCase(),
|
||||
},
|
||||
{
|
||||
id: 'req2',
|
||||
toy: mockToys[3], // Plush Teddy Bear (owned by user1)
|
||||
requesterName: 'Diana Prince',
|
||||
requesterId: 'user5',
|
||||
requestedDates: 'September 1, 2024 - September 5, 2024',
|
||||
status: 'approved',
|
||||
dataAiHint: mockToys[3]?.category.toLowerCase(),
|
||||
},
|
||||
{
|
||||
id: 'req3',
|
||||
toy: mockToys[0],
|
||||
requesterName: 'Edward Nigma',
|
||||
requesterId: 'user6',
|
||||
requestedDates: 'July 20, 2024 - July 22, 2024',
|
||||
status: 'declined',
|
||||
message: 'Looking for a weekend rental.',
|
||||
dataAiHint: mockToys[0]?.category.toLowerCase(),
|
||||
},
|
||||
];
|
||||
|
||||
// Assuming current user is user1 for whom these requests are relevant
|
||||
const currentUserToyRequests = rentalRequests.filter(req => req.toy.ownerId === 'user1');
|
||||
|
||||
|
||||
export default function RentalRequestsPage() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold font-headline text-primary">Rental Requests</h1>
|
||||
<p className="text-muted-foreground">Manage incoming rental requests for your toys.</p>
|
||||
</div>
|
||||
|
||||
{currentUserToyRequests.length === 0 ? (
|
||||
<Card className="text-center py-12 shadow-md">
|
||||
<CardHeader>
|
||||
<ListOrdered className="h-16 w-16 mx-auto text-muted-foreground mb-4" />
|
||||
<CardTitle>No Rental Requests</CardTitle>
|
||||
<CardDescription>You currently have no pending rental requests for your toys.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="text-sm text-muted-foreground">When someone requests to rent one of your toys, it will appear here.</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{currentUserToyRequests.map(request => (
|
||||
<RequestItemCard key={request.id} request={request} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
interface RequestItemCardProps {
|
||||
request: RentalRequest;
|
||||
}
|
||||
|
||||
function RequestItemCard({ request }: RequestItemCardProps) {
|
||||
const placeholderHint = request.dataAiHint || request.toy.category.toLowerCase() || "toy";
|
||||
|
||||
const getStatusBadgeVariant = (status: RentalRequest['status']) => {
|
||||
if (status === 'approved') return 'default'; // default is primary
|
||||
if (status === 'declined') return 'destructive';
|
||||
return 'secondary'; // pending
|
||||
};
|
||||
|
||||
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-auto">
|
||||
<Image
|
||||
src={request.toy.images[0] || 'https://placehold.co/200x150.png'}
|
||||
alt={request.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">{request.toy.name}</CardTitle>
|
||||
<Badge variant={getStatusBadgeVariant(request.status)} className="capitalize">{request.status}</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Requested by: <span className="font-medium text-foreground">{request.requesterName}</span>
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Dates: <span className="font-medium text-foreground">{request.requestedDates}</span>
|
||||
</p>
|
||||
{request.message && (
|
||||
<p className="text-sm text-muted-foreground mt-2 bg-muted/50 p-2 rounded-md">
|
||||
<span className="font-medium text-foreground">Message:</span> "{request.message}"
|
||||
</p>
|
||||
)}
|
||||
|
||||
{request.status === 'pending' && (
|
||||
<div className="mt-4 flex space-x-3">
|
||||
<Button size="sm" className="bg-green-600 hover:bg-green-700">
|
||||
<Check className="mr-1 h-4 w-4" /> Approve
|
||||
</Button>
|
||||
<Button variant="destructive" size="sm">
|
||||
<X className="mr-1 h-4 w-4" /> Decline
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">Message Requester</Button>
|
||||
</div>
|
||||
)}
|
||||
{request.status !== 'pending' && (
|
||||
<div className="mt-4">
|
||||
<Link href={`/dashboard/my-toys/edit/${request.toy.id}`} passHref>
|
||||
<Button variant="link" size="sm" className="p-0 h-auto">View Toy Listing</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -3,78 +3,99 @@
|
|||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
font-family: var(--font-body), sans-serif;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--background: 208 100% 97%; /* Very Light Blue #F0F8FF */
|
||||
--foreground: 210 25% 25%; /* Darker blue-gray for contrast */
|
||||
|
||||
--muted: 210 30% 90%; /* Lighter muted blue */
|
||||
--muted-foreground: 210 25% 45%; /* Slightly lighter than main foreground */
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--popover-foreground: 210 25% 25%;
|
||||
|
||||
--card: 0 0% 100%; /* White cards */
|
||||
--card-foreground: 210 25% 25%;
|
||||
|
||||
--border: 210 30% 80%;
|
||||
--input: 210 30% 85%;
|
||||
|
||||
--primary: 210 40% 60%; /* Muted Blue #6699CC */
|
||||
--primary-foreground: 210 40% 98%; /* Very light, almost white */
|
||||
|
||||
--secondary: 210 30% 92%; /* Lighter muted blue, slightly different from muted */
|
||||
--secondary-foreground: 210 30% 30%;
|
||||
|
||||
--accent: 120 25% 65%; /* Soft Green #8FBC8F */
|
||||
--accent-foreground: 120 25% 20%; /* Dark green for contrast */
|
||||
|
||||
--destructive: 0 72% 51%; /* A standard destructive red */
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
|
||||
--ring: 210 40% 60%; /* Muted Blue for ring */
|
||||
|
||||
--radius: 0.5rem;
|
||||
--sidebar-background: 0 0% 98%;
|
||||
--sidebar-foreground: 240 5.3% 26.1%;
|
||||
--sidebar-primary: 240 5.9% 10%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
--sidebar-accent: 240 4.8% 95.9%;
|
||||
--sidebar-accent-foreground: 240 5.9% 10%;
|
||||
--sidebar-border: 220 13% 91%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
}
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
|
||||
--chart-1: 210 40% 60%;
|
||||
--chart-2: 120 25% 65%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
--sidebar-background: 240 5.9% 10%;
|
||||
--sidebar-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-primary: 224.3 76.3% 48%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 240 3.7% 15.9%;
|
||||
--sidebar-accent-foreground: 240 4.8% 95.9%;
|
||||
--sidebar-border: 240 3.7% 15.9%;
|
||||
--sidebar-ring: 217.2 91.2% 59.8%;
|
||||
|
||||
--sidebar-background: 208 60% 94%;
|
||||
--sidebar-foreground: 210 25% 20%;
|
||||
--sidebar-primary: 210 40% 55%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
--sidebar-accent: 210 40% 88%;
|
||||
--sidebar-accent-foreground: 210 25% 15%;
|
||||
--sidebar-border: 210 30% 75%;
|
||||
--sidebar-ring: 210 40% 55%;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 210 20% 12%;
|
||||
--foreground: 210 20% 90%;
|
||||
|
||||
--muted: 210 20% 20%;
|
||||
--muted-foreground: 210 20% 60%;
|
||||
|
||||
--popover: 210 20% 10%;
|
||||
--popover-foreground: 210 20% 90%;
|
||||
|
||||
--card: 210 20% 15%;
|
||||
--card-foreground: 210 20% 90%;
|
||||
|
||||
--border: 210 20% 25%;
|
||||
--input: 210 20% 22%;
|
||||
|
||||
--primary: 210 40% 60%;
|
||||
--primary-foreground: 210 40% 10%;
|
||||
|
||||
--secondary: 210 20% 25%;
|
||||
--secondary-foreground: 210 20% 90%;
|
||||
|
||||
--accent: 120 25% 65%;
|
||||
--accent-foreground: 120 25% 95%;
|
||||
|
||||
--destructive: 0 60% 50%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--ring: 210 40% 60%;
|
||||
|
||||
--chart-1: 210 40% 60%;
|
||||
--chart-2: 120 25% 65%;
|
||||
|
||||
--sidebar-background: 210 20% 10%;
|
||||
--sidebar-foreground: 210 20% 85%;
|
||||
--sidebar-primary: 210 40% 55%;
|
||||
--sidebar-primary-foreground: 0 0% 98%;
|
||||
--sidebar-accent: 210 20% 20%;
|
||||
--sidebar-accent-foreground: 210 20% 90%;
|
||||
--sidebar-border: 210 20% 25%;
|
||||
--sidebar-ring: 210 40% 55%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -84,5 +105,7 @@ body {
|
|||
}
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,23 @@
|
|||
import type {Metadata} from 'next';
|
||||
import type { Metadata } from 'next';
|
||||
import { PT_Sans } from 'next/font/google';
|
||||
import './globals.css';
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import Header from '@/components/layout/Header';
|
||||
import Footer from '@/components/layout/Footer';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const ptSans = PT_Sans({
|
||||
subsets: ['latin'],
|
||||
weight: ['400', '700'],
|
||||
variable: '--font-body',
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Firebase Studio App',
|
||||
description: 'Generated by Firebase Studio',
|
||||
title: 'ToyShare - Share and Rent Toys',
|
||||
description: 'A friendly platform to share and rent toys in your community.',
|
||||
icons: {
|
||||
icon: '/favicon.ico', // Basic favicon, can be improved
|
||||
}
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
|
|
@ -12,13 +26,20 @@ export default function RootLayout({
|
|||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<head>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter&display=swap" rel="stylesheet"></link>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||
<link href="https://fonts.googleapis.com/css2?family=PT+Sans:wght@400;700&display=swap" rel="stylesheet" />
|
||||
</head>
|
||||
<body className="font-body antialiased">{children}</body>
|
||||
<body className={cn('min-h-screen bg-background font-body antialiased flex flex-col', ptSans.variable)}>
|
||||
<Header />
|
||||
<main className="flex-grow container mx-auto px-4 py-8">
|
||||
{children}
|
||||
</main>
|
||||
<Footer />
|
||||
<Toaster />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,107 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
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 { LogIn } from 'lucide-react';
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
|
||||
export default function LoginPage() {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
// Mock API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Mock success
|
||||
// In a real app, you'd validate credentials against a backend
|
||||
if (email === "user@example.com" && password === "password") {
|
||||
localStorage.setItem('isToyShareAuthenticated', 'true'); // Mock auth persistence
|
||||
toast({
|
||||
title: "Login Successful",
|
||||
description: "Welcome back!",
|
||||
});
|
||||
router.push('/dashboard');
|
||||
} else {
|
||||
toast({
|
||||
title: "Login Failed",
|
||||
description: "Invalid email or password. (Hint: user@example.com / password)",
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-[calc(100vh-12rem)] py-12">
|
||||
<Card className="w-full max-w-md shadow-xl">
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto bg-primary text-primary-foreground rounded-full p-3 w-fit mb-4">
|
||||
<LogIn className="h-8 w-8" />
|
||||
</div>
|
||||
<CardTitle className="text-3xl font-headline">Welcome Back!</CardTitle>
|
||||
<CardDescription>Log in to your ToyShare account to continue.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email Address</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? 'Logging in...' : 'Log In'}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter className="flex flex-col gap-4 text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Forgot your password? <Link href="#" className="text-primary hover:underline">Reset it here</Link>.
|
||||
</p>
|
||||
<Separator />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Don't have an account?{' '}
|
||||
<Link href="/register" className="text-primary font-semibold hover:underline">
|
||||
Sign up now
|
||||
</Link>
|
||||
</p>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Simple separator, can be moved to ui components if used elsewhere
|
||||
function Separator() {
|
||||
return <div className="h-px w-full bg-border my-2" />;
|
||||
}
|
||||
|
|
@ -1,3 +1,40 @@
|
|||
export default function Home() {
|
||||
return <></>;
|
||||
import ToyList from '@/components/toys/ToyList';
|
||||
import { mockToys } from '@/lib/mockData';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import Link from 'next/link';
|
||||
import { PlusCircle } from 'lucide-react';
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<section className="text-center py-12 bg-gradient-to-r from-primary/10 via-background to-accent/10 rounded-lg shadow">
|
||||
<h1 className="text-4xl md:text-5xl font-bold font-headline text-primary mb-4">
|
||||
Welcome to ToyShare!
|
||||
</h1>
|
||||
<p className="text-lg text-foreground/80 max-w-2xl mx-auto mb-8">
|
||||
Discover a world of fun! Share your beloved toys or find new adventures by renting from our friendly community.
|
||||
</p>
|
||||
<div className="space-x-4">
|
||||
<Link href="/#toy-listings" passHref>
|
||||
<Button size="lg" variant="default" className="transition-transform transform hover:scale-105">
|
||||
Explore Toys
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/dashboard/my-toys/add" passHref>
|
||||
<Button size="lg" variant="outline" className="transition-transform transform hover:scale-105">
|
||||
<PlusCircle className="mr-2 h-5 w-5" />
|
||||
Share Your Toy
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="toy-listings" className="pt-8">
|
||||
<h2 className="text-3xl font-bold font-headline text-center mb-8 text-primary">
|
||||
Available Toys
|
||||
</h2>
|
||||
<ToyList toys={mockToys.map(toy => ({...toy, dataAiHint: toy.category.toLowerCase()}))} />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,127 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
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 { UserPlus } from 'lucide-react';
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
|
||||
export default function RegisterPage() {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const [name, setName] = useState('');
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
toast({
|
||||
title: "Registration Error",
|
||||
description: "Passwords do not match.",
|
||||
variant: "destructive",
|
||||
});
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mock API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Mock success
|
||||
// In a real app, you'd save the user to a database
|
||||
localStorage.setItem('isToyShareAuthenticated', 'true'); // Mock auth persistence
|
||||
toast({
|
||||
title: "Registration Successful",
|
||||
description: "Your account has been created. Welcome!",
|
||||
});
|
||||
router.push('/dashboard');
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex justify-center items-center min-h-[calc(100vh-12rem)] py-12">
|
||||
<Card className="w-full max-w-md shadow-xl">
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto bg-primary text-primary-foreground rounded-full p-3 w-fit mb-4">
|
||||
<UserPlus className="h-8 w-8" />
|
||||
</div>
|
||||
<CardTitle className="text-3xl font-headline">Create Your Account</CardTitle>
|
||||
<CardDescription>Join ToyShare and start sharing the fun!</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name">Full Name</Label>
|
||||
<Input
|
||||
id="name"
|
||||
type="text"
|
||||
placeholder="John Doe"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email Address</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder="you@example.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="confirmPassword">Confirm Password</Label>
|
||||
<Input
|
||||
id="confirmPassword"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
required
|
||||
disabled={isLoading}
|
||||
/>
|
||||
</div>
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? 'Registering...' : 'Create Account'}
|
||||
</Button>
|
||||
</form>
|
||||
</CardContent>
|
||||
<CardFooter className="text-center">
|
||||
<p className="text-sm text-muted-foreground w-full">
|
||||
Already have an account?{' '}
|
||||
<Link href="/login" className="text-primary font-semibold hover:underline">
|
||||
Log in
|
||||
</Link>
|
||||
</p>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
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 Link from 'next/link';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
|
||||
interface ToyPageProps {
|
||||
params: { id: string };
|
||||
}
|
||||
|
||||
// Server Component to fetch toy data (mocked for now)
|
||||
async function getToyById(id: string): Promise<Toy | undefined> {
|
||||
// In a real app, this would fetch from a database
|
||||
await new Promise(resolve => setTimeout(resolve, 200)); // Simulate network delay
|
||||
return mockToys.find(toy => toy.id === id);
|
||||
}
|
||||
|
||||
export default async function ToyPage({ params }: ToyPageProps) {
|
||||
const toy = await getToyById(params.id);
|
||||
|
||||
if (!toy) {
|
||||
return (
|
||||
<div className="text-center py-12">
|
||||
<h1 className="text-2xl font-bold mb-4">Toy Not Found</h1>
|
||||
<p className="text-muted-foreground mb-6">Sorry, the toy you are looking for does not exist or has been removed.</p>
|
||||
<Link href="/" passHref>
|
||||
<Button variant="outline">
|
||||
<ArrowLeft className="mr-2 h-4 w-4" />
|
||||
Back to All Toys
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const placeholderHint = toy.category.toLowerCase() || "toy detail";
|
||||
|
||||
return (
|
||||
<div className="container mx-auto py-8 px-4">
|
||||
<Link href="/" className="inline-flex items-center text-primary hover:underline mb-6 group">
|
||||
<ArrowLeft className="mr-2 h-4 w-4 transition-transform group-hover:-translate-x-1" />
|
||||
Back to All Toys
|
||||
</Link>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-8 lg:gap-12 items-start">
|
||||
{/* Image Gallery Section */}
|
||||
<div className="space-y-4">
|
||||
<div className="aspect-video relative w-full rounded-lg overflow-hidden shadow-lg">
|
||||
<Image
|
||||
src={toy.images[0] || 'https://placehold.co/600x400.png'}
|
||||
alt={toy.name}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
data-ai-hint={placeholderHint + " main"}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
{toy.images.length > 1 && (
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{toy.images.slice(1, 4).map((img, index) => (
|
||||
<div key={index} className="aspect-square relative w-full rounded-md overflow-hidden shadow">
|
||||
<Image
|
||||
src={img}
|
||||
alt={`${toy.name} - image ${index + 2}`}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
data-ai-hint={placeholderHint + " thumbnail"}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Toy Details Section */}
|
||||
<div className="space-y-6">
|
||||
<Badge variant="secondary" className="text-sm">{toy.category}</Badge>
|
||||
<h1 className="text-4xl font-bold font-headline text-primary">{toy.name}</h1>
|
||||
|
||||
<div className="text-lg text-foreground/90 leading-relaxed">
|
||||
<p>{toy.description}</p>
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
|
||||
<div className="flex items-center">
|
||||
<UserCircle2 className="h-5 w-5 mr-2 text-accent" />
|
||||
<div>
|
||||
<span className="font-medium text-muted-foreground">Owner: </span>
|
||||
<span className="text-foreground">{toy.ownerName}</span>
|
||||
</div>
|
||||
</div>
|
||||
{toy.location && (
|
||||
<div className="flex items-center">
|
||||
<MapPin className="h-5 w-5 mr-2 text-accent" />
|
||||
<div>
|
||||
<span className="font-medium text-muted-foreground">Location: </span>
|
||||
<span className="text-foreground">{toy.location}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{toy.pricePerDay !== undefined && (
|
||||
<div className="flex items-center col-span-full sm:col-span-1">
|
||||
<DollarSign className="h-5 w-5 mr-2 text-accent" />
|
||||
<div>
|
||||
<span className="font-medium text-muted-foreground">Price: </span>
|
||||
<span className="text-foreground font-semibold">{toy.pricePerDay > 0 ? `$${toy.pricePerDay}/day` : 'Free'}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<AvailabilityCalendar availability={toy.availability} />
|
||||
|
||||
<Button size="lg" className="w-full mt-6 transition-transform transform hover:scale-105">
|
||||
<ShoppingBag className="mr-2 h-5 w-5" /> Request to Rent
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Generate static paths for all toys
|
||||
export async function generateStaticParams() {
|
||||
return mockToys.map((toy) => ({
|
||||
id: toy.id,
|
||||
}));
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { Home, ToyBrick, PlusCircle, ListOrdered, User, LogOut, Settings, ShoppingBag } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
const sidebarNavItems = [
|
||||
{ href: '/dashboard', label: 'Overview', icon: Home },
|
||||
{ href: '/dashboard/my-toys', label: 'My Toys', icon: ToyBrick },
|
||||
{ href: '/dashboard/my-toys/add', label: 'Add New Toy', icon: PlusCircle },
|
||||
{ href: '/dashboard/rentals', label: 'My Rentals', icon: ShoppingBag }, // Toys I'm renting
|
||||
{ href: '/dashboard/requests', label: 'Rental Requests', icon: ListOrdered }, // Requests for my toys
|
||||
];
|
||||
|
||||
const accountNavItems = [
|
||||
{ href: '/dashboard/profile', label: 'Profile Settings', icon: Settings },
|
||||
];
|
||||
|
||||
export default function DashboardSidebar() {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('isToyShareAuthenticated'); // Mock logout
|
||||
toast({ description: "You have been logged out." });
|
||||
router.push('/');
|
||||
};
|
||||
|
||||
const NavLink = ({ href, label, icon: Icon }: typeof sidebarNavItems[0]) => {
|
||||
const isActive = pathname === href || (href !== '/dashboard' && pathname.startsWith(href));
|
||||
return (
|
||||
<Link href={href} passHref>
|
||||
<Button
|
||||
variant={isActive ? 'secondary' : 'ghost'}
|
||||
className={cn('w-full justify-start', isActive && 'font-semibold')}
|
||||
>
|
||||
<Icon className="mr-3 h-5 w-5" />
|
||||
{label}
|
||||
</Button>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
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">
|
||||
<ToyBrick className="h-7 w-7" />
|
||||
<h2 className="text-xl font-headline font-bold">ToyShare</h2>
|
||||
</Link>
|
||||
<p className="text-xs text-muted-foreground">User Dashboard</p>
|
||||
</div>
|
||||
|
||||
<nav className="flex-grow space-y-1">
|
||||
<p className="px-3 py-2 text-xs font-medium text-muted-foreground">Toy Management</p>
|
||||
{sidebarNavItems.map((item) => (
|
||||
<NavLink key={item.href} {...item} />
|
||||
))}
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
<p className="px-3 py-2 text-xs font-medium text-muted-foreground">Account</p>
|
||||
{accountNavItems.map((item) => (
|
||||
<NavLink key={item.href} {...item} />
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<Separator className="my-4" />
|
||||
|
||||
<div>
|
||||
<Button variant="ghost" className="w-full justify-start text-red-600 hover:bg-red-100 hover:text-red-700" onClick={handleLogout}>
|
||||
<LogOut className="mr-3 h-5 w-5" />
|
||||
Logout
|
||||
</Button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
export default function Footer() {
|
||||
return (
|
||||
<footer className="bg-muted/50 border-t border-border py-8 text-center text-muted-foreground">
|
||||
<div className="container mx-auto px-4">
|
||||
<p className="text-sm">
|
||||
© {new Date().getFullYear()} ToyShare. All rights reserved.
|
||||
</p>
|
||||
<p className="text-xs mt-1">
|
||||
Sharing happiness, one toy at a time.
|
||||
</p>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { ToyBrick, UserCircle2, LogIn, UserPlus, LayoutDashboard } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
export default function Header() {
|
||||
const pathname = usePathname();
|
||||
// Mock authentication state
|
||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||
// This effect runs only on the client after hydration
|
||||
useEffect(() => {
|
||||
// In a real app, you'd check localStorage or an auth context
|
||||
// For now, we'll simulate it.
|
||||
// To test logged-in state, you can manually set this in browser console: localStorage.setItem('isToyShareAuthenticated', 'true');
|
||||
const authStatus = localStorage.getItem('isToyShareAuthenticated');
|
||||
setIsAuthenticated(authStatus === 'true');
|
||||
}, [pathname]); // Re-check on path change, e.g. after login/logout actions
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('isToyShareAuthenticated');
|
||||
setIsAuthenticated(false);
|
||||
// Potentially redirect to home or login page
|
||||
// router.push('/'); // if using useRouter
|
||||
};
|
||||
|
||||
|
||||
return (
|
||||
<header className="bg-card border-b border-border shadow-sm sticky top-0 z-50">
|
||||
<div className="container mx-auto px-4 h-16 flex items-center justify-between">
|
||||
<Link href="/" className="flex items-center gap-2 text-primary hover:text-primary/80 transition-colors">
|
||||
<ToyBrick className="h-7 w-7" />
|
||||
<h1 className="text-2xl font-headline font-bold">ToyShare</h1>
|
||||
</Link>
|
||||
<nav className="flex items-center gap-4">
|
||||
<Link href="/" passHref>
|
||||
<Button variant={pathname === '/' ? 'secondary' : 'ghost'} size="sm">
|
||||
Browse Toys
|
||||
</Button>
|
||||
</Link>
|
||||
{isAuthenticated ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="rounded-full">
|
||||
<UserCircle2 className="h-6 w-6" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>My Account</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/dashboard">
|
||||
<LayoutDashboard className="mr-2 h-4 w-4" />
|
||||
Dashboard
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
<Link href="/dashboard/profile">
|
||||
<UserCircle2 className="mr-2 h-4 w-4" />
|
||||
Profile
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={handleLogout}>
|
||||
<LogIn className="mr-2 h-4 w-4" />
|
||||
Logout
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : (
|
||||
<>
|
||||
<Link href="/login" passHref>
|
||||
<Button variant={pathname === '/login' ? 'secondary' : 'ghost'} size="sm">
|
||||
<LogIn className="mr-2 h-4 w-4" />
|
||||
Login
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/register" passHref>
|
||||
<Button variant="default" size="sm">
|
||||
<UserPlus className="mr-2 h-4 w-4" />
|
||||
Register
|
||||
</Button>
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,216 @@
|
|||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
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 { 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, UploadCloud, Save } from 'lucide-react';
|
||||
import type { Toy } from '@/types';
|
||||
|
||||
const toyCategories = ["Educational", "Vehicles", "Electronics", "Plush Toys", "Musical", "Outdoor", "Board Games", "Action Figures", "Dolls", "Puzzles", "Arts & Crafts", "Building Blocks"];
|
||||
const daysOfWeek = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'] as const;
|
||||
|
||||
|
||||
interface AddToyFormProps {
|
||||
initialData?: Partial<Toy>; // For editing existing toys
|
||||
isEditMode?: boolean;
|
||||
}
|
||||
|
||||
export default function AddToyForm({ initialData, isEditMode = false }: AddToyFormProps) {
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
|
||||
const [name, setName] = useState(initialData?.name || '');
|
||||
const [description, setDescription] = useState(initialData?.description || '');
|
||||
const [category, setCategory] = useState(initialData?.category || '');
|
||||
const [pricePerDay, setPricePerDay] = useState(initialData?.pricePerDay?.toString() || '0');
|
||||
const [location, setLocation] = useState(initialData?.location || '');
|
||||
const [images, setImages] = useState<string[]>(initialData?.images || ['']); // Store image URLs
|
||||
const [availability, setAvailability] = useState<Toy['availability']>(
|
||||
initialData?.availability || {
|
||||
monday: true, tuesday: true, wednesday: true, thursday: true, friday: true, saturday: false, sunday: false
|
||||
}
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleImageChange = (index: number, value: string) => {
|
||||
const newImages = [...images];
|
||||
newImages[index] = value;
|
||||
setImages(newImages);
|
||||
};
|
||||
|
||||
const addImageField = () => setImages([...images, '']);
|
||||
const removeImageField = (index: number) => {
|
||||
if (images.length > 1) {
|
||||
setImages(images.filter((_, i) => i !== index));
|
||||
} else {
|
||||
setImages(['']); // Keep at least one field, but clear it
|
||||
}
|
||||
};
|
||||
|
||||
const handleAvailabilityChange = (day: keyof Toy['availability']) => {
|
||||
setAvailability(prev => ({ ...prev, [day]: !prev[day] }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault();
|
||||
setIsLoading(true);
|
||||
|
||||
// Form validation (basic example)
|
||||
if (!name || !description || !category) {
|
||||
toast({ title: "Missing Fields", description: "Please fill in all required fields.", variant: "destructive" });
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const toyData = {
|
||||
name, description, category,
|
||||
pricePerDay: parseFloat(pricePerDay) || 0,
|
||||
location,
|
||||
images: images.filter(img => img.trim() !== ''), // Filter out empty image URLs
|
||||
availability,
|
||||
// ownerId and id would be set by backend
|
||||
};
|
||||
|
||||
// Mock API call
|
||||
console.log("Submitting toy data:", toyData);
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
toast({
|
||||
title: isEditMode ? "Toy Updated!" : "Toy Added!",
|
||||
description: `${name} has been successfully ${isEditMode ? 'updated' : 'listed'}.`,
|
||||
});
|
||||
router.push('/dashboard/my-toys'); // Redirect after success
|
||||
// Optionally, could clear form or reset state here
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-2xl mx-auto shadow-xl">
|
||||
<CardHeader className="text-center">
|
||||
<div className="mx-auto bg-primary text-primary-foreground rounded-full p-3 w-fit mb-4">
|
||||
<ToyBrick className="h-8 w-8" />
|
||||
</div>
|
||||
<CardTitle className="text-3xl font-headline">{isEditMode ? 'Edit Your Toy' : 'Share a New Toy'}</CardTitle>
|
||||
<CardDescription>
|
||||
{isEditMode ? 'Update the details of your toy listing.' : 'Fill in the details below to list your toy for others to enjoy.'}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<form onSubmit={handleSubmit} className="space-y-8">
|
||||
{/* Toy Name */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="name" className="text-base">Toy Name</Label>
|
||||
<Input id="name" value={name} onChange={(e) => setName(e.target.value)} placeholder="e.g., Red Racing Car" required disabled={isLoading} />
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="description" className="text-base">Description</Label>
|
||||
<Textarea id="description" value={description} onChange={(e) => setDescription(e.target.value)} placeholder="Describe your toy, its condition, and any accessories." required disabled={isLoading} rows={4} />
|
||||
</div>
|
||||
|
||||
{/* Category */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="category" className="text-base">Category</Label>
|
||||
<Select value={category} onValueChange={setCategory} required disabled={isLoading}>
|
||||
<SelectTrigger id="category">
|
||||
<SelectValue placeholder="Select a category" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{toyCategories.map(cat => <SelectItem key={cat} value={cat}>{cat}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Price per Day */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="pricePerDay" className="text-base">Rental Price per Day ($)</Label>
|
||||
<Input id="pricePerDay" type="number" value={pricePerDay} onChange={(e) => setPricePerDay(e.target.value)} placeholder="0 for free" min="0" step="0.50" disabled={isLoading} />
|
||||
</div>
|
||||
|
||||
{/* Location */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="location" className="text-base">Location (Optional)</Label>
|
||||
<Input id="location" value={location} onChange={(e) => setLocation(e.target.value)} placeholder="e.g., Springfield Park Area" disabled={isLoading} />
|
||||
</div>
|
||||
|
||||
{/* Image URLs */}
|
||||
<div className="space-y-4">
|
||||
<Label className="text-base">Toy Images (URLs)</Label>
|
||||
<p className="text-sm text-muted-foreground">Enter direct URLs to your toy images. Add up to 5 images.</p>
|
||||
{images.map((imgUrl, index) => (
|
||||
<div key={index} className="flex items-center gap-2">
|
||||
<Input
|
||||
type="url"
|
||||
value={imgUrl}
|
||||
onChange={(e) => handleImageChange(index, e.target.value)}
|
||||
placeholder={`Image URL ${index + 1}`}
|
||||
disabled={isLoading}
|
||||
/>
|
||||
{images.length > 1 && (
|
||||
<Button type="button" variant="ghost" size="icon" onClick={() => removeImageField(index)} disabled={isLoading}>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{images.length < 5 && (
|
||||
<Button type="button" variant="outline" onClick={addImageField} disabled={isLoading}>
|
||||
<PlusCircle className="mr-2 h-4 w-4" /> Add Another Image
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Availability */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-base">Weekly Availability</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 ? 'Saving Changes...' : 'Listing Toy...') : (
|
||||
<>
|
||||
<Save className="mr-2 h-5 w-5" />
|
||||
{isEditMode ? 'Save Changes' : 'List My Toy'}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Icon for file upload, not used in URL version but good for future
|
||||
function FileUploadIcon() {
|
||||
return (
|
||||
<div className="border-2 border-dashed border-border rounded-lg p-6 text-center cursor-pointer hover:border-primary transition-colors">
|
||||
<UploadCloud className="mx-auto h-12 w-12 text-muted-foreground" />
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
<span className="font-semibold text-primary">Click to upload</span> or drag and drop
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">PNG, JPG, GIF up to 10MB</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
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>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import type { Toy } from '@/types';
|
||||
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { DollarSign, MapPin, Tag } from 'lucide-react';
|
||||
|
||||
interface ToyCardProps {
|
||||
toy: Toy & { dataAiHint?: string }; // dataAiHint is for placeholder, not part of core Toy type
|
||||
}
|
||||
|
||||
export default function ToyCard({ toy }: ToyCardProps) {
|
||||
const primaryImage = toy.images && toy.images.length > 0 ? toy.images[0] : 'https://placehold.co/300x200.png';
|
||||
const placeholderHint = toy.dataAiHint || toy.category.toLowerCase() || "toy";
|
||||
|
||||
return (
|
||||
<Card className="flex flex-col h-full overflow-hidden shadow-lg hover:shadow-xl transition-shadow duration-300 rounded-lg">
|
||||
<CardHeader className="p-0">
|
||||
<div className="aspect-[3/2] relative w-full">
|
||||
<Image
|
||||
src={primaryImage}
|
||||
alt={toy.name}
|
||||
layout="fill"
|
||||
objectFit="cover"
|
||||
data-ai-hint={placeholderHint}
|
||||
className="rounded-t-lg"
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 flex-grow">
|
||||
<CardTitle className="text-lg font-headline mb-2 truncate" title={toy.name}>{toy.name}</CardTitle>
|
||||
<div className="text-sm text-muted-foreground space-y-1">
|
||||
<div className="flex items-center">
|
||||
<Tag className="h-4 w-4 mr-2 text-accent" />
|
||||
<span>{toy.category}</span>
|
||||
</div>
|
||||
{toy.location && (
|
||||
<div className="flex items-center">
|
||||
<MapPin className="h-4 w-4 mr-2 text-accent" />
|
||||
<span>{toy.location}</span>
|
||||
</div>
|
||||
)}
|
||||
{toy.pricePerDay !== undefined && (
|
||||
<div className="flex items-center font-semibold text-foreground">
|
||||
<DollarSign className="h-4 w-4 mr-1 text-accent" />
|
||||
<span>{toy.pricePerDay > 0 ? `${toy.pricePerDay}/day` : 'Free'}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="p-4 border-t">
|
||||
<Link href={`/toys/${toy.id}`} passHref className="w-full">
|
||||
<Button variant="default" className="w-full transition-transform transform hover:scale-105">
|
||||
View Details
|
||||
</Button>
|
||||
</Link>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import type { Toy } from '@/types';
|
||||
import ToyCard from './ToyCard';
|
||||
|
||||
interface ToyListProps {
|
||||
toys: (Toy & { dataAiHint?: string })[];
|
||||
}
|
||||
|
||||
export default function ToyList({ toys }: ToyListProps) {
|
||||
if (!toys || toys.length === 0) {
|
||||
return <p className="text-center text-muted-foreground py-8">No toys available at the moment. Check back soon!</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
|
||||
{toys.map((toy) => (
|
||||
<ToyCard key={toy.id} toy={toy} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
import type { Toy } from '@/types';
|
||||
|
||||
export const mockToys: Toy[] = [
|
||||
{
|
||||
id: '1',
|
||||
name: 'Colorful Building Blocks Set',
|
||||
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 },
|
||||
ownerName: 'Alice Wonderland',
|
||||
ownerId: 'user1',
|
||||
pricePerDay: 5,
|
||||
location: 'Springfield Gardens',
|
||||
dataAiHint: 'building blocks'
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
name: 'Remote Control Racing Car',
|
||||
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 },
|
||||
ownerName: 'Bob The Builder',
|
||||
ownerId: 'user2',
|
||||
pricePerDay: 8,
|
||||
location: 'Willow Creek',
|
||||
dataAiHint: 'remote car'
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
name: 'Interactive Learning Tablet',
|
||||
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 },
|
||||
ownerName: 'Carol Danvers',
|
||||
ownerId: 'user3',
|
||||
pricePerDay: 7,
|
||||
location: 'Metro City',
|
||||
dataAiHint: 'learning tablet'
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
name: 'Plush Teddy Bear Large',
|
||||
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: 'David Copperfield',
|
||||
ownerId: 'user1',
|
||||
pricePerDay: 3,
|
||||
location: 'Springfield Gardens',
|
||||
dataAiHint: 'teddy bear'
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
name: 'Beginner Acoustic Guitar',
|
||||
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: 'Eve Adamson',
|
||||
ownerId: 'user2',
|
||||
pricePerDay: 10,
|
||||
location: 'Willow Creek',
|
||||
dataAiHint: 'acoustic guitar'
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
name: 'Outdoor Sports Kit',
|
||||
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: 'Frank Castle',
|
||||
ownerId: 'user3',
|
||||
pricePerDay: 6,
|
||||
location: 'Metro City',
|
||||
dataAiHint: 'outdoor sports'
|
||||
}
|
||||
];
|
||||
|
||||
// Add dataAiHint to mockToys where Toy might have it.
|
||||
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.
|
||||
}
|
||||
});
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
|
||||
export interface Toy {
|
||||
id: string;
|
||||
name: string;
|
||||
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;
|
||||
};
|
||||
ownerName: string; // Simplified for now
|
||||
ownerId: string;
|
||||
pricePerDay?: number; // Optional daily rental price
|
||||
location?: string; // Optional, e.g., "City, State" or "Neighborhood"
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
// a real app would have hashed passwords, etc.
|
||||
}
|
||||
|
||||
// Represents the availability of a toy for a specific day
|
||||
export interface DailyAvailability {
|
||||
date: Date;
|
||||
isAvailable: boolean;
|
||||
bookedBy?: string; // User ID of the renter if booked
|
||||
}
|
||||
|
|
@ -8,10 +8,17 @@ export default {
|
|||
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
],
|
||||
theme: {
|
||||
container: {
|
||||
center: true,
|
||||
padding: "2rem",
|
||||
screens: {
|
||||
"2xl": "1400px",
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
fontFamily: {
|
||||
body: ['Inter', 'sans-serif'],
|
||||
headline: ['Inter', 'sans-serif'],
|
||||
body: ['PT Sans', 'sans-serif'],
|
||||
headline: ['PT Sans', 'sans-serif'],
|
||||
code: ['monospace'],
|
||||
},
|
||||
colors: {
|
||||
|
|
@ -88,10 +95,15 @@ export default {
|
|||
height: '0',
|
||||
},
|
||||
},
|
||||
"caret-blink": {
|
||||
"0%,70%,100%": { opacity: "1" },
|
||||
"20%,50%": { opacity: "0" },
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||
"caret-blink": "caret-blink 1.25s ease-out infinite",
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue