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,
|
||||
Backpack,
|
||||
CheckCircle2,
|
||||
Facebook,
|
||||
Globe,
|
||||
Heart,
|
||||
Instagram,
|
||||
Mail,
|
||||
MapPin,
|
||||
Menu,
|
||||
Moon,
|
||||
Phone,
|
||||
Shirt,
|
||||
Sparkles,
|
||||
Star,
|
||||
SunMedium,
|
||||
Users,
|
||||
Building2,
|
||||
BookOpenText,
|
||||
@@ -67,6 +62,10 @@ import AdvancedAnalyticsDashboard from './components/AdvancedAnalyticsDashboard'
|
||||
import MobileVolunteerApp from './components/MobileVolunteerApp'
|
||||
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
|
||||
* A comprehensive 501(c)3 organization website with modern design,
|
||||
@@ -778,175 +777,9 @@ function SkipToContent() {
|
||||
)
|
||||
}
|
||||
|
||||
interface NavProps {
|
||||
darkMode: boolean
|
||||
setDarkMode: (value: boolean) => void
|
||||
mobileMenuOpen: boolean
|
||||
setMobileMenuOpen: (value: boolean) => void
|
||||
}
|
||||
// Nav component has been extracted to ./components/Navigation.tsx
|
||||
|
||||
function Nav({ 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
|
||||
}}
|
||||
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>
|
||||
)
|
||||
}
|
||||
// LogoMark component has been extracted to ./components/ui/LogoMark.tsx
|
||||
|
||||
/* ===================== Home Page ===================== */
|
||||
function HomePage() {
|
||||
@@ -4374,61 +4207,7 @@ function BackgroundDecor() {
|
||||
)
|
||||
}
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
// Footer component has been extracted to ./components/Footer.tsx
|
||||
|
||||
function StickyDonate() {
|
||||
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">
|
||||
<Nav
|
||||
<Navigation
|
||||
darkMode={darkMode}
|
||||
setDarkMode={setDarkMode}
|
||||
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 { LucideIcon } from 'lucide-react'
|
||||
import { Navigation } from '../components/Navigation'
|
||||
import { Footer } from '../components/Footer'
|
||||
|
||||
interface MainLayoutProps {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
darkMode?: boolean
|
||||
setDarkMode?: (value: boolean) => void
|
||||
}
|
||||
|
||||
interface PageShellProps {
|
||||
@@ -17,11 +21,31 @@ interface PageShellProps {
|
||||
className?: string
|
||||
}
|
||||
|
||||
// Main layout wrapper
|
||||
export const MainLayout: React.FC<MainLayoutProps> = ({ children, className = '' }) => {
|
||||
// Main layout wrapper with Navigation and Footer
|
||||
export const MainLayout: React.FC<MainLayoutProps> = ({
|
||||
children,
|
||||
className = '',
|
||||
darkMode = false,
|
||||
setDarkMode = () => {}
|
||||
}) => {
|
||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
|
||||
|
||||
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}`}>
|
||||
{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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user