mirror of
https://github.com/harivansh-afk/Habit-Tracker.git
synced 2026-04-15 21:03:24 +00:00
added features and settings
This commit is contained in:
parent
0ab0745a2c
commit
d79452d6ae
4 changed files with 263 additions and 14 deletions
BIN
habits.db
BIN
habits.db
Binary file not shown.
135
src/App.tsx
135
src/App.tsx
|
|
@ -8,9 +8,9 @@ import { useWeek } from './hooks/useWeek';
|
|||
import { ThemeProvider, useThemeContext } from './contexts/ThemeContext';
|
||||
|
||||
function HabitTrackerContent() {
|
||||
const { theme, isDark, toggleDarkMode } = useThemeContext();
|
||||
const { theme, isDark, toggleDarkMode, defaultView, habitSort } = useThemeContext();
|
||||
const [newHabit, setNewHabit] = useState('');
|
||||
const [activeView, setActiveView] = useState<'habits' | 'calendar' | 'settings'>('habits');
|
||||
const [activeView, setActiveView] = useState<'habits' | 'calendar' | 'settings'>(defaultView);
|
||||
const [currentMonth, setCurrentMonth] = useState(new Date());
|
||||
|
||||
const {
|
||||
|
|
@ -63,6 +63,14 @@ function HabitTrackerContent() {
|
|||
setCurrentWeek(getCurrentWeekDates());
|
||||
};
|
||||
|
||||
const getSortedHabits = () => {
|
||||
if (habitSort === 'alphabetical') {
|
||||
return [...habits].sort((a, b) => a.name.localeCompare(b.name));
|
||||
}
|
||||
// Default to dateCreated (assuming habits are already in creation order)
|
||||
return habits;
|
||||
};
|
||||
|
||||
const renderHabitsView = () => (
|
||||
<div className="space-y-6">
|
||||
<form onSubmit={handleAddHabit} className="flex gap-2">
|
||||
|
|
@ -112,7 +120,7 @@ function HabitTrackerContent() {
|
|||
</div>
|
||||
|
||||
<HabitList
|
||||
habits={habits}
|
||||
habits={getSortedHabits()}
|
||||
currentWeek={currentWeek}
|
||||
daysOfWeek={daysOfWeek}
|
||||
onToggleHabit={toggleHabit}
|
||||
|
|
@ -132,12 +140,40 @@ function HabitTrackerContent() {
|
|||
onChangeMonth={changeMonth}
|
||||
getDaysInMonth={(year, month) => new Date(year, month + 1, 0).getDate()}
|
||||
getCompletedHabitsForDate={getCompletedHabitsForDate}
|
||||
onToggleHabit={async (habitId, date) => {
|
||||
await toggleHabit(habitId, date);
|
||||
await fetchHabits();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
const renderSettingsView = () => {
|
||||
const { theme, isDark, showStreaks, toggleDarkMode, toggleStreaks } = useThemeContext();
|
||||
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-2xl font-bold mb-6 dark:text-white">Settings</h2>
|
||||
|
|
@ -164,8 +200,6 @@ function HabitTrackerContent() {
|
|||
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
|
||||
|
|
@ -177,6 +211,95 @@ function HabitTrackerContent() {
|
|||
/>
|
||||
</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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||
import { ChevronLeft, ChevronRight, Check } from 'lucide-react';
|
||||
import { useThemeContext } from '../contexts/ThemeContext';
|
||||
import { Habit } from '../types';
|
||||
|
||||
|
|
@ -9,6 +9,7 @@ interface CalendarProps {
|
|||
onChangeMonth: (direction: 'prev' | 'next') => void;
|
||||
getDaysInMonth: (year: number, month: number) => number;
|
||||
getCompletedHabitsForDate: (date: string) => Habit[];
|
||||
onToggleHabit: (habitId: number, date: string) => void;
|
||||
}
|
||||
|
||||
export const Calendar: React.FC<CalendarProps> = ({
|
||||
|
|
@ -16,7 +17,8 @@ export const Calendar: React.FC<CalendarProps> = ({
|
|||
habits,
|
||||
onChangeMonth,
|
||||
getDaysInMonth,
|
||||
getCompletedHabitsForDate
|
||||
getCompletedHabitsForDate,
|
||||
onToggleHabit
|
||||
}) => {
|
||||
const { theme } = useThemeContext();
|
||||
|
||||
|
|
@ -35,6 +37,11 @@ export const Calendar: React.FC<CalendarProps> = ({
|
|||
// Get today's date in YYYY-MM-DD format
|
||||
const today = formatDate(new Date());
|
||||
|
||||
const handleToggleHabit = async (e: React.MouseEvent, habitId: number, date: string) => {
|
||||
e.stopPropagation();
|
||||
await onToggleHabit(habitId, date);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`rounded-lg shadow-md p-6 ${theme.calendar.background}`}>
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
|
|
@ -151,6 +158,7 @@ export const Calendar: React.FC<CalendarProps> = ({
|
|||
`}
|
||||
>
|
||||
<div className={`
|
||||
pointer-events-auto
|
||||
rounded-lg p-4
|
||||
min-w-[200px] max-w-[300px]
|
||||
${theme.calendar.tooltip.background}
|
||||
|
|
@ -158,7 +166,6 @@ export const Calendar: React.FC<CalendarProps> = ({
|
|||
${theme.calendar.tooltip.shadow}
|
||||
border
|
||||
backdrop-blur-sm
|
||||
pointer-events-none
|
||||
`}>
|
||||
{completedHabits.length > 0 && (
|
||||
<div className="mb-3">
|
||||
|
|
@ -167,8 +174,17 @@ export const Calendar: React.FC<CalendarProps> = ({
|
|||
</span>
|
||||
<ul className="space-y-1.5">
|
||||
{completedHabits.map(habit => (
|
||||
<li key={habit.id} className={`${theme.text} text-sm truncate`}>
|
||||
{habit.name}
|
||||
<li
|
||||
key={habit.id}
|
||||
className={`${theme.text} text-sm truncate flex items-center justify-between`}
|
||||
>
|
||||
<span>{habit.name}</span>
|
||||
<button
|
||||
onClick={(e) => handleToggleHabit(e, habit.id, date)}
|
||||
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
|
||||
>
|
||||
<Check className="h-4 w-4 text-[#2ecc71]" />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
|
@ -181,8 +197,17 @@ export const Calendar: React.FC<CalendarProps> = ({
|
|||
</span>
|
||||
<ul className="space-y-1.5">
|
||||
{incompleteHabits.map(habit => (
|
||||
<li key={habit.id} className={`${theme.text} text-sm truncate`}>
|
||||
{habit.name}
|
||||
<li
|
||||
key={habit.id}
|
||||
className={`${theme.text} text-sm truncate flex items-center justify-between group`}
|
||||
>
|
||||
<span>{habit.name}</span>
|
||||
<button
|
||||
onClick={(e) => handleToggleHabit(e, habit.id, date)}
|
||||
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<Check className="h-4 w-4 text-gray-400 hover:text-[#2ecc71]" />
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,20 @@
|
|||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||
import { Theme, useTheme } from '../styles/theme';
|
||||
|
||||
type HabitSortOption = 'dateCreated' | 'alphabetical';
|
||||
|
||||
interface ThemeContextType {
|
||||
theme: Theme;
|
||||
isDark: boolean;
|
||||
showStreaks: boolean;
|
||||
dailyReminder: boolean;
|
||||
defaultView: 'habits' | 'calendar';
|
||||
habitSort: HabitSortOption;
|
||||
toggleDarkMode: () => void;
|
||||
toggleStreaks: () => void;
|
||||
toggleDailyReminder: () => void;
|
||||
setDefaultView: (view: 'habits' | 'calendar') => void;
|
||||
setHabitSort: (sort: HabitSortOption) => void;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
||||
|
|
@ -14,6 +22,13 @@ const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
|||
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const [isDark, setIsDark] = useState(() => localStorage.getItem('darkMode') === 'true');
|
||||
const [showStreaks, setShowStreaks] = useState(() => localStorage.getItem('showStreaks') !== 'false');
|
||||
const [dailyReminder, setDailyReminder] = useState(() => localStorage.getItem('dailyReminder') === 'true');
|
||||
const [defaultView, setDefaultView] = useState<'habits' | 'calendar'>(() =>
|
||||
(localStorage.getItem('defaultView') as 'habits' | 'calendar') || 'habits'
|
||||
);
|
||||
const [habitSort, setHabitSort] = useState<HabitSortOption>(() =>
|
||||
(localStorage.getItem('habitSort') as HabitSortOption) || 'dateCreated'
|
||||
);
|
||||
const theme = useTheme(isDark);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -29,11 +44,97 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre
|
|||
localStorage.setItem('showStreaks', showStreaks.toString());
|
||||
}, [showStreaks]);
|
||||
|
||||
useEffect(() => {
|
||||
if (dailyReminder) {
|
||||
// Request notification permission if not granted
|
||||
if (Notification.permission !== 'granted') {
|
||||
Notification.requestPermission();
|
||||
}
|
||||
|
||||
// Schedule notification for 10am
|
||||
const now = new Date();
|
||||
const scheduledTime = new Date(
|
||||
now.getFullYear(),
|
||||
now.getMonth(),
|
||||
now.getDate(),
|
||||
10, // 10am
|
||||
0,
|
||||
0
|
||||
);
|
||||
|
||||
// If it's past 10am, schedule for tomorrow
|
||||
if (now > scheduledTime) {
|
||||
scheduledTime.setDate(scheduledTime.getDate() + 1);
|
||||
}
|
||||
|
||||
const timeUntilNotification = scheduledTime.getTime() - now.getTime();
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
if (Notification.permission === 'granted') {
|
||||
new Notification('Habit Tracker Reminder', {
|
||||
body: "Don't forget to track your habits for today!",
|
||||
icon: '/favicon.ico' // Add your app's icon path here
|
||||
});
|
||||
|
||||
// Schedule next day's notification
|
||||
const nextDay = new Date(scheduledTime);
|
||||
nextDay.setDate(nextDay.getDate() + 1);
|
||||
const nextTimeUntilNotification = nextDay.getTime() - new Date().getTime();
|
||||
setTimeout(() => {
|
||||
if (dailyReminder) {
|
||||
new Notification('Habit Tracker Reminder', {
|
||||
body: "Don't forget to track your habits for today!",
|
||||
icon: '/favicon.ico'
|
||||
});
|
||||
}
|
||||
}, nextTimeUntilNotification);
|
||||
}
|
||||
}, timeUntilNotification);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [dailyReminder]);
|
||||
|
||||
const toggleDarkMode = () => setIsDark(!isDark);
|
||||
const toggleStreaks = () => setShowStreaks(!showStreaks);
|
||||
const toggleDailyReminder = () => {
|
||||
if (!dailyReminder && Notification.permission !== 'granted') {
|
||||
Notification.requestPermission().then(permission => {
|
||||
if (permission === 'granted') {
|
||||
setDailyReminder(true);
|
||||
localStorage.setItem('dailyReminder', 'true');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
setDailyReminder(!dailyReminder);
|
||||
localStorage.setItem('dailyReminder', (!dailyReminder).toString());
|
||||
}
|
||||
};
|
||||
|
||||
const handleSetDefaultView = (view: 'habits' | 'calendar') => {
|
||||
setDefaultView(view);
|
||||
localStorage.setItem('defaultView', view);
|
||||
};
|
||||
|
||||
const handleSetHabitSort = (sort: HabitSortOption) => {
|
||||
setHabitSort(sort);
|
||||
localStorage.setItem('habitSort', sort);
|
||||
};
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider value={{ theme, isDark, showStreaks, toggleDarkMode, toggleStreaks }}>
|
||||
<ThemeContext.Provider value={{
|
||||
theme,
|
||||
isDark,
|
||||
showStreaks,
|
||||
dailyReminder,
|
||||
defaultView,
|
||||
habitSort,
|
||||
toggleDarkMode,
|
||||
toggleStreaks,
|
||||
toggleDailyReminder,
|
||||
setDefaultView: handleSetDefaultView,
|
||||
setHabitSort: handleSetHabitSort
|
||||
}}>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue