added features and settings

This commit is contained in:
Harivansh Rathi 2024-11-20 18:46:31 -05:00
parent 0ab0745a2c
commit d79452d6ae
4 changed files with 263 additions and 14 deletions

BIN
habits.db

Binary file not shown.

View file

@ -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>
);

View file

@ -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>

View file

@ -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>
);