mirror of
https://github.com/harivansh-afk/Habit-Tracker.git
synced 2026-04-17 07:03:30 +00:00
adding today feature on main habit page, show streaks feature on settings page and improved dark mode functionality
This commit is contained in:
parent
1bb06006e8
commit
6b6cba21e5
9 changed files with 705 additions and 362 deletions
BIN
habits.db
BIN
habits.db
Binary file not shown.
403
src/App.tsx
403
src/App.tsx
|
|
@ -1,17 +1,35 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import React, { useState, useEffect } from 'react';
|
||||||
import { Plus, CalendarIcon, SettingsIcon, Moon, Sun, ChevronLeft, ChevronRight } from 'lucide-react';
|
import { ChevronLeft, ChevronRight, Sun, Moon } from 'lucide-react';
|
||||||
import { HabitList } from './components/HabitList';
|
import { HabitList } from './components/HabitList';
|
||||||
import { Calendar } from './components/Calendar';
|
import { Calendar } from './components/Calendar';
|
||||||
import { Habit } from './types';
|
import { Sidebar } from './components/Sidebar';
|
||||||
|
import { useHabits } from './hooks/useHabits';
|
||||||
|
import { useWeek } from './hooks/useWeek';
|
||||||
|
import { ThemeProvider, useThemeContext } from './contexts/ThemeContext';
|
||||||
|
|
||||||
export default function HabitTracker() {
|
function HabitTrackerContent() {
|
||||||
const [habits, setHabits] = useState<Habit[]>([]);
|
const { theme, isDark, toggleDarkMode } = useThemeContext();
|
||||||
const [newHabit, setNewHabit] = useState('');
|
const [newHabit, setNewHabit] = useState('');
|
||||||
const [currentWeek, setCurrentWeek] = useState<string[]>([]);
|
|
||||||
const [activeView, setActiveView] = useState<'habits' | 'calendar' | 'settings'>('habits');
|
const [activeView, setActiveView] = useState<'habits' | 'calendar' | 'settings'>('habits');
|
||||||
const [darkMode, setDarkMode] = useState(() => localStorage.getItem('darkMode') === 'true');
|
|
||||||
const [currentMonth, setCurrentMonth] = useState(new Date());
|
const [currentMonth, setCurrentMonth] = useState(new Date());
|
||||||
|
|
||||||
|
const {
|
||||||
|
habits,
|
||||||
|
fetchHabits,
|
||||||
|
addHabit: addHabitApi,
|
||||||
|
toggleHabit,
|
||||||
|
updateHabit,
|
||||||
|
deleteHabit,
|
||||||
|
updateStreak
|
||||||
|
} = useHabits();
|
||||||
|
|
||||||
|
const {
|
||||||
|
currentWeek,
|
||||||
|
setCurrentWeek,
|
||||||
|
getCurrentWeekDates,
|
||||||
|
changeWeek
|
||||||
|
} = useWeek();
|
||||||
|
|
||||||
const daysOfWeek = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
const daysOfWeek = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -19,104 +37,16 @@ export default function HabitTracker() {
|
||||||
setCurrentWeek(getCurrentWeekDates());
|
setCurrentWeek(getCurrentWeekDates());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const handleAddHabit = async (e: React.FormEvent) => {
|
||||||
if (darkMode) {
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
}
|
|
||||||
localStorage.setItem('darkMode', darkMode.toString());
|
|
||||||
}, [darkMode]);
|
|
||||||
|
|
||||||
const fetchHabits = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch('http://localhost:5000/api/habits');
|
|
||||||
const data = await response.json();
|
|
||||||
setHabits(data);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching habits:', error);
|
|
||||||
setHabits([]); // Set empty array on error
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const addHabit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (newHabit.trim()) {
|
if (newHabit.trim()) {
|
||||||
try {
|
const success = await addHabitApi(newHabit);
|
||||||
const response = await fetch('http://localhost:5000/api/habits', {
|
if (success) {
|
||||||
method: 'POST',
|
setNewHabit('');
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ name: newHabit }),
|
|
||||||
});
|
|
||||||
if (response.ok) {
|
|
||||||
setNewHabit('');
|
|
||||||
fetchHabits();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error adding habit:', error);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const toggleHabit = async (id: number, date: string) => {
|
|
||||||
try {
|
|
||||||
await fetch(`http://localhost:5000/api/habits/${id}/toggle`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ date }),
|
|
||||||
});
|
|
||||||
fetchHabits();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error toggling habit:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateHabit = async (id: number, name: string) => {
|
|
||||||
try {
|
|
||||||
await fetch(`http://localhost:5000/api/habits/${id}`, {
|
|
||||||
method: 'PUT',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ name }),
|
|
||||||
});
|
|
||||||
fetchHabits();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error updating habit:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteHabit = async (id: number) => {
|
|
||||||
try {
|
|
||||||
await fetch(`http://localhost:5000/api/habits/${id}`, {
|
|
||||||
method: 'DELETE',
|
|
||||||
});
|
|
||||||
fetchHabits();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting habit:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCurrentWeekDates = () => {
|
|
||||||
const now = new Date();
|
|
||||||
const dayOfWeek = now.getDay();
|
|
||||||
const diff = now.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1);
|
|
||||||
const monday = new Date(now.setDate(diff));
|
|
||||||
return Array.from({ length: 7 }, (_, i) => {
|
|
||||||
const date = new Date(monday);
|
|
||||||
date.setDate(monday.getDate() + i);
|
|
||||||
return date.toISOString().split('T')[0];
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeWeek = (direction: 'prev' | 'next') => {
|
|
||||||
const firstDay = new Date(currentWeek[0]);
|
|
||||||
const newFirstDay = new Date(firstDay.setDate(firstDay.getDate() + (direction === 'prev' ? -7 : 7)));
|
|
||||||
setCurrentWeek(Array.from({ length: 7 }, (_, i) => {
|
|
||||||
const date = new Date(newFirstDay);
|
|
||||||
date.setDate(newFirstDay.getDate() + i);
|
|
||||||
return date.toISOString().split('T')[0];
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
const changeMonth = (direction: 'prev' | 'next') => {
|
const changeMonth = (direction: 'prev' | 'next') => {
|
||||||
setCurrentMonth(prevMonth => {
|
setCurrentMonth(prevMonth => {
|
||||||
const newMonth = new Date(prevMonth);
|
const newMonth = new Date(prevMonth);
|
||||||
|
|
@ -125,168 +55,147 @@ export default function HabitTracker() {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDaysInMonth = (year: number, month: number) => {
|
|
||||||
return new Date(year, month + 1, 0).getDate();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCompletedHabitsForDate = (date: string) => {
|
const getCompletedHabitsForDate = (date: string) => {
|
||||||
return habits.filter(habit => habit.completedDates.includes(date));
|
return habits.filter(habit => habit.completedDates.includes(date));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateStreak = async (id: number, newStreak: number) => {
|
const goToCurrentWeek = () => {
|
||||||
// Prevent negative streaks
|
setCurrentWeek(getCurrentWeekDates());
|
||||||
if (newStreak < 0) return;
|
};
|
||||||
|
|
||||||
try {
|
const renderHabitsView = () => (
|
||||||
// Update in database
|
<div className="space-y-6">
|
||||||
await fetch(`http://localhost:5000/api/habits/${id}/streak`, {
|
<form onSubmit={handleAddHabit} className="flex gap-2">
|
||||||
method: 'PUT',
|
<input
|
||||||
headers: { 'Content-Type': 'application/json' },
|
type="text"
|
||||||
body: JSON.stringify({ streak: newStreak }),
|
value={newHabit}
|
||||||
});
|
onChange={(e) => setNewHabit(e.target.value)}
|
||||||
|
placeholder="Add a new habit"
|
||||||
|
className={`flex-grow px-4 py-2 border rounded-lg ${theme.input}`}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={`px-4 py-2 rounded-lg ${theme.button.primary}`}
|
||||||
|
>
|
||||||
|
Add Habit
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
// Update state
|
<div className={`rounded-lg shadow p-6 ${theme.cardBackground}`}>
|
||||||
setHabits(habits.map(habit =>
|
<div className="flex justify-between items-center mb-6">
|
||||||
habit.id === id ? { ...habit, manualStreak: newStreak } : habit
|
<h2 className="text-2xl font-bold dark:text-white">Weekly Progress</h2>
|
||||||
));
|
<div className="flex items-center space-x-2">
|
||||||
} catch (error) {
|
<button
|
||||||
console.error('Error updating streak:', error);
|
onClick={goToCurrentWeek}
|
||||||
}
|
className={`px-4 py-2 rounded-lg ${theme.button.primary} text-sm`}
|
||||||
|
>
|
||||||
|
Today
|
||||||
|
</button>
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<button
|
||||||
|
onClick={() => changeWeek('prev')}
|
||||||
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full"
|
||||||
|
>
|
||||||
|
<ChevronLeft className="h-5 w-5 dark:text-white" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => changeWeek('next')}
|
||||||
|
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full"
|
||||||
|
>
|
||||||
|
<ChevronRight className="h-5 w-5 dark:text-white" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<HabitList
|
||||||
|
habits={habits}
|
||||||
|
currentWeek={currentWeek}
|
||||||
|
daysOfWeek={daysOfWeek}
|
||||||
|
onToggleHabit={toggleHabit}
|
||||||
|
onUpdateHabit={updateHabit}
|
||||||
|
onDeleteHabit={deleteHabit}
|
||||||
|
onUpdateStreak={updateStreak}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderCalendarView = () => (
|
||||||
|
<Calendar
|
||||||
|
currentMonth={currentMonth}
|
||||||
|
habits={habits}
|
||||||
|
onChangeMonth={changeMonth}
|
||||||
|
getDaysInMonth={(year, month) => new Date(year, month + 1, 0).getDate()}
|
||||||
|
getCompletedHabitsForDate={getCompletedHabitsForDate}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderSettingsView = () => {
|
||||||
|
const { theme, isDark, showStreaks, toggleDarkMode, toggleStreaks } = useThemeContext();
|
||||||
|
|
||||||
|
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'}
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-[#2ecc71] focus:ring-offset-2
|
||||||
|
dark:focus:ring-offset-gray-800
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-white dark:bg-black">
|
<div className={`min-h-screen ${theme.background}`}>
|
||||||
<div className="flex h-screen">
|
<div className="flex h-screen">
|
||||||
<nav className="w-64 border-r border-gray-200 dark:border-gray-800">
|
<Sidebar activeView={activeView} setActiveView={setActiveView} />
|
||||||
<div className="p-4">
|
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Habit Tracker</h1>
|
|
||||||
</div>
|
|
||||||
<ul className="space-y-2 p-4">
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveView('habits')}
|
|
||||||
className={`w-full px-4 py-2 text-left rounded-lg ${
|
|
||||||
activeView === 'habits'
|
|
||||||
? 'bg-gray-100 dark:bg-gray-800'
|
|
||||||
: 'hover:bg-gray-50 dark:hover:bg-gray-800'
|
|
||||||
} dark:text-white`}
|
|
||||||
>
|
|
||||||
<Plus className="inline-block mr-2 h-4 w-4" />
|
|
||||||
Habits
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveView('calendar')}
|
|
||||||
className={`w-full px-4 py-2 text-left rounded-lg ${
|
|
||||||
activeView === 'calendar'
|
|
||||||
? 'bg-gray-100 dark:bg-gray-800'
|
|
||||||
: 'hover:bg-gray-50 dark:hover:bg-gray-800'
|
|
||||||
} dark:text-white`}
|
|
||||||
>
|
|
||||||
<CalendarIcon className="inline-block mr-2 h-4 w-4" />
|
|
||||||
Calendar
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
onClick={() => setActiveView('settings')}
|
|
||||||
className={`w-full px-4 py-2 text-left rounded-lg ${
|
|
||||||
activeView === 'settings'
|
|
||||||
? 'bg-gray-100 dark:bg-gray-800'
|
|
||||||
: 'hover:bg-gray-50 dark:hover:bg-gray-800'
|
|
||||||
} dark:text-white`}
|
|
||||||
>
|
|
||||||
<SettingsIcon className="inline-block mr-2 h-4 w-4" />
|
|
||||||
Settings
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<main className="flex-1 p-8">
|
<main className="flex-1 p-8">
|
||||||
{activeView === 'habits' && (
|
{activeView === 'habits' && renderHabitsView()}
|
||||||
<div className="space-y-6">
|
{activeView === 'calendar' && renderCalendarView()}
|
||||||
<form onSubmit={addHabit} className="flex gap-2">
|
{activeView === 'settings' && renderSettingsView()}
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={newHabit}
|
|
||||||
onChange={(e) => setNewHabit(e.target.value)}
|
|
||||||
placeholder="Add a new habit"
|
|
||||||
className="flex-grow px-4 py-2 border rounded-lg dark:bg-gray-800 dark:border-gray-700 dark:text-white"
|
|
||||||
/>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="px-4 py-2 bg-black text-white rounded-lg dark:bg-white dark:text-black"
|
|
||||||
>
|
|
||||||
Add Habit
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div className="bg-white dark:bg-gray-900 rounded-lg shadow p-6">
|
|
||||||
<div className="flex justify-between items-center mb-6">
|
|
||||||
<h2 className="text-2xl font-bold dark:text-white">Weekly Progress</h2>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<button
|
|
||||||
onClick={() => changeWeek('prev')}
|
|
||||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full"
|
|
||||||
>
|
|
||||||
<ChevronLeft className="h-5 w-5 dark:text-white" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => changeWeek('next')}
|
|
||||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full"
|
|
||||||
>
|
|
||||||
<ChevronRight className="h-5 w-5 dark:text-white" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<HabitList
|
|
||||||
habits={habits}
|
|
||||||
currentWeek={currentWeek}
|
|
||||||
daysOfWeek={daysOfWeek}
|
|
||||||
onToggleHabit={toggleHabit}
|
|
||||||
onUpdateHabit={updateHabit}
|
|
||||||
onDeleteHabit={deleteHabit}
|
|
||||||
onUpdateStreak={handleUpdateStreak}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeView === 'calendar' && (
|
|
||||||
<Calendar
|
|
||||||
currentMonth={currentMonth}
|
|
||||||
habits={habits}
|
|
||||||
onChangeMonth={changeMonth}
|
|
||||||
getDaysInMonth={getDaysInMonth}
|
|
||||||
getCompletedHabitsForDate={getCompletedHabitsForDate}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{activeView === 'settings' && (
|
|
||||||
<div className="bg-white dark:bg-gray-900 rounded-lg shadow p-6">
|
|
||||||
<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={() => setDarkMode(!darkMode)}
|
|
||||||
className="p-2 rounded-full hover:bg-gray-100 dark:hover:bg-gray-800"
|
|
||||||
>
|
|
||||||
{darkMode ? (
|
|
||||||
<Sun className="h-5 w-5 dark:text-white" />
|
|
||||||
) : (
|
|
||||||
<Moon className="h-5 w-5" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function HabitTracker() {
|
||||||
|
return (
|
||||||
|
<ThemeProvider>
|
||||||
|
<HabitTrackerContent />
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
|
import { useThemeContext } from '../contexts/ThemeContext';
|
||||||
import { Habit } from '../types';
|
import { Habit } from '../types';
|
||||||
|
|
||||||
interface CalendarProps {
|
interface CalendarProps {
|
||||||
|
|
@ -10,46 +11,55 @@ interface CalendarProps {
|
||||||
getCompletedHabitsForDate: (date: string) => Habit[];
|
getCompletedHabitsForDate: (date: string) => Habit[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Calendar({
|
export const Calendar: React.FC<CalendarProps> = ({
|
||||||
currentMonth,
|
currentMonth,
|
||||||
habits,
|
habits,
|
||||||
onChangeMonth,
|
onChangeMonth,
|
||||||
getDaysInMonth,
|
getDaysInMonth,
|
||||||
getCompletedHabitsForDate
|
getCompletedHabitsForDate
|
||||||
}: CalendarProps) {
|
}) => {
|
||||||
|
const { theme } = useThemeContext();
|
||||||
|
|
||||||
const daysOfWeek = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
const daysOfWeek = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||||
|
|
||||||
const getFirstDayOfMonth = (year: number, month: number) => {
|
const getFirstDayOfMonth = (year: number, month: number) => {
|
||||||
const date = new Date(year, month, 1);
|
const date = new Date(year, month, 1);
|
||||||
// Convert Sunday (0) to 6 for our Monday-based week
|
|
||||||
return date.getDay() === 0 ? 6 : date.getDay() - 1;
|
return date.getDay() === 0 ? 6 : date.getDay() - 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Helper function to format date to YYYY-MM-DD
|
||||||
|
const formatDate = (date: Date): string => {
|
||||||
|
return date.toISOString().split('T')[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get today's date in YYYY-MM-DD format
|
||||||
|
const today = formatDate(new Date());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-lg p-6">
|
<div className={`rounded-lg shadow-md p-6 ${theme.calendar.background}`}>
|
||||||
<div className="flex items-center justify-between mb-8">
|
<div className="flex justify-between items-center mb-8">
|
||||||
<h2 className="text-2xl font-bold dark:text-white">
|
<h2 className={`text-2xl font-bold ${theme.calendar.header}`}>
|
||||||
{currentMonth.toLocaleString('default', { month: 'long', year: 'numeric' })}
|
{currentMonth.toLocaleString('default', { month: 'long', year: 'numeric' })}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex space-x-2">
|
<div className="flex space-x-3">
|
||||||
<button
|
<button
|
||||||
onClick={() => onChangeMonth('prev')}
|
onClick={() => onChangeMonth('prev')}
|
||||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full transition-colors duration-200"
|
className={`p-2.5 rounded-lg ${theme.calendar.navigation.button}`}
|
||||||
>
|
>
|
||||||
<ChevronLeft className="h-5 w-5 dark:text-white" />
|
<ChevronLeft className={theme.calendar.navigation.icon} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => onChangeMonth('next')}
|
onClick={() => onChangeMonth('next')}
|
||||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full transition-colors duration-200"
|
className={`p-2.5 rounded-lg ${theme.calendar.navigation.button}`}
|
||||||
>
|
>
|
||||||
<ChevronRight className="h-5 w-5 dark:text-white" />
|
<ChevronRight className={theme.calendar.navigation.icon} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-7 gap-4">
|
<div className="grid grid-cols-7 gap-4">
|
||||||
{daysOfWeek.map(day => (
|
{daysOfWeek.map(day => (
|
||||||
<div key={day} className="text-center font-semibold text-gray-600 dark:text-gray-300 mb-2">
|
<div key={day} className={`text-center font-semibold mb-2 ${theme.calendar.weekDay}`}>
|
||||||
{day}
|
{day}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -61,13 +71,12 @@ export function Calendar({
|
||||||
const daysInMonth = getDaysInMonth(year, month);
|
const daysInMonth = getDaysInMonth(year, month);
|
||||||
const daysInPrevMonth = getDaysInMonth(year, month - 1);
|
const daysInPrevMonth = getDaysInMonth(year, month - 1);
|
||||||
|
|
||||||
// Calculate days to show
|
|
||||||
const days = [];
|
const days = [];
|
||||||
|
|
||||||
// Previous month days
|
// Previous month days
|
||||||
for (let i = 0; i < firstDayOfMonth; i++) {
|
for (let i = 0; i < firstDayOfMonth; i++) {
|
||||||
const day = daysInPrevMonth - firstDayOfMonth + i + 1;
|
const day = daysInPrevMonth - firstDayOfMonth + i + 1;
|
||||||
const date = new Date(year, month - 1, day).toISOString().split('T')[0];
|
const date = formatDate(new Date(year, month - 1, day));
|
||||||
days.push({
|
days.push({
|
||||||
date,
|
date,
|
||||||
dayNumber: day,
|
dayNumber: day,
|
||||||
|
|
@ -77,7 +86,7 @@ export function Calendar({
|
||||||
|
|
||||||
// Current month days
|
// Current month days
|
||||||
for (let i = 1; i <= daysInMonth; i++) {
|
for (let i = 1; i <= daysInMonth; i++) {
|
||||||
const date = new Date(year, month, i).toISOString().split('T')[0];
|
const date = formatDate(new Date(year, month, i));
|
||||||
days.push({
|
days.push({
|
||||||
date,
|
date,
|
||||||
dayNumber: i,
|
dayNumber: i,
|
||||||
|
|
@ -85,10 +94,10 @@ export function Calendar({
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Next month days to complete the grid
|
// Next month days
|
||||||
const remainingDays = 42 - days.length; // 6 rows * 7 days
|
const remainingDays = 42 - days.length;
|
||||||
for (let i = 1; i <= remainingDays; i++) {
|
for (let i = 1; i <= remainingDays; i++) {
|
||||||
const date = new Date(year, month + 1, i).toISOString().split('T')[0];
|
const date = formatDate(new Date(year, month + 1, i));
|
||||||
days.push({
|
days.push({
|
||||||
date,
|
date,
|
||||||
dayNumber: i,
|
dayNumber: i,
|
||||||
|
|
@ -99,41 +108,62 @@ export function Calendar({
|
||||||
return days.map(({ date, dayNumber, isCurrentMonth }) => {
|
return days.map(({ date, dayNumber, isCurrentMonth }) => {
|
||||||
const completedHabits = getCompletedHabitsForDate(date);
|
const completedHabits = getCompletedHabitsForDate(date);
|
||||||
const incompleteHabits = habits.filter(habit => !habit.completedDates.includes(date));
|
const incompleteHabits = habits.filter(habit => !habit.completedDates.includes(date));
|
||||||
|
const isToday = date === today;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={date}
|
key={date}
|
||||||
className={`border dark:border-gray-700 rounded-lg p-3 min-h-[80px] relative group hover:shadow-md transition-shadow duration-200 ${
|
className={`
|
||||||
!isCurrentMonth ? 'bg-gray-50 dark:bg-gray-800/50' : ''
|
border rounded-lg p-3 min-h-[80px] relative
|
||||||
}`}
|
${theme.border}
|
||||||
|
${isCurrentMonth ? theme.calendar.day.default : theme.calendar.day.otherMonth}
|
||||||
|
${isToday ? `border-2 ${theme.calendar.day.today}` : ''}
|
||||||
|
`}
|
||||||
>
|
>
|
||||||
<span className={`text-sm font-medium ${
|
<span className={`font-medium ${isCurrentMonth ? theme.text : theme.calendar.day.otherMonth}`}>
|
||||||
isCurrentMonth
|
|
||||||
? 'dark:text-white'
|
|
||||||
: 'text-gray-400 dark:text-gray-500'
|
|
||||||
}`}>
|
|
||||||
{dayNumber}
|
{dayNumber}
|
||||||
</span>
|
</span>
|
||||||
{habits.length > 0 && (
|
{habits.length > 0 && (
|
||||||
<div className="absolute bottom-3 left-1/2 transform -translate-x-1/2">
|
<div className="absolute bottom-3 left-1/2 transform -translate-x-1/2">
|
||||||
<div className="relative">
|
<div className="group relative inline-block">
|
||||||
<div
|
<div
|
||||||
className={`h-3 w-3 ${
|
className={`
|
||||||
completedHabits.length > 0
|
h-4 w-4 rounded-full cursor-pointer
|
||||||
? 'bg-green-500 shadow-sm shadow-green-200'
|
transition-colors duration-200
|
||||||
: 'bg-gray-300 dark:bg-gray-600'
|
${completedHabits.length > 0
|
||||||
} rounded-full transition-colors duration-200`}
|
? 'bg-[#2ecc71] dark:bg-[#2ecc71] shadow-sm shadow-[#2ecc7150]'
|
||||||
|
: `bg-[#e9e9e8] dark:bg-[#393939]`
|
||||||
|
}
|
||||||
|
`}
|
||||||
/>
|
/>
|
||||||
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-3 hidden group-hover:block">
|
<div
|
||||||
<div className="bg-white dark:bg-gray-800 text-sm rounded-lg shadow-lg p-4 border dark:border-gray-700 min-w-[200px]">
|
className={`
|
||||||
|
absolute bottom-full left-1/2 -translate-x-1/2 mb-2
|
||||||
|
opacity-0 invisible
|
||||||
|
group-hover:opacity-100 group-hover:visible
|
||||||
|
transition-all duration-150 ease-in-out
|
||||||
|
z-50 transform
|
||||||
|
translate-y-1 group-hover:translate-y-0
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div className={`
|
||||||
|
rounded-lg p-4
|
||||||
|
min-w-[200px] max-w-[300px]
|
||||||
|
${theme.calendar.tooltip.background}
|
||||||
|
${theme.calendar.tooltip.border}
|
||||||
|
${theme.calendar.tooltip.shadow}
|
||||||
|
border
|
||||||
|
backdrop-blur-sm
|
||||||
|
pointer-events-none
|
||||||
|
`}>
|
||||||
{completedHabits.length > 0 && (
|
{completedHabits.length > 0 && (
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<span className="text-green-500 font-semibold block mb-1">
|
<span className="text-[#2ecc71] font-semibold block mb-2">
|
||||||
✓ Completed
|
✓ Completed
|
||||||
</span>
|
</span>
|
||||||
<ul className="space-y-1">
|
<ul className="space-y-1.5">
|
||||||
{completedHabits.map(habit => (
|
{completedHabits.map(habit => (
|
||||||
<li key={habit.id} className="text-gray-600 dark:text-gray-300">
|
<li key={habit.id} className={`${theme.text} text-sm truncate`}>
|
||||||
{habit.name}
|
{habit.name}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|
@ -142,12 +172,12 @@ export function Calendar({
|
||||||
)}
|
)}
|
||||||
{incompleteHabits.length > 0 && (
|
{incompleteHabits.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<span className="text-red-500 font-semibold block mb-1">
|
<span className="text-[#e74c3c] font-semibold block mb-2">
|
||||||
○ Pending
|
○ Pending
|
||||||
</span>
|
</span>
|
||||||
<ul className="space-y-1">
|
<ul className="space-y-1.5">
|
||||||
{incompleteHabits.map(habit => (
|
{incompleteHabits.map(habit => (
|
||||||
<li key={habit.id} className="text-gray-600 dark:text-gray-300">
|
<li key={habit.id} className={`${theme.text} text-sm truncate`}>
|
||||||
{habit.name}
|
{habit.name}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|
@ -166,4 +196,4 @@ export function Calendar({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } 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';
|
||||||
|
|
||||||
interface HabitListProps {
|
interface HabitListProps {
|
||||||
habits: Habit[];
|
habits: Habit[];
|
||||||
|
|
@ -99,6 +100,8 @@ export function HabitList({
|
||||||
onDeleteHabit,
|
onDeleteHabit,
|
||||||
onUpdateStreak,
|
onUpdateStreak,
|
||||||
}: HabitListProps) {
|
}: HabitListProps) {
|
||||||
|
const { showStreaks } = useThemeContext();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('Current week dates:',
|
console.log('Current week dates:',
|
||||||
currentWeek.map(date =>
|
currentWeek.map(date =>
|
||||||
|
|
@ -108,82 +111,114 @@ export function HabitList({
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table className="w-full">
|
<div className="overflow-x-auto">
|
||||||
<thead>
|
<table className="w-full">
|
||||||
<tr>
|
<thead>
|
||||||
<th className="px-4 py-2 text-left dark:text-white">Habit</th>
|
<tr>
|
||||||
{currentWeek.map((dateStr, index) => {
|
<th className="text-left px-4 py-2 dark:text-white">Habit</th>
|
||||||
const date = new Date(dateStr);
|
{currentWeek.map((dateStr, index) => {
|
||||||
// Ensure date is interpreted in local timezone
|
const date = new Date(dateStr);
|
||||||
const displayDate = new Date(date.getTime() + date.getTimezoneOffset() * 60000);
|
const displayDate = new Date(date.getTime() + date.getTimezoneOffset() * 60000);
|
||||||
|
|
||||||
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[index]}</div>
|
<div>{daysOfWeek[index]}</div>
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
{displayDate.getDate()}
|
{displayDate.getDate()}
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</th>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
<th className="px-4 py-2 text-center dark:text-white">Current Streak</th>
|
{showStreaks && (
|
||||||
<th className="px-4 py-2 text-center dark:text-white">Best 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">Current Streak</th>
|
||||||
</tr>
|
<th className="px-4 py-2 text-center dark:text-white">Best Streak</th>
|
||||||
</thead>
|
</>
|
||||||
<tbody>
|
)}
|
||||||
{habits.map((habit) => (
|
<th className="px-4 py-2 text-center dark:text-white">Actions</th>
|
||||||
<tr key={habit.id} className="border-t dark:border-gray-700">
|
</tr>
|
||||||
<td className="px-4 py-2 dark:text-white">
|
</thead>
|
||||||
<input
|
<tbody>
|
||||||
type="text"
|
{habits.map((habit) => (
|
||||||
value={habit.name}
|
<tr key={habit.id} className="border-t dark:border-gray-700">
|
||||||
onChange={(e) => onUpdateHabit(habit.id, e.target.value)}
|
<td className="px-4 py-2 dark:text-white">
|
||||||
aria-label="Habit name"
|
|
||||||
placeholder="Enter habit name"
|
|
||||||
className="bg-transparent border-none focus:outline-none focus:ring-2 focus:ring-gray-300 rounded px-2"
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
{currentWeek.map((date) => (
|
|
||||||
<td key={date} className="px-4 py-2 text-center">
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="text"
|
||||||
checked={habit.completedDates.includes(date)}
|
value={habit.name}
|
||||||
onChange={() => {
|
onChange={(e) => onUpdateHabit(habit.id, e.target.value)}
|
||||||
onToggleHabit(habit.id, date);
|
aria-label="Habit name"
|
||||||
const newCompletedDates = habit.completedDates.includes(date)
|
placeholder="Enter habit name"
|
||||||
? habit.completedDates.filter(d => d !== date)
|
className="bg-transparent border-none focus:outline-none focus:ring-2 focus:ring-gray-300 rounded px-2"
|
||||||
: [...habit.completedDates, date];
|
|
||||||
|
|
||||||
const { bestStreak } = calculateStreak(newCompletedDates);
|
|
||||||
onUpdateStreak(habit.id, bestStreak);
|
|
||||||
}}
|
|
||||||
aria-label={`Mark ${habit.name} as completed for ${date}`}
|
|
||||||
className="w-4 h-4 rounded border-gray-300 dark:border-gray-600"
|
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
))}
|
{currentWeek.map((date) => (
|
||||||
<td className="px-4 py-2 text-center">
|
<td key={date} className="px-4 py-2 text-center">
|
||||||
<span className="dark:text-white font-medium">
|
<label className="relative inline-block cursor-pointer">
|
||||||
{calculateStreak(habit.completedDates || []).currentStreak}
|
<input
|
||||||
</span>
|
type="checkbox"
|
||||||
</td>
|
checked={habit.completedDates.includes(date)}
|
||||||
<td className="px-4 py-2 text-center">
|
onChange={() => {
|
||||||
<span className="dark:text-white font-medium">
|
onToggleHabit(habit.id, date);
|
||||||
{calculateStreak(habit.completedDates || []).bestStreak}
|
const newCompletedDates = habit.completedDates.includes(date)
|
||||||
</span>
|
? habit.completedDates.filter(d => d !== date)
|
||||||
</td>
|
: [...habit.completedDates, date];
|
||||||
<td className="px-4 py-2 text-center">
|
|
||||||
<button
|
const { bestStreak } = calculateStreak(newCompletedDates);
|
||||||
onClick={() => onDeleteHabit(habit.id)}
|
onUpdateStreak(habit.id, bestStreak);
|
||||||
className="p-2 text-red-500 hover:bg-red-100 dark:hover:bg-red-900 rounded-full"
|
}}
|
||||||
>
|
aria-label={`Mark ${habit.name} as completed for ${date}`}
|
||||||
<Trash2 className="h-4 w-4" />
|
className="sr-only"
|
||||||
</button>
|
/>
|
||||||
</td>
|
<div className={`
|
||||||
</tr>
|
w-6 h-6 rounded-md border-2 transition-all duration-200
|
||||||
))}
|
${habit.completedDates.includes(date)
|
||||||
</tbody>
|
? 'bg-green-500 border-green-500'
|
||||||
</table>
|
: '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>
|
||||||
|
))}
|
||||||
|
{showStreaks && (
|
||||||
|
<>
|
||||||
|
<td className="px-4 py-2 text-center">
|
||||||
|
<span className="text-yellow-500 dark:text-yellow-400 font-medium text-lg">
|
||||||
|
{calculateStreak(habit.completedDates || []).currentStreak}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-2 text-center">
|
||||||
|
<span className="text-yellow-500 dark:text-yellow-400 font-medium text-lg">
|
||||||
|
{calculateStreak(habit.completedDates || []).bestStreak}
|
||||||
|
</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>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
63
src/components/Sidebar.tsx
Normal file
63
src/components/Sidebar.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Plus, CalendarIcon, SettingsIcon } from 'lucide-react';
|
||||||
|
import { useThemeContext } from '../contexts/ThemeContext';
|
||||||
|
|
||||||
|
type View = 'habits' | 'calendar' | 'settings';
|
||||||
|
|
||||||
|
interface SidebarProps {
|
||||||
|
activeView: View;
|
||||||
|
setActiveView: (view: View) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Sidebar: React.FC<SidebarProps> = ({ activeView, setActiveView }) => {
|
||||||
|
const { theme } = useThemeContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className={`w-64 border-r ${theme.border} ${theme.sidebarBackground}`}>
|
||||||
|
<div className="p-4">
|
||||||
|
<h1 className={`text-2xl font-bold ${theme.text}`}>Habit Tracker</h1>
|
||||||
|
</div>
|
||||||
|
<ul className="space-y-2 p-4">
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveView('habits')}
|
||||||
|
className={`w-full px-4 py-2 text-left rounded-lg ${
|
||||||
|
activeView === 'habits'
|
||||||
|
? theme.button.secondary
|
||||||
|
: `${theme.text} ${theme.habitItem}`
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Plus className="inline-block mr-2 h-4 w-4" />
|
||||||
|
Habits
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveView('calendar')}
|
||||||
|
className={`w-full px-4 py-2 text-left rounded-lg ${
|
||||||
|
activeView === 'calendar'
|
||||||
|
? theme.button.secondary
|
||||||
|
: `${theme.text} ${theme.habitItem}`
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<CalendarIcon className="inline-block mr-2 h-4 w-4" />
|
||||||
|
Calendar
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveView('settings')}
|
||||||
|
className={`w-full px-4 py-2 text-left rounded-lg ${
|
||||||
|
activeView === 'settings'
|
||||||
|
? theme.button.secondary
|
||||||
|
: `${theme.text} ${theme.habitItem}`
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<SettingsIcon className="inline-block mr-2 h-4 w-4" />
|
||||||
|
Settings
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
48
src/contexts/ThemeContext.tsx
Normal file
48
src/contexts/ThemeContext.tsx
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||||
|
import { Theme, useTheme } from '../styles/theme';
|
||||||
|
|
||||||
|
interface ThemeContextType {
|
||||||
|
theme: Theme;
|
||||||
|
isDark: boolean;
|
||||||
|
showStreaks: boolean;
|
||||||
|
toggleDarkMode: () => void;
|
||||||
|
toggleStreaks: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||||
|
|
||||||
|
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
|
const [isDark, setIsDark] = useState(() => localStorage.getItem('darkMode') === 'true');
|
||||||
|
const [showStreaks, setShowStreaks] = useState(() => localStorage.getItem('showStreaks') !== 'false');
|
||||||
|
const theme = useTheme(isDark);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDark) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
}
|
||||||
|
localStorage.setItem('darkMode', isDark.toString());
|
||||||
|
}, [isDark]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem('showStreaks', showStreaks.toString());
|
||||||
|
}, [showStreaks]);
|
||||||
|
|
||||||
|
const toggleDarkMode = () => setIsDark(!isDark);
|
||||||
|
const toggleStreaks = () => setShowStreaks(!showStreaks);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ThemeContext.Provider value={{ theme, isDark, showStreaks, toggleDarkMode, toggleStreaks }}>
|
||||||
|
{children}
|
||||||
|
</ThemeContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useThemeContext = () => {
|
||||||
|
const context = useContext(ThemeContext);
|
||||||
|
if (context === undefined) {
|
||||||
|
throw new Error('useThemeContext must be used within a ThemeProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
99
src/hooks/useHabits.ts
Normal file
99
src/hooks/useHabits.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { Habit } from '../types';
|
||||||
|
|
||||||
|
export const useHabits = () => {
|
||||||
|
const [habits, setHabits] = useState<Habit[]>([]);
|
||||||
|
|
||||||
|
const fetchHabits = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost:5000/api/habits');
|
||||||
|
const data = await response.json();
|
||||||
|
setHabits(data);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching habits:', error);
|
||||||
|
setHabits([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addHabit = async (name: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('http://localhost:5000/api/habits', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name }),
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
await fetchHabits();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error adding habit:', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleHabit = async (id: number, date: string) => {
|
||||||
|
try {
|
||||||
|
await fetch(`http://localhost:5000/api/habits/${id}/toggle`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ date }),
|
||||||
|
});
|
||||||
|
await fetchHabits();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error toggling habit:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateHabit = async (id: number, name: string) => {
|
||||||
|
try {
|
||||||
|
await fetch(`http://localhost:5000/api/habits/${id}`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ name }),
|
||||||
|
});
|
||||||
|
await fetchHabits();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating habit:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const deleteHabit = async (id: number) => {
|
||||||
|
try {
|
||||||
|
await fetch(`http://localhost:5000/api/habits/${id}`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
await fetchHabits();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting habit:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateStreak = async (id: number, newStreak: number) => {
|
||||||
|
if (newStreak < 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fetch(`http://localhost:5000/api/habits/${id}/streak`, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ streak: newStreak }),
|
||||||
|
});
|
||||||
|
setHabits(habits.map(habit =>
|
||||||
|
habit.id === id ? { ...habit, manualStreak: newStreak } : habit
|
||||||
|
));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating streak:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
habits,
|
||||||
|
fetchHabits,
|
||||||
|
addHabit,
|
||||||
|
toggleHabit,
|
||||||
|
updateHabit,
|
||||||
|
deleteHabit,
|
||||||
|
updateStreak
|
||||||
|
};
|
||||||
|
};
|
||||||
34
src/hooks/useWeek.ts
Normal file
34
src/hooks/useWeek.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
export const useWeek = () => {
|
||||||
|
const [currentWeek, setCurrentWeek] = useState<string[]>([]);
|
||||||
|
|
||||||
|
const getCurrentWeekDates = () => {
|
||||||
|
const now = new Date();
|
||||||
|
const dayOfWeek = now.getDay();
|
||||||
|
const diff = now.getDate() - dayOfWeek + (dayOfWeek === 0 ? -6 : 1);
|
||||||
|
const monday = new Date(now.setDate(diff));
|
||||||
|
return Array.from({ length: 7 }, (_, i) => {
|
||||||
|
const date = new Date(monday);
|
||||||
|
date.setDate(monday.getDate() + i);
|
||||||
|
return date.toISOString().split('T')[0];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeWeek = (direction: 'prev' | 'next') => {
|
||||||
|
const firstDay = new Date(currentWeek[0]);
|
||||||
|
const newFirstDay = new Date(firstDay.setDate(firstDay.getDate() + (direction === 'prev' ? -7 : 7)));
|
||||||
|
setCurrentWeek(Array.from({ length: 7 }, (_, i) => {
|
||||||
|
const date = new Date(newFirstDay);
|
||||||
|
date.setDate(newFirstDay.getDate() + i);
|
||||||
|
return date.toISOString().split('T')[0];
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentWeek,
|
||||||
|
setCurrentWeek,
|
||||||
|
getCurrentWeekDates,
|
||||||
|
changeWeek
|
||||||
|
};
|
||||||
|
};
|
||||||
125
src/styles/theme.ts
Normal file
125
src/styles/theme.ts
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
export const lightTheme = {
|
||||||
|
// Background colors
|
||||||
|
background: 'bg-[#ffffff]',
|
||||||
|
cardBackground: 'bg-[#ffffff]',
|
||||||
|
sidebarBackground: 'bg-[#fbfbfa]',
|
||||||
|
|
||||||
|
// Text colors
|
||||||
|
text: 'text-[#37352f]',
|
||||||
|
mutedText: 'text-[#787774]',
|
||||||
|
|
||||||
|
// Border colors
|
||||||
|
border: 'border-[#e9e9e8]',
|
||||||
|
|
||||||
|
// Interactive elements
|
||||||
|
button: {
|
||||||
|
primary: 'bg-[#37352f] text-white hover:bg-[#2f2f2f]',
|
||||||
|
secondary: 'bg-[#f1f1ef] text-[#37352f] hover:bg-[#e9e9e8]',
|
||||||
|
icon: 'hover:bg-[#f1f1ef] text-[#37352f]'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Input elements
|
||||||
|
input: 'bg-[#ffffff] border-[#e9e9e8] focus:border-[#37352f] text-[#37352f]',
|
||||||
|
|
||||||
|
// Calendar specific
|
||||||
|
calendarDay: 'bg-[#ffffff] hover:bg-[#f1f1ef]',
|
||||||
|
calendarDaySelected: 'bg-[#37352f] text-white',
|
||||||
|
|
||||||
|
// Habit list specific
|
||||||
|
habitItem: 'hover:bg-[#f1f1ef]',
|
||||||
|
habitCheckbox: 'border-[#e9e9e8] text-[#37352f]',
|
||||||
|
|
||||||
|
// Calendar specific (expanded)
|
||||||
|
calendar: {
|
||||||
|
background: 'bg-[#ffffff]',
|
||||||
|
header: 'text-[#37352f]',
|
||||||
|
weekDay: 'text-[#787774]',
|
||||||
|
day: {
|
||||||
|
default: 'bg-[#ffffff] hover:bg-[#f1f1ef] text-[#37352f] shadow-sm',
|
||||||
|
selected: 'bg-[#37352f] text-white',
|
||||||
|
today: 'border-[#37352f]',
|
||||||
|
otherMonth: 'text-[#787774] bg-[#fafafa]'
|
||||||
|
},
|
||||||
|
navigation: {
|
||||||
|
button: 'hover:bg-[#f1f1ef] text-[#37352f]',
|
||||||
|
icon: 'h-5 w-5 text-[#37352f]'
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
background: 'bg-[#ffffff]',
|
||||||
|
border: 'border-[#e9e9e8]',
|
||||||
|
shadow: 'shadow-lg shadow-[#00000008]'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
nav: {
|
||||||
|
active: 'bg-[#f1f1ef] text-[#37352f]',
|
||||||
|
inactive: 'text-[#37352f] hover:bg-[#f1f1ef]'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const darkTheme = {
|
||||||
|
// Background colors
|
||||||
|
background: 'bg-[#191919]',
|
||||||
|
cardBackground: 'bg-[#2f2f2f]',
|
||||||
|
sidebarBackground: 'bg-[#191919]',
|
||||||
|
|
||||||
|
// Text colors
|
||||||
|
text: 'text-[#ffffff]',
|
||||||
|
mutedText: 'text-[#999999]',
|
||||||
|
|
||||||
|
// Border colors
|
||||||
|
border: 'border-[#393939]',
|
||||||
|
|
||||||
|
// Interactive elements
|
||||||
|
button: {
|
||||||
|
primary: 'bg-[#ffffff] text-[#191919] hover:bg-[#e6e6e6]',
|
||||||
|
secondary: 'bg-[#363636] text-[#ffffff] hover:bg-[#424242]',
|
||||||
|
icon: 'hover:bg-[#363636] text-[#ffffff]'
|
||||||
|
},
|
||||||
|
|
||||||
|
// Input elements
|
||||||
|
input: 'bg-[#2f2f2f] border-[#393939] focus:border-[#525252] text-[#ffffff]',
|
||||||
|
|
||||||
|
// Calendar specific
|
||||||
|
calendarDay: 'bg-[#2f2f2f] hover:bg-[#363636]',
|
||||||
|
calendarDaySelected: 'bg-[#ffffff] text-[#191919]',
|
||||||
|
|
||||||
|
// Habit list specific
|
||||||
|
habitItem: 'hover:bg-[#363636]',
|
||||||
|
habitCheckbox: 'border-[#393939] text-[#ffffff]',
|
||||||
|
|
||||||
|
// Calendar specific (expanded)
|
||||||
|
calendar: {
|
||||||
|
background: 'bg-[#191919]',
|
||||||
|
header: 'text-[#ffffff]',
|
||||||
|
weekDay: 'text-[#999999]',
|
||||||
|
day: {
|
||||||
|
default: 'bg-[#2f2f2f] hover:bg-[#363636] text-[#ffffff] shadow-md shadow-[#00000030]',
|
||||||
|
selected: 'bg-[#ffffff] text-[#191919]',
|
||||||
|
today: 'border-[#ffffff]',
|
||||||
|
otherMonth: 'text-[#666666] bg-[#242424]'
|
||||||
|
},
|
||||||
|
navigation: {
|
||||||
|
button: 'hover:bg-[#363636] text-[#ffffff]',
|
||||||
|
icon: 'h-5 w-5 text-[#ffffff]'
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
background: 'bg-[#2f2f2f]',
|
||||||
|
border: 'border-[#393939]',
|
||||||
|
shadow: 'shadow-lg shadow-[#00000050]'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
nav: {
|
||||||
|
active: 'bg-[#363636] text-[#ffffff]',
|
||||||
|
inactive: 'text-[#ffffff] hover:bg-[#363636]'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Theme = typeof lightTheme;
|
||||||
|
|
||||||
|
export const useTheme = (isDark: boolean): Theme => {
|
||||||
|
return isDark ? darkTheme : lightTheme;
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue