mirror of
https://github.com/harivansh-afk/Habit-Tracker.git
synced 2026-04-18 05:01:57 +00:00
added user auth
This commit is contained in:
parent
d113922158
commit
da9bd83f32
9 changed files with 506 additions and 308 deletions
|
|
@ -1,7 +1,8 @@
|
|||
import React, { useEffect } from 'react';
|
||||
import React from 'react';
|
||||
import { Trash2 } from 'lucide-react';
|
||||
import { Habit } from '../types';
|
||||
import { useThemeContext } from '../contexts/ThemeContext';
|
||||
import { calculateStreak } from '../utils/streakCalculator';
|
||||
|
||||
interface HabitListProps {
|
||||
habits: Habit[];
|
||||
|
|
@ -10,83 +11,9 @@ interface HabitListProps {
|
|||
onToggleHabit: (id: number, date: string) => void;
|
||||
onUpdateHabit: (id: number, name: string) => void;
|
||||
onDeleteHabit: (id: number) => void;
|
||||
onUpdateStreak: (id: number, newStreak: number) => Promise<void>;
|
||||
onUpdateStreak?: (id: number, newStreak: number) => Promise<void>;
|
||||
}
|
||||
|
||||
const calculateStreak = (completedDates: string[]): { currentStreak: number; bestStreak: number } => {
|
||||
if (!completedDates || completedDates.length === 0) {
|
||||
return { currentStreak: 0, bestStreak: 0 };
|
||||
}
|
||||
|
||||
// Get today at midnight in UTC
|
||||
const today = new Date();
|
||||
const utcToday = new Date(Date.UTC(today.getUTCFullYear(), today.getUTCMonth(), today.getUTCDate()));
|
||||
|
||||
const todayStr = utcToday.toISOString().split('T')[0];
|
||||
|
||||
// Sort dates in descending order (most recent first)
|
||||
const sortedDates = [...completedDates]
|
||||
.filter(date => !isNaN(new Date(date).getTime()))
|
||||
.sort((a, b) => new Date(b).getTime() - new Date(a).getTime());
|
||||
|
||||
let currentStreak = 0;
|
||||
let bestStreak = 0;
|
||||
let tempStreak = 0;
|
||||
|
||||
// Check if today is completed to maintain streak
|
||||
const hasTodayCompleted = sortedDates.includes(todayStr);
|
||||
|
||||
// Initialize current streak based on today's completion
|
||||
if (hasTodayCompleted) {
|
||||
currentStreak = 1;
|
||||
let checkDate = new Date(utcToday);
|
||||
|
||||
// Check previous days
|
||||
while (true) {
|
||||
checkDate.setUTCDate(checkDate.getUTCDate() - 1);
|
||||
const dateStr = checkDate.toISOString().split('T')[0];
|
||||
|
||||
if (sortedDates.includes(dateStr)) {
|
||||
currentStreak++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate best streak
|
||||
for (let i = 0; i < sortedDates.length; i++) {
|
||||
const currentDate = new Date(sortedDates[i]);
|
||||
currentDate.setHours(0, 0, 0, 0); // Normalize time part for comparison
|
||||
|
||||
if (i === 0) {
|
||||
tempStreak = 1;
|
||||
} else {
|
||||
const prevDate = new Date(sortedDates[i - 1]);
|
||||
const diffDays = Math.floor(
|
||||
(prevDate.getTime() - currentDate.getTime()) / (1000 * 60 * 60 * 24)
|
||||
);
|
||||
|
||||
if (diffDays === 1) {
|
||||
tempStreak++;
|
||||
} else if (diffDays === 0) {
|
||||
// Same day, skip
|
||||
continue;
|
||||
} else {
|
||||
// Reset streak on gap
|
||||
bestStreak = Math.max(bestStreak, tempStreak);
|
||||
tempStreak = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Final check for best streak
|
||||
bestStreak = Math.max(bestStreak, tempStreak);
|
||||
bestStreak = Math.max(bestStreak, currentStreak);
|
||||
|
||||
return { currentStreak, bestStreak };
|
||||
};
|
||||
|
||||
export function HabitList({
|
||||
habits,
|
||||
currentWeek,
|
||||
|
|
@ -94,17 +21,15 @@ export function HabitList({
|
|||
onToggleHabit,
|
||||
onUpdateHabit,
|
||||
onDeleteHabit,
|
||||
onUpdateStreak,
|
||||
}: HabitListProps) {
|
||||
const { showStreaks } = useThemeContext();
|
||||
|
||||
useEffect(() => {
|
||||
console.log('Current week dates:',
|
||||
currentWeek.map(date =>
|
||||
`${new Date(date).toLocaleDateString()} (${daysOfWeek[new Date(date).getDay() === 0 ? 6 : new Date(date).getDay() - 1]})`
|
||||
)
|
||||
);
|
||||
}, []);
|
||||
|
||||
// Helper function to get day name
|
||||
const getDayName = (dateStr: string) => {
|
||||
const date = new Date(dateStr);
|
||||
const dayIndex = date.getDay() === 0 ? 6 : date.getDay() - 1;
|
||||
return daysOfWeek[dayIndex];
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="overflow-x-auto">
|
||||
|
|
@ -112,13 +37,11 @@ export function HabitList({
|
|||
<thead>
|
||||
<tr>
|
||||
<th className="text-left px-4 py-2 dark:text-white">Habit</th>
|
||||
{currentWeek.map((dateStr, index) => {
|
||||
{currentWeek.map((dateStr) => {
|
||||
const date = new Date(dateStr);
|
||||
const dayIndex = date.getDay() === 0 ? 6 : date.getDay() - 1;
|
||||
|
||||
return (
|
||||
<th key={dateStr} className="px-4 py-2 text-center dark:text-white">
|
||||
<div>{daysOfWeek[dayIndex]}</div>
|
||||
<div>{getDayName(dateStr)}</div>
|
||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||
{date.getDate()}
|
||||
</div>
|
||||
|
|
@ -155,12 +78,6 @@ export function HabitList({
|
|||
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"
|
||||
|
|
|
|||
67
src/components/Login.tsx
Normal file
67
src/components/Login.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { useState } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useThemeContext } from '../contexts/ThemeContext';
|
||||
|
||||
export function Login({ onSwitchToSignUp }: { onSwitchToSignUp: () => void }) {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const { signIn } = useAuth();
|
||||
const { theme } = useThemeContext();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
setError('');
|
||||
await signIn(email, password);
|
||||
} catch (error) {
|
||||
setError('Failed to sign in');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className={`max-w-md w-full p-6 rounded-lg shadow-lg ${theme.cardBackground}`}>
|
||||
<h2 className={`text-2xl font-bold mb-6 ${theme.text}`}>Log In</h2>
|
||||
{error && <div className="text-red-500 mb-4">{error}</div>}
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Email"
|
||||
className={`w-full px-4 py-2 rounded-lg ${theme.input}`}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Password"
|
||||
className={`w-full px-4 py-2 rounded-lg ${theme.input}`}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className={`w-full py-2 rounded-lg ${theme.button.primary}`}
|
||||
>
|
||||
Log In
|
||||
</button>
|
||||
</form>
|
||||
<p className={`mt-4 text-center ${theme.text}`}>
|
||||
Don't have an account?{' '}
|
||||
<button
|
||||
onClick={onSwitchToSignUp}
|
||||
className="text-blue-500 hover:underline"
|
||||
>
|
||||
Sign Up
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
120
src/components/SettingsView.tsx
Normal file
120
src/components/SettingsView.tsx
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
import React from 'react';
|
||||
import { Sun, Moon } from 'lucide-react';
|
||||
import { useThemeContext } from '../contexts/ThemeContext';
|
||||
|
||||
export function SettingsView() {
|
||||
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-xl font-semibold mb-6 dark:text-white">Settings</h2>
|
||||
|
||||
{/* Theme Toggle */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-medium mb-2 dark:text-white">Theme</h3>
|
||||
<button
|
||||
onClick={toggleDarkMode}
|
||||
className={`flex items-center space-x-2 px-4 py-2 rounded-lg transition-colors duration-200
|
||||
${isDark
|
||||
? 'bg-gray-700 text-white hover:bg-gray-600'
|
||||
: 'bg-gray-100 text-gray-900 hover:bg-gray-200'}`}
|
||||
>
|
||||
{isDark ? <Moon className="h-5 w-5" /> : <Sun className="h-5 w-5" />}
|
||||
<span>{isDark ? 'Dark Mode' : 'Light Mode'}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Streaks Toggle */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-medium mb-2 dark:text-white">Streaks</h3>
|
||||
<label className="flex items-center cursor-pointer">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="sr-only"
|
||||
checked={showStreaks}
|
||||
onChange={toggleStreaks}
|
||||
/>
|
||||
<div className={`w-10 h-6 rounded-full transition-colors duration-200
|
||||
${showStreaks ? 'bg-green-500' : 'bg-gray-300'}`}>
|
||||
<div className={`absolute w-4 h-4 rounded-full bg-white transition-transform duration-200 transform
|
||||
${showStreaks ? 'translate-x-5' : 'translate-x-1'} top-1`} />
|
||||
</div>
|
||||
</div>
|
||||
<span className="ml-3 dark:text-white">Show Streaks</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Daily Reminder Toggle */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-medium mb-2 dark:text-white">Notifications</h3>
|
||||
<label className="flex items-center cursor-pointer">
|
||||
<div className="relative">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="sr-only"
|
||||
checked={dailyReminder}
|
||||
onChange={handleReminderToggle}
|
||||
/>
|
||||
<div className={`w-10 h-6 rounded-full transition-colors duration-200
|
||||
${dailyReminder ? 'bg-green-500' : 'bg-gray-300'}`}>
|
||||
<div className={`absolute w-4 h-4 rounded-full bg-white transition-transform duration-200 transform
|
||||
${dailyReminder ? 'translate-x-5' : 'translate-x-1'} top-1`} />
|
||||
</div>
|
||||
</div>
|
||||
<span className="ml-3 dark:text-white">Daily Reminder</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Default View */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-medium mb-2 dark:text-white">Default View</h3>
|
||||
<select
|
||||
value={defaultView}
|
||||
onChange={(e) => setDefaultView(e.target.value as 'habits' | 'calendar')}
|
||||
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 sm:text-sm rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
>
|
||||
<option value="habits">Habits</option>
|
||||
<option value="calendar">Calendar</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Habit Sort */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-lg font-medium mb-2 dark:text-white">Sort Habits By</h3>
|
||||
<select
|
||||
value={habitSort}
|
||||
onChange={(e) => setHabitSort(e.target.value as 'dateCreated' | 'alphabetical')}
|
||||
className="mt-1 block w-full pl-3 pr-10 py-2 text-base border-gray-300 focus:outline-none focus:ring-2 focus:ring-green-500 focus:border-green-500 sm:text-sm rounded-md dark:bg-gray-700 dark:border-gray-600 dark:text-white"
|
||||
>
|
||||
<option value="dateCreated">Date Created</option>
|
||||
<option value="alphabetical">Alphabetical</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
import React from 'react';
|
||||
import { Plus, CalendarIcon, SettingsIcon } from 'lucide-react';
|
||||
import { Plus, CalendarIcon, SettingsIcon, LogOut } from 'lucide-react';
|
||||
import { useThemeContext } from '../contexts/ThemeContext';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
|
||||
type View = 'habits' | 'calendar' | 'settings';
|
||||
|
||||
|
|
@ -11,13 +12,14 @@ interface SidebarProps {
|
|||
|
||||
export const Sidebar: React.FC<SidebarProps> = ({ activeView, setActiveView }) => {
|
||||
const { theme } = useThemeContext();
|
||||
const { signOut } = useAuth();
|
||||
|
||||
return (
|
||||
<nav className={`w-64 border-r ${theme.border} ${theme.sidebarBackground}`}>
|
||||
<nav className={`w-64 border-r ${theme.border} ${theme.sidebarBackground} flex flex-col`}>
|
||||
<div className="p-4">
|
||||
<h1 className={`text-2xl font-bold ${theme.text}`}>Habit Tracker</h1>
|
||||
</div>
|
||||
<ul className="space-y-2 p-4">
|
||||
<ul className="space-y-2 p-4 flex-grow">
|
||||
<li>
|
||||
<button
|
||||
onClick={() => setActiveView('habits')}
|
||||
|
|
@ -58,6 +60,15 @@ export const Sidebar: React.FC<SidebarProps> = ({ activeView, setActiveView }) =
|
|||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="p-4 border-t border-gray-200">
|
||||
<button
|
||||
onClick={signOut}
|
||||
className={`w-full px-4 py-2 text-left rounded-lg ${theme.text} ${theme.habitItem}`}
|
||||
>
|
||||
<LogOut className="inline-block mr-2 h-4 w-4" />
|
||||
Sign Out
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
};
|
||||
67
src/components/SignUp.tsx
Normal file
67
src/components/SignUp.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { useState } from 'react';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useThemeContext } from '../contexts/ThemeContext';
|
||||
|
||||
export function SignUp({ onSwitchToLogin }: { onSwitchToLogin: () => void }) {
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [error, setError] = useState('');
|
||||
const { signUp } = useAuth();
|
||||
const { theme } = useThemeContext();
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
try {
|
||||
setError('');
|
||||
await signUp(email, password);
|
||||
} catch (error) {
|
||||
setError('Failed to create an account');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className={`max-w-md w-full p-6 rounded-lg shadow-lg ${theme.cardBackground}`}>
|
||||
<h2 className={`text-2xl font-bold mb-6 ${theme.text}`}>Sign Up</h2>
|
||||
{error && <div className="text-red-500 mb-4">{error}</div>}
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div>
|
||||
<input
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="Email"
|
||||
className={`w-full px-4 py-2 rounded-lg ${theme.input}`}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<input
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Password"
|
||||
className={`w-full px-4 py-2 rounded-lg ${theme.input}`}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
className={`w-full py-2 rounded-lg ${theme.button.primary}`}
|
||||
>
|
||||
Sign Up
|
||||
</button>
|
||||
</form>
|
||||
<p className={`mt-4 text-center ${theme.text}`}>
|
||||
Already have an account?{' '}
|
||||
<button
|
||||
onClick={onSwitchToLogin}
|
||||
className="text-blue-500 hover:underline"
|
||||
>
|
||||
Log In
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue