@@ -2,14 +2,9 @@ import { useForm } from "@tanstack/react-form";
22import { useMutation } from "@tanstack/react-query" ;
33import { Link , useRouterState } from "@tanstack/react-router" ;
44import { ArrowRightIcon , ExternalLinkIcon , MailIcon } from "lucide-react" ;
5- import { useState } from "react" ;
5+ import { useEffect , useRef , useState } from "react" ;
66
77import { Checkbox } from "@hypr/ui/components/ui/checkbox" ;
8- import {
9- Popover ,
10- PopoverContent ,
11- PopoverTrigger ,
12- } from "@hypr/ui/components/ui/popover" ;
138import { cn } from "@hypr/utils" ;
149
1510import { Image } from "@/components/image" ;
@@ -68,13 +63,30 @@ export function Footer() {
6863}
6964
7065function BrandSection ( { currentYear } : { currentYear : number } ) {
71- const [ popoverOpen , setPopoverOpen ] = useState ( false ) ;
66+ const [ expanded , setExpanded ] = useState ( false ) ;
7267 const [ email , setEmail ] = useState ( "" ) ;
7368 const [ subscriptions , setSubscriptions ] = useState ( {
7469 releaseNotesStable : false ,
7570 releaseNotesBeta : false ,
7671 newsletter : false ,
7772 } ) ;
73+ const containerRef = useRef < HTMLDivElement > ( null ) ;
74+
75+ useEffect ( ( ) => {
76+ if ( ! expanded ) return ;
77+
78+ const handleClickOutside = ( event : MouseEvent ) => {
79+ if (
80+ containerRef . current &&
81+ ! containerRef . current . contains ( event . target as Node )
82+ ) {
83+ setExpanded ( false ) ;
84+ }
85+ } ;
86+
87+ document . addEventListener ( "mousedown" , handleClickOutside ) ;
88+ return ( ) => document . removeEventListener ( "mousedown" , handleClickOutside ) ;
89+ } , [ expanded ] ) ;
7890
7991 const mutation = useMutation ( {
8092 mutationFn : async ( ) => {
@@ -90,7 +102,7 @@ function BrandSection({ currentYear }: { currentYear: number }) {
90102 } ) ;
91103 } ,
92104 onSuccess : ( ) => {
93- setPopoverOpen ( false ) ;
105+ setExpanded ( false ) ;
94106 setEmail ( "" ) ;
95107 setSubscriptions ( {
96108 releaseNotesStable : false ,
@@ -104,7 +116,6 @@ function BrandSection({ currentYear }: { currentYear: number }) {
104116 defaultValues : { email : "" } ,
105117 onSubmit : async ( { value } ) => {
106118 setEmail ( value . email ) ;
107- setPopoverOpen ( true ) ;
108119 } ,
109120 } ) ;
110121
@@ -124,151 +135,132 @@ function BrandSection({ currentYear }: { currentYear: number }) {
124135 </ Link >
125136 < p className = "text-sm text-neutral-500 mb-4" > Fastrepl © { currentYear } </ p >
126137
127- < div className = "mb-4" >
128- < Popover open = { popoverOpen } onOpenChange = { setPopoverOpen } >
129- < PopoverTrigger asChild >
130- < form
131- onSubmit = { ( e ) => {
132- e . preventDefault ( ) ;
133- form . handleSubmit ( ) ;
134- } }
135- className = "flex items-center"
136- >
137- < form . Field name = "email" >
138- { ( field ) => (
139- < div className = { cn ( [
140- "relative flex items-center max-w-64 border border-neutral-100 laptop:border-l-0 bg-white overflow-hidden transition-all" ,
141- "focus-within:ring-1 focus-within:ring-stone-400 focus-within:border-stone-400" ,
142- ] ) } >
143- < MailIcon className = "absolute left-2.5 size-3.5 text-neutral-400" />
144- < input
145- type = "email"
146- value = { field . state . value }
147- onChange = { ( e ) => field . handleChange ( e . target . value ) }
148- placeholder = "Subscribe to updates"
149- className = { cn ( [
150- "min-w-0 flex-1 pl-8 pr-2 py-1.5 text-sm" ,
151- "bg-transparent placeholder:text-neutral-400" ,
152- "focus:outline-none" ,
153- ] ) }
154- required
155- />
156- < button
157- type = "submit"
158- className = { cn ( [
159- "shrink-0 px-2 transition-colors focus:outline-none" ,
160- field . state . value ? "text-stone-600" : "text-neutral-300" ,
161- ] ) }
162- >
163- < ArrowRightIcon className = "size-4" />
164- </ button >
165- </ div >
166- ) }
167- </ form . Field >
168- </ form >
169- </ PopoverTrigger >
170- < PopoverContent
171- align = "start"
172- className = "w-72 p-4 bg-white border border-neutral-200 shadow-lg"
173- >
174- < div className = "space-y-4" >
175- < div >
176- < p className = "text-sm font-medium text-neutral-900 mb-1" >
177- What would you like to receive?
178- </ p >
179- < p className = "text-xs text-neutral-500" >
180- Select your preferences for { email }
138+ < div className = "mb-4 relative" ref = { containerRef } >
139+ { expanded && (
140+ < div className = "absolute bottom-full left-0 w-72 bg-white border border-b-0 laptop:border-l-0 border-stone-100 p-4 space-y-4" >
141+ < p className = "text-sm font-medium text-neutral-900" >
142+ What would you like to receive?
143+ </ p >
144+
145+ < div className = "space-y-3" >
146+ < div className = "space-y-2" >
147+ < p className = "text-xs font-medium text-neutral-700 uppercase tracking-wide" >
148+ Release Notes
181149 </ p >
150+ < label className = "flex items-center gap-2 cursor-pointer" >
151+ < Checkbox
152+ checked = { subscriptions . releaseNotesStable }
153+ onCheckedChange = { ( checked ) =>
154+ setSubscriptions ( ( prev ) => ( {
155+ ...prev ,
156+ releaseNotesStable : checked === true ,
157+ } ) )
158+ }
159+ className = "data-[state=checked]:bg-black data-[state=checked]:border-black data-[state=checked]:text-white"
160+ />
161+ < span className = "text-sm text-neutral-600" > Stable</ span >
162+ </ label >
163+ < label className = "flex items-center gap-2 cursor-pointer" >
164+ < Checkbox
165+ checked = { subscriptions . releaseNotesBeta }
166+ onCheckedChange = { ( checked ) =>
167+ setSubscriptions ( ( prev ) => ( {
168+ ...prev ,
169+ releaseNotesBeta : checked === true ,
170+ } ) )
171+ }
172+ className = "data-[state=checked]:bg-black data-[state=checked]:border-black data-[state=checked]:text-white"
173+ />
174+ < div className = "flex items-center gap-1.5" >
175+ < span className = "text-sm text-neutral-600" > Beta</ span >
176+ < span className = "text-xs text-neutral-400" >
177+ - includes beta download link
178+ </ span >
179+ </ div >
180+ </ label >
182181 </ div >
183182
184- < div className = "space-y-3" >
185- < div className = "space-y-2" >
186- < p className = "text-xs font-medium text-neutral-700 uppercase tracking-wide" >
187- Release Notes
188- </ p >
189- < label className = "flex items-center gap-2 cursor-pointer" >
190- < Checkbox
191- checked = { subscriptions . releaseNotesStable }
192- onCheckedChange = { ( checked ) =>
193- setSubscriptions ( ( prev ) => ( {
194- ...prev ,
195- releaseNotesStable : checked === true ,
196- } ) )
197- }
198- />
199- < span className = "text-sm text-neutral-600" > Stable</ span >
200- </ label >
201- < label className = "flex items-center gap-2 cursor-pointer" >
202- < Checkbox
203- checked = { subscriptions . releaseNotesBeta }
204- onCheckedChange = { ( checked ) =>
205- setSubscriptions ( ( prev ) => ( {
206- ...prev ,
207- releaseNotesBeta : checked === true ,
208- } ) )
209- }
210- />
211- < div className = "flex items-center gap-1.5" >
212- < span className = "text-sm text-neutral-600" > Beta</ span >
213- < span className = "text-xs text-neutral-400" >
214- - includes beta download link
215- </ span >
216- </ div >
217- </ label >
218- </ div >
219-
220- < div className = "space-y-2" >
221- < p className = "text-xs font-medium text-neutral-700 uppercase tracking-wide" >
222- Newsletter
223- </ p >
224- < label className = "flex items-center gap-2 cursor-pointer" >
225- < Checkbox
226- checked = { subscriptions . newsletter }
227- onCheckedChange = { ( checked ) =>
228- setSubscriptions ( ( prev ) => ( {
229- ...prev ,
230- newsletter : checked === true ,
231- } ) )
232- }
233- />
234- < div className = "flex flex-col" >
235- < span className = "text-sm text-neutral-600" >
236- Subscribe to newsletter
237- </ span >
238- < span className = "text-xs text-neutral-400" >
239- About notetaking, opensource, and AI
240- </ span >
241- </ div >
242- </ label >
243- </ div >
183+ < div className = "space-y-2" >
184+ < p className = "text-xs font-medium text-neutral-700 uppercase tracking-wide" >
185+ Newsletter
186+ </ p >
187+ < label className = "flex items-center gap-2 cursor-pointer" >
188+ < Checkbox
189+ checked = { subscriptions . newsletter }
190+ onCheckedChange = { ( checked ) =>
191+ setSubscriptions ( ( prev ) => ( {
192+ ...prev ,
193+ newsletter : checked === true ,
194+ } ) )
195+ }
196+ className = "data-[state=checked]:bg-black data-[state=checked]:border-black data-[state=checked]:text-white"
197+ />
198+ < span className = "text-sm text-neutral-600" > Blog</ span >
199+ </ label >
244200 </ div >
201+ </ div >
245202
246- < button
247- onClick = { ( ) => mutation . mutate ( ) }
248- disabled = { ! hasSelection || mutation . isPending }
249- className = { cn ( [
250- "w-full py-2 px-4 text-sm font-medium rounded-md transition-all" ,
251- hasSelection
252- ? "bg-stone-600 text-white hover:bg-stone-700"
253- : "bg-neutral-100 text-neutral-400 cursor-not-allowed" ,
254- mutation . isPending && "opacity-50 cursor-wait" ,
255- ] ) }
256- >
257- { mutation . isPending
258- ? "Subscribing..."
259- : mutation . isSuccess
260- ? "Subscribed!"
261- : "Subscribe" }
262- </ button >
203+ { mutation . isError && (
204+ < p className = "text-xs text-red-500" >
205+ Something went wrong. Please try again.
206+ </ p >
207+ ) }
208+ </ div >
209+ ) }
263210
264- { mutation . isError && (
265- < p className = "text-xs text-red-500" >
266- Something went wrong. Please try again.
267- </ p >
268- ) }
269- </ div >
270- </ PopoverContent >
271- </ Popover >
211+ < form
212+ onSubmit = { ( e ) => {
213+ e . preventDefault ( ) ;
214+ if ( expanded && hasSelection && email ) {
215+ mutation . mutate ( ) ;
216+ }
217+ } }
218+ className = { cn ( [
219+ "max-w-72 border border-neutral-100 bg-white transition-all laptop:border-l-0" ,
220+ expanded && "shadow-lg" ,
221+ ] ) }
222+ >
223+ < form . Field name = "email" >
224+ { ( field ) => (
225+ < div className = "relative flex items-center" >
226+ < MailIcon className = "absolute left-2.5 size-3.5 text-neutral-400" />
227+ < input
228+ type = "email"
229+ value = { field . state . value }
230+ onChange = { ( e ) => {
231+ field . handleChange ( e . target . value ) ;
232+ setEmail ( e . target . value ) ;
233+ } }
234+ onFocus = { ( ) => setExpanded ( true ) }
235+ placeholder = {
236+ expanded ? "Enter your email" : "Subscribe to updates"
237+ }
238+ className = { cn ( [
239+ "min-w-0 flex-1 pl-8 pr-2 py-1.5 text-sm" ,
240+ "bg-transparent placeholder:text-neutral-400" ,
241+ "focus:outline-none" ,
242+ ] ) }
243+ />
244+ < button
245+ type = { expanded ? "submit" : "button" }
246+ onClick = { ( ) => ! expanded && setExpanded ( true ) }
247+ disabled = {
248+ expanded && ( ! hasSelection || ! email || mutation . isPending )
249+ }
250+ className = { cn ( [
251+ "shrink-0 px-2 transition-colors focus:outline-none" ,
252+ expanded && hasSelection && email
253+ ? "text-stone-600"
254+ : "text-neutral-300" ,
255+ mutation . isPending && "opacity-50" ,
256+ ] ) }
257+ >
258+ < ArrowRightIcon className = "size-4" />
259+ </ button >
260+ </ div >
261+ ) }
262+ </ form . Field >
263+ </ form >
272264 </ div >
273265
274266 < p className = "text-sm text-neutral-500" >
0 commit comments