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:
parent
0098f188e8
commit
9b82e08d34
6 changed files with 1076 additions and 85 deletions
882
frontend/package-lock.json
generated
882
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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 />,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue