This commit is contained in:
2026-03-16 08:23:07 -04:00
parent 3e9a193d08
commit e289d1a29b
13 changed files with 429 additions and 227 deletions

File diff suppressed because one or more lines are too long

171
frontend/dist/assets/index-C7Wct-hD.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -23,8 +23,8 @@
/> />
<title>AnthoLume</title> <title>AnthoLume</title>
<link rel="manifest" href="/manifest.json" /> <link rel="manifest" href="/manifest.json" />
<script type="module" crossorigin src="/assets/index-CAfunjs7.js"></script> <script type="module" crossorigin src="/assets/index-C7Wct-hD.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BfBW0EJh.css"> <link rel="stylesheet" crossorigin href="/assets/index-Co--bktJ.css">
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>

View File

@@ -1,17 +0,0 @@
{
"name": "AnthoLume",
"short_name": "AnthoLume",
"lang": "en-US",
"theme_color": "#1F2937",
"display": "standalone",
"scope": "/",
"start_url": "/",
"icons": [
{
"purpose": "any",
"sizes": "512x512",
"src": "/assets/icons/icon512.png",
"type": "image/png"
}
]
}

View File

@@ -8,6 +8,10 @@ import ActivityPage from './pages/ActivityPage';
import SearchPage from './pages/SearchPage'; import SearchPage from './pages/SearchPage';
import SettingsPage from './pages/SettingsPage'; import SettingsPage from './pages/SettingsPage';
import LoginPage from './pages/LoginPage'; 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'; import { ProtectedRoute } from './auth/ProtectedRoute';
export function Routes() { export function Routes() {
@@ -70,6 +74,39 @@ export function Routes() {
</ProtectedRoute> </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>
<Route path="/login" element={<LoginPage />} /> <Route path="/login" element={<LoginPage />} />
</ReactRoutes> </ReactRoutes>

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

View File

@@ -2,23 +2,8 @@ import { useState, useEffect, useRef } from 'react';
import { Link, useLocation, Outlet, Navigate } from 'react-router-dom'; import { Link, useLocation, Outlet, Navigate } from 'react-router-dom';
import { useGetMe } from '../generated/anthoLumeAPIV1'; import { useGetMe } from '../generated/anthoLumeAPIV1';
import { useAuth } from '../auth/AuthContext'; import { useAuth } from '../auth/AuthContext';
import { Home, FileText, Activity, Search, Settings, User, ChevronDown } from 'lucide-react'; import { User, ChevronDown } from 'lucide-react';
import HamburgerMenu from './HamburgerMenu';
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' },
];
export default function Layout() { export default function Layout() {
const location = useLocation(); const location = useLocation();
@@ -48,6 +33,14 @@ export default function Layout() {
}, []); }, []);
// Get current page title // 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'; const currentPageTitle = navItems.find(item => location.pathname === item.path)?.title || 'Documents';
// Show loading while checking authentication status // 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"> <div className="bg-gray-100 dark:bg-gray-800 min-h-screen">
{/* Header */} {/* Header */}
<div className="flex items-center justify-between w-full h-16"> <div className="flex items-center justify-between w-full h-16">
{/* Mobile Navigation Button */} {/* Mobile Navigation Button with CSS animations */}
<div className="flex flex-col z-40 relative ml-6"> <HamburgerMenu />
<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>
{/* Header Title */} {/* Header Title */}
<h1 className="text-xl font-bold dark:text-white px-6 lg:ml-44"> <h1 className="text-xl font-bold dark:text-white px-6 lg:ml-44">

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

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

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

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