Skip to content

Commit 41d8fdf

Browse files
feat(newsletter): implement NewsletterSignup component and integrate it across multiple pages; add subscription API endpoint for user sign-ups
1 parent 8dda9a3 commit 41d8fdf

12 files changed

Lines changed: 760 additions & 32 deletions

File tree

‎package-lock.json‎

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"extends browserslist-config-baseline"
2727
],
2828
"dependencies": {
29+
"@cloudflare/next-on-pages": "^1.13.5",
2930
"@eslint/config-array": "^0.18.0",
3031
"@eslint/object-schema": "^2.1.4",
3132
"@headlessui/react": "^2.2.2",
@@ -35,6 +36,7 @@
3536
"@vercel/routing-utils": "^3.1.0",
3637
"browserslist-config-baseline": "^0.4.0",
3738
"chokidar": "^3.5.3",
39+
"country-list": "^2.4.1",
3840
"debug": "^4.3.7",
3941
"framer-motion": "^11.11.1",
4042
"glob": "^11.0.0",
@@ -48,8 +50,7 @@
4850
"react-select": "^5.8.1",
4951
"resend": "^4.6.0",
5052
"rimraf": "^6.0.1",
51-
"uuid": "^11.0.0",
52-
"@cloudflare/next-on-pages": "^1.13.5"
53+
"uuid": "^11.0.0"
5354
},
5455
"devDependencies": {
5556
"@types/node": "^20",

‎public/yourselftoscience.pdf‎

511 Bytes
Binary file not shown.

‎src/app/api/subscribe/route.js‎

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
2+
export async function POST(request) {
3+
const {
4+
email,
5+
firstName,
6+
country,
7+
gender,
8+
yearOfBirth,
9+
healthInterests,
10+
signupSource,
11+
} = await request.json();
12+
13+
if (!email || !email.includes('@')) {
14+
return new Response(JSON.stringify({ error: 'A valid email is required.' }), {
15+
status: 400,
16+
headers: { 'Content-Type': 'application/json' },
17+
});
18+
}
19+
20+
const { MAILCHIMP_API_KEY, MAILCHIMP_AUDIENCE_ID, MAILCHIMP_SERVER_PREFIX } =
21+
process.env;
22+
23+
if (
24+
!MAILCHIMP_API_KEY ||
25+
!MAILCHIMP_AUDIENCE_ID ||
26+
!MAILCHIMP_SERVER_PREFIX
27+
) {
28+
console.error('Mailchimp environment variables are not configured.');
29+
return new Response(
30+
JSON.stringify({
31+
error:
32+
'The subscription service is not available at the moment. Please try again later.',
33+
}),
34+
{
35+
status: 503, // Service Unavailable
36+
headers: { 'Content-Type': 'application/json' },
37+
}
38+
);
39+
}
40+
41+
const merge_fields = {};
42+
if (firstName) merge_fields.FNAME = firstName;
43+
// For custom fields like Gender, Year of Birth, and Interests,
44+
// you must define corresponding Merge Tags in your Mailchimp Audience settings.
45+
// e.g., GENDER, YOB, INTERESTS
46+
if (gender) merge_fields.GENDER = gender;
47+
if (yearOfBirth) merge_fields.YOB = yearOfBirth;
48+
if (healthInterests) merge_fields.INTERESTS = healthInterests;
49+
if (country && country.length > 0) merge_fields.COUNTRY = country.join(', ');
50+
51+
const data = {
52+
email_address: email,
53+
status: 'subscribed',
54+
merge_fields,
55+
};
56+
57+
if (signupSource) {
58+
data.tags = [signupSource];
59+
}
60+
61+
try {
62+
const response = await fetch(
63+
`https://${MAILCHIMP_SERVER_PREFIX}.api.mailchimp.com/3.0/lists/${MAILCHIMP_AUDIENCE_ID}/members`,
64+
{
65+
body: JSON.stringify(data),
66+
headers: {
67+
Authorization: `apikey ${MAILCHIMP_API_KEY}`,
68+
'Content-Type': 'application/json',
69+
},
70+
method: 'POST',
71+
}
72+
);
73+
74+
const responseData = await response.json();
75+
76+
if (!response.ok) {
77+
console.error('Mailchimp API error:', responseData);
78+
// Provide a user-friendly error from Mailchimp if available
79+
const errorMessage =
80+
responseData.title === 'Member Exists'
81+
? 'This email is already subscribed.'
82+
: responseData.detail || 'An error occurred during subscription.';
83+
84+
return new Response(JSON.stringify({ error: errorMessage }), {
85+
status: response.status,
86+
headers: { 'Content-Type': 'application/json' },
87+
});
88+
}
89+
90+
return new Response(JSON.stringify({ message: 'Successfully subscribed!' }), {
91+
status: 201,
92+
headers: { 'Content-Type': 'application/json' },
93+
});
94+
} catch (error) {
95+
console.error('Internal server error:', error);
96+
return new Response(
97+
JSON.stringify({
98+
error: 'An internal error occurred. Please try again later.',
99+
}),
100+
{
101+
status: 500,
102+
headers: { 'Content-Type': 'application/json' },
103+
}
104+
);
105+
}
106+
}
Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,28 @@
11
'use client';
2+
import React from 'react';
3+
import { Suspense } from 'react';
4+
import ClinicalTrialsLoader from './ClinicalTrialsLoader';
5+
import ClinicalTrialsSkeleton from './ClinicalTrialsSkeleton';
6+
import NewsletterSignup from '../../components/NewsletterSignup';
27

3-
import dynamic from 'next/dynamic';
4-
import ClinicalTrialsSkeleton from './ClinicalTrialsSkeleton.js';
5-
6-
const DynamicLoader = dynamic(
7-
() => import('./ClinicalTrialsLoader'),
8-
{
9-
ssr: false,
10-
// The skeleton is used as a fallback while the client-side component is loading.
11-
loading: () => <ClinicalTrialsSkeleton />,
12-
}
13-
);
8+
// Since this is a client component, we can use hooks and event handlers.
9+
// All logic related to fetching data and interactivity for the clinical trials
10+
// page will be handled here and in the components it loads.
1411

1512
export default function ClinicalTrialsPageClient() {
16-
return <DynamicLoader />;
13+
return (
14+
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-12">
15+
<div className="text-center">
16+
<h1 className="text-4xl font-bold mb-4">Clinical Trials</h1>
17+
<p className="text-lg text-gray-600 mb-8 max-w-3xl mx-auto">
18+
Find opportunities to participate in clinical studies. Your involvement
19+
can help drive medical advancements and new treatments.
20+
</p>
21+
<NewsletterSignup />
22+
</div>
23+
<Suspense fallback={<ClinicalTrialsSkeleton />}>
24+
<ClinicalTrialsLoader />
25+
</Suspense>
26+
</div>
27+
);
1728
}

‎src/app/get-involved/page.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
import { Suspense } from 'react';
22
import GetInvolvedClientPage from './GetInvolvedClientPage';
3+
import NewsletterSignup from '../../components/NewsletterSignup';
34

45
export const metadata = {
5-
title: 'Get Involved - Yourself To Science',
6+
title: 'Get Involved | Yourself To Science',
67
description: 'Help build the future of citizen science. Suggest a new service, contact us, join the community discussion, or contribute to the code.',
78
};
89

910
export default function GetInvolvedPage() {
1011
return (
1112
<Suspense>
1213
<GetInvolvedClientPage />
14+
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-12 flex justify-center">
15+
<NewsletterSignup />
16+
</div>
1317
</Suspense>
1418
);
1519
}
Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,30 @@
11
'use client';
2+
import React from 'react';
3+
import { Suspense } from 'react';
4+
import OrganBodyTissueDonationLoader from './OrganBodyTissueDonationLoader';
5+
import OrganBodyTissueDonationSkeleton from './OrganBodyTissueDonationSkeleton';
6+
import NewsletterSignup from '../../components/NewsletterSignup';
27

3-
import dynamic from 'next/dynamic';
4-
import OrganBodyTissueDonationSkeleton from './OrganBodyTissueDonationSkeleton.js';
5-
6-
const DynamicLoader = dynamic(
7-
() => import('./OrganBodyTissueDonationLoader'),
8-
{
9-
ssr: false,
10-
// The skeleton is used as a fallback while the client-side component is loading.
11-
loading: () => <OrganBodyTissueDonationSkeleton />,
12-
}
13-
);
8+
// Client component for the Organ, Body & Tissue Donation page.
9+
// This component will handle the interactive parts of the page, including
10+
// loading and displaying the resources.
1411

1512
export default function OrganBodyTissueDonationPageClient() {
16-
return <DynamicLoader />;
13+
return (
14+
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-12">
15+
<div className="text-center">
16+
<h1 className="text-4xl font-bold mb-4">
17+
Organ, Body & Tissue Donation
18+
</h1>
19+
<p className="text-lg text-gray-600 mb-8 max-w-3xl mx-auto">
20+
Discover ways to make a lasting contribution to medical research and
21+
education through donation.
22+
</p>
23+
<NewsletterSignup />
24+
</div>
25+
<Suspense fallback={<OrganBodyTissueDonationSkeleton />}>
26+
<OrganBodyTissueDonationLoader />
27+
</Suspense>
28+
</div>
29+
);
1730
}

‎src/app/page.js‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { FaHeart, FaDollarSign, FaTimes, FaFilter, FaDownload, FaSlidersH, FaUnd
1515
import Header from '@/components/Header';
1616
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
1717
import Link from 'next/link';
18+
import NewsletterSignup from '../components/NewsletterSignup';
1819

1920
// Dynamically import ResourceGrid
2021
const ResourceGrid = dynamic(() => import('@/components/ResourceGrid'), {
@@ -671,6 +672,9 @@ function HomePageContent({ scrollY }) {
671672
Browse our curated resources to find ways to share your data, genome, body samples, and more.
672673
</p>
673674

675+
{/* Newsletter Signup */}
676+
<NewsletterSignup />
677+
674678
{/* Main Layout Grid */}
675679
<div className="grid grid-cols-layout lg:grid-cols-lg-layout gap-6">
676680
{/* Sidebar */}
@@ -848,8 +852,8 @@ function HomePageContent({ scrollY }) {
848852
countryOptions={countryOptions}
849853
dataTypeOptions={dataTypeOptions}
850854
paymentTypes={PAYMENT_TYPES}
851-
handleCheckboxChange={handleCheckboxChange}
852-
handlePaymentCheckboxChange={handlePaymentCheckboxChange}
855+
handleCheckboxChange={(filterKey, value) => handleFilterChange(filterKey, value)}
856+
handlePaymentCheckboxChange={(option) => handleFilterChange('compensationType', option.value)}
853857
handleResetFilters={handleResetFilters}
854858
renderFilterContent={renderFilterContent} // Pass the existing render function
855859
/>

0 commit comments

Comments
 (0)