wip 4
This commit is contained in:
52
frontend/src/components/Button.tsx
Normal file
52
frontend/src/components/Button.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { ButtonHTMLAttributes, AnchorHTMLAttributes, forwardRef } from 'react';
|
||||
|
||||
interface BaseButtonProps {
|
||||
variant?: 'default' | 'secondary';
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
type ButtonProps = BaseButtonProps & ButtonHTMLAttributes<HTMLButtonElement>;
|
||||
type LinkProps = BaseButtonProps & AnchorHTMLAttributes<HTMLAnchorElement> & { href: string };
|
||||
|
||||
const getVariantClasses = (variant: 'default' | 'secondary' = 'default'): string => {
|
||||
const baseClass = 'transition duration-100 ease-in font-medium w-full h-full px-2 py-1 text-white';
|
||||
|
||||
if (variant === 'secondary') {
|
||||
return `${baseClass} bg-black shadow-md hover:text-black hover:bg-white`;
|
||||
}
|
||||
|
||||
return `${baseClass} bg-gray-500 dark:text-gray-800 hover:bg-gray-800 dark:hover:bg-gray-100`;
|
||||
};
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ variant = 'default', children, className = '', ...props }, ref) => {
|
||||
return (
|
||||
<button
|
||||
ref={ref}
|
||||
className={`${getVariantClasses(variant)} ${className}`.trim()}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Button.displayName = 'Button';
|
||||
|
||||
export const ButtonLink = forwardRef<HTMLAnchorElement, LinkProps>(
|
||||
({ variant = 'default', children, className = '', ...props }, ref) => {
|
||||
return (
|
||||
<a
|
||||
ref={ref}
|
||||
className={`${getVariantClasses(variant)} ${className}`.trim()}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ButtonLink.displayName = 'ButtonLink';
|
||||
@@ -1,36 +1,65 @@
|
||||
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: string;
|
||||
icon: React.ElementType;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{ path: '/', label: 'Home', icon: 'home' },
|
||||
{ path: '/documents', label: 'Documents', icon: 'documents' },
|
||||
{ path: '/progress', label: 'Progress', icon: 'activity' },
|
||||
{ path: '/activity', label: 'Activity', icon: 'activity' },
|
||||
{ path: '/search', label: 'Search', icon: 'search' },
|
||||
{ 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() {
|
||||
const location = useLocation();
|
||||
const { isAuthenticated, user, logout } = useAuth();
|
||||
const { isAuthenticated, user, logout, isCheckingAuth } = useAuth();
|
||||
const { data } = useGetMe(isAuthenticated ? {} : undefined);
|
||||
const userData = data?.data || user;
|
||||
const [isUserDropdownOpen, setIsUserDropdownOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
setIsUserDropdownOpen(false);
|
||||
};
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||
setIsUserDropdownOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Get current page title
|
||||
const currentPageTitle = navItems.find(item => location.pathname === item.path)?.title || 'Documents';
|
||||
|
||||
// Show loading while checking authentication status
|
||||
if (isCheckingAuth) {
|
||||
return <div className="text-gray-500 dark:text-white">Loading...</div>;
|
||||
}
|
||||
|
||||
// Redirect to login if not authenticated
|
||||
if (!isAuthenticated) {
|
||||
return <Navigate to="/login" replace />;
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
logout();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-gray-100 dark:bg-gray-800 min-h-screen">
|
||||
{/* Header */}
|
||||
@@ -40,7 +69,7 @@ export default function Layout() {
|
||||
<input
|
||||
type="checkbox"
|
||||
className="absolute lg:hidden z-50 -top-2 w-7 h-7 flex cursor-pointer opacity-0"
|
||||
id="mobile-nav-toggle"
|
||||
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>
|
||||
@@ -48,6 +77,7 @@ export default function Layout() {
|
||||
<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">
|
||||
@@ -65,6 +95,7 @@ export default function Layout() {
|
||||
: '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>
|
||||
))}
|
||||
@@ -81,44 +112,66 @@ export default function Layout() {
|
||||
|
||||
{/* Header Title */}
|
||||
<h1 className="text-xl font-bold dark:text-white px-6 lg:ml-44">
|
||||
<Link to="/documents">Documents</Link>
|
||||
{currentPageTitle}
|
||||
</h1>
|
||||
|
||||
{/* User Dropdown */}
|
||||
<div className="relative flex items-center justify-end w-full p-4 space-x-4">
|
||||
<input type="checkbox" id="user-dropdown-button" className="hidden" />
|
||||
<div
|
||||
id="user-dropdown"
|
||||
className="transition duration-200 z-20 absolute right-4 top-16 pt-4"
|
||||
<div className="relative flex items-center justify-end w-full p-4 space-x-4" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setIsUserDropdownOpen(!isUserDropdownOpen)}
|
||||
className="relative block text-gray-800 dark:text-gray-200"
|
||||
>
|
||||
<div className="w-40 origin-top-right bg-white rounded-md shadow-lg dark:shadow-gray-800 dark:bg-gray-700 ring-1 ring-black ring-opacity-5">
|
||||
<div className="py-1">
|
||||
<Link
|
||||
to="/settings"
|
||||
className="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"
|
||||
<User size={20} />
|
||||
</button>
|
||||
|
||||
{isUserDropdownOpen && (
|
||||
<div className="transition duration-200 z-20 absolute right-4 top-16 pt-4">
|
||||
<div className="w-40 origin-top-right bg-white rounded-md shadow-lg dark:shadow-gray-800 dark:bg-gray-700 ring-1 ring-black ring-opacity-5">
|
||||
<div
|
||||
className="py-1"
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
aria-labelledby="options-menu"
|
||||
>
|
||||
Settings
|
||||
</Link>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 w-full text-left"
|
||||
>
|
||||
Logout
|
||||
</button>
|
||||
<Link
|
||||
to="/settings"
|
||||
onClick={() => setIsUserDropdownOpen(false)}
|
||||
className="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600"
|
||||
role="menuitem"
|
||||
>
|
||||
<span className="flex flex-col">
|
||||
<span>Settings</span>
|
||||
</span>
|
||||
</Link>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="block px-4 py-2 text-md text-gray-700 hover:bg-gray-100 hover:text-gray-900 dark:text-gray-100 dark:hover:text-white dark:hover:bg-gray-600 w-full text-left"
|
||||
role="menuitem"
|
||||
>
|
||||
<span className="flex flex-col">
|
||||
<span>Logout</span>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<label htmlFor="user-dropdown-button">
|
||||
<div className="flex items-center gap-2 text-gray-500 dark:text-white text-md py-4 cursor-pointer">
|
||||
<span>{userData?.username || 'User'}</span>
|
||||
</div>
|
||||
</label>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => setIsUserDropdownOpen(!isUserDropdownOpen)}
|
||||
className="flex items-center gap-2 text-gray-500 dark:text-white text-md py-4 cursor-pointer"
|
||||
>
|
||||
<span>{userData?.username || 'User'}</span>
|
||||
<span className="text-gray-800 dark:text-gray-200 transition-transform duration-200" style={{ transform: isUserDropdownOpen ? 'rotate(180deg)' : 'rotate(0deg)' }}>
|
||||
<ChevronDown size={20} />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main Content */}
|
||||
<main className="relative overflow-hidden">
|
||||
<div id="container" className="h-[100dvh] px-4 overflow-auto md:px-6 lg:ml-48">
|
||||
<main className="relative overflow-hidden" style={{ height: 'calc(100dvh - 4rem - env(safe-area-inset-top))' }}>
|
||||
<div id="container" className="h-[100dvh] px-4 overflow-auto md:px-6 lg:ml-48" style={{ paddingBottom: 'calc(5em + env(safe-area-inset-bottom) * 2)' }}>
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
Reference in New Issue
Block a user