← back to Norma
app/pulse/layout.tsx
457 lines
'use client';
import React, { useState, useEffect } from 'react';
import Link from 'next/link';
import { usePathname, useRouter } from 'next/navigation';
import {
Menu, X, Twitter, Facebook, Instagram, Mail,
Home, Activity, Bookmark, User, LogOut, ArrowLeft,
LogIn,
} from 'lucide-react';
import PulseSearch from '@/components/PulseSearch';
/* ────────────────────────── Types ────────────────────────── */
interface SessionInfo {
authenticated: boolean;
user?: string;
role?: 'admin' | 'staff' | 'intern' | 'pulse';
displayName?: string;
}
/* ────────────────────────── Colors ────────────────────────── */
const THEMES = {
classic: {
name: 'Classic',
darkGreen: '#1B4332',
darkGreenHover: '#163728',
gold: '#DAA520',
goldHover: '#c8941a',
cream: '#FAFAF5',
textLight: '#fafafa',
},
institutional: {
name: 'Institutional',
darkGreen: '#1e3a5f',
darkGreenHover: '#162d4a',
gold: '#2563eb',
goldHover: '#1d4ed8',
cream: '#f8fafc',
textLight: '#f1f5f9',
},
};
type ThemeKey = keyof typeof THEMES;
/* ────────────────────────── Nav Links ────────────────────────── */
const PUBLIC_NAV = [
{ href: '/pulse', label: 'Home', icon: Home },
{ href: '/pulse/petitions', label: 'Petitions' },
{ href: '/pulse/about', label: 'About' },
];
const USER_NAV = [
{ href: '/pulse', label: 'Home', icon: Home },
{ href: '/pulse/petitions', label: 'Petitions' },
{ href: '/pulse/activity', label: 'Activity', icon: Activity },
{ href: '/pulse/tracked', label: 'Tracked', icon: Bookmark },
{ href: '/pulse/profile', label: 'Profile', icon: User },
];
/* ────────────────────────── Layout ────────────────────────── */
export default function PulseLayout({ children }: { children: React.ReactNode }) {
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
const [session, setSession] = useState<SessionInfo>({ authenticated: false });
const [sessionLoaded, setSessionLoaded] = useState(false);
const [themeKey, setThemeKey] = useState<ThemeKey>(() => {
if (typeof window !== 'undefined') {
return (localStorage.getItem('pulse-theme') as ThemeKey) || 'classic';
}
return 'classic';
});
const C = THEMES[themeKey];
const pathname = usePathname();
const router = useRouter();
function toggleTheme() {
const next: ThemeKey = themeKey === 'classic' ? 'institutional' : 'classic';
setThemeKey(next);
if (typeof window !== 'undefined') localStorage.setItem('pulse-theme', next);
}
useEffect(() => {
fetch('/api/auth/session', { credentials: 'include' })
.then(r => r.json())
.then(data => {
if (data.authenticated) {
setSession({
authenticated: true,
user: data.user,
role: data.role,
displayName: data.displayName,
});
}
})
.catch(() => {})
.finally(() => setSessionLoaded(true));
}, []);
const navLinks = session.authenticated ? USER_NAV : PUBLIC_NAV;
const isAdminOrStaff = session.role === 'admin' || session.role === 'staff';
async function handleSignOut() {
await fetch('/api/auth/logout', { method: 'POST', credentials: 'include' });
setSession({ authenticated: false });
router.push('/pulse');
router.refresh();
}
function isActive(href: string): boolean {
if (href === '/pulse') return pathname === '/pulse';
return pathname.startsWith(href);
}
return (
<div style={{ minHeight: '100vh', backgroundColor: C.cream, color: '#1a1a1a', fontFamily: 'Georgia, "Times New Roman", serif' }}>
{/* Navigation */}
<nav style={{ backgroundColor: C.darkGreen, position: 'sticky', top: 0, zIndex: 50 }}>
<div style={{ maxWidth: 1200, margin: '0 auto', padding: '0 24px' }}>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', height: 72 }}>
{/* Logo */}
<Link href="/pulse" style={{ textDecoration: 'none', display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{
width: 40, height: 40, borderRadius: '50%', border: '2px solid ' + C.gold,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontFamily: 'Georgia, serif', fontWeight: 'bold', fontSize: 18,
color: C.gold,
}}>
P
</div>
<span style={{ color: C.gold, fontSize: 22, fontWeight: 'bold', letterSpacing: '0.5px', fontFamily: 'Georgia, serif' }}>
Pulse of America
</span>
</Link>
{/* Desktop nav */}
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }} className="poa-desktop-nav">
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
style={{
color: isActive(link.href) ? C.gold : C.textLight,
textDecoration: 'none', fontSize: 15,
fontFamily: 'system-ui, -apple-system, sans-serif', fontWeight: isActive(link.href) ? 600 : 500,
padding: '8px 14px', borderRadius: 8,
backgroundColor: isActive(link.href) ? 'rgba(218,165,32,0.12)' : 'transparent',
transition: 'all 150ms',
display: 'inline-flex', alignItems: 'center', gap: 6,
}}
>
{link.icon && <link.icon size={16} />}
{link.label}
</Link>
))}
{/* Theme toggle */}
<button
onClick={toggleTheme}
title={`Switch to ${themeKey === 'classic' ? 'Institutional' : 'Classic'} theme`}
style={{
background: 'rgba(255,255,255,0.1)', border: 'none', color: C.textLight,
padding: '6px 10px', borderRadius: 6, cursor: 'pointer', fontSize: 12,
fontFamily: 'system-ui, sans-serif', fontWeight: 600, transition: 'all 150ms',
}}
>
{themeKey === 'classic' ? '🏛' : '🌿'} {THEMES[themeKey === 'classic' ? 'institutional' : 'classic'].name}
</button>
{/* Global search (⌘K) — public-tier search over petitions/politicians/orgs/topics/articles */}
<PulseSearch theme={C} />
{/* Separator */}
<div style={{ width: 1, height: 28, backgroundColor: 'rgba(255,255,255,0.15)', margin: '0 8px' }} />
{/* Auth section */}
{sessionLoaded && session.authenticated ? (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
{/* Admin/staff: back to dashboard */}
{isAdminOrStaff && (
<Link
href="/"
style={{
color: C.textLight, textDecoration: 'none', fontSize: 13,
fontFamily: 'system-ui, sans-serif', fontWeight: 500,
padding: '6px 12px', borderRadius: 6,
backgroundColor: 'rgba(255,255,255,0.08)',
display: 'inline-flex', alignItems: 'center', gap: 5,
transition: 'background-color 150ms',
}}
>
<ArrowLeft size={14} />
Dashboard
</Link>
)}
{/* Username badge */}
<span style={{
color: C.gold, fontSize: 13, fontFamily: 'system-ui, sans-serif',
fontWeight: 600, padding: '6px 12px', borderRadius: 6,
backgroundColor: 'rgba(218,165,32,0.1)',
}}>
{session.displayName || session.user}
</span>
{/* Sign out */}
<button
onClick={handleSignOut}
style={{
color: C.textLight, background: 'none', border: 'none', cursor: 'pointer',
fontSize: 13, fontFamily: 'system-ui, sans-serif', fontWeight: 500,
padding: '6px 10px', borderRadius: 6, display: 'inline-flex',
alignItems: 'center', gap: 5, transition: 'background-color 150ms',
}}
onMouseEnter={e => (e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.08)')}
onMouseLeave={e => (e.currentTarget.style.backgroundColor = 'transparent')}
>
<LogOut size={14} />
Sign Out
</button>
</div>
) : sessionLoaded ? (
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
<Link
href={`/login?returnTo=${encodeURIComponent(pathname)}`}
style={{
color: C.textLight, textDecoration: 'none', fontSize: 14,
fontFamily: 'system-ui, sans-serif', fontWeight: 500,
padding: '8px 16px', borderRadius: 8,
display: 'inline-flex', alignItems: 'center', gap: 6,
transition: 'background-color 150ms',
}}
onMouseEnter={e => (e.currentTarget.style.backgroundColor = 'rgba(255,255,255,0.08)')}
onMouseLeave={e => (e.currentTarget.style.backgroundColor = 'transparent')}
>
<LogIn size={15} />
Sign In
</Link>
<Link
href="/pulse/petitions/create"
style={{
color: C.darkGreen, backgroundColor: C.gold, textDecoration: 'none',
fontSize: 14, fontFamily: 'system-ui, sans-serif', fontWeight: 600,
padding: '10px 24px', borderRadius: 9999, transition: 'background-color 150ms',
}}
onMouseEnter={e => (e.currentTarget.style.backgroundColor = C.goldHover)}
onMouseLeave={e => (e.currentTarget.style.backgroundColor = C.gold)}
>
Create A Petition
</Link>
</div>
) : null}
</div>
{/* Mobile menu button */}
<button
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
className="poa-mobile-menu-btn"
style={{
display: 'none', background: 'none', border: 'none', cursor: 'pointer',
color: C.textLight, padding: 8,
}}
aria-label="Toggle menu"
>
{mobileMenuOpen ? <X size={24} /> : <Menu size={24} />}
</button>
</div>
{/* Mobile menu */}
{mobileMenuOpen && (
<div className="poa-mobile-menu" style={{ paddingBottom: 16 }}>
{navLinks.map((link) => (
<Link
key={link.href}
href={link.href}
onClick={() => setMobileMenuOpen(false)}
style={{
display: 'flex', alignItems: 'center', gap: 8,
color: isActive(link.href) ? C.gold : C.textLight,
textDecoration: 'none', fontSize: 16,
fontFamily: 'system-ui, -apple-system, sans-serif',
fontWeight: isActive(link.href) ? 600 : 400,
padding: '12px 0',
borderBottom: '1px solid rgba(255,255,255,0.1)',
}}
>
{link.icon && <link.icon size={16} />}
{link.label}
</Link>
))}
{session.authenticated ? (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginTop: 12 }}>
{isAdminOrStaff && (
<Link
href="/"
onClick={() => setMobileMenuOpen(false)}
style={{
display: 'inline-flex', alignItems: 'center', gap: 6,
color: C.textLight, textDecoration: 'none', fontSize: 14,
fontFamily: 'system-ui, sans-serif',
}}
>
<ArrowLeft size={14} />
Back to Dashboard
</Link>
)}
<button
onClick={() => { setMobileMenuOpen(false); handleSignOut(); }}
style={{
display: 'inline-flex', alignItems: 'center', gap: 6,
color: C.textLight, background: 'none', border: 'none', cursor: 'pointer',
fontSize: 14, fontFamily: 'system-ui, sans-serif', padding: 0,
}}
>
<LogOut size={14} />
Sign Out ({session.displayName || session.user})
</button>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginTop: 12 }}>
<Link
href={`/login?returnTo=${encodeURIComponent(pathname)}`}
onClick={() => setMobileMenuOpen(false)}
style={{
display: 'inline-flex', alignItems: 'center', gap: 6,
color: C.textLight, textDecoration: 'none', fontSize: 14,
fontFamily: 'system-ui, sans-serif',
}}
>
<LogIn size={14} />
Sign In
</Link>
<Link
href="/pulse/petitions/create"
onClick={() => setMobileMenuOpen(false)}
style={{
display: 'inline-block', color: C.darkGreen, backgroundColor: C.gold,
textDecoration: 'none', fontSize: 14, fontWeight: 600,
padding: '10px 24px', borderRadius: 9999,
}}
>
Create A Petition
</Link>
</div>
)}
</div>
)}
</div>
</nav>
{/* Main content */}
<main>{children}</main>
{/* Footer */}
<footer style={{ backgroundColor: C.darkGreen, color: C.textLight, marginTop: 0 }}>
<div style={{ maxWidth: 1200, margin: '0 auto', padding: '48px 24px 32px' }}>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 48, justifyContent: 'space-between', marginBottom: 40 }}>
{/* Brand */}
<div style={{ maxWidth: 320 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 16 }}>
<div style={{
width: 36, height: 36, borderRadius: '50%', border: '2px solid ' + C.gold,
display: 'flex', alignItems: 'center', justifyContent: 'center',
fontWeight: 'bold', fontSize: 16, color: C.gold,
}}>P</div>
<span style={{ color: C.gold, fontSize: 20, fontWeight: 'bold', fontFamily: 'Georgia, serif' }}>
Pulse of America
</span>
</div>
<p style={{ fontSize: 14, lineHeight: '22px', opacity: 0.8, fontFamily: 'system-ui, sans-serif' }}>
Your Voice Matters. We empower citizens to create, sign, and share petitions
that drive real change in communities across America.
</p>
</div>
{/* Quick links */}
<div>
<h4 style={{ fontSize: 14, fontWeight: 600, color: C.gold, marginBottom: 16, fontFamily: 'system-ui, sans-serif', textTransform: 'uppercase', letterSpacing: 1 }}>
Quick Links
</h4>
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
{PUBLIC_NAV.map((link) => (
<Link
key={link.href}
href={link.href}
style={{ color: C.textLight, textDecoration: 'none', fontSize: 14, opacity: 0.8, fontFamily: 'system-ui, sans-serif' }}
>
{link.label}
</Link>
))}
</div>
</div>
{/* Social */}
<div>
<h4 style={{ fontSize: 14, fontWeight: 600, color: C.gold, marginBottom: 16, fontFamily: 'system-ui, sans-serif', textTransform: 'uppercase', letterSpacing: 1 }}>
Connect
</h4>
<div style={{ display: 'flex', gap: 16 }}>
{[
{ icon: <Twitter size={20} />, label: 'Twitter' },
{ icon: <Facebook size={20} />, label: 'Facebook' },
{ icon: <Instagram size={20} />, label: 'Instagram' },
{ icon: <Mail size={20} />, label: 'Email' },
].map((social) => (
<button
key={social.label}
aria-label={social.label}
style={{
width: 40, height: 40, borderRadius: '50%', border: '1px solid rgba(218,165,32,0.4)',
backgroundColor: 'transparent', color: C.gold, cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center',
transition: 'all 150ms',
}}
onMouseEnter={e => { e.currentTarget.style.backgroundColor = C.gold; e.currentTarget.style.color = C.darkGreen; }}
onMouseLeave={e => { e.currentTarget.style.backgroundColor = 'transparent'; e.currentTarget.style.color = C.gold; }}
>
{social.icon}
</button>
))}
</div>
</div>
</div>
{/* Bottom bar */}
<div style={{
borderTop: '1px solid rgba(255,255,255,0.15)', paddingTop: 24,
display: 'flex', flexWrap: 'wrap', justifyContent: 'space-between', alignItems: 'center', gap: 12,
}}>
<p style={{ fontSize: 13, opacity: 0.7, fontFamily: 'system-ui, sans-serif', margin: 0 }}>
Pulse of America — Your Voice Matters
</p>
<p style={{ fontSize: 13, opacity: 0.5, fontFamily: 'system-ui, sans-serif', margin: 0 }}>
Making democracy work, one petition at a time.
</p>
</div>
</div>
</footer>
{/* Scoped responsive styles */}
<style>{`
.poa-desktop-nav { display: flex !important; }
.poa-mobile-menu-btn { display: none !important; }
.poa-mobile-menu { display: block; }
@media (max-width: 768px) {
.poa-desktop-nav { display: none !important; }
.poa-mobile-menu-btn { display: block !important; }
}
`}</style>
</div>
);
}