diff --git a/.modified b/.modified
new file mode 100644
index 0000000..e69de29
diff --git a/docs/blueprint.md b/docs/blueprint.md
new file mode 100644
index 0000000..0c6f5ad
--- /dev/null
+++ b/docs/blueprint.md
@@ -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.
\ No newline at end of file
diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx
new file mode 100644
index 0000000..8444f3b
--- /dev/null
+++ b/src/app/dashboard/layout.tsx
@@ -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 (
+
+
+
Loading Dashboard...
+
+ );
+ }
+
+ return (
+ {/* Adjust min-height based on header height */}
+
+
+ {children}
+
+
+ );
+}
diff --git a/src/app/dashboard/my-toys/add/page.tsx b/src/app/dashboard/my-toys/add/page.tsx
new file mode 100644
index 0000000..2dc73b7
--- /dev/null
+++ b/src/app/dashboard/my-toys/add/page.tsx
@@ -0,0 +1,11 @@
+import AddToyForm from '@/components/toys/AddToyForm';
+
+export default function AddNewToyPage() {
+ return (
+
+ {/* Page Title can be part of the AddToyForm or here */}
+ {/*
Share a New Toy */}
+
+
+ );
+}
diff --git a/src/app/dashboard/my-toys/edit/[id]/page.tsx b/src/app/dashboard/my-toys/edit/[id]/page.tsx
new file mode 100644
index 0000000..82461ce
--- /dev/null
+++ b/src/app/dashboard/my-toys/edit/[id]/page.tsx
@@ -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 | 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 (
+
+
Toy Not Found
+
Cannot edit a toy that does not exist.
+
+
+
+ Back to My Toys
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Back to My Toys
+
+
+
+ );
+}
diff --git a/src/app/dashboard/my-toys/page.tsx b/src/app/dashboard/my-toys/page.tsx
new file mode 100644
index 0000000..7801b32
--- /dev/null
+++ b/src/app/dashboard/my-toys/page.tsx
@@ -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 (
+
+
+
+
My Listed Toys
+
Manage the toys you've shared with the community.
+
+
+
+
+ Add New Toy
+
+
+
+
+ {userToys.length === 0 ? (
+
+
+
+ No Toys Listed Yet
+ Share your first toy and spread the joy!
+
+
+
+
+
+ Add Your First Toy
+
+
+
+
+ ) : (
+
+ {userToys.map(toy => (
+
+ ))}
+
+ )}
+
+ );
+}
+
+interface ListedToyItemProps {
+ toy: Toy & {dataAiHint?: string};
+}
+
+function ListedToyItem({ toy }: ListedToyItemProps) {
+ const placeholderHint = toy.dataAiHint || toy.category.toLowerCase() || "toy";
+ return (
+
+
+
+
+
+
+
+
+ {toy.name}
+ {toy.category}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{toy.description}
+
+ Price:
+ {toy.pricePerDay !== undefined ? (toy.pricePerDay > 0 ? `$${toy.pricePerDay}/day` : 'Free') : 'Not set'}
+
+ {/* Could add more stats like number of rentals, views etc. here */}
+
+
+
+ );
+}
+
diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx
new file mode 100644
index 0000000..d21f2c6
--- /dev/null
+++ b/src/app/dashboard/page.tsx
@@ -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 (
+
+
+
+ Welcome to Your Dashboard!
+ Manage your toys, rentals, and account settings all in one place.
+
+
+ }
+ actionLink="/dashboard/my-toys"
+ actionLabel="View My Toys"
+ />
+ }
+ actionLink="/dashboard/rentals"
+ actionLabel="View My Rentals"
+ />
+ }
+ actionLink="/dashboard/requests"
+ actionLabel="Manage Requests"
+ />
+
+
+
+
+
+
+ Quick Actions
+
+
+
+
+ Add a New Toy
+
+
+
+
+ Update Profile
+
+
+
+
+
+
+
+ Tips for Sharers
+
+
+
+ Take clear photos of your toys from multiple angles.
+ Write detailed and accurate descriptions.
+ Keep your availability calendar up-to-date.
+ Respond promptly to rental requests.
+
+
+
+
+
+ );
+}
+
+interface DashboardStatCardProps {
+ title: string;
+ value: string;
+ icon: React.ReactNode;
+ actionLink: string;
+ actionLabel: string;
+}
+
+function DashboardStatCard({ title, value, icon, actionLink, actionLabel }: DashboardStatCardProps) {
+ return (
+
+
+ {title}
+ {icon}
+
+
+ {value}
+
+ {actionLabel} →
+
+
+
+ );
+}
diff --git a/src/app/dashboard/profile/page.tsx b/src/app/dashboard/profile/page.tsx
new file mode 100644
index 0000000..9d9f4bc
--- /dev/null
+++ b/src/app/dashboard/profile/page.tsx
@@ -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) => {
+ 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) => {
+ 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 (
+
+
+
Profile Settings
+
Manage your personal information and account settings.
+
+
+ {/* Profile Information Card */}
+
+
+ Personal Information
+ Update your publicly visible profile information.
+
+
+
+
+ {/* Change Password Card */}
+
+
+ Change Password
+ Update your account password for security.
+
+
+
+
+ Current Password
+ setCurrentPassword(e.target.value)} required disabled={isPasswordLoading} />
+
+
+ New Password
+ setNewPassword(e.target.value)} required disabled={isPasswordLoading} />
+
+
+ Confirm New Password
+ setConfirmNewPassword(e.target.value)} required disabled={isPasswordLoading} />
+
+
+
+
+
+ {isPasswordLoading ? 'Updating Password...' : 'Update Password'}
+
+
+
+
+
+ );
+}
diff --git a/src/app/dashboard/rentals/page.tsx b/src/app/dashboard/rentals/page.tsx
new file mode 100644
index 0000000..c1676ba
--- /dev/null
+++ b/src/app/dashboard/rentals/page.tsx
@@ -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 (
+
+
+
My Rentals
+
Toys you are currently renting from others.
+
+
+ {rentedToys.length === 0 ? (
+
+
+
+ No Active Rentals
+ You haven't rented any toys yet. Explore available toys and find your next adventure!
+
+
+
+
+
+ Browse Toys
+
+
+
+
+ ) : (
+
+ {rentedToys.map(toy => (
+
+ ))}
+
+ )}
+
+ );
+}
+
+interface RentalItemCardProps {
+ toy: Toy & { rentalEndDate?: string, dataAiHint?: string };
+}
+
+function RentalItemCard({ toy }: RentalItemCardProps) {
+ const placeholderHint = toy.dataAiHint || toy.category.toLowerCase() || "toy";
+ return (
+
+
+
+
+
+
+
{toy.name}
+
Rented from: {toy.ownerName}
+ {toy.rentalEndDate && (
+
+ Rental ends: {new Date(toy.rentalEndDate).toLocaleDateString()}
+
+ )}
+
{toy.description}
+
+
+ View Toy Details
+
+ Contact Owner {/* Mock action */}
+
+
+
+
+ );
+}
diff --git a/src/app/dashboard/requests/page.tsx b/src/app/dashboard/requests/page.tsx
new file mode 100644
index 0000000..93a8fe1
--- /dev/null
+++ b/src/app/dashboard/requests/page.tsx
@@ -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 (
+
+
+
Rental Requests
+
Manage incoming rental requests for your toys.
+
+
+ {currentUserToyRequests.length === 0 ? (
+
+
+
+ No Rental Requests
+ You currently have no pending rental requests for your toys.
+
+
+ When someone requests to rent one of your toys, it will appear here.
+
+
+ ) : (
+
+ {currentUserToyRequests.map(request => (
+
+ ))}
+
+ )}
+
+ );
+}
+
+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 (
+
+
+
+
+
+
+
+ {request.toy.name}
+ {request.status}
+
+
+ Requested by: {request.requesterName}
+
+
+ Dates: {request.requestedDates}
+
+ {request.message && (
+
+ Message: "{request.message}"
+
+ )}
+
+ {request.status === 'pending' && (
+
+
+ Approve
+
+
+ Decline
+
+ Message Requester
+
+ )}
+ {request.status !== 'pending' && (
+
+
+ View Toy Listing
+
+
+ )}
+
+
+
+ );
+}
diff --git a/src/app/globals.css b/src/app/globals.css
index a8144b6..ed97443 100644
--- a/src/app/globals.css
+++ b/src/app/globals.css
@@ -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;
}
}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index c81ce2d..0abd4fb 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -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 (
-
+
-
-
-
+
+
+
- {children}
+
+
+
+ {children}
+
+
+
+
);
}
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx
new file mode 100644
index 0000000..ade6f7d
--- /dev/null
+++ b/src/app/login/page.tsx
@@ -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) => {
+ 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 (
+
+
+
+
+
+
+ Welcome Back!
+ Log in to your ToyShare account to continue.
+
+
+
+
+ Email Address
+ setEmail(e.target.value)}
+ required
+ disabled={isLoading}
+ />
+
+
+ Password
+ setPassword(e.target.value)}
+ required
+ disabled={isLoading}
+ />
+
+
+ {isLoading ? 'Logging in...' : 'Log In'}
+
+
+
+
+
+ Forgot your password? Reset it here.
+
+
+
+ Don't have an account?{' '}
+
+ Sign up now
+
+
+
+
+
+ );
+}
+
+// Simple separator, can be moved to ui components if used elsewhere
+function Separator() {
+ return
;
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 6ff5373..082b1be 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -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 (
+
+
+
+ Welcome to ToyShare!
+
+
+ Discover a world of fun! Share your beloved toys or find new adventures by renting from our friendly community.
+
+
+
+
+ Explore Toys
+
+
+
+
+
+ Share Your Toy
+
+
+
+
+
+
+
+ Available Toys
+
+ ({...toy, dataAiHint: toy.category.toLowerCase()}))} />
+
+
+ );
}
diff --git a/src/app/register/page.tsx b/src/app/register/page.tsx
new file mode 100644
index 0000000..750624b
--- /dev/null
+++ b/src/app/register/page.tsx
@@ -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) => {
+ 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 (
+
+ );
+}
diff --git a/src/app/toys/[id]/page.tsx b/src/app/toys/[id]/page.tsx
new file mode 100644
index 0000000..3e45e44
--- /dev/null
+++ b/src/app/toys/[id]/page.tsx
@@ -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 {
+ // 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 (
+
+
Toy Not Found
+
Sorry, the toy you are looking for does not exist or has been removed.
+
+
+
+ Back to All Toys
+
+
+
+ );
+ }
+
+ const placeholderHint = toy.category.toLowerCase() || "toy detail";
+
+ return (
+
+
+
+ Back to All Toys
+
+
+
+ {/* Image Gallery Section */}
+
+
+
+
+ {toy.images.length > 1 && (
+
+ {toy.images.slice(1, 4).map((img, index) => (
+
+
+
+ ))}
+
+ )}
+
+
+ {/* Toy Details Section */}
+
+
{toy.category}
+
{toy.name}
+
+
+
+
+
+
+
+
+
+ Owner:
+ {toy.ownerName}
+
+
+ {toy.location && (
+
+
+
+ Location:
+ {toy.location}
+
+
+ )}
+ {toy.pricePerDay !== undefined && (
+
+
+
+ Price:
+ {toy.pricePerDay > 0 ? `$${toy.pricePerDay}/day` : 'Free'}
+
+
+ )}
+
+
+
+
+
+
+
+ Request to Rent
+
+
+
+
+ );
+}
+
+// Generate static paths for all toys
+export async function generateStaticParams() {
+ return mockToys.map((toy) => ({
+ id: toy.id,
+ }));
+}
diff --git a/src/components/layout/DashboardSidebar.tsx b/src/components/layout/DashboardSidebar.tsx
new file mode 100644
index 0000000..35f220a
--- /dev/null
+++ b/src/components/layout/DashboardSidebar.tsx
@@ -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 (
+
+
+
+ {label}
+
+
+ );
+ };
+
+ return (
+
+
+
+
+
ToyShare
+
+
User Dashboard
+
+
+
+ Toy Management
+ {sidebarNavItems.map((item) => (
+
+ ))}
+
+
+
+ Account
+ {accountNavItems.map((item) => (
+
+ ))}
+
+
+
+
+
+
+
+ Logout
+
+
+
+ );
+}
diff --git a/src/components/layout/Footer.tsx b/src/components/layout/Footer.tsx
new file mode 100644
index 0000000..960cd93
--- /dev/null
+++ b/src/components/layout/Footer.tsx
@@ -0,0 +1,14 @@
+export default function Footer() {
+ return (
+
+ );
+}
diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx
new file mode 100644
index 0000000..3b97297
--- /dev/null
+++ b/src/components/layout/Header.tsx
@@ -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 (
+
+ );
+}
diff --git a/src/components/toys/AddToyForm.tsx b/src/components/toys/AddToyForm.tsx
new file mode 100644
index 0000000..1956aa2
--- /dev/null
+++ b/src/components/toys/AddToyForm.tsx
@@ -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; // 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(initialData?.images || ['']); // Store image URLs
+ const [availability, setAvailability] = useState(
+ 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) => {
+ 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 (
+
+
+
+
+
+ {isEditMode ? 'Edit Your Toy' : 'Share a New Toy'}
+
+ {isEditMode ? 'Update the details of your toy listing.' : 'Fill in the details below to list your toy for others to enjoy.'}
+
+
+
+
+ {/* Toy Name */}
+
+ Toy Name
+ setName(e.target.value)} placeholder="e.g., Red Racing Car" required disabled={isLoading} />
+
+
+ {/* Description */}
+
+ Description
+ setDescription(e.target.value)} placeholder="Describe your toy, its condition, and any accessories." required disabled={isLoading} rows={4} />
+
+
+ {/* Category */}
+
+ Category
+
+
+
+
+
+ {toyCategories.map(cat => {cat} )}
+
+
+
+
+ {/* Price per Day */}
+
+ Rental Price per Day ($)
+ setPricePerDay(e.target.value)} placeholder="0 for free" min="0" step="0.50" disabled={isLoading} />
+
+
+ {/* Location */}
+
+ Location (Optional)
+ setLocation(e.target.value)} placeholder="e.g., Springfield Park Area" disabled={isLoading} />
+
+
+ {/* Image URLs */}
+
+
Toy Images (URLs)
+
Enter direct URLs to your toy images. Add up to 5 images.
+ {images.map((imgUrl, index) => (
+
+ handleImageChange(index, e.target.value)}
+ placeholder={`Image URL ${index + 1}`}
+ disabled={isLoading}
+ />
+ {images.length > 1 && (
+ removeImageField(index)} disabled={isLoading}>
+
+
+ )}
+
+ ))}
+ {images.length < 5 && (
+
+ Add Another Image
+
+ )}
+
+
+ {/* Availability */}
+
+
Weekly Availability
+
+ {daysOfWeek.map(day => (
+
+ handleAvailabilityChange(day as keyof Toy['availability'])}
+ disabled={isLoading}
+ />
+ {day}
+
+ ))}
+
+
+
+
+
+ {isLoading ? (isEditMode ? 'Saving Changes...' : 'Listing Toy...') : (
+ <>
+
+ {isEditMode ? 'Save Changes' : 'List My Toy'}
+ >
+ )}
+
+
+
+
+
+ );
+}
+
+// Icon for file upload, not used in URL version but good for future
+function FileUploadIcon() {
+ return (
+
+
+
+ Click to upload or drag and drop
+
+
PNG, JPG, GIF up to 10MB
+
+ );
+}
diff --git a/src/components/toys/AvailabilityCalendar.tsx b/src/components/toys/AvailabilityCalendar.tsx
new file mode 100644
index 0000000..b616397
--- /dev/null
+++ b/src/components/toys/AvailabilityCalendar.tsx
@@ -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 (
+
+
+ Weekly Availability
+
+
+
+ {daysOfWeek.map((day) => {
+ const isAvailable = availability[day.key];
+ return (
+
+ {day.label}
+ {isAvailable ? (
+
+ ) : (
+
+ )}
+
+ );
+ })}
+
+
+ This calendar shows general weekly availability. Specific dates may vary.
+
+
+
+ );
+}
diff --git a/src/components/toys/ToyCard.tsx b/src/components/toys/ToyCard.tsx
new file mode 100644
index 0000000..094ac89
--- /dev/null
+++ b/src/components/toys/ToyCard.tsx
@@ -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 (
+
+
+
+
+
+
+
+ {toy.name}
+
+
+
+ {toy.category}
+
+ {toy.location && (
+
+
+ {toy.location}
+
+ )}
+ {toy.pricePerDay !== undefined && (
+
+
+ {toy.pricePerDay > 0 ? `${toy.pricePerDay}/day` : 'Free'}
+
+ )}
+
+
+
+
+
+ View Details
+
+
+
+
+ );
+}
diff --git a/src/components/toys/ToyList.tsx b/src/components/toys/ToyList.tsx
new file mode 100644
index 0000000..06732a6
--- /dev/null
+++ b/src/components/toys/ToyList.tsx
@@ -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 No toys available at the moment. Check back soon!
;
+ }
+
+ return (
+
+ {toys.map((toy) => (
+
+ ))}
+
+ );
+}
diff --git a/src/lib/mockData.ts b/src/lib/mockData.ts
new file mode 100644
index 0000000..e38c8e3
--- /dev/null
+++ b/src/lib/mockData.ts
@@ -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.
+ }
+});
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000..85522d9
--- /dev/null
+++ b/src/types/index.ts
@@ -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
+}
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 4d4a68f..a7a24d5 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -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",
},
},
},