Implement my toys edit toys and rental toys function to sqlite database

This commit is contained in:
Indigo Tang 2025-07-06 13:25:55 +00:00
parent 1e994e8a4c
commit 0f224287fd
3 changed files with 166 additions and 23 deletions

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

@ -0,0 +1,75 @@
'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.
export type ToyFormData = Omit<Toy, 'id' | 'name' | 'description' | 'category' | 'images' | 'unavailableRanges' | 'pricePerDay' | 'location' | 'ownerName' | 'ownerAvatarUrl' | 'dataAiHint'> & {
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: string },
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);
}
revalidatePath('/dashboard/my-toys');
revalidatePath(`/toys/${savedToy.id}`);
revalidatePath('/');
revalidatePath(`/[locale]/toys/${savedToy.id}`, 'page');
revalidatePath(`/[locale]/dashboard/my-toys`, 'page');
return {
success: true,
message: toyId ? 'Toy updated successfully!' : 'Toy created successfully!',
toy: savedToy,
};
} catch (error) {
console.error('Error in createOrUpdateToy:', error);
return { success: false, message: 'An unexpected error occurred.' };
}
}

View File

@ -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];
@ -74,28 +89,38 @@ export default function AddToyForm({ initialData, isEditMode = false }: AddToyFo
setIsLoading(false); setIsLoading(false);
return; 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> = { const toyFormData: ToyFormData & { ownerId: string } = {
name, description, category, 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" />

View File

@ -1,6 +1,7 @@
import db from '@/lib/db'; import db from '@/lib/db';
import type { Toy, User } from '@/types'; import type { Toy, User } from '@/types';
import { randomUUID } from 'crypto';
// Helper to parse toy data from DB, converting JSON strings back to objects // Helper to parse toy data from DB, converting JSON strings back to objects
const parseToy = (toyData: any): Toy => { const parseToy = (toyData: any): Toy => {
@ -49,7 +50,49 @@ 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 {
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: string): User | undefined { export function getOwnerProfile(ownerId: string): User | undefined {
return getUserById(ownerId); return getUserById(ownerId);
} }