Skip to content

Item Management

Item Management enables users to add specific gift items to their wishlists, including product names, images, URLs, prices, and mark favorites.

As a wishlist owner,
I want to add items with details and links,
So that my Secret Santa match knows exactly what I want and where to buy it.

  • ✅ User can add custom items with name, URL, image, and price
  • ✅ User can add items from trending suggestions
  • ✅ User can mark items as favorites (priority)
  • ✅ User can edit item details
  • ✅ User can delete items from wishlist
  • ✅ User can upload custom item images
  • ✅ Items display with blurhash placeholder while loading
  • ✅ External links open in new tab

Inputs:

  • Item name (string, required)
  • Item URL (string, required, must be valid URL)
  • Item image (File or URL, required)
  • Price (number, optional)
  • Is favorite (boolean, default: false)

Outputs:

  • Created item with unique ID
  • Updated wishlist with new item count
  • Image uploaded to CDN with blurhash
User navigates to /wishlist/:id/edit/items
Click "Add Item"
Fill form:
- Name: "Wireless Headphones"
- URL: "https://amazon.com/..."
- Price: $99.99
- Upload image or provide URL
- Mark as favorite: ☑️
├─ Upload custom image ────────┐
│ │
│ ▼
│ Validate file:
│ - Type: image/jpeg, image/png
│ - Size: < 5 MB
│ │
│ ▼
│ Preview image
└─ Provide image URL ──────────┐
Validate URL format
Click "Add to Wishlist"
POST /wishlists/v2/:id/item
FormData: { name, url, isFavorite, image }
├─ Success ────────────────────┐
│ │
│ ▼
│ Backend uploads image to CDN
│ │
│ ▼
│ Generate blurhash for image
│ │
│ ▼
│ Return item with CDN URLs
│ │
│ ▼
│ Add item to Redux wishlist
│ │
│ ▼
│ Show "Item added" toast
│ │
│ ▼
│ Reset form for next item
└─ Error ──────────────────────┐
Show error toast
"Failed to add item"
User browses /items (trending items)
View curated gift suggestions:
┌─────────┬─────────┬─────────┬─────────┐
│ Item 1 │ Item 2 │ Item 3 │ Item 4 │
│ [image] │ [image] │ [image] │ [image] │
│ $25 │ $50 │ $100 │ $15 │
│ [+ Add] │ [+ Add] │ [+ Add] │ [+ Add] │
└─────────┴─────────┴─────────┴─────────┘
Click "+ Add" on Item 2
Show bottom sheet:
"Add to wishlist"
- Select wishlist: [dropdown]
- Mark as favorite: ☐
User selects wishlist and confirms
POST /wishlists/:wishlistId/item
{ name, url, image, isFavorite }
├─ Success ────────────────────┐
│ │
│ ▼
│ Add item to Redux
│ │
│ ▼
│ Show "Added to wishlist" toast
└─ Error ──────────────────────┐
Show error toast
User navigates to /wishlist/:id/edit/items
View items list with edit buttons
Click "Edit" on an item
Load item data into form:
- Name: [current name]
- URL: [current URL]
- Price: [current price]
- Image: [current image preview]
- Is favorite: [current state]
User updates fields
Click "Save Changes"
PATCH /wishlists/v2/:wishlistId/item/:itemId
FormData: { name, url, isFavorite, image? }
├─ Success ────────────────────┐
│ │
│ ▼
│ Update item in Redux
│ │
│ ▼
│ Show "Item updated" toast
└─ Error ──────────────────────┐
Show error toast
User clicks "Delete" on item
Show confirmation modal:
"Remove this item from wishlist?"
User confirms
DELETE /wishlists/:wishlistId/item/:itemId
├─ Success ────────────────────┐
│ │
│ ▼
│ Remove item from Redux
│ │
│ ▼
│ Show "Item removed" toast
└─ Error ──────────────────────┐
Show error toast
Match navigates to /group/:groupId/wishlists/:wishlistId
GET /groups/:groupId/wishlists/:wishlistId
Display items grid:
┌──────────────────┬──────────────────┐
│ ⭐ Wireless HD... │ Gaming Mouse │
│ [image] │ [image] │
│ $99.99 │ $49.99 │
│ [View Product] │ [View Product] │
└──────────────────┴──────────────────┘
User clicks "View Product" on item
Open item.url in new tab
(e.g., https://amazon.com/product/...)
  1. Invalid URL: Show error “Please enter a valid URL (e.g., https://…)”
  2. Image upload fails: Fall back to placeholder image
  3. Duplicate item: Allow duplicates; no uniqueness check
  4. Item with no price: Display “Price not specified”
  5. Very long item name: Truncate with ellipsis after 50 characters
  6. Image too large: Reject files > 5 MB
  7. Network error during save: Retry automatically once, then manual

Request:

POST /wishlists/v2/1/item
Authorization: Bearer {token}
Content-Type: multipart/form-data
name=Wireless Headphones
url=https://amazon.com/product/123
isFavorite=true
image=<binary file data>

Response:

{
"success": true,
"item": {
"id": 10,
"name": "Wireless Headphones",
"url": "https://amazon.com/product/123",
"price": 99.99,
"image": "https://cdn.rnb.la/items/item10.jpg",
"imageBlurhash": "L6PZfSi_.AyE_3t7t7R**0o#DgR4",
"isFavorite": true,
"wishlistId": 1,
"createdAt": "2025-10-29T12:00:00Z"
}
}

Request:

POST /wishlists/1/item
Authorization: Bearer {token}
Content-Type: application/json
{
"name": "Smart Watch",
"url": "https://store.com/watch",
"price": 199.99,
"image": "https://cdn.rnb.la/trending/watch.jpg",
"isFavorite": false
}

Response:

{
"success": true,
"item": {
"id": 11,
"name": "Smart Watch",
"url": "https://store.com/watch",
"price": 199.99,
"image": "https://cdn.rnb.la/trending/watch.jpg",
"imageBlurhash": "L6PZfSi_.AyE_3t7t7R**0o#DgR4",
"isFavorite": false,
"wishlistId": 1,
"createdAt": "2025-10-29T12:05:00Z"
}
}

Request:

PATCH /wishlists/v2/1/item/10
Authorization: Bearer {token}
Content-Type: multipart/form-data
name=Updated Headphones
url=https://amazon.com/product/456
isFavorite=false
image=<optional new image>

Response:

{
"success": true,
"item": {
"id": 10,
"name": "Updated Headphones",
"url": "https://amazon.com/product/456",
"isFavorite": false,
...
}
}

Request:

DELETE /wishlists/1/item/10
Authorization: Bearer {token}

Response:

{
"success": true,
"message": "Item deleted"
}
  • Empty name: Form validation prevents submission
  • Invalid URL: Validate format before submission
  • No image provided: Require either file upload or URL
  • Image too large: Check file size before upload
  • 400 Bad Request: “Invalid item data”
  • 404 Not Found: “Wishlist not found”
  • 413 Payload Too Large: “Image file too large”
  • 500 Server Error: “Failed to save item”
  • Create/update: Manual retry (user clicks save again)
  • Image upload: Automatic retry once, then manual
  • Delete: Manual retry only
  • item.created_custom: Incremented when custom item added
  • item.created_from_trending: Incremented when trending item added
  • item.edited: Incremented on item update
  • item.deleted: Incremented on item deletion
  • item.marked_favorite: Incremented when favorite toggled on
  • item.link_clicked: Incremented when match clicks “View Product”
console.log('Item created:', itemId);
console.log('Item added from trending:', trendingItemId);
Sentry.addBreadcrumb({
category: 'item',
message: 'User added item to wishlist',
data: { wishlistId, itemId, source: 'custom' },
});

Assumption: No feature flags. Consider adding for:

  • Beta testing new item creation UI
  • A/B testing trending items integration
  • Database: Items table requires imageBlurhash column (added in migration v3.2)
  • Data: Existing items without blurhash get generated on first access
src/sections/crear-item/add-item-view.tsx
'use client';
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { yupResolver } from '@hookform/resolvers/yup';
import * as yup from 'yup';
import { itemHandlers } from 'src/services/handlers/items/itemHandlers';
import { toast } from 'react-toastify';
const schema = yup.object({
name: yup.string().required('Name is required'),
url: yup.string().url('Must be a valid URL').required('URL is required'),
price: yup.number().positive().optional(),
isFavorite: yup.boolean(),
});
export default function AddItemView({ wishlistId }: Props) {
const [imageFile, setImageFile] = useState<File | null>(null);
const { register, handleSubmit, formState: { errors, isSubmitting }, reset } = useForm({
resolver: yupResolver(schema),
});
const onSubmit = async (data) => {
if (!imageFile) {
toast.error('Please upload an image');
return;
}
try {
await itemHandlers.createCustomItem(
wishlistId,
data.isFavorite || false,
data.name,
data.url,
imageFile
);
toast.success('Item added to wishlist!');
reset();
setImageFile(null);
} catch (error) {
toast.error('Failed to add item');
console.error(error);
}
};
const handleImageChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (file) {
if (file.size > 5 * 1024 * 1024) {
toast.error('Image too large (max 5 MB)');
return;
}
setImageFile(file);
}
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name')} placeholder="Item name" />
{errors.name && <span>{errors.name.message}</span>}
<input {...register('url')} placeholder="Product URL" type="url" />
{errors.url && <span>{errors.url.message}</span>}
<input {...register('price')} placeholder="Price (optional)" type="number" step="0.01" />
<input type="file" accept="image/*" onChange={handleImageChange} />
{imageFile && <img src={URL.createObjectURL(imageFile)} alt="Preview" width={100} />}
<label>
<input type="checkbox" {...register('isFavorite')} />
Mark as favorite
</label>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Adding...' : 'Add Item'}
</button>
</form>
);
}