Add responsive layout with side navigation for desktop and bottom navigation for mobile
This commit is contained in:
parent
e29728e2be
commit
d9e7d856f0
2 changed files with 542 additions and 27 deletions
47
src/App.tsx
47
src/App.tsx
|
|
@ -5,7 +5,7 @@ import { Analytics } from '@vercel/analytics/react';
|
|||
import { SpeedInsights } from '@vercel/speed-insights/react';
|
||||
import { AuthProvider, useAuth } from './contexts/AuthContext';
|
||||
import { ThemeProvider } from './contexts/ThemeContext';
|
||||
import MobileNavbar from './components/MobileNavbar';
|
||||
import ResponsiveLayout from './components/ResponsiveLayout';
|
||||
import Dashboard from './pages/Dashboard';
|
||||
import Collections from './pages/Collections';
|
||||
import Decks from './pages/Decks';
|
||||
|
|
@ -77,17 +77,12 @@ const PublicRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|||
return <>{children}</>;
|
||||
};
|
||||
|
||||
// Mobile Layout Component
|
||||
const MobileLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
// Responsive Layout Component
|
||||
const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
return (
|
||||
<div className="min-h-screen-safe bg-surface-50 dark:bg-surface-900 transition-colors">
|
||||
<MobileNavbar />
|
||||
<main className="pt-16 pb-20 px-4 max-w-md mx-auto">
|
||||
<div className="animate-fade-in">
|
||||
<ResponsiveLayout>
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</ResponsiveLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -128,58 +123,58 @@ function App() {
|
|||
{/* Protected Routes */}
|
||||
<Route path="/dashboard" element={
|
||||
<ProtectedRoute>
|
||||
<MobileLayout>
|
||||
<AppLayout>
|
||||
<Dashboard />
|
||||
</MobileLayout>
|
||||
</AppLayout>
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
<Route path="/collections" element={
|
||||
<ProtectedRoute>
|
||||
<MobileLayout>
|
||||
<AppLayout>
|
||||
<Collections />
|
||||
</MobileLayout>
|
||||
</AppLayout>
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
<Route path="/decks" element={
|
||||
<ProtectedRoute>
|
||||
<MobileLayout>
|
||||
<AppLayout>
|
||||
<Decks />
|
||||
</MobileLayout>
|
||||
</AppLayout>
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
<Route path="/cards" element={
|
||||
<ProtectedRoute>
|
||||
<MobileLayout>
|
||||
<AppLayout>
|
||||
<Cards />
|
||||
</MobileLayout>
|
||||
</AppLayout>
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
<Route path="/scanner" element={
|
||||
<ProtectedRoute>
|
||||
<MobileLayout>
|
||||
<AppLayout>
|
||||
<Scanner />
|
||||
</MobileLayout>
|
||||
</AppLayout>
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
<Route path="/settings" element={
|
||||
<ProtectedRoute>
|
||||
<MobileLayout>
|
||||
<AppLayout>
|
||||
<Settings />
|
||||
</MobileLayout>
|
||||
</AppLayout>
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
{/* Admin Routes */}
|
||||
<Route path="/admin" element={
|
||||
<ProtectedRoute>
|
||||
<MobileLayout>
|
||||
<AppLayout>
|
||||
<AdminPanel />
|
||||
</MobileLayout>
|
||||
</AppLayout>
|
||||
</ProtectedRoute>
|
||||
} />
|
||||
|
||||
|
|
@ -188,7 +183,7 @@ function App() {
|
|||
|
||||
{/* 404 fallback */}
|
||||
<Route path="*" element={
|
||||
<MobileLayout>
|
||||
<AppLayout>
|
||||
<div className="text-center py-12">
|
||||
<div className="w-16 h-16 bg-surface-200 dark:bg-surface-700 rounded-2xl flex items-center justify-center mx-auto mb-6">
|
||||
<svg className="w-8 h-8 text-surface-500 dark:text-surface-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
|
@ -204,7 +199,7 @@ function App() {
|
|||
Go Back
|
||||
</button>
|
||||
</div>
|
||||
</MobileLayout>
|
||||
</AppLayout>
|
||||
} />
|
||||
</Routes>
|
||||
<Analytics />
|
||||
|
|
|
|||
520
src/components/ResponsiveLayout.tsx
Normal file
520
src/components/ResponsiveLayout.tsx
Normal file
|
|
@ -0,0 +1,520 @@
|
|||
import React, { useState } from 'react';
|
||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||
import { useAuth } from '../contexts/AuthContext';
|
||||
import { useTheme } from '../contexts/ThemeContext';
|
||||
|
||||
const ResponsiveLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||
const { user, logout, isAdmin } = useAuth();
|
||||
const { toggleTheme, effectiveTheme } = useTheme();
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [showUserMenu, setShowUserMenu] = useState(false);
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false);
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
setShowUserMenu(false);
|
||||
setSidebarOpen(false);
|
||||
navigate('/login');
|
||||
};
|
||||
|
||||
const isActive = (path: string) => location.pathname === path;
|
||||
|
||||
const navigation = [
|
||||
{ name: 'Dashboard', href: '/dashboard', icon: '📊', mobileIcon: '🏠' },
|
||||
{ name: 'Cards', href: '/cards', icon: '🃏', mobileIcon: '🃏' },
|
||||
{ name: 'Collections', href: '/collections', icon: '📚', mobileIcon: '📚' },
|
||||
{ name: 'Decks', href: '/decks', icon: '🎯', mobileIcon: '🎯' },
|
||||
{ name: 'Scanner', href: '/scanner', icon: '📷', mobileIcon: '📷' },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-surface-50 dark:bg-surface-900 transition-colors">
|
||||
{/* Desktop Layout */}
|
||||
<div className="hidden lg:flex h-screen">
|
||||
{/* Sidebar */}
|
||||
<div className="w-64 bg-white dark:bg-surface-800 border-r border-surface-200 dark:border-surface-700 flex flex-col">
|
||||
{/* Logo */}
|
||||
<div className="p-6 border-b border-surface-200 dark:border-surface-700">
|
||||
<Link to="/dashboard" className="flex items-center space-x-3">
|
||||
<div className="bg-gradient-to-r from-primary-500 to-accent-500 text-white p-3 rounded-xl">
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-surface-900 dark:text-white">TCG Vault</h1>
|
||||
<p className="text-sm text-surface-600 dark:text-surface-400">
|
||||
Hi, {user?.firstName || user?.username}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 p-4 space-y-2">
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
to={item.href}
|
||||
className={`flex items-center space-x-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-200 ${
|
||||
isActive(item.href)
|
||||
? 'bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300'
|
||||
: 'text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-white hover:bg-surface-100 dark:hover:bg-surface-700'
|
||||
}`}
|
||||
>
|
||||
<span className="text-lg">{item.icon}</span>
|
||||
<span>{item.name}</span>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* User Section */}
|
||||
<div className="p-4 border-t border-surface-200 dark:border-surface-700">
|
||||
<div className="flex items-center space-x-3 p-3 rounded-xl bg-surface-50 dark:bg-surface-700">
|
||||
<div className="w-10 h-10 bg-gradient-to-r from-primary-500 to-accent-500 rounded-xl flex items-center justify-center text-white font-semibold">
|
||||
{user?.firstName?.[0] || user?.username?.[0]?.toUpperCase() || 'U'}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-surface-900 dark:text-white truncate">
|
||||
{user?.firstName && user?.lastName
|
||||
? `${user.firstName} ${user.lastName}`
|
||||
: user?.username}
|
||||
</p>
|
||||
<p className="text-xs text-surface-600 dark:text-surface-400 truncate">{user?.email}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setShowUserMenu(!showUserMenu)}
|
||||
className="p-1 rounded-lg hover:bg-surface-200 dark:hover:bg-surface-600 transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4 text-surface-600 dark:text-surface-400" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* User Menu Dropdown */}
|
||||
{showUserMenu && (
|
||||
<div className="absolute bottom-20 left-4 w-56 bg-white dark:bg-surface-800 rounded-xl shadow-lg ring-1 ring-black ring-opacity-5 border border-surface-200 dark:border-surface-700">
|
||||
<div className="py-2">
|
||||
<Link
|
||||
to="/settings"
|
||||
onClick={() => setShowUserMenu(false)}
|
||||
className="flex items-center space-x-3 px-4 py-2 text-sm text-surface-700 dark:text-surface-300 hover:bg-surface-100 dark:hover:bg-surface-700"
|
||||
>
|
||||
<span>⚙️</span>
|
||||
<span>Settings</span>
|
||||
</Link>
|
||||
|
||||
{isAdmin() && (
|
||||
<Link
|
||||
to="/admin"
|
||||
onClick={() => setShowUserMenu(false)}
|
||||
className="flex items-center space-x-3 px-4 py-2 text-sm text-yellow-700 dark:text-yellow-300 hover:bg-yellow-50 dark:hover:bg-yellow-900/20"
|
||||
>
|
||||
<span>👑</span>
|
||||
<span>Admin Panel</span>
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<div className="border-t border-surface-200 dark:border-surface-700 my-1"></div>
|
||||
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="flex items-center space-x-3 px-4 py-2 text-sm text-red-700 dark:text-red-400 hover:bg-red-50 dark:hover:bg-red-900/20 w-full text-left"
|
||||
>
|
||||
<span>🚪</span>
|
||||
<span>Sign Out</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
{/* Top Header */}
|
||||
<header className="bg-white dark:bg-surface-800 border-b border-surface-200 dark:border-surface-700 px-6 py-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-4">
|
||||
<h2 className="text-2xl font-bold text-surface-900 dark:text-white">
|
||||
{navigation.find(item => isActive(item.href))?.name || 'TCG Vault'}
|
||||
</h2>
|
||||
{isAdmin() && (
|
||||
<span className="px-3 py-1 bg-yellow-100 dark:bg-yellow-900/30 text-yellow-800 dark:text-yellow-200 text-xs font-medium rounded-full">
|
||||
⚡ Admin
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
{/* Theme Toggle */}
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="p-2 rounded-xl bg-surface-100 dark:bg-surface-700 text-surface-600 dark:text-surface-300 hover:bg-surface-200 dark:hover:bg-surface-600 transition-colors"
|
||||
>
|
||||
{effectiveTheme === 'dark' ? (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Page Content */}
|
||||
<main className="flex-1 overflow-auto p-6">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile Layout */}
|
||||
<div className="lg:hidden">
|
||||
{/* Top Header */}
|
||||
<header className="fixed top-0 left-0 right-0 z-40 bg-white dark:bg-surface-900 border-b border-surface-200 dark:border-surface-700 px-4 py-3 safe-area-inset-top">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<button
|
||||
onClick={() => setSidebarOpen(!sidebarOpen)}
|
||||
className="p-2 rounded-xl bg-surface-100 dark:bg-surface-800 text-surface-600 dark:text-surface-300 hover:bg-surface-200 dark:hover:bg-surface-700 transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</button>
|
||||
<div className="bg-gradient-to-r from-primary-500 to-accent-500 text-white p-2 rounded-xl">
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-lg font-bold text-surface-900 dark:text-white">TCG Vault</h1>
|
||||
<p className="text-xs text-surface-600 dark:text-surface-400">
|
||||
Hi, {user?.firstName || user?.username}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
{/* Theme Toggle */}
|
||||
<button
|
||||
onClick={toggleTheme}
|
||||
className="p-2 rounded-xl bg-surface-100 dark:bg-surface-800 text-surface-600 dark:text-surface-300 hover:bg-surface-200 dark:hover:bg-surface-700 transition-colors"
|
||||
>
|
||||
{effectiveTheme === 'dark' ? (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clipRule="evenodd" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
|
||||
{/* User Avatar */}
|
||||
<button
|
||||
onClick={() => setShowUserMenu(true)}
|
||||
className="relative p-1"
|
||||
>
|
||||
<div className="w-8 h-8 bg-gradient-to-r from-primary-500 to-accent-500 rounded-xl flex items-center justify-center text-white font-semibold text-sm">
|
||||
{user?.firstName?.[0] || user?.username?.[0]?.toUpperCase() || 'U'}
|
||||
</div>
|
||||
{isAdmin() && (
|
||||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-yellow-400 rounded-full border border-white dark:border-surface-900"></div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Mobile Sidebar */}
|
||||
{sidebarOpen && (
|
||||
<div className="fixed inset-0 z-50 lg:hidden">
|
||||
<div className="fixed inset-0 bg-black/50" onClick={() => setSidebarOpen(false)}></div>
|
||||
<div className="fixed left-0 top-0 bottom-0 w-64 bg-white dark:bg-surface-800 border-r border-surface-200 dark:border-surface-700 transform transition-transform duration-300 ease-in-out">
|
||||
<div className="p-6 border-b border-surface-200 dark:border-surface-700">
|
||||
<div className="flex items-center justify-between">
|
||||
<Link to="/dashboard" className="flex items-center space-x-3" onClick={() => setSidebarOpen(false)}>
|
||||
<div className="bg-gradient-to-r from-primary-500 to-accent-500 text-white p-3 rounded-xl">
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-surface-900 dark:text-white">TCG Vault</h1>
|
||||
<p className="text-sm text-surface-600 dark:text-surface-400">
|
||||
Hi, {user?.firstName || user?.username}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
className="p-2 rounded-xl bg-surface-100 dark:bg-surface-800 text-surface-600 dark:text-surface-300 hover:bg-surface-200 dark:hover:bg-surface-700 transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 p-4 space-y-2">
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
key={item.name}
|
||||
to={item.href}
|
||||
onClick={() => setSidebarOpen(false)}
|
||||
className={`flex items-center space-x-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-200 ${
|
||||
isActive(item.href)
|
||||
? 'bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300'
|
||||
: 'text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-white hover:bg-surface-100 dark:hover:bg-surface-700'
|
||||
}`}
|
||||
>
|
||||
<span className="text-lg">{item.icon}</span>
|
||||
<span>{item.name}</span>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="p-4 border-t border-surface-200 dark:border-surface-700">
|
||||
<div className="flex items-center space-x-3 p-3 rounded-xl bg-surface-50 dark:bg-surface-700">
|
||||
<div className="w-10 h-10 bg-gradient-to-r from-primary-500 to-accent-500 rounded-xl flex items-center justify-center text-white font-semibold">
|
||||
{user?.firstName?.[0] || user?.username?.[0]?.toUpperCase() || 'U'}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-surface-900 dark:text-white truncate">
|
||||
{user?.firstName && user?.lastName
|
||||
? `${user.firstName} ${user.lastName}`
|
||||
: user?.username}
|
||||
</p>
|
||||
<p className="text-xs text-surface-600 dark:text-surface-400 truncate">{user?.email}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bottom Navigation */}
|
||||
<nav className="fixed bottom-0 left-0 right-0 z-50 bg-white dark:bg-surface-900 border-t border-surface-200 dark:border-surface-700 safe-area-inset-bottom">
|
||||
<div className="relative flex items-end justify-around px-4 py-2">
|
||||
{/* Home */}
|
||||
<Link
|
||||
to="/dashboard"
|
||||
className={`flex flex-col items-center justify-center p-2 rounded-xl transition-all duration-200 ${
|
||||
isActive('/dashboard')
|
||||
? 'text-primary-600 dark:text-primary-400'
|
||||
: 'text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<div className="mb-1">
|
||||
{isActive('/dashboard') ? (
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M10 20v-6h4v6h5v-8h3L12 3 2 12h3v8z"/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs font-medium">Home</span>
|
||||
</Link>
|
||||
|
||||
{/* Cards */}
|
||||
<Link
|
||||
to="/cards"
|
||||
className={`flex flex-col items-center justify-center p-2 rounded-xl transition-all duration-200 ${
|
||||
isActive('/cards')
|
||||
? 'text-primary-600 dark:text-primary-400'
|
||||
: 'text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<div className="mb-1">
|
||||
{isActive('/cards') ? (
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs font-medium">Cards</span>
|
||||
</Link>
|
||||
|
||||
{/* Scanner - Special Raised Button */}
|
||||
<Link
|
||||
to="/scanner"
|
||||
className="relative flex flex-col items-center justify-center -mt-6 mb-2"
|
||||
>
|
||||
<div className={`w-14 h-14 rounded-full shadow-lg transition-all duration-200 flex items-center justify-center ${
|
||||
isActive('/scanner')
|
||||
? 'bg-gradient-to-r from-primary-600 to-accent-600 shadow-primary-500/25 shadow-xl'
|
||||
: 'bg-gradient-to-r from-primary-500 to-accent-500 hover:from-primary-600 hover:to-accent-600 shadow-primary-500/20 hover:shadow-xl'
|
||||
}`}>
|
||||
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-xs font-medium text-primary-600 dark:text-primary-400 mt-1">Scanner</span>
|
||||
</Link>
|
||||
|
||||
{/* Collections */}
|
||||
<Link
|
||||
to="/collections"
|
||||
className={`flex flex-col items-center justify-center p-2 rounded-xl transition-all duration-200 ${
|
||||
isActive('/collections')
|
||||
? 'text-primary-600 dark:text-primary-400'
|
||||
: 'text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-white'
|
||||
}`}
|
||||
>
|
||||
<div className="mb-1">
|
||||
{isActive('/collections') ? (
|
||||
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24">
|
||||
<path d="M7 4V2a1 1 0 011-1h8a1 1 0 011 1v2h3a1 1 0 110 2h-1v10a2 2 0 01-2 2H7a2 2 0 01-2-2V6H4a1 1 0 010-2h3zM9 4h6V3H9v1zm8 2H7v10h10V6z"/>
|
||||
</svg>
|
||||
) : (
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs font-medium">Collections</span>
|
||||
</Link>
|
||||
|
||||
{/* More */}
|
||||
<button
|
||||
onClick={() => setShowUserMenu(true)}
|
||||
className="flex flex-col items-center justify-center p-2 rounded-xl transition-all duration-200 text-surface-600 dark:text-surface-400 hover:text-surface-900 dark:hover:text-white"
|
||||
>
|
||||
<div className="mb-1">
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
||||
</svg>
|
||||
</div>
|
||||
<span className="text-xs font-medium">More</span>
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Page Content */}
|
||||
<main className="pt-16 pb-20 px-4 max-w-md mx-auto">
|
||||
<div className="animate-fade-in">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* User Menu Modal */}
|
||||
{showUserMenu && (
|
||||
<div className="fixed inset-0 z-50 bg-black/50" onClick={() => setShowUserMenu(false)}>
|
||||
<div className="fixed bottom-0 left-0 right-0 bg-white dark:bg-surface-900 rounded-t-3xl p-6 safe-area-inset-bottom animate-slide-up">
|
||||
<div className="w-12 h-1.5 bg-surface-300 dark:bg-surface-600 rounded-full mx-auto mb-6"></div>
|
||||
|
||||
<div className="flex items-center space-x-4 mb-6 p-4 bg-surface-50 dark:bg-surface-800 rounded-2xl">
|
||||
<div className="w-12 h-12 bg-gradient-to-r from-primary-500 to-accent-500 rounded-xl flex items-center justify-center text-white font-semibold text-lg">
|
||||
{user?.firstName?.[0] || user?.username?.[0]?.toUpperCase() || 'U'}
|
||||
</div>
|
||||
<div>
|
||||
<p className="font-semibold text-surface-900 dark:text-white">
|
||||
{user?.firstName && user?.lastName
|
||||
? `${user.firstName} ${user.lastName}`
|
||||
: user?.username}
|
||||
</p>
|
||||
<p className="text-sm text-surface-600 dark:text-surface-400">{user?.email}</p>
|
||||
<div className="flex gap-1 mt-1">
|
||||
{user?.roles.map((role) => (
|
||||
<span
|
||||
key={role}
|
||||
className={`px-2 py-0.5 rounded-full text-xs font-medium ${
|
||||
role === 'admin'
|
||||
? 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200'
|
||||
: 'bg-primary-100 text-primary-800 dark:bg-primary-900 dark:text-primary-200'
|
||||
}`}
|
||||
>
|
||||
{role}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 mb-6">
|
||||
<Link
|
||||
to="/decks"
|
||||
onClick={() => setShowUserMenu(false)}
|
||||
className="flex items-center space-x-3 p-3 rounded-xl hover:bg-surface-100 dark:hover:bg-surface-800 transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5 text-surface-600 dark:text-surface-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
<span className="text-surface-900 dark:text-white font-medium">Decks</span>
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/settings"
|
||||
onClick={() => setShowUserMenu(false)}
|
||||
className="flex items-center space-x-3 p-3 rounded-xl hover:bg-surface-100 dark:hover:bg-surface-800 transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5 text-surface-600 dark:text-surface-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<span className="text-surface-900 dark:text-white font-medium">Settings</span>
|
||||
</Link>
|
||||
|
||||
{isAdmin() && (
|
||||
<Link
|
||||
to="/admin"
|
||||
onClick={() => setShowUserMenu(false)}
|
||||
className="flex items-center space-x-3 p-3 rounded-xl hover:bg-yellow-50 dark:hover:bg-yellow-900/20 transition-colors"
|
||||
>
|
||||
<svg className="w-5 h-5 text-yellow-600 dark:text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
<span className="text-yellow-700 dark:text-yellow-300 font-medium">Admin Panel</span>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full flex items-center justify-center space-x-2 p-3 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-xl hover:bg-red-100 dark:hover:bg-red-900/30 transition-colors font-medium"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" />
|
||||
</svg>
|
||||
<span>Sign Out</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Click outside to close menus */}
|
||||
{(showUserMenu || sidebarOpen) && (
|
||||
<div
|
||||
className="fixed inset-0 z-40"
|
||||
onClick={() => {
|
||||
setShowUserMenu(false);
|
||||
setSidebarOpen(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ResponsiveLayout;
|
||||
Loading…
Reference in a new issue