mirror of
https://github.com/harivansh-afk/Habit-Tracker.git
synced 2026-04-17 13:05:08 +00:00
improved mobile ui
This commit is contained in:
parent
36126e6a65
commit
1cd3445bd6
5 changed files with 233 additions and 131 deletions
92
src/App.tsx
92
src/App.tsx
|
|
@ -10,6 +10,7 @@ import { AuthProvider, useAuth } from './contexts/AuthContext';
|
||||||
import { Login } from './components/Login';
|
import { Login } from './components/Login';
|
||||||
import { SignUp } from './components/SignUp';
|
import { SignUp } from './components/SignUp';
|
||||||
import { SettingsView } from './components/SettingsView';
|
import { SettingsView } from './components/SettingsView';
|
||||||
|
import { MobileNav } from './components/MobileNav';
|
||||||
|
|
||||||
const DAYS_OF_WEEK = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
const DAYS_OF_WEEK = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'];
|
||||||
|
|
||||||
|
|
@ -77,48 +78,56 @@ function HabitTrackerContent() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderHabitsView = () => (
|
const renderHabitsView = () => (
|
||||||
<div className="space-y-6">
|
<div className="flex-1">
|
||||||
<form onSubmit={handleAddHabit} className="flex gap-2">
|
<div className="max-w-5xl mx-auto">
|
||||||
<input
|
<div className="mb-8 flex items-center gap-4">
|
||||||
type="text"
|
<input
|
||||||
value={newHabit}
|
type="text"
|
||||||
onChange={(e) => setNewHabit(e.target.value)}
|
value={newHabit}
|
||||||
placeholder="Add a new habit"
|
onChange={(e) => setNewHabit(e.target.value)}
|
||||||
className={`flex-grow px-4 py-2 border rounded-lg ${theme.input}`}
|
onKeyPress={(e) => {
|
||||||
/>
|
if (e.key === 'Enter' && newHabit.trim()) {
|
||||||
<button
|
handleAddHabit(e);
|
||||||
type="submit"
|
}
|
||||||
className={`px-4 py-2 rounded-lg ${theme.button.primary}`}
|
}}
|
||||||
>
|
placeholder="Add a new habit"
|
||||||
Add Habit
|
className={`flex-1 px-4 py-2 rounded-lg ${theme.input}`}
|
||||||
</button>
|
/>
|
||||||
</form>
|
<button
|
||||||
|
onClick={handleAddHabit}
|
||||||
|
disabled={!newHabit.trim()}
|
||||||
|
className={`px-4 py-2 rounded-lg ${theme.button.primary} disabled:opacity-50`}
|
||||||
|
>
|
||||||
|
Add Habit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={`rounded-lg shadow p-6 ${theme.cardBackground}`}>
|
<div className="mb-6">
|
||||||
<div className="flex justify-between items-center mb-6">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-2xl font-bold dark:text-white">Your Habits</h2>
|
<h2 className={`text-xl font-bold ${theme.text}`}>Your Habits</h2>
|
||||||
<p className="text-sm text-gray-400 dark:text-gray-300 mt-1">Track your weekly progress</p>
|
<p className={`text-sm ${theme.mutedText}`}>Track your weekly progress</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex gap-2">
|
||||||
<button
|
|
||||||
onClick={goToCurrentWeek}
|
|
||||||
className={`px-4 py-2 rounded-lg ${theme.button.primary} text-sm`}
|
|
||||||
>
|
|
||||||
Today
|
|
||||||
</button>
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<button
|
<button
|
||||||
onClick={() => changeWeek('prev')}
|
onClick={() => changeWeek('prev')}
|
||||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full"
|
className={`p-2 rounded-lg ${theme.button.icon}`}
|
||||||
>
|
>
|
||||||
<ChevronLeft className="h-5 w-5 dark:text-white" />
|
<ChevronLeft className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setCurrentWeek(getCurrentWeekDates());
|
||||||
|
}}
|
||||||
|
className={`px-4 py-2 rounded-lg ${theme.button.secondary}`}
|
||||||
|
>
|
||||||
|
Today
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => changeWeek('next')}
|
onClick={() => changeWeek('next')}
|
||||||
className="p-2 hover:bg-gray-100 dark:hover:bg-gray-800 rounded-full"
|
className={`p-2 rounded-lg ${theme.button.icon}`}
|
||||||
>
|
>
|
||||||
<ChevronRight className="h-5 w-5 dark:text-white" />
|
<ChevronRight className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -132,7 +141,9 @@ function HabitTrackerContent() {
|
||||||
onUpdateHabit={updateHabit}
|
onUpdateHabit={updateHabit}
|
||||||
onDeleteHabit={deleteHabit}
|
onDeleteHabit={deleteHabit}
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-gray-500 dark:text-gray-300 mt-4">Keep up the good work! Consistency is key.</p>
|
<p className={`text-sm ${theme.mutedText} mt-4`}>
|
||||||
|
Keep up the good work! Consistency is key.
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -165,9 +176,14 @@ function HabitTrackerContent() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`min-h-screen ${theme.background}`}>
|
<div className={`min-h-screen ${theme.background}`}>
|
||||||
<div className="flex h-screen">
|
<div className="flex flex-col md:flex-row h-screen">
|
||||||
<Sidebar activeView={activeView} setActiveView={setActiveView} />
|
<div className="md:hidden">
|
||||||
<main className="flex-1 p-8">
|
<MobileNav activeView={activeView} setActiveView={setActiveView} />
|
||||||
|
</div>
|
||||||
|
<div className="hidden md:block">
|
||||||
|
<Sidebar activeView={activeView} setActiveView={setActiveView} />
|
||||||
|
</div>
|
||||||
|
<main className="flex-1 p-4 md:p-8 overflow-y-auto pb-24 md:pb-8">
|
||||||
{activeView === 'habits' && renderHabitsView()}
|
{activeView === 'habits' && renderHabitsView()}
|
||||||
{activeView === 'calendar' && renderCalendarView()}
|
{activeView === 'calendar' && renderCalendarView()}
|
||||||
{activeView === 'settings' && <SettingsView />}
|
{activeView === 'settings' && <SettingsView />}
|
||||||
|
|
|
||||||
|
|
@ -41,19 +41,14 @@ export function HabitList({
|
||||||
const date = new Date(dateStr);
|
const date = new Date(dateStr);
|
||||||
return (
|
return (
|
||||||
<th key={dateStr} className="px-4 py-2 text-center dark:text-white">
|
<th key={dateStr} className="px-4 py-2 text-center dark:text-white">
|
||||||
<div>{getDayName(dateStr)}</div>
|
<div className="hidden md:block">{getDayName(dateStr)}</div>
|
||||||
|
<div className="md:hidden">{getDayName(dateStr).slice(0, 1)}</div>
|
||||||
<div className="text-xs text-gray-500 dark:text-gray-400">
|
<div className="text-xs text-gray-500 dark:text-gray-400">
|
||||||
{date.getDate()}
|
{date.getDate()}
|
||||||
</div>
|
</div>
|
||||||
</th>
|
</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>
|
<th className="px-4 py-2 text-center dark:text-white">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
@ -65,9 +60,7 @@ export function HabitList({
|
||||||
type="text"
|
type="text"
|
||||||
value={habit.name}
|
value={habit.name}
|
||||||
onChange={(e) => onUpdateHabit(habit.id, e.target.value)}
|
onChange={(e) => onUpdateHabit(habit.id, e.target.value)}
|
||||||
aria-label="Habit name"
|
className="bg-transparent border-none focus:outline-none focus:ring-2 focus:ring-gray-300 rounded px-2 w-full"
|
||||||
placeholder="Enter habit name"
|
|
||||||
className="bg-transparent border-none focus:outline-none focus:ring-2 focus:ring-gray-300 rounded px-2"
|
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
{currentWeek.map((date) => (
|
{currentWeek.map((date) => (
|
||||||
|
|
@ -76,19 +69,18 @@ export function HabitList({
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={habit.completedDates.includes(date)}
|
checked={habit.completedDates.includes(date)}
|
||||||
onChange={() => {
|
onChange={() => onToggleHabit(habit.id, date)}
|
||||||
onToggleHabit(habit.id, date);
|
|
||||||
}}
|
|
||||||
aria-label={`Mark ${habit.name} as completed for ${date}`}
|
|
||||||
className="sr-only"
|
className="sr-only"
|
||||||
/>
|
/>
|
||||||
<div className={`
|
<div
|
||||||
w-6 h-6 rounded-md border-2 transition-all duration-200
|
className={`
|
||||||
${habit.completedDates.includes(date)
|
w-6 h-6 rounded-md border-2 transition-all duration-200
|
||||||
? 'bg-green-500 border-green-500'
|
${habit.completedDates.includes(date)
|
||||||
: 'border-gray-300 dark:border-gray-600 hover:border-green-400 dark:hover:border-green-400'}
|
? 'bg-green-500 border-green-500'
|
||||||
flex items-center justify-center
|
: '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) && (
|
{habit.completedDates.includes(date) && (
|
||||||
<svg
|
<svg
|
||||||
className="w-4 h-4 text-white"
|
className="w-4 h-4 text-white"
|
||||||
|
|
@ -106,20 +98,6 @@ export function HabitList({
|
||||||
</label>
|
</label>
|
||||||
</td>
|
</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">
|
<td className="px-4 py-2 text-center">
|
||||||
<button
|
<button
|
||||||
onClick={() => onDeleteHabit(habit.id)}
|
onClick={() => onDeleteHabit(habit.id)}
|
||||||
|
|
|
||||||
105
src/components/MobileNav.tsx
Normal file
105
src/components/MobileNav.tsx
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Plus, CalendarIcon, SettingsIcon, LogOut } from 'lucide-react';
|
||||||
|
import { useThemeContext } from '../contexts/ThemeContext';
|
||||||
|
import { useAuth } from '../contexts/AuthContext';
|
||||||
|
|
||||||
|
type View = 'habits' | 'calendar' | 'settings';
|
||||||
|
|
||||||
|
interface MobileNavProps {
|
||||||
|
activeView: View;
|
||||||
|
setActiveView: (view: View) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MobileNav: React.FC<MobileNavProps> = ({ activeView, setActiveView }) => {
|
||||||
|
const { theme } = useThemeContext();
|
||||||
|
const { signOut } = useAuth();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* Spacer to prevent content from being hidden behind the nav */}
|
||||||
|
<div className="h-20 md:hidden" />
|
||||||
|
|
||||||
|
<nav className={`
|
||||||
|
fixed bottom-4 left-4 right-4 md:hidden
|
||||||
|
${theme.cardBackground}
|
||||||
|
rounded-2xl shadow-lg backdrop-blur-lg
|
||||||
|
border ${theme.border}
|
||||||
|
z-50
|
||||||
|
`}>
|
||||||
|
<div className="flex justify-around items-center p-2">
|
||||||
|
<NavButton
|
||||||
|
active={activeView === 'habits'}
|
||||||
|
onClick={() => setActiveView('habits')}
|
||||||
|
icon={<Plus className="h-5 w-5" />}
|
||||||
|
label="Habits"
|
||||||
|
/>
|
||||||
|
<NavButton
|
||||||
|
active={activeView === 'calendar'}
|
||||||
|
onClick={() => setActiveView('calendar')}
|
||||||
|
icon={<CalendarIcon className="h-5 w-5" />}
|
||||||
|
label="Calendar"
|
||||||
|
/>
|
||||||
|
<NavButton
|
||||||
|
active={activeView === 'settings'}
|
||||||
|
onClick={() => setActiveView('settings')}
|
||||||
|
icon={<SettingsIcon className="h-5 w-5" />}
|
||||||
|
label="Settings"
|
||||||
|
/>
|
||||||
|
<NavButton
|
||||||
|
onClick={signOut}
|
||||||
|
icon={<LogOut className="h-5 w-5" />}
|
||||||
|
label="Sign Out"
|
||||||
|
variant="danger"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface NavButtonProps {
|
||||||
|
active?: boolean;
|
||||||
|
onClick: () => void;
|
||||||
|
icon: React.ReactNode;
|
||||||
|
label: string;
|
||||||
|
variant?: 'default' | 'danger';
|
||||||
|
}
|
||||||
|
|
||||||
|
const NavButton: React.FC<NavButtonProps> = ({
|
||||||
|
active = false,
|
||||||
|
onClick,
|
||||||
|
icon,
|
||||||
|
label,
|
||||||
|
variant = 'default'
|
||||||
|
}) => {
|
||||||
|
const { theme } = useThemeContext();
|
||||||
|
|
||||||
|
const baseStyles = "flex flex-col items-center justify-center px-3 py-2 rounded-xl transition-all duration-200";
|
||||||
|
const variantStyles = variant === 'danger'
|
||||||
|
? "text-red-500 hover:bg-red-500/10 active:bg-red-500/20"
|
||||||
|
: `${active ? theme.nav.active : theme.nav.inactive}`;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
onClick={onClick}
|
||||||
|
className={`
|
||||||
|
${baseStyles}
|
||||||
|
${variantStyles}
|
||||||
|
${active ? 'scale-95 shadow-inner' : 'hover:scale-105'}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div className={`
|
||||||
|
${active ? 'scale-95' : ''}
|
||||||
|
transition-transform duration-200
|
||||||
|
`}>
|
||||||
|
{icon}
|
||||||
|
</div>
|
||||||
|
<span className={`
|
||||||
|
text-xs mt-1 font-medium
|
||||||
|
${active ? 'opacity-100' : 'opacity-70'}
|
||||||
|
`}>
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
@ -15,58 +15,61 @@ export const Sidebar: React.FC<SidebarProps> = ({ activeView, setActiveView }) =
|
||||||
const { signOut } = useAuth();
|
const { signOut } = useAuth();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className={`w-64 border-r ${theme.border} ${theme.sidebarBackground} flex flex-col`}>
|
<nav className={`w-72 h-screen sticky top-0 border-r ${theme.border} ${theme.sidebarBackground} flex flex-col`}>
|
||||||
<div className="p-4">
|
<div className="p-6 border-b border-gray-200 dark:border-gray-700">
|
||||||
<h1 className={`text-2xl font-bold ${theme.text}`}>Habit Tracker</h1>
|
<h1 className={`text-2xl font-bold ${theme.text}`}>Habit Tracker</h1>
|
||||||
</div>
|
</div>
|
||||||
<ul className="space-y-2 p-4 flex-grow">
|
<div className="flex-grow overflow-y-auto">
|
||||||
<li>
|
<ul className="space-y-2 p-4">
|
||||||
<button
|
<li>
|
||||||
onClick={() => setActiveView('habits')}
|
<button
|
||||||
className={`w-full px-4 py-2 text-left rounded-lg ${
|
onClick={() => setActiveView('habits')}
|
||||||
activeView === 'habits'
|
className={`w-full px-6 py-3 text-left rounded-lg transition-all duration-200 flex items-center ${
|
||||||
? theme.button.secondary
|
activeView === 'habits'
|
||||||
: `${theme.text} ${theme.habitItem}`
|
? `${theme.button.secondary} shadow-md`
|
||||||
}`}
|
: `${theme.text} ${theme.habitItem} hover:translate-x-1`
|
||||||
>
|
}`}
|
||||||
<Plus className="inline-block mr-2 h-4 w-4" />
|
>
|
||||||
Habits
|
<Plus className="h-5 w-5 mr-3" />
|
||||||
</button>
|
<span className="font-medium">Habits</span>
|
||||||
</li>
|
</button>
|
||||||
<li>
|
</li>
|
||||||
<button
|
<li>
|
||||||
onClick={() => setActiveView('calendar')}
|
<button
|
||||||
className={`w-full px-4 py-2 text-left rounded-lg ${
|
onClick={() => setActiveView('calendar')}
|
||||||
activeView === 'calendar'
|
className={`w-full px-6 py-3 text-left rounded-lg transition-all duration-200 flex items-center ${
|
||||||
? theme.button.secondary
|
activeView === 'calendar'
|
||||||
: `${theme.text} ${theme.habitItem}`
|
? `${theme.button.secondary} shadow-md`
|
||||||
}`}
|
: `${theme.text} ${theme.habitItem} hover:translate-x-1`
|
||||||
>
|
}`}
|
||||||
<CalendarIcon className="inline-block mr-2 h-4 w-4" />
|
>
|
||||||
Calendar
|
<CalendarIcon className="h-5 w-5 mr-3" />
|
||||||
</button>
|
<span className="font-medium">Calendar</span>
|
||||||
</li>
|
</button>
|
||||||
<li>
|
</li>
|
||||||
<button
|
<li>
|
||||||
onClick={() => setActiveView('settings')}
|
<button
|
||||||
className={`w-full px-4 py-2 text-left rounded-lg ${
|
onClick={() => setActiveView('settings')}
|
||||||
activeView === 'settings'
|
className={`w-full px-6 py-3 text-left rounded-lg transition-all duration-200 flex items-center ${
|
||||||
? theme.button.secondary
|
activeView === 'settings'
|
||||||
: `${theme.text} ${theme.habitItem}`
|
? `${theme.button.secondary} shadow-md`
|
||||||
}`}
|
: `${theme.text} ${theme.habitItem} hover:translate-x-1`
|
||||||
>
|
}`}
|
||||||
<SettingsIcon className="inline-block mr-2 h-4 w-4" />
|
>
|
||||||
Settings
|
<SettingsIcon className="h-5 w-5 mr-3" />
|
||||||
</button>
|
<span className="font-medium">Settings</span>
|
||||||
</li>
|
</button>
|
||||||
</ul>
|
</li>
|
||||||
<div className="p-4 border-t border-gray-200">
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div className="p-4 border-t border-gray-200 dark:border-gray-700">
|
||||||
<button
|
<button
|
||||||
onClick={signOut}
|
onClick={signOut}
|
||||||
className={`w-full px-4 py-2 text-left rounded-lg ${theme.text} ${theme.habitItem}`}
|
className={`w-full px-6 py-3 text-left rounded-lg transition-all duration-200 flex items-center
|
||||||
|
${theme.text} ${theme.habitItem} hover:bg-red-500/10 hover:text-red-500`}
|
||||||
>
|
>
|
||||||
<LogOut className="inline-block mr-2 h-4 w-4" />
|
<LogOut className="h-5 w-5 mr-3" />
|
||||||
Sign Out
|
<span className="font-medium">Sign Out</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
|
||||||
|
|
@ -53,8 +53,8 @@ export const lightTheme = {
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
nav: {
|
nav: {
|
||||||
active: 'bg-[#f1f1ef] text-[#37352f]',
|
active: 'bg-[#f1f1ef]/80 text-[#37352f] shadow-inner',
|
||||||
inactive: 'text-[#37352f] hover:bg-[#f1f1ef]'
|
inactive: 'text-[#37352f] hover:bg-[#f1f1ef]/50'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -113,8 +113,8 @@ export const darkTheme = {
|
||||||
|
|
||||||
// Navigation
|
// Navigation
|
||||||
nav: {
|
nav: {
|
||||||
active: 'bg-[#363636] text-[#ffffff]',
|
active: 'bg-[#363636]/80 text-white shadow-inner',
|
||||||
inactive: 'text-[#ffffff] hover:bg-[#363636]'
|
inactive: 'text-white/70 hover:bg-[#363636]/50'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue