From 99c0632b16a8f3cc43f90e702edcf6a32211daa8 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 11 Jan 2026 20:20:27 +0000 Subject: [PATCH 1/3] feat: Add Supabase streaks table and service Co-authored-by: alebiosuf0802 --- frontend/constants/supabase.ts | 9 +++ frontend/services/streak-service.ts | 55 +++++++++++++ .../migrations/20260111000000_streaks.sql | 80 +++++++++++++++++++ frontend/types/database.ts | 26 ++++++ 4 files changed, 170 insertions(+) create mode 100644 frontend/supabase/migrations/20260111000000_streaks.sql diff --git a/frontend/constants/supabase.ts b/frontend/constants/supabase.ts index 4334da2..579eece 100644 --- a/frontend/constants/supabase.ts +++ b/frontend/constants/supabase.ts @@ -12,6 +12,7 @@ export const TABLES = { PUSH_TOKENS: 'push_tokens', NOTIFICATION_SETTINGS: 'notification_settings', PRIVACY_SETTINGS: 'privacy_settings', + STREAKS: 'streaks', } as const; // Storage Bucket Names @@ -120,5 +121,13 @@ export const SCHEMA = { location_share: 'boolean NOT NULL DEFAULT true', created_at: 'timestamptz DEFAULT now()', updated_at: 'timestamptz DEFAULT now()', + }, + STREAKS: { + id: 'bigserial PRIMARY KEY', + user_id: 'uuid NOT NULL REFERENCES profiles(id) ON DELETE CASCADE', + current_streak: 'integer NOT NULL DEFAULT 0', + max_streak: 'integer NOT NULL DEFAULT 0', + created_at: 'timestamptz DEFAULT now()', + updated_at: 'timestamptz DEFAULT now()', } } as const; \ No newline at end of file diff --git a/frontend/services/streak-service.ts b/frontend/services/streak-service.ts index b1a8ca6..5278d2d 100644 --- a/frontend/services/streak-service.ts +++ b/frontend/services/streak-service.ts @@ -1,6 +1,7 @@ import { format, differenceInDays, startOfDay } from 'date-fns'; import { deviceStorage } from './device-storage'; import { logger } from '@/lib/logger'; +import { supabase } from '@/lib/supabase'; /** * Streak Service - Manages user daily entry streaks @@ -40,6 +41,42 @@ export class StreakService { console.error('Failed to load streak data:', error); } + // Fallback to Supabase (single row per user) for cross-device sync of streak stats + try { + const { data, error } = await supabase + .from('streaks') + .select('current_streak, max_streak') + .eq('user_id', userId) + .maybeSingle<{ + current_streak: number | null; + max_streak: number | null; + }>(); + + if (error) { + console.error('Error fetching streak data from Supabase:', error); + } else if (data) { + const fromRemote: StreakData = { + currentStreak: data.current_streak ?? 0, + maxStreak: data.max_streak ?? 0, + // These fields are still tracked locally for streak logic; the Supabase table + // intentionally stores only streak stats (current/max). + lastEntryDate: null, + lastAccessTime: null, + }; + + // Cache remotely-loaded data locally (best-effort) + try { + await deviceStorage.setItem(`streak_${userId}`, fromRemote); + } catch (cacheError) { + console.error('Failed to cache remote streak data locally:', cacheError); + } + + return fromRemote; + } + } catch (error) { + console.error('Error in Supabase streak fallback:', error); + } + // Return default streak data return { currentStreak: 0, @@ -51,12 +88,30 @@ export class StreakService { // Save streak data to storage static async saveStreakData(userId: string, data: StreakData): Promise { + // 1. Save to local storage (best-effort cache; don't throw) try { await deviceStorage.setItem(`streak_${userId}`, data); console.log('Saved streak data:', data); } catch (error) { console.error('Failed to save streak data:', error); } + + // 2. Sync streak stats to Supabase (best-effort; do not block core app flows) + try { + const { error } = await supabase + .from('streaks') + .upsert({ + user_id: userId, + current_streak: data.currentStreak, + max_streak: data.maxStreak, + } as never, { onConflict: 'user_id' } as never); + + if (error) { + console.error('Error saving streak data to Supabase:', error); + } + } catch (error) { + console.error('Error in saveStreakData Supabase sync:', error); + } } /** diff --git a/frontend/supabase/migrations/20260111000000_streaks.sql b/frontend/supabase/migrations/20260111000000_streaks.sql new file mode 100644 index 0000000..6059348 --- /dev/null +++ b/frontend/supabase/migrations/20260111000000_streaks.sql @@ -0,0 +1,80 @@ +/* + # Streaks Table + + 1. New Table + - `streaks` - Per-user streak stats (1 row per user) + - `id` (bigint, primary key) + - `user_id` (uuid, references profiles) + - `current_streak` (integer) + - `max_streak` (integer) + - `created_at` (timestamp) + - `updated_at` (timestamp) + + 2. Security + - Enable RLS + - Policies so users can manage only their own streak record +*/ + +-- Create streaks table +CREATE TABLE IF NOT EXISTS streaks ( + id bigserial PRIMARY KEY, + user_id uuid REFERENCES profiles(id) ON DELETE CASCADE NOT NULL, + current_streak integer NOT NULL DEFAULT 0, + max_streak integer NOT NULL DEFAULT 0, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now(), + UNIQUE(user_id) +); + +-- Backfill and enforce NOT NULL constraints for existing rows (if any) +UPDATE streaks +SET created_at = now() +WHERE created_at IS NULL; + +UPDATE streaks +SET updated_at = now() +WHERE updated_at IS NULL; + +ALTER TABLE streaks + ALTER COLUMN created_at SET NOT NULL, + ALTER COLUMN updated_at SET NOT NULL; + +-- Enable Row Level Security +ALTER TABLE streaks ENABLE ROW LEVEL SECURITY; + +-- Policies: users can manage their own streak record +CREATE POLICY "Users can read own streak" + ON streaks + FOR SELECT + TO authenticated + USING (auth.uid() = user_id); + +CREATE POLICY "Users can insert own streak" + ON streaks + FOR INSERT + TO authenticated + WITH CHECK (auth.uid() = user_id); + +CREATE POLICY "Users can update own streak" + ON streaks + FOR UPDATE + TO authenticated + USING (auth.uid() = user_id); + WITH CHECK (auth.uid() = user_id); + +CREATE POLICY "Users can delete own streak" + ON streaks + FOR DELETE + TO authenticated + USING (auth.uid() = user_id); + +-- Indexes for performance +CREATE INDEX IF NOT EXISTS idx_streaks_user_id + ON streaks(user_id); + +-- updated_at trigger +CREATE TRIGGER update_streaks_updated_at + BEFORE UPDATE ON streaks + FOR EACH ROW + EXECUTE FUNCTION update_updated_at_column(); + diff --git a/frontend/types/database.ts b/frontend/types/database.ts index b493d76..8d79520 100644 --- a/frontend/types/database.ts +++ b/frontend/types/database.ts @@ -317,6 +317,32 @@ export interface Database { updated_at?: string } } + streaks: { + Row: { + id: number + user_id: string + current_streak: number + max_streak: number + created_at: string + updated_at: string + } + Insert: { + id?: number + user_id: string + current_streak?: number + max_streak?: number + created_at?: string + updated_at?: string + } + Update: { + id?: number + user_id?: string + current_streak?: number + max_streak?: number + created_at?: string + updated_at?: string + } + } } Views: { [_ in never]: never From b3e96f91cc2fa8a1720e289bf629068c50785cee Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 11 Jan 2026 20:44:19 +0000 Subject: [PATCH 2/3] Refactor: Rename streaks table to user_streaks Co-authored-by: alebiosuf0802 --- frontend/constants/supabase.ts | 6 +-- frontend/services/streak-service.ts | 4 +- .../migrations/20260111000000_streaks.sql | 39 +++++++++---------- frontend/types/database.ts | 2 +- 4 files changed, 25 insertions(+), 26 deletions(-) diff --git a/frontend/constants/supabase.ts b/frontend/constants/supabase.ts index 579eece..f3a63e6 100644 --- a/frontend/constants/supabase.ts +++ b/frontend/constants/supabase.ts @@ -12,7 +12,7 @@ export const TABLES = { PUSH_TOKENS: 'push_tokens', NOTIFICATION_SETTINGS: 'notification_settings', PRIVACY_SETTINGS: 'privacy_settings', - STREAKS: 'streaks', + USER_STREAKS: 'user_streaks', } as const; // Storage Bucket Names @@ -122,9 +122,9 @@ export const SCHEMA = { created_at: 'timestamptz DEFAULT now()', updated_at: 'timestamptz DEFAULT now()', }, - STREAKS: { + USER_STREAKS: { id: 'bigserial PRIMARY KEY', - user_id: 'uuid NOT NULL REFERENCES profiles(id) ON DELETE CASCADE', + user_id: 'uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE', current_streak: 'integer NOT NULL DEFAULT 0', max_streak: 'integer NOT NULL DEFAULT 0', created_at: 'timestamptz DEFAULT now()', diff --git a/frontend/services/streak-service.ts b/frontend/services/streak-service.ts index 5278d2d..742bb2d 100644 --- a/frontend/services/streak-service.ts +++ b/frontend/services/streak-service.ts @@ -44,7 +44,7 @@ export class StreakService { // Fallback to Supabase (single row per user) for cross-device sync of streak stats try { const { data, error } = await supabase - .from('streaks') + .from('user_streaks') .select('current_streak, max_streak') .eq('user_id', userId) .maybeSingle<{ @@ -99,7 +99,7 @@ export class StreakService { // 2. Sync streak stats to Supabase (best-effort; do not block core app flows) try { const { error } = await supabase - .from('streaks') + .from('user_streaks') .upsert({ user_id: userId, current_streak: data.currentStreak, diff --git a/frontend/supabase/migrations/20260111000000_streaks.sql b/frontend/supabase/migrations/20260111000000_streaks.sql index 6059348..f3996f7 100644 --- a/frontend/supabase/migrations/20260111000000_streaks.sql +++ b/frontend/supabase/migrations/20260111000000_streaks.sql @@ -1,10 +1,10 @@ /* - # Streaks Table + # User Streaks Table 1. New Table - - `streaks` - Per-user streak stats (1 row per user) + - `user_streaks` - Per-user streak stats (1 row per user) - `id` (bigint, primary key) - - `user_id` (uuid, references profiles) + - `user_id` (uuid, references auth.users) - `current_streak` (integer) - `max_streak` (integer) - `created_at` (timestamp) @@ -15,66 +15,65 @@ - Policies so users can manage only their own streak record */ --- Create streaks table -CREATE TABLE IF NOT EXISTS streaks ( +-- Create user_streaks table +CREATE TABLE IF NOT EXISTS user_streaks ( id bigserial PRIMARY KEY, - user_id uuid REFERENCES profiles(id) ON DELETE CASCADE NOT NULL, + user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, current_streak integer NOT NULL DEFAULT 0, max_streak integer NOT NULL DEFAULT 0, created_at timestamptz NOT NULL DEFAULT now(), - updated_at timestamptz NOT NULL DEFAULT now(), - UNIQUE(user_id) + updated_at timestamptz NOT NULL DEFAULT now() ); -- Backfill and enforce NOT NULL constraints for existing rows (if any) -UPDATE streaks +UPDATE user_streaks SET created_at = now() WHERE created_at IS NULL; -UPDATE streaks +UPDATE user_streaks SET updated_at = now() WHERE updated_at IS NULL; -ALTER TABLE streaks +ALTER TABLE user_streaks ALTER COLUMN created_at SET NOT NULL, ALTER COLUMN updated_at SET NOT NULL; -- Enable Row Level Security -ALTER TABLE streaks ENABLE ROW LEVEL SECURITY; +ALTER TABLE user_streaks ENABLE ROW LEVEL SECURITY; -- Policies: users can manage their own streak record CREATE POLICY "Users can read own streak" - ON streaks + ON user_streaks FOR SELECT TO authenticated USING (auth.uid() = user_id); CREATE POLICY "Users can insert own streak" - ON streaks + ON user_streaks FOR INSERT TO authenticated WITH CHECK (auth.uid() = user_id); CREATE POLICY "Users can update own streak" - ON streaks + ON user_streaks FOR UPDATE TO authenticated USING (auth.uid() = user_id); WITH CHECK (auth.uid() = user_id); CREATE POLICY "Users can delete own streak" - ON streaks + ON user_streaks FOR DELETE TO authenticated USING (auth.uid() = user_id); -- Indexes for performance -CREATE INDEX IF NOT EXISTS idx_streaks_user_id - ON streaks(user_id); +CREATE UNIQUE INDEX IF NOT EXISTS idx_user_streaks_user_id + ON user_streaks(user_id); -- updated_at trigger -CREATE TRIGGER update_streaks_updated_at - BEFORE UPDATE ON streaks +CREATE TRIGGER update_user_streaks_updated_at + BEFORE UPDATE ON user_streaks FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); diff --git a/frontend/types/database.ts b/frontend/types/database.ts index 8d79520..0e82bf9 100644 --- a/frontend/types/database.ts +++ b/frontend/types/database.ts @@ -317,7 +317,7 @@ export interface Database { updated_at?: string } } - streaks: { + user_streaks: { Row: { id: number user_id: string From 6733b917c26d389adaf0487ac200e937dda84fdf Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 11 Jan 2026 21:33:00 +0000 Subject: [PATCH 3/3] Add last_entry_date and last_access_time to user_streaks Co-authored-by: alebiosuf0802 --- frontend/constants/supabase.ts | 2 ++ frontend/services/streak-service.ts | 14 ++++++++------ .../supabase/migrations/20260111000000_streaks.sql | 6 +++++- frontend/types/database.ts | 6 ++++++ 4 files changed, 21 insertions(+), 7 deletions(-) diff --git a/frontend/constants/supabase.ts b/frontend/constants/supabase.ts index f3a63e6..513ad7f 100644 --- a/frontend/constants/supabase.ts +++ b/frontend/constants/supabase.ts @@ -127,6 +127,8 @@ export const SCHEMA = { user_id: 'uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE', current_streak: 'integer NOT NULL DEFAULT 0', max_streak: 'integer NOT NULL DEFAULT 0', + last_entry_date: 'date', + last_access_time: 'timestamptz', created_at: 'timestamptz DEFAULT now()', updated_at: 'timestamptz DEFAULT now()', } diff --git a/frontend/services/streak-service.ts b/frontend/services/streak-service.ts index 742bb2d..f44ace2 100644 --- a/frontend/services/streak-service.ts +++ b/frontend/services/streak-service.ts @@ -41,15 +41,17 @@ export class StreakService { console.error('Failed to load streak data:', error); } - // Fallback to Supabase (single row per user) for cross-device sync of streak stats + // Fallback to Supabase (single row per user) for cross-device sync of streak data try { const { data, error } = await supabase .from('user_streaks') - .select('current_streak, max_streak') + .select('current_streak, max_streak, last_entry_date, last_access_time') .eq('user_id', userId) .maybeSingle<{ current_streak: number | null; max_streak: number | null; + last_entry_date: string | null; + last_access_time: string | null; }>(); if (error) { @@ -58,10 +60,8 @@ export class StreakService { const fromRemote: StreakData = { currentStreak: data.current_streak ?? 0, maxStreak: data.max_streak ?? 0, - // These fields are still tracked locally for streak logic; the Supabase table - // intentionally stores only streak stats (current/max). - lastEntryDate: null, - lastAccessTime: null, + lastEntryDate: data.last_entry_date, + lastAccessTime: data.last_access_time, }; // Cache remotely-loaded data locally (best-effort) @@ -104,6 +104,8 @@ export class StreakService { user_id: userId, current_streak: data.currentStreak, max_streak: data.maxStreak, + last_entry_date: data.lastEntryDate, + last_access_time: data.lastAccessTime, } as never, { onConflict: 'user_id' } as never); if (error) { diff --git a/frontend/supabase/migrations/20260111000000_streaks.sql b/frontend/supabase/migrations/20260111000000_streaks.sql index f3996f7..789b51c 100644 --- a/frontend/supabase/migrations/20260111000000_streaks.sql +++ b/frontend/supabase/migrations/20260111000000_streaks.sql @@ -7,6 +7,8 @@ - `user_id` (uuid, references auth.users) - `current_streak` (integer) - `max_streak` (integer) + - `last_entry_date` (date, nullable) + - `last_access_time` (timestamptz, nullable) - `created_at` (timestamp) - `updated_at` (timestamp) @@ -21,6 +23,8 @@ CREATE TABLE IF NOT EXISTS user_streaks ( user_id uuid NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, current_streak integer NOT NULL DEFAULT 0, max_streak integer NOT NULL DEFAULT 0, + last_entry_date date, + last_access_time timestamptz, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now() ); @@ -58,7 +62,7 @@ CREATE POLICY "Users can update own streak" ON user_streaks FOR UPDATE TO authenticated - USING (auth.uid() = user_id); + USING (auth.uid() = user_id) WITH CHECK (auth.uid() = user_id); CREATE POLICY "Users can delete own streak" diff --git a/frontend/types/database.ts b/frontend/types/database.ts index 0e82bf9..a59b233 100644 --- a/frontend/types/database.ts +++ b/frontend/types/database.ts @@ -323,6 +323,8 @@ export interface Database { user_id: string current_streak: number max_streak: number + last_entry_date: string | null + last_access_time: string | null created_at: string updated_at: string } @@ -331,6 +333,8 @@ export interface Database { user_id: string current_streak?: number max_streak?: number + last_entry_date?: string | null + last_access_time?: string | null created_at?: string updated_at?: string } @@ -339,6 +343,8 @@ export interface Database { user_id?: string current_streak?: number max_streak?: number + last_entry_date?: string | null + last_access_time?: string | null created_at?: string updated_at?: string }