Add gift modal

This commit is contained in:
2025-03-16 17:56:14 +01:00
parent e2c6c64fe7
commit e10e43fb8b
2 changed files with 525 additions and 2 deletions

View File

@@ -47,6 +47,7 @@ import {
} from "@/components/ui/select";
import { GiftStatus, GiftPriority } from "@/client/types.gen";
import { CategoryModal } from "@/components/gifts/category-modal";
import { GiftModal } from "@/components/gifts/gift-modal";
export default function GiftRegistryPage() {
const { slug } = useParams<{ slug: string }>();
@@ -588,6 +589,15 @@ export default function GiftRegistryPage() {
/>
)}
{/* Category Modals */}
{isAddCategoryModalOpen && (
<CategoryModal
isOpen={isAddCategoryModalOpen}
onClose={() => setIsAddCategoryModalOpen(false)}
eventId={event.id}
/>
)}
{isEditCategoryModalOpen && selectedCategoryId && (
<CategoryModal
isOpen={isEditCategoryModalOpen}
@@ -600,8 +610,26 @@ export default function GiftRegistryPage() {
/>
)}
{/* Modal placeholders - will be implemented later */}
{/* These would render the appropriate modals when isAddGiftModalOpen, etc. are true */}
{/* Gift Modals */}
{isAddGiftModalOpen && (
<GiftModal
isOpen={isAddGiftModalOpen}
onClose={() => setIsAddGiftModalOpen(false)}
eventId={event.id}
/>
)}
{isEditGiftModalOpen && selectedGiftId && (
<GiftModal
isOpen={isEditGiftModalOpen}
onClose={() => {
setIsEditGiftModalOpen(false);
setSelectedGiftId(null);
}}
giftId={selectedGiftId}
eventId={event.id}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,495 @@
// components/gifts/gift-modal.tsx
import React, { useEffect } from "react";
import { useForm } from "react-hook-form";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { useGifts } from "@/context/gift-context";
import {
GiftItem,
GiftItemCreate,
GiftItemUpdate,
GiftPriority,
GiftStatus,
} from "@/client/types.gen";
import { Loader2 } from "lucide-react";
import { useAuth } from "@/context/auth-context";
interface GiftModalProps {
isOpen: boolean;
onClose: () => void;
giftId?: string;
eventId: string;
}
export function GiftModal({
isOpen,
onClose,
giftId,
eventId,
}: GiftModalProps) {
const {
item,
categories,
createItem,
updateItem,
fetchItemById,
isLoadingItem,
isLoadingCategories,
} = useGifts();
const { user } = useAuth();
const isEditMode = Boolean(giftId);
const {
register,
handleSubmit,
reset,
setValue,
watch,
formState: { errors, isSubmitting },
} = useForm<GiftItemCreate | GiftItemUpdate>();
const watchPriority = watch("priority");
const watchStatus = watch("status");
// Load gift data when editing
useEffect(() => {
if (isEditMode && giftId) {
fetchItemById(giftId);
}
}, [isEditMode, giftId, fetchItemById]);
// Reset form when modal opens or gift changes
useEffect(() => {
if (isOpen) {
if (isEditMode && item) {
reset({
name: item.name,
description: item.description,
price: item.price,
currency: item.currency || "CHF",
quantity_requested: item.quantity_requested || 1,
quantity_received: item.quantity_received || 0,
status: item.status,
priority: item.priority,
purchase_url: item.purchase_url,
store_name: item.store_name,
brand: item.brand,
model: item.model,
image_url: item.image_url,
notes: item.notes,
category_id: item.category_id,
});
} else {
reset({
name: "",
description: "",
price: undefined,
currency: "CHF",
quantity_requested: 1,
quantity_received: 0,
status: GiftStatus.AVAILABLE,
priority: GiftPriority.MEDIUM,
purchase_url: "",
store_name: "",
brand: "",
model: "",
image_url: "",
notes: "",
category_id: "none",
});
}
}
}, [isOpen, item, isEditMode, reset]);
const onSubmit = async (data: GiftItemCreate | GiftItemUpdate) => {
try {
// Handle the "none" category value by converting it to null/undefined
const submissionData = {
...data,
category_id: data.category_id === "none" ? undefined : data.category_id,
};
if (isEditMode && giftId) {
await updateItem(giftId, submissionData as GiftItemUpdate);
} else {
const createData = {
...submissionData,
event_id: eventId,
added_by: user?.id,
} as GiftItemCreate;
await createItem(createData);
}
onClose();
} catch (error) {
console.error("Error saving gift:", error);
}
};
const isLoading = isLoadingItem || isLoadingCategories;
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-[600px]">
<DialogHeader>
<DialogTitle>{isEditMode ? "Edit Gift" : "Add Gift"}</DialogTitle>
</DialogHeader>
{isLoading && isEditMode ? (
<div className="flex justify-center items-center py-8">
<Loader2 className="h-8 w-8 animate-spin text-blue-500" />
</div>
) : (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
<div className="space-y-1">
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">
Gift Information
</h3>
<div className="h-px bg-gray-200 dark:bg-gray-700" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="name" className="text-right col-span-1">
Name*
</Label>
<div className="col-span-3">
<Input
id="name"
placeholder="Gift name"
{...register("name", { required: "Name is required" })}
className={errors.name ? "border-red-500" : ""}
/>
{errors.name && (
<p className="text-red-500 text-sm mt-1">
{errors.name.message}
</p>
)}
</div>
</div>
<div className="grid grid-cols-4 items-start gap-4">
<Label
htmlFor="description"
className="text-right col-span-1 pt-2"
>
Description
</Label>
<div className="col-span-3">
<Textarea
id="description"
placeholder="Gift description"
{...register("description")}
className="min-h-[80px]"
/>
</div>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="category_id" className="text-right col-span-1">
Category
</Label>
<div className="col-span-3">
<Select
onValueChange={(value) => setValue("category_id", value)}
defaultValue={watch("category_id") || ""}
>
<SelectTrigger>
<SelectValue placeholder="Select a category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">Uncategorized</SelectItem>
{categories?.map((category) => (
<SelectItem key={category.id} value={category.id}>
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-4 gap-4">
<Label htmlFor="price" className="text-right col-span-1 pt-2">
Price
</Label>
<div className="col-span-1">
<Input
id="price"
type="number"
step="0.01"
min="0"
placeholder="0.00"
{...register("price", {
valueAsNumber: true,
validate: (value) =>
!value || value >= 0 || "Price cannot be negative",
})}
/>
{errors.price && (
<p className="text-red-500 text-sm mt-1">
{errors.price.message}
</p>
)}
</div>
<Label htmlFor="currency" className="text-right col-span-1 pt-2">
Currency
</Label>
<div className="col-span-1">
<Select
onValueChange={(value) => setValue("currency", value)}
defaultValue={watch("currency") || "CHF"}
>
<SelectTrigger>
<SelectValue placeholder="CHF" />
</SelectTrigger>
<SelectContent>
<SelectItem value="CHF">CHF</SelectItem>
<SelectItem value="EUR">EUR</SelectItem>
<SelectItem value="USD">USD</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="grid grid-cols-4 gap-4">
<Label
htmlFor="quantity_requested"
className="text-right col-span-1 pt-2"
>
Quantity
</Label>
<div className="col-span-1">
<Input
id="quantity_requested"
type="number"
min="1"
placeholder="1"
{...register("quantity_requested", {
valueAsNumber: true,
validate: (value) =>
(value && value >= 1) || "Quantity must be at least 1",
})}
/>
{errors.quantity_requested && (
<p className="text-red-500 text-sm mt-1">
{errors.quantity_requested.message}
</p>
)}
</div>
{isEditMode && (
<>
<Label
htmlFor="quantity_received"
className="text-right col-span-1 pt-2"
>
Received
</Label>
<div className="col-span-1">
<Input
id="quantity_received"
type="number"
min="0"
placeholder="0"
{...register("quantity_received", {
valueAsNumber: true,
validate: (value) =>
(value && value >= 0) ||
"Received quantity cannot be negative",
})}
/>
</div>
</>
)}
</div>
<div className="grid grid-cols-4 gap-4">
<Label htmlFor="priority" className="text-right col-span-1 pt-2">
Priority
</Label>
<div className="col-span-1">
<Select
onValueChange={(value) =>
setValue("priority", value as GiftPriority)
}
defaultValue={watchPriority || GiftPriority.MEDIUM}
>
<SelectTrigger>
<SelectValue placeholder="Medium" />
</SelectTrigger>
<SelectContent>
<SelectItem value={GiftPriority.LOW}>Low</SelectItem>
<SelectItem value={GiftPriority.MEDIUM}>Medium</SelectItem>
<SelectItem value={GiftPriority.HIGH}>High</SelectItem>
<SelectItem value={GiftPriority.MUST_HAVE}>
Must Have
</SelectItem>
</SelectContent>
</Select>
</div>
{isEditMode && (
<>
<Label
htmlFor="status"
className="text-right col-span-1 pt-2"
>
Status
</Label>
<div className="col-span-1">
<Select
onValueChange={(value) =>
setValue("status", value as GiftStatus)
}
defaultValue={watchStatus || GiftStatus.AVAILABLE}
>
<SelectTrigger>
<SelectValue placeholder="Available" />
</SelectTrigger>
<SelectContent>
<SelectItem value={GiftStatus.AVAILABLE}>
Available
</SelectItem>
<SelectItem value={GiftStatus.RESERVED}>
Reserved
</SelectItem>
<SelectItem value={GiftStatus.PURCHASED}>
Purchased
</SelectItem>
<SelectItem value={GiftStatus.RECEIVED}>
Received
</SelectItem>
<SelectItem value={GiftStatus.REMOVED}>
Removed
</SelectItem>
</SelectContent>
</Select>
</div>
</>
)}
</div>
<div className="space-y-1 pt-4">
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300">
Purchase Information
</h3>
<div className="h-px bg-gray-200 dark:bg-gray-700" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="store_name" className="text-right col-span-1">
Store
</Label>
<div className="col-span-3">
<Input
id="store_name"
placeholder="Store name"
{...register("store_name")}
/>
</div>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="purchase_url" className="text-right col-span-1">
URL
</Label>
<div className="col-span-3">
<Input
id="purchase_url"
placeholder="Purchase URL"
{...register("purchase_url")}
/>
</div>
</div>
<div className="grid grid-cols-4 gap-4">
<Label htmlFor="brand" className="text-right col-span-1 pt-2">
Brand
</Label>
<div className="col-span-1">
<Input
id="brand"
placeholder="Brand name"
{...register("brand")}
/>
</div>
<Label htmlFor="model" className="text-right col-span-1 pt-2">
Model/SKU
</Label>
<div className="col-span-1">
<Input
id="model"
placeholder="Model number"
{...register("model")}
/>
</div>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="image_url" className="text-right col-span-1">
Image URL
</Label>
<div className="col-span-3">
<Input
id="image_url"
placeholder="Image URL"
{...register("image_url")}
/>
</div>
</div>
<div className="grid grid-cols-4 items-start gap-4">
<Label htmlFor="notes" className="text-right col-span-1 pt-2">
Notes
</Label>
<div className="col-span-3">
<Textarea
id="notes"
placeholder="Additional notes"
{...register("notes")}
className="min-h-[80px]"
/>
</div>
</div>
<DialogFooter className="pt-4">
<Button type="button" variant="outline" onClick={onClose}>
Cancel
</Button>
<Button type="submit" disabled={isSubmitting}>
{isSubmitting ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
Saving...
</>
) : (
<>{isEditMode ? "Update" : "Create"}</>
)}
</Button>
</DialogFooter>
</form>
)}
</DialogContent>
</Dialog>
);
}