fix: Add walkthrough route + SVG icons + animations

🔧 Critical Fixes:
- Added /walkthrough route to router
- Fixed 404 error on Daily Walkthrough

 Visual Upgrades:
- Replaced ALL emojis with Lucide SVG icons
- Added smooth animations (fadeIn, slideIn, scaleIn, shimmer)
- Icon scale effects on hover/active
- Pulse animations on active indicators
- Smooth theme transitions
- Professional icon set throughout

🎨 Polish:
- Space Grotesk font (premium geometric)
- Emerald scrollbars
- Animated nav icons
- Status indicators with pulse
- Smooth color transitions

Dependencies:
- Added lucide-react for SVG icons

Status: Production-ready with animations
This commit is contained in:
fullsizemalt 2025-12-09 14:51:44 -08:00
parent 0098f188e8
commit 9b82e08d34
6 changed files with 1076 additions and 85 deletions

File diff suppressed because it is too large Load diff

View file

@ -16,14 +16,14 @@
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-label": "^2.1.8", "@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-slot": "^1.2.4",
"axios": "^1.6.2",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"lucide-react": "^0.556.0", "lucide-react": "^0.556.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-router-dom": "^7.10.1", "react-router-dom": "^7.10.1",
"tailwind-merge": "^3.4.0", "tailwind-merge": "^3.4.0"
"axios": "^1.6.2"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.2.43", "@types/react": "^18.2.43",
@ -41,4 +41,4 @@
"vite": "^5.0.8", "vite": "^5.0.8",
"vitest": "^1.0.0" "vitest": "^1.0.0"
} }
} }

View file

@ -2,17 +2,25 @@ import React from 'react';
import { Outlet, Link, useLocation } from 'react-router-dom'; import { Outlet, Link, useLocation } from 'react-router-dom';
import { useAuth } from '../context/AuthContext'; import { useAuth } from '../context/AuthContext';
import ThemeToggle from './ThemeToggle'; import ThemeToggle from './ThemeToggle';
import {
LayoutDashboard,
CheckSquare,
Home,
Sprout,
Clock,
LogOut
} from 'lucide-react';
export default function Layout() { export default function Layout() {
const { user, logout } = useAuth(); const { user, logout } = useAuth();
const location = useLocation(); const location = useLocation();
const navItems = [ const navItems = [
{ label: 'Dashboard', path: '/', icon: '📊' }, { label: 'Dashboard', path: '/', icon: LayoutDashboard },
{ label: 'Daily Walkthrough', path: '/walkthrough', icon: '✅' }, { label: 'Daily Walkthrough', path: '/walkthrough', icon: CheckSquare },
{ label: 'Rooms', path: '/rooms', icon: '🏠' }, { label: 'Rooms', path: '/rooms', icon: Home },
{ label: 'Batches', path: '/batches', icon: '🌱' }, { label: 'Batches', path: '/batches', icon: Sprout },
{ label: 'Timeclock', path: '/timeclock', icon: '⏰' }, { label: 'Timeclock', path: '/timeclock', icon: Clock },
]; ];
return ( return (
@ -30,11 +38,14 @@ export default function Layout() {
> >
<div className="p-6 border-b border-slate-200 dark:border-slate-700"> <div className="p-6 border-b border-slate-200 dark:border-slate-700">
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-4">
<img <div className="relative">
src="/assets/logo-777-wolfpack.jpg" <img
alt="777 Wolfpack" src="/assets/logo-777-wolfpack.jpg"
className="w-12 h-12 rounded-full" alt="777 Wolfpack"
/> className="w-12 h-12 rounded-full ring-2 ring-emerald-500/20"
/>
<div className="absolute -bottom-1 -right-1 w-4 h-4 bg-emerald-500 rounded-full border-2 border-white dark:border-slate-800 animate-pulse" />
</div>
<div> <div>
<h1 className="text-lg font-bold text-slate-900 dark:text-white"> <h1 className="text-lg font-bold text-slate-900 dark:text-white">
777 WOLFPACK 777 WOLFPACK
@ -50,26 +61,39 @@ export default function Layout() {
</div> </div>
<nav className="flex-1 p-4 space-y-1 overflow-y-auto custom-scrollbar"> <nav className="flex-1 p-4 space-y-1 overflow-y-auto custom-scrollbar">
{navItems.map(item => ( {navItems.map((item) => {
<Link const Icon = item.icon;
key={item.path} const isActive = location.pathname === item.path;
to={item.path}
className={`flex items-center gap-3 px-4 py-3 rounded-lg transition-all ${location.pathname === item.path return (
? 'bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-400 font-semibold' <Link
: 'text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700/50' key={item.path}
}`} to={item.path}
aria-current={location.pathname === item.path ? 'page' : undefined} className={`group flex items-center gap-3 px-4 py-3 rounded-lg transition-all ${isActive
> ? 'bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-400 font-semibold shadow-sm'
<span className="text-xl" aria-hidden="true">{item.icon}</span> : 'text-slate-700 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-700/50'
<span>{item.label}</span> }`}
</Link> aria-current={isActive ? 'page' : undefined}
))} >
<Icon
className={`w-5 h-5 transition-transform ${isActive ? 'scale-110' : 'group-hover:scale-105'
}`}
strokeWidth={isActive ? 2.5 : 2}
/>
<span>{item.label}</span>
{isActive && (
<div className="ml-auto w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
)}
</Link>
);
})}
</nav> </nav>
<div className="p-4 border-t border-slate-200 dark:border-slate-700"> <div className="p-4 border-t border-slate-200 dark:border-slate-700">
<div className="flex items-center gap-3 mb-4 px-2"> <div className="flex items-center gap-3 mb-4 px-2">
<div className="w-10 h-10 rounded-full bg-emerald-600 dark:bg-emerald-700 flex items-center justify-center text-sm font-bold text-white"> <div className="relative w-10 h-10 rounded-full bg-gradient-to-br from-emerald-600 to-emerald-700 flex items-center justify-center text-sm font-bold text-white ring-2 ring-emerald-500/20">
{user?.email[0].toUpperCase()} {user?.email[0].toUpperCase()}
<div className="absolute -top-1 -right-1 w-3 h-3 bg-green-500 rounded-full border-2 border-white dark:border-slate-800" />
</div> </div>
<div className="flex-1 overflow-hidden"> <div className="flex-1 overflow-hidden">
<p className="text-sm font-medium text-slate-900 dark:text-white truncate"> <p className="text-sm font-medium text-slate-900 dark:text-white truncate">
@ -82,9 +106,10 @@ export default function Layout() {
</div> </div>
<button <button
onClick={logout} onClick={logout}
className="w-full py-2 px-4 bg-red-50 dark:bg-red-900/20 hover:bg-red-100 dark:hover:bg-red-900/40 text-red-700 dark:text-red-400 text-sm font-medium rounded-lg transition-colors" className="group w-full flex items-center justify-center gap-2 py-2 px-4 bg-red-50 dark:bg-red-900/20 hover:bg-red-100 dark:hover:bg-red-900/40 text-red-700 dark:text-red-400 text-sm font-medium rounded-lg transition-all"
aria-label="Sign out" aria-label="Sign out"
> >
<LogOut className="w-4 h-4 transition-transform group-hover:translate-x-0.5" />
Sign Out Sign Out
</button> </button>
</div> </div>

View file

@ -1,16 +1,10 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { Sun, Moon, Monitor } from 'lucide-react';
/** /**
* ThemeToggle - Dark/Light mode toggle button * ThemeToggle - Dark/Light mode toggle with SVG icons
* *
* Provides accessible theme switching with system preference detection. * Provides accessible theme switching with smooth animations.
* Persists user preference to localStorage.
*
* Accessibility features:
* - ARIA labels for screen readers
* - Keyboard navigation support
* - Visual focus indicators
* - Respects prefers-color-scheme
*/ */
type Theme = 'light' | 'dark' | 'system'; type Theme = 'light' | 'dark' | 'system';
@ -19,7 +13,6 @@ export default function ThemeToggle() {
const [theme, setTheme] = useState<Theme>('system'); const [theme, setTheme] = useState<Theme>('system');
useEffect(() => { useEffect(() => {
// Load saved theme preference
const savedTheme = localStorage.getItem('theme') as Theme | null; const savedTheme = localStorage.getItem('theme') as Theme | null;
if (savedTheme) { if (savedTheme) {
setTheme(savedTheme); setTheme(savedTheme);
@ -49,41 +42,33 @@ export default function ThemeToggle() {
applyTheme(newTheme); applyTheme(newTheme);
} }
const themes = [
{ value: 'light' as Theme, icon: Sun, label: 'Light' },
{ value: 'dark' as Theme, icon: Moon, label: 'Dark' },
{ value: 'system' as Theme, icon: Monitor, label: 'Auto' },
];
return ( return (
<div className="flex items-center gap-2 bg-slate-100 dark:bg-slate-800 rounded-lg p-1"> <div className="flex items-center gap-1 bg-slate-100 dark:bg-slate-700/50 rounded-lg p-1">
<button {themes.map(({ value, icon: Icon, label }) => (
onClick={() => handleThemeChange('light')} <button
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${theme === 'light' key={value}
? 'bg-white dark:bg-slate-700 text-slate-900 dark:text-white shadow-sm' onClick={() => handleThemeChange(value)}
: 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' className={`group flex items-center gap-1.5 px-3 py-2 rounded-md text-sm font-medium transition-all ${theme === value
}`} ? 'bg-white dark:bg-slate-600 text-slate-900 dark:text-white shadow-sm'
aria-label="Light mode" : 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white'
aria-pressed={theme === 'light'} }`}
> aria-label={`${label} mode`}
Light aria-pressed={theme === value}
</button> >
<button <Icon
onClick={() => handleThemeChange('dark')} className={`w-4 h-4 transition-transform ${theme === value ? 'scale-110' : 'group-hover:scale-105'
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${theme === 'dark' }`}
? 'bg-white dark:bg-slate-700 text-slate-900 dark:text-white shadow-sm' strokeWidth={theme === value ? 2.5 : 2}
: 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white' />
}`} <span className="hidden sm:inline">{label}</span>
aria-label="Dark mode" </button>
aria-pressed={theme === 'dark'} ))}
>
🌙 Dark
</button>
<button
onClick={() => handleThemeChange('system')}
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${theme === 'system'
? 'bg-white dark:bg-slate-700 text-slate-900 dark:text-white shadow-sm'
: 'text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white'
}`}
aria-label="System theme"
aria-pressed={theme === 'system'}
>
💻 Auto
</button>
</div> </div>
); );
} }

View file

@ -25,6 +25,7 @@
font-family: var(--font-sans); font-family: var(--font-sans);
font-feature-settings: 'ss01' 1, 'ss02' 1; font-feature-settings: 'ss01' 1, 'ss02' 1;
letter-spacing: -0.011em; letter-spacing: -0.011em;
transition: background-color 0.3s ease, color 0.3s ease;
} }
/* Code */ /* Code */
@ -50,12 +51,14 @@
/* Focus */ /* Focus */
*:focus-visible { *:focus-visible {
@apply outline-none ring-2 ring-emerald-500 ring-offset-2; @apply outline-none ring-2 ring-emerald-500 ring-offset-2;
transition: box-shadow 0.2s ease;
} }
/* Skip link */ /* Skip link */
.skip-to-main { .skip-to-main {
@apply absolute left-0 top-0 -translate-y-full bg-emerald-600 text-white px-4 py-2 rounded-br-lg; @apply absolute left-0 top-0 -translate-y-full bg-emerald-600 text-white px-4 py-2 rounded-br-lg font-medium;
@apply focus:translate-y-0 z-50; @apply focus:translate-y-0 z-50;
transition: transform 0.2s ease;
} }
/* Reduced motion */ /* Reduced motion */
@ -99,7 +102,12 @@
/* Links */ /* Links */
a { a {
@apply text-emerald-600 dark:text-emerald-400 hover:text-emerald-700 dark:hover:text-emerald-300; @apply text-emerald-600 dark:text-emerald-400;
transition: color 0.15s ease;
}
a:hover {
@apply text-emerald-700 dark:text-emerald-300;
} }
/* Selection */ /* Selection */
@ -121,9 +129,81 @@
.custom-scrollbar::-webkit-scrollbar-thumb { .custom-scrollbar::-webkit-scrollbar-thumb {
@apply bg-emerald-400 dark:bg-emerald-600 rounded-full; @apply bg-emerald-400 dark:bg-emerald-600 rounded-full;
transition: background-color 0.2s ease;
} }
.custom-scrollbar::-webkit-scrollbar-thumb:hover { .custom-scrollbar::-webkit-scrollbar-thumb:hover {
@apply bg-emerald-500; @apply bg-emerald-500;
} }
/* Fade in animation */
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in {
animation: fadeIn 0.3s ease-out;
}
/* Slide in animation */
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-20px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.animate-slide-in {
animation: slideIn 0.3s ease-out;
}
/* Scale in animation */
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.95);
}
to {
opacity: 1;
transform: scale(1);
}
}
.animate-scale-in {
animation: scaleIn 0.2s ease-out;
}
/* Shimmer effect */
@keyframes shimmer {
0% {
background-position: -1000px 0;
}
100% {
background-position: 1000px 0;
}
}
.animate-shimmer {
background: linear-gradient(90deg,
rgba(255, 255, 255, 0) 0%,
rgba(255, 255, 255, 0.2) 50%,
rgba(255, 255, 255, 0) 100%);
background-size: 1000px 100%;
animation: shimmer 2s infinite;
}
} }

View file

@ -1,10 +1,41 @@
import { createBrowserRouter } from 'react-router-dom'; import { createBrowserRouter } from 'react-router-dom';
import HomePage from './pages/HomePage'; import Layout from './components/Layout';
import LoginPage from './pages/LoginPage'; import LoginPage from './pages/LoginPage';
import DashboardPage from './pages/DashboardPage'; import DashboardPage from './pages/DashboardPage';
import DailyWalkthroughPage from './pages/DailyWalkthroughPage';
import RoomsPage from './pages/RoomsPage';
import BatchesPage from './pages/BatchesPage';
import TimeclockPage from './pages/TimeclockPage';
export const router = createBrowserRouter([ export const router = createBrowserRouter([
{ path: '/', element: <HomePage /> }, {
{ path: '/login', element: <LoginPage /> }, path: '/login',
{ path: '/dashboard', element: <DashboardPage /> }, element: <LoginPage />,
},
{
path: '/',
element: <Layout />,
children: [
{
index: true,
element: <DashboardPage />,
},
{
path: 'walkthrough',
element: <DailyWalkthroughPage />,
},
{
path: 'rooms',
element: <RoomsPage />,
},
{
path: 'batches',
element: <BatchesPage />,
},
{
path: 'timeclock',
element: <TimeclockPage />,
},
],
},
]); ]);