feat: Refactor UI components by extracting Navigation, Footer, LogoMark, and Card; implement responsive design and improve code organization
This commit is contained in:
237
src/App.tsx
237
src/App.tsx
@@ -4,19 +4,14 @@ import {
|
|||||||
ArrowRight,
|
ArrowRight,
|
||||||
Backpack,
|
Backpack,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
Facebook,
|
|
||||||
Globe,
|
Globe,
|
||||||
Heart,
|
Heart,
|
||||||
Instagram,
|
|
||||||
Mail,
|
Mail,
|
||||||
MapPin,
|
MapPin,
|
||||||
Menu,
|
|
||||||
Moon,
|
|
||||||
Phone,
|
Phone,
|
||||||
Shirt,
|
Shirt,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
Star,
|
Star,
|
||||||
SunMedium,
|
|
||||||
Users,
|
Users,
|
||||||
Building2,
|
Building2,
|
||||||
BookOpenText,
|
BookOpenText,
|
||||||
@@ -67,6 +62,10 @@ import AdvancedAnalyticsDashboard from './components/AdvancedAnalyticsDashboard'
|
|||||||
import MobileVolunteerApp from './components/MobileVolunteerApp'
|
import MobileVolunteerApp from './components/MobileVolunteerApp'
|
||||||
import StaffTrainingDashboard from './components/StaffTrainingDashboard'
|
import StaffTrainingDashboard from './components/StaffTrainingDashboard'
|
||||||
|
|
||||||
|
// Phase 4: Extracted Components
|
||||||
|
import { Navigation } from './components/Navigation'
|
||||||
|
import { Footer } from './components/Footer'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Miracles in Motion — Complete Non-Profit Website
|
* Miracles in Motion — Complete Non-Profit Website
|
||||||
* A comprehensive 501(c)3 organization website with modern design,
|
* A comprehensive 501(c)3 organization website with modern design,
|
||||||
@@ -778,175 +777,9 @@ function SkipToContent() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NavProps {
|
// Nav component has been extracted to ./components/Navigation.tsx
|
||||||
darkMode: boolean
|
|
||||||
setDarkMode: (value: boolean) => void
|
|
||||||
mobileMenuOpen: boolean
|
|
||||||
setMobileMenuOpen: (value: boolean) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
function Nav({ darkMode, setDarkMode, mobileMenuOpen, setMobileMenuOpen }: NavProps) {
|
// LogoMark component has been extracted to ./components/ui/LogoMark.tsx
|
||||||
// Close mobile menu when route changes
|
|
||||||
useEffect(() => {
|
|
||||||
setMobileMenuOpen(false)
|
|
||||||
}, [window.location.hash])
|
|
||||||
|
|
||||||
// Handle keyboard navigation
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent, action: () => void) => {
|
|
||||||
if (e.key === 'Enter' || e.key === ' ') {
|
|
||||||
e.preventDefault()
|
|
||||||
action()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<nav className="mx-auto flex w-full max-w-7xl items-center justify-between px-4 py-3 sm:px-6 lg:px-8" role="navigation" aria-label="Main navigation">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<a
|
|
||||||
href="#/"
|
|
||||||
className="flex items-center gap-3 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded-lg p-1"
|
|
||||||
aria-label="Miracles in Motion - Home"
|
|
||||||
>
|
|
||||||
<LogoMark />
|
|
||||||
<div className="-space-y-1">
|
|
||||||
<div className="font-semibold tracking-tight">Miracles in Motion</div>
|
|
||||||
<div className="text-xs text-neutral-600 dark:text-neutral-400">Essentials for every student</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Desktop Navigation */}
|
|
||||||
<div className="hidden items-center gap-6 md:flex">
|
|
||||||
<a className="navlink focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded" href="#/stories" aria-label="Read success stories">Stories</a>
|
|
||||||
<a className="navlink focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded" href="#/testimonies" aria-label="View testimonies">Testimonies</a>
|
|
||||||
<a className="navlink focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded" href="#/volunteers" aria-label="Volunteer opportunities">Volunteers</a>
|
|
||||||
<a className="navlink focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded" href="#/sponsors" aria-label="Corporate partnerships">Corporate</a>
|
|
||||||
<a className="navlink focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded" href="#/request-assistance" aria-label="Request assistance">Get Help</a>
|
|
||||||
<a className="navlink focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded" href="#/portals" aria-label="Portal login">Portals</a>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Desktop Actions */}
|
|
||||||
<div className="hidden md:flex items-center gap-3">
|
|
||||||
<Magnetic>
|
|
||||||
<a
|
|
||||||
href="#/donate"
|
|
||||||
className="btn-primary focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
|
||||||
aria-label="Make a donation"
|
|
||||||
>
|
|
||||||
<Heart className="mr-2 h-4 w-4" aria-hidden="true" /> Donate
|
|
||||||
</a>
|
|
||||||
</Magnetic>
|
|
||||||
<button
|
|
||||||
aria-label={darkMode ? 'Switch to light mode' : 'Switch to dark mode'}
|
|
||||||
onClick={() => setDarkMode(!darkMode)}
|
|
||||||
onKeyDown={(e) => handleKeyDown(e, () => setDarkMode(!darkMode))}
|
|
||||||
className="group rounded-full border border-neutral-200/70 bg-white/70 p-2 shadow-sm transition hover:scale-105 hover:bg-white dark:border-white/10 dark:bg-white/10 dark:hover:bg-white/15 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
|
||||||
>
|
|
||||||
{darkMode ? (
|
|
||||||
<SunMedium className="h-5 w-5 transition group-hover:rotate-12" aria-hidden="true" />
|
|
||||||
) : (
|
|
||||||
<Moon className="h-5 w-5 transition group-hover:-rotate-12" aria-hidden="true" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile Actions */}
|
|
||||||
<div className="flex md:hidden items-center gap-2">
|
|
||||||
<Magnetic>
|
|
||||||
<a
|
|
||||||
href="#/donate"
|
|
||||||
className="btn-primary text-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
|
||||||
aria-label="Make a donation"
|
|
||||||
>
|
|
||||||
<Heart className="h-4 w-4" aria-hidden="true" />
|
|
||||||
</a>
|
|
||||||
</Magnetic>
|
|
||||||
<button
|
|
||||||
aria-label={darkMode ? 'Switch to light mode' : 'Switch to dark mode'}
|
|
||||||
onClick={() => setDarkMode(!darkMode)}
|
|
||||||
className="rounded-full border border-neutral-200/70 bg-white/70 p-2 shadow-sm transition hover:scale-105 hover:bg-white dark:border-white/10 dark:bg-white/10 dark:hover:bg-white/15 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
|
||||||
>
|
|
||||||
{darkMode ? (
|
|
||||||
<SunMedium className="h-4 w-4" aria-hidden="true" />
|
|
||||||
) : (
|
|
||||||
<Moon className="h-4 w-4" aria-hidden="true" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
aria-label={mobileMenuOpen ? 'Close navigation menu' : 'Open navigation menu'}
|
|
||||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
|
||||||
onKeyDown={(e) => handleKeyDown(e, () => setMobileMenuOpen(!mobileMenuOpen))}
|
|
||||||
className="rounded-full border border-neutral-200/70 bg-white/70 p-2 shadow-sm transition hover:scale-105 hover:bg-white dark:border-white/10 dark:bg-white/10 dark:hover:bg-white/15 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
|
||||||
aria-expanded={mobileMenuOpen}
|
|
||||||
aria-controls="mobile-menu"
|
|
||||||
>
|
|
||||||
{mobileMenuOpen ? (
|
|
||||||
<X className="h-5 w-5" aria-hidden="true" />
|
|
||||||
) : (
|
|
||||||
<Menu className="h-5 w-5" aria-hidden="true" />
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
{/* Mobile Menu */}
|
|
||||||
<motion.div
|
|
||||||
id="mobile-menu"
|
|
||||||
className="md:hidden"
|
|
||||||
initial={false}
|
|
||||||
animate={{
|
|
||||||
height: mobileMenuOpen ? 'auto' : 0,
|
|
||||||
opacity: mobileMenuOpen ? 1 : 0
|
|
||||||
}}
|
|
||||||
transition={{ duration: 0.3, ease: 'easeInOut' }}
|
|
||||||
style={{ overflow: 'hidden' }}
|
|
||||||
role="region"
|
|
||||||
aria-label="Mobile navigation menu"
|
|
||||||
>
|
|
||||||
<div className="border-t border-neutral-200/50 dark:border-white/10 bg-white/95 dark:bg-gray-900/95 backdrop-blur">
|
|
||||||
<div className="max-w-7xl mx-auto px-4 py-4 space-y-3">
|
|
||||||
<a className="block py-3 px-4 text-lg font-medium text-neutral-900 dark:text-white hover:bg-primary-50 dark:hover:bg-primary-900/20 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2" href="#/stories">Success Stories</a>
|
|
||||||
<a className="block py-3 px-4 text-lg font-medium text-neutral-900 dark:text-white hover:bg-primary-50 dark:hover:bg-primary-900/20 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2" href="#/testimonies">Testimonies</a>
|
|
||||||
<a className="block py-3 px-4 text-lg font-medium text-neutral-900 dark:text-white hover:bg-primary-50 dark:hover:bg-primary-900/20 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2" href="#/volunteers">Volunteer</a>
|
|
||||||
<a className="block py-3 px-4 text-lg font-medium text-neutral-900 dark:text-white hover:bg-primary-50 dark:hover:bg-primary-900/20 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2" href="#/sponsors">Corporate Partners</a>
|
|
||||||
<a className="block py-3 px-4 text-lg font-medium text-emerald-900 dark:text-emerald-100 bg-emerald-50 dark:bg-emerald-900/20 hover:bg-emerald-100 dark:hover:bg-emerald-900/30 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2" href="#/request-assistance">
|
|
||||||
<ClipboardList className="inline mr-2 h-5 w-5" aria-hidden="true" />
|
|
||||||
Request Assistance
|
|
||||||
</a>
|
|
||||||
<a className="block py-3 px-4 text-lg font-medium text-neutral-900 dark:text-white hover:bg-primary-50 dark:hover:bg-primary-900/20 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2" href="#/portals">Staff & Partner Portals</a>
|
|
||||||
<a className="block py-3 px-4 text-lg font-medium text-neutral-900 dark:text-white hover:bg-primary-50 dark:hover:bg-primary-900/20 rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2" href="#/legal">Legal & Privacy</a>
|
|
||||||
<div className="pt-2 border-t border-neutral-200/50 dark:border-white/10">
|
|
||||||
<a className="block py-3 px-4 bg-primary-600 text-white font-semibold rounded-lg hover:bg-primary-700 transition-colors focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2" href="#/donate">
|
|
||||||
<Heart className="inline mr-2 h-5 w-5" aria-hidden="true" />
|
|
||||||
Donate Now
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function LogoMark() {
|
|
||||||
return (
|
|
||||||
<div className="relative grid h-10 w-10 place-items-center overflow-hidden rounded-2xl bg-gradient-to-br from-primary-500 via-secondary-500 to-secondary-600 shadow-lg shadow-primary-500/20">
|
|
||||||
<motion.div
|
|
||||||
className="absolute inset-0 opacity-60"
|
|
||||||
animate={{
|
|
||||||
background: [
|
|
||||||
"radial-gradient(120px 80px at 20% 20%, rgba(255,255,255,0.4), transparent)",
|
|
||||||
"radial-gradient(120px 80px at 80% 30%, rgba(255,255,255,0.4), transparent)",
|
|
||||||
"radial-gradient(120px 80px at 50% 80%, rgba(255,255,255,0.4), transparent)",
|
|
||||||
]
|
|
||||||
}}
|
|
||||||
transition={{ duration: 6, repeat: Infinity, ease: "easeInOut" }}
|
|
||||||
/>
|
|
||||||
<Sparkles className="relative h-6 w-6 text-white drop-shadow" />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ===================== Home Page ===================== */
|
/* ===================== Home Page ===================== */
|
||||||
function HomePage() {
|
function HomePage() {
|
||||||
@@ -4374,61 +4207,7 @@ function BackgroundDecor() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function Footer() {
|
// Footer component has been extracted to ./components/Footer.tsx
|
||||||
return (
|
|
||||||
<footer className="relative mt-24 border-t border-white/30 bg-white/50 backdrop-blur dark:border-white/10 dark:bg-white/5">
|
|
||||||
<div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
|
|
||||||
<div className="grid gap-8 lg:grid-cols-4">
|
|
||||||
<div className="lg:col-span-2">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<LogoMark />
|
|
||||||
<div>
|
|
||||||
<div className="font-semibold">Miracles in Motion</div>
|
|
||||||
<div className="text-sm text-neutral-600 dark:text-neutral-400">Essentials for every student</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="mt-4 max-w-md text-sm text-neutral-600 dark:text-neutral-400">
|
|
||||||
A 501(c)(3) nonprofit providing students with school supplies, clothing, and emergency support to help them succeed.
|
|
||||||
</p>
|
|
||||||
<div className="mt-4 flex gap-4">
|
|
||||||
<a href="#" className="text-neutral-600 hover:text-primary-600 dark:text-neutral-400">
|
|
||||||
<Facebook className="h-5 w-5" />
|
|
||||||
</a>
|
|
||||||
<a href="#" className="text-neutral-600 hover:text-primary-600 dark:text-neutral-400">
|
|
||||||
<Instagram className="h-5 w-5" />
|
|
||||||
</a>
|
|
||||||
<a href="#" className="text-neutral-600 hover:text-primary-600 dark:text-neutral-400">
|
|
||||||
<Globe className="h-5 w-5" />
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold">Get Involved</h3>
|
|
||||||
<ul className="mt-4 space-y-2 text-sm">
|
|
||||||
<li><a href="#/donate" className="navlink">Donate</a></li>
|
|
||||||
<li><a href="#/volunteers" className="navlink">Volunteer</a></li>
|
|
||||||
<li><a href="#/sponsors" className="navlink">Corporate Partnerships</a></li>
|
|
||||||
<li><a href="#/stories" className="navlink">Success Stories</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold">Organization</h3>
|
|
||||||
<ul className="mt-4 space-y-2 text-sm">
|
|
||||||
<li><a href="#/testimonies" className="navlink">Testimonials</a></li>
|
|
||||||
<li><a href="#/legal" className="navlink">Legal & Policies</a></li>
|
|
||||||
<li><a href="mailto:contact@miraclesinmotion.org" className="navlink">Contact Us</a></li>
|
|
||||||
<li><a href="tel:+15551234567" className="navlink">(555) 123-4567</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-8 border-t border-white/30 pt-8 text-center text-xs text-neutral-500 dark:border-white/10 dark:text-neutral-400">
|
|
||||||
<p>© 2024 Miracles in Motion. All rights reserved. EIN: 12-3456789</p>
|
|
||||||
<p className="mt-1">501(c)(3) nonprofit organization. Donations are tax-deductible to the extent allowed by law.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function StickyDonate() {
|
function StickyDonate() {
|
||||||
return (
|
return (
|
||||||
@@ -4615,7 +4394,7 @@ function AppContent() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<header className="sticky top-0 z-50 backdrop-blur supports-[backdrop-filter]:bg-white/50 dark:supports-[backdrop-filter]:bg-black/40 border-b border-white/30 dark:border-white/10">
|
<header className="sticky top-0 z-50 backdrop-blur supports-[backdrop-filter]:bg-white/50 dark:supports-[backdrop-filter]:bg-black/40 border-b border-white/30 dark:border-white/10">
|
||||||
<Nav
|
<Navigation
|
||||||
darkMode={darkMode}
|
darkMode={darkMode}
|
||||||
setDarkMode={setDarkMode}
|
setDarkMode={setDarkMode}
|
||||||
mobileMenuOpen={isMobileMenuOpen}
|
mobileMenuOpen={isMobileMenuOpen}
|
||||||
|
|||||||
64
src/components/Footer.tsx
Normal file
64
src/components/Footer.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import {
|
||||||
|
Facebook,
|
||||||
|
Globe,
|
||||||
|
Instagram,
|
||||||
|
} from 'lucide-react'
|
||||||
|
import { LogoMark } from './ui/LogoMark'
|
||||||
|
|
||||||
|
export function Footer() {
|
||||||
|
return (
|
||||||
|
<footer className="relative mt-24 border-t border-white/30 bg-white/50 backdrop-blur dark:border-white/10 dark:bg-white/5">
|
||||||
|
<div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
|
||||||
|
<div className="grid gap-8 lg:grid-cols-4">
|
||||||
|
<div className="lg:col-span-2">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<LogoMark />
|
||||||
|
<div>
|
||||||
|
<div className="font-semibold">Miracles in Motion</div>
|
||||||
|
<div className="text-sm text-neutral-600 dark:text-neutral-400">Essentials for every student</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p className="mt-4 max-w-md text-sm text-neutral-600 dark:text-neutral-400">
|
||||||
|
A 501(c)(3) nonprofit providing students with school supplies, clothing, and emergency support to help them succeed.
|
||||||
|
</p>
|
||||||
|
<div className="mt-4 flex gap-4">
|
||||||
|
<a href="#" className="text-neutral-600 hover:text-primary-600 dark:text-neutral-400">
|
||||||
|
<Facebook className="h-5 w-5" />
|
||||||
|
</a>
|
||||||
|
<a href="#" className="text-neutral-600 hover:text-primary-600 dark:text-neutral-400">
|
||||||
|
<Instagram className="h-5 w-5" />
|
||||||
|
</a>
|
||||||
|
<a href="#" className="text-neutral-600 hover:text-primary-600 dark:text-neutral-400">
|
||||||
|
<Globe className="h-5 w-5" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold">Get Involved</h3>
|
||||||
|
<ul className="mt-4 space-y-2 text-sm">
|
||||||
|
<li><a href="#/donate" className="navlink">Donate</a></li>
|
||||||
|
<li><a href="#/volunteers" className="navlink">Volunteer</a></li>
|
||||||
|
<li><a href="#/sponsors" className="navlink">Corporate Partnerships</a></li>
|
||||||
|
<li><a href="#/stories" className="navlink">Success Stories</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="font-semibold">Organization</h3>
|
||||||
|
<ul className="mt-4 space-y-2 text-sm">
|
||||||
|
<li><a href="#/testimonies" className="navlink">Testimonials</a></li>
|
||||||
|
<li><a href="#/legal" className="navlink">Legal & Policies</a></li>
|
||||||
|
<li><a href="mailto:contact@miraclesinmotion.org" className="navlink">Contact Us</a></li>
|
||||||
|
<li><a href="tel:+15551234567" className="navlink">(555) 123-4567</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-8 border-t border-white/30 pt-8 text-center text-xs text-neutral-500 dark:border-white/10 dark:text-neutral-400">
|
||||||
|
<p>© 2024 Miracles in Motion. All rights reserved. EIN: 12-3456789</p>
|
||||||
|
<p className="mt-1">501(c)(3) nonprofit organization. Donations are tax-deductible to the extent allowed by law.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Footer
|
||||||
154
src/components/Navigation.tsx
Normal file
154
src/components/Navigation.tsx
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
import React, { useEffect } from 'react'
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import {
|
||||||
|
Heart,
|
||||||
|
Menu,
|
||||||
|
Moon,
|
||||||
|
SunMedium,
|
||||||
|
X,
|
||||||
|
} from 'lucide-react'
|
||||||
|
|
||||||
|
// Import UI components
|
||||||
|
import { Magnetic, LogoMark } from './ui'
|
||||||
|
|
||||||
|
interface NavProps {
|
||||||
|
darkMode: boolean
|
||||||
|
setDarkMode: (value: boolean) => void
|
||||||
|
mobileMenuOpen: boolean
|
||||||
|
setMobileMenuOpen: (value: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Navigation({ darkMode, setDarkMode, mobileMenuOpen, setMobileMenuOpen }: NavProps) {
|
||||||
|
// Close mobile menu when route changes
|
||||||
|
useEffect(() => {
|
||||||
|
setMobileMenuOpen(false)
|
||||||
|
}, [window.location.hash])
|
||||||
|
|
||||||
|
// Handle keyboard navigation
|
||||||
|
const handleKeyDown = (e: React.KeyboardEvent, action: () => void) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault()
|
||||||
|
action()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<nav className="mx-auto flex w-full max-w-7xl items-center justify-between px-4 py-3 sm:px-6 lg:px-8" role="navigation" aria-label="Main navigation">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<a
|
||||||
|
href="#/"
|
||||||
|
className="flex items-center gap-3 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded-lg p-1"
|
||||||
|
aria-label="Miracles in Motion - Home"
|
||||||
|
>
|
||||||
|
<LogoMark />
|
||||||
|
<div className="-space-y-1">
|
||||||
|
<div className="font-semibold tracking-tight">Miracles in Motion</div>
|
||||||
|
<div className="text-xs text-neutral-600 dark:text-neutral-400">Essentials for every student</div>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop Navigation */}
|
||||||
|
<div className="hidden items-center gap-6 md:flex">
|
||||||
|
<a className="navlink focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded" href="#/stories" aria-label="Read success stories">Stories</a>
|
||||||
|
<a className="navlink focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded" href="#/testimonies" aria-label="View testimonies">Testimonies</a>
|
||||||
|
<a className="navlink focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded" href="#/volunteers" aria-label="Volunteer opportunities">Volunteers</a>
|
||||||
|
<a className="navlink focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded" href="#/sponsors" aria-label="Corporate partnerships">Corporate</a>
|
||||||
|
<a className="navlink focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded" href="#/request-assistance" aria-label="Request assistance">Get Help</a>
|
||||||
|
<a className="navlink focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 rounded" href="#/portals" aria-label="Portal login">Portals</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Desktop Actions */}
|
||||||
|
<div className="hidden md:flex items-center gap-3">
|
||||||
|
<Magnetic>
|
||||||
|
<a
|
||||||
|
href="#/donate"
|
||||||
|
className="btn-primary focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||||
|
aria-label="Make a donation"
|
||||||
|
>
|
||||||
|
<Heart className="mr-2 h-4 w-4" aria-hidden="true" /> Donate
|
||||||
|
</a>
|
||||||
|
</Magnetic>
|
||||||
|
<button
|
||||||
|
aria-label={darkMode ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||||
|
onClick={() => setDarkMode(!darkMode)}
|
||||||
|
onKeyDown={(e) => handleKeyDown(e, () => setDarkMode(!darkMode))}
|
||||||
|
className="group rounded-full border border-neutral-200/70 bg-white/70 p-2 shadow-sm transition hover:scale-105 hover:bg-white dark:border-white/10 dark:bg-white/10 dark:hover:bg-white/15 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
{darkMode ? (
|
||||||
|
<SunMedium className="h-5 w-5 transition group-hover:rotate-12" aria-hidden="true" />
|
||||||
|
) : (
|
||||||
|
<Moon className="h-5 w-5 transition group-hover:-rotate-12" aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Mobile Actions */}
|
||||||
|
<div className="flex md:hidden items-center gap-2">
|
||||||
|
<Magnetic>
|
||||||
|
<a
|
||||||
|
href="#/donate"
|
||||||
|
className="btn-primary text-sm px-3 py-2 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||||
|
aria-label="Make a donation"
|
||||||
|
>
|
||||||
|
<Heart className="h-4 w-4" aria-hidden="true" />
|
||||||
|
</a>
|
||||||
|
</Magnetic>
|
||||||
|
<button
|
||||||
|
aria-label={darkMode ? 'Switch to light mode' : 'Switch to dark mode'}
|
||||||
|
onClick={() => setDarkMode(!darkMode)}
|
||||||
|
className="rounded-full border border-neutral-200/70 bg-white/70 p-2 shadow-sm transition hover:scale-105 hover:bg-white dark:border-white/10 dark:bg-white/10 dark:hover:bg-white/15 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
||||||
|
>
|
||||||
|
{darkMode ? (
|
||||||
|
<SunMedium className="h-4 w-4" aria-hidden="true" />
|
||||||
|
) : (
|
||||||
|
<Moon className="h-4 w-4" aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
aria-label={mobileMenuOpen ? 'Close navigation menu' : 'Open navigation menu'}
|
||||||
|
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
||||||
|
onKeyDown={(e) => handleKeyDown(e, () => setMobileMenuOpen(!mobileMenuOpen))}
|
||||||
|
className="rounded-full border border-neutral-200/70 bg-white/70 p-2 shadow-sm transition hover:scale-105 hover:bg-white dark:border-white/10 dark:bg-white/10 dark:hover:bg-white/15 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2"
|
||||||
|
aria-expanded={mobileMenuOpen}
|
||||||
|
aria-controls="mobile-menu"
|
||||||
|
>
|
||||||
|
{mobileMenuOpen ? (
|
||||||
|
<X className="h-5 w-5" aria-hidden="true" />
|
||||||
|
) : (
|
||||||
|
<Menu className="h-5 w-5" aria-hidden="true" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
{/* Mobile Menu */}
|
||||||
|
<motion.div
|
||||||
|
id="mobile-menu"
|
||||||
|
className="md:hidden"
|
||||||
|
initial={false}
|
||||||
|
animate={{
|
||||||
|
height: mobileMenuOpen ? 'auto' : 0,
|
||||||
|
opacity: mobileMenuOpen ? 1 : 0
|
||||||
|
}}
|
||||||
|
exit={{ height: 0, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.2 }}
|
||||||
|
style={{ overflow: 'hidden' }}
|
||||||
|
>
|
||||||
|
<div className="border-t border-neutral-200/50 bg-white/95 px-4 py-3 backdrop-blur dark:border-white/10 dark:bg-black/90">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<a className="block py-2 text-sm font-medium" href="#/stories">Stories</a>
|
||||||
|
<a className="block py-2 text-sm font-medium" href="#/testimonies">Testimonies</a>
|
||||||
|
<a className="block py-2 text-sm font-medium" href="#/volunteers">Volunteers</a>
|
||||||
|
<a className="block py-2 text-sm font-medium" href="#/sponsors">Corporate</a>
|
||||||
|
<a className="block py-2 text-sm font-medium" href="#/request-assistance">Get Help</a>
|
||||||
|
<a className="block py-2 text-sm font-medium" href="#/portals">Portals</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Navigation
|
||||||
23
src/components/ui/Card.tsx
Normal file
23
src/components/ui/Card.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { LucideIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
interface CardProps {
|
||||||
|
title: string
|
||||||
|
icon: LucideIcon
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Card({ title, icon: Icon, children }: CardProps) {
|
||||||
|
return (
|
||||||
|
<div className="card">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="grid h-10 w-10 place-items-center rounded-xl bg-gradient-to-br from-primary-500 to-secondary-600 text-white shadow">
|
||||||
|
<Icon className="h-5 w-5" />
|
||||||
|
</div>
|
||||||
|
<div className="font-semibold tracking-tight">{title}</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-3">{children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Card
|
||||||
24
src/components/ui/LogoMark.tsx
Normal file
24
src/components/ui/LogoMark.tsx
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
// LogoMark component
|
||||||
|
import { motion } from 'framer-motion'
|
||||||
|
import { Sparkles } from 'lucide-react'
|
||||||
|
|
||||||
|
export function LogoMark() {
|
||||||
|
return (
|
||||||
|
<div className="relative grid h-10 w-10 place-items-center overflow-hidden rounded-2xl bg-gradient-to-br from-primary-500 via-secondary-500 to-secondary-600 shadow-lg shadow-primary-500/20">
|
||||||
|
<motion.div
|
||||||
|
className="absolute inset-0 opacity-60"
|
||||||
|
animate={{
|
||||||
|
background: [
|
||||||
|
"radial-gradient(120px 80px at 20% 20%, rgba(255,255,255,0.4), transparent)",
|
||||||
|
"radial-gradient(120px 80px at 80% 30%, rgba(255,255,255,0.4), transparent)",
|
||||||
|
"radial-gradient(120px 80px at 50% 80%, rgba(255,255,255,0.4), transparent)",
|
||||||
|
]
|
||||||
|
}}
|
||||||
|
transition={{ duration: 6, repeat: Infinity, ease: "easeInOut" }}
|
||||||
|
/>
|
||||||
|
<Sparkles className="relative h-6 w-6 text-white drop-shadow" />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default LogoMark
|
||||||
15
src/components/ui/Magnetic.tsx
Normal file
15
src/components/ui/Magnetic.tsx
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react'
|
||||||
|
|
||||||
|
interface MagneticProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Magnetic({ children }: MagneticProps) {
|
||||||
|
return (
|
||||||
|
<div className="relative">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Magnetic
|
||||||
17
src/components/ui/SectionHeader.tsx
Normal file
17
src/components/ui/SectionHeader.tsx
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
interface SectionHeaderProps {
|
||||||
|
eyebrow?: string
|
||||||
|
title: string
|
||||||
|
subtitle?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SectionHeader({ eyebrow, title, subtitle }: SectionHeaderProps) {
|
||||||
|
return (
|
||||||
|
<div className="section-header">
|
||||||
|
{eyebrow && <div className="section-eyebrow">{eyebrow}</div>}
|
||||||
|
<h2 className="section-title">{title}</h2>
|
||||||
|
{subtitle && <p className="section-subtitle">{subtitle}</p>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SectionHeader
|
||||||
11
src/components/ui/index.tsx
Normal file
11
src/components/ui/index.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
// UI Component Exports
|
||||||
|
export { LogoMark } from './LogoMark'
|
||||||
|
export { Magnetic } from './Magnetic'
|
||||||
|
export { SectionHeader } from './SectionHeader'
|
||||||
|
export { Card } from './Card'
|
||||||
|
|
||||||
|
// Re-export everything
|
||||||
|
export * from './LogoMark'
|
||||||
|
export * from './Magnetic'
|
||||||
|
export * from './SectionHeader'
|
||||||
|
export * from './Card'
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
import React, { ReactNode } from 'react'
|
import React, { ReactNode, useState } from 'react'
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import { LucideIcon } from 'lucide-react'
|
import { LucideIcon } from 'lucide-react'
|
||||||
|
import { Navigation } from '../components/Navigation'
|
||||||
|
import { Footer } from '../components/Footer'
|
||||||
|
|
||||||
interface MainLayoutProps {
|
interface MainLayoutProps {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
className?: string
|
className?: string
|
||||||
|
darkMode?: boolean
|
||||||
|
setDarkMode?: (value: boolean) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PageShellProps {
|
interface PageShellProps {
|
||||||
@@ -17,11 +21,31 @@ interface PageShellProps {
|
|||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Main layout wrapper
|
// Main layout wrapper with Navigation and Footer
|
||||||
export const MainLayout: React.FC<MainLayoutProps> = ({ children, className = '' }) => {
|
export const MainLayout: React.FC<MainLayoutProps> = ({
|
||||||
|
children,
|
||||||
|
className = '',
|
||||||
|
darkMode = false,
|
||||||
|
setDarkMode = () => {}
|
||||||
|
}) => {
|
||||||
|
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`min-h-screen bg-gradient-to-br from-purple-50 via-white to-pink-50 dark:from-gray-900 dark:via-gray-800 dark:to-purple-900 ${className}`}>
|
<div className={`min-h-screen bg-gradient-to-br from-purple-50 via-white to-pink-50 dark:from-gray-900 dark:via-gray-800 dark:to-purple-900 ${className}`}>
|
||||||
{children}
|
<header className="sticky top-0 z-40 bg-white/80 backdrop-blur dark:bg-black/80">
|
||||||
|
<Navigation
|
||||||
|
darkMode={darkMode}
|
||||||
|
setDarkMode={setDarkMode}
|
||||||
|
mobileMenuOpen={mobileMenuOpen}
|
||||||
|
setMobileMenuOpen={setMobileMenuOpen}
|
||||||
|
/>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main id="content">
|
||||||
|
{children}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user