improved mobile ui

This commit is contained in:
Harivansh Rathi 2024-11-21 15:55:52 -05:00
parent 36126e6a65
commit 1cd3445bd6
5 changed files with 233 additions and 131 deletions

View file

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

View file

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

View 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>
);
};

View file

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

View file

@ -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'
} }
}; };