mirror of
https://github.com/harivansh-afk/Habit-Tracker.git
synced 2026-04-18 07:01:32 +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
|
|
@ -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<CalendarProps> = ({
|
||||
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 (
|
||||
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-lg p-6">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h2 className="text-2xl font-bold dark:text-white">
|
||||
<div className={`rounded-lg shadow-md p-6 ${theme.calendar.background}`}>
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<h2 className={`text-2xl font-bold ${theme.calendar.header}`}>
|
||||
{currentMonth.toLocaleString('default', { month: 'long', year: 'numeric' })}
|
||||
</h2>
|
||||
<div className="flex space-x-2">
|
||||
<div className="flex space-x-3">
|
||||
<button
|
||||
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
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-7 gap-4">
|
||||
{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}
|
||||
</div>
|
||||
))}
|
||||
|
|
@ -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 (
|
||||
<div
|
||||
key={date}
|
||||
className={`border dark:border-gray-700 rounded-lg p-3 min-h-[80px] relative group hover:shadow-md transition-shadow duration-200 ${
|
||||
!isCurrentMonth ? 'bg-gray-50 dark:bg-gray-800/50' : ''
|
||||
}`}
|
||||
className={`
|
||||
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 ${
|
||||
isCurrentMonth
|
||||
? 'dark:text-white'
|
||||
: 'text-gray-400 dark:text-gray-500'
|
||||
}`}>
|
||||
<span className={`font-medium ${isCurrentMonth ? theme.text : theme.calendar.day.otherMonth}`}>
|
||||
{dayNumber}
|
||||
</span>
|
||||
{habits.length > 0 && (
|
||||
<div className="absolute bottom-3 left-1/2 transform -translate-x-1/2">
|
||||
<div className="relative">
|
||||
<div className="group relative inline-block">
|
||||
<div
|
||||
className={`h-3 w-3 ${
|
||||
completedHabits.length > 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]`
|
||||
}
|
||||
`}
|
||||
/>
|
||||
<div className="absolute bottom-full left-1/2 transform -translate-x-1/2 mb-3 hidden group-hover:block">
|
||||
<div className="bg-white dark:bg-gray-800 text-sm rounded-lg shadow-lg p-4 border dark:border-gray-700 min-w-[200px]">
|
||||
<div
|
||||
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 && (
|
||||
<div className="mb-3">
|
||||
<span className="text-green-500 font-semibold block mb-1">
|
||||
<span className="text-[#2ecc71] font-semibold block mb-2">
|
||||
✓ Completed
|
||||
</span>
|
||||
<ul className="space-y-1">
|
||||
<ul className="space-y-1.5">
|
||||
{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}
|
||||
</li>
|
||||
))}
|
||||
|
|
@ -142,12 +172,12 @@ export function Calendar({
|
|||
)}
|
||||
{incompleteHabits.length > 0 && (
|
||||
<div>
|
||||
<span className="text-red-500 font-semibold block mb-1">
|
||||
<span className="text-[#e74c3c] font-semibold block mb-2">
|
||||
○ Pending
|
||||
</span>
|
||||
<ul className="space-y-1">
|
||||
<ul className="space-y-1.5">
|
||||
{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}
|
||||
</li>
|
||||
))}
|
||||
|
|
@ -166,4 +196,4 @@ export function Calendar({
|
|||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
|
@ -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 (
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="px-4 py-2 text-left dark:text-white">Habit</th>
|
||||
{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 (
|
||||
<th key={dateStr} className="px-4 py-2 text-center dark:text-white">
|
||||
<div>{daysOfWeek[index]}</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{displayDate.getDate()}
|
||||
</div>
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
<th className="px-4 py-2 text-center dark:text-white">Current Streak</th>
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{habits.map((habit) => (
|
||||
<tr key={habit.id} className="border-t dark:border-gray-700">
|
||||
<td className="px-4 py-2 dark:text-white">
|
||||
<input
|
||||
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"
|
||||
/>
|
||||
</td>
|
||||
{currentWeek.map((date) => (
|
||||
<td key={date} className="px-4 py-2 text-center">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left px-4 py-2 dark:text-white">Habit</th>
|
||||
{currentWeek.map((dateStr, index) => {
|
||||
const date = new Date(dateStr);
|
||||
const displayDate = new Date(date.getTime() + date.getTimezoneOffset() * 60000);
|
||||
|
||||
return (
|
||||
<th key={dateStr} className="px-4 py-2 text-center dark:text-white">
|
||||
<div>{daysOfWeek[index]}</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{displayDate.getDate()}
|
||||
</div>
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
{showStreaks && (
|
||||
<>
|
||||
<th className="px-4 py-2 text-center dark:text-white">Current Streak</th>
|
||||
<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>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{habits.map((habit) => (
|
||||
<tr key={habit.id} className="border-t dark:border-gray-700">
|
||||
<td className="px-4 py-2 dark:text-white">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={habit.completedDates.includes(date)}
|
||||
onChange={() => {
|
||||
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"
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
<td className="px-4 py-2 text-center">
|
||||
<span className="dark:text-white font-medium">
|
||||
{calculateStreak(habit.completedDates || []).currentStreak}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-2 text-center">
|
||||
<span className="dark:text-white font-medium">
|
||||
{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"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
{currentWeek.map((date) => (
|
||||
<td key={date} className="px-4 py-2 text-center">
|
||||
<label className="relative inline-block cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={habit.completedDates.includes(date)}
|
||||
onChange={() => {
|
||||
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="sr-only"
|
||||
/>
|
||||
<div className={`
|
||||
w-6 h-6 rounded-md border-2 transition-all duration-200
|
||||
${habit.completedDates.includes(date)
|
||||
? 'bg-green-500 border-green-500'
|
||||
: '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>
|
||||
);
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue