Skip to content

Comments

[UXIT-3791][filecoin.io V3] Hook up NewsletterForm [skip percy]#2181

Open
barbaraperic wants to merge 7 commits intomainfrom
bp/newsletter-form
Open

[UXIT-3791][filecoin.io V3] Hook up NewsletterForm [skip percy]#2181
barbaraperic wants to merge 7 commits intomainfrom
bp/newsletter-form

Conversation

@barbaraperic
Copy link
Collaborator

@barbaraperic barbaraperic commented Feb 19, 2026

📝 Description

This PR adds a functional newsletter subscription form that integrates with Mailchimp for the Filecoin site.

  • Type: New feature

🛠️ Key Changes

  • NewsletterForm.tsx - Refactored to use ControlledForm and ControlledFormInput for form handling
  • useNewsletterForm.ts - New custom hook that manages form state, validation, and submission logic with notification dialogs for success/error feedback
  • api/subscribe/route.ts - New API route that handles Mailchimp subscription requests with robust error handling for inconsistent API responses

Notes

  • The Mailchimp embedded form endpoint (post-json) can return HTML instead of JSON in certain cases (e.g., already subscribed emails). The API route includes error handling to gracefully handle these responses.
  • Requires MAILCHIMP_U and MAILCHIMP_LIST_ID environment variables to be set.

🧪 How to Test

  1. Enter a valid email in the newsletter form
  2. Verify success notification appears
  3. Check if you get an email confirmation
  4. Test with an already-subscribed email to verify error handling
  5. Test if ZH-CN translation works for the dialog messages

📸 Screenshots

Screenshot 2026-02-20 at 16 15 58 Screenshot 2026-02-20 at 16 16 20

…olledFormInput for improved form handling; update success message in useNewsletterForm hook; enhance Mailchimp subscription API error handling and response parsing.
@barbaraperic barbaraperic requested a review from Copilot February 19, 2026 16:05
@barbaraperic barbaraperic self-assigned this Feb 19, 2026
@vercel
Copy link

vercel bot commented Feb 19, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
filecoin-site Ready Ready Preview, Comment Feb 20, 2026 4:25pm
3 Skipped Deployments
Project Deployment Actions Updated (UTC)
ffdweb-site Skipped Skipped Feb 20, 2026 4:25pm
filecoin-foundation-site Skipped Skipped Feb 20, 2026 4:25pm
filecoin-foundation-uxit Skipped Skipped Feb 20, 2026 4:25pm

Request Review

@notion-workspace
Copy link

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a functional newsletter subscription flow to the Filecoin site by wiring the footer newsletter form to a new /api/subscribe endpoint that proxies Mailchimp’s embedded JSONP subscription endpoint, and centralizing client-side form handling in a custom hook.

Changes:

  • Added a Next.js route handler to submit newsletter signups to Mailchimp and normalize responses to JSON.
  • Introduced useNewsletterForm to own form validation + submission + notification dialog behavior.
  • Refactored NewsletterForm to use shared controlled form components and the new hook.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
apps/filecoin-site/src/app/api/subscribe/route.ts New API route that calls Mailchimp subscribe endpoint and returns normalized JSON responses.
apps/filecoin-site/src/app/_hooks/useNewsletterForm.ts New hook for form schema, submission, and notification dialog interactions.
apps/filecoin-site/src/app/_components/Footer/NewsletterForm.tsx Refactor footer newsletter UI to use ControlledForm + useNewsletterForm + NotificationDialog.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


let data: { result?: string; msg?: string }
try {
const json = text.replace(/^[^(]+\(/, '').replace(/\)$/, '')
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSONP stripping logic only removes the final ) and will fail to parse common JSONP formats that end with ); or have trailing whitespace/newlines. Make the extraction more robust (e.g., strip an optional trailing semicolon/whitespace and/or match the expected callback wrapper) so successful Mailchimp responses don't get treated as parse failures.

Suggested change
const json = text.replace(/^[^(]+\(/, '').replace(/\)$/, '')
const callbackRegex = new RegExp(
`^\\s*${MAILCHIMP_JSONP_CALLBACK}\\s*\\(([\\s\\S]*?)\\)\\s*;?\\s*$`,
)
const match = text.match(callbackRegex)
if (!match) {
throw new Error('Invalid Mailchimp JSONP format')
}
const json = match[1]

Copilot uses AI. Check for mistakes.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what this is doing

@vercel vercel bot temporarily deployed to Preview – filecoin-foundation-site February 20, 2026 11:44 Inactive
@vercel vercel bot temporarily deployed to Preview – ffdweb-site February 20, 2026 11:44 Inactive
@vercel vercel bot temporarily deployed to Preview – filecoin-foundation-uxit February 20, 2026 11:44 Inactive
@github-actions github-actions bot added size/L and removed size/L labels Feb 20, 2026
…ironment variables, improving error handling and user feedback in the subscription process, and updating translations for better user experience.
@vercel vercel bot temporarily deployed to Preview – filecoin-foundation-uxit February 20, 2026 15:01 Inactive
@vercel vercel bot temporarily deployed to Preview – filecoin-foundation-site February 20, 2026 15:01 Inactive
@vercel vercel bot temporarily deployed to Preview – ffdweb-site February 20, 2026 15:01 Inactive
@barbaraperic barbaraperic requested a review from Copilot February 20, 2026 15:01
@github-actions github-actions bot added size/L and removed size/L labels Feb 20, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +47 to +56
if (data.result === 'error') {
return Response.json(
{ ok: false, ...(data.msg && { message: data.msg }) },
{ status: 400 },
)
}

const isAlreadySubscribed = data.msg
?.toLowerCase()
.includes('already subscribed')
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "already subscribed" check occurs after the error check, but "already subscribed" responses from Mailchimp likely have result='error'. This means already-subscribed emails will be returned as errors (status 400) instead of successes with isAlreadySubscribed=true. The logic should check for "already subscribed" within the error handling block, treating it as a special case that returns ok=true.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

@barbaraperic barbaraperic Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It actually has result="success"


Mailchimp response: {
  result: 'success',
  msg: "You're already subscribed, your profile has been updated. Thank you!",
  type: 'custom',
  webEngagementCookieValue: null
}

Comment on lines 5 to 59
export async function POST(request: NextRequest) {
let body: { email?: unknown }

try {
body = await request.json()
} catch {
return Response.json({ ok: false }, { status: 400 })
}

const { email } = body

if (!email || typeof email !== 'string') {
return Response.json({ ok: false }, { status: 400 })
}

const baseUrl = getMailchimpSubscribeUrl()
if (!baseUrl) {
return Response.json({ ok: false }, { status: 503 })
}

const response = await fetch(
`${baseUrl}&EMAIL=${encodeURIComponent(email)}&c=${MAILCHIMP_JSONP_CALLBACK}`,
{ method: 'GET' },
)

if (!response.ok) {
return Response.json({ ok: false }, { status: 502 })
}

const text = await response.text()

let data: { result?: string; msg?: string }
try {
const json = text.replace(/^[^(]+\(/, '').replace(/\)$/, '')
data = JSON.parse(json)
} catch {
console.error(
'Failed to parse Mailchimp response as JSON from Mailchimp API',
)
return Response.json({ ok: false }, { status: 502 })
}

if (data.result === 'error') {
return Response.json(
{ ok: false, ...(data.msg && { message: data.msg }) },
{ status: 400 },
)
}

const isAlreadySubscribed = data.msg
?.toLowerCase()
.includes('already subscribed')

return Response.json({ ok: true, isAlreadySubscribed })
}
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API route lacks rate limiting, which could allow abuse of the newsletter subscription endpoint. Consider implementing rate limiting middleware or using a service like Vercel's edge config to prevent spam submissions and protect the Mailchimp API quota.

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mailchimp already has a limit rate

Mailchimp raw response: handle_response({"result":"error","msg":"Too many subscribe attempts for this email address. Please try again in about 5 minutes. (#6592)"})handle_response({"result":"error","msg":"There are errors below","errors":"handleRecordAndEmailValidation","type":"validation_error"})

Comment on lines +25 to +29
const response = await fetch(
`${baseUrl}&EMAIL=${encodeURIComponent(email)}&c=${MAILCHIMP_JSONP_CALLBACK}`,
{ method: 'GET' },
)

Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fetch call to Mailchimp lacks a timeout configuration, which could cause the API route to hang indefinitely if Mailchimp's servers are unresponsive. Consider adding an AbortSignal with a reasonable timeout (e.g., 10 seconds) to prevent long-running requests.

Suggested change
const response = await fetch(
`${baseUrl}&EMAIL=${encodeURIComponent(email)}&c=${MAILCHIMP_JSONP_CALLBACK}`,
{ method: 'GET' },
)
const controller = new AbortController()
const timeoutId = setTimeout(() => {
controller.abort()
}, 10_000)
let response: Response
try {
response = await fetch(
`${baseUrl}&EMAIL=${encodeURIComponent(email)}&c=${MAILCHIMP_JSONP_CALLBACK}`,
{ method: 'GET', signal: controller.signal },
)
} catch (error) {
if ((error as Error).name === 'AbortError') {
return Response.json({ ok: false }, { status: 504 })
}
throw error
} finally {
clearTimeout(timeoutId)
}

Copilot uses AI. Check for mistakes.
Comment on lines +47 to +50
if (data.result === 'error') {
return Response.json(
{ ok: false, ...(data.msg && { message: data.msg }) },
{ status: 400 },
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Mailchimp error message is passed directly to the client without sanitization. While Mailchimp is likely to return safe content, consider sanitizing or limiting the message content to prevent potential XSS if Mailchimp's response were to be compromised or contain unexpected HTML/script content.

Copilot uses AI. Check for mistakes.
@vercel vercel bot temporarily deployed to Preview – filecoin-foundation-uxit February 20, 2026 15:19 Inactive
@vercel vercel bot temporarily deployed to Preview – filecoin-site February 20, 2026 15:19 Inactive
@vercel vercel bot temporarily deployed to Preview – ffdweb-site February 20, 2026 15:19 Inactive
@github-actions github-actions bot added size/L and removed size/L labels Feb 20, 2026
…uccessful subscription and enhancing error logging for Mailchimp API response parsing.
@vercel vercel bot temporarily deployed to Preview – filecoin-foundation-site February 20, 2026 16:21 Inactive
@vercel vercel bot temporarily deployed to Preview – filecoin-foundation-uxit February 20, 2026 16:21 Inactive
@vercel vercel bot temporarily deployed to Preview – ffdweb-site February 20, 2026 16:21 Inactive
@barbaraperic barbaraperic marked this pull request as ready for review February 20, 2026 16:21
@github-actions github-actions bot added size/L and removed size/L labels Feb 20, 2026
Copy link
Collaborator

@CharlyMartin CharlyMartin left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pasting here a suggestion for the API endpoint. Not sure if it works, let me know :)

import { type NextRequest } from 'next/server'

import { z } from 'zod'

const MAILCHIMP_JSONP_CALLBACK = 'handle_response'

const RequestSchema = z.object({
  email: z.email(),
})

export async function POST(request: NextRequest) {
  try {
    const body = await request.json()
    const { email } = RequestSchema.parse(body)

    const baseUrl = getMailchimpSubscribeUrl()
    // This might be encapsulated in getMailchimpSubscribeUrl too
    const url = new URL(baseUrl)
    url.searchParams.set('EMAIL', email)
    url.searchParams.set('c', MAILCHIMP_JSONP_CALLBACK)

    const response = await fetch(url, { method: 'GET' })
    if (!response.ok) {
      return Response.json({ ok: false }, { status: 502 })
    }

    const text = await response.text()
    // handle response here.
  } catch (error) {
    return Response.json(
      { ok: false },
      { status: 400, statusText: String(error) },
    )
  }
}

function getMailchimpSubscribeUrl() {
  const u = process.env.MAILCHIMP_U
  const id = process.env.MAILCHIMP_LIST_ID
  return `https://protocol.us16.list-manage.com/subscribe/post-json?u=${u}&id=${id}`
}

body: JSON.stringify({ email: values.email }),
})

const body = await response.json().catch(() => ({}))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This catch clause is not doing anything, right?

Suggested change
const body = await response.json().catch(() => ({}))
const body = await response.json()

const MAILCHIMP_JSONP_CALLBACK = 'handle_response'

export async function POST(request: NextRequest) {
let body: { email?: unknown }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this initialized here? What's the purpose of it?

Comment on lines +10 to +20
try {
body = await request.json()
} catch {
return Response.json({ ok: false }, { status: 400 })
}

const { email } = body

if (!email || typeof email !== 'string') {
return Response.json({ ok: false }, { status: 400 })
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we use Zod here?

Comment on lines +27 to +30
const response = await fetch(
`${baseUrl}&EMAIL=${encodeURIComponent(email)}&c=${MAILCHIMP_JSONP_CALLBACK}`,
{ method: 'GET' },
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would something like this work? Just for readability.

const url = new URL(baseUrl)
url.searchParams.set('EMAIL', email)
url.searchParams.set('c', MAILCHIMP_JSONP_CALLBACK)

const response = await fetch(url, { method: 'GET' })

return Response.json({ ok: false }, { status: 502 })
}

const text = await response.text()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably be wrapped in a try/catch as well.


let data: { result?: string; msg?: string }
try {
const json = text.replace(/^[^(]+\(/, '').replace(/\)$/, '')
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what this is doing

"description": "Enterprise-grade hot storage for AI & data lakes.",
"labels": ["Drag and drop", "S3-compatible"],
"keyFeatures": ["S3-compatible API", "Client-side encryption", "Access control"]
"keyFeatures": [
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't belong to this PR

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants