Skip to main content

What It Does

Bookmark system. Users save recipes, posts, or collections, then optionally organize them into user-created collections.

User Flow

1

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.
2

Organize (Optional)

SaveModal appears showing user’s collections with checkboxes → toggle collection membership → adds/removes collection_saved_items.
3

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

ScreenPurpose
app/(tabs)/saved.tsxSaved tab — shows collections + uncategorized items
app/saved-items.tsxFull saved items list

Components

ComponentPurpose
SaveModalCollection picker modal, mounted globally from app/_layout.tsx and controlled by SaveProvider
SaveButtonBookmark icon button
ContentTabsFilter 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)

  1. onMutate: Optimistically toggle isStandaloneSaved and savedItemId in the save status cache
  2. mutationFn: Check current status → insert or delete from saved_items
  3. onError: Rollback to previous snapshot
  4. onSettled: Narrow invalidation — only the specific item’s status + saved items list (not all queries)

useToggleCollectionMembership (Add/Remove from Collection)

  1. onMutate: Optimistically add/remove collection from the item’s collection list
  2. mutationFn: Insert or delete from collection_saved_items
  3. onError: Rollback to previous snapshot
  4. 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)

MethodRoutePurpose
GET/api/saved-itemsList user’s saved items
POST/api/saved-itemsSave an item
DELETE/api/saved-items/[id]Unsave an item (cascades: removes from all collections)
GET/api/saved-items/statusCheck save status for content
POST/api/saved-items/toggle-standaloneToggle standalone save without collection

RPC Functions

FunctionParametersReturns
get_saved_page_datap_user_idJSON — collections with preview images + uncategorized count
get_uncategorized_saved_itemsp_user_id, p_limit (12), p_offset (0)JSON — saved items not in any collection
get_user_collections_for_save_modalp_user_id, p_saved_item_id (optional)TABLE — collections with is_in_collection flag for current item

Database

TableKey ColumnsNotes
saved_itemsid (uuid PK), user_id (FK→profiles), content_type (CHECK: ‘post’, ‘recipe’, ‘collection’), content_id (uuid), created_atPolymorphic: content_type + content_id point to any content
collection_saved_itemscollection_id (FK→collections), saved_item_id (FK→saved_items), added_atComposite 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()