Updated app

This commit is contained in:
Indigo Tang 2025-06-09 02:49:59 +00:00
parent 7d3cda96fb
commit 3db4ec2066
26 changed files with 1953 additions and 74 deletions

0
.modified Normal file
View File

19
docs/blueprint.md Normal file
View File

@ -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.

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

106
src/app/dashboard/page.tsx Normal file
View File

@ -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} &rarr;
</Link>
</CardContent>
</Card>
);
}

View File

@ -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>
);
}

View File

@ -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&apos;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>
);
}

View File

@ -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>
);
}

View File

@ -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;
}
}

View File

@ -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>
);
}

107
src/app/login/page.tsx Normal file
View File

@ -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&apos;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" />;
}

View File

@ -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>
);
}

127
src/app/register/page.tsx Normal file
View File

@ -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>
);
}

136
src/app/toys/[id]/page.tsx Normal file
View File

@ -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,
}));
}

View File

@ -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>
);
}

View File

@ -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">
&copy; {new Date().getFullYear()} ToyShare. All rights reserved.
</p>
<p className="text-xs mt-1">
Sharing happiness, one toy at a time.
</p>
</div>
</footer>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

View File

@ -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>
);
}

93
src/lib/mockData.ts Normal file
View File

@ -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.
}
});

35
src/types/index.ts Normal file
View File

@ -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
}

View File

@ -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",
},
},
},