diff --git a/app/package-lock.json b/app/package-lock.json index 22ce382a..c152b754 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -18,6 +18,7 @@ "@tanstack/react-query-devtools": "^5.83.0", "dayjs": "^1.11.13", "framer-motion": "^12.23.24", + "jsonp": "^0.2.1", "react": "^19.1.0", "react-dom": "^19.1.0", "react-plotly.js": "^2.6.0", @@ -34,6 +35,7 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", + "@types/jsonp": "^0.2.3", "@types/node": "^22.15.11", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", @@ -2735,6 +2737,13 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "license": "MIT" }, + "node_modules/@types/jsonp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@types/jsonp/-/jsonp-0.2.3.tgz", + "integrity": "sha512-+bbowFC2n6TkpbQ369/tqL8pRPF4JRfRNf7Z6cpnAlLlEdy6YxG4HYZE7Qkeg2otcDbwNPIuSMux+P3pE4viFA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/less": { "version": "3.0.8", "resolved": "https://registry.npmjs.org/@types/less/-/less-3.0.8.tgz", @@ -7921,6 +7930,29 @@ "node": ">=6" } }, + "node_modules/jsonp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/jsonp/-/jsonp-0.2.1.tgz", + "integrity": "sha512-pfog5gdDxPdV4eP7Kg87M8/bHgshlZ5pybl+yKxAnCZ5O7lCIn7Ixydj03wOlnDQesky2BPyA91SQ+5Y/mNwzw==", + "dependencies": { + "debug": "^2.1.3" + } + }, + "node_modules/jsonp/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/jsonp/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, "node_modules/jsx-ast-utils": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", diff --git a/app/package.json b/app/package.json index 7eafef8b..de24d440 100644 --- a/app/package.json +++ b/app/package.json @@ -33,6 +33,7 @@ "@tanstack/react-query-devtools": "^5.83.0", "dayjs": "^1.11.13", "framer-motion": "^12.23.24", + "jsonp": "^0.2.1", "react": "^19.1.0", "react-dom": "^19.1.0", "react-plotly.js": "^2.6.0", @@ -49,6 +50,7 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", + "@types/jsonp": "^0.2.3", "@types/node": "^22.15.11", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", @@ -76,4 +78,4 @@ "vite-tsconfig-paths": "^5.1.4", "vitest": "^3.2.3" } -} \ No newline at end of file +} diff --git a/app/src/components/Footer.tsx b/app/src/components/Footer.tsx index 9959b07b..dbd9ee5d 100644 --- a/app/src/components/Footer.tsx +++ b/app/src/components/Footer.tsx @@ -7,18 +7,21 @@ import { IconBrandYoutube, } from '@tabler/icons-react'; import { Anchor, Box, Container, Group, SimpleGrid, Stack, Text } from '@mantine/core'; +import type { CountryId } from '@/api/report'; import PolicyEngineLogo from '@/assets/policyengine-logo.svg'; import FooterSubscribe from '@/components/FooterSubscribe'; import { colors, spacing, typography } from '@/designTokens'; +import { useCurrentCountry } from '@/hooks/useCurrentCountry'; -const CONTACT_LINKS = { +const getContactLinks = (countryId: CountryId) => ({ email: 'mailto:hello@policyengine.org', - about: '#', - donate: '#', - privacy: '#', - terms: '#', - developerTools: '#', -}; + about: `/${countryId}/team`, + donate: `/${countryId}/donate`, + privacy: `/${countryId}/privacy`, + terms: `/${countryId}/terms`, + // TODO: Add developer-tools page once it's built out + // developerTools: `/${countryId}/developer-tools`, +}); const SOCIAL_LINKS = [ { icon: IconBrandTwitter, href: 'https://twitter.com/ThePolicyEngine' }, @@ -30,6 +33,8 @@ const SOCIAL_LINKS = [ ]; export default function Footer() { + const countryId = useCurrentCountry(); + const CONTACT_LINKS = getContactLinks(countryId); return ( - Terms and conditions + Terms and Conditions + {/* TODO: Uncomment when developer-tools page is built Developer tools + */} diff --git a/app/src/components/FooterSubscribe.tsx b/app/src/components/FooterSubscribe.tsx index d8b84e17..ac8a8615 100644 --- a/app/src/components/FooterSubscribe.tsx +++ b/app/src/components/FooterSubscribe.tsx @@ -1,13 +1,41 @@ import { useState } from 'react'; import { Button, Stack, Text, TextInput } from '@mantine/core'; import { colors, typography } from '@/designTokens'; +import { submitToMailchimp } from '@/utils/mailchimpSubscription'; export default function FooterSubscribe() { const [email, setEmail] = useState(''); + const [status, setStatus] = useState<'idle' | 'loading' | 'success' | 'error'>('idle'); + const [message, setMessage] = useState(''); - const handleSubscribe = () => { - // Functionality to handle subscription to be added - // alert(`Subscribe button clicked with email: ${email}`); + const handleSubscribe = async () => { + if (!email) { + setStatus('error'); + setMessage('Please enter a valid email address.'); + return; + } + + setStatus('loading'); + setMessage(''); + + try { + const result = await submitToMailchimp(email); + if (result.isSuccessful) { + setStatus('success'); + setMessage(result.message); + setEmail(''); + } else { + setStatus('error'); + setMessage(result.message); + } + } catch (error) { + setStatus('error'); + setMessage( + error instanceof Error + ? error.message + : 'There was an issue processing your subscription; please try again later.' + ); + } }; return ( @@ -28,15 +56,28 @@ export default function FooterSubscribe() { styles={{ input: { backgroundColor: colors.white, flex: 1 }, }} + disabled={status === 'loading'} /> + {message && ( + + {message} + + )} ); diff --git a/app/src/components/StaticLayout.tsx b/app/src/components/StaticLayout.tsx index 790ce843..07c5f8c2 100644 --- a/app/src/components/StaticLayout.tsx +++ b/app/src/components/StaticLayout.tsx @@ -1,4 +1,5 @@ import { Outlet } from 'react-router-dom'; +import Footer from '@/components/Footer'; import HeaderNavigation from '@/components/shared/HomeHeader'; export default function StaticLayout() { @@ -6,6 +7,7 @@ export default function StaticLayout() { <> +