user settings debugged

This commit is contained in:
Harivansh Rathi 2024-11-21 16:13:24 -05:00
parent 1cd3445bd6
commit 20e91dae4c
7 changed files with 282 additions and 224 deletions

View file

@ -11,6 +11,7 @@ import { Login } from './components/Login';
import { SignUp } from './components/SignUp';
import { SettingsView } from './components/SettingsView';
import { MobileNav } from './components/MobileNav';
import { PreferencesProvider, usePreferences } from './contexts/PreferencesContext';
const DAYS_OF_WEEK = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
@ -21,6 +22,7 @@ function HabitTrackerContent() {
const [currentMonth, setCurrentMonth] = useState(new Date());
const { user, loading, signOut } = useAuth();
const [authView, setAuthView] = useState<'login' | 'signup'>('login');
const { preferences } = usePreferences();
const {
habits,
@ -43,6 +45,12 @@ function HabitTrackerContent() {
setCurrentWeek(getCurrentWeekDates());
}, []);
useEffect(() => {
if (preferences?.default_view) {
setActiveView(preferences.default_view);
}
}, [preferences?.default_view]);
const handleAddHabit = async (e: React.FormEvent) => {
e.preventDefault();
if (newHabit.trim()) {
@ -196,9 +204,11 @@ function HabitTrackerContent() {
export default function HabitTracker() {
return (
<AuthProvider>
<ThemeProvider>
<HabitTrackerContent />
</ThemeProvider>
<PreferencesProvider>
<ThemeProvider>
<HabitTrackerContent />
</ThemeProvider>
</PreferencesProvider>
</AuthProvider>
);
}

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useMemo } from 'react';
import { Trash2 } from 'lucide-react';
import { Habit } from '../types';
import { useThemeContext } from '../contexts/ThemeContext';
@ -22,7 +22,15 @@ export function HabitList({
onUpdateHabit,
onDeleteHabit,
}: HabitListProps) {
const { showStreaks } = useThemeContext();
const { habitSort, showStreaks } = useThemeContext();
const sortedHabits = useMemo(() => {
if (habitSort === 'alphabetical') {
return [...habits].sort((a, b) => a.name.localeCompare(b.name));
}
// Default to dateCreated sort
return habits;
}, [habits, habitSort]);
// Helper function to get day name
const getDayName = (dateStr: string) => {
@ -53,7 +61,7 @@ export function HabitList({
</tr>
</thead>
<tbody>
{habits.map((habit) => (
{sortedHabits.map((habit) => (
<tr key={habit.id} className="border-t dark:border-gray-700">
<td className="px-4 py-2 dark:text-white">
<input
@ -106,6 +114,13 @@ export function HabitList({
<Trash2 className="h-4 w-4" />
</button>
</td>
{showStreaks && (
<>
<td className="px-4 py-2 text-center">
{/* ... streak content ... */}
</td>
</>
)}
</tr>
))}
</tbody>

View file

@ -1,120 +1,95 @@
import React from 'react';
import { Sun, Moon } from 'lucide-react';
import { usePreferences } from '../contexts/PreferencesContext';
import { useThemeContext } from '../contexts/ThemeContext';
export function SettingsView() {
const {
theme,
isDark,
showStreaks,
dailyReminder,
defaultView,
habitSort,
toggleDarkMode,
toggleStreaks,
toggleDailyReminder,
setDefaultView,
setHabitSort
} = useThemeContext();
const { preferences, updatePreferences } = usePreferences();
const { theme } = useThemeContext();
const handleReminderToggle = () => {
if (!dailyReminder && Notification.permission === 'default') {
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
toggleDailyReminder();
}
});
} else {
toggleDailyReminder();
}
const handleThemeChange = async (newTheme: 'light' | 'dark') => {
await updatePreferences({ theme: newTheme });
// Theme will be updated automatically through ThemeContext
};
const handleSortChange = async (newSort: 'dateCreated' | 'alphabetical') => {
await updatePreferences({ habit_sort: newSort });
// Sort will be updated automatically through ThemeContext
};
const handleStreaksChange = async (showStreaks: boolean) => {
await updatePreferences({ show_streaks: showStreaks });
// Streaks visibility will be updated automatically through ThemeContext
};
return (
<div className={`rounded-lg shadow p-6 ${theme.cardBackground}`}>
<h2 className="text-xl font-semibold mb-6 dark:text-white">Settings</h2>
{/* Theme Toggle */}
<div className="mb-6">
<h3 className="text-lg font-medium mb-2 dark:text-white">Theme</h3>
<button
onClick={toggleDarkMode}
className={`flex items-center space-x-2 px-4 py-2 rounded-lg transition-colors duration-200
${isDark
? 'bg-gray-700 text-white hover:bg-gray-600'
: 'bg-gray-100 text-gray-900 hover:bg-gray-200'}`}
>
{isDark ? <Moon className="h-5 w-5" /> : <Sun className="h-5 w-5" />}
<span>{isDark ? 'Dark Mode' : 'Light Mode'}</span>
</button>
</div>
{/* Streaks Toggle */}
<div className="mb-6">
<h3 className="text-lg font-medium mb-2 dark:text-white">Streaks</h3>
<label className="flex items-center cursor-pointer">
<div className="relative">
<input
type="checkbox"
className="sr-only"
checked={showStreaks}
onChange={toggleStreaks}
/>
<div className={`w-10 h-6 rounded-full transition-colors duration-200
${showStreaks ? 'bg-green-500' : 'bg-gray-300'}`}>
<div className={`absolute w-4 h-4 rounded-full bg-white transition-transform duration-200 transform
${showStreaks ? 'translate-x-5' : 'translate-x-1'} top-1`} />
</div>
</div>
<span className="ml-3 dark:text-white">Show Streaks</span>
</label>
</div>
{/* Daily Reminder Toggle */}
<div className="mb-6">
<h3 className="text-lg font-medium mb-2 dark:text-white">Notifications</h3>
<label className="flex items-center cursor-pointer">
<div className="relative">
<input
type="checkbox"
className="sr-only"
checked={dailyReminder}
onChange={handleReminderToggle}
/>
<div className={`w-10 h-6 rounded-full transition-colors duration-200
${dailyReminder ? 'bg-green-500' : 'bg-gray-300'}`}>
<div className={`absolute w-4 h-4 rounded-full bg-white transition-transform duration-200 transform
${dailyReminder ? 'translate-x-5' : 'translate-x-1'} top-1`} />
</div>
</div>
<span className="ml-3 dark:text-white">Daily Reminder</span>
</label>
</div>
<div className="max-w-2xl mx-auto space-y-8 p-4">
{/* Default View */}
<div className="mb-6">
<h3 className="text-lg font-medium mb-2 dark:text-white">Default View</h3>
<div className="space-y-2">
<h3 className={`text-lg font-medium ${theme.text}`}>Default View</h3>
<select
value={defaultView}
onChange={(e) => setDefaultView(e.target.value as 'habits' | 'calendar')}
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 sm:text-sm rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white"
value={preferences.default_view}
onChange={(e) => updatePreferences({ default_view: e.target.value as 'habits' | 'calendar' })}
className={`w-full p-2 rounded-lg border ${theme.input}`}
>
<option value="habits">Habits</option>
<option value="calendar">Calendar</option>
</select>
</div>
{/* Habit Sort */}
<div className="mb-6">
<h3 className="text-lg font-medium mb-2 dark:text-white">Sort Habits By</h3>
{/* Sort Habits */}
<div className="space-y-2">
<h3 className={`text-lg font-medium ${theme.text}`}>Sort Habits By</h3>
<select
value={habitSort}
onChange={(e) => setHabitSort(e.target.value as 'dateCreated' | 'alphabetical')}
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 sm:text-sm rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white"
value={preferences.habit_sort}
onChange={(e) => handleSortChange(e.target.value as 'dateCreated' | 'alphabetical')}
className={`w-full p-2 rounded-lg border ${theme.input}`}
>
<option value="dateCreated">Date Created</option>
<option value="alphabetical">Alphabetical</option>
</select>
</div>
{/* Show Streaks */}
<div className="space-y-2">
<h3 className={`text-lg font-medium ${theme.text}`}>Show Streaks</h3>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={preferences.show_streaks}
onChange={(e) => handleStreaksChange(e.target.checked)}
className="form-checkbox h-5 w-5 text-green-500"
/>
<span className={theme.text}>Enable streak counting</span>
</label>
</div>
{/* Daily Reminder */}
<div className="space-y-2">
<h3 className={`text-lg font-medium ${theme.text}`}>Daily Reminder</h3>
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={preferences.daily_reminder}
onChange={(e) => updatePreferences({ daily_reminder: e.target.checked })}
className="form-checkbox h-5 w-5 text-green-500"
/>
<span className={theme.text}>Enable daily reminders</span>
</label>
</div>
{/* Theme */}
<div className="space-y-2">
<h3 className={`text-lg font-medium ${theme.text}`}>Theme</h3>
<select
value={preferences.theme}
onChange={(e) => handleThemeChange(e.target.value as 'light' | 'dark')}
className={`w-full p-2 rounded-lg border ${theme.input}`}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</div>
</div>
);
}

View file

@ -0,0 +1,143 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { supabase } from '../lib/supabase';
import { UserPreferences, PreferenceUpdate } from '../types/preferences';
import { useAuth } from './AuthContext';
interface PreferencesContextType {
preferences: UserPreferences | null;
loading: boolean;
error: string | null;
updatePreferences: (updates: PreferenceUpdate) => Promise<void>;
retryFetch: () => Promise<void>;
}
const PreferencesContext = createContext<PreferencesContextType | undefined>(undefined);
export function PreferencesProvider({ children }: { children: React.ReactNode }) {
const [preferences, setPreferences] = useState<UserPreferences | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const { user } = useAuth();
const fetchPreferences = async () => {
if (!user) {
setLoading(false);
return;
}
try {
setLoading(true);
setError(null);
// Log the user and connection status
console.log('Current user:', user);
console.log('Supabase connection:', supabase);
// First, try to fetch existing preferences
const { data, error: fetchError } = await supabase
.from('user_preferences')
.select('*')
.eq('user_id', user.id)
.single();
if (fetchError) {
console.error('Fetch error:', fetchError);
// If the error is that no rows were found, create default preferences
if (fetchError.code === 'PGRST116') {
const defaultPreferences = {
user_id: user.id,
show_streaks: true,
daily_reminder: false,
default_view: 'habits' as const,
habit_sort: 'dateCreated' as const,
theme: 'light' as const
};
console.log('Creating default preferences:', defaultPreferences);
const { data: newData, error: insertError } = await supabase
.from('user_preferences')
.insert([defaultPreferences])
.select()
.single();
if (insertError) {
console.error('Insert error:', insertError);
throw insertError;
}
console.log('Created preferences:', newData);
setPreferences(newData);
return;
}
throw fetchError;
}
console.log('Fetched preferences:', data);
setPreferences(data);
} catch (err) {
console.error('Error in fetchPreferences:', err);
setError(err instanceof Error ? err.message : 'Failed to fetch preferences');
} finally {
setLoading(false);
}
};
const updatePreferences = async (updates: PreferenceUpdate) => {
if (!user || !preferences) return;
try {
console.log('Updating preferences:', updates);
const { data, error } = await supabase
.from('user_preferences')
.update(updates)
.eq('user_id', user.id)
.select()
.single();
if (error) {
console.error('Update error:', error);
throw error;
}
console.log('Updated preferences:', data);
setPreferences(data);
} catch (err) {
console.error('Error in updatePreferences:', err);
setError(err instanceof Error ? err.message : 'Failed to update preferences');
throw err;
}
};
const retryFetch = async () => {
await fetchPreferences();
};
useEffect(() => {
fetchPreferences();
}, [user]);
return (
<PreferencesContext.Provider value={{
preferences,
loading,
error,
updatePreferences,
retryFetch
}}>
{children}
</PreferencesContext.Provider>
);
}
export function usePreferences() {
const context = useContext(PreferencesContext);
if (context === undefined) {
throw new Error('usePreferences must be used within a PreferencesProvider');
}
return context;
}

View file

@ -1,149 +1,47 @@
import React, { createContext, useContext, useState, useEffect } from 'react';
import { Theme, useTheme } from '../styles/theme';
type HabitSortOption = 'dateCreated' | 'alphabetical';
import React, { createContext, useContext, useEffect } from 'react';
import { lightTheme, darkTheme } from '../styles/theme';
import { usePreferences } from './PreferencesContext';
interface ThemeContextType {
theme: Theme;
isDark: boolean;
theme: typeof lightTheme;
showStreaks: boolean;
dailyReminder: boolean;
defaultView: 'habits' | 'calendar';
habitSort: HabitSortOption;
toggleDarkMode: () => void;
toggleStreaks: () => void;
toggleDailyReminder: () => void;
setDefaultView: (view: 'habits' | 'calendar') => void;
setHabitSort: (sort: HabitSortOption) => void;
habitSort: 'dateCreated' | 'alphabetical';
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [isDark, setIsDark] = useState(() => localStorage.getItem('darkMode') === 'true');
const [showStreaks, setShowStreaks] = useState(() => localStorage.getItem('showStreaks') !== 'false');
const [dailyReminder, setDailyReminder] = useState(() => localStorage.getItem('dailyReminder') === 'true');
const [defaultView, setDefaultView] = useState<'habits' | 'calendar'>(() =>
(localStorage.getItem('defaultView') as 'habits' | 'calendar') || 'habits'
);
const [habitSort, setHabitSort] = useState<HabitSortOption>(() =>
(localStorage.getItem('habitSort') as HabitSortOption) || 'dateCreated'
);
const theme = useTheme(isDark);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const { preferences, updatePreferences } = usePreferences();
// Use preferences or fallback to defaults
const currentTheme = preferences?.theme === 'dark' ? darkTheme : lightTheme;
const showStreaks = preferences?.show_streaks ?? true;
const habitSort = preferences?.habit_sort ?? 'dateCreated';
useEffect(() => {
if (isDark) {
// Apply theme to document
if (preferences?.theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
localStorage.setItem('darkMode', isDark.toString());
}, [isDark]);
useEffect(() => {
localStorage.setItem('showStreaks', showStreaks.toString());
}, [showStreaks]);
useEffect(() => {
if (dailyReminder) {
// Request notification permission if not granted
if (Notification.permission !== 'granted') {
Notification.requestPermission();
}
// Schedule notification for 10am
const now = new Date();
const scheduledTime = new Date(
now.getFullYear(),
now.getMonth(),
now.getDate(),
10, // 10am
0,
0
);
// If it's past 10am, schedule for tomorrow
if (now > scheduledTime) {
scheduledTime.setDate(scheduledTime.getDate() + 1);
}
const timeUntilNotification = scheduledTime.getTime() - now.getTime();
const timer = setTimeout(() => {
if (Notification.permission === 'granted') {
new Notification('Habit Tracker Reminder', {
body: "Don't forget to track your habits for today!",
icon: '/favicon.ico' // Add your app's icon path here
});
// Schedule next day's notification
const nextDay = new Date(scheduledTime);
nextDay.setDate(nextDay.getDate() + 1);
const nextTimeUntilNotification = nextDay.getTime() - new Date().getTime();
setTimeout(() => {
if (dailyReminder) {
new Notification('Habit Tracker Reminder', {
body: "Don't forget to track your habits for today!",
icon: '/favicon.ico'
});
}
}, nextTimeUntilNotification);
}
}, timeUntilNotification);
return () => clearTimeout(timer);
}
}, [dailyReminder]);
const toggleDarkMode = () => setIsDark(!isDark);
const toggleStreaks = () => setShowStreaks(!showStreaks);
const toggleDailyReminder = () => {
if (!dailyReminder && Notification.permission !== 'granted') {
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
setDailyReminder(true);
localStorage.setItem('dailyReminder', 'true');
}
});
} else {
setDailyReminder(!dailyReminder);
localStorage.setItem('dailyReminder', (!dailyReminder).toString());
}
};
const handleSetDefaultView = (view: 'habits' | 'calendar') => {
setDefaultView(view);
localStorage.setItem('defaultView', view);
};
const handleSetHabitSort = (sort: HabitSortOption) => {
setHabitSort(sort);
localStorage.setItem('habitSort', sort);
};
}, [preferences?.theme]);
return (
<ThemeContext.Provider value={{
theme,
isDark,
theme: currentTheme,
showStreaks,
dailyReminder,
defaultView,
habitSort,
toggleDarkMode,
toggleStreaks,
toggleDailyReminder,
setDefaultView: handleSetDefaultView,
setHabitSort: handleSetHabitSort
habitSort
}}>
{children}
</ThemeContext.Provider>
);
};
}
export const useThemeContext = () => {
export function useThemeContext() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useThemeContext must be used within a ThemeProvider');
}
return context;
};
}

View file

@ -1,5 +1,4 @@
import { createClient } from '@supabase/supabase-js';
import { Database } from '../types/supabase';
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
@ -8,4 +7,9 @@ if (!supabaseUrl || !supabaseAnonKey) {
throw new Error('Missing Supabase environment variables');
}
export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey);
export const supabase = createClient(supabaseUrl, supabaseAnonKey, {
auth: {
persistSession: true,
autoRefreshToken: true,
}
});

13
src/types/preferences.ts Normal file
View file

@ -0,0 +1,13 @@
export interface UserPreferences {
id: string;
user_id: string;
show_streaks: boolean;
daily_reminder: boolean;
default_view: 'habits' | 'calendar';
habit_sort: 'dateCreated' | 'alphabetical';
theme: 'light' | 'dark';
created_at: string;
updated_at: string;
}
export type PreferenceUpdate = Partial<Omit<UserPreferences, 'id' | 'user_id' | 'created_at' | 'updated_at'>>;