mirror of
https://github.com/harivansh-afk/Habit-Tracker.git
synced 2026-04-20 02:03:18 +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';
|
import { ThemeProvider, useThemeContext } from './contexts/ThemeContext';
|
||||||
|
|
||||||
function HabitTrackerContent() {
|
function HabitTrackerContent() {
|
||||||
const { theme, isDark, toggleDarkMode } = useThemeContext();
|
const { theme, isDark, toggleDarkMode, defaultView, habitSort } = useThemeContext();
|
||||||
const [newHabit, setNewHabit] = useState('');
|
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 [currentMonth, setCurrentMonth] = useState(new Date());
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
@ -63,6 +63,14 @@ function HabitTrackerContent() {
|
||||||
setCurrentWeek(getCurrentWeekDates());
|
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 = () => (
|
const renderHabitsView = () => (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<form onSubmit={handleAddHabit} className="flex gap-2">
|
<form onSubmit={handleAddHabit} className="flex gap-2">
|
||||||
|
|
@ -112,7 +120,7 @@ function HabitTrackerContent() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<HabitList
|
<HabitList
|
||||||
habits={habits}
|
habits={getSortedHabits()}
|
||||||
currentWeek={currentWeek}
|
currentWeek={currentWeek}
|
||||||
daysOfWeek={daysOfWeek}
|
daysOfWeek={daysOfWeek}
|
||||||
onToggleHabit={toggleHabit}
|
onToggleHabit={toggleHabit}
|
||||||
|
|
@ -132,11 +140,39 @@ function HabitTrackerContent() {
|
||||||
onChangeMonth={changeMonth}
|
onChangeMonth={changeMonth}
|
||||||
getDaysInMonth={(year, month) => new Date(year, month + 1, 0).getDate()}
|
getDaysInMonth={(year, month) => new Date(year, month + 1, 0).getDate()}
|
||||||
getCompletedHabitsForDate={getCompletedHabitsForDate}
|
getCompletedHabitsForDate={getCompletedHabitsForDate}
|
||||||
|
onToggleHabit={async (habitId, date) => {
|
||||||
|
await toggleHabit(habitId, date);
|
||||||
|
await fetchHabits();
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderSettingsView = () => {
|
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 (
|
return (
|
||||||
<div className={`rounded-lg shadow p-6 ${theme.cardBackground}`}>
|
<div className={`rounded-lg shadow p-6 ${theme.cardBackground}`}>
|
||||||
|
|
@ -164,8 +200,6 @@ function HabitTrackerContent() {
|
||||||
relative inline-flex h-6 w-11 items-center rounded-full
|
relative inline-flex h-6 w-11 items-center rounded-full
|
||||||
transition-colors duration-200 ease-in-out
|
transition-colors duration-200 ease-in-out
|
||||||
${showStreaks ? 'bg-[#2ecc71]' : 'bg-gray-200 dark:bg-gray-700'}
|
${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
|
<span
|
||||||
|
|
@ -177,6 +211,95 @@ function HabitTrackerContent() {
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
import { ChevronLeft, ChevronRight, Check } from 'lucide-react';
|
||||||
import { useThemeContext } from '../contexts/ThemeContext';
|
import { useThemeContext } from '../contexts/ThemeContext';
|
||||||
import { Habit } from '../types';
|
import { Habit } from '../types';
|
||||||
|
|
||||||
|
|
@ -9,6 +9,7 @@ interface CalendarProps {
|
||||||
onChangeMonth: (direction: 'prev' | 'next') => void;
|
onChangeMonth: (direction: 'prev' | 'next') => void;
|
||||||
getDaysInMonth: (year: number, month: number) => number;
|
getDaysInMonth: (year: number, month: number) => number;
|
||||||
getCompletedHabitsForDate: (date: string) => Habit[];
|
getCompletedHabitsForDate: (date: string) => Habit[];
|
||||||
|
onToggleHabit: (habitId: number, date: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Calendar: React.FC<CalendarProps> = ({
|
export const Calendar: React.FC<CalendarProps> = ({
|
||||||
|
|
@ -16,7 +17,8 @@ export const Calendar: React.FC<CalendarProps> = ({
|
||||||
habits,
|
habits,
|
||||||
onChangeMonth,
|
onChangeMonth,
|
||||||
getDaysInMonth,
|
getDaysInMonth,
|
||||||
getCompletedHabitsForDate
|
getCompletedHabitsForDate,
|
||||||
|
onToggleHabit
|
||||||
}) => {
|
}) => {
|
||||||
const { theme } = useThemeContext();
|
const { theme } = useThemeContext();
|
||||||
|
|
||||||
|
|
@ -35,6 +37,11 @@ export const Calendar: React.FC<CalendarProps> = ({
|
||||||
// Get today's date in YYYY-MM-DD format
|
// Get today's date in YYYY-MM-DD format
|
||||||
const today = formatDate(new Date());
|
const today = formatDate(new Date());
|
||||||
|
|
||||||
|
const handleToggleHabit = async (e: React.MouseEvent, habitId: number, date: string) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
await onToggleHabit(habitId, date);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`rounded-lg shadow-md p-6 ${theme.calendar.background}`}>
|
<div className={`rounded-lg shadow-md p-6 ${theme.calendar.background}`}>
|
||||||
<div className="flex justify-between items-center mb-8">
|
<div className="flex justify-between items-center mb-8">
|
||||||
|
|
@ -151,6 +158,7 @@ export const Calendar: React.FC<CalendarProps> = ({
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<div className={`
|
<div className={`
|
||||||
|
pointer-events-auto
|
||||||
rounded-lg p-4
|
rounded-lg p-4
|
||||||
min-w-[200px] max-w-[300px]
|
min-w-[200px] max-w-[300px]
|
||||||
${theme.calendar.tooltip.background}
|
${theme.calendar.tooltip.background}
|
||||||
|
|
@ -158,7 +166,6 @@ export const Calendar: React.FC<CalendarProps> = ({
|
||||||
${theme.calendar.tooltip.shadow}
|
${theme.calendar.tooltip.shadow}
|
||||||
border
|
border
|
||||||
backdrop-blur-sm
|
backdrop-blur-sm
|
||||||
pointer-events-none
|
|
||||||
`}>
|
`}>
|
||||||
{completedHabits.length > 0 && (
|
{completedHabits.length > 0 && (
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
|
|
@ -167,8 +174,17 @@ export const Calendar: React.FC<CalendarProps> = ({
|
||||||
</span>
|
</span>
|
||||||
<ul className="space-y-1.5">
|
<ul className="space-y-1.5">
|
||||||
{completedHabits.map(habit => (
|
{completedHabits.map(habit => (
|
||||||
<li key={habit.id} className={`${theme.text} text-sm truncate`}>
|
<li
|
||||||
{habit.name}
|
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>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
@ -181,8 +197,17 @@ export const Calendar: React.FC<CalendarProps> = ({
|
||||||
</span>
|
</span>
|
||||||
<ul className="space-y-1.5">
|
<ul className="space-y-1.5">
|
||||||
{incompleteHabits.map(habit => (
|
{incompleteHabits.map(habit => (
|
||||||
<li key={habit.id} className={`${theme.text} text-sm truncate`}>
|
<li
|
||||||
{habit.name}
|
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>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,20 @@
|
||||||
import React, { createContext, useContext, useState, useEffect } from 'react';
|
import React, { createContext, useContext, useState, useEffect } from 'react';
|
||||||
import { Theme, useTheme } from '../styles/theme';
|
import { Theme, useTheme } from '../styles/theme';
|
||||||
|
|
||||||
|
type HabitSortOption = 'dateCreated' | 'alphabetical';
|
||||||
|
|
||||||
interface ThemeContextType {
|
interface ThemeContextType {
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
isDark: boolean;
|
isDark: boolean;
|
||||||
showStreaks: boolean;
|
showStreaks: boolean;
|
||||||
|
dailyReminder: boolean;
|
||||||
|
defaultView: 'habits' | 'calendar';
|
||||||
|
habitSort: HabitSortOption;
|
||||||
toggleDarkMode: () => void;
|
toggleDarkMode: () => void;
|
||||||
toggleStreaks: () => void;
|
toggleStreaks: () => void;
|
||||||
|
toggleDailyReminder: () => void;
|
||||||
|
setDefaultView: (view: 'habits' | 'calendar') => void;
|
||||||
|
setHabitSort: (sort: HabitSortOption) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
|
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 }) => {
|
export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
const [isDark, setIsDark] = useState(() => localStorage.getItem('darkMode') === 'true');
|
const [isDark, setIsDark] = useState(() => localStorage.getItem('darkMode') === 'true');
|
||||||
const [showStreaks, setShowStreaks] = useState(() => localStorage.getItem('showStreaks') !== 'false');
|
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);
|
const theme = useTheme(isDark);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -29,11 +44,97 @@ export const ThemeProvider: React.FC<{ children: React.ReactNode }> = ({ childre
|
||||||
localStorage.setItem('showStreaks', showStreaks.toString());
|
localStorage.setItem('showStreaks', showStreaks.toString());
|
||||||
}, [showStreaks]);
|
}, [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 toggleDarkMode = () => setIsDark(!isDark);
|
||||||
const toggleStreaks = () => setShowStreaks(!showStreaks);
|
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 (
|
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}
|
{children}
|
||||||
</ThemeContext.Provider>
|
</ThemeContext.Provider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue