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) {
|
||||
const t = await getI18n();
|
||||
const userData = getUserById(params.id);
|
||||
const userId = Number(params.id);
|
||||
const userData = getUserById(userId);
|
||||
|
||||
if (!userData) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export default function AdminUserManagementPage() {
|
|||
}, [t, toast]);
|
||||
|
||||
|
||||
const handleDeleteUser = async (userId: string) => {
|
||||
const handleDeleteUser = async (userId: number) => {
|
||||
setIsDeleting(true);
|
||||
const result = await deleteUser(userId);
|
||||
if (result.success) {
|
||||
|
|
|
|||
|
|
@ -17,20 +17,20 @@ import { ArrowLeft, Send, Loader2, AlertTriangle, ToyBrick } from 'lucide-react'
|
|||
import { useToast } from '@/hooks/use-toast';
|
||||
import { format } from 'date-fns';
|
||||
|
||||
// Assume current user is 'user1' for mock purposes
|
||||
const currentUserId = 'user1';
|
||||
const currentUserProfiles: Record<string, { name: string; avatarInitial: string; avatarUrl?: string }> = {
|
||||
'user1': { 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' },
|
||||
'user3': { 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' },
|
||||
'user5': { 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' },
|
||||
// Assume current user is 'user1' (ID: 1) for mock purposes
|
||||
const currentUserId = 1;
|
||||
const currentUserProfiles: Record<number, { name: string; avatarInitial: string; avatarUrl?: string }> = {
|
||||
1: { name: 'Alice Wonderland', avatarInitial: 'AW', avatarUrl: 'https://placehold.co/40x40.png?text=AW' }, // Logged in user
|
||||
2: { name: 'Bob The Builder', avatarInitial: 'BT', avatarUrl: 'https://placehold.co/40x40.png?text=BT' },
|
||||
3: { name: 'Carol Danvers', avatarInitial: 'CD', avatarUrl: 'https://placehold.co/40x40.png?text=CD' },
|
||||
4: { name: 'Charlie Brown', avatarInitial: 'CB', avatarUrl: 'https://placehold.co/40x40.png?text=CB' },
|
||||
5: { name: 'Diana Prince', avatarInitial: 'DP', avatarUrl: 'https://placehold.co/40x40.png?text=DP' },
|
||||
6: { name: 'Edward Nigma', avatarInitial: 'EN', avatarUrl: 'https://placehold.co/40x40.png?text=EN' },
|
||||
};
|
||||
|
||||
|
||||
// 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) {
|
||||
return { id: request.requesterId, name: request.requesterName };
|
||||
}
|
||||
|
|
@ -238,5 +238,3 @@ export default function MessageDetailPage({ params }: { params: { id: string } }
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -9,10 +9,10 @@ import Link from "next/link";
|
|||
import { MessageSquareQuote, ToyBrick } from "lucide-react";
|
||||
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
|
||||
const getOtherParticipant = (request: RentalRequest, currentUserId: string) => {
|
||||
const getOtherParticipant = (request: RentalRequest, currentUserId: number) => {
|
||||
if (request.toy.ownerId === currentUserId) {
|
||||
return { id: request.requesterId, name: request.requesterName };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,30 +2,19 @@
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { getToysByOwner } from "@/data/operations";
|
||||
import { mockRentalHistory } from "@/lib/mockData";
|
||||
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 type { Toy } from "@/types";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { getI18n } from "@/locales/server";
|
||||
|
||||
const currentUserId = 'user1';
|
||||
const currentUserId = 1;
|
||||
|
||||
export default async function MyToysPage() {
|
||||
const t = await getI18n();
|
||||
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 (
|
||||
<div className="space-y-8">
|
||||
<div className="flex justify-between items-center">
|
||||
|
|
@ -41,7 +30,7 @@ export default async function MyToysPage() {
|
|||
</Link>
|
||||
</div>
|
||||
|
||||
{userToysWithRentalCount.length === 0 ? (
|
||||
{userToys.length === 0 ? (
|
||||
<Card className="text-center py-12 shadow-md">
|
||||
<CardHeader>
|
||||
<ToyBrickIcon className="h-16 w-16 mx-auto text-muted-foreground mb-4" />
|
||||
|
|
@ -59,8 +48,8 @@ export default async function MyToysPage() {
|
|||
</Card>
|
||||
) : (
|
||||
<div className="space-y-6">
|
||||
{userToysWithRentalCount.map(toy => (
|
||||
<ListedToyItem key={toy.id} toy={toy} t={t} rentalCount={toy.rentalCount} />
|
||||
{userToys.map(toy => (
|
||||
<ListedToyItem key={toy.id} toy={toy} t={t} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -71,10 +60,9 @@ export default async function MyToysPage() {
|
|||
interface ListedToyItemProps {
|
||||
toy: Toy & {dataAiHint?: 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";
|
||||
return (
|
||||
<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>
|
||||
{toy.pricePerDay !== undefined ? (toy.pricePerDay > 0 ? `$${toy.pricePerDay}${t('toy_details.price_per_day')}` : t('toy_details.price_free')) : 'Not set'}
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -3,15 +3,20 @@ import { Button } from "@/components/ui/button";
|
|||
import Link from "next/link";
|
||||
import { ToyBrick, PlusCircle, ListOrdered, User, ShoppingBag } from "lucide-react";
|
||||
import { getI18n } from "@/locales/server";
|
||||
import { getToysByOwner } from "@/data/operations";
|
||||
|
||||
const userStats = {
|
||||
listedToys: 3,
|
||||
activeRentals: 1,
|
||||
pendingRequests: 2,
|
||||
};
|
||||
const currentUserId = 1; // Mock logged-in user ID
|
||||
|
||||
export default async function DashboardOverviewPage() {
|
||||
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 (
|
||||
<div className="space-y-8">
|
||||
<Card className="shadow-lg">
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ import type { Locale } from "@/locales/server";
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
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 } }) {
|
||||
const t = await getI18n();
|
||||
|
|
@ -112,4 +112,3 @@ function RentalHistoryItemCard({ item, t, locale }: RentalHistoryItemCardProps)
|
|||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,55 +4,53 @@ import { ListOrdered, Check, X, MessageSquareText } from "lucide-react";
|
|||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Image from "next/image";
|
||||
import type { Toy } from "@/types";
|
||||
import { mockToys } from "@/lib/mockData";
|
||||
import type { Toy, RentalRequest } from "@/types";
|
||||
import { getAllToys } from "@/data/operations";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { getI18n } from "@/locales/server";
|
||||
|
||||
interface RentalRequest {
|
||||
id: string;
|
||||
toy: Toy;
|
||||
requesterName: string;
|
||||
requesterId: string;
|
||||
requestedDates: string; // e.g., "Aug 5, 2024 - Aug 10, 2024"
|
||||
status: 'pending' | 'approved' | 'declined';
|
||||
message?: string;
|
||||
dataAiHint?: string;
|
||||
// This is still mock data. In a real app, this would come from the database.
|
||||
function getMockRequests(): RentalRequest[] {
|
||||
const allToys = getAllToys();
|
||||
const myToys = allToys.filter(t => t.ownerId === 1);
|
||||
|
||||
if (myToys.length === 0) return [];
|
||||
|
||||
const rentalRequests: RentalRequest[] = [
|
||||
{
|
||||
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[] = [
|
||||
{
|
||||
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');
|
||||
const currentUserToyRequests = getMockRequests();
|
||||
|
||||
|
||||
export default async function RentalRequestsPage() {
|
||||
|
|
@ -167,4 +165,3 @@ function RequestItemCard({ request, t }: RequestItemCardProps) {
|
|||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,15 +3,12 @@ import Image from 'next/image';
|
|||
import { getToyById } from '@/data/operations';
|
||||
import type { Toy } from '@/types';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Calendar } from '@/components/ui/calendar';
|
||||
import { ArrowLeft, DollarSign, MapPin } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { getI18n, getStaticParams as getLocaleStaticParams } 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 { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { ShoppingBag } from 'lucide-react';
|
||||
|
|
@ -41,12 +38,6 @@ export default async function ToyPage({ params }: ToyPageProps) {
|
|||
|
||||
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 (
|
||||
<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">
|
||||
|
|
@ -131,23 +122,6 @@ export default async function ToyPage({ params }: ToyPageProps) {
|
|||
</div>
|
||||
|
||||
<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">
|
||||
<ShoppingBag className="mr-2 h-5 w-5" /> {t('toy_details.request_to_rent')}
|
||||
|
|
|
|||
|
|
@ -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 type { User } from '@/types';
|
||||
import { randomUUID } from 'crypto';
|
||||
import { revalidatePath } from 'next/cache';
|
||||
import { getAllUsers as dbGetAllUsers, getUserById } from '@/data/operations';
|
||||
|
||||
|
|
@ -25,22 +24,22 @@ export async function registerUser(data: { name: string; nickname?: string; emai
|
|||
if (existingUser) {
|
||||
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(
|
||||
'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');
|
||||
|
||||
|
|
@ -96,7 +95,7 @@ interface DeleteUserResult {
|
|||
message: string;
|
||||
}
|
||||
|
||||
export async function deleteUser(id: string): Promise<DeleteUserResult> {
|
||||
export async function deleteUser(id: number): Promise<DeleteUserResult> {
|
||||
try {
|
||||
const user = getUserById(id);
|
||||
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 {
|
||||
id: string;
|
||||
id: number;
|
||||
name: string;
|
||||
nickname?: string;
|
||||
avatarUrl?: string;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
|
||||
import AddToyForm from '@/components/toys/AddToyForm';
|
||||
import { mockToys } from '@/lib/mockData';
|
||||
import { getToyById } from '@/data/operations';
|
||||
import type { Toy } from '@/types';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import Link from 'next/link';
|
||||
|
|
@ -9,14 +10,8 @@ interface EditToyPageProps {
|
|||
params: { id: string };
|
||||
}
|
||||
|
||||
// Server Component to fetch toy data (mocked for now)
|
||||
async function getToyForEdit(id: string): Promise<Partial<Toy> | undefined> {
|
||||
await new Promise(resolve => setTimeout(resolve, 100)); // Simulate fetch
|
||||
return mockToys.find(toy => toy.id === id);
|
||||
}
|
||||
|
||||
export default async function EditToyPage({ params }: EditToyPageProps) {
|
||||
const toyData = await getToyForEdit(params.id);
|
||||
const toyData = getToyById(params.id);
|
||||
|
||||
if (!toyData) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,20 +1,18 @@
|
|||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import ToyCard from "@/components/toys/ToyCard";
|
||||
import { mockToys } from "@/lib/mockData"; // Using all toys for now, filter by ownerId in real app
|
||||
import { getToysByOwner } from "@/data/operations";
|
||||
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 type { Toy } from "@/types";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
// Assume this is the logged-in user's ID
|
||||
const currentUserId = 'user1';
|
||||
const currentUserId = 1;
|
||||
|
||||
// Filter toys by current user
|
||||
const userToys = mockToys.filter(toy => toy.ownerId === currentUserId);
|
||||
export default async function MyToysPage() {
|
||||
const userToys = getToysByOwner(currentUserId);
|
||||
|
||||
export default function MyToysPage() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="flex justify-between items-center">
|
||||
|
|
@ -33,7 +31,7 @@ export default function MyToysPage() {
|
|||
{userToys.length === 0 ? (
|
||||
<Card className="text-center py-12 shadow-md">
|
||||
<CardHeader>
|
||||
<ToyBrick className="h-16 w-16 mx-auto text-muted-foreground mb-4" />
|
||||
<ToyBrickIcon className="h-16 w-16 mx-auto text-muted-foreground mb-4" />
|
||||
<CardTitle>No Toys Listed Yet</CardTitle>
|
||||
<CardDescription>Share your first toy and spread the joy!</CardDescription>
|
||||
</CardHeader>
|
||||
|
|
@ -99,14 +97,14 @@ function ListedToyItem({ toy }: ListedToyItemProps) {
|
|||
</div>
|
||||
</div>
|
||||
<p className="text-muted-foreground mt-2 text-sm line-clamp-2">{toy.description}</p>
|
||||
<div className="mt-4 text-sm">
|
||||
<span className="font-semibold">Price: </span>
|
||||
{toy.pricePerDay !== undefined ? (toy.pricePerDay > 0 ? `$${toy.pricePerDay}/day` : 'Free') : 'Not set'}
|
||||
<div className="mt-4 text-sm space-y-1">
|
||||
<div>
|
||||
<span className="font-semibold">Price: </span>
|
||||
{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>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,21 @@
|
|||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Link from "next/link";
|
||||
import { ToyBrick, PlusCircle, ListOrdered, User, ShoppingBag } from "lucide-react";
|
||||
import { getToysByOwner } from "@/data/operations";
|
||||
|
||||
// Mock data for dashboard overview
|
||||
const userStats = {
|
||||
listedToys: 3,
|
||||
activeRentals: 1, // Toys I'm renting
|
||||
pendingRequests: 2, // Requests for my toys
|
||||
};
|
||||
const currentUserId = 1; // Mock logged-in user ID
|
||||
|
||||
export default async function DashboardOverviewPage() {
|
||||
const userToys = getToysByOwner(currentUserId);
|
||||
|
||||
const userStats = {
|
||||
listedToys: userToys.length, // Real data from DB
|
||||
activeRentals: 1, // Mock data
|
||||
pendingRequests: 2, // Mock data
|
||||
};
|
||||
|
||||
export default function DashboardOverviewPage() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<Card className="shadow-lg">
|
||||
|
|
|
|||
|
|
@ -1,20 +1,24 @@
|
|||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { ShoppingBag, ToyBrick } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Image from "next/image";
|
||||
import type { Toy } from "@/types";
|
||||
import { mockToys } from "@/lib/mockData"; // Using all toys for now
|
||||
|
||||
// Mock data: toys rented by the current user
|
||||
// In a real app, this would come from a database query based on rental records
|
||||
const rentedToys: (Toy & { rentalEndDate?: string, dataAiHint?: string })[] = [
|
||||
{ ...mockToys[1], rentalEndDate: "2024-08-15", dataAiHint: mockToys[1]?.category.toLowerCase() }, // Remote Control Car
|
||||
{ ...mockToys[4], rentalEndDate: "2024-09-01", dataAiHint: mockToys[4]?.category.toLowerCase() }, // Beginner Guitar
|
||||
];
|
||||
|
||||
import { getAllToys } from "@/data/operations";
|
||||
|
||||
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 (
|
||||
<div className="space-y-8">
|
||||
<div>
|
||||
|
|
@ -50,7 +54,7 @@ export default function MyRentalsPage() {
|
|||
}
|
||||
|
||||
interface RentalItemCardProps {
|
||||
toy: Toy & { rentalEndDate?: string, dataAiHint?: string };
|
||||
toy: Toy & { rentalEndDate?: string };
|
||||
}
|
||||
|
||||
function RentalItemCard({ toy }: RentalItemCardProps) {
|
||||
|
|
|
|||
|
|
@ -1,58 +1,55 @@
|
|||
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { ListOrdered, Check, X } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import Image from "next/image";
|
||||
import type { Toy } from "@/types";
|
||||
import { mockToys } from "@/lib/mockData";
|
||||
import type { Toy, RentalRequest } from "@/types";
|
||||
import { getAllToys } from "@/data/operations";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
|
||||
interface RentalRequest {
|
||||
id: string;
|
||||
toy: Toy;
|
||||
requesterName: string;
|
||||
requesterId: string;
|
||||
requestedDates: string; // e.g., "Aug 5, 2024 - Aug 10, 2024"
|
||||
status: 'pending' | 'approved' | 'declined';
|
||||
message?: string;
|
||||
dataAiHint?: string;
|
||||
// This is still mock data. In a real app, this would come from the database.
|
||||
function getMockRequests(): RentalRequest[] {
|
||||
const allToys = getAllToys();
|
||||
const myToys = allToys.filter(t => t.ownerId === 1);
|
||||
|
||||
if (myToys.length === 0) return [];
|
||||
|
||||
const rentalRequests: RentalRequest[] = [
|
||||
{
|
||||
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 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');
|
||||
const currentUserToyRequests = getMockRequests();
|
||||
|
||||
|
||||
export default function RentalRequestsPage() {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,28 @@
|
|||
|
||||
import ToyList from '@/components/toys/ToyList';
|
||||
import { mockToys } from '@/lib/mockData';
|
||||
import { getAllToys } from '@/data/operations';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import Link from 'next/link';
|
||||
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 (
|
||||
<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">
|
||||
|
|
@ -33,7 +51,7 @@ export default function HomePage() {
|
|||
<h2 className="text-3xl font-bold font-headline text-center mb-8 text-primary">
|
||||
Available Toys
|
||||
</h2>
|
||||
<ToyList toys={mockToys.map(toy => ({...toy, dataAiHint: toy.category.toLowerCase()}))} />
|
||||
<ToyList toys={toys} t={t} />
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,26 +1,21 @@
|
|||
|
||||
import Image from 'next/image';
|
||||
import { mockToys } from '@/lib/mockData';
|
||||
import { getToyById, getAllToys } from '@/data/operations';
|
||||
import type { Toy } from '@/types';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import AvailabilityCalendar from '@/components/toys/AvailabilityCalendar';
|
||||
import { ArrowLeft, CalendarDays, DollarSign, MapPin, ShoppingBag, UserCircle2 } from 'lucide-react';
|
||||
import { ArrowLeft, DollarSign, MapPin, ShoppingBag, UserCircle2 } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
|
||||
|
||||
interface ToyPageProps {
|
||||
params: { id: string };
|
||||
}
|
||||
|
||||
// Server Component to fetch toy data (mocked for now)
|
||||
async function getToyById(id: string): Promise<Toy | undefined> {
|
||||
// In a real app, this would fetch from a database
|
||||
await new Promise(resolve => setTimeout(resolve, 200)); // Simulate network delay
|
||||
return mockToys.find(toy => toy.id === id);
|
||||
}
|
||||
|
||||
export default async function ToyPage({ params }: ToyPageProps) {
|
||||
const toy = await getToyById(params.id);
|
||||
const toy = getToyById(params.id);
|
||||
|
||||
if (!toy) {
|
||||
return (
|
||||
|
|
@ -47,7 +42,6 @@ export default async function ToyPage({ params }: ToyPageProps) {
|
|||
</Link>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-8 lg:gap-12 items-start">
|
||||
{/* Image Gallery Section */}
|
||||
<div className="space-y-4">
|
||||
<div className="aspect-video relative w-full rounded-lg overflow-hidden shadow-lg">
|
||||
<Image
|
||||
|
|
@ -76,7 +70,6 @@ export default async function ToyPage({ params }: ToyPageProps) {
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Toy Details Section */}
|
||||
<div className="space-y-6">
|
||||
<Badge variant="secondary" className="text-sm">{toy.category}</Badge>
|
||||
<h1 className="text-4xl font-bold font-headline text-primary">{toy.name}</h1>
|
||||
|
|
@ -88,12 +81,19 @@ export default async function ToyPage({ params }: ToyPageProps) {
|
|||
<Separator />
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4 text-sm">
|
||||
<div className="flex items-center">
|
||||
<UserCircle2 className="h-5 w-5 mr-2 text-accent" />
|
||||
<div>
|
||||
<span className="font-medium text-muted-foreground">Owner: </span>
|
||||
<span className="text-foreground">{toy.ownerName}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<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>
|
||||
<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>
|
||||
{toy.location && (
|
||||
<div className="flex items-center">
|
||||
|
|
@ -109,15 +109,15 @@ export default async function ToyPage({ params }: ToyPageProps) {
|
|||
<DollarSign className="h-5 w-5 mr-2 text-accent" />
|
||||
<div>
|
||||
<span className="font-medium text-muted-foreground">Price: </span>
|
||||
<span className="text-foreground font-semibold">{toy.pricePerDay > 0 ? `$${toy.pricePerDay}/day` : 'Free'}</span>
|
||||
<span className="text-foreground font-semibold">
|
||||
{toy.pricePerDay > 0 ? `$${toy.pricePerDay}/day` : 'Free'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Separator />
|
||||
|
||||
<AvailabilityCalendar availability={toy.availability} />
|
||||
|
||||
<Button size="lg" className="w-full mt-6 transition-transform transform hover:scale-105">
|
||||
<ShoppingBag className="mr-2 h-5 w-5" /> Request to Rent
|
||||
|
|
@ -130,7 +130,8 @@ export default async function ToyPage({ params }: ToyPageProps) {
|
|||
|
||||
// Generate static paths for all toys
|
||||
export async function generateStaticParams() {
|
||||
return mockToys.map((toy) => ({
|
||||
id: toy.id,
|
||||
}));
|
||||
const toys = getAllToys();
|
||||
return toys.map((toy) => ({
|
||||
id: toy.id,
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { Button } from '@/components/ui/button';
|
||||
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 { useToast } from '@/hooks/use-toast';
|
||||
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 { createOrUpdateToy, type ToyFormData } from '@/app/actions/toy';
|
||||
import { getUserByEmail } from '@/app/actions/user';
|
||||
|
||||
|
||||
const toyCategoryDefinitions = [
|
||||
{ key: 'educational', value: 'Educational' },
|
||||
|
|
@ -30,7 +33,7 @@ const toyCategoryDefinitions = [
|
|||
];
|
||||
|
||||
interface AddToyFormProps {
|
||||
initialData?: Partial<Toy>;
|
||||
initialData?: Toy;
|
||||
isEditMode?: boolean;
|
||||
}
|
||||
|
||||
|
|
@ -45,10 +48,22 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
|
|||
const [category, setCategory] = useState(initialData?.category || '');
|
||||
const [pricePerDay, setPricePerDay] = useState(initialData?.pricePerDay?.toString() || '0');
|
||||
const [location, setLocation] = useState(initialData?.location || '');
|
||||
const [images, setImages] = useState<string[]>(initialData?.images || ['']);
|
||||
// unavailableRanges will be initialized as empty or from initialData, but not editable in this form version.
|
||||
const [images, setImages] = useState<string[]>(initialData?.images?.length ? initialData.images : ['']);
|
||||
const [unavailableRanges, setUnavailableRanges] = useState<Toy['unavailableRanges']>(initialData?.unavailableRanges || []);
|
||||
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 newImages = [...images];
|
||||
|
|
@ -74,28 +89,38 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
|
|||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!currentUser) {
|
||||
toast({ title: "Authentication Error", description: "Could not identify current user. Please log in again.", variant: "destructive" });
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const toyData: Partial<Toy> = {
|
||||
name, description, category,
|
||||
const toyFormData: ToyFormData & { ownerId: number } = {
|
||||
name,
|
||||
description,
|
||||
category,
|
||||
pricePerDay: parseFloat(pricePerDay) || 0,
|
||||
location,
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
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`);
|
||||
|
||||
const result = await createOrUpdateToy(toyFormData, isEditMode && initialData?.id ? initialData.id : null);
|
||||
|
||||
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 (
|
||||
|
|
@ -174,7 +199,7 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
|
|||
</div>
|
||||
|
||||
<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')) : (
|
||||
<>
|
||||
<Save className="mr-2 h-5 w-5" />
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
|
||||
import db from '@/lib/db';
|
||||
import type { Toy, User } from '@/types';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
// Helper to parse toy data from DB, converting JSON strings back to objects
|
||||
const parseToy = (toyData: any): Toy => {
|
||||
|
|
@ -38,7 +39,7 @@ export function getToyById(id: string): Toy | undefined {
|
|||
return toy ? parseToy(toy) : undefined;
|
||||
}
|
||||
|
||||
export function getToysByOwner(ownerId: string): Toy[] {
|
||||
export function getToysByOwner(ownerId: number): Toy[] {
|
||||
const stmt = db.prepare(`
|
||||
SELECT t.*, u.name as ownerName, u.avatarUrl as ownerAvatarUrl
|
||||
FROM toys t
|
||||
|
|
@ -49,8 +50,50 @@ export function getToysByOwner(ownerId: string): Toy[] {
|
|||
return toys.map(parseToy);
|
||||
}
|
||||
|
||||
// This now fetches the full user profile from the database
|
||||
export function getOwnerProfile(ownerId: string): User | undefined {
|
||||
export function createToy(toyData: Omit<Toy, 'id' | 'ownerName' | 'ownerAvatarUrl' | 'dataAiHint'>): Toy {
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
@ -61,7 +104,7 @@ export function getAllUsers(): 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 = ?');
|
||||
return stmt.get(id) as User | undefined;
|
||||
}
|
||||
|
|
|
|||
119
src/lib/db.ts
119
src/lib/db.ts
|
|
@ -15,65 +15,57 @@ db.pragma('journal_mode = WAL');
|
|||
db.pragma('foreign_keys = ON');
|
||||
|
||||
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
|
||||
db.exec(`
|
||||
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
|
||||
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);
|
||||
}
|
||||
if (!table) {
|
||||
console.log("Database not found. Initializing and seeding...");
|
||||
|
||||
// Create Users Table
|
||||
db.exec(`
|
||||
CREATE TABLE users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL,
|
||||
nickname TEXT,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
role TEXT,
|
||||
avatarUrl TEXT,
|
||||
bio TEXT
|
||||
);
|
||||
`);
|
||||
|
||||
// Create Toys Table if it doesn't exist
|
||||
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...");
|
||||
// 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
|
||||
);
|
||||
`);
|
||||
|
||||
console.log("Seeding initial data...");
|
||||
|
||||
// Use INSERT OR IGNORE to only add data if the primary key doesn't exist.
|
||||
// This prevents errors on subsequent runs and adds missing users/toys without overwriting.
|
||||
const insertUser = db.prepare(`
|
||||
INSERT OR IGNORE INTO users (id, name, nickname, email, role, avatarUrl, bio)
|
||||
VALUES (@id, @name, @nickname, @email, @role, @avatarUrl, @bio)
|
||||
`);
|
||||
|
||||
const insertToy = db.prepare(`
|
||||
INSERT OR IGNORE INTO toys (id, name, description, category, images, unavailableRanges, ownerId, pricePerDay, location)
|
||||
VALUES (@id, @name, @description, @category, @images, @unavailableRanges, @ownerId, @pricePerDay, @location)
|
||||
`);
|
||||
// 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)
|
||||
`);
|
||||
|
||||
const insertToy = db.prepare(`
|
||||
INSERT INTO toys (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[]) => {
|
||||
for (const user of users) {
|
||||
// Use a transaction for efficiency
|
||||
const seedData = db.transaction(() => {
|
||||
for (const user of rawUsers) {
|
||||
insertUser.run({
|
||||
id: user.id,
|
||||
name: user.name,
|
||||
|
|
@ -83,23 +75,22 @@ function initDb() {
|
|||
avatarUrl: user.avatarUrl ?? null,
|
||||
bio: user.bio ?? null
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const insertManyToys = db.transaction((toys) => {
|
||||
for (const toy of toys) {
|
||||
}
|
||||
for (const toy of rawToys) {
|
||||
insertToy.run({
|
||||
...toy,
|
||||
images: JSON.stringify(toy.images),
|
||||
unavailableRanges: JSON.stringify(toy.unavailableRanges),
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
insertManyUsers(rawUsers);
|
||||
insertManyToys(rawToys);
|
||||
seedData();
|
||||
|
||||
console.log("Database initialization and seeding complete.");
|
||||
console.log("Database initialization and seeding complete.");
|
||||
} else {
|
||||
console.log("Database already initialized.");
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize and export db
|
||||
|
|
|
|||
|
|
@ -5,13 +5,13 @@ import { addDays, formatISO, subDays } from 'date-fns';
|
|||
const today = new Date();
|
||||
|
||||
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: '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: '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: 'user4', 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: 'user6', 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: 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: 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: 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: 4, name: 'Charlie Brown', nickname: 'Chuck', email: 'user4@example.com', role: 'User', avatarUrl: '', bio: '' },
|
||||
{ id: 5, name: 'Diana Prince', nickname: 'Wonder Woman', email: 'user5@example.com', role: 'User', avatarUrl: '', bio: '' },
|
||||
{ id: 6, name: 'Edward Nigma', nickname: 'Riddler', email: 'user6@example.com', role: 'User', avatarUrl: '', bio: '' },
|
||||
{ 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'>[] = [
|
||||
|
|
@ -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, 15), { representation: 'date' }), endDate: formatISO(addDays(today, 16), { representation: 'date' }) },
|
||||
],
|
||||
ownerId: 'user1',
|
||||
ownerId: 1,
|
||||
pricePerDay: 5,
|
||||
location: 'Springfield Gardens',
|
||||
},
|
||||
|
|
@ -38,7 +38,7 @@ export const rawToys: Omit<Toy, 'ownerName' | 'ownerAvatarUrl' | 'dataAiHint'>[]
|
|||
unavailableRanges: [
|
||||
{ startDate: formatISO(addDays(today, 10), { representation: 'date' }), endDate: formatISO(addDays(today, 12), { representation: 'date' }) },
|
||||
],
|
||||
ownerId: 'user2',
|
||||
ownerId: 2,
|
||||
pricePerDay: 8,
|
||||
location: 'Willow Creek',
|
||||
},
|
||||
|
|
@ -49,7 +49,7 @@ export const rawToys: Omit<Toy, 'ownerName' | 'ownerAvatarUrl' | 'dataAiHint'>[]
|
|||
category: 'Electronics',
|
||||
images: ['https://placehold.co/600x400.png?text=Kids+Tablet', 'https://placehold.co/600x400.png?text=Tablet+Screen'],
|
||||
unavailableRanges: [],
|
||||
ownerId: 'user3',
|
||||
ownerId: 3,
|
||||
pricePerDay: 7,
|
||||
location: 'Metro City',
|
||||
},
|
||||
|
|
@ -62,7 +62,7 @@ export const rawToys: Omit<Toy, 'ownerName' | 'ownerAvatarUrl' | 'dataAiHint'>[]
|
|||
unavailableRanges: [
|
||||
{ startDate: formatISO(addDays(today, 20), { representation: 'date' }), endDate: formatISO(addDays(today, 25), { representation: 'date' }) },
|
||||
],
|
||||
ownerId: 'user1',
|
||||
ownerId: 1,
|
||||
pricePerDay: 3,
|
||||
location: 'Springfield Gardens',
|
||||
},
|
||||
|
|
@ -73,7 +73,7 @@ export const rawToys: Omit<Toy, 'ownerName' | 'ownerAvatarUrl' | 'dataAiHint'>[]
|
|||
category: 'Musical',
|
||||
images: ['https://placehold.co/600x400.png?text=Kids+Guitar'],
|
||||
unavailableRanges: [],
|
||||
ownerId: 'user2',
|
||||
ownerId: 2,
|
||||
pricePerDay: 10,
|
||||
location: 'Willow Creek',
|
||||
},
|
||||
|
|
@ -84,7 +84,7 @@ export const rawToys: Omit<Toy, 'ownerName' | 'ownerAvatarUrl' | 'dataAiHint'>[]
|
|||
category: 'Outdoor',
|
||||
images: ['https://placehold.co/600x400.png?text=Sports+Kit'],
|
||||
unavailableRanges: [],
|
||||
ownerId: 'user3',
|
||||
ownerId: 3,
|
||||
pricePerDay: 6,
|
||||
location: 'Metro City',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
|
||||
export interface MessageEntry {
|
||||
id: string;
|
||||
senderId: string;
|
||||
senderId: number;
|
||||
senderName: string;
|
||||
text: string;
|
||||
timestamp: string; // ISO date string
|
||||
|
|
@ -15,7 +15,7 @@ export interface Toy {
|
|||
images: string[];
|
||||
unavailableRanges: { startDate: string; endDate: string }[];
|
||||
ownerName: string;
|
||||
ownerId: string;
|
||||
ownerId: number;
|
||||
ownerAvatarUrl?: string;
|
||||
pricePerDay?: number;
|
||||
location?: string;
|
||||
|
|
@ -23,7 +23,7 @@ export interface Toy {
|
|||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
id: number;
|
||||
name: string; // This is now Full Name
|
||||
nickname?: string;
|
||||
email: string;
|
||||
|
|
@ -40,7 +40,7 @@ export interface DailyAvailability {
|
|||
|
||||
export interface RentalHistoryEntry {
|
||||
id:string;
|
||||
userId: string;
|
||||
userId: number;
|
||||
toy: Toy;
|
||||
rentalStartDate: string;
|
||||
rentalEndDate: string;
|
||||
|
|
@ -53,7 +53,7 @@ export interface RentalRequest {
|
|||
id: string;
|
||||
toy: Toy;
|
||||
requesterName: string;
|
||||
requesterId: string;
|
||||
requesterId: number;
|
||||
requestedDates: string;
|
||||
status: 'pending' | 'approved' | 'declined';
|
||||
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