From 6b6cba21e5d50d5bba0b8ffd4a887e9337cf4c0c Mon Sep 17 00:00:00 2001 From: rathi Date: Wed, 20 Nov 2024 14:44:45 -0500 Subject: [PATCH] adding today feature on main habit page, show streaks feature on settings page and improved dark mode functionality --- habits.db | Bin 20480 -> 28672 bytes src/App.tsx | 403 +++++++++++++--------------------- src/components/Calendar.tsx | 112 ++++++---- src/components/HabitList.tsx | 183 ++++++++------- src/components/Sidebar.tsx | 63 ++++++ src/contexts/ThemeContext.tsx | 48 ++++ src/hooks/useHabits.ts | 99 +++++++++ src/hooks/useWeek.ts | 34 +++ src/styles/theme.ts | 125 +++++++++++ 9 files changed, 705 insertions(+), 362 deletions(-) create mode 100644 src/components/Sidebar.tsx create mode 100644 src/contexts/ThemeContext.tsx create mode 100644 src/hooks/useHabits.ts create mode 100644 src/hooks/useWeek.ts create mode 100644 src/styles/theme.ts diff --git a/habits.db b/habits.db index 4ce22e1dcb7d00660a230346c2f099288b90191a..0a8f677c783f0cf8281a4e4afe5b0e673e1db34c 100644 GIT binary patch literal 28672 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 WiZ+A*#+o;?8gQ0Gs4yXBtsMYIVy~V6 delta 663 zcmZp8z}T>Wae@>ZV=@B+1M5TuJ0`~DjR{Nmxflc(82G>Quj0SInNQ#xze;IZdO>D> zaau}xX<}M>W`0UqVrhCoer8HqI=dD#gJ?-=amgly|NQmZy!@9Lia8E5@PFig#D9c; z3;z=SDf}(`CHyJ;A^a}zw}o#B-xR(Uz7jqYJ{3L@z7)O? zJ{LX~-Y>jQc(3rL@P_cV@Rsl%;oZW!gm((B3$F>U3aZS;8}g zr-i44Cxs`3$EBXfghz!(golOu3-=T5E8Iu8w{S1vp2FS2UBaEh9m4IxZNja>EyB&h z^@ZyQ*A}iNTu->Ja82QA;VR)u;R@k$;WFV;;S%9u;rzn+g!2mL5zZ}~OE{-+ws4kk zrf`OEx^S9ss&I;MvT%Iic*1do<1hm%*f`-B`{qj)0_^n+3=I5=Mg~SEx`u|jh9Uk&dYVlh0=JD3^ zeCK(_bCu@^uOu%APXo_Hp3OXqc=CCCdE$6%xEFIz;?d^*!Tp^3DEBq)Ej&^@oZOAv ze%$fg1>CmWI^5FSTwFi7UT|IKI>xn?YYCS>R{~cd*JQ3HE<4VboHsa+b8h2Y$~lEg zmrI6=oAVcE0H-~t9;Ym)Y$9h7XEVo5j#r#K9KShEaI|m~b0l#Da7^V`#<6{4V-ox3 W1dm2`0R{#J0Y*eHF;9LNFAo5-*sy2- diff --git a/src/App.tsx b/src/App.tsx index 519f628..18bca0f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,17 +1,35 @@ 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 { 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() { - const [habits, setHabits] = useState([]); +function HabitTrackerContent() { + const { theme, isDark, toggleDarkMode } = useThemeContext(); const [newHabit, setNewHabit] = useState(''); - const [currentWeek, setCurrentWeek] = useState([]); const [activeView, setActiveView] = useState<'habits' | 'calendar' | 'settings'>('habits'); - const [darkMode, setDarkMode] = useState(() => localStorage.getItem('darkMode') === 'true'); 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']; useEffect(() => { @@ -19,104 +37,16 @@ export default function HabitTracker() { setCurrentWeek(getCurrentWeekDates()); }, []); - useEffect(() => { - 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) => { + const handleAddHabit = async (e: React.FormEvent) => { e.preventDefault(); if (newHabit.trim()) { - try { - const response = await fetch('http://localhost:5000/api/habits', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ name: newHabit }), - }); - if (response.ok) { - setNewHabit(''); - fetchHabits(); - } - } catch (error) { - console.error('Error adding habit:', error); + const success = await addHabitApi(newHabit); + if (success) { + setNewHabit(''); } } }; - 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') => { setCurrentMonth(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) => { return habits.filter(habit => habit.completedDates.includes(date)); }; - const handleUpdateStreak = async (id: number, newStreak: number) => { - // Prevent negative streaks - if (newStreak < 0) return; + const goToCurrentWeek = () => { + setCurrentWeek(getCurrentWeekDates()); + }; - try { - // Update in database - await fetch(`http://localhost:5000/api/habits/${id}/streak`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ streak: newStreak }), - }); + const renderHabitsView = () => ( +
+
+ setNewHabit(e.target.value)} + placeholder="Add a new habit" + className={`flex-grow px-4 py-2 border rounded-lg ${theme.input}`} + /> + +
- // Update state - setHabits(habits.map(habit => - habit.id === id ? { ...habit, manualStreak: newStreak } : habit - )); - } catch (error) { - console.error('Error updating streak:', error); - } +
+
+

Weekly Progress

+
+ +
+ + +
+
+
+ + +
+
+ ); + + const renderCalendarView = () => ( + new Date(year, month + 1, 0).getDate()} + getCompletedHabitsForDate={getCompletedHabitsForDate} + /> + ); + + const renderSettingsView = () => { + const { theme, isDark, showStreaks, toggleDarkMode, toggleStreaks } = useThemeContext(); + + return ( +
+

Settings

+
+
+ Dark Mode + +
+ +
+ Show Streaks + +
+
+
+ ); }; return ( -
+
- - +
- {activeView === 'habits' && ( -
-
- 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" - /> - -
- -
-
-

Weekly Progress

-
- - -
-
- - -
-
- )} - - {activeView === 'calendar' && ( - - )} - - {activeView === 'settings' && ( -
-

Settings

-
-
- Dark Mode - -
-
-
- )} + {activeView === 'habits' && renderHabitsView()} + {activeView === 'calendar' && renderCalendarView()} + {activeView === 'settings' && renderSettingsView()}
); +} + +export default function HabitTracker() { + return ( + + + + ); } \ No newline at end of file diff --git a/src/components/Calendar.tsx b/src/components/Calendar.tsx index 1c236ea..dd2fab5 100644 --- a/src/components/Calendar.tsx +++ b/src/components/Calendar.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { ChevronLeft, ChevronRight } from 'lucide-react'; +import { useThemeContext } from '../contexts/ThemeContext'; import { Habit } from '../types'; interface CalendarProps { @@ -10,46 +11,55 @@ interface CalendarProps { getCompletedHabitsForDate: (date: string) => Habit[]; } -export function Calendar({ +export const Calendar: React.FC = ({ currentMonth, habits, onChangeMonth, getDaysInMonth, getCompletedHabitsForDate -}: CalendarProps) { +}) => { + const { theme } = useThemeContext(); + const daysOfWeek = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; const getFirstDayOfMonth = (year: number, month: number) => { 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; }; + // 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 ( -
-
-

+
+
+

{currentMonth.toLocaleString('default', { month: 'long', year: 'numeric' })}

-
+
{daysOfWeek.map(day => ( -
+
{day}
))} @@ -61,13 +71,12 @@ export function Calendar({ const daysInMonth = getDaysInMonth(year, month); const daysInPrevMonth = getDaysInMonth(year, month - 1); - // Calculate days to show const days = []; // Previous month days for (let i = 0; i < firstDayOfMonth; i++) { 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({ date, dayNumber: day, @@ -77,7 +86,7 @@ export function Calendar({ // Current month days 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({ date, dayNumber: i, @@ -85,10 +94,10 @@ export function Calendar({ }); } - // Next month days to complete the grid - const remainingDays = 42 - days.length; // 6 rows * 7 days + // Next month days + const remainingDays = 42 - days.length; 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({ date, dayNumber: i, @@ -99,41 +108,62 @@ export function Calendar({ return days.map(({ date, dayNumber, isCurrentMonth }) => { const completedHabits = getCompletedHabitsForDate(date); const incompleteHabits = habits.filter(habit => !habit.completedDates.includes(date)); + const isToday = date === today; return (
- + {dayNumber} {habits.length > 0 && (
-
+
0 - ? 'bg-green-500 shadow-sm shadow-green-200' - : 'bg-gray-300 dark:bg-gray-600' - } rounded-full transition-colors duration-200`} + className={` + h-4 w-4 rounded-full cursor-pointer + transition-colors duration-200 + ${completedHabits.length > 0 + ? 'bg-[#2ecc71] dark:bg-[#2ecc71] shadow-sm shadow-[#2ecc7150]' + : `bg-[#e9e9e8] dark:bg-[#393939]` + } + `} /> -
-
+
+
{completedHabits.length > 0 && (
- + ✓ Completed -
    +
      {completedHabits.map(habit => ( -
    • +
    • {habit.name}
    • ))} @@ -142,12 +172,12 @@ export function Calendar({ )} {incompleteHabits.length > 0 && (
      - + ○ Pending -
        +
          {incompleteHabits.map(habit => ( -
        • +
        • {habit.name}
        • ))} @@ -166,4 +196,4 @@ export function Calendar({
); -} \ No newline at end of file +}; \ No newline at end of file diff --git a/src/components/HabitList.tsx b/src/components/HabitList.tsx index 2361676..1cd223b 100644 --- a/src/components/HabitList.tsx +++ b/src/components/HabitList.tsx @@ -1,6 +1,7 @@ import React, { useEffect } from 'react'; import { Trash2 } from 'lucide-react'; import { Habit } from '../types'; +import { useThemeContext } from '../contexts/ThemeContext'; interface HabitListProps { habits: Habit[]; @@ -99,6 +100,8 @@ export function HabitList({ onDeleteHabit, onUpdateStreak, }: HabitListProps) { + const { showStreaks } = useThemeContext(); + useEffect(() => { console.log('Current week dates:', currentWeek.map(date => @@ -108,82 +111,114 @@ export function HabitList({ }, []); return ( - - - - - {currentWeek.map((dateStr, index) => { - const date = new Date(dateStr); - // Ensure date is interpreted in local timezone - const displayDate = new Date(date.getTime() + date.getTimezoneOffset() * 60000); - - return ( - - ); - })} - - - - - - - {habits.map((habit) => ( - - - {currentWeek.map((date) => ( - + ))} + {showStreaks && ( + <> + + + + )} + + + ))} + +
Habit -
{daysOfWeek[index]}
-
- {displayDate.getDate()} -
-
Current StreakBest StreakActions
- onUpdateHabit(habit.id, e.target.value)} - 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" - /> - +
+ + + + + {currentWeek.map((dateStr, index) => { + const date = new Date(dateStr); + const displayDate = new Date(date.getTime() + date.getTimezoneOffset() * 60000); + + return ( + + ); + })} + {showStreaks && ( + <> + + + + )} + + + + + {habits.map((habit) => ( + + - ))} - - - - - ))} - -
Habit +
{daysOfWeek[index]}
+
+ {displayDate.getDate()} +
+
Current StreakBest StreakActions
{ - onToggleHabit(habit.id, date); - const newCompletedDates = habit.completedDates.includes(date) - ? habit.completedDates.filter(d => d !== date) - : [...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" + type="text" + value={habit.name} + onChange={(e) => onUpdateHabit(habit.id, e.target.value)} + 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" /> - - {calculateStreak(habit.completedDates || []).currentStreak} - - - - {calculateStreak(habit.completedDates || []).bestStreak} - - - -
+ {currentWeek.map((date) => ( +
+ + + + {calculateStreak(habit.completedDates || []).currentStreak} + + + + {calculateStreak(habit.completedDates || []).bestStreak} + + + +
+
); } diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx new file mode 100644 index 0000000..f653652 --- /dev/null +++ b/src/components/Sidebar.tsx @@ -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 = ({ activeView, setActiveView }) => { + const { theme } = useThemeContext(); + + return ( + + ); +}; \ No newline at end of file diff --git a/src/contexts/ThemeContext.tsx b/src/contexts/ThemeContext.tsx new file mode 100644 index 0000000..13eb285 --- /dev/null +++ b/src/contexts/ThemeContext.tsx @@ -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(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 ( + + {children} + + ); +}; + +export const useThemeContext = () => { + const context = useContext(ThemeContext); + if (context === undefined) { + throw new Error('useThemeContext must be used within a ThemeProvider'); + } + return context; +}; \ No newline at end of file diff --git a/src/hooks/useHabits.ts b/src/hooks/useHabits.ts new file mode 100644 index 0000000..7b8002b --- /dev/null +++ b/src/hooks/useHabits.ts @@ -0,0 +1,99 @@ +import { useState } from 'react'; +import { Habit } from '../types'; + +export const useHabits = () => { + const [habits, setHabits] = useState([]); + + 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 + }; +}; \ No newline at end of file diff --git a/src/hooks/useWeek.ts b/src/hooks/useWeek.ts new file mode 100644 index 0000000..9e82a5e --- /dev/null +++ b/src/hooks/useWeek.ts @@ -0,0 +1,34 @@ +import { useState } from 'react'; + +export const useWeek = () => { + const [currentWeek, setCurrentWeek] = useState([]); + + 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 + }; +}; \ No newline at end of file diff --git a/src/styles/theme.ts b/src/styles/theme.ts new file mode 100644 index 0000000..8df1968 --- /dev/null +++ b/src/styles/theme.ts @@ -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; +}; \ No newline at end of file