1+ import { useForm } from "@tanstack/react-form" ;
2+ import { useMutation } from "@tanstack/react-query" ;
13import { Link , useRouterState } from "@tanstack/react-router" ;
2- import { ExternalLinkIcon , MailIcon } from "lucide-react" ;
4+ import { ArrowRightIcon , ExternalLinkIcon , MailIcon } from "lucide-react" ;
35import { useState } from "react" ;
46
7+ import { Checkbox } from "@hypr/ui/components/ui/checkbox" ;
8+ import {
9+ Popover ,
10+ PopoverContent ,
11+ PopoverTrigger ,
12+ } from "@hypr/ui/components/ui/popover" ;
13+ import { cn } from "@hypr/utils" ;
14+
515import { Image } from "@/components/image" ;
16+ import { addContact } from "@/functions/loops" ;
617
718function getNextRandomIndex ( length : number , prevIndex : number ) : number {
819 if ( length <= 1 ) return 0 ;
@@ -57,6 +68,51 @@ export function Footer() {
5768}
5869
5970function BrandSection ( { currentYear } : { currentYear : number } ) {
71+ const [ popoverOpen , setPopoverOpen ] = useState ( false ) ;
72+ const [ email , setEmail ] = useState ( "" ) ;
73+ const [ subscriptions , setSubscriptions ] = useState ( {
74+ releaseNotesStable : false ,
75+ releaseNotesBeta : false ,
76+ newsletter : false ,
77+ } ) ;
78+
79+ const mutation = useMutation ( {
80+ mutationFn : async ( ) => {
81+ await addContact ( {
82+ data : {
83+ email,
84+ userGroup : "Subscriber" ,
85+ source : "FOOTER" ,
86+ releaseNotesStable : subscriptions . releaseNotesStable ,
87+ releaseNotesBeta : subscriptions . releaseNotesBeta ,
88+ newsletter : subscriptions . newsletter ,
89+ } ,
90+ } ) ;
91+ } ,
92+ onSuccess : ( ) => {
93+ setPopoverOpen ( false ) ;
94+ setEmail ( "" ) ;
95+ setSubscriptions ( {
96+ releaseNotesStable : false ,
97+ releaseNotesBeta : false ,
98+ newsletter : false ,
99+ } ) ;
100+ } ,
101+ } ) ;
102+
103+ const form = useForm ( {
104+ defaultValues : { email : "" } ,
105+ onSubmit : async ( { value } ) => {
106+ setEmail ( value . email ) ;
107+ setPopoverOpen ( true ) ;
108+ } ,
109+ } ) ;
110+
111+ const hasSelection =
112+ subscriptions . releaseNotesStable ||
113+ subscriptions . releaseNotesBeta ||
114+ subscriptions . newsletter ;
115+
60116 return (
61117 < div className = "lg:flex-1" >
62118 < Link to = "/" className = "inline-block mb-4" >
@@ -76,6 +132,155 @@ function BrandSection({ currentYear }: { currentYear: number }) {
76132 Get started
77133 </ Link >
78134 </ p >
135+
136+ < div className = "mb-4" >
137+ < Popover open = { popoverOpen } onOpenChange = { setPopoverOpen } >
138+ < PopoverTrigger asChild >
139+ < form
140+ onSubmit = { ( e ) => {
141+ e . preventDefault ( ) ;
142+ form . handleSubmit ( ) ;
143+ } }
144+ className = "flex items-center gap-2"
145+ >
146+ < form . Field name = "email" >
147+ { ( field ) => (
148+ < div className = "relative flex-1 max-w-[220px]" >
149+ < MailIcon className = "absolute left-2.5 top-1/2 -translate-y-1/2 size-3.5 text-neutral-400" />
150+ < input
151+ type = "email"
152+ value = { field . state . value }
153+ onChange = { ( e ) => field . handleChange ( e . target . value ) }
154+ placeholder = "Subscribe to updates"
155+ className = { cn ( [
156+ "w-full pl-8 pr-3 py-1.5 text-sm" ,
157+ "border border-neutral-200 rounded-md" ,
158+ "bg-white placeholder:text-neutral-400" ,
159+ "focus:outline-none focus:ring-1 focus:ring-stone-400 focus:border-stone-400" ,
160+ "transition-all" ,
161+ ] ) }
162+ required
163+ />
164+ </ div >
165+ ) }
166+ </ form . Field >
167+ < button
168+ type = "submit"
169+ className = { cn ( [
170+ "p-1.5 rounded-md" ,
171+ "bg-stone-600 text-white" ,
172+ "hover:bg-stone-700 transition-colors" ,
173+ "focus:outline-none focus:ring-1 focus:ring-stone-400" ,
174+ ] ) }
175+ >
176+ < ArrowRightIcon className = "size-3.5" />
177+ </ button >
178+ </ form >
179+ </ PopoverTrigger >
180+ < PopoverContent
181+ align = "start"
182+ className = "w-72 p-4 bg-white border border-neutral-200 shadow-lg"
183+ >
184+ < div className = "space-y-4" >
185+ < div >
186+ < p className = "text-sm font-medium text-neutral-900 mb-1" >
187+ What would you like to receive?
188+ </ p >
189+ < p className = "text-xs text-neutral-500" >
190+ Select your preferences for { email }
191+ </ p >
192+ </ div >
193+
194+ < div className = "space-y-3" >
195+ < div className = "space-y-2" >
196+ < p className = "text-xs font-medium text-neutral-700 uppercase tracking-wide" >
197+ Release Notes
198+ </ p >
199+ < label className = "flex items-center gap-2 cursor-pointer" >
200+ < Checkbox
201+ checked = { subscriptions . releaseNotesStable }
202+ onCheckedChange = { ( checked ) =>
203+ setSubscriptions ( ( prev ) => ( {
204+ ...prev ,
205+ releaseNotesStable : checked === true ,
206+ } ) )
207+ }
208+ />
209+ < span className = "text-sm text-neutral-600" > Stable</ span >
210+ </ label >
211+ < label className = "flex items-center gap-2 cursor-pointer" >
212+ < Checkbox
213+ checked = { subscriptions . releaseNotesBeta }
214+ onCheckedChange = { ( checked ) =>
215+ setSubscriptions ( ( prev ) => ( {
216+ ...prev ,
217+ releaseNotesBeta : checked === true ,
218+ } ) )
219+ }
220+ />
221+ < div className = "flex items-center gap-1.5" >
222+ < span className = "text-sm text-neutral-600" > Beta</ span >
223+ < span className = "text-xs text-neutral-400" >
224+ - includes beta download link
225+ </ span >
226+ </ div >
227+ </ label >
228+ </ div >
229+
230+ < div className = "space-y-2" >
231+ < p className = "text-xs font-medium text-neutral-700 uppercase tracking-wide" >
232+ Newsletter
233+ </ p >
234+ < label className = "flex items-center gap-2 cursor-pointer" >
235+ < Checkbox
236+ checked = { subscriptions . newsletter }
237+ onCheckedChange = { ( checked ) =>
238+ setSubscriptions ( ( prev ) => ( {
239+ ...prev ,
240+ newsletter : checked === true ,
241+ } ) )
242+ }
243+ />
244+ < div className = "flex flex-col" >
245+ < span className = "text-sm text-neutral-600" >
246+ Subscribe to newsletter
247+ </ span >
248+ < span className = "text-xs text-neutral-400" >
249+ About notetaking, opensource, and AI
250+ </ span >
251+ </ div >
252+ </ label >
253+ </ div >
254+ </ div >
255+
256+ < button
257+ onClick = { ( ) => mutation . mutate ( ) }
258+ disabled = { ! hasSelection || mutation . isPending }
259+ className = { cn ( [
260+ "w-full py-2 px-4 text-sm font-medium rounded-md transition-all" ,
261+ hasSelection
262+ ? "bg-stone-600 text-white hover:bg-stone-700"
263+ : "bg-neutral-100 text-neutral-400 cursor-not-allowed" ,
264+ mutation . isPending && "opacity-50 cursor-wait" ,
265+ ] ) }
266+ >
267+ { mutation . isPending
268+ ? "Subscribing..."
269+ : mutation . isSuccess
270+ ? "Subscribed!"
271+ : "Subscribe" }
272+ </ button >
273+
274+ { mutation . isError && (
275+ < p className = "text-xs text-red-500" >
276+ Something went wrong. Please try again.
277+ </ p >
278+ ) }
279+ </ div >
280+ </ PopoverContent >
281+ </ Popover >
282+ </ div >
283+
79284 < p className = "text-sm text-neutral-500" >
80285 < Link
81286 to = "/legal/$slug"
0 commit comments