11import { useRef , useState } from 'react' ;
2- import Link from 'next/link' ;
32import useNewsletterStats from '@hooks/useNewsletterStats' ;
43import posthog from 'posthog-js' ;
54import { toast } from 'sonner' ;
65
76import Button from '@components/Button' ;
87import { Input } from '@ui/input' ;
98import { trpc } from '@utils/trpc' ;
9+ import type { SubscribeMutationResponse } from '@server/routers/mailingList' ;
1010
1111type SubscriptionFormProps = {
1212 tags ?: string [ ] ;
@@ -21,10 +21,15 @@ const SubscriptionForm: React.FC<SubscriptionFormProps> = ({
2121} ) => {
2222 const [ getHoneypottedNerd , setGetHoneypottedNerd ] = useState < boolean > ( false ) ;
2323 const [ alreadySubscribed , setAlreadySubscribed ] = useState < boolean > ( false ) ;
24+ const [ validationError , setValidationError ] = useState < string | null > ( null ) ;
25+
2426 const addSubscriberMutation = trpc . mailingList . subscribe . useMutation ( {
25- onSuccess : ( data ) => {
27+ onSuccess : ( data : SubscribeMutationResponse ) => {
28+ // Clear any validation errors on success
29+ setValidationError ( null ) ;
30+
2631 // Check if this is the "already subscribed" case
27- if ( data ? .error ?. name === 'already_subscribed' ) {
32+ if ( data . error ?. name === 'already_subscribed' ) {
2833 const email = emailRef . current ?. value ;
2934 const firstName = firstNameRef . current ?. value ;
3035
@@ -75,17 +80,34 @@ const SubscriptionForm: React.FC<SubscriptionFormProps> = ({
7580 const email = emailRef . current ?. value ;
7681 const firstName = firstNameRef . current ?. value ;
7782
83+ // Handle validation errors specifically
84+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
85+ const errorData = error as any ;
86+ if ( errorData ?. data ?. zodError ?. fieldErrors ) {
87+ const fieldErrors = errorData . data . zodError . fieldErrors ;
88+ if ( fieldErrors . email ) {
89+ setValidationError ( 'Please enter a valid email address (e.g., person@gmail.com)' ) ;
90+ } else if ( fieldErrors . firstName ) {
91+ setValidationError ( 'Please enter your first name' ) ;
92+ } else {
93+ setValidationError ( 'Please check your input and try again' ) ;
94+ }
95+ } else if ( errorData ?. message ) {
96+ setValidationError ( errorData . message ) ;
97+ } else {
98+ setValidationError ( 'Something went wrong. Please try again or contact support.' ) ;
99+ }
100+
78101 toast . error ( 'Subscription failed' , {
79- description :
80- 'Please try again or contact hello@mikebifulco.com for help.' ,
102+ description : 'Please check your input and try again.' ,
81103 duration : 5000 ,
82104 } ) ;
83105
84106 posthog . capture ( 'newsletter/subscribe_error' , {
85107 source,
86108 email,
87109 firstName,
88- error,
110+ error : errorData ?. message || 'Unknown error' ,
89111 } ) ;
90112 } ,
91113 } ) ;
@@ -97,9 +119,12 @@ const SubscriptionForm: React.FC<SubscriptionFormProps> = ({
97119 const firstNameRef = useRef < HTMLInputElement > ( null ) ;
98120 const honeypotRef = useRef < HTMLInputElement > ( null ) ;
99121
100- const handleSubmission = async ( e : React . FormEvent < HTMLFormElement > ) => {
122+ const handleSubmission = ( e : React . FormEvent < HTMLFormElement > ) => {
101123 e . preventDefault ( ) ;
102124
125+ // Clear any previous validation errors
126+ setValidationError ( null ) ;
127+
103128 const email = emailRef . current ?. value ;
104129 const firstName = firstNameRef . current ?. value ;
105130 const honeypot = honeypotRef . current ?. value ;
@@ -110,37 +135,85 @@ const SubscriptionForm: React.FC<SubscriptionFormProps> = ({
110135 }
111136
112137 if ( ! email ) {
138+ setValidationError ( 'Please enter your email address' ) ;
139+ return ;
140+ }
141+
142+ // Basic email validation on frontend
143+ const emailRegex = / ^ [ ^ \s @ ] + @ [ ^ \s @ ] + \. [ ^ \s @ ] + $ / ;
144+ if ( ! emailRegex . test ( email ) ) {
145+ setValidationError ( 'Please enter a valid email address (e.g., person@gmail.com)' ) ;
113146 return ;
114147 }
115148
116149 posthog . identify ( email , {
117150 firstName,
118151 } ) ;
119152
120- await addSubscriberMutation . mutateAsync ( {
153+ addSubscriberMutation . mutate ( {
121154 email,
122155 firstName,
123156 } ) ;
124157 } ;
125158
126- if ( addSubscriberMutation . error ) {
159+ // Show validation error inline with the form
160+ if ( validationError ) {
127161 return (
128162 < div className = "flex flex-col gap-2" >
129- < p className = "text-xl font-semibold text-inherit" >
130- { addSubscriberMutation . error . message }
131- </ p >
132- < p className = "text-inherit" >
133- If you continue to have issues, please email{ ' ' }
134- < Link
135- className = "text-pink-600 hover:underline"
136- href = "mailto:hello@mikebifulco.com"
137- rel = "noopener noreferrer"
138- target = "_blank"
139- >
140- hello@mikebifulco.com
141- </ Link > { ' ' }
142- and I'll help get this sorted.
143- </ p >
163+ < div className = "rounded-md border border-red-200 bg-red-50 p-4" >
164+ < p className = "text-sm font-medium text-red-800" >
165+ { validationError }
166+ </ p >
167+ </ div >
168+ < form ref = { formRef } className = "w-full" onSubmit = { handleSubmission } >
169+ < fieldset disabled = { addSubscriberMutation . isPending } >
170+ < div data-style = "clean" >
171+ < div
172+ className = "seva-fields formkit-fields grid w-full items-center rounded-sm"
173+ data-element = "fields"
174+ data-stacked = "false"
175+ >
176+ < Input
177+ type = "text"
178+ aria-label = "Last Name"
179+ ref = { honeypotRef }
180+ style = { { display : 'none' } }
181+ name = "fields[last_name]"
182+ />
183+ < Input
184+ className = "h-10 w-full grow rounded-t rounded-b-none border border-b-0 border-solid border-pink-600 bg-white px-[2ch] py-[1ch] font-normal text-gray-950"
185+ aria-label = "First Name"
186+ name = "fields[first_name]"
187+ required
188+ placeholder = "First Name"
189+ type = "text"
190+ ref = { firstNameRef }
191+ />
192+ < Input
193+ className = "h-10 w-full grow rounded-b-none rounded-none border border-b-0 border-solid border-pink-600 bg-white px-[2ch] py-[1ch] font-normal text-gray-950"
194+ name = "email_address"
195+ aria-label = "Email Address"
196+ placeholder = "Email Address"
197+ required
198+ type = "email"
199+ ref = { emailRef }
200+ />
201+ < Button
202+ type = "submit"
203+ data-element = "submit"
204+ className = "formkit-submit formkit-submit padding-[1ch 2ch] h-10 grow rounded-t-none rounded-b font-normal"
205+ >
206+ < div className = "formkit-spinner" >
207+ < div > </ div >
208+ < div > </ div >
209+ < div > </ div >
210+ </ div >
211+ < span > 💌 { buttonText } </ span >
212+ </ Button >
213+ </ div >
214+ </ div >
215+ </ fieldset >
216+ </ form >
144217 </ div >
145218 ) ;
146219 }
0 commit comments