Compare commits

...

10 Commits

27 changed files with 566 additions and 357 deletions

View File

@ -0,0 +1,103 @@
import Link from 'next/link';
import { Button } from '@/components/ui/button';
import ToyList from '@/components/toys/ToyList';
import { getToysByOwner, getOwnerProfile, getAllToys } from '@/data/operations';
import { getI18n, getStaticParams as getLocaleStaticParams } from '@/locales/server';
import { Home, UserCircle } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { ToyBrick } from 'lucide-react';
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
interface OwnerToysPageProps {
params: { ownerId: string; locale: string };
}
export default async function OwnerToysPage({ params }: OwnerToysPageProps) {
const t = await getI18n();
const ownerId = Number(params.ownerId);
const ownerToys = getToysByOwner(ownerId);
const ownerProfile = getOwnerProfile(ownerId);
const ownerNameFromToys = ownerToys.length > 0 ? ownerToys[0].ownerName : undefined;
let displayOwnerName = ownerProfile?.name || ownerNameFromToys || t('owner_toys.unknown_owner');
const pageTitle = displayOwnerName !== t('owner_toys.unknown_owner')
? t('owner_toys.title_specific', { ownerName: displayOwnerName })
: t('owner_toys.title_generic');
return (
<div className="space-y-8">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<h1 className="text-3xl font-bold font-headline text-primary">{pageTitle}</h1>
<Link href={`/${params.locale}/`} passHref>
<Button variant="outline" className="w-full sm:w-auto">
<Home className="mr-2 h-4 w-4" />
{t('owner_toys.back_to_home')}
</Button>
</Link>
</div>
{ownerProfile && (
<Card className="mb-8 shadow-lg border-accent/30">
<CardHeader>
<div className="flex flex-col sm:flex-row items-center gap-4">
<Avatar className="h-20 w-20 sm:h-24 sm:w-24 border-2 border-accent">
<AvatarImage src={ownerProfile.avatarUrl} alt={displayOwnerName} data-ai-hint="owner avatar" />
<AvatarFallback className="text-3xl bg-muted">
{displayOwnerName ? displayOwnerName.split(' ').map(n => n[0]).join('').toUpperCase() : <UserCircle />}
</AvatarFallback>
</Avatar>
<div className="text-center sm:text-left">
<CardTitle className="text-2xl font-headline text-accent">
{t('owner_toys.about_owner', { ownerName: displayOwnerName })}
</CardTitle>
{ownerNameFromToys && ownerNameFromToys !== displayOwnerName && (
<CardDescription>{t('toy_details.owner')}: {ownerNameFromToys}</CardDescription>
)}
</div>
</div>
</CardHeader>
<CardContent>
<p className="text-foreground/80 leading-relaxed">{ownerProfile.bio}</p>
</CardContent>
</Card>
)}
{ownerToys.length > 0 ? (
<ToyList toys={ownerToys} t={t} />
) : (
<Card className="text-center py-12 shadow-md">
<CardHeader>
<ToyBrick className="h-16 w-16 mx-auto text-muted-foreground mb-4" />
<CardTitle>
{displayOwnerName !== t('owner_toys.unknown_owner')
? t('owner_toys.no_toys_listed_by', { ownerName: displayOwnerName })
: t('owner_toys.owner_not_found')}
</CardTitle>
{displayOwnerName !== t('owner_toys.unknown_owner') && <CardDescription>{t('home.explore_toys')}</CardDescription>}
</CardHeader>
<CardContent>
<Link href={`/${params.locale}/`} passHref>
<Button size="lg">
{t('home.explore_toys')}
</Button>
</Link>
</CardContent>
</Card>
)}
</div>
);
}
export async function generateStaticParams() {
const localeParams = getLocaleStaticParams();
const allToys = getAllToys();
const ownerIds = Array.from(new Set(allToys.map(toy => toy.ownerId)));
const ownerParams = ownerIds.map(id => ({ ownerId: String(id) }));
return localeParams.flatMap(lang =>
ownerParams.map(owner => ({ ...lang, ...owner }))
);
}

View File

@ -13,7 +13,8 @@ interface EditUserPageProps {
export default async function EditUserPage({ params }: EditUserPageProps) { export default async function EditUserPage({ params }: EditUserPageProps) {
const t = await getI18n(); const t = await getI18n();
const userData = getUserById(params.id); const userId = Number(params.id);
const userData = getUserById(userId);
if (!userData) { if (!userData) {
return ( return (

View File

@ -53,7 +53,7 @@ export default function AdminUserManagementPage() {
}, [t, toast]); }, [t, toast]);
const handleDeleteUser = async (userId: string) => { const handleDeleteUser = async (userId: number) => {
setIsDeleting(true); setIsDeleting(true);
const result = await deleteUser(userId); const result = await deleteUser(userId);
if (result.success) { if (result.success) {

View File

@ -17,20 +17,20 @@ import { ArrowLeft, Send, Loader2, AlertTriangle, ToyBrick } from 'lucide-react'
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
import { format } from 'date-fns'; import { format } from 'date-fns';
// Assume current user is 'user1' for mock purposes // Assume current user is 'user1' (ID: 1) for mock purposes
const currentUserId = 'user1'; const currentUserId = 1;
const currentUserProfiles: Record<string, { name: string; avatarInitial: string; avatarUrl?: string }> = { const currentUserProfiles: Record<number, { name: string; avatarInitial: string; avatarUrl?: string }> = {
'user1': { name: 'Alice Wonderland', avatarInitial: 'AW', avatarUrl: 'https://placehold.co/40x40.png?text=AW' }, // Logged in user 1: { name: 'Alice Wonderland', avatarInitial: 'AW', avatarUrl: 'https://placehold.co/40x40.png?text=AW' }, // Logged in user
'user2': { name: 'Bob The Builder', avatarInitial: 'BT', avatarUrl: 'https://placehold.co/40x40.png?text=BT' }, 2: { name: 'Bob The Builder', avatarInitial: 'BT', avatarUrl: 'https://placehold.co/40x40.png?text=BT' },
'user3': { name: 'Carol Danvers', avatarInitial: 'CD', avatarUrl: 'https://placehold.co/40x40.png?text=CD' }, 3: { name: 'Carol Danvers', avatarInitial: 'CD', avatarUrl: 'https://placehold.co/40x40.png?text=CD' },
'user4': { name: 'Charlie Brown', avatarInitial: 'CB', avatarUrl: 'https://placehold.co/40x40.png?text=CB' }, 4: { name: 'Charlie Brown', avatarInitial: 'CB', avatarUrl: 'https://placehold.co/40x40.png?text=CB' },
'user5': { name: 'Diana Prince', avatarInitial: 'DP', avatarUrl: 'https://placehold.co/40x40.png?text=DP' }, 5: { name: 'Diana Prince', avatarInitial: 'DP', avatarUrl: 'https://placehold.co/40x40.png?text=DP' },
'user6': { name: 'Edward Nigma', avatarInitial: 'EN', avatarUrl: 'https://placehold.co/40x40.png?text=EN' }, 6: { name: 'Edward Nigma', avatarInitial: 'EN', avatarUrl: 'https://placehold.co/40x40.png?text=EN' },
}; };
// Helper function to get the other participant in a conversation // Helper function to get the other participant in a conversation
const getOtherParticipant = (request: RentalRequest, currentUserId: string) => { const getOtherParticipant = (request: RentalRequest, currentUserId: number) => {
if (request.toy.ownerId === currentUserId) { if (request.toy.ownerId === currentUserId) {
return { id: request.requesterId, name: request.requesterName }; return { id: request.requesterId, name: request.requesterName };
} }
@ -238,5 +238,3 @@ export default function MessageDetailPage({ params }: { params: { id: string } }
</div> </div>
); );
} }

View File

@ -9,10 +9,10 @@ import Link from "next/link";
import { MessageSquareQuote, ToyBrick } from "lucide-react"; import { MessageSquareQuote, ToyBrick } from "lucide-react";
import { format } from 'date-fns'; import { format } from 'date-fns';
const currentUserId = 'user1'; // Assume this is the logged-in user's ID const currentUserId = 1; // Assume this is the logged-in user's ID
// Helper function to get the other participant in a conversation // Helper function to get the other participant in a conversation
const getOtherParticipant = (request: RentalRequest, currentUserId: string) => { const getOtherParticipant = (request: RentalRequest, currentUserId: number) => {
if (request.toy.ownerId === currentUserId) { if (request.toy.ownerId === currentUserId) {
return { id: request.requesterId, name: request.requesterName }; return { id: request.requesterId, name: request.requesterName };
} }

View File

@ -2,30 +2,19 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { getToysByOwner } from "@/data/operations"; import { getToysByOwner } from "@/data/operations";
import { mockRentalHistory } from "@/lib/mockData";
import Link from "next/link"; import Link from "next/link";
import { PlusCircle, Edit3, Trash2, Eye, ToyBrick as ToyBrickIcon, BarChartHorizontalBig } from "lucide-react"; import { PlusCircle, Edit3, Trash2, Eye, ToyBrick as ToyBrickIcon } from "lucide-react";
import Image from "next/image"; import Image from "next/image";
import type { Toy } from "@/types"; import type { Toy } from "@/types";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { getI18n } from "@/locales/server"; import { getI18n } from "@/locales/server";
const currentUserId = 'user1'; const currentUserId = 1;
export default async function MyToysPage() { export default async function MyToysPage() {
const t = await getI18n(); const t = await getI18n();
const userToys = getToysByOwner(currentUserId); const userToys = getToysByOwner(currentUserId);
const getRentalCountForToy = (toyId: string): number => {
// NOTE: This part still uses mock data and will need to be migrated.
return mockRentalHistory.filter(entry => entry.toy.id === toyId && entry.toy.ownerId === currentUserId).length;
};
const userToysWithRentalCount = userToys.map(toy => ({
...toy,
rentalCount: getRentalCountForToy(toy.id),
}));
return ( return (
<div className="space-y-8"> <div className="space-y-8">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
@ -41,7 +30,7 @@ export default async function MyToysPage() {
</Link> </Link>
</div> </div>
{userToysWithRentalCount.length === 0 ? ( {userToys.length === 0 ? (
<Card className="text-center py-12 shadow-md"> <Card className="text-center py-12 shadow-md">
<CardHeader> <CardHeader>
<ToyBrickIcon className="h-16 w-16 mx-auto text-muted-foreground mb-4" /> <ToyBrickIcon className="h-16 w-16 mx-auto text-muted-foreground mb-4" />
@ -59,8 +48,8 @@ export default async function MyToysPage() {
</Card> </Card>
) : ( ) : (
<div className="space-y-6"> <div className="space-y-6">
{userToysWithRentalCount.map(toy => ( {userToys.map(toy => (
<ListedToyItem key={toy.id} toy={toy} t={t} rentalCount={toy.rentalCount} /> <ListedToyItem key={toy.id} toy={toy} t={t} />
))} ))}
</div> </div>
)} )}
@ -71,10 +60,9 @@ export default async function MyToysPage() {
interface ListedToyItemProps { interface ListedToyItemProps {
toy: Toy & {dataAiHint?: string}; toy: Toy & {dataAiHint?: string};
t: (key: string, params?: Record<string, string | number>) => string; t: (key: string, params?: Record<string, string | number>) => string;
rentalCount: number;
} }
function ListedToyItem({ toy, t, rentalCount }: ListedToyItemProps) { function ListedToyItem({ toy, t }: ListedToyItemProps) {
const placeholderHint = toy.dataAiHint || toy.category.toLowerCase() || "toy"; const placeholderHint = toy.dataAiHint || toy.category.toLowerCase() || "toy";
return ( return (
<Card className="overflow-hidden shadow-lg hover:shadow-xl transition-shadow duration-300"> <Card className="overflow-hidden shadow-lg hover:shadow-xl transition-shadow duration-300">
@ -117,14 +105,6 @@ function ListedToyItem({ toy, t, rentalCount }: ListedToyItemProps) {
<span className="font-semibold">{t('toy_details.price')}: </span> <span className="font-semibold">{t('toy_details.price')}: </span>
{toy.pricePerDay !== undefined ? (toy.pricePerDay > 0 ? `$${toy.pricePerDay}${t('toy_details.price_per_day')}` : t('toy_details.price_free')) : 'Not set'} {toy.pricePerDay !== undefined ? (toy.pricePerDay > 0 ? `$${toy.pricePerDay}${t('toy_details.price_per_day')}` : t('toy_details.price_free')) : 'Not set'}
</div> </div>
{rentalCount > 0 && (
<div className="flex items-center text-accent">
<BarChartHorizontalBig className="mr-1.5 h-4 w-4" />
{rentalCount === 1
? t('dashboard.my_toys.rental_count_one')
: t('dashboard.my_toys.rental_count_many', { count: rentalCount })}
</div>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -3,15 +3,20 @@ import { Button } from "@/components/ui/button";
import Link from "next/link"; import Link from "next/link";
import { ToyBrick, PlusCircle, ListOrdered, User, ShoppingBag } from "lucide-react"; import { ToyBrick, PlusCircle, ListOrdered, User, ShoppingBag } from "lucide-react";
import { getI18n } from "@/locales/server"; import { getI18n } from "@/locales/server";
import { getToysByOwner } from "@/data/operations";
const userStats = { const currentUserId = 1; // Mock logged-in user ID
listedToys: 3,
activeRentals: 1,
pendingRequests: 2,
};
export default async function DashboardOverviewPage() { export default async function DashboardOverviewPage() {
const t = await getI18n(); const t = await getI18n();
const userToys = getToysByOwner(currentUserId);
const userStats = {
listedToys: userToys.length, // Real data from DB
activeRentals: 1, // Mock data
pendingRequests: 2, // Mock data
};
return ( return (
<div className="space-y-8"> <div className="space-y-8">
<Card className="shadow-lg"> <Card className="shadow-lg">

View File

@ -11,7 +11,7 @@ import type { Locale } from "@/locales/server";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { format } from 'date-fns'; import { format } from 'date-fns';
const currentUserId = 'user1'; // Assume this is the logged-in user's ID const currentUserId = 1; // Assume this is the logged-in user's ID
export default async function RentalHistoryPage({ params }: { params: { locale: Locale } }) { export default async function RentalHistoryPage({ params }: { params: { locale: Locale } }) {
const t = await getI18n(); const t = await getI18n();
@ -112,4 +112,3 @@ function RentalHistoryItemCard({ item, t, locale }: RentalHistoryItemCardProps)
</Card> </Card>
); );
} }

View File

@ -4,55 +4,53 @@ import { ListOrdered, Check, X, MessageSquareText } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import Image from "next/image"; import Image from "next/image";
import type { Toy } from "@/types"; import type { Toy, RentalRequest } from "@/types";
import { mockToys } from "@/lib/mockData"; import { getAllToys } from "@/data/operations";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
import { getI18n } from "@/locales/server"; import { getI18n } from "@/locales/server";
interface RentalRequest { // This is still mock data. In a real app, this would come from the database.
id: string; function getMockRequests(): RentalRequest[] {
toy: Toy; const allToys = getAllToys();
requesterName: string; const myToys = allToys.filter(t => t.ownerId === 1);
requesterId: string;
requestedDates: string; // e.g., "Aug 5, 2024 - Aug 10, 2024"
status: 'pending' | 'approved' | 'declined';
message?: string;
dataAiHint?: string;
}
const rentalRequests: RentalRequest[] = [ if (myToys.length === 0) return [];
const rentalRequests: RentalRequest[] = [
{ {
id: 'req1', id: 'req1',
toy: mockToys[0], toy: myToys[0],
requesterName: 'Charlie Brown', requesterName: 'Charlie Brown',
requesterId: 'user4', requesterId: 4,
requestedDates: 'August 10, 2024 - August 17, 2024', requestedDates: 'August 10, 2024 - August 17, 2024',
status: 'pending', status: 'pending',
message: 'My son would love to play with these for his birthday week! We are very careful with toys and will ensure it is returned in perfect condition. Could we possibly pick it up on the 9th evening?', message: 'My son would love to play with these for his birthday week! We are very careful with toys and will ensure it is returned in perfect condition. Could we possibly pick it up on the 9th evening?',
dataAiHint: mockToys[0]?.category.toLowerCase(), dataAiHint: myToys[0]?.category.toLowerCase(),
}, },
{ {
id: 'req2', id: 'req2',
toy: mockToys[3], toy: myToys.length > 1 ? myToys[1] : myToys[0],
requesterName: 'Diana Prince', requesterName: 'Diana Prince',
requesterId: 'user5', requesterId: 5,
requestedDates: 'September 1, 2024 - September 5, 2024', requestedDates: 'September 1, 2024 - September 5, 2024',
status: 'approved', status: 'approved',
dataAiHint: mockToys[3]?.category.toLowerCase(), dataAiHint: (myToys.length > 1 ? myToys[1] : myToys[0])?.category.toLowerCase(),
}, },
{ {
id: 'req3', id: 'req3',
toy: mockToys[0], toy: myToys[0],
requesterName: 'Edward Nigma', requesterName: 'Edward Nigma',
requesterId: 'user6', requesterId: 6,
requestedDates: 'July 20, 2024 - July 22, 2024', requestedDates: 'July 20, 2024 - July 22, 2024',
status: 'declined', status: 'declined',
message: 'Looking for a weekend rental.', message: 'Looking for a weekend rental.',
dataAiHint: mockToys[0]?.category.toLowerCase(), dataAiHint: myToys[0]?.category.toLowerCase(),
}, },
]; ];
return rentalRequests;
}
const currentUserToyRequests = rentalRequests.filter(req => req.toy.ownerId === 'user1'); const currentUserToyRequests = getMockRequests();
export default async function RentalRequestsPage() { export default async function RentalRequestsPage() {
@ -167,4 +165,3 @@ function RequestItemCard({ request, t }: RequestItemCardProps) {
</Card> </Card>
); );
} }

View File

@ -3,15 +3,12 @@ import Image from 'next/image';
import { getToyById } from '@/data/operations'; import { getToyById } from '@/data/operations';
import type { Toy } from '@/types'; import type { Toy } from '@/types';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Calendar } from '@/components/ui/calendar';
import { ArrowLeft, DollarSign, MapPin } from 'lucide-react'; import { ArrowLeft, DollarSign, MapPin } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { getI18n, getStaticParams as getLocaleStaticParams } from '@/locales/server'; import { getI18n, getStaticParams as getLocaleStaticParams } from '@/locales/server';
import type { Locale } from '@/locales/server'; import type { Locale } from '@/locales/server';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { addDays, parseISO } from 'date-fns';
import { getAllToys } from '@/data/operations'; import { getAllToys } from '@/data/operations';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { ShoppingBag } from 'lucide-react'; import { ShoppingBag } from 'lucide-react';
@ -41,12 +38,6 @@ export default async function ToyPage({ params }: ToyPageProps) {
const placeholderHint = toy.category.toLowerCase() || "toy detail"; const placeholderHint = toy.category.toLowerCase() || "toy detail";
const disabledDates = toy.unavailableRanges.map(range => {
const from = parseISO(range.startDate);
const to = parseISO(range.endDate);
return { from, to };
});
return ( return (
<div className="container mx-auto py-8 px-4"> <div className="container mx-auto py-8 px-4">
<Link href={`/${params.locale}/`} className="inline-flex items-center text-primary hover:underline mb-6 group"> <Link href={`/${params.locale}/`} className="inline-flex items-center text-primary hover:underline mb-6 group">
@ -132,23 +123,6 @@ export default async function ToyPage({ params }: ToyPageProps) {
<Separator /> <Separator />
<Card className="shadow-md">
<CardHeader>
<CardTitle className="text-xl font-headline text-primary">{t('toy_details.availability_calendar_title')}</CardTitle>
</CardHeader>
<CardContent className="flex justify-center">
<Calendar
mode="single"
disabled={disabledDates}
month={new Date()}
className="rounded-md border"
/>
</CardContent>
<p className="text-xs text-muted-foreground mt-0 pb-4 text-center">
{t('toy_details.calendar_note')}
</p>
</Card>
<Button size="lg" className="w-full mt-6 transition-transform transform hover:scale-105"> <Button size="lg" className="w-full mt-6 transition-transform transform hover:scale-105">
<ShoppingBag className="mr-2 h-5 w-5" /> {t('toy_details.request_to_rent')} <ShoppingBag className="mr-2 h-5 w-5" /> {t('toy_details.request_to_rent')}
</Button> </Button>

76
src/app/actions/toy.ts Normal file
View File

@ -0,0 +1,76 @@
'use server';
import { revalidatePath } from 'next/cache';
import { createToy, updateToy, getToyById } from '@/data/operations';
import type { Toy } from '@/types';
// This type represents the data coming from the form.
// Simplified for clarity and maintainability.
export type ToyFormData = {
name: string;
description: string;
category: string;
images: string[];
unavailableRanges: { startDate: string; endDate: string }[];
pricePerDay?: number;
location?: string;
};
interface FormResult {
success: boolean;
message: string;
toy?: Toy;
}
export async function createOrUpdateToy(
formData: ToyFormData & { ownerId: number },
toyId: string | null // null for create, string for update
): Promise<FormResult> {
// Basic validation
if (!formData.name || !formData.description || !formData.category || !formData.ownerId) {
return { success: false, message: 'Missing required fields.' };
}
try {
let savedToy: Toy;
if (toyId) {
// Update existing toy
const existingToy = getToyById(toyId);
if (!existingToy) {
return { success: false, message: 'Toy not found.' };
}
if (existingToy.ownerId !== formData.ownerId) {
return { success: false, message: 'Unauthorized action.' }; // Security check
}
savedToy = updateToy({
id: toyId,
...formData,
});
} else {
// Create new toy
savedToy = createToy(formData);
}
// Revalidate paths to show the new data.
// This tells Next.js to re-fetch the data on the next request for these paths across all locales.
revalidatePath('/dashboard/my-toys');
revalidatePath(`/toys/${savedToy.id}`);
revalidatePath('/');
return {
success: true,
message: toyId ? 'Toy updated successfully!' : 'Toy created successfully!',
toy: savedToy,
};
} catch (error) {
console.error('Error in createOrUpdateToy:', error);
return { success: false, message: 'An unexpected error occurred.' };
}
}

View File

@ -3,7 +3,6 @@
import db from '@/lib/db'; import db from '@/lib/db';
import type { User } from '@/types'; import type { User } from '@/types';
import { randomUUID } from 'crypto';
import { revalidatePath } from 'next/cache'; import { revalidatePath } from 'next/cache';
import { getAllUsers as dbGetAllUsers, getUserById } from '@/data/operations'; import { getAllUsers as dbGetAllUsers, getUserById } from '@/data/operations';
@ -26,21 +25,21 @@ export async function registerUser(data: { name: string; nickname?: string; emai
return { success: false, message: 'An account with this email already exists.' }; return { success: false, message: 'An account with this email already exists.' };
} }
const newUser: User = { const stmt = db.prepare(
id: `user-${randomUUID()}`, 'INSERT INTO users (name, nickname, email, role, avatarUrl, bio) VALUES (@name, @nickname, @email, @role, @avatarUrl, @bio)'
);
const info = stmt.run({
name, name,
nickname, nickname: nickname ?? null,
email, email,
role: role, role: role,
avatarUrl: '', avatarUrl: '',
bio: '' bio: ''
}; });
const stmt = db.prepare( const newUserId = info.lastInsertRowid as number;
'INSERT INTO users (id, name, nickname, email, role, avatarUrl, bio) VALUES (@id, @name, @nickname, @email, @role, @avatarUrl, @bio)' const newUser = getUserById(newUserId);
);
stmt.run(newUser);
revalidatePath('/admin/users'); revalidatePath('/admin/users');
@ -96,7 +95,7 @@ interface DeleteUserResult {
message: string; message: string;
} }
export async function deleteUser(id: string): Promise<DeleteUserResult> { export async function deleteUser(id: number): Promise<DeleteUserResult> {
try { try {
const user = getUserById(id); const user = getUserById(id);
if (user && (user.email === 'user@example.com' || user.email === 'admin@example.com')) { if (user && (user.email === 'user@example.com' || user.email === 'admin@example.com')) {
@ -143,7 +142,7 @@ export async function getUserByEmail(email: string): Promise<User | null> {
} }
interface UpdateProfileData { interface UpdateProfileData {
id: string; id: number;
name: string; name: string;
nickname?: string; nickname?: string;
avatarUrl?: string; avatarUrl?: string;

View File

@ -1,5 +1,6 @@
import AddToyForm from '@/components/toys/AddToyForm'; import AddToyForm from '@/components/toys/AddToyForm';
import { mockToys } from '@/lib/mockData'; import { getToyById } from '@/data/operations';
import type { Toy } from '@/types'; import type { Toy } from '@/types';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import Link from 'next/link'; import Link from 'next/link';
@ -9,14 +10,8 @@ interface EditToyPageProps {
params: { id: string }; 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) { export default async function EditToyPage({ params }: EditToyPageProps) {
const toyData = await getToyForEdit(params.id); const toyData = getToyById(params.id);
if (!toyData) { if (!toyData) {
return ( return (

View File

@ -1,20 +1,18 @@
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import ToyCard from "@/components/toys/ToyCard"; import { getToysByOwner } from "@/data/operations";
import { mockToys } from "@/lib/mockData"; // Using all toys for now, filter by ownerId in real app
import Link from "next/link"; import Link from "next/link";
import { PlusCircle, Edit3, Trash2, Eye } from "lucide-react"; import { PlusCircle, Edit3, Trash2, Eye, ToyBrick as ToyBrickIcon } from "lucide-react";
import Image from "next/image"; import Image from "next/image";
import type { Toy } from "@/types"; import type { Toy } from "@/types";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
// Assume this is the logged-in user's ID const currentUserId = 1;
const currentUserId = 'user1';
// Filter toys by current user export default async function MyToysPage() {
const userToys = mockToys.filter(toy => toy.ownerId === currentUserId); const userToys = getToysByOwner(currentUserId);
export default function MyToysPage() {
return ( return (
<div className="space-y-8"> <div className="space-y-8">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
@ -33,7 +31,7 @@ export default function MyToysPage() {
{userToys.length === 0 ? ( {userToys.length === 0 ? (
<Card className="text-center py-12 shadow-md"> <Card className="text-center py-12 shadow-md">
<CardHeader> <CardHeader>
<ToyBrick className="h-16 w-16 mx-auto text-muted-foreground mb-4" /> <ToyBrickIcon className="h-16 w-16 mx-auto text-muted-foreground mb-4" />
<CardTitle>No Toys Listed Yet</CardTitle> <CardTitle>No Toys Listed Yet</CardTitle>
<CardDescription>Share your first toy and spread the joy!</CardDescription> <CardDescription>Share your first toy and spread the joy!</CardDescription>
</CardHeader> </CardHeader>
@ -99,14 +97,14 @@ function ListedToyItem({ toy }: ListedToyItemProps) {
</div> </div>
</div> </div>
<p className="text-muted-foreground mt-2 text-sm line-clamp-2">{toy.description}</p> <p className="text-muted-foreground mt-2 text-sm line-clamp-2">{toy.description}</p>
<div className="mt-4 text-sm"> <div className="mt-4 text-sm space-y-1">
<div>
<span className="font-semibold">Price: </span> <span className="font-semibold">Price: </span>
{toy.pricePerDay !== undefined ? (toy.pricePerDay > 0 ? `$${toy.pricePerDay}/day` : 'Free') : 'Not set'} {toy.pricePerDay !== undefined ? (toy.pricePerDay > 0 ? `$${toy.pricePerDay}/day` : 'Free') : 'Not set'}
</div> </div>
{/* Could add more stats like number of rentals, views etc. here */} </div>
</div> </div>
</div> </div>
</Card> </Card>
); );
} }

View File

@ -1,16 +1,21 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import Link from "next/link"; import Link from "next/link";
import { ToyBrick, PlusCircle, ListOrdered, User, ShoppingBag } from "lucide-react"; import { ToyBrick, PlusCircle, ListOrdered, User, ShoppingBag } from "lucide-react";
import { getToysByOwner } from "@/data/operations";
// Mock data for dashboard overview const currentUserId = 1; // Mock logged-in user ID
const userStats = {
listedToys: 3, export default async function DashboardOverviewPage() {
activeRentals: 1, // Toys I'm renting const userToys = getToysByOwner(currentUserId);
pendingRequests: 2, // Requests for my toys
}; const userStats = {
listedToys: userToys.length, // Real data from DB
activeRentals: 1, // Mock data
pendingRequests: 2, // Mock data
};
export default function DashboardOverviewPage() {
return ( return (
<div className="space-y-8"> <div className="space-y-8">
<Card className="shadow-lg"> <Card className="shadow-lg">

View File

@ -1,20 +1,24 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { ShoppingBag, ToyBrick } from "lucide-react"; import { ShoppingBag, ToyBrick } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import Image from "next/image"; import Image from "next/image";
import type { Toy } from "@/types"; import type { Toy } from "@/types";
import { mockToys } from "@/lib/mockData"; // Using all toys for now import { getAllToys } from "@/data/operations";
// 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() { export default function MyRentalsPage() {
const allToys = getAllToys();
// 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 })[] = [];
if (allToys.length > 1) {
rentedToys.push({ ...allToys[1], rentalEndDate: "2024-12-15" });
}
if (allToys.length > 4) {
rentedToys.push({ ...allToys[4], rentalEndDate: "2025-01-01" });
}
return ( return (
<div className="space-y-8"> <div className="space-y-8">
<div> <div>
@ -50,7 +54,7 @@ export default function MyRentalsPage() {
} }
interface RentalItemCardProps { interface RentalItemCardProps {
toy: Toy & { rentalEndDate?: string, dataAiHint?: string }; toy: Toy & { rentalEndDate?: string };
} }
function RentalItemCard({ toy }: RentalItemCardProps) { function RentalItemCard({ toy }: RentalItemCardProps) {

View File

@ -1,58 +1,55 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { ListOrdered, Check, X } from "lucide-react"; import { ListOrdered, Check, X } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import Image from "next/image"; import Image from "next/image";
import type { Toy } from "@/types"; import type { Toy, RentalRequest } from "@/types";
import { mockToys } from "@/lib/mockData"; import { getAllToys } from "@/data/operations";
import { Badge } from "@/components/ui/badge"; import { Badge } from "@/components/ui/badge";
interface RentalRequest { // This is still mock data. In a real app, this would come from the database.
id: string; function getMockRequests(): RentalRequest[] {
toy: Toy; const allToys = getAllToys();
requesterName: string; const myToys = allToys.filter(t => t.ownerId === 1);
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 if (myToys.length === 0) return [];
const rentalRequests: RentalRequest[] = [
const rentalRequests: RentalRequest[] = [
{ {
id: 'req1', id: 'req1',
toy: mockToys[0], // Colorful Building Blocks Set (owned by user1) toy: myToys[0],
requesterName: 'Charlie Brown', requesterName: 'Charlie Brown',
requesterId: 'user4', requesterId: 4,
requestedDates: 'August 10, 2024 - August 17, 2024', requestedDates: 'August 10, 2024 - August 17, 2024',
status: 'pending', status: 'pending',
message: 'My son would love to play with these for his birthday week!', message: 'My son would love to play with these for his birthday week!',
dataAiHint: mockToys[0]?.category.toLowerCase(), dataAiHint: myToys[0]?.category.toLowerCase(),
}, },
{ {
id: 'req2', id: 'req2',
toy: mockToys[3], // Plush Teddy Bear (owned by user1) toy: myToys.length > 1 ? myToys[1] : myToys[0],
requesterName: 'Diana Prince', requesterName: 'Diana Prince',
requesterId: 'user5', requesterId: 5,
requestedDates: 'September 1, 2024 - September 5, 2024', requestedDates: 'September 1, 2024 - September 5, 2024',
status: 'approved', status: 'approved',
dataAiHint: mockToys[3]?.category.toLowerCase(), dataAiHint: (myToys.length > 1 ? myToys[1] : myToys[0])?.category.toLowerCase(),
}, },
{ {
id: 'req3', id: 'req3',
toy: mockToys[0], toy: myToys[0],
requesterName: 'Edward Nigma', requesterName: 'Edward Nigma',
requesterId: 'user6', requesterId: 6,
requestedDates: 'July 20, 2024 - July 22, 2024', requestedDates: 'July 20, 2024 - July 22, 2024',
status: 'declined', status: 'declined',
message: 'Looking for a weekend rental.', message: 'Looking for a weekend rental.',
dataAiHint: mockToys[0]?.category.toLowerCase(), dataAiHint: myToys[0]?.category.toLowerCase(),
}, },
]; ];
return rentalRequests;
}
// Assuming current user is user1 for whom these requests are relevant const currentUserToyRequests = getMockRequests();
const currentUserToyRequests = rentalRequests.filter(req => req.toy.ownerId === 'user1');
export default function RentalRequestsPage() { export default function RentalRequestsPage() {

View File

@ -1,10 +1,28 @@
import ToyList from '@/components/toys/ToyList'; import ToyList from '@/components/toys/ToyList';
import { mockToys } from '@/lib/mockData'; import { getAllToys } from '@/data/operations';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import Link from 'next/link'; import Link from 'next/link';
import { PlusCircle } from 'lucide-react'; import { PlusCircle } from 'lucide-react';
export default function HomePage() { export default async function HomePage() {
const toys = getAllToys();
const t = (key: string, params?: any) => {
// Basic mock t function for non-localized pages
const keyParts = key.split('.');
let text = keyParts.pop() || key;
text = text.replace(/_/g, ' ');
text = text.charAt(0).toUpperCase() + text.slice(1);
if (params) {
Object.keys(params).forEach(pKey => {
text = text.replace(`{${pKey}}`, params[pKey]);
})
}
return text;
};
return ( return (
<div className="space-y-8"> <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"> <section className="text-center py-12 bg-gradient-to-r from-primary/10 via-background to-accent/10 rounded-lg shadow">
@ -33,7 +51,7 @@ export default function HomePage() {
<h2 className="text-3xl font-bold font-headline text-center mb-8 text-primary"> <h2 className="text-3xl font-bold font-headline text-center mb-8 text-primary">
Available Toys Available Toys
</h2> </h2>
<ToyList toys={mockToys.map(toy => ({...toy, dataAiHint: toy.category.toLowerCase()}))} /> <ToyList toys={toys} t={t} />
</section> </section>
</div> </div>
); );

View File

@ -1,26 +1,21 @@
import Image from 'next/image'; import Image from 'next/image';
import { mockToys } from '@/lib/mockData'; import { getToyById, getAllToys } from '@/data/operations';
import type { Toy } from '@/types'; import type { Toy } from '@/types';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import AvailabilityCalendar from '@/components/toys/AvailabilityCalendar'; import { ArrowLeft, DollarSign, MapPin, ShoppingBag, UserCircle2 } from 'lucide-react';
import { ArrowLeft, CalendarDays, DollarSign, MapPin, ShoppingBag, UserCircle2 } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Separator } from '@/components/ui/separator'; import { Separator } from '@/components/ui/separator';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
interface ToyPageProps { interface ToyPageProps {
params: { id: string }; 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) { export default async function ToyPage({ params }: ToyPageProps) {
const toy = await getToyById(params.id); const toy = getToyById(params.id);
if (!toy) { if (!toy) {
return ( return (
@ -47,7 +42,6 @@ export default async function ToyPage({ params }: ToyPageProps) {
</Link> </Link>
<div className="grid md:grid-cols-2 gap-8 lg:gap-12 items-start"> <div className="grid md:grid-cols-2 gap-8 lg:gap-12 items-start">
{/* Image Gallery Section */}
<div className="space-y-4"> <div className="space-y-4">
<div className="aspect-video relative w-full rounded-lg overflow-hidden shadow-lg"> <div className="aspect-video relative w-full rounded-lg overflow-hidden shadow-lg">
<Image <Image
@ -76,7 +70,6 @@ export default async function ToyPage({ params }: ToyPageProps) {
)} )}
</div> </div>
{/* Toy Details Section */}
<div className="space-y-6"> <div className="space-y-6">
<Badge variant="secondary" className="text-sm">{toy.category}</Badge> <Badge variant="secondary" className="text-sm">{toy.category}</Badge>
<h1 className="text-4xl font-bold font-headline text-primary">{toy.name}</h1> <h1 className="text-4xl font-bold font-headline text-primary">{toy.name}</h1>
@ -88,11 +81,18 @@ export default async function ToyPage({ params }: ToyPageProps) {
<Separator /> <Separator />
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
<div className="flex items-center"> <div className="flex items-center gap-2">
<UserCircle2 className="h-5 w-5 mr-2 text-accent" /> <Link href={`/owner/${toy.ownerId}/toys`} className="flex-shrink-0">
<Avatar className="h-8 w-8">
<AvatarImage src={toy.ownerAvatarUrl} alt={toy.ownerName} data-ai-hint="owner avatar" />
<AvatarFallback>{toy.ownerName.split(' ').map(n => n[0]).join('').toUpperCase()}</AvatarFallback>
</Avatar>
</Link>
<div> <div>
<span className="font-medium text-muted-foreground">Owner: </span> <span className="font-medium text-muted-foreground">Owner: </span>
<span className="text-foreground">{toy.ownerName}</span> <Link href={`/owner/${toy.ownerId}/toys`} className="text-foreground hover:underline">
{toy.ownerName}
</Link>
</div> </div>
</div> </div>
{toy.location && ( {toy.location && (
@ -109,7 +109,9 @@ export default async function ToyPage({ params }: ToyPageProps) {
<DollarSign className="h-5 w-5 mr-2 text-accent" /> <DollarSign className="h-5 w-5 mr-2 text-accent" />
<div> <div>
<span className="font-medium text-muted-foreground">Price: </span> <span className="font-medium text-muted-foreground">Price: </span>
<span className="text-foreground font-semibold">{toy.pricePerDay > 0 ? `$${toy.pricePerDay}/day` : 'Free'}</span> <span className="text-foreground font-semibold">
{toy.pricePerDay > 0 ? `$${toy.pricePerDay}/day` : 'Free'}
</span>
</div> </div>
</div> </div>
)} )}
@ -117,8 +119,6 @@ export default async function ToyPage({ params }: ToyPageProps) {
<Separator /> <Separator />
<AvailabilityCalendar availability={toy.availability} />
<Button size="lg" className="w-full mt-6 transition-transform transform hover:scale-105"> <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 <ShoppingBag className="mr-2 h-5 w-5" /> Request to Rent
</Button> </Button>
@ -130,7 +130,8 @@ export default async function ToyPage({ params }: ToyPageProps) {
// Generate static paths for all toys // Generate static paths for all toys
export async function generateStaticParams() { export async function generateStaticParams() {
return mockToys.map((toy) => ({ const toys = getAllToys();
return toys.map((toy) => ({
id: toy.id, id: toy.id,
})); }));
} }

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
@ -11,8 +11,11 @@ import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { useToast } from '@/hooks/use-toast'; import { useToast } from '@/hooks/use-toast';
import { ToyBrick, Save, PlusCircle, Trash2 } from 'lucide-react'; import { ToyBrick, Save, PlusCircle, Trash2 } from 'lucide-react';
import type { Toy } from '@/types'; import type { Toy, User } from '@/types';
import { useI18n, useCurrentLocale } from '@/locales/client'; import { useI18n, useCurrentLocale } from '@/locales/client';
import { createOrUpdateToy, type ToyFormData } from '@/app/actions/toy';
import { getUserByEmail } from '@/app/actions/user';
const toyCategoryDefinitions = [ const toyCategoryDefinitions = [
{ key: 'educational', value: 'Educational' }, { key: 'educational', value: 'Educational' },
@ -30,7 +33,7 @@ const toyCategoryDefinitions = [
]; ];
interface AddToyFormProps { interface AddToyFormProps {
initialData?: Partial<Toy>; initialData?: Toy;
isEditMode?: boolean; isEditMode?: boolean;
} }
@ -45,10 +48,22 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
const [category, setCategory] = useState(initialData?.category || ''); const [category, setCategory] = useState(initialData?.category || '');
const [pricePerDay, setPricePerDay] = useState(initialData?.pricePerDay?.toString() || '0'); const [pricePerDay, setPricePerDay] = useState(initialData?.pricePerDay?.toString() || '0');
const [location, setLocation] = useState(initialData?.location || ''); const [location, setLocation] = useState(initialData?.location || '');
const [images, setImages] = useState<string[]>(initialData?.images || ['']); const [images, setImages] = useState<string[]>(initialData?.images?.length ? initialData.images : ['']);
// unavailableRanges will be initialized as empty or from initialData, but not editable in this form version.
const [unavailableRanges, setUnavailableRanges] = useState<Toy['unavailableRanges']>(initialData?.unavailableRanges || []); const [unavailableRanges, setUnavailableRanges] = useState<Toy['unavailableRanges']>(initialData?.unavailableRanges || []);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [currentUser, setCurrentUser] = useState<User | null>(null);
useEffect(() => {
const fetchUser = async () => {
const userEmail = localStorage.getItem('userEmail');
if (userEmail) {
const user = await getUserByEmail(userEmail);
setCurrentUser(user);
}
};
fetchUser();
}, []);
const handleImageChange = (index: number, value: string) => { const handleImageChange = (index: number, value: string) => {
const newImages = [...images]; const newImages = [...images];
@ -75,27 +90,37 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
return; return;
} }
const toyData: Partial<Toy> = { if (!currentUser) {
name, description, category, toast({ title: "Authentication Error", description: "Could not identify current user. Please log in again.", variant: "destructive" });
setIsLoading(false);
return;
}
const toyFormData: ToyFormData & { ownerId: number } = {
name,
description,
category,
pricePerDay: parseFloat(pricePerDay) || 0, pricePerDay: parseFloat(pricePerDay) || 0,
location, location,
images: images.filter(img => img.trim() !== ''), images: images.filter(img => img.trim() !== ''),
unavailableRanges: initialData?.unavailableRanges || [], // Preserve existing, or empty for new unavailableRanges,
ownerId: currentUser.id,
}; };
if (isEditMode && initialData?.id) {
toyData.id = initialData.id;
}
const result = await createOrUpdateToy(toyFormData, isEditMode && initialData?.id ? initialData.id : null);
console.log("Submitting toy data:", toyData); setIsLoading(false);
await new Promise(resolve => setTimeout(resolve, 1500));
if (result.success && result.toy) {
toast({ toast({
title: isEditMode ? t('add_toy_form.edit_title_toast') : t('add_toy_form.add_title_toast'), title: isEditMode ? t('add_toy_form.edit_title_toast') : t('add_toy_form.add_title_toast'),
description: t('add_toy_form.success_description_toast', { toyName: name, action: isEditMode ? t('add_toy_form.updated_action_toast') : t('add_toy_form.listed_action_toast')}) description: t('add_toy_form.success_description_toast', { toyName: result.toy.name, action: isEditMode ? t('add_toy_form.updated_action_toast') : t('add_toy_form.listed_action_toast')})
}); });
router.push(`/${locale}/dashboard/my-toys`); router.push(`/${locale}/dashboard/my-toys`);
setIsLoading(false); router.refresh();
} else {
toast({ title: "Error", description: result.message, variant: "destructive" });
}
}; };
return ( return (
@ -174,7 +199,7 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
</div> </div>
<CardFooter className="p-0 pt-6"> <CardFooter className="p-0 pt-6">
<Button type="submit" className="w-full" size="lg" disabled={isLoading}> <Button type="submit" className="w-full" size="lg" disabled={isLoading || !currentUser}>
{isLoading ? (isEditMode ? t('add_toy_form.saving_button') : t('add_toy_form.listing_button')) : ( {isLoading ? (isEditMode ? t('add_toy_form.saving_button') : t('add_toy_form.listing_button')) : (
<> <>
<Save className="mr-2 h-5 w-5" /> <Save className="mr-2 h-5 w-5" />

View File

@ -1,6 +1,7 @@
import db from '@/lib/db'; import db from '@/lib/db';
import type { Toy, User } from '@/types'; import type { Toy, User } from '@/types';
import { randomUUID } from 'crypto';
// Helper to parse toy data from DB, converting JSON strings back to objects // Helper to parse toy data from DB, converting JSON strings back to objects
const parseToy = (toyData: any): Toy => { const parseToy = (toyData: any): Toy => {
@ -38,7 +39,7 @@ export function getToyById(id: string): Toy | undefined {
return toy ? parseToy(toy) : undefined; return toy ? parseToy(toy) : undefined;
} }
export function getToysByOwner(ownerId: string): Toy[] { export function getToysByOwner(ownerId: number): Toy[] {
const stmt = db.prepare(` const stmt = db.prepare(`
SELECT t.*, u.name as ownerName, u.avatarUrl as ownerAvatarUrl SELECT t.*, u.name as ownerName, u.avatarUrl as ownerAvatarUrl
FROM toys t FROM toys t
@ -49,8 +50,50 @@ export function getToysByOwner(ownerId: string): Toy[] {
return toys.map(parseToy); return toys.map(parseToy);
} }
// This now fetches the full user profile from the database export function createToy(toyData: Omit<Toy, 'id' | 'ownerName' | 'ownerAvatarUrl' | 'dataAiHint'>): Toy {
export function getOwnerProfile(ownerId: string): User | undefined { const id = `toy-${randomUUID()}`;
const stmt = db.prepare(
`INSERT INTO toys (id, name, description, category, images, unavailableRanges, ownerId, pricePerDay, location)
VALUES (@id, @name, @description, @category, @images, @unavailableRanges, @ownerId, @pricePerDay, @location)`
);
stmt.run({
...toyData,
id,
images: JSON.stringify(toyData.images),
unavailableRanges: JSON.stringify(toyData.unavailableRanges),
});
return getToyById(id)!;
}
export function updateToy(toyData: Omit<Toy, 'ownerName' | 'ownerAvatarUrl' | 'dataAiHint'>): Toy {
const stmt = db.prepare(
`UPDATE toys SET
name = @name,
description = @description,
category = @category,
images = @images,
unavailableRanges = @unavailableRanges,
pricePerDay = @pricePerDay,
location = @location
WHERE id = @id AND ownerId = @ownerId` // Security check
);
const result = stmt.run({
...toyData,
images: JSON.stringify(toyData.images),
unavailableRanges: JSON.stringify(toyData.unavailableRanges),
});
if (result.changes === 0) {
throw new Error("Toy not found or user not authorized to update.");
}
return getToyById(toyData.id)!;
}
export function getOwnerProfile(ownerId: number): User | undefined {
return getUserById(ownerId); return getUserById(ownerId);
} }
@ -61,7 +104,7 @@ export function getAllUsers(): User[] {
return stmt.all() as User[]; return stmt.all() as User[];
} }
export function getUserById(id: string): User | undefined { export function getUserById(id: number): User | undefined {
const stmt = db.prepare('SELECT * FROM users WHERE id = ?'); const stmt = db.prepare('SELECT * FROM users WHERE id = ?');
return stmt.get(id) as User | undefined; return stmt.get(id) as User | undefined;
} }

View File

@ -15,12 +15,16 @@ db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON'); db.pragma('foreign_keys = ON');
function initDb() { function initDb() {
console.log("Initializing database schema if needed..."); // Check if the users table exists. If it does, we assume the DB is initialized.
const table = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'users'").get();
// Create Users Table if it doesn't exist if (!table) {
console.log("Database not found. Initializing and seeding...");
// Create Users Table
db.exec(` db.exec(`
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE users (
id TEXT PRIMARY KEY, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL, name TEXT NOT NULL,
nickname TEXT, nickname TEXT,
email TEXT NOT NULL UNIQUE, email TEXT NOT NULL UNIQUE,
@ -30,50 +34,38 @@ function initDb() {
); );
`); `);
// Migration: Add nickname column to users table if it doesn't exist // Create Toys Table
try {
const columns = db.prepare("PRAGMA table_info(users)").all();
if (!columns.some((col: any) => col.name === 'nickname')) {
console.log("Adding 'nickname' column to 'users' table...");
db.exec('ALTER TABLE users ADD COLUMN nickname TEXT');
}
} catch (error) {
console.error("Error during 'users' table migration:", error);
}
// Create Toys Table if it doesn't exist
db.exec(` db.exec(`
CREATE TABLE IF NOT EXISTS toys ( CREATE TABLE toys (
id TEXT PRIMARY KEY, id TEXT PRIMARY KEY,
name TEXT NOT NULL, name TEXT NOT NULL,
description TEXT NOT NULL, description TEXT NOT NULL,
category TEXT NOT NULL, category TEXT NOT NULL,
images TEXT, images TEXT,
unavailableRanges TEXT, unavailableRanges TEXT,
ownerId TEXT NOT NULL, ownerId INTEGER NOT NULL,
pricePerDay REAL, pricePerDay REAL,
location TEXT, location TEXT,
FOREIGN KEY (ownerId) REFERENCES users(id) ON DELETE CASCADE FOREIGN KEY (ownerId) REFERENCES users(id) ON DELETE CASCADE
); );
`); `);
console.log("Seeding initial data if missing..."); console.log("Seeding initial data...");
// Use INSERT OR IGNORE to only add data if the primary key doesn't exist. // Prepare insert statements
// This prevents errors on subsequent runs and adds missing users/toys without overwriting.
const insertUser = db.prepare(` const insertUser = db.prepare(`
INSERT OR IGNORE INTO users (id, name, nickname, email, role, avatarUrl, bio) INSERT INTO users (id, name, nickname, email, role, avatarUrl, bio)
VALUES (@id, @name, @nickname, @email, @role, @avatarUrl, @bio) VALUES (@id, @name, @nickname, @email, @role, @avatarUrl, @bio)
`); `);
const insertToy = db.prepare(` const insertToy = db.prepare(`
INSERT OR IGNORE INTO toys (id, name, description, category, images, unavailableRanges, ownerId, pricePerDay, location) INSERT INTO toys (id, name, description, category, images, unavailableRanges, ownerId, pricePerDay, location)
VALUES (@id, @name, @description, @category, @images, @unavailableRanges, @ownerId, @pricePerDay, @location) VALUES (@id, @name, @description, @category, @images, @unavailableRanges, @ownerId, @pricePerDay, @location)
`); `);
const insertManyUsers = db.transaction((users: User[]) => { // Use a transaction for efficiency
for (const user of users) { const seedData = db.transaction(() => {
for (const user of rawUsers) {
insertUser.run({ insertUser.run({
id: user.id, id: user.id,
name: user.name, name: user.name,
@ -84,10 +76,7 @@ function initDb() {
bio: user.bio ?? null bio: user.bio ?? null
}); });
} }
}); for (const toy of rawToys) {
const insertManyToys = db.transaction((toys) => {
for (const toy of toys) {
insertToy.run({ insertToy.run({
...toy, ...toy,
images: JSON.stringify(toy.images), images: JSON.stringify(toy.images),
@ -96,10 +85,12 @@ function initDb() {
} }
}); });
insertManyUsers(rawUsers); seedData();
insertManyToys(rawToys);
console.log("Database initialization and seeding complete."); console.log("Database initialization and seeding complete.");
} else {
console.log("Database already initialized.");
}
} }
// Initialize and export db // Initialize and export db

View File

@ -5,13 +5,13 @@ import { addDays, formatISO, subDays } from 'date-fns';
const today = new Date(); const today = new Date();
export const rawUsers: User[] = [ export const rawUsers: User[] = [
{ id: 'user1', name: 'Alice Wonderland', nickname: 'Alice', email: 'user@example.com', role: 'Admin', 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 that my kids have outgrown but still have lots of life left in them!" }, { id: 1, name: 'Alice Wonderland', nickname: 'Alice', email: 'user@example.com', role: 'Admin', 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 that my kids have outgrown but still have lots of life left in them!" },
{ id: 'user2', name: 'Bob The Builder', nickname: 'Bob', email: 'user2@example.com', role: 'User', avatarUrl: 'https://placehold.co/100x100.png?text=BT', bio: "Can we fix it? Yes, we can! Sharing my collection of construction toys, tools, and playsets. Always happy to help another budding builder." }, { id: 2, name: 'Bob The Builder', nickname: 'Bob', email: 'user2@example.com', role: 'User', avatarUrl: 'https://placehold.co/100x100.png?text=BT', bio: "Can we fix it? Yes, we can! Sharing my collection of construction toys, tools, and playsets. Always happy to help another budding builder." },
{ id: 'user3', name: 'Carol Danvers', nickname: 'Captain Marvel', email: 'user3@example.com', role: 'User', avatarUrl: 'https://placehold.co/100x100.png?text=CD', bio: "Higher, further, faster. Sharing toys that inspire adventure, courage, and exploration. My collection includes superhero action figures and space-themed playsets." }, { id: 3, name: 'Carol Danvers', nickname: 'Captain Marvel', email: 'user3@example.com', role: 'User', avatarUrl: 'https://placehold.co/100x100.png?text=CD', bio: "Higher, further, faster. Sharing toys that inspire adventure, courage, and exploration. My collection includes superhero action figures and space-themed playsets." },
{ id: 'user4', name: 'Charlie Brown', nickname: 'Chuck', email: 'user4@example.com', role: 'User', avatarUrl: '', bio: '' }, { id: 4, name: 'Charlie Brown', nickname: 'Chuck', email: 'user4@example.com', role: 'User', avatarUrl: '', bio: '' },
{ id: 'user5', name: 'Diana Prince', nickname: 'Wonder Woman', email: 'user5@example.com', role: 'User', avatarUrl: '', bio: '' }, { id: 5, name: 'Diana Prince', nickname: 'Wonder Woman', email: 'user5@example.com', role: 'User', avatarUrl: '', bio: '' },
{ id: 'user6', name: 'Edward Nigma', nickname: 'Riddler', email: 'user6@example.com', role: 'User', avatarUrl: '', bio: '' }, { id: 6, name: 'Edward Nigma', nickname: 'Riddler', email: 'user6@example.com', role: 'User', avatarUrl: '', bio: '' },
{ id: 'admin-main', name: 'Main Admin', nickname: 'Head Honcho', email: 'admin@example.com', role: 'Admin', avatarUrl: 'https://placehold.co/100x100.png?text=ADM', bio: 'Keeping the toy box tidy.' }, { id: 7, name: 'Main Admin', nickname: 'Head Honcho', email: 'admin@example.com', role: 'Admin', avatarUrl: 'https://placehold.co/100x100.png?text=ADM', bio: 'Keeping the toy box tidy.' },
]; ];
export const rawToys: Omit<Toy, 'ownerName' | 'ownerAvatarUrl' | 'dataAiHint'>[] = [ export const rawToys: Omit<Toy, 'ownerName' | 'ownerAvatarUrl' | 'dataAiHint'>[] = [
@ -25,7 +25,7 @@ export const rawToys: Omit<Toy, 'ownerName' | 'ownerAvatarUrl' | 'dataAiHint'>[]
{ startDate: formatISO(addDays(today, 5), { representation: 'date' }), endDate: formatISO(addDays(today, 7), { representation: 'date' }) }, { startDate: formatISO(addDays(today, 5), { representation: 'date' }), endDate: formatISO(addDays(today, 7), { representation: 'date' }) },
{ startDate: formatISO(addDays(today, 15), { representation: 'date' }), endDate: formatISO(addDays(today, 16), { representation: 'date' }) }, { startDate: formatISO(addDays(today, 15), { representation: 'date' }), endDate: formatISO(addDays(today, 16), { representation: 'date' }) },
], ],
ownerId: 'user1', ownerId: 1,
pricePerDay: 5, pricePerDay: 5,
location: 'Springfield Gardens', location: 'Springfield Gardens',
}, },
@ -38,7 +38,7 @@ export const rawToys: Omit<Toy, 'ownerName' | 'ownerAvatarUrl' | 'dataAiHint'>[]
unavailableRanges: [ unavailableRanges: [
{ startDate: formatISO(addDays(today, 10), { representation: 'date' }), endDate: formatISO(addDays(today, 12), { representation: 'date' }) }, { startDate: formatISO(addDays(today, 10), { representation: 'date' }), endDate: formatISO(addDays(today, 12), { representation: 'date' }) },
], ],
ownerId: 'user2', ownerId: 2,
pricePerDay: 8, pricePerDay: 8,
location: 'Willow Creek', location: 'Willow Creek',
}, },
@ -49,7 +49,7 @@ export const rawToys: Omit<Toy, 'ownerName' | 'ownerAvatarUrl' | 'dataAiHint'>[]
category: 'Electronics', category: 'Electronics',
images: ['https://placehold.co/600x400.png?text=Kids+Tablet', 'https://placehold.co/600x400.png?text=Tablet+Screen'], images: ['https://placehold.co/600x400.png?text=Kids+Tablet', 'https://placehold.co/600x400.png?text=Tablet+Screen'],
unavailableRanges: [], unavailableRanges: [],
ownerId: 'user3', ownerId: 3,
pricePerDay: 7, pricePerDay: 7,
location: 'Metro City', location: 'Metro City',
}, },
@ -62,7 +62,7 @@ export const rawToys: Omit<Toy, 'ownerName' | 'ownerAvatarUrl' | 'dataAiHint'>[]
unavailableRanges: [ unavailableRanges: [
{ startDate: formatISO(addDays(today, 20), { representation: 'date' }), endDate: formatISO(addDays(today, 25), { representation: 'date' }) }, { startDate: formatISO(addDays(today, 20), { representation: 'date' }), endDate: formatISO(addDays(today, 25), { representation: 'date' }) },
], ],
ownerId: 'user1', ownerId: 1,
pricePerDay: 3, pricePerDay: 3,
location: 'Springfield Gardens', location: 'Springfield Gardens',
}, },
@ -73,7 +73,7 @@ export const rawToys: Omit<Toy, 'ownerName' | 'ownerAvatarUrl' | 'dataAiHint'>[]
category: 'Musical', category: 'Musical',
images: ['https://placehold.co/600x400.png?text=Kids+Guitar'], images: ['https://placehold.co/600x400.png?text=Kids+Guitar'],
unavailableRanges: [], unavailableRanges: [],
ownerId: 'user2', ownerId: 2,
pricePerDay: 10, pricePerDay: 10,
location: 'Willow Creek', location: 'Willow Creek',
}, },
@ -84,7 +84,7 @@ export const rawToys: Omit<Toy, 'ownerName' | 'ownerAvatarUrl' | 'dataAiHint'>[]
category: 'Outdoor', category: 'Outdoor',
images: ['https://placehold.co/600x400.png?text=Sports+Kit'], images: ['https://placehold.co/600x400.png?text=Sports+Kit'],
unavailableRanges: [], unavailableRanges: [],
ownerId: 'user3', ownerId: 3,
pricePerDay: 6, pricePerDay: 6,
location: 'Metro City', location: 'Metro City',
} }

View File

@ -1,7 +1,7 @@
export interface MessageEntry { export interface MessageEntry {
id: string; id: string;
senderId: string; senderId: number;
senderName: string; senderName: string;
text: string; text: string;
timestamp: string; // ISO date string timestamp: string; // ISO date string
@ -15,7 +15,7 @@ export interface Toy {
images: string[]; images: string[];
unavailableRanges: { startDate: string; endDate: string }[]; unavailableRanges: { startDate: string; endDate: string }[];
ownerName: string; ownerName: string;
ownerId: string; ownerId: number;
ownerAvatarUrl?: string; ownerAvatarUrl?: string;
pricePerDay?: number; pricePerDay?: number;
location?: string; location?: string;
@ -23,7 +23,7 @@ export interface Toy {
} }
export interface User { export interface User {
id: string; id: number;
name: string; // This is now Full Name name: string; // This is now Full Name
nickname?: string; nickname?: string;
email: string; email: string;
@ -40,7 +40,7 @@ export interface DailyAvailability {
export interface RentalHistoryEntry { export interface RentalHistoryEntry {
id:string; id:string;
userId: string; userId: number;
toy: Toy; toy: Toy;
rentalStartDate: string; rentalStartDate: string;
rentalEndDate: string; rentalEndDate: string;
@ -53,7 +53,7 @@ export interface RentalRequest {
id: string; id: string;
toy: Toy; toy: Toy;
requesterName: string; requesterName: string;
requesterId: string; requesterId: number;
requestedDates: string; requestedDates: string;
status: 'pending' | 'approved' | 'declined'; status: 'pending' | 'approved' | 'declined';
message?: string; message?: string;

Binary file not shown.

Binary file not shown.

Binary file not shown.