Spaces:
Running
Running
| import React from 'react'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | |
| import { Link, useLocation } from 'react-router-dom'; | |
| import { useAuth } from '../context/AuthContext'; | |
| import logo from '../assets/logo.png'; | |
| const Layout = ({ children }) => { | |
| const { user } = useAuth(); | |
| const location = useLocation(); | |
| const isLanding = location.pathname === '/'; | |
| const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false); | |
| const navItems = [ | |
| { name: 'Product', href: '#ai-agent' }, | |
| { name: 'How it Flows', href: '#how-it-works' }, | |
| { name: 'Templates', href: '#templates' }, | |
| { name: 'Pricing', href: '#pricing' } | |
| ]; | |
| const [activeSection, setActiveSection] = React.useState('#ai-agent'); | |
| React.useEffect(() => { | |
| if (!isLanding) return; | |
| const timer = setTimeout(() => { | |
| const observerOptions = { | |
| root: null, | |
| rootMargin: '-30% 0px -60% 0px', | |
| threshold: 0 | |
| }; | |
| const observer = new IntersectionObserver((entries) => { | |
| entries.forEach(entry => { | |
| if (entry.isIntersecting) { | |
| if (entry.target.id === 'hero') { | |
| setActiveSection('#ai-agent'); | |
| } else { | |
| setActiveSection(`#${entry.target.id}`); | |
| } | |
| } | |
| }); | |
| }, observerOptions); | |
| const sectionsToObserve = ['hero', 'ai-agent', 'how-it-works', 'templates', 'pricing']; | |
| sectionsToObserve.forEach(id => { | |
| const el = document.getElementById(id); | |
| if (el) observer.observe(el); | |
| }); | |
| return () => observer.disconnect(); | |
| }, 100); | |
| return () => clearTimeout(timer); | |
| }, [isLanding]); | |
| return ( | |
| <div className="flex flex-col min-h-screen text-on-background"> | |
| {/* TopNavBar */} | |
| <nav className="bg-surface-container-lowest border-b-2 border-primary shadow-[6px_6px_0px_0px_var(--color-primary)] dark:shadow-[6px_6px_0px_0px_rgba(255,255,255,0.07)] sticky top-0 z-50"> | |
| <div className="flex justify-between items-center w-full px-margin_edge py-5 max-w-screen-2xl mx-auto"> | |
| <Link to="/"> | |
| <motion.div | |
| initial={{ opacity: 0, x: -20 }} | |
| animate={{ opacity: 1, x: 0 }} | |
| className="font-display-lg text-4xl text-primary tracking-tight cursor-pointer flex items-center gap-3" | |
| > | |
| <img src={logo} alt="TaskFlow Logo" className="w-10 h-10 object-contain" /> | |
| <span>TaskFlow</span> | |
| </motion.div> | |
| </Link> | |
| <div className="hidden md:flex gap-gutter items-center"> | |
| {navItems.map((item, i) => { | |
| const isActive = activeSection === item.href; | |
| return ( | |
| <motion.a | |
| key={item.name} | |
| initial={{ opacity: 0, y: -10 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: 0.1 * i }} | |
| className={`text-xl transition-all relative py-2 ${ | |
| isActive | |
| ? 'text-primary font-bold border-b-2 border-secondary' | |
| : 'text-on-surface-variant hover:text-primary' | |
| }`} | |
| href={isLanding ? item.href : `/${item.href}`} | |
| > | |
| {item.name} | |
| </motion.a> | |
| ); | |
| })} | |
| </div> | |
| <div className="flex gap-6 items-center"> | |
| <div className="hidden sm:flex gap-6 items-center"> | |
| {user ? ( | |
| <Link to="/dashboard"> | |
| <motion.button | |
| whileHover={{ scale: 1.05 }} | |
| whileTap={{ scale: 0.95 }} | |
| className="bg-primary text-on-primary font-bold py-3 px-6 rough-border rough-shadow-hover transition-all text-xl" | |
| > | |
| Go to Workspace | |
| </motion.button> | |
| </Link> | |
| ) : ( | |
| <> | |
| <Link to="/auth"> | |
| <button className="text-xl font-medium text-on-surface-variant hover:text-primary transition-colors">Log In</button> | |
| </Link> | |
| <Link to="/auth"> | |
| <motion.button | |
| whileHover={{ scale: 1.05 }} | |
| whileTap={{ scale: 0.95 }} | |
| className="bg-primary text-on-primary font-bold py-3 px-6 rough-border rough-shadow-hover transition-all" | |
| > | |
| Start Sketching | |
| </motion.button> | |
| </Link> | |
| </> | |
| )} | |
| </div> | |
| {/* Mobile Menu Toggle */} | |
| <button | |
| className="md:hidden p-2 text-primary" | |
| onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)} | |
| > | |
| <svg xmlns="http://www.w3.org/2000/svg" className="h-10 w-10" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={isMobileMenuOpen ? "M6 18L18 6M6 6l12 12" : "M4 6h16M4 12h16M4 18h16"} /> | |
| </svg> | |
| </button> | |
| </div> | |
| </div> | |
| {/* Mobile Menu Overlay */} | |
| <AnimatePresence> | |
| {isMobileMenuOpen && ( | |
| <motion.div | |
| initial={{ opacity: 0, height: 0 }} | |
| animate={{ opacity: 1, height: 'auto' }} | |
| exit={{ opacity: 0, height: 0 }} | |
| className="md:hidden bg-surface-container-lowest border-t-2 border-primary overflow-hidden" | |
| > | |
| <div className="flex flex-col p-margin_edge gap-6"> | |
| {navItems.map((item) => { | |
| const isActive = activeSection === item.href; | |
| return ( | |
| <a | |
| key={item.name} | |
| href={isLanding ? item.href : `/${item.href}`} | |
| className={`text-2xl font-display-lg transition-colors ${ | |
| isActive ? 'text-primary font-bold' : 'text-on-surface-variant hover:text-primary' | |
| }`} | |
| onClick={() => setIsMobileMenuOpen(false)} | |
| > | |
| {item.name} | |
| </a> | |
| ); | |
| })} | |
| <div className="flex flex-col gap-4 pt-6 border-t border-dashed border-primary/20"> | |
| {user ? ( | |
| <Link to="/dashboard" onClick={() => setIsMobileMenuOpen(false)}> | |
| <button className="w-full py-4 text-2xl font-bold bg-primary text-on-primary rough-border rough-shadow">Go to Workspace</button> | |
| </Link> | |
| ) : ( | |
| <> | |
| <Link to="/auth" onClick={() => setIsMobileMenuOpen(false)}> | |
| <button className="w-full py-4 text-2xl font-bold text-primary rough-border">Log In</button> | |
| </Link> | |
| <Link to="/auth" onClick={() => setIsMobileMenuOpen(false)}> | |
| <button className="w-full py-4 text-2xl font-bold bg-primary text-on-primary rough-border rough-shadow">Start Sketching</button> | |
| </Link> | |
| </> | |
| )} | |
| </div> | |
| </div> | |
| </motion.div> | |
| )} | |
| </AnimatePresence> | |
| </nav> | |
| {/* Page Content */} | |
| <main className="flex-grow"> | |
| {children} | |
| </main> | |
| {/* Footer */} | |
| <footer className="bg-surface-container-lowest w-full border-t-4 border-primary mt-20"> | |
| <div className="grid grid-cols-1 md:grid-cols-2 gap-gutter px-margin_edge py-16 w-full max-w-screen-2xl mx-auto"> | |
| <div className="flex flex-col gap-6"> | |
| <div className="text-5xl font-display-lg text-primary flex items-center gap-3"> | |
| <img src={logo} alt="TaskFlow Logo" className="w-12 h-12 object-contain" /> | |
| <span>TaskFlow</span> | |
| </div> | |
| <div className="text-xl text-on-surface-variant"> | |
| © 2026 TaskFlow. Designed for focus, built for resilience. | |
| </div> | |
| </div> | |
| <div className="flex flex-wrap gap-10 items-center md:justify-end"> | |
| {['Privacy', 'Terms', 'Twitter', 'Changelog'].map(item => ( | |
| <a key={item} className="text-xl font-bold text-on-surface-variant hover:text-primary underline decoration-secondary decoration-4 transition-all" href="#">{item}</a> | |
| ))} | |
| </div> | |
| </div> | |
| </footer> | |
| </div> | |
| ); | |
| }; | |
| export default Layout; | |