Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<head>
<meta charset="UTF-8" />
<meta name="google-site-verification" content="yPFrltKCrAwfFqVv0l2yJVPokUucON17oNFquEu_Zeg" />
<link rel="icon" type="image/svg+xml" href="/policyengine-favicon.svg" />
<link rel="icon" type="image/svg+xml" href="https://www.policyengine.org/assets/logos/policyengine/teal-square.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Track state-level tax and benefit legislation across all 50 states. See fiscal impacts, winners and losers, and district-level analysis powered by PolicyEngine microsimulation." />
<meta property="og:title" content="2026 State Legislative Tracker | PolicyEngine" />
Expand Down
120 changes: 27 additions & 93 deletions src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { useState, useEffect, useCallback, useMemo, lazy, Suspense } from "react";
import PolicyEngineHeader from "./components/PolicyEngineHeader";
import PolicyEngineFooter from "./components/PolicyEngineFooter";
import USMap from "./components/USMap";
import Breadcrumb from "./components/Breadcrumb";
import StateSearchCombobox from "./components/StateSearchCombobox";
Expand Down Expand Up @@ -125,47 +127,38 @@ function App() {

return (
<div className="app-shell" style={{ minHeight: "100vh" }}>
{/* Header */}
{/* PE org header */}
<PolicyEngineHeader />

{/* Tracker title bar + state search */}
<header
className="header-accent"
style={{
backgroundColor: colors.white,
boxShadow: "var(--shadow-elevation-low)",
position: "sticky",
top: 0,
zIndex: 50,
}}
>
<div className="app-header-inner" style={{ maxWidth: "1400px", margin: "0 auto", padding: `${spacing.xl} ${spacing["2xl"]}` }}>
<div className="app-header-row" style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
<div className="app-header-brand" style={{ display: "flex", alignItems: "center", gap: spacing.lg }}>
<a href="https://policyengine.org" target="_blank" rel="noopener noreferrer">
<img
src="/policyengine-favicon.svg"
alt="PolicyEngine"
style={{ height: "40px", width: "auto" }}
/>
</a>
<div>
<h1 style={{
margin: 0,
color: colors.secondary[900],
fontSize: typography.fontSize["2xl"],
fontWeight: typography.fontWeight.bold,
fontFamily: typography.fontFamily.primary,
letterSpacing: "-0.02em",
}}>
2026 State Legislative Tracker
</h1>
<p style={{
margin: "2px 0 0",
color: colors.text.secondary,
fontSize: typography.fontSize.sm,
fontFamily: typography.fontFamily.body,
}}>
PolicyEngine State Tax Research
</p>
</div>
<div>
<h1 style={{
margin: 0,
color: colors.secondary[900],
fontSize: typography.fontSize["2xl"],
fontWeight: typography.fontWeight.bold,
fontFamily: typography.fontFamily.primary,
letterSpacing: "-0.02em",
}}>
2026 State Legislative Tracker
</h1>
<p style={{
margin: "2px 0 0",
color: colors.text.secondary,
fontSize: typography.fontSize.sm,
fontFamily: typography.fontFamily.body,
}}>
PolicyEngine State Tax Research
</p>
</div>
<StateSearchCombobox onSelect={handleStateSelect} statesWithBills={statesWithBills} />
</div>
Expand Down Expand Up @@ -338,44 +331,8 @@ function App() {
)}
</main>

{/* Footer */}
<footer style={{
backgroundColor: colors.secondary[900],
color: colors.white,
position: "relative",
overflow: "hidden",
}}>
{/* Gradient accent at top */}
<div style={{
position: "absolute",
top: 0,
left: 0,
right: 0,
height: "3px",
background: "linear-gradient(90deg, #2C7A7B 0%, #38B2AC 50%, #0EA5E9 100%)",
}} />
<div className="app-footer-inner" style={{
maxWidth: "1400px",
margin: "0 auto",
padding: `${spacing["2xl"]} ${spacing["2xl"]}`,
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}>
<p style={{
margin: 0,
fontSize: typography.fontSize.sm,
fontFamily: typography.fontFamily.body,
color: colors.gray[400],
}}>
© {new Date().getFullYear()} PolicyEngine. Open-source tax and benefit policy simulation.
</p>
<div className="app-footer-links" style={{ display: "flex", gap: spacing.lg }}>
<FooterLink href="https://github.com/policyengine">GitHub</FooterLink>
<FooterLink href="https://policyengine.org">PolicyEngine.org</FooterLink>
</div>
</div>
</footer>
{/* PE org footer */}
<PolicyEngineFooter />
</div>
);
}
Expand Down Expand Up @@ -536,27 +493,4 @@ function QuickLinkCard({ href, title, description }) {
);
}

// Footer Link Component
function FooterLink({ href, children }) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
style={{
color: colors.gray[400],
textDecoration: "none",
fontSize: typography.fontSize.sm,
fontFamily: typography.fontFamily.body,
fontWeight: typography.fontWeight.medium,
transition: "color 0.2s ease",
}}
onMouseEnter={(e) => e.currentTarget.style.color = colors.primary[300]}
onMouseLeave={(e) => e.currentTarget.style.color = colors.gray[400]}
>
{children}
</a>
);
}

export default App;
206 changes: 206 additions & 0 deletions src/components/PolicyEngineFooter.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
import { colors, typography } from '../designTokens';

/**
* PolicyEngine org footer — matches policyengine.org production footer.
* Adapted from keep-your-pay-act Footer.tsx.
*
* TODO: Replace this entire file with `import { Footer } from "@policyengine/ui-kit"`
* once ui-kit 0.4.0 is published to npm. See:
* - https://github.com/PolicyEngine/policyengine-ui-kit/issues/18 (build OOM blocking publish)
* - https://github.com/PolicyEngine/policyengine-ui-kit/issues/16 (changelog/publish pipeline)
*/

const COLORS = {
primary500: colors.primary[500],
primary600: colors.primary[600],
primary800: colors.primary[800],
};

const FONT = typography.fontFamily.primary;

const NAV_LINKS = [
{ href: 'https://policyengine.org/us/team', text: 'About us' },
{ href: 'https://policyengine.org/us/donate', text: 'Donate' },
{ href: 'https://policyengine.org/us/privacy', text: 'Privacy policy' },
{ href: 'https://policyengine.org/us/terms', text: 'Terms and conditions' },
];

const SOCIAL_LINKS = [
{
label: 'Email',
href: 'mailto:hello@policyengine.org',
icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<rect x="2" y="4" width="20" height="16" rx="2" />
<path d="M22 7l-10 7L2 7" />
</svg>
),
},
{
label: 'Twitter',
href: 'https://twitter.com/ThePolicyEngine',
icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M22 4s-.7 2.1-2 3.4c1.6 10-9.4 17.3-18 11.6 2.2.1 4.4-.6 6-2C3 15.5.5 9.6 3 5c2.2 2.6 5.6 4.1 9 4-.9-4.2 4-6.6 7-3.8 1.1 0 3-1.2 3-1.2z" />
</svg>
),
},
{
label: 'Facebook',
href: 'https://www.facebook.com/PolicyEngine',
icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M18 2h-3a5 5 0 00-5 5v3H7v4h3v8h4v-8h3l1-4h-4V7a1 1 0 011-1h3z" />
</svg>
),
},
{
label: 'LinkedIn',
href: 'https://www.linkedin.com/company/thepolicyengine',
icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M16 8a6 6 0 016 6v7h-4v-7a2 2 0 00-4 0v7h-4v-7a6 6 0 016-6z" />
<rect x="2" y="9" width="4" height="12" />
<circle cx="4" cy="4" r="2" />
</svg>
),
},
{
label: 'YouTube',
href: 'https://www.youtube.com/@policyengine',
icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M22.54 6.42a2.78 2.78 0 00-1.94-2C18.88 4 12 4 12 4s-6.88 0-8.6.46a2.78 2.78 0 00-1.94 2A29 29 0 001 11.75a29 29 0 00.46 5.33A2.78 2.78 0 003.4 19.1c1.72.46 8.6.46 8.6.46s6.88 0 8.6-.46a2.78 2.78 0 001.94-2 29 29 0 00.46-5.25 29 29 0 00-.46-5.43z" />
<polygon points="9.75 15.02 15.5 11.75 9.75 8.48 9.75 15.02" />
</svg>
),
},
{
label: 'Instagram',
href: 'https://www.instagram.com/PolicyEngine/',
icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<rect x="2" y="2" width="20" height="20" rx="5" ry="5" />
<path d="M16 11.37A4 4 0 1112.63 8 4 4 0 0116 11.37z" />
<line x1="17.5" y1="6.5" x2="17.51" y2="6.5" />
</svg>
),
},
{
label: 'GitHub',
href: 'https://github.com/PolicyEngine',
icon: (
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 00-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0020 4.77 5.07 5.07 0 0019.91 1S18.73.65 16 2.48a13.38 13.38 0 00-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 005 4.77a5.44 5.44 0 00-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 009 18.13V22" />
</svg>
),
},
];

export default function PolicyEngineFooter() {
return (
<footer
data-testid="pe-footer"
style={{
width: '100%',
padding: '48px 64px',
background: `linear-gradient(to right, ${COLORS.primary800}, ${COLORS.primary600})`,
fontFamily: FONT,
}}
>
<div style={{ maxWidth: '1536px', margin: '0 auto', padding: '0 16px' }}>
<img
src="https://policyengine.org/assets/logos/policyengine/white.svg"
alt="PolicyEngine"
style={{ height: '52px', width: 'auto' }}
/>

<div className="grid grid-cols-1 md:grid-cols-2 mt-8" style={{ gap: '48px' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '24px', alignItems: 'flex-start' }}>
<div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
{NAV_LINKS.map(({ href, text }) => (
<a
key={href}
href={href}
rel="noopener noreferrer"
style={{ color: colors.white, fontSize: '16px', textDecoration: 'none', fontFamily: FONT }}
>
{text}
</a>
))}
</div>

<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
<div style={{ display: 'flex', flexDirection: 'row', alignItems: 'center', gap: '12px' }}>
{SOCIAL_LINKS.map(({ label, href, icon }) => (
<a
key={href}
href={href}
target="_blank"
rel="noopener noreferrer"
aria-label={label}
style={{ color: colors.white }}
>
{icon}
</a>
))}
</div>
<p style={{ fontSize: '12px', color: colors.white, margin: 0, fontFamily: FONT }}>
&copy; {new Date().getFullYear()} PolicyEngine. All rights reserved.
</p>
</div>
</div>

<div style={{ paddingLeft: '24px' }}>
<p style={{ fontWeight: 600, color: colors.white, fontFamily: FONT, fontSize: '24px', margin: '0 0 0 0' }}>
Subscribe to PolicyEngine
</p>
<p style={{ fontSize: '18px', color: colors.white, fontFamily: FONT, margin: '0 0 20px 0' }}>
Get the latest posts delivered right to your inbox.
</p>
<div style={{ width: '80%', display: 'flex', flexDirection: 'column', gap: '8px' }}>
<input
type="email"
placeholder="Enter your email address"
style={{
width: '100%',
height: '40px',
padding: '8px 12px',
borderRadius: '6px',
border: '1px solid #E2E8F0',
fontSize: '14px',
fontFamily: FONT,
boxSizing: 'border-box',
backgroundColor: colors.white,
}}
/>
<a
href="https://policyengine.org/us/subscribe"
rel="noopener noreferrer"
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: '100%',
height: '40px',
backgroundColor: COLORS.primary500,
color: colors.white,
border: 'none',
borderRadius: '6px',
fontSize: '14px',
fontWeight: 600,
fontFamily: FONT,
letterSpacing: '0.05em',
cursor: 'pointer',
textDecoration: 'none',
}}
>
SUBSCRIBE
</a>
</div>
</div>
</div>
</div>
</footer>
);
}
Loading