What It Does
Bookmark system. Users save recipes, posts, or collections, then optionally organize them into user-created collections.
User Flow
Save Content
Tap bookmark icon on any content → SaveButton opens the shared save flow via useSaveModal().openSaveModal() → if needed, the item is auto-saved first (creates saved_item) before the collection picker opens.
Organize (Optional)
SaveModal appears showing user’s collections with checkboxes → toggle collection membership → adds/removes collection_saved_items.
Browse Saved
Access via Saved tab (app/(tabs)/saved.tsx) and the uncategorized saved-items screen (app/saved-items.tsx).
Saved Tab Layout
app/(tabs)/saved.tsx — requires authentication (AuthGuard). Uses the shared header layout (RECIPE ROOM / Plus / NotificationBell).
Grid Layout
2-column grid with 12px gap, 16px padding. Each card is a rounded-corner (16px) image thumbnail with metadata below.
The first card is always “Saved Items” — the uncategorized bucket containing all saved items not assigned to any collection. It shows:
- Latest saved item’s image (or
Bookmark icon placeholder)
- “Saved Items” label
Lock icon + “Private” + item count
Remaining cards are user-created collections, each showing:
- Latest item image (or
FolderOpen icon placeholder)
- Collection name
Globe icon + “Public” or Lock icon + “Private” + item count
Loading State
4 skeleton cards (2×2 grid) with grey rounded rectangles + text placeholders.
Empty State
When no collections exist and uncategorized count is 0: EmptyState with FolderOpen icon, “No collections yet”, “Create a collection to organize your favorite recipes”, and “Create Collection” action button.
Screens
| Screen | Purpose |
|---|
app/(tabs)/saved.tsx | Saved tab — shows collections + uncategorized items |
app/saved-items.tsx | Full saved items list |
Components
| Component | Purpose |
|---|
SaveModal | Collection picker modal, mounted globally from app/_layout.tsx and controlled by SaveProvider |
SaveButton | Bookmark icon button |
ContentTabs | Filter tabs on the uncategorized saved-items screen |
Context
SaveProvider (src/context/SaveContext.tsx) — manages save modal state, auto-save behavior, and item-level save status caches. The modal itself is rendered once from the root layout rather than through a dedicated route.
Hooks
useSaveStatus (src/hooks/api/useSaveStatus.ts), useCollections
Data Access Pattern
The mobile app calls Supabase directly for save operations — supabase.from('saved_items').insert/delete, supabase.from('collection_saved_items').insert/delete. The web API routes exist for the web client (Phase 2).
Optimistic Updates & Cross-Screen Sync
useToggleStandalone (Save/Unsave)
onMutate: Optimistically toggle isStandaloneSaved and savedItemId in the save status cache
mutationFn: Check current status → insert or delete from saved_items
onError: Rollback to previous snapshot
onSettled: Narrow invalidation — only the specific item’s status + saved items list (not all queries)
useToggleCollectionMembership (Add/Remove from Collection)
onMutate: Optimistically add/remove collection from the item’s collection list
mutationFn: Insert or delete from collection_saved_items
onError: Rollback to previous snapshot
onSettled: Invalidate specific item status + specific collection detail
useSavedItemIds (Bulk Status for List Views)
For feed views that need to show bookmark state on every card, useSavedItemIds fetches all saved item IDs in one query and returns a Set<string> of "contentType:contentId" strings for O(1) lookup. Cached for 1 minute.
Web API Routes (Phase 2)
| Method | Route | Purpose |
|---|
GET | /api/saved-items | List user’s saved items |
POST | /api/saved-items | Save an item |
DELETE | /api/saved-items/[id] | Unsave an item (cascades: removes from all collections) |
GET | /api/saved-items/status | Check save status for content |
POST | /api/saved-items/toggle-standalone | Toggle standalone save without collection |
RPC Functions
| Function | Parameters | Returns |
|---|
get_saved_page_data | p_user_id | JSON — collections with preview images + uncategorized count |
get_uncategorized_saved_items | p_user_id, p_limit (12), p_offset (0) | JSON — saved items not in any collection |
get_user_collections_for_save_modal | p_user_id, p_saved_item_id (optional) | TABLE — collections with is_in_collection flag for current item |
Database
| Table | Key Columns | Notes |
|---|
saved_items | id (uuid PK), user_id (FK→profiles), content_type (CHECK: ‘post’, ‘recipe’, ‘collection’), content_id (uuid), created_at | Polymorphic: content_type + content_id point to any content |
collection_saved_items | collection_id (FK→collections), saved_item_id (FK→saved_items), added_at | Composite PK |
RLS Policies
saved_items:
- SELECT:
user_id = auth.uid() (private — only owner sees their saves)
- INSERT:
user_id = auth.uid()
- DELETE:
user_id = auth.uid()