mirror of
https://github.com/harivansh-afk/Habit-Tracker.git
synced 2026-04-19 07:03:46 +00:00
UI updates and bug fixes
This commit is contained in:
parent
20e91dae4c
commit
94c26c6b50
5 changed files with 274 additions and 244 deletions
84
src/App.tsx
84
src/App.tsx
|
|
@ -30,7 +30,8 @@ function HabitTrackerContent() {
|
||||||
addHabit: addHabitApi,
|
addHabit: addHabitApi,
|
||||||
toggleHabit,
|
toggleHabit,
|
||||||
updateHabit,
|
updateHabit,
|
||||||
deleteHabit
|
deleteHabit,
|
||||||
|
error
|
||||||
} = useHabits();
|
} = useHabits();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
@ -88,26 +89,23 @@ function HabitTrackerContent() {
|
||||||
const renderHabitsView = () => (
|
const renderHabitsView = () => (
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="max-w-5xl mx-auto">
|
<div className="max-w-5xl mx-auto">
|
||||||
<div className="mb-8 flex items-center gap-4">
|
<div className="mb-8">
|
||||||
<input
|
<form onSubmit={handleAddHabit} className="flex items-center gap-4">
|
||||||
type="text"
|
<input
|
||||||
value={newHabit}
|
type="text"
|
||||||
onChange={(e) => setNewHabit(e.target.value)}
|
value={newHabit}
|
||||||
onKeyPress={(e) => {
|
onChange={(e) => setNewHabit(e.target.value)}
|
||||||
if (e.key === 'Enter' && newHabit.trim()) {
|
placeholder="Add a new habit"
|
||||||
handleAddHabit(e);
|
className={`flex-1 px-4 py-2 rounded-lg ${theme.input}`}
|
||||||
}
|
/>
|
||||||
}}
|
<button
|
||||||
placeholder="Add a new habit"
|
type="submit"
|
||||||
className={`flex-1 px-4 py-2 rounded-lg ${theme.input}`}
|
disabled={!newHabit.trim()}
|
||||||
/>
|
className={`px-4 py-2 rounded-lg ${theme.button.primary} disabled:opacity-50`}
|
||||||
<button
|
>
|
||||||
onClick={handleAddHabit}
|
Add Habit
|
||||||
disabled={!newHabit.trim()}
|
</button>
|
||||||
className={`px-4 py-2 rounded-lg ${theme.button.primary} disabled:opacity-50`}
|
</form>
|
||||||
>
|
|
||||||
Add Habit
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-6">
|
<div className="mb-6">
|
||||||
|
|
@ -124,9 +122,7 @@ function HabitTrackerContent() {
|
||||||
<ChevronLeft className="h-5 w-5" />
|
<ChevronLeft className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => {
|
onClick={goToCurrentWeek}
|
||||||
setCurrentWeek(getCurrentWeekDates());
|
|
||||||
}}
|
|
||||||
className={`px-4 py-2 rounded-lg ${theme.button.secondary}`}
|
className={`px-4 py-2 rounded-lg ${theme.button.secondary}`}
|
||||||
>
|
>
|
||||||
Today
|
Today
|
||||||
|
|
@ -141,17 +137,35 @@ function HabitTrackerContent() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<HabitList
|
{loading ? (
|
||||||
habits={getSortedHabits()}
|
<div className="flex justify-center items-center py-8">
|
||||||
currentWeek={currentWeek}
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900 dark:border-white"></div>
|
||||||
daysOfWeek={DAYS_OF_WEEK}
|
</div>
|
||||||
onToggleHabit={toggleHabit}
|
) : error ? (
|
||||||
onUpdateHabit={updateHabit}
|
<div className="text-red-500 text-center py-4">
|
||||||
onDeleteHabit={deleteHabit}
|
{error}
|
||||||
/>
|
<button
|
||||||
<p className={`text-sm ${theme.mutedText} mt-4`}>
|
onClick={fetchHabits}
|
||||||
Keep up the good work! Consistency is key.
|
className="ml-2 text-blue-500 hover:underline"
|
||||||
</p>
|
>
|
||||||
|
Retry
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<HabitList
|
||||||
|
habits={getSortedHabits()}
|
||||||
|
currentWeek={currentWeek}
|
||||||
|
daysOfWeek={DAYS_OF_WEEK}
|
||||||
|
onToggleHabit={toggleHabit}
|
||||||
|
onUpdateHabit={updateHabit}
|
||||||
|
onDeleteHabit={deleteHabit}
|
||||||
|
/>
|
||||||
|
<p className={`text-sm ${theme.mutedText} mt-4`}>
|
||||||
|
Keep up the good work! Consistency is key.
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,15 @@ 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';
|
import { calculateStreak } from '../utils/streakCalculator';
|
||||||
|
import { getDayIndex } from '../utils/dateUtils';
|
||||||
|
|
||||||
interface HabitListProps {
|
interface HabitListProps {
|
||||||
habits: Habit[];
|
habits: Habit[];
|
||||||
currentWeek: string[];
|
currentWeek: string[];
|
||||||
daysOfWeek: string[];
|
daysOfWeek: string[];
|
||||||
onToggleHabit: (id: number, date: string) => void;
|
onToggleHabit: (id: number, date: string) => Promise<void>;
|
||||||
onUpdateHabit: (id: number, name: string) => void;
|
onUpdateHabit: (id: number, name: string) => Promise<void>;
|
||||||
onDeleteHabit: (id: number) => void;
|
onDeleteHabit: (id: number) => Promise<void>;
|
||||||
onUpdateStreak?: (id: number, newStreak: number) => Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function HabitList({
|
export function HabitList({
|
||||||
|
|
@ -22,21 +22,18 @@ export function HabitList({
|
||||||
onUpdateHabit,
|
onUpdateHabit,
|
||||||
onDeleteHabit,
|
onDeleteHabit,
|
||||||
}: HabitListProps) {
|
}: HabitListProps) {
|
||||||
const { habitSort, showStreaks } = useThemeContext();
|
const { theme, habitSort, showStreaks } = useThemeContext();
|
||||||
|
|
||||||
const sortedHabits = useMemo(() => {
|
const sortedHabits = useMemo(() => {
|
||||||
if (habitSort === 'alphabetical') {
|
if (habitSort === 'alphabetical') {
|
||||||
return [...habits].sort((a, b) => a.name.localeCompare(b.name));
|
return [...habits].sort((a, b) => a.name.localeCompare(b.name));
|
||||||
}
|
}
|
||||||
// Default to dateCreated sort
|
|
||||||
return habits;
|
return habits;
|
||||||
}, [habits, habitSort]);
|
}, [habits, habitSort]);
|
||||||
|
|
||||||
// Helper function to get day name
|
|
||||||
const getDayName = (dateStr: string) => {
|
const getDayName = (dateStr: string) => {
|
||||||
const date = new Date(dateStr);
|
const date = new Date(dateStr);
|
||||||
const dayIndex = date.getDay() === 0 ? 6 : date.getDay() - 1;
|
return daysOfWeek[getDayIndex(date)];
|
||||||
return daysOfWeek[dayIndex];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -45,84 +42,88 @@ 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) => {
|
{currentWeek.map((dateStr) => (
|
||||||
const date = new Date(dateStr);
|
<th key={dateStr} className="px-4 py-2 text-center dark:text-white">
|
||||||
return (
|
<div className="hidden md:block">{getDayName(dateStr)}</div>
|
||||||
<th key={dateStr} className="px-4 py-2 text-center dark:text-white">
|
<div className="md:hidden">{getDayName(dateStr).slice(0, 1)}</div>
|
||||||
<div className="hidden md:block">{getDayName(dateStr)}</div>
|
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
<div className="md:hidden">{getDayName(dateStr).slice(0, 1)}</div>
|
{new Date(dateStr).getDate()}
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
</div>
|
||||||
{date.getDate()}
|
</th>
|
||||||
</div>
|
))}
|
||||||
</th>
|
{showStreaks && (
|
||||||
);
|
<th className="px-4 py-2 text-center dark:text-white">Streak</th>
|
||||||
})}
|
)}
|
||||||
<th className="px-4 py-2 text-center dark:text-white">Actions</th>
|
<th className="px-4 py-2 text-center dark:text-white">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{sortedHabits.map((habit) => (
|
{sortedHabits.map((habit) => {
|
||||||
<tr key={habit.id} className="border-t dark:border-gray-700">
|
const { currentStreak } = calculateStreak(habit.completedDates);
|
||||||
<td className="px-4 py-2 dark:text-white">
|
|
||||||
<input
|
return (
|
||||||
type="text"
|
<tr key={habit.id} className="border-t dark:border-gray-700">
|
||||||
value={habit.name}
|
<td className="px-4 py-2 dark:text-white">
|
||||||
onChange={(e) => onUpdateHabit(habit.id, e.target.value)}
|
<input
|
||||||
className="bg-transparent border-none focus:outline-none focus:ring-2 focus:ring-gray-300 rounded px-2 w-full"
|
type="text"
|
||||||
/>
|
value={habit.name}
|
||||||
</td>
|
onChange={(e) => onUpdateHabit(habit.id, e.target.value)}
|
||||||
{currentWeek.map((date) => (
|
className="bg-transparent border-none focus:outline-none focus:ring-2 focus:ring-gray-300 rounded px-2 w-full"
|
||||||
<td key={date} className="px-4 py-2 text-center">
|
/>
|
||||||
<label className="relative inline-block cursor-pointer">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={habit.completedDates.includes(date)}
|
|
||||||
onChange={() => onToggleHabit(habit.id, date)}
|
|
||||||
className="sr-only"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className={`
|
|
||||||
w-6 h-6 rounded-md border-2 transition-all duration-200
|
|
||||||
${habit.completedDates.includes(date)
|
|
||||||
? 'bg-green-500 border-green-500'
|
|
||||||
: 'border-gray-300 dark:border-gray-600 hover:border-green-400 dark:hover:border-green-400'}
|
|
||||||
flex items-center justify-center
|
|
||||||
`}
|
|
||||||
>
|
|
||||||
{habit.completedDates.includes(date) && (
|
|
||||||
<svg
|
|
||||||
className="w-4 h-4 text-white"
|
|
||||||
fill="none"
|
|
||||||
strokeLinecap="round"
|
|
||||||
strokeLinejoin="round"
|
|
||||||
strokeWidth="2"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
stroke="currentColor"
|
|
||||||
>
|
|
||||||
<path d="M5 13l4 4L19 7"></path>
|
|
||||||
</svg>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</td>
|
</td>
|
||||||
))}
|
{currentWeek.map((date) => (
|
||||||
<td className="px-4 py-2 text-center">
|
<td key={date} className="px-4 py-2 text-center">
|
||||||
<button
|
<label className="relative inline-block cursor-pointer">
|
||||||
onClick={() => onDeleteHabit(habit.id)}
|
<input
|
||||||
className="p-2 text-red-500 hover:bg-red-100 dark:hover:bg-red-900 rounded-full transition-colors duration-200"
|
type="checkbox"
|
||||||
>
|
checked={habit.completedDates.includes(date)}
|
||||||
<Trash2 className="h-4 w-4" />
|
onChange={() => onToggleHabit(habit.id, date)}
|
||||||
</button>
|
className="sr-only"
|
||||||
</td>
|
/>
|
||||||
{showStreaks && (
|
<div
|
||||||
<>
|
className={`
|
||||||
<td className="px-4 py-2 text-center">
|
w-6 h-6 rounded-md border-2 transition-all duration-200
|
||||||
{/* ... streak content ... */}
|
${habit.completedDates.includes(date)
|
||||||
|
? 'bg-green-500 border-green-500'
|
||||||
|
: 'border-gray-300 dark:border-gray-600 hover:border-green-400 dark:hover:border-green-400'}
|
||||||
|
flex items-center justify-center
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
{habit.completedDates.includes(date) && (
|
||||||
|
<svg
|
||||||
|
className="w-4 h-4 text-white"
|
||||||
|
fill="none"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
strokeWidth="2"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path d="M5 13l4 4L19 7"></path>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
</td>
|
</td>
|
||||||
</>
|
))}
|
||||||
)}
|
{showStreaks && (
|
||||||
</tr>
|
<td className="px-4 py-2 text-center dark:text-white">
|
||||||
))}
|
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-100">
|
||||||
|
{currentStreak} days
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
<td className="px-4 py-2 text-center">
|
||||||
|
<button
|
||||||
|
onClick={() => onDeleteHabit(habit.id)}
|
||||||
|
className="p-2 text-red-500 hover:bg-red-100 dark:hover:bg-red-900 rounded-full transition-colors duration-200"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,27 +3,34 @@ 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';
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
import { formatDate } from '../utils/dateUtils';
|
||||||
|
|
||||||
export const useHabits = () => {
|
export const useHabits = () => {
|
||||||
const [habits, setHabits] = useState<Habit[]>([]);
|
const [habits, setHabits] = useState<Habit[]>([]);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
|
|
||||||
// Automatically fetch habits when user changes
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (user) {
|
if (user) {
|
||||||
fetchHabits();
|
fetchHabits();
|
||||||
} else {
|
} else {
|
||||||
setHabits([]); // Clear habits when user logs out
|
setHabits([]);
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [user?.id]); // Depend on user.id to prevent unnecessary rerenders
|
}, [user?.id]);
|
||||||
|
|
||||||
const fetchHabits = async () => {
|
const fetchHabits = async () => {
|
||||||
if (!user) {
|
if (!user) {
|
||||||
setHabits([]);
|
setHabits([]);
|
||||||
|
setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setError(null);
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('habits')
|
.from('habits')
|
||||||
.select(`
|
.select(`
|
||||||
|
|
@ -40,7 +47,7 @@ export const useHabits = () => {
|
||||||
|
|
||||||
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,
|
||||||
|
|
@ -51,157 +58,144 @@ export const useHabits = () => {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
setHabits(formattedHabits);
|
setHabits(formattedHabits);
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error('Error fetching habits:', error);
|
console.error('Error fetching habits:', err);
|
||||||
setHabits([]);
|
setError(err instanceof Error ? err.message : 'Failed to fetch habits');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const addHabit = async (name: string) => {
|
const addHabit = async (name: string): Promise<boolean> => {
|
||||||
if (!user) return false;
|
if (!user || !name.trim()) return false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase
|
const { data, error } = await supabase
|
||||||
.from('habits')
|
.from('habits')
|
||||||
.insert([{
|
.insert([{
|
||||||
name,
|
name: name.trim(),
|
||||||
best_streak: 0,
|
user_id: user.id,
|
||||||
created_at: new Date().toISOString(),
|
created_at: new Date().toISOString(),
|
||||||
user_id: user.id
|
best_streak: 0
|
||||||
}])
|
}])
|
||||||
.select('id')
|
.select()
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
if (error) throw error;
|
if (error) throw error;
|
||||||
|
|
||||||
await fetchHabits();
|
await fetchHabits();
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (err) {
|
||||||
console.error('Error adding habit:', error);
|
console.error('Error adding habit:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to add habit');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleHabit = async (id: number, date: string) => {
|
const toggleHabit = async (id: number, date: string): Promise<void> => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// First verify this habit belongs to the user
|
const isCompleted = habits
|
||||||
const { data: habitData, error: habitError } = await supabase
|
.find(h => h.id === id)
|
||||||
.from('habits')
|
?.completedDates.includes(date);
|
||||||
.select('id')
|
|
||||||
.eq('id', id)
|
|
||||||
.eq('user_id', user.id)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (habitError || !habitData) {
|
if (isCompleted) {
|
||||||
throw new Error('Unauthorized access to habit');
|
// Remove completion
|
||||||
}
|
const { error } = await supabase
|
||||||
|
|
||||||
// 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) {
|
|
||||||
const { error: deleteError } = await supabase
|
|
||||||
.from('habit_completions')
|
.from('habit_completions')
|
||||||
.delete()
|
.delete()
|
||||||
.eq('habit_id', id)
|
.eq('habit_id', id)
|
||||||
.eq('completion_date', formattedDate)
|
.eq('completion_date', date);
|
||||||
.eq('user_id', user.id);
|
|
||||||
|
|
||||||
if (deleteError) throw deleteError;
|
if (error) throw error;
|
||||||
} else {
|
} else {
|
||||||
const { error: insertError } = await supabase
|
// Add completion
|
||||||
|
const { error } = await supabase
|
||||||
.from('habit_completions')
|
.from('habit_completions')
|
||||||
.insert([{
|
.insert({
|
||||||
habit_id: id,
|
habit_id: id,
|
||||||
completion_date: formattedDate,
|
completion_date: date,
|
||||||
user_id: user.id
|
user_id: user.id
|
||||||
}]);
|
});
|
||||||
|
|
||||||
if (insertError) throw insertError;
|
if (error) throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update local state optimistically
|
||||||
|
setHabits(prevHabits =>
|
||||||
|
prevHabits.map(h => {
|
||||||
|
if (h.id !== id) return h;
|
||||||
|
return {
|
||||||
|
...h,
|
||||||
|
completedDates: isCompleted
|
||||||
|
? h.completedDates.filter(d => d !== date)
|
||||||
|
: [...h.completedDates, date]
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fetch updated data to ensure consistency
|
||||||
|
await fetchHabits();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error toggling habit:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to toggle habit');
|
||||||
await fetchHabits();
|
await fetchHabits();
|
||||||
} catch (error) {
|
|
||||||
console.error('Error toggling habit:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateHabit = async (id: number, name: string) => {
|
const updateHabit = async (id: number, name: string): Promise<void> => {
|
||||||
if (!user) return;
|
if (!user || !name.trim()) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// First verify this habit belongs to the user
|
const { error } = await supabase
|
||||||
const { data: habitData, error: habitError } = await supabase
|
|
||||||
.from('habits')
|
.from('habits')
|
||||||
.select('id')
|
.update({ name: name.trim() })
|
||||||
.eq('id', id)
|
|
||||||
.eq('user_id', user.id)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (habitError || !habitData) {
|
|
||||||
throw new Error('Unauthorized access to habit');
|
|
||||||
}
|
|
||||||
|
|
||||||
await supabase
|
|
||||||
.from('habits')
|
|
||||||
.update({ name })
|
|
||||||
.eq('id', id)
|
.eq('id', id)
|
||||||
.eq('user_id', user.id);
|
.eq('user_id', user.id);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
// Update local state optimistically
|
||||||
|
setHabits(prevHabits =>
|
||||||
|
prevHabits.map(h =>
|
||||||
|
h.id === id ? { ...h, name: name.trim() } : h
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error updating habit:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to update habit');
|
||||||
|
// Revert optimistic update on error
|
||||||
await fetchHabits();
|
await fetchHabits();
|
||||||
} catch (error) {
|
|
||||||
console.error('Error updating habit:', error);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const deleteHabit = async (id: number) => {
|
const deleteHabit = async (id: number): Promise<void> => {
|
||||||
if (!user) return;
|
if (!user) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// First verify this habit belongs to the user
|
const { error } = await supabase
|
||||||
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
|
|
||||||
.from('habits')
|
.from('habits')
|
||||||
.delete()
|
.delete()
|
||||||
.eq('id', id)
|
.eq('id', id)
|
||||||
.eq('user_id', user.id);
|
.eq('user_id', user.id);
|
||||||
|
|
||||||
|
if (error) throw error;
|
||||||
|
|
||||||
|
// Update local state optimistically
|
||||||
|
setHabits(prevHabits => prevHabits.filter(h => h.id !== id));
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error deleting habit:', err);
|
||||||
|
setError(err instanceof Error ? err.message : 'Failed to delete habit');
|
||||||
|
// Revert optimistic update on error
|
||||||
await fetchHabits();
|
await fetchHabits();
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting habit:', error);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
habits,
|
habits,
|
||||||
|
loading,
|
||||||
|
error,
|
||||||
fetchHabits,
|
fetchHabits,
|
||||||
addHabit,
|
addHabit,
|
||||||
toggleHabit,
|
toggleHabit,
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,17 @@
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { getWeekDates, formatDate } from '../utils/dateUtils';
|
||||||
|
|
||||||
export const useWeek = () => {
|
export const useWeek = () => {
|
||||||
const [currentWeek, setCurrentWeek] = useState<string[]>([]);
|
const [currentWeek, setCurrentWeek] = useState<string[]>(getWeekDates());
|
||||||
|
|
||||||
const getCurrentWeekDates = () => {
|
const getCurrentWeekDates = () => getWeekDates();
|
||||||
const now = new Date();
|
|
||||||
const currentDay = now.getDay();
|
|
||||||
const diff = currentDay === 0 ? -6 : 1 - currentDay;
|
|
||||||
|
|
||||||
const monday = new Date(now);
|
|
||||||
monday.setDate(now.getDate() + diff);
|
|
||||||
monday.setHours(0, 0, 0, 0);
|
|
||||||
|
|
||||||
return Array.from({ length: 7 }, (_, i) => {
|
|
||||||
const date = new Date(monday);
|
|
||||||
date.setDate(monday.getDate() + i);
|
|
||||||
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;
|
setCurrentWeek(prevWeek => {
|
||||||
|
const firstDay = new Date(prevWeek[0]);
|
||||||
const firstDay = new Date(currentWeek[0]);
|
firstDay.setDate(firstDay.getDate() + (direction === 'prev' ? -7 : 7));
|
||||||
firstDay.setHours(0, 0, 0, 0);
|
return getWeekDates(firstDay);
|
||||||
const newFirstDay = new Date(firstDay);
|
});
|
||||||
newFirstDay.setDate(firstDay.getDate() + (direction === 'prev' ? -7 : 7));
|
|
||||||
|
|
||||||
setCurrentWeek(Array.from({ length: 7 }, (_, i) => {
|
|
||||||
const date = new Date(newFirstDay);
|
|
||||||
date.setDate(newFirstDay.getDate() + i);
|
|
||||||
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}`;
|
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
47
src/utils/dateUtils.ts
Normal file
47
src/utils/dateUtils.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
// Simple date formatting without timezone complications
|
||||||
|
export const formatDate = (date: Date): string => {
|
||||||
|
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}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get day index (0-6) where Monday is 0 and Sunday is 6
|
||||||
|
export const getDayIndex = (date: Date): number => {
|
||||||
|
const day = date.getDay();
|
||||||
|
return day === 0 ? 6 : day - 1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get dates for current week starting from Monday
|
||||||
|
export const getWeekDates = (baseDate: Date = new Date()): string[] => {
|
||||||
|
const current = new Date(baseDate);
|
||||||
|
const dayIndex = getDayIndex(current);
|
||||||
|
|
||||||
|
// Adjust to Monday
|
||||||
|
current.setDate(current.getDate() - dayIndex);
|
||||||
|
|
||||||
|
// Generate dates starting from Monday
|
||||||
|
const dates = [];
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
|
const date = new Date(current);
|
||||||
|
date.setDate(current.getDate() + i);
|
||||||
|
dates.push(formatDate(date));
|
||||||
|
}
|
||||||
|
|
||||||
|
return dates;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the first day of the month adjusted for Monday start
|
||||||
|
export const getFirstDayOfMonth = (year: number, month: number): number => {
|
||||||
|
const date = new Date(year, month, 1);
|
||||||
|
return getDayIndex(date);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check if two dates are the same day
|
||||||
|
export const isSameDay = (date1: Date, date2: Date): boolean => {
|
||||||
|
return (
|
||||||
|
date1.getDate() === date2.getDate() &&
|
||||||
|
date1.getMonth() === date2.getMonth() &&
|
||||||
|
date1.getFullYear() === date2.getFullYear()
|
||||||
|
);
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue