This document outlines the core logic, data structures, and workflows for the Challengers application.
The application uses a points-based system to incentivize consistency.
- Joining a Challenge: +500 points (Initial buy-in/starting capital).
- Creating a Challenge: +500 points (Creator bonus).
- Daily Check-in:
- Base: 0 points (Standard check-in maintains the streak).
- Streak Bonus: +100 points every 3 days of consecutive streak (e.g., Day 3, Day 6, Day 9).
- Missed Task: -100 points.
- This is calculated daily by the
checkMissedLogsfunction. - If a user fails to check in by the deadline (or end of day), 100 points are deducted.
- Streak Reset: Missing a task resets the
streak_currentto 0.
- This is calculated daily by the
The Dashboard displays a circular progress indicator for the current day.
- Numerator (Completed): Count of
daily_logswhere:datematches today.statusis 'completed'.challenge_idbelongs to a challenge that is active today (i.e., not a rest day).
- Denominator (Total): Count of Active Challenges that are scheduled for today (excluding rest days).
- Challenges can have defined "Rest Days" (e.g., Sundays).
- On these days, the challenge is excluded from the "Total Tasks" count, so users are not penalized for not checking in.
The Dashboard features an Area Chart comparing user progress.
- Primary:
challenge_participantscollection ->points_historyfield. - Fallback:
profilescollection ->points_historyfield (Global history).
- The app fetches the first active challenge.
- It retrieves all participants for that challenge.
- It extracts the
points_historyarray for each participant. - It merges these histories into a single timeline (by date).
- Gap Filling: If a user has data for Day 1 and Day 3 but not Day 2, the chart logic carries forward the Day 1 value to Day 2 to ensure continuous lines.
The Check-in process verifies user activity.
- Image Upload:
- User captures/selects a photo.
- Image is uploaded directly to Supabase Storage (Client-side) in the
checkins/{challengeId}bucket. - Returns a public URL.
- Log Creation:
- A document is created in the
daily_logsFirestore collection. - Fields:
challenge_id,user_id,date,status: 'completed',proof_url,verified: true.
- A document is created in the
- Gamification Update:
- The user's document in
challenge_participantsis updated:streak_currentincrements by 1.streak_bestupdates if current > best.current_pointsincreases ONLY if the streak bonus applies (Day % 3 == 0).points_historyarray is appended with the new entry.
- Global Profile Update:
- If points were added, the global
profilesdocument is also updated (total_earned,current_points,points_history).
- If points were added, the global
- The user's document in
Data is denormalized across collections for performance.
- Purpose: Stores global stats and identity.
- Key Fields:
current_points: Total points across ALL challenges.total_earned: Lifetime points earned.total_lost: Lifetime points lost (penalties).points_history: Array of{ date, points, taskStatus }. Used for the Dashboard chart when no specific challenge is selected.
- Purpose: Stores rules and settings.
- Key Fields:
title,description,start_date,end_date,rest_days,join_code.
- Purpose: Links Users to Challenges and stores context-specific progress.
- Key Fields:
user_id,challenge_id.current_points: Points earned in this specific challenge.streak_current: Active streak for this challenge.points_history: Array of point changes specific to this challenge. Used for the Chart comparison.
- Purpose: Immutable record of every action.
- Key Fields:
date,status('completed' | 'missed'),proof_url.
points_historyis stored in BOTHprofilesandchallenge_participants.- Why?
profiles: Allows fast rendering of the global dashboard chart without querying every single past challenge.challenge_participants: Allows fast rendering of challenge-specific leaderboards and comparison charts without filtering a massive global history.
- Why?
- Storage: Theme preference (
'light' | 'dark' | 'system') is stored in theprofilescollection under thethemefield. - Synchronization:
- Initial Load: The app checks the DB preference. If it differs from the local
next-themesstate, it updates the local state to match the DB. - User Change: When the user toggles the theme, the new value is saved to the DB (debounced to prevent excessive writes).
- Conflict Resolution: A
lastDbThemeref tracks the last value synced from the DB to prevent local changes from being overwritten by background refetches.
- Initial Load: The app checks the DB preference. If it differs from the local
- Challenge Cards:
- Designed for high affordance with hover effects (border highlight, scale).
- Includes a visual consistency tracker (checkmarks/crosses) derived from
points_historyanddaily_logs. - Uses a "traffic light" system for status: Green (Completed), Red (Missed), Dashed (Pending), Gray (Rest/Future).
- Architecture: Uses Firestore
onSnapshotlisteners for real-time updates. - Structure: Messages are stored in a subcollection:
conversations/{conversationId}/messages. - Pagination:
- Initial Load: Fetches the latest 50 messages (descending order).
- Infinite Scroll: Uses an
IntersectionObserversentinel at the top of the chat. When visible, it triggers a fetch for the next batch of 50 messages usingstartAfterthe last loaded document. - Scroll Anchoring: Preserves the user's scroll position when older messages are loaded by adjusting
scrollTopbased on the change inscrollHeight.
- FCM Integration: Uses Firebase Cloud Messaging for push notifications.
- Token Management:
- Tokens are requested on the Profile page.
- Stored in the
profilescollection underfcm_tokens(array). - Service Worker: A custom
firebase-messaging-sw.jshandles background notifications. - Foreground: A
ForegroundNotificationListenercomponent handles messages when the app is open, displaying them as toasts.
- Problem: N+1 query performance issues when fetching challenges and their participant counts.
- Solution:
- Batch Fetching: The
/api/challenges/activeand/api/participantsendpoints now use Firestoreinqueries to fetch all related documents in parallel batches (chunked by 10). - In-Memory Aggregation: Participant counts are calculated in memory after fetching all participants in a single batch, rather than querying for each challenge individually.
- Client-Side Caching: RTK Query is configured with
keepUnusedDataFor: 300(5 minutes) to prevent unnecessary network requests when navigating between tabs.
- Batch Fetching: The
- Problem: Rendering large chat histories caused DOM lag.
- Solution:
- Memoization: The message list rendering is memoized using
useMemoto prevent re-rendering all message bubbles on every keystroke. - Lazy Loading: Only a limited subset of messages is loaded initially.
- Memoization: The message list rendering is memoized using