Skip to content

feat(competitions): save button, count at 3 or more, and /competitions/saved #200

Description

@AnayDhawan

Blocked by #199.

The client side of saving: a save button on cards and detail pages, the public count next to it, and a /competitions/saved page.

Files

  • src/lib/competition-saves.ts - new, mirroring src/lib/user-places.ts
  • src/components/competitions/save-button.tsx - 'use client'
  • src/app/competitions/saved/page.tsx

Follow the existing Supabase client pattern exactly

src/lib/user-places.ts is the model (141 lines): requireClient() at :61-65 throws when the client is null, currentUserId() at :67-72, then thin fetch/create/delete wrappers. Copy that structure.

Functions needed:

fetchSavedCompetitionIds(): Promise<string[]>
saveCompetition(competitionId: string): Promise<void>
unsaveCompetition(competitionId: string): Promise<void>
fetchSaveCounts(): Promise<Record<string, number>>   // reads competition_stats, works signed out

The count rendering rule

Show the number only when it is 3 or more. Below that, render the save icon alone.

Do not reserve empty layout space for the hidden number. A permanently blank slot on every card for the first few months is worse than no slot at all. The button should be visibly narrower when there is no count, not a gap where a count would go.

Preview mode is a supported path, not an edge case

src/lib/supabase/config.ts:9-14 gates every call site and both client.ts and server.ts return null when the env vars are absent. Self-hosting without Supabase is first-class: the map, filters and calendar all work without it.

So: with no Supabase configured, the save button does not render at all, counts do not render, and /competitions/saved either redirects or shows the same "preview mode" card that src/app/login/login-form.tsx:29-44 uses. Nothing throws.

Signed out but Supabase configured: the button renders and prompts sign-in on click rather than silently doing nothing.

Optimistic update

Toggle the icon immediately, then reconcile. On failure, revert and surface a sonner toast; the repo already has sonner 2.0.7 wired up.

The saved page

Same card component as browse (#196), filtered to saved ids, sorted by soonest deadline. Empty state links to /competitions rather than being a dead end.

Acceptance criteria

  • Save then reload: the state persists
  • Count appears at 3, is absent at 2, and no gap is left behind
  • Unsaving decrements the count
  • Signed out: clicking prompts sign-in, no console error
  • Supabase unconfigured: no save UI anywhere, no crash, npm run build still passes
  • Failed write reverts the optimistic state and shows a toast
  • /competitions/saved empty state is useful

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions