mirror of
https://github.com/harivansh-afk/Habit-Tracker.git
synced 2026-04-20 10:01:27 +00:00
added user auth
This commit is contained in:
parent
d113922158
commit
da9bd83f32
9 changed files with 506 additions and 308 deletions
190
src/App.tsx
190
src/App.tsx
|
|
@ -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 (!user) {
|
||||||
if (!dailyReminder && Notification.permission === 'default') {
|
return authView === 'login' ? (
|
||||||
Notification.requestPermission().then(permission => {
|
<Login onSwitchToSignUp={() => setAuthView('signup')} />
|
||||||
if (permission === 'granted') {
|
) : (
|
||||||
toggleDailyReminder();
|
<SignUp onSwitchToLogin={() => setAuthView('login')} />
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
toggleDailyReminder();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`rounded-lg shadow p-6 ${theme.cardBackground}`}>
|
|
||||||
<h2 className="text-2xl font-bold mb-6 dark:text-white">Settings</h2>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -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
67
src/components/Login.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
120
src/components/SettingsView.tsx
Normal file
120
src/components/SettingsView.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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
67
src/components/SignUp.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
68
src/contexts/AuthContext.tsx
Normal file
68
src/contexts/AuthContext.tsx
Normal 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;
|
||||||
|
};
|
||||||
|
|
@ -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)
|
||||||
|
.eq('user_id', user.id);
|
||||||
|
|
||||||
|
if (deleteError) throw deleteError;
|
||||||
} else {
|
} else {
|
||||||
await supabase
|
const { error: insertError } = await supabase
|
||||||
.from('habit_completions')
|
.from('habit_completions')
|
||||||
.insert([{ habit_id: id, completion_date: date }]);
|
.insert([{
|
||||||
}
|
habit_id: id,
|
||||||
|
completion_date: formattedDate,
|
||||||
|
user_id: user.id
|
||||||
|
}]);
|
||||||
|
|
||||||
// After toggling, recalculate streak
|
if (insertError) throw insertError;
|
||||||
const habit = habits.find(h => h.id === id);
|
|
||||||
if (habit) {
|
|
||||||
const newCompletedDates = existing
|
|
||||||
? habit.completedDates.filter(d => d !== date)
|
|
||||||
: [...habit.completedDates, date];
|
|
||||||
|
|
||||||
const { bestStreak } = calculateStreak(newCompletedDates);
|
|
||||||
|
|
||||||
// Update best_streak if the new streak is higher
|
|
||||||
if (bestStreak > habit.best_streak) {
|
|
||||||
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);
|
||||||
|
|
|
||||||
|
|
@ -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}`;
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue