wip 5
This commit is contained in:
@@ -8,6 +8,10 @@ import ActivityPage from './pages/ActivityPage';
|
||||
import SearchPage from './pages/SearchPage';
|
||||
import SettingsPage from './pages/SettingsPage';
|
||||
import LoginPage from './pages/LoginPage';
|
||||
import AdminPage from './pages/AdminPage';
|
||||
import AdminImportPage from './pages/AdminImportPage';
|
||||
import AdminUsersPage from './pages/AdminUsersPage';
|
||||
import AdminLogsPage from './pages/AdminLogsPage';
|
||||
import { ProtectedRoute } from './auth/ProtectedRoute';
|
||||
|
||||
export function Routes() {
|
||||
@@ -70,6 +74,39 @@ export function Routes() {
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
{/* Admin routes */}
|
||||
<Route
|
||||
path="admin"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<AdminPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="admin/import"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<AdminImportPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="admin/users"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<AdminUsersPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="admin/logs"
|
||||
element={
|
||||
<ProtectedRoute>
|
||||
<AdminLogsPage />
|
||||
</ProtectedRoute>
|
||||
}
|
||||
/>
|
||||
</Route>
|
||||
<Route path="/login" element={<LoginPage />} />
|
||||
</ReactRoutes>
|
||||
|
||||
174
frontend/src/components/HamburgerMenu.tsx
Normal file
174
frontend/src/components/HamburgerMenu.tsx
Normal file
@@ -0,0 +1,174 @@
|
||||
import { useState } from 'react';
|
||||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { Home, FileText, Activity, Search, Settings } from 'lucide-react';
|
||||
import { useAuth } from '../auth/AuthContext';
|
||||
|
||||
interface NavItem {
|
||||
path: string;
|
||||
label: string;
|
||||
icon: React.ElementType;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{ path: '/', label: 'Home', icon: Home, title: 'Home' },
|
||||
{ path: '/documents', label: 'Documents', icon: FileText, title: 'Documents' },
|
||||
{ path: '/progress', label: 'Progress', icon: Activity, title: 'Progress' },
|
||||
{ path: '/activity', label: 'Activity', icon: Activity, title: 'Activity' },
|
||||
{ path: '/search', label: 'Search', icon: Search, title: 'Search' },
|
||||
];
|
||||
|
||||
const adminSubItems: NavItem[] = [
|
||||
{ path: '/admin', label: 'General', icon: Settings, title: 'General' },
|
||||
{ path: '/admin/import', label: 'Import', icon: Settings, title: 'Import' },
|
||||
{ path: '/admin/users', label: 'Users', icon: Settings, title: 'Users' },
|
||||
{ path: '/admin/logs', label: 'Logs', icon: Settings, title: 'Logs' },
|
||||
];
|
||||
|
||||
// Helper function to check if pathname has a prefix
|
||||
function hasPrefix(path: string, prefix: string): boolean {
|
||||
return path.startsWith(prefix);
|
||||
}
|
||||
|
||||
export default function HamburgerMenu() {
|
||||
const location = useLocation();
|
||||
const { user } = useAuth();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const isAdmin = user?.is_admin ?? false;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col z-40 relative ml-6">
|
||||
{/* Checkbox input for state management */}
|
||||
<input
|
||||
type="checkbox"
|
||||
className="absolute lg:hidden z-50 -top-2 w-7 h-7 flex cursor-pointer opacity-0"
|
||||
id="mobile-nav-checkbox"
|
||||
checked={isOpen}
|
||||
onChange={(e) => setIsOpen(e.target.checked)}
|
||||
/>
|
||||
|
||||
{/* Hamburger icon lines with CSS animations - hidden on desktop */}
|
||||
<span
|
||||
className="lg:hidden bg-black w-7 h-0.5 z-40 mt-0.5 dark:bg-white transition-transform transition-background transition-opacity duration-500"
|
||||
style={{
|
||||
transformOrigin: '5px 0px',
|
||||
transition: 'transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), opacity 0.55s ease',
|
||||
transform: isOpen ? 'rotate(45deg) translate(2px, -2px)' : 'none',
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
className="lg:hidden bg-black w-7 h-0.5 z-40 mt-1 dark:bg-white transition-transform transition-background transition-opacity duration-500"
|
||||
style={{
|
||||
transformOrigin: '0% 100%',
|
||||
transition: 'transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), opacity 0.55s ease',
|
||||
opacity: isOpen ? 0 : 1,
|
||||
transform: isOpen ? 'rotate(0deg) scale(0.2, 0.2)' : 'none',
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
className="lg:hidden bg-black w-7 h-0.5 z-40 mt-1 dark:bg-white transition-transform transition-background transition-opacity duration-500"
|
||||
style={{
|
||||
transformOrigin: '0% 0%',
|
||||
transition: 'transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), background 0.5s cubic-bezier(0.77, 0.2, 0.05, 1), opacity 0.55s ease',
|
||||
transform: isOpen ? 'rotate(-45deg) translate(0, 6px)' : 'none',
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Navigation menu with slide animation */}
|
||||
<div
|
||||
id="menu"
|
||||
className="fixed -ml-6 h-full w-56 lg:w-48 bg-white dark:bg-gray-700 shadow-lg"
|
||||
style={{
|
||||
top: 0,
|
||||
paddingTop: 'env(safe-area-inset-top)',
|
||||
transformOrigin: '0% 0%',
|
||||
// On desktop (lg), always show the menu via CSS class
|
||||
// On mobile, control via state
|
||||
transform: isOpen ? 'none' : 'translate(-100%, 0)',
|
||||
transition: 'transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1)',
|
||||
}}
|
||||
>
|
||||
{/* Desktop override - always visible */}
|
||||
<style>{`
|
||||
@media (min-width: 1024px) {
|
||||
#menu {
|
||||
transform: none !important;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
<div className="h-16 flex justify-end lg:justify-around">
|
||||
<p className="text-xl font-bold dark:text-white text-right my-auto pr-8 lg:pr-0">
|
||||
AnthoLume
|
||||
</p>
|
||||
</div>
|
||||
<nav>
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
onClick={() => setIsOpen(false)}
|
||||
className={`flex items-center justify-start w-full p-2 pl-6 my-2 transition-colors duration-200 border-l-4 ${
|
||||
location.pathname === item.path
|
||||
? 'border-purple-500 dark:text-white'
|
||||
: 'border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100'
|
||||
}`}
|
||||
>
|
||||
<item.icon size={20} />
|
||||
<span className="mx-4 text-sm font-normal">{item.label}</span>
|
||||
</Link>
|
||||
))}
|
||||
|
||||
{/* Admin section - only visible for admins */}
|
||||
{isAdmin && (
|
||||
<div className={`flex flex-col gap-4 p-2 pl-6 my-2 transition-colors duration-200 border-l-4 ${
|
||||
hasPrefix(location.pathname, '/admin')
|
||||
? 'border-purple-500 dark:text-white'
|
||||
: 'border-transparent text-gray-400'
|
||||
}`}>
|
||||
{/* Admin header - always shown */}
|
||||
<Link
|
||||
to="/admin"
|
||||
onClick={() => setIsOpen(false)}
|
||||
className={`flex justify-start w-full ${
|
||||
location.pathname === '/admin' && !hasPrefix(location.pathname, '/admin/')
|
||||
? 'dark:text-white'
|
||||
: 'text-gray-400 hover:text-gray-800 dark:hover:text-gray-100'
|
||||
}`}
|
||||
>
|
||||
<Settings size={20} />
|
||||
<span className="mx-4 text-sm font-normal">Admin</span>
|
||||
</Link>
|
||||
|
||||
{hasPrefix(location.pathname, '/admin') && (
|
||||
<div className="flex flex-col gap-4">
|
||||
{adminSubItems.map((item) => (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
onClick={() => setIsOpen(false)}
|
||||
className={`flex justify-start w-full ${
|
||||
location.pathname === item.path
|
||||
? 'dark:text-white'
|
||||
: 'text-gray-400 hover:text-gray-800 dark:hover:text-gray-100'
|
||||
}`}
|
||||
style={{ paddingLeft: '1.75em' }}
|
||||
>
|
||||
<span className="mx-4 text-sm font-normal">{item.label}</span>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
<a
|
||||
className="flex flex-col gap-2 justify-center items-center p-6 w-full absolute bottom-0 text-black dark:text-white"
|
||||
target="_blank"
|
||||
href="https://gitea.va.reichard.io/evan/AnthoLume"
|
||||
>
|
||||
<span className="text-xs">v1.0.0</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2,23 +2,8 @@ import { useState, useEffect, useRef } from 'react';
|
||||
import { Link, useLocation, Outlet, Navigate } from 'react-router-dom';
|
||||
import { useGetMe } from '../generated/anthoLumeAPIV1';
|
||||
import { useAuth } from '../auth/AuthContext';
|
||||
import { Home, FileText, Activity, Search, Settings, User, ChevronDown } from 'lucide-react';
|
||||
|
||||
interface NavItem {
|
||||
path: string;
|
||||
label: string;
|
||||
icon: React.ElementType;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{ path: '/', label: 'Home', icon: Home, title: 'Home' },
|
||||
{ path: '/documents', label: 'Documents', icon: FileText, title: 'Documents' },
|
||||
{ path: '/progress', label: 'Progress', icon: Activity, title: 'Progress' },
|
||||
{ path: '/activity', label: 'Activity', icon: Activity, title: 'Activity' },
|
||||
{ path: '/search', label: 'Search', icon: Search, title: 'Search' },
|
||||
{ path: '/settings', label: 'Settings', icon: Settings, title: 'Settings' },
|
||||
];
|
||||
import { User, ChevronDown } from 'lucide-react';
|
||||
import HamburgerMenu from './HamburgerMenu';
|
||||
|
||||
export default function Layout() {
|
||||
const location = useLocation();
|
||||
@@ -48,6 +33,14 @@ export default function Layout() {
|
||||
}, []);
|
||||
|
||||
// Get current page title
|
||||
const navItems = [
|
||||
{ path: '/', title: 'Home' },
|
||||
{ path: '/documents', title: 'Documents' },
|
||||
{ path: '/progress', title: 'Progress' },
|
||||
{ path: '/activity', title: 'Activity' },
|
||||
{ path: '/search', title: 'Search' },
|
||||
{ path: '/settings', title: 'Settings' },
|
||||
];
|
||||
const currentPageTitle = navItems.find(item => location.pathname === item.path)?.title || 'Documents';
|
||||
|
||||
// Show loading while checking authentication status
|
||||
@@ -64,51 +57,8 @@ export default function Layout() {
|
||||
<div className="bg-gray-100 dark:bg-gray-800 min-h-screen">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between w-full h-16">
|
||||
{/* Mobile Navigation Button */}
|
||||
<div className="flex flex-col z-40 relative ml-6">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="absolute lg:hidden z-50 -top-2 w-7 h-7 flex cursor-pointer opacity-0"
|
||||
id="mobile-nav-button"
|
||||
/>
|
||||
<span className="lg:hidden bg-black w-7 h-0.5 z-40 mt-0.5 dark:bg-white"></span>
|
||||
<span className="lg:hidden bg-black w-7 h-0.5 z-40 mt-1 dark:bg-white"></span>
|
||||
<span className="lg:hidden bg-black w-7 h-0.5 z-40 mt-1 dark:bg-white"></span>
|
||||
<div
|
||||
id="menu"
|
||||
className="fixed -ml-6 h-full w-56 lg:w-48 bg-white dark:bg-gray-700 shadow-lg"
|
||||
style={{ top: 0, paddingTop: 'env(safe-area-inset-top)' }}
|
||||
>
|
||||
<div className="h-16 flex justify-end lg:justify-around">
|
||||
<p className="text-xl font-bold dark:text-white text-right my-auto pr-8 lg:pr-0">
|
||||
AnthoLume
|
||||
</p>
|
||||
</div>
|
||||
<nav className="flex flex-col">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={`flex items-center justify-start w-full p-2 pl-6 my-2 transition-colors duration-200 border-l-4 ${
|
||||
location.pathname === item.path
|
||||
? 'border-purple-500 dark:text-white'
|
||||
: 'border-transparent text-gray-400 hover:text-gray-800 dark:hover:text-gray-100'
|
||||
}`}
|
||||
>
|
||||
<item.icon size={20} />
|
||||
<span className="mx-4 text-sm font-normal">{item.label}</span>
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
<a
|
||||
className="flex flex-col gap-2 justify-center items-center p-6 w-full absolute bottom-0 text-black dark:text-white"
|
||||
target="_blank"
|
||||
href="https://gitea.va.reichard.io/evan/AnthoLume"
|
||||
>
|
||||
<span className="text-xs">v1.0.0</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{/* Mobile Navigation Button with CSS animations */}
|
||||
<HamburgerMenu />
|
||||
|
||||
{/* Header Title */}
|
||||
<h1 className="text-xl font-bold dark:text-white px-6 lg:ml-44">
|
||||
|
||||
8
frontend/src/pages/AdminImportPage.tsx
Normal file
8
frontend/src/pages/AdminImportPage.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
export default function AdminImportPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-xl font-bold dark:text-white">Admin - Import</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">Document import page</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
8
frontend/src/pages/AdminLogsPage.tsx
Normal file
8
frontend/src/pages/AdminLogsPage.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
export default function AdminLogsPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-xl font-bold dark:text-white">Admin - Logs</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">System logs page</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
8
frontend/src/pages/AdminPage.tsx
Normal file
8
frontend/src/pages/AdminPage.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
export default function AdminPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-xl font-bold dark:text-white">Admin - General</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">Admin general settings page</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
8
frontend/src/pages/AdminUsersPage.tsx
Normal file
8
frontend/src/pages/AdminUsersPage.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
export default function AdminUsersPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-xl font-bold dark:text-white">Admin - Users</h1>
|
||||
<p className="text-gray-500 dark:text-gray-400">User management page</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user