Files
project_vollidioten_website/components/Layout.tsx
Lars Behrends 065a6e657d feat: add world map functionality and admin map management
- Added world map page with interactive marker display
- Implemented admin map management for marker CRUD operations
- Added map layers and markers seed data to database
- Integrated new routes for map functionality
- Updated database configuration for production environment
- Added documentation page route
- Enhanced package.json with required dependencies for map features
2026-01-02 05:08:07 +01:00

204 lines
9.7 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Icons } from './IconSet';
import { authService } from '../services/AuthService';
import { DiscordUser } from '../types';
interface LayoutProps {
children: React.ReactNode;
activeTab: string;
onNavigate: (tab: string) => void;
}
const NavItem = ({
active,
label,
to
}: {
active: boolean;
label: string;
to: string;
}) => (
<Link
to={to}
className={`text-sm font-medium transition-colors duration-200 px-1 py-4 border-b-2 ${
active
? 'text-textMain border-accentInfo'
: 'text-textMuted border-transparent hover:text-textMain hover:border-border'
}`}
>
{label}
</Link>
);
const Layout: React.FC<LayoutProps> = ({ children, activeTab, onNavigate }) => {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [user, setUser] = useState<DiscordUser | null>(null);
const [rememberMe, setRememberMe] = useState(() => {
// Load remember me preference from localStorage
return localStorage.getItem('rememberMe') === 'true';
});
useEffect(() => {
// Subscribe to auth changes
const unsubscribe = authService.subscribe(setUser);
return unsubscribe;
}, []);
return (
<div className="min-h-screen flex flex-col font-sans">
{/* Top Header - Website Style */}
<header className="sticky top-0 z-40 bg-background/80 backdrop-blur-md border-b border-border">
<div className="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
<div className="flex items-center gap-10">
{/* Logo */}
<div
className="flex items-center gap-3 cursor-pointer group"
onClick={() => onNavigate('/')}
>
<div className="w-8 h-8 bg-gradient-to-br from-accentInfo to-blue-900 rounded flex items-center justify-center shadow-glow group-hover:shadow-lg transition-shadow">
<span className="font-bold text-white text-sm">P.V.</span>
</div>
<span className="font-bold text-lg tracking-tight text-textMain">Projekt: Vollidion</span>
</div>
{/* Desktop Nav */}
<nav className="hidden md:flex items-center gap-6 h-full">
<NavItem active={activeTab === 'dashboard'} label="Übersicht" to="/" />
<NavItem active={activeTab === 'cities'} label="Städte" to="/cities" />
<NavItem active={activeTab === 'players'} label="Bürger" to="/players" />
{/* <NavItem active={activeTab === 'organizations'} label="Organisationen" to="/organizations" />*/}
<NavItem active={activeTab === 'projects'} label="Unternehmen" to="/projects" />
<NavItem active={activeTab === 'world-map'} label="Weltkarte" to="/world-map" />
{user?.isAdmin && (
<NavItem active={activeTab === 'admin'} label="Admin" to="/admin" />
)}
</nav>
</div>
<div className="flex items-center gap-4">
{user?.isAdmin && (
<>
<button
onClick={() => onNavigate('datapack')}
className="hidden md:flex items-center gap-2 text-xs font-medium text-textMain hover:text-accentInfo transition-colors"
>
<Icons.Box className="w-3 h-3" />
<span>Datapack holen</span>
</button><button
onClick={() => onNavigate('setup')}
className="hidden md:flex items-center gap-2 text-xs font-medium text-textMuted hover:text-accentInfo transition-colors border border-border rounded-full px-4 py-1.5 hover:bg-surfaceHighlight"
>
<Icons.Terminal className="w-3 h-3" />
<span>Admin Setup</span>
</button>
</>
)}
{/* Mobile Menu Toggle */}
<button
className="md:hidden text-textMuted hover:text-textMain"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
>
<Icons.Layers className="w-6 h-6" />
</button>
</div>
</div>
{/* Mobile Nav Dropdown */}
{mobileMenuOpen && (
<div className="md:hidden border-t border-border bg-surface px-6 py-4 space-y-4 shadow-xl">
<Link to="/" onClick={() => setMobileMenuOpen(false)} className="block py-2 text-textMuted hover:text-textMain">Übersicht</Link>
<Link to="/cities" onClick={() => setMobileMenuOpen(false)} className="block py-2 text-textMuted hover:text-textMain">Städte</Link>
<Link to="/players" onClick={() => setMobileMenuOpen(false)} className="block py-2 text-textMuted hover:text-textMain">Bürger</Link>
<Link to="/organizations" onClick={() => setMobileMenuOpen(false)} className="block py-2 text-textMuted hover:text-textMain">Organisationen</Link>
<Link to="/projects" onClick={() => setMobileMenuOpen(false)} className="block py-2 text-textMuted hover:text-textMain">Unternehmen</Link>
<Link to="/world-map" onClick={() => setMobileMenuOpen(false)} className="block py-2 text-textMuted hover:text-textMain">Weltkarte</Link>
{user?.isAdmin && (
<Link to="/admin" onClick={() => setMobileMenuOpen(false)} className="block py-2 text-red-400 hover:text-red-300">Admin</Link>
)}
<Link to="/datapack" onClick={() => setMobileMenuOpen(false)} className="block py-2 text-textMain">Datapack holen</Link>
<Link to="/setup" onClick={() => setMobileMenuOpen(false)} className="block py-2 text-accentInfo font-mono text-sm border-t border-white/5 pt-4">{"Admin Setup >_"}</Link>
</div>
)}
</header>
{/* Main Content - Page Flow */}
<main className="flex-1 w-full max-w-7xl mx-auto px-6 py-12 md:py-16">
<div className="animate-in fade-in duration-700 slide-in-from-bottom-4">
{children}
</div>
</main>
{/* Footer - Adds to the "Website" feel */}
<footer className="border-t border-border mt-auto bg-surface/30">
<div className="max-w-7xl mx-auto px-6 py-10 flex flex-col md:flex-row justify-between items-center text-sm text-textMuted">
<div className="flex items-center gap-2 mb-4 md:mb-0 opacity-50">
<div className="w-4 h-4 bg-textMuted rounded-full"></div>
<p>© 2024 Obsidian Platform</p>
</div>
<div className="flex flex-col md:flex-row items-center gap-8">
{/* Auth Section */}
<div className="flex items-center gap-4 mb-4 md:mb-0">
{user ? (
<div className="flex items-center gap-3">
<div className="flex items-center gap-2">
<img
src={user.avatarUrl}
alt={user.username}
className="w-6 h-6 rounded-full"
/>
<span className="text-textMain font-medium">{user.username}</span>
</div>
<button
onClick={() => authService.logout()}
className="text-xs text-textMuted hover:text-accentInfo transition-colors"
>
Logout
</button>
</div>
) : (
<div className="flex items-center gap-2">
<button
onClick={() => authService.login(rememberMe)}
className="flex items-center gap-2 text-textMain hover:text-accentInfo transition-colors font-medium text-sm"
>
<Icons.Users className="w-4 h-4" />
<span>Discord Login</span>
</button>
<label className="flex items-center gap-1 text-xs text-textMuted cursor-pointer">
<input
type="checkbox"
checked={rememberMe}
onChange={(e) => {
setRememberMe(e.target.checked);
// Store preference for next login
localStorage.setItem('rememberMe', e.target.checked.toString());
}}
className="w-3 h-3 text-accentInfo bg-surface border-border rounded focus:ring-accentInfo"
/>
<span>Remember me (30 days)</span>
</label>
</div>
)}
</div>
{/* Links */}
<div className="flex gap-8">
<button
onClick={() => onNavigate('/dokumentation')}
className="cursor-pointer hover:text-textMain transition-colors"
>
Dokumentation
</button>
<span className="cursor-pointer hover:text-textMain transition-colors">Server Status</span>
<span className="cursor-pointer hover:text-textMain transition-colors">Datenschutz</span>
</div>
</div>
</div>
</footer>
</div>
);
};
export default Layout;