Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
207 changes: 197 additions & 10 deletions apps/web/src/components/footer.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { useForm } from "@tanstack/react-form";
import { useMutation } from "@tanstack/react-query";
import { Link, useRouterState } from "@tanstack/react-router";
import { ExternalLinkIcon, MailIcon } from "lucide-react";
import { useState } from "react";
import { ArrowRightIcon, ExternalLinkIcon, MailIcon } from "lucide-react";
import { useEffect, useRef, useState } from "react";

import { Checkbox } from "@hypr/ui/components/ui/checkbox";
import { cn } from "@hypr/utils";

import { Image } from "@/components/image";
import { addContact } from "@/functions/loops";

function getNextRandomIndex(length: number, prevIndex: number): number {
if (length <= 1) return 0;
Expand Down Expand Up @@ -57,6 +63,67 @@ export function Footer() {
}

function BrandSection({ currentYear }: { currentYear: number }) {
const [expanded, setExpanded] = useState(false);
const [email, setEmail] = useState("");
const [subscriptions, setSubscriptions] = useState({
releaseNotesStable: false,
releaseNotesBeta: false,
newsletter: false,
});
const containerRef = useRef<HTMLDivElement>(null);

useEffect(() => {
if (!expanded) return;

const handleClickOutside = (event: MouseEvent) => {
if (
containerRef.current &&
!containerRef.current.contains(event.target as Node)
) {
setExpanded(false);
}
};

document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [expanded]);

const mutation = useMutation({
mutationFn: async () => {
await addContact({
data: {
email,
userGroup: "Subscriber",
source: "FOOTER",
releaseNotesStable: subscriptions.releaseNotesStable,
releaseNotesBeta: subscriptions.releaseNotesBeta,
newsletter: subscriptions.newsletter,
},
});
},
onSuccess: () => {
setExpanded(false);
setEmail("");
setSubscriptions({
releaseNotesStable: false,
releaseNotesBeta: false,
newsletter: false,
});
},
});

const form = useForm({
defaultValues: { email: "" },
onSubmit: async ({ value }) => {
setEmail(value.email);
},
});

const hasSelection =
subscriptions.releaseNotesStable ||
subscriptions.releaseNotesBeta ||
subscriptions.newsletter;

return (
<div className="lg:flex-1">
<Link to="/" className="inline-block mb-4">
Expand All @@ -67,15 +134,135 @@ function BrandSection({ currentYear }: { currentYear: number }) {
/>
</Link>
<p className="text-sm text-neutral-500 mb-4">Fastrepl © {currentYear}</p>
<p className="text-sm text-neutral-600 mb-3">
Are you in back-to-back meetings?{" "}
<Link
to="/auth"
className="text-neutral-600 hover:text-stone-600 transition-colors underline decoration-solid"

<div className="mb-4 relative" ref={containerRef}>
{expanded && (
<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">
<p className="text-sm font-medium text-neutral-900">
What would you like to receive?
</p>

<div className="space-y-3">
<div className="space-y-2">
<p className="text-xs font-medium text-neutral-700 uppercase tracking-wide">
Release Notes
</p>
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={subscriptions.releaseNotesStable}
onCheckedChange={(checked) =>
setSubscriptions((prev) => ({
...prev,
releaseNotesStable: checked === true,
}))
}
className="data-[state=checked]:bg-black data-[state=checked]:border-black data-[state=checked]:text-white"
/>
<span className="text-sm text-neutral-600">Stable</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={subscriptions.releaseNotesBeta}
onCheckedChange={(checked) =>
setSubscriptions((prev) => ({
...prev,
releaseNotesBeta: checked === true,
}))
}
className="data-[state=checked]:bg-black data-[state=checked]:border-black data-[state=checked]:text-white"
/>
<div className="flex items-center gap-1.5">
<span className="text-sm text-neutral-600">Beta</span>
<span className="text-xs text-neutral-400">
- includes beta download link
</span>
</div>
</label>
</div>

<div className="space-y-2">
<p className="text-xs font-medium text-neutral-700 uppercase tracking-wide">
Newsletter
</p>
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={subscriptions.newsletter}
onCheckedChange={(checked) =>
setSubscriptions((prev) => ({
...prev,
newsletter: checked === true,
}))
}
className="data-[state=checked]:bg-black data-[state=checked]:border-black data-[state=checked]:text-white"
/>
<span className="text-sm text-neutral-600">Blog</span>
</label>
</div>
</div>

{mutation.isError && (
<p className="text-xs text-red-500">
Something went wrong. Please try again.
</p>
)}
</div>
)}

<form
onSubmit={(e) => {
e.preventDefault();
if (expanded && hasSelection && email) {
mutation.mutate();
}
}}
className={cn([
"max-w-72 border border-neutral-100 bg-white transition-all laptop:border-l-0",
expanded && "shadow-lg",
])}
>
Get started
</Link>
</p>
<form.Field name="email">
{(field) => (
<div className="relative flex items-center">
<MailIcon className="absolute left-2.5 size-3.5 text-neutral-400" />
<input
type="email"
value={field.state.value}
onChange={(e) => {
field.handleChange(e.target.value);
setEmail(e.target.value);
}}
onFocus={() => setExpanded(true)}
placeholder={
expanded ? "Enter your email" : "Subscribe to updates"
}
className={cn([
"min-w-0 flex-1 pl-8 pr-2 py-1.5 text-sm",
"bg-transparent placeholder:text-neutral-400",
"focus:outline-none",
])}
/>
<button
type={expanded ? "submit" : "button"}
onClick={() => !expanded && setExpanded(true)}
disabled={
expanded && (!hasSelection || !email || mutation.isPending)
}
className={cn([
"shrink-0 px-2 transition-colors focus:outline-none",
expanded && hasSelection && email
? "text-stone-600"
: "text-neutral-300",
mutation.isPending && "opacity-50",
])}
>
<ArrowRightIcon className="size-4" />
</button>
</div>
)}
</form.Field>
</form>
</div>

<p className="text-sm text-neutral-500">
<Link
to="/legal/$slug"
Expand Down
6 changes: 6 additions & 0 deletions apps/web/src/functions/loops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ const inputSchema = z.object({
platform: z.string().optional(),
source: z.string().optional(),
intent: z.string().optional(),
releaseNotesStable: z.boolean().optional(),
releaseNotesBeta: z.boolean().optional(),
newsletter: z.boolean().optional(),
});

export const addContact = createServerFn({ method: "POST" })
Expand All @@ -30,6 +33,9 @@ export const addContact = createServerFn({ method: "POST" })
source: data.source,
intent: data.intent,
platform: data.platform,
releaseNotesStable: data.releaseNotesStable,
releaseNotesBeta: data.releaseNotesBeta,
newsletter: data.newsletter,
}),
},
);
Expand Down
Loading