added user auth

This commit is contained in:
Harivansh Rathi 2024-11-21 14:59:18 -05:00
parent d113922158
commit da9bd83f32
9 changed files with 506 additions and 308 deletions

View file

@ -6,12 +6,20 @@ import { Sidebar } from './components/Sidebar';
import { useHabits } from './hooks/useHabits'; import { useHabits } from './hooks/useHabits';
import { useWeek } from './hooks/useWeek'; import { useWeek } from './hooks/useWeek';
import { ThemeProvider, useThemeContext } from './contexts/ThemeContext'; import { ThemeProvider, useThemeContext } from './contexts/ThemeContext';
import { AuthProvider, useAuth } from './contexts/AuthContext';
import { Login } from './components/Login';
import { SignUp } from './components/SignUp';
import { SettingsView } from './components/SettingsView';
const DAYS_OF_WEEK = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
function HabitTrackerContent() { function HabitTrackerContent() {
const { theme, isDark, toggleDarkMode, defaultView, habitSort } = useThemeContext(); const { theme, isDark, toggleDarkMode, defaultView, habitSort } = useThemeContext();
const [newHabit, setNewHabit] = useState(''); const [newHabit, setNewHabit] = useState('');
const [activeView, setActiveView] = useState<'habits' | 'calendar' | 'settings'>(defaultView); const [activeView, setActiveView] = useState<'habits' | 'calendar' | 'settings'>(defaultView);
const [currentMonth, setCurrentMonth] = useState(new Date()); const [currentMonth, setCurrentMonth] = useState(new Date());
const { user, loading, signOut } = useAuth();
const [authView, setAuthView] = useState<'login' | 'signup'>('login');
const { const {
habits, habits,
@ -19,8 +27,7 @@ function HabitTrackerContent() {
addHabit: addHabitApi, addHabit: addHabitApi,
toggleHabit, toggleHabit,
updateHabit, updateHabit,
deleteHabit, deleteHabit
updateStreak
} = useHabits(); } = useHabits();
const { const {
@ -30,8 +37,6 @@ function HabitTrackerContent() {
changeWeek changeWeek
} = useWeek(); } = useWeek();
const daysOfWeek = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
useEffect(() => { useEffect(() => {
fetchHabits(); fetchHabits();
setCurrentWeek(getCurrentWeekDates()); setCurrentWeek(getCurrentWeekDates());
@ -122,11 +127,10 @@ function HabitTrackerContent() {
<HabitList <HabitList
habits={getSortedHabits()} habits={getSortedHabits()}
currentWeek={currentWeek} currentWeek={currentWeek}
daysOfWeek={daysOfWeek} daysOfWeek={DAYS_OF_WEEK}
onToggleHabit={toggleHabit} onToggleHabit={toggleHabit}
onUpdateHabit={updateHabit} onUpdateHabit={updateHabit}
onDeleteHabit={deleteHabit} onDeleteHabit={deleteHabit}
onUpdateStreak={updateStreak}
/> />
<p className="text-sm text-gray-500 dark:text-gray-300 mt-4">Keep up the good work! Consistency is key.</p> <p className="text-sm text-gray-500 dark:text-gray-300 mt-4">Keep up the good work! Consistency is key.</p>
</div> </div>
@ -147,163 +151,17 @@ function HabitTrackerContent() {
/> />
); );
const renderSettingsView = () => { if (loading) {
const { return <div>Loading...</div>;
theme, }
isDark,
showStreaks,
dailyReminder,
defaultView,
habitSort,
toggleDarkMode,
toggleStreaks,
toggleDailyReminder,
setDefaultView,
setHabitSort
} = useThemeContext();
const handleReminderToggle = () => {
if (!dailyReminder && Notification.permission === 'default') {
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
toggleDailyReminder();
}
});
} else {
toggleDailyReminder();
}
};
return ( if (!user) {
<div className={`rounded-lg shadow p-6 ${theme.cardBackground}`}> return authView === 'login' ? (
<h2 className="text-2xl font-bold mb-6 dark:text-white">Settings</h2> <Login onSwitchToSignUp={() => setAuthView('signup')} />
<div className="space-y-6"> ) : (
<div className="flex items-center justify-between"> <SignUp onSwitchToLogin={() => setAuthView('login')} />
<span className="dark:text-white">Dark Mode</span>
<button
onClick={toggleDarkMode}
className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800"
>
{isDark ? (
<Sun className="h-5 w-5 dark:text-white" />
) : (
<Moon className="h-5 w-5" />
)}
</button>
</div>
<div className="flex items-center justify-between">
<span className="dark:text-white">Show Streaks</span>
<button
onClick={toggleStreaks}
className={`
relative inline-flex h-6 w-11 items-center rounded-full
transition-colors duration-200 ease-in-out
${showStreaks ? 'bg-[#2ecc71]' : 'bg-gray-200 dark:bg-gray-700'}
`}
>
<span
className={`
inline-block h-4 w-4 transform rounded-full bg-white
transition-transform duration-200 ease-in-out
${showStreaks ? 'translate-x-6' : 'translate-x-1'}
`}
/>
</button>
</div>
<div className="flex items-center justify-between">
<div>
<span className="dark:text-white">Daily Reminder</span>
<p className="text-sm text-gray-500 dark:text-gray-400">Get notified at 10:00 AM daily</p>
</div>
<button
onClick={handleReminderToggle}
className={`
relative inline-flex h-6 w-11 items-center rounded-full
transition-colors duration-200 ease-in-out
${dailyReminder ? 'bg-[#2ecc71]' : 'bg-gray-200 dark:bg-gray-700'}
`}
>
<span
className={`
inline-block h-4 w-4 transform rounded-full bg-white
transition-transform duration-200 ease-in-out
${dailyReminder ? 'translate-x-6' : 'translate-x-1'}
`}
/>
</button>
</div>
<div className="flex items-center justify-between">
<div>
<span className="dark:text-white">Default View</span>
<p className="text-sm text-gray-500 dark:text-gray-400">Choose your starting page</p>
</div>
<div className="flex gap-2">
<button
onClick={() => setDefaultView('habits')}
className={`
px-3 py-1.5 rounded-lg text-sm transition-colors duration-200
${defaultView === 'habits'
? 'bg-[#2ecc71] text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
}
`}
>
Habits
</button>
<button
onClick={() => setDefaultView('calendar')}
className={`
px-3 py-1.5 rounded-lg text-sm transition-colors duration-200
${defaultView === 'calendar'
? 'bg-[#2ecc71] text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
}
`}
>
Calendar
</button>
</div>
</div>
<div className="flex items-center justify-between">
<div>
<span className="dark:text-white">Sort Habits</span>
<p className="text-sm text-gray-500 dark:text-gray-400">Choose how to order your habits</p>
</div>
<div className="flex gap-2">
<button
onClick={() => setHabitSort('dateCreated')}
className={`
px-3 py-1.5 rounded-lg text-sm transition-colors duration-200
${habitSort === 'dateCreated'
? 'bg-[#2ecc71] text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
}
`}
>
Date Created
</button>
<button
onClick={() => setHabitSort('alphabetical')}
className={`
px-3 py-1.5 rounded-lg text-sm transition-colors duration-200
${habitSort === 'alphabetical'
? 'bg-[#2ecc71] text-white'
: 'bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300'
}
`}
>
Alphabetical
</button>
</div>
</div>
</div>
</div>
); );
}; }
return ( return (
<div className={`min-h-screen ${theme.background}`}> <div className={`min-h-screen ${theme.background}`}>
@ -312,7 +170,7 @@ function HabitTrackerContent() {
<main className="flex-1 p-8"> <main className="flex-1 p-8">
{activeView === 'habits' && renderHabitsView()} {activeView === 'habits' && renderHabitsView()}
{activeView === 'calendar' && renderCalendarView()} {activeView === 'calendar' && renderCalendarView()}
{activeView === 'settings' && renderSettingsView()} {activeView === 'settings' && <SettingsView />}
</main> </main>
</div> </div>
</div> </div>
@ -321,8 +179,10 @@ function HabitTrackerContent() {
export default function HabitTracker() { export default function HabitTracker() {
return ( return (
<ThemeProvider> <AuthProvider>
<HabitTrackerContent /> <ThemeProvider>
</ThemeProvider> <HabitTrackerContent />
</ThemeProvider>
</AuthProvider>
); );
} }

View file

@ -1,7 +1,8 @@
import React, { useEffect } from 'react'; import React from 'react';
import { Trash2 } from 'lucide-react'; import { Trash2 } from 'lucide-react';
import { Habit } from '../types'; import { Habit } from '../types';
import { useThemeContext } from '../contexts/ThemeContext'; import { useThemeContext } from '../contexts/ThemeContext';
import { calculateStreak } from '../utils/streakCalculator';
interface HabitListProps { interface HabitListProps {
habits: Habit[]; habits: Habit[];
@ -10,83 +11,9 @@ interface HabitListProps {
onToggleHabit: (id: number, date: string) => void; onToggleHabit: (id: number, date: string) => void;
onUpdateHabit: (id: number, name: string) => void; onUpdateHabit: (id: number, name: string) => void;
onDeleteHabit: (id: number) => void; onDeleteHabit: (id: number) => void;
onUpdateStreak: (id: number, newStreak: number) => Promise<void>; onUpdateStreak?: (id: number, newStreak: number) => Promise<void>;
} }
const calculateStreak = (completedDates: string[]): { currentStreak: number; bestStreak: number } => {
if (!completedDates || completedDates.length === 0) {
return { currentStreak: 0, bestStreak: 0 };
}
// Get today at midnight in UTC
const today = new Date();
const utcToday = new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()));
const todayStr = utcToday.toISOString().split('T')[0];
// Sort dates in descending order (most recent first)
const sortedDates = [...completedDates]
.filter(date => !isNaN(new Date(date).getTime()))
.sort((a, b) => new Date(b).getTime() - new Date(a).getTime());
let currentStreak = 0;
let bestStreak = 0;
let tempStreak = 0;
// Check if today is completed to maintain streak
const hasTodayCompleted = sortedDates.includes(todayStr);
// Initialize current streak based on today's completion
if (hasTodayCompleted) {
currentStreak = 1;
let checkDate = new Date(utcToday);
// Check previous days
while (true) {
checkDate.setUTCDate(checkDate.getUTCDate() - 1);
const dateStr = checkDate.toISOString().split('T')[0];
if (sortedDates.includes(dateStr)) {
currentStreak++;
} else {
break;
}
}
}
// Calculate best streak
for (let i = 0; i < sortedDates.length; i++) {
const currentDate = new Date(sortedDates[i]);
currentDate.setHours(0, 0, 0, 0); // Normalize time part for comparison
if (i === 0) {
tempStreak = 1;
} else {
const prevDate = new Date(sortedDates[i - 1]);
const diffDays = Math.floor(
(prevDate.getTime() - currentDate.getTime()) / (1000 * 60 * 60 * 24)
);
if (diffDays === 1) {
tempStreak++;
} else if (diffDays === 0) {
// Same day, skip
continue;
} else {
// Reset streak on gap
bestStreak = Math.max(bestStreak, tempStreak);
tempStreak = 1;
}
}
}
// Final check for best streak
bestStreak = Math.max(bestStreak, tempStreak);
bestStreak = Math.max(bestStreak, currentStreak);
return { currentStreak, bestStreak };
};
export function HabitList({ export function HabitList({
habits, habits,
currentWeek, currentWeek,
@ -94,17 +21,15 @@ export function HabitList({
onToggleHabit, onToggleHabit,
onUpdateHabit, onUpdateHabit,
onDeleteHabit, onDeleteHabit,
onUpdateStreak,
}: HabitListProps) { }: HabitListProps) {
const { showStreaks } = useThemeContext(); const { showStreaks } = useThemeContext();
useEffect(() => { // Helper function to get day name
console.log('Current week dates:', const getDayName = (dateStr: string) => {
currentWeek.map(date => const date = new Date(dateStr);
`${new Date(date).toLocaleDateString()} (${daysOfWeek[new Date(date).getDay() === 0 ? 6 : new Date(date).getDay() - 1]})` const dayIndex = date.getDay() === 0 ? 6 : date.getDay() - 1;
) return daysOfWeek[dayIndex];
); };
}, []);
return ( return (
<div className="overflow-x-auto"> <div className="overflow-x-auto">
@ -112,13 +37,11 @@ export function HabitList({
<thead> <thead>
<tr> <tr>
<th className="text-left px-4 py-2 dark:text-white">Habit</th> <th className="text-left px-4 py-2 dark:text-white">Habit</th>
{currentWeek.map((dateStr, index) => { {currentWeek.map((dateStr) => {
const date = new Date(dateStr); const date = new Date(dateStr);
const dayIndex = date.getDay() === 0 ? 6 : date.getDay() - 1;
return ( return (
<th key={dateStr} className="px-4 py-2 text-center dark:text-white"> <th key={dateStr} className="px-4 py-2 text-center dark:text-white">
<div>{daysOfWeek[dayIndex]}</div> <div>{getDayName(dateStr)}</div>
<div className="text-xs text-gray-500 dark:text-gray-400"> <div className="text-xs text-gray-500 dark:text-gray-400">
{date.getDate()} {date.getDate()}
</div> </div>
@ -155,12 +78,6 @@ export function HabitList({
checked={habit.completedDates.includes(date)} checked={habit.completedDates.includes(date)}
onChange={() => { onChange={() => {
onToggleHabit(habit.id, date); onToggleHabit(habit.id, date);
const newCompletedDates = habit.completedDates.includes(date)
? habit.completedDates.filter(d => d !== date)
: [...habit.completedDates, date];
const { bestStreak } = calculateStreak(newCompletedDates);
onUpdateStreak(habit.id, bestStreak);
}} }}
aria-label={`Mark ${habit.name} as completed for ${date}`} aria-label={`Mark ${habit.name} as completed for ${date}`}
className="sr-only" className="sr-only"

67
src/components/Login.tsx Normal file
View file

@ -0,0 +1,67 @@
import { useState } from 'react';
import { useAuth } from '../contexts/AuthContext';
import { useThemeContext } from '../contexts/ThemeContext';
export function Login({ onSwitchToSignUp }: { onSwitchToSignUp: () => void }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const { signIn } = useAuth();
const { theme } = useThemeContext();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
setError('');
await signIn(email, password);
} catch (error) {
setError('Failed to sign in');
}
};
return (
<div className="min-h-screen flex items-center justify-center">
<div className={`max-w-md w-full p-6 rounded-lg shadow-lg ${theme.cardBackground}`}>
<h2 className={`text-2xl font-bold mb-6 ${theme.text}`}>Log In</h2>
{error && <div className="text-red-500 mb-4">{error}</div>}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
className={`w-full px-4 py-2 rounded-lg ${theme.input}`}
required
/>
</div>
<div>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
className={`w-full px-4 py-2 rounded-lg ${theme.input}`}
required
/>
</div>
<button
type="submit"
className={`w-full py-2 rounded-lg ${theme.button.primary}`}
>
Log In
</button>
</form>
<p className={`mt-4 text-center ${theme.text}`}>
Don't have an account?{' '}
<button
onClick={onSwitchToSignUp}
className="text-blue-500 hover:underline"
>
Sign Up
</button>
</p>
</div>
</div>
);
}

View file

@ -0,0 +1,120 @@
import React from 'react';
import { Sun, Moon } from 'lucide-react';
import { useThemeContext } from '../contexts/ThemeContext';
export function SettingsView() {
const {
theme,
isDark,
showStreaks,
dailyReminder,
defaultView,
habitSort,
toggleDarkMode,
toggleStreaks,
toggleDailyReminder,
setDefaultView,
setHabitSort
} = useThemeContext();
const handleReminderToggle = () => {
if (!dailyReminder && Notification.permission === 'default') {
Notification.requestPermission().then(permission => {
if (permission === 'granted') {
toggleDailyReminder();
}
});
} else {
toggleDailyReminder();
}
};
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>
{/* Default View */}
<div className="mb-6">
<h3 className="text-lg font-medium mb-2 dark:text-white">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"
>
<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>
<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"
>
<option value="dateCreated">Date Created</option>
<option value="alphabetical">Alphabetical</option>
</select>
</div>
</div>
);
}

View file

@ -1,6 +1,7 @@
import React from 'react'; import React from 'react';
import { Plus, CalendarIcon, SettingsIcon } from 'lucide-react'; import { Plus, CalendarIcon, SettingsIcon, LogOut } from 'lucide-react';
import { useThemeContext } from '../contexts/ThemeContext'; import { useThemeContext } from '../contexts/ThemeContext';
import { useAuth } from '../contexts/AuthContext';
type View = 'habits' | 'calendar' | 'settings'; type View = 'habits' | 'calendar' | 'settings';
@ -11,13 +12,14 @@ interface SidebarProps {
export const Sidebar: React.FC<SidebarProps> = ({ activeView, setActiveView }) => { export const Sidebar: React.FC<SidebarProps> = ({ activeView, setActiveView }) => {
const { theme } = useThemeContext(); const { theme } = useThemeContext();
const { signOut } = useAuth();
return ( return (
<nav className={`w-64 border-r ${theme.border} ${theme.sidebarBackground}`}> <nav className={`w-64 border-r ${theme.border} ${theme.sidebarBackground} flex flex-col`}>
<div className="p-4"> <div className="p-4">
<h1 className={`text-2xl font-bold ${theme.text}`}>Habit Tracker</h1> <h1 className={`text-2xl font-bold ${theme.text}`}>Habit Tracker</h1>
</div> </div>
<ul className="space-y-2 p-4"> <ul className="space-y-2 p-4 flex-grow">
<li> <li>
<button <button
onClick={() => setActiveView('habits')} onClick={() => setActiveView('habits')}
@ -58,6 +60,15 @@ export const Sidebar: React.FC<SidebarProps> = ({ activeView, setActiveView }) =
</button> </button>
</li> </li>
</ul> </ul>
<div className="p-4 border-t border-gray-200">
<button
onClick={signOut}
className={`w-full px-4 py-2 text-left rounded-lg ${theme.text} ${theme.habitItem}`}
>
<LogOut className="inline-block mr-2 h-4 w-4" />
Sign Out
</button>
</div>
</nav> </nav>
); );
}; };

67
src/components/SignUp.tsx Normal file
View file

@ -0,0 +1,67 @@
import { useState } from 'react';
import { useAuth } from '../contexts/AuthContext';
import { useThemeContext } from '../contexts/ThemeContext';
export function SignUp({ onSwitchToLogin }: { onSwitchToLogin: () => void }) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const { signUp } = useAuth();
const { theme } = useThemeContext();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
try {
setError('');
await signUp(email, password);
} catch (error) {
setError('Failed to create an account');
}
};
return (
<div className="min-h-screen flex items-center justify-center">
<div className={`max-w-md w-full p-6 rounded-lg shadow-lg ${theme.cardBackground}`}>
<h2 className={`text-2xl font-bold mb-6 ${theme.text}`}>Sign Up</h2>
{error && <div className="text-red-500 mb-4">{error}</div>}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Email"
className={`w-full px-4 py-2 rounded-lg ${theme.input}`}
required
/>
</div>
<div>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="Password"
className={`w-full px-4 py-2 rounded-lg ${theme.input}`}
required
/>
</div>
<button
type="submit"
className={`w-full py-2 rounded-lg ${theme.button.primary}`}
>
Sign Up
</button>
</form>
<p className={`mt-4 text-center ${theme.text}`}>
Already have an account?{' '}
<button
onClick={onSwitchToLogin}
className="text-blue-500 hover:underline"
>
Log In
</button>
</p>
</div>
</div>
);
}

View file

@ -0,0 +1,68 @@
import { createContext, useContext, useEffect, useState } from 'react';
import { User } from '@supabase/supabase-js';
import { supabase } from '../lib/supabase';
interface AuthContextType {
user: User | null;
loading: boolean;
signUp: (email: string, password: string) => Promise<void>;
signIn: (email: string, password: string) => Promise<void>;
signOut: () => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// Check active sessions and sets the user
supabase.auth.getSession().then(({ data: { session } }) => {
setUser(session?.user ?? null);
setLoading(false);
});
// Listen for auth changes
const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => {
setUser(session?.user ?? null);
});
return () => subscription.unsubscribe();
}, []);
const signUp = async (email: string, password: string) => {
const { error } = await supabase.auth.signUp({
email,
password,
});
if (error) throw error;
};
const signIn = async (email: string, password: string) => {
const { error } = await supabase.auth.signInWithPassword({
email,
password,
});
if (error) throw error;
};
const signOut = async () => {
const { error } = await supabase.auth.signOut();
if (error) throw error;
};
return (
<AuthContext.Provider value={{ user, loading, signUp, signIn, signOut }}>
{children}
</AuthContext.Provider>
);
}
export const useAuth = () => {
const context = useContext(AuthContext);
if (context === undefined) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};

View file

@ -1,12 +1,28 @@
import { useState } from 'react'; import { useState, useEffect } from 'react';
import { supabase } from '../lib/supabase'; import { supabase } from '../lib/supabase';
import { Habit } from '../types'; import { Habit } from '../types';
import { calculateStreak } from '../utils/streakCalculator'; import { calculateStreak } from '../utils/streakCalculator';
import { useAuth } from '../contexts/AuthContext';
export const useHabits = () => { export const useHabits = () => {
const [habits, setHabits] = useState<Habit[]>([]); const [habits, setHabits] = useState<Habit[]>([]);
const { user } = useAuth();
// Automatically fetch habits when user changes
useEffect(() => {
if (user) {
fetchHabits();
} else {
setHabits([]); // Clear habits when user logs out
}
}, [user?.id]); // Depend on user.id to prevent unnecessary rerenders
const fetchHabits = async () => { const fetchHabits = async () => {
if (!user) {
setHabits([]);
return;
}
try { try {
const { data, error } = await supabase const { data, error } = await supabase
.from('habits') .from('habits')
@ -15,19 +31,23 @@ export const useHabits = () => {
name, name,
created_at, created_at,
best_streak, best_streak,
habit_completions (completion_date) habit_completions (
`); completion_date
)
`)
.eq('user_id', user.id)
.order('created_at', { ascending: false });
if (error) throw error; if (error) throw error;
const formattedHabits = data.map(habit => ({ const formattedHabits = (data || []).map(habit => ({
id: habit.id, id: habit.id,
name: habit.name, name: habit.name,
created_at: habit.created_at, created_at: habit.created_at,
best_streak: habit.best_streak, best_streak: habit.best_streak,
completedDates: habit.habit_completions.map( completedDates: habit.habit_completions?.map(
(completion: { completion_date: string }) => completion.completion_date (completion: { completion_date: string }) => completion.completion_date
) ) || []
})); }));
setHabits(formattedHabits); setHabits(formattedHabits);
@ -38,18 +58,22 @@ export const useHabits = () => {
}; };
const addHabit = async (name: string) => { const addHabit = async (name: string) => {
if (!user) return false;
try { try {
const { error } = await supabase const { data, error } = await supabase
.from('habits') .from('habits')
.insert([{ .insert([{
name, name,
best_streak: 0, best_streak: 0,
created_at: new Date().toISOString() created_at: new Date().toISOString(),
user_id: user.id
}]) }])
.select() .select('id')
.single(); .single();
if (error) throw error; if (error) throw error;
await fetchHabits(); await fetchHabits();
return true; return true;
} catch (error) { } catch (error) {
@ -59,56 +83,89 @@ export const useHabits = () => {
}; };
const toggleHabit = async (id: number, date: string) => { const toggleHabit = async (id: number, date: string) => {
if (!user) return;
try { try {
const { data: existing } = await supabase // First verify this habit belongs to the user
.from('habit_completions') const { data: habitData, error: habitError } = await supabase
.select() .from('habits')
.eq('habit_id', id) .select('id')
.eq('completion_date', date) .eq('id', id)
.eq('user_id', user.id)
.single(); .single();
if (habitError || !habitData) {
throw new Error('Unauthorized access to habit');
}
// Keep the original date string without any timezone conversion
const formattedDate = date;
console.log('Toggling habit:', { id, date, formattedDate });
// Check for existing completion
const { data: existing, error: existingError } = await supabase
.from('habit_completions')
.select('*')
.eq('habit_id', id)
.eq('completion_date', formattedDate)
.eq('user_id', user.id)
.single();
if (existingError && existingError.code !== 'PGRST116') {
throw existingError;
}
if (existing) { if (existing) {
await supabase const { error: deleteError } = await supabase
.from('habit_completions') .from('habit_completions')
.delete() .delete()
.eq('habit_id', id) .eq('habit_id', id)
.eq('completion_date', date); .eq('completion_date', formattedDate)
} else { .eq('user_id', user.id);
await supabase
.from('habit_completions')
.insert([{ habit_id: id, completion_date: date }]);
}
// After toggling, recalculate streak if (deleteError) throw deleteError;
const habit = habits.find(h => h.id === id); } else {
if (habit) { const { error: insertError } = await supabase
const newCompletedDates = existing .from('habit_completions')
? habit.completedDates.filter(d => d !== date) .insert([{
: [...habit.completedDates, date]; habit_id: id,
completion_date: formattedDate,
const { bestStreak } = calculateStreak(newCompletedDates); user_id: user.id
}]);
// Update best_streak if the new streak is higher
if (bestStreak > habit.best_streak) { if (insertError) throw insertError;
await supabase
.from('habits')
.update({ best_streak: bestStreak })
.eq('id', id);
}
} }
await fetchHabits(); await fetchHabits();
} catch (error) { } catch (error) {
console.error('Error toggling habit:', error); console.error('Error toggling habit:', error);
throw error;
} }
}; };
const updateHabit = async (id: number, name: string) => { const updateHabit = async (id: number, name: string) => {
if (!user) return;
try { try {
// First verify this habit belongs to the user
const { data: habitData, error: habitError } = await supabase
.from('habits')
.select('id')
.eq('id', id)
.eq('user_id', user.id)
.single();
if (habitError || !habitData) {
throw new Error('Unauthorized access to habit');
}
await supabase await supabase
.from('habits') .from('habits')
.update({ name }) .update({ name })
.eq('id', id); .eq('id', id)
.eq('user_id', user.id);
await fetchHabits(); await fetchHabits();
} catch (error) { } catch (error) {
console.error('Error updating habit:', error); console.error('Error updating habit:', error);
@ -116,11 +173,27 @@ export const useHabits = () => {
}; };
const deleteHabit = async (id: number) => { const deleteHabit = async (id: number) => {
if (!user) return;
try { try {
// First verify this habit belongs to the user
const { data: habitData, error: habitError } = await supabase
.from('habits')
.select('id')
.eq('id', id)
.eq('user_id', user.id)
.single();
if (habitError || !habitData) {
throw new Error('Unauthorized access to habit');
}
await supabase await supabase
.from('habits') .from('habits')
.delete() .delete()
.eq('id', id); .eq('id', id)
.eq('user_id', user.id);
await fetchHabits(); await fetchHabits();
} catch (error) { } catch (error) {
console.error('Error deleting habit:', error); console.error('Error deleting habit:', error);

View file

@ -5,23 +5,38 @@ export const useWeek = () => {
const getCurrentWeekDates = () => { const getCurrentWeekDates = () => {
const now = new Date(); const now = new Date();
const dayOfWeek = now.getDay(); const currentDay = now.getDay();
const diff = now.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1); const diff = currentDay === 0 ? -6 : 1 - currentDay;
const monday = new Date(now.setDate(diff));
const monday = new Date(now);
monday.setDate(now.getDate() + diff);
monday.setHours(0, 0, 0, 0);
return Array.from({ length: 7 }, (_, i) => { return Array.from({ length: 7 }, (_, i) => {
const date = new Date(monday); const date = new Date(monday);
date.setDate(monday.getDate() + i); date.setDate(monday.getDate() + i);
return date.toISOString().split('T')[0]; const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}); });
}; };
const changeWeek = (direction: 'prev' | 'next') => { const changeWeek = (direction: 'prev' | 'next') => {
if (currentWeek.length === 0) return;
const firstDay = new Date(currentWeek[0]); const firstDay = new Date(currentWeek[0]);
const newFirstDay = new Date(firstDay.setDate(firstDay.getDate() + (direction === 'prev' ? -7 : 7))); firstDay.setHours(0, 0, 0, 0);
const newFirstDay = new Date(firstDay);
newFirstDay.setDate(firstDay.getDate() + (direction === 'prev' ? -7 : 7));
setCurrentWeek(Array.from({ length: 7 }, (_, i) => { setCurrentWeek(Array.from({ length: 7 }, (_, i) => {
const date = new Date(newFirstDay); const date = new Date(newFirstDay);
date.setDate(newFirstDay.getDate() + i); date.setDate(newFirstDay.getDate() + i);
return date.toISOString().split('T')[0]; const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
})); }));
}; };