Compare commits
10 Commits
1e994e8a4c
...
0d49a916c7
| Author | SHA1 | Date |
|---|---|---|
|
|
0d49a916c7 | |
|
|
1154e56555 | |
|
|
c74edf7397 | |
|
|
276db4fa31 | |
|
|
e6491b8440 | |
|
|
0b22c28dfa | |
|
|
a6ccc6a5f4 | |
|
|
bcaebf0494 | |
|
|
dad13c9cfb | |
|
|
0f224287fd |
|
|
@ -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 }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -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 };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
if (myToys.length === 0) return [];
|
||||||
status: 'pending' | 'approved' | 'declined';
|
|
||||||
message?: string;
|
const rentalRequests: RentalRequest[] = [
|
||||||
dataAiHint?: string;
|
{
|
||||||
|
id: 'req1',
|
||||||
|
toy: myToys[0],
|
||||||
|
requesterName: 'Charlie Brown',
|
||||||
|
requesterId: 4,
|
||||||
|
requestedDates: 'August 10, 2024 - August 17, 2024',
|
||||||
|
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?',
|
||||||
|
dataAiHint: myToys[0]?.category.toLowerCase(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'req2',
|
||||||
|
toy: myToys.length > 1 ? myToys[1] : myToys[0],
|
||||||
|
requesterName: 'Diana Prince',
|
||||||
|
requesterId: 5,
|
||||||
|
requestedDates: 'September 1, 2024 - September 5, 2024',
|
||||||
|
status: 'approved',
|
||||||
|
dataAiHint: (myToys.length > 1 ? myToys[1] : myToys[0])?.category.toLowerCase(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'req3',
|
||||||
|
toy: myToys[0],
|
||||||
|
requesterName: 'Edward Nigma',
|
||||||
|
requesterId: 6,
|
||||||
|
requestedDates: 'July 20, 2024 - July 22, 2024',
|
||||||
|
status: 'declined',
|
||||||
|
message: 'Looking for a weekend rental.',
|
||||||
|
dataAiHint: myToys[0]?.category.toLowerCase(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return rentalRequests;
|
||||||
}
|
}
|
||||||
|
|
||||||
const rentalRequests: RentalRequest[] = [
|
const currentUserToyRequests = getMockRequests();
|
||||||
{
|
|
||||||
id: 'req1',
|
|
||||||
toy: mockToys[0],
|
|
||||||
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! 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(),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'req2',
|
|
||||||
toy: mockToys[3],
|
|
||||||
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(),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const currentUserToyRequests = rentalRequests.filter(req => req.toy.ownerId === 'user1');
|
|
||||||
|
|
||||||
|
|
||||||
export default async function RentalRequestsPage() {
|
export default async function RentalRequestsPage() {
|
||||||
|
|
@ -167,4 +165,3 @@ function RequestItemCard({ request, t }: RequestItemCardProps) {
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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.' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 = {
|
|
||||||
id: `user-${randomUUID()}`,
|
|
||||||
name,
|
|
||||||
nickname,
|
|
||||||
email,
|
|
||||||
role: role,
|
|
||||||
avatarUrl: '',
|
|
||||||
bio: ''
|
|
||||||
};
|
|
||||||
|
|
||||||
const stmt = db.prepare(
|
const stmt = db.prepare(
|
||||||
'INSERT INTO users (id, name, nickname, email, role, avatarUrl, bio) VALUES (@id, @name, @nickname, @email, @role, @avatarUrl, @bio)'
|
'INSERT INTO users (name, nickname, email, role, avatarUrl, bio) VALUES (@name, @nickname, @email, @role, @avatarUrl, @bio)'
|
||||||
);
|
);
|
||||||
|
|
||||||
stmt.run(newUser);
|
const info = stmt.run({
|
||||||
|
name,
|
||||||
|
nickname: nickname ?? null,
|
||||||
|
email,
|
||||||
|
role: role,
|
||||||
|
avatarUrl: '',
|
||||||
|
bio: ''
|
||||||
|
});
|
||||||
|
|
||||||
|
const newUserId = info.lastInsertRowid as number;
|
||||||
|
const newUser = getUserById(newUserId);
|
||||||
|
|
||||||
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;
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
<span className="font-semibold">Price: </span>
|
<div>
|
||||||
{toy.pricePerDay !== undefined ? (toy.pricePerDay > 0 ? `$${toy.pricePerDay}/day` : 'Free') : 'Not set'}
|
<span className="font-semibold">Price: </span>
|
||||||
|
{toy.pricePerDay !== undefined ? (toy.pricePerDay > 0 ? `$${toy.pricePerDay}/day` : 'Free') : 'Not set'}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Could add more stats like number of rentals, views etc. here */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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"
|
if (myToys.length === 0) return [];
|
||||||
status: 'pending' | 'approved' | 'declined';
|
|
||||||
message?: string;
|
const rentalRequests: RentalRequest[] = [
|
||||||
dataAiHint?: string;
|
{
|
||||||
|
id: 'req1',
|
||||||
|
toy: myToys[0],
|
||||||
|
requesterName: 'Charlie Brown',
|
||||||
|
requesterId: 4,
|
||||||
|
requestedDates: 'August 10, 2024 - August 17, 2024',
|
||||||
|
status: 'pending',
|
||||||
|
message: 'My son would love to play with these for his birthday week!',
|
||||||
|
dataAiHint: myToys[0]?.category.toLowerCase(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'req2',
|
||||||
|
toy: myToys.length > 1 ? myToys[1] : myToys[0],
|
||||||
|
requesterName: 'Diana Prince',
|
||||||
|
requesterId: 5,
|
||||||
|
requestedDates: 'September 1, 2024 - September 5, 2024',
|
||||||
|
status: 'approved',
|
||||||
|
dataAiHint: (myToys.length > 1 ? myToys[1] : myToys[0])?.category.toLowerCase(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'req3',
|
||||||
|
toy: myToys[0],
|
||||||
|
requesterName: 'Edward Nigma',
|
||||||
|
requesterId: 6,
|
||||||
|
requestedDates: 'July 20, 2024 - July 22, 2024',
|
||||||
|
status: 'declined',
|
||||||
|
message: 'Looking for a weekend rental.',
|
||||||
|
dataAiHint: myToys[0]?.category.toLowerCase(),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
return rentalRequests;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mock data: rental requests for the current user's toys
|
const currentUserToyRequests = getMockRequests();
|
||||||
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() {
|
export default function RentalRequestsPage() {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -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,12 +81,19 @@ 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">
|
||||||
<div>
|
<Avatar className="h-8 w-8">
|
||||||
<span className="font-medium text-muted-foreground">Owner: </span>
|
<AvatarImage src={toy.ownerAvatarUrl} alt={toy.ownerName} data-ai-hint="owner avatar" />
|
||||||
<span className="text-foreground">{toy.ownerName}</span>
|
<AvatarFallback>{toy.ownerName.split(' ').map(n => n[0]).join('').toUpperCase()}</AvatarFallback>
|
||||||
</div>
|
</Avatar>
|
||||||
|
</Link>
|
||||||
|
<div>
|
||||||
|
<span className="font-medium text-muted-foreground">Owner: </span>
|
||||||
|
<Link href={`/owner/${toy.ownerId}/toys`} className="text-foreground hover:underline">
|
||||||
|
{toy.ownerName}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{toy.location && (
|
{toy.location && (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
|
|
@ -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();
|
||||||
id: toy.id,
|
return toys.map((toy) => ({
|
||||||
}));
|
id: toy.id,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
|
||||||
|
|
||||||
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')})
|
|
||||||
});
|
|
||||||
router.push(`/${locale}/dashboard/my-toys`);
|
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
|
|
||||||
|
if (result.success && result.toy) {
|
||||||
|
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: 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.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" />
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
115
src/lib/db.ts
115
src/lib/db.ts
|
|
@ -15,65 +15,57 @@ 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) {
|
||||||
db.exec(`
|
console.log("Database not found. Initializing and seeding...");
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
nickname TEXT,
|
|
||||||
email TEXT NOT NULL UNIQUE,
|
|
||||||
role TEXT,
|
|
||||||
avatarUrl TEXT,
|
|
||||||
bio TEXT
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
// Migration: Add nickname column to users table if it doesn't exist
|
// Create Users Table
|
||||||
try {
|
db.exec(`
|
||||||
const columns = db.prepare("PRAGMA table_info(users)").all();
|
CREATE TABLE users (
|
||||||
if (!columns.some((col: any) => col.name === 'nickname')) {
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
console.log("Adding 'nickname' column to 'users' table...");
|
name TEXT NOT NULL,
|
||||||
db.exec('ALTER TABLE users ADD COLUMN nickname TEXT');
|
nickname TEXT,
|
||||||
}
|
email TEXT NOT NULL UNIQUE,
|
||||||
} catch (error) {
|
role TEXT,
|
||||||
console.error("Error during 'users' table migration:", error);
|
avatarUrl TEXT,
|
||||||
}
|
bio TEXT
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Create Toys Table
|
||||||
|
db.exec(`
|
||||||
|
CREATE TABLE toys (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
category TEXT NOT NULL,
|
||||||
|
images TEXT,
|
||||||
|
unavailableRanges TEXT,
|
||||||
|
ownerId INTEGER NOT NULL,
|
||||||
|
pricePerDay REAL,
|
||||||
|
location TEXT,
|
||||||
|
FOREIGN KEY (ownerId) REFERENCES users(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
`);
|
||||||
|
|
||||||
// Create Toys Table if it doesn't exist
|
console.log("Seeding initial data...");
|
||||||
db.exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS toys (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
description TEXT NOT NULL,
|
|
||||||
category TEXT NOT NULL,
|
|
||||||
images TEXT,
|
|
||||||
unavailableRanges TEXT,
|
|
||||||
ownerId TEXT NOT NULL,
|
|
||||||
pricePerDay REAL,
|
|
||||||
location TEXT,
|
|
||||||
FOREIGN KEY (ownerId) REFERENCES users(id) ON DELETE CASCADE
|
|
||||||
);
|
|
||||||
`);
|
|
||||||
|
|
||||||
console.log("Seeding initial data if missing...");
|
// Prepare insert statements
|
||||||
|
const insertUser = db.prepare(`
|
||||||
|
INSERT INTO users (id, name, nickname, email, role, avatarUrl, bio)
|
||||||
|
VALUES (@id, @name, @nickname, @email, @role, @avatarUrl, @bio)
|
||||||
|
`);
|
||||||
|
|
||||||
// Use INSERT OR IGNORE to only add data if the primary key doesn't exist.
|
const insertToy = db.prepare(`
|
||||||
// This prevents errors on subsequent runs and adds missing users/toys without overwriting.
|
INSERT INTO toys (id, name, description, category, images, unavailableRanges, ownerId, pricePerDay, location)
|
||||||
const insertUser = db.prepare(`
|
VALUES (@id, @name, @description, @category, @images, @unavailableRanges, @ownerId, @pricePerDay, @location)
|
||||||
INSERT OR IGNORE INTO users (id, name, nickname, email, role, avatarUrl, bio)
|
`);
|
||||||
VALUES (@id, @name, @nickname, @email, @role, @avatarUrl, @bio)
|
|
||||||
`);
|
|
||||||
|
|
||||||
const insertToy = db.prepare(`
|
// Use a transaction for efficiency
|
||||||
INSERT OR IGNORE INTO toys (id, name, description, category, images, unavailableRanges, ownerId, pricePerDay, location)
|
const seedData = db.transaction(() => {
|
||||||
VALUES (@id, @name, @description, @category, @images, @unavailableRanges, @ownerId, @pricePerDay, @location)
|
for (const user of rawUsers) {
|
||||||
`);
|
|
||||||
|
|
||||||
const insertManyUsers = db.transaction((users: User[]) => {
|
|
||||||
for (const user of users) {
|
|
||||||
insertUser.run({
|
insertUser.run({
|
||||||
id: user.id,
|
id: user.id,
|
||||||
name: user.name,
|
name: user.name,
|
||||||
|
|
@ -83,23 +75,22 @@ function initDb() {
|
||||||
avatarUrl: user.avatarUrl ?? null,
|
avatarUrl: user.avatarUrl ?? null,
|
||||||
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),
|
||||||
unavailableRanges: JSON.stringify(toy.unavailableRanges),
|
unavailableRanges: JSON.stringify(toy.unavailableRanges),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -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',
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
BIN
toyshare.db
BIN
toyshare.db
Binary file not shown.
BIN
toyshare.db-shm
BIN
toyshare.db-shm
Binary file not shown.
BIN
toyshare.db-wal
BIN
toyshare.db-wal
Binary file not shown.
Loading…
Reference in New Issue