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.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
Bookmarkicon placeholder) - “Saved Items” label
Lockicon + “Private” + item count
- Latest item image (or
FolderOpenicon placeholder) - Collection name
Globeicon + “Public” orLockicon + “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 toggleisStandaloneSavedandsavedItemIdin the save status cachemutationFn: Check current status → insert or delete fromsaved_itemsonError: Rollback to previous snapshotonSettled: 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 listmutationFn: Insert or delete fromcollection_saved_itemsonError: Rollback to previous snapshotonSettled: 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()
Collections
Organize saved items into collections
Recipes
Save recipes