Files
dewedev/src/components/Layout.js

434 lines
21 KiB
JavaScript
Executable File
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useRef } from 'react';
import { useLocation } from 'react-router-dom';
import ToolSidebar from './ToolSidebar';
import NavigationConfirmModal from './NavigationConfirmModal';
import useNavigationGuard from '../hooks/useNavigationGuard';
import { Menu, X, ChevronDown, Terminal, Sparkles, Home } from 'lucide-react';
import ThemeToggle from './ThemeToggle';
import SEOHead from './SEOHead';
import ConsentBanner from './ConsentBanner';
import { NON_TOOLS, TOOLS, SITE_CONFIG, getCategoryConfig } from '../config/tools';
import { useAnalytics } from '../hooks/useAnalytics';
const Layout = ({ children }) => {
const location = useLocation();
const { showModal, pendingNavigation, handleConfirm, handleCancel, hasUnsavedData, navigateWithGuard } = useNavigationGuard();
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const dropdownRef = useRef(null);
// Initialize analytics tracking
useAnalytics();
const isActive = (path) => {
return location.pathname === path;
};
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsDropdownOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
// Close mobile menu when route changes
useEffect(() => {
setIsMobileMenuOpen(false);
setIsDropdownOpen(false);
}, [location.pathname]);
// Check if we're on a tool page (not homepage)
const isToolPage = location.pathname !== '/';
// Check if we're on invoice preview page (no sidebar needed)
const isInvoicePreviewPage = location.pathname === '/invoice-preview';
return (
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-blue-50 to-indigo-50 dark:from-slate-900 dark:via-slate-800 dark:to-indigo-900 flex flex-col">
{/* SEO Head Management */}
<SEOHead />
{/* Header */}
<header className="fixed top-0 left-0 right-0 z-50 bg-white/80 dark:bg-slate-800/80 backdrop-blur-md shadow-lg border-b border-slate-200/50 dark:border-slate-700/50 flex-shrink-0">
<div className={isToolPage ? "px-4 sm:px-6 lg:px-8" : "max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"}>
<div className="flex justify-between items-center h-16">
<button onClick={() => navigateWithGuard('/')} className="flex items-center group">
<div className="relative">
<div className="absolute inset-0 rounded-lg blur opacity-20 group-hover:opacity-40 transition-opacity"></div>
<div className="relative p-2">
<img
src="/logo.svg"
alt={SITE_CONFIG.title}
className="h-8 w-auto"
style={{ maxWidth: '150px' }}
onError={(e) => {
// Fallback to Terminal icon with text if logo fails to load
e.target.style.display = 'none';
e.target.nextSibling.style.display = 'flex';
}}
/>
<div className="hidden items-center space-x-3">
<Terminal className="h-6 w-6 text-blue-500" />
<span className="text-xl font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-indigo-600 bg-clip-text text-transparent">
{SITE_CONFIG.title}
</span>
</div>
</div>
</div>
</button>
<div className="flex items-center space-x-4">
{/* Desktop Navigation - only show on homepage */}
{!isToolPage && (
<nav className="hidden md:flex items-center space-x-6">
<button
onClick={() => {
setIsDropdownOpen(false);
navigateWithGuard('/');
}}
className={`flex items-center space-x-2 px-4 py-2 rounded-xl text-sm font-medium transition-all duration-300 ${
isActive('/')
? 'bg-gradient-to-r from-blue-500 to-purple-500 text-white shadow-lg'
: 'text-slate-600 dark:text-slate-300 hover:text-blue-600 dark:hover:text-blue-400 hover:bg-white/50 dark:hover:bg-slate-700/50'
}`}
>
<Home className="h-4 w-4" />
<span>Home</span>
</button>
{/* Tools Dropdown */}
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
className="flex items-center space-x-2 px-4 py-2 rounded-xl text-sm font-medium text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50 transition-all duration-300"
aria-expanded={isDropdownOpen}
aria-haspopup="true"
>
<Sparkles className="h-4 w-4" />
<span>Tools</span>
<ChevronDown className={`h-4 w-4 transition-transform duration-300 ${
isDropdownOpen ? 'rotate-180' : ''
}`} />
</button>
{/* Dropdown Menu */}
{isDropdownOpen && (
<div className="absolute top-full left-0 mt-3 w-80 bg-white/90 dark:bg-slate-800/90 backdrop-blur-md rounded-2xl shadow-2xl border border-slate-200/50 dark:border-slate-700/50 py-3 z-50 overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-blue-50/50 to-purple-50/50 dark:from-slate-800/50 dark:to-slate-700/50"></div>
<div className="relative">
{TOOLS.map((tool) => {
const IconComponent = tool.icon;
const categoryConfig = getCategoryConfig(tool.category);
return (
<button
key={tool.path}
onClick={() => {
setIsDropdownOpen(false);
navigateWithGuard(tool.path);
}}
className={`group flex items-center space-x-4 px-4 py-3 text-sm hover:bg-white/50 dark:hover:bg-slate-700/50 transition-all duration-300 w-full text-left ${
isActive(tool.path)
? 'bg-gradient-to-r from-blue-50 to-purple-50 dark:from-slate-700 dark:to-slate-600 text-blue-700 dark:text-blue-300'
: 'text-slate-700 dark:text-slate-300'
}`}
>
<div className={`p-2 rounded-lg bg-gradient-to-br ${categoryConfig.color} shadow-sm group-hover:scale-110 transition-transform duration-300`}>
<IconComponent className="h-4 w-4 text-white" />
</div>
<div className="flex-1">
<div className="font-medium">{tool.name}</div>
<div className="text-xs text-slate-600 dark:text-slate-600">{tool.description}</div>
</div>
<div className="opacity-0 group-hover:opacity-100 transition-opacity duration-300">
<ChevronDown className="h-4 w-4 -rotate-90 text-slate-600" />
</div>
</button>
);
})}
</div>
</div>
)}
</div>
</nav>
)}
<ThemeToggle />
{/* Mobile Menu Button */}
<button
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
className="md:hidden p-2 rounded-xl text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50 transition-all duration-300"
aria-label={isMobileMenuOpen ? "Close menu" : "Open menu"}
aria-expanded={isMobileMenuOpen}
>
{isMobileMenuOpen ? <X className="h-6 w-6" /> : <Menu className="h-6 w-6" />}
</button>
</div>
</div>
</div>
</header>
{/* Mobile Navigation Menu */}
{isMobileMenuOpen && (
<>
{/* Overlay */}
<div
className="md:hidden fixed inset-0 bg-black/20 z-30"
onClick={() => setIsMobileMenuOpen(false)}
/>
{/* Menu */}
<div className="md:hidden fixed top-16 left-0 right-0 z-40 bg-white/95 dark:bg-slate-800/95 backdrop-blur-md border-b border-slate-200/50 dark:border-slate-700/50 shadow-lg max-h-[calc(100vh-4rem)]">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="space-y-2">
{/* Non-Tools Section */}
{NON_TOOLS.map((tool) => {
const IconComponent = tool.icon;
return (
<button
key={tool.path}
onClick={() => {
setIsMobileMenuOpen(false);
navigateWithGuard(tool.path);
}}
className={`flex items-center space-x-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 w-full text-left ${
isActive(tool.path)
? 'bg-gradient-to-r from-indigo-500 to-purple-500 text-white shadow-lg'
: 'text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50'
}`}
>
<div className={`p-2 rounded-lg ${isActive(tool.path) ? 'bg-white/20' : 'bg-gradient-to-br from-indigo-500 to-purple-500'} shadow-sm`}>
<IconComponent className={`h-4 w-4 ${isActive(tool.path) ? 'text-white' : 'text-white'}`} />
</div>
<span>{tool.name}</span>
</button>
);
})}
<div className="border-t border-slate-200/50 dark:border-slate-700/50 pt-4 mt-4">
<div className="text-xs font-semibold text-slate-600 dark:text-slate-600 uppercase tracking-wider px-4 py-2 flex items-center gap-2">
<Sparkles className="h-3 w-3" />
{isToolPage ? 'Switch Tools' : 'Tools'}
</div>
{TOOLS.map((tool) => {
const IconComponent = tool.icon;
const categoryConfig = getCategoryConfig(tool.category);
return (
<button
key={tool.path}
onClick={() => {
setIsMobileMenuOpen(false);
navigateWithGuard(tool.path);
}}
className={`flex items-center space-x-4 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 w-full text-left ${
isActive(tool.path)
? 'bg-gradient-to-r from-blue-50 to-purple-50 dark:from-slate-700 dark:to-slate-600 text-blue-700 dark:text-blue-300'
: 'text-slate-600 hover:text-slate-900 dark:text-slate-300 dark:hover:text-white hover:bg-white/50 dark:hover:bg-slate-700/50'
}`}
>
<div className={`p-2 rounded-lg bg-gradient-to-br ${categoryConfig.color} shadow-sm`}>
<IconComponent className="h-4 w-4 text-white" />
</div>
<div className="flex-1">
<div className="font-medium">{tool.name}</div>
<div className="text-xs text-slate-600 dark:text-slate-600">{tool.description}</div>
</div>
</button>
);
})}
</div>
</div>
</div>
</div>
</>
)}
{/* Main Content */}
<div className="flex flex-1 pt-16 min-w-0 w-full max-w-full overflow-x-hidden">
{/* Main Content Area */}
<main className="flex-1 flex flex-col min-w-0 w-full max-w-full">
{isToolPage && !isInvoicePreviewPage ? (
<div className="block">
<div className="hidden lg:block fixed top-16 left-0 z-[9999]">
<ToolSidebar navigateWithGuard={navigateWithGuard} />
</div>
<div className="flex-1 flex flex-col pl-0 lg:pl-16 min-w-0">
<div className="p-4 sm:p-6 w-full min-w-0 max-w-full overflow-x-hidden">
{children}
</div>
</div>
</div>
) : isInvoicePreviewPage ? (
<div className="flex-1 flex flex-col">
<div className="flex-1">
{children}
</div>
</div>
) : (
<div className="flex-1 flex flex-col">
<div className="flex-1">
{children}
</div>
{/* Global Footer for Homepage */}
<footer className="bg-white/30 dark:bg-slate-800/30 backdrop-blur-sm border-t border-slate-200/30 dark:border-slate-700/30 mt-20">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div className="text-center">
<div className="flex items-center justify-center mb-4">
<div className="relative">
<div className="absolute inset-0 rounded-lg blur opacity-20"></div>
<div className="relative p-2">
<img
src="/icon-192x192.png"
alt={SITE_CONFIG.title}
className="h-16 w-auto"
style={{ maxWidth: '100px' }}
onError={(e) => {
// Fallback to Terminal icon with text if logo fails to load
e.target.style.display = 'none';
e.target.nextSibling.style.display = 'flex';
}}
/>
<div className="hidden items-center gap-3">
<Terminal className="h-5 w-5 text-blue-500" />
<span className="text-lg font-bold bg-gradient-to-r from-blue-600 via-purple-600 to-indigo-600 bg-clip-text text-transparent">
{SITE_CONFIG.title}
</span>
</div>
</div>
</div>
</div>
<div className="flex items-center justify-center gap-2 mb-3">
<div className="w-2 h-2 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full"></div>
<span className="text-sm font-medium text-slate-600 dark:text-slate-600">
© {SITE_CONFIG.year} {SITE_CONFIG.title}
</span>
<div className="w-2 h-2 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-full"></div>
</div>
<p className="text-sm text-slate-600 dark:text-slate-600 mb-4">
Built with for developers worldwide
</p>
<div className="flex flex-col items-center gap-4">
<div className="flex justify-center items-center gap-6 text-xs text-slate-600 dark:text-slate-600">
<div className="flex items-center gap-1">
<div className="w-1.5 h-1.5 bg-green-500 rounded-full animate-pulse"></div>
<span>100% Client-Side</span>
</div>
<div className="flex items-center gap-1">
<div className="w-1.5 h-1.5 bg-blue-500 rounded-full"></div>
<span>Privacy First</span>
</div>
<div className="flex items-center gap-1">
<div className="w-1.5 h-1.5 bg-purple-500 rounded-full"></div>
<span>Open Source</span>
</div>
</div>
<div className="flex items-center gap-4 text-xs">
<button
onClick={() => navigateWithGuard('/release-notes')}
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Release Notes
</button>
<span className="text-slate-300 dark:text-slate-600"></span>
<button
onClick={() => navigateWithGuard('/privacy')}
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Privacy Policy
</button>
<span className="text-slate-300 dark:text-slate-600"></span>
<button
onClick={() => navigateWithGuard('/terms')}
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Terms of Service
</button>
</div>
</div>
</div>
</div>
</footer>
</div>
)}
</main>
</div>
{/* Footer for Tool Pages */}
{isToolPage && (
<footer className="bg-white/30 dark:bg-slate-800/30 backdrop-blur-sm border-t border-slate-200/30 dark:border-slate-700/30">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
<div className="text-center">
<div className="flex items-center justify-center gap-2 mb-2">
<img
src="/icon-192x192.png"
alt={SITE_CONFIG.title}
className="h-16 w-auto"
style={{ maxWidth: '100px' }}
onError={(e) => {
// Fallback to Terminal icon with text if logo fails to load
e.target.style.display = 'none';
e.target.nextSibling.style.display = 'flex';
}}
/>
</div>
<div className="flex items-center justify-center gap-2 mb-2">
<div className="w-2 h-2 bg-gradient-to-r from-blue-500 to-purple-500 rounded-full"></div>
<span className="text-xs font-medium text-slate-600 dark:text-slate-600">
© {SITE_CONFIG.year} {SITE_CONFIG.title}
</span>
<div className="w-2 h-2 bg-gradient-to-r from-purple-500 to-indigo-500 rounded-full"></div>
</div>
<div className="flex items-center justify-center gap-4 text-xs">
<button
onClick={() => navigateWithGuard('/release-notes')}
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Release Notes
</button>
<span className="text-slate-300 dark:text-slate-600"></span>
<button
onClick={() => navigateWithGuard('/privacy')}
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Privacy Policy
</button>
<span className="text-slate-300 dark:text-slate-600"></span>
<button
onClick={() => navigateWithGuard('/terms')}
className="text-slate-600 hover:text-slate-600 dark:hover:text-slate-300 transition-colors"
>
Terms of Service
</button>
</div>
</div>
</div>
</footer>
)}
{/* GDPR Consent Banner */}
<ConsentBanner />
{/* Navigation Confirmation Modal */}
<NavigationConfirmModal
isOpen={showModal}
onConfirm={handleConfirm}
onCancel={handleCancel}
targetPath={pendingNavigation?.to}
hasData={hasUnsavedData}
/>
</div>
);
};
export default Layout;