Skip to content

Commit 3f8ffa3

Browse files
Add email subscription field with popup in footer
Co-Authored-By: john@hyprnote.com <john@hyprnote.com>
1 parent 2168033 commit 3f8ffa3

File tree

2 files changed

+212
-1
lines changed

2 files changed

+212
-1
lines changed

apps/web/src/components/footer.tsx

Lines changed: 206 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
1+
import { useForm } from "@tanstack/react-form";
2+
import { useMutation } from "@tanstack/react-query";
13
import { Link, useRouterState } from "@tanstack/react-router";
2-
import { ExternalLinkIcon, MailIcon } from "lucide-react";
4+
import { ArrowRightIcon, ExternalLinkIcon, MailIcon } from "lucide-react";
35
import { 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+
515
import { Image } from "@/components/image";
16+
import { addContact } from "@/functions/loops";
617

718
function getNextRandomIndex(length: number, prevIndex: number): number {
819
if (length <= 1) return 0;
@@ -57,6 +68,51 @@ export function Footer() {
5768
}
5869

5970
function 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"

apps/web/src/functions/loops.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ const inputSchema = z.object({
1010
platform: z.string().optional(),
1111
source: z.string().optional(),
1212
intent: z.string().optional(),
13+
releaseNotesStable: z.boolean().optional(),
14+
releaseNotesBeta: z.boolean().optional(),
15+
newsletter: z.boolean().optional(),
1316
});
1417

1518
export const addContact = createServerFn({ method: "POST" })
@@ -30,6 +33,9 @@ export const addContact = createServerFn({ method: "POST" })
3033
source: data.source,
3134
intent: data.intent,
3235
platform: data.platform,
36+
releaseNotesStable: data.releaseNotesStable,
37+
releaseNotesBeta: data.releaseNotesBeta,
38+
newsletter: data.newsletter,
3339
}),
3440
},
3541
);

0 commit comments

Comments
 (0)