Skip to content
Merged
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
2 changes: 2 additions & 0 deletions web/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Extension } from "@/components/Extension/Extension";
import { Cursor } from "@/components/Cursor/Cursor";
import { DownloadGate } from "@/components/DownloadGate/DownloadGate";
import { Faq } from "@/components/Faq/Faq";
import { Features } from "@/components/Features/Features";
import { FinalCta } from "@/components/FinalCta/FinalCta";
Expand Down Expand Up @@ -44,6 +45,7 @@ export function App() {

<Footer />
<Cursor />
<DownloadGate />
<Grain aria-hidden="true" />
</>
);
Expand Down
187 changes: 187 additions & 0 deletions web/src/components/DownloadGate/DownloadGate.styles.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { styled } from "styled-components";

export const Backdrop = styled.div`
position: fixed;
inset: 0;
z-index: 300;
display: grid;
place-items: center;
padding: 1.2rem;
background: rgba(4, 4, 3, 0.78);
backdrop-filter: blur(18px);
animation: gate-in 260ms ease both;

@keyframes gate-in {
from {
opacity: 0;
}
}
`;

export const DialogCard = styled.div`
position: relative;
overflow: hidden;
width: min(100%, 34rem);
padding: clamp(1.5rem, 1.1rem + 2vw, 2.5rem);
border: 1px solid var(--outline-strong);
border-radius: clamp(22px, 4vw, 30px);
background:
radial-gradient(80% 90% at 50% 110%, rgba(232, 133, 97, 0.2), transparent 68%),
var(--canvas-raised);
box-shadow: 0 34px 100px rgba(0, 0, 0, 0.52);
animation: card-in 520ms var(--ease-out) both;

@keyframes card-in {
from {
opacity: 0;
transform: translateY(22px) scale(0.97);
}
}
`;

export const CloseButton = styled.button`
position: absolute;
top: 1rem;
right: 1rem;
display: grid;
width: 2.2rem;
height: 2.2rem;
place-items: center;
border: 1px solid var(--outline);
border-radius: 50%;
background: var(--surface);
color: var(--ink-secondary);
font-size: 1.25rem;
line-height: 1;
transition:
border-color var(--dur-fast) ease,
color var(--dur-fast) ease,
transform var(--dur-fast) ease;

&:hover {
border-color: var(--outline-strong);
color: var(--ink-primary);
transform: rotate(5deg);
}
`;

export const Eyebrow = styled.p`
margin-bottom: 1rem;
color: var(--accent);
font-family: var(--font-mono);
font-size: 0.68rem;
letter-spacing: 0.12em;
text-transform: uppercase;
`;

export const GateTitle = styled.h2`
max-width: 11ch;
padding-right: 2.5rem;
font-size: clamp(2rem, 1.45rem + 2.6vw, 3.35rem);
letter-spacing: -0.05em;
`;

export const GateBody = styled.p`
max-width: 42ch;
margin-top: 1rem;
color: var(--ink-secondary);
line-height: 1.55;
`;

export const Form = styled.form`
display: grid;
gap: 0.9rem;
margin-top: 1.65rem;
`;

export const Label = styled.label`
color: var(--ink-secondary);
font-size: 0.82rem;
`;

export const EmailInput = styled.input`
width: 100%;
padding: 0.92rem 1rem;
border: 1px solid var(--outline-strong);
border-radius: 14px;
outline: none;
background: rgba(243, 237, 227, 0.06);
color: var(--ink-primary);
font: inherit;
transition:
border-color var(--dur-fast) ease,
box-shadow var(--dur-fast) ease;

&::placeholder {
color: var(--ink-faint);
}

&:focus {
border-color: rgba(232, 133, 97, 0.72);
box-shadow: 0 0 0 4px rgba(232, 133, 97, 0.11);
}
`;

export const ConsentLabel = styled.label`
display: grid;
grid-template-columns: auto 1fr;
align-items: start;
gap: 0.65rem;
color: var(--ink-tertiary);
font-size: 0.78rem;
line-height: 1.5;

input {
width: 1rem;
height: 1rem;
margin-top: 0.15rem;
accent-color: var(--accent);
}

a {
border-bottom: 1px solid var(--outline-strong);
color: var(--ink-secondary);
}
`;

export const SubmitButton = styled.button`
display: inline-flex;
min-height: 3.15rem;
align-items: center;
justify-content: center;
padding: 0.82rem 1.25rem;
border-radius: var(--r-pill);
background: var(--cream);
color: #14100c;
font-weight: 600;
transition:
background-color var(--dur) var(--ease-out),
box-shadow var(--dur) var(--ease-out),
transform var(--dur) var(--ease-out);

&:hover:not(:disabled) {
background: var(--accent);
box-shadow: 0 16px 34px -18px rgba(232, 133, 97, 0.7);
transform: translateY(-2px);
}

&:disabled {
cursor: wait;
opacity: 0.62;
}
`;

export const FormNote = styled.p`
color: var(--ink-faint);
font-size: 0.74rem;
line-height: 1.5;
text-align: center;
`;

export const Honeypot = styled.div`
position: absolute;
left: -10000px;
width: 1px;
height: 1px;
overflow: hidden;
`;
124 changes: 124 additions & 0 deletions web/src/components/DownloadGate/DownloadGate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { useEffect, useId, useRef, useState } from "react";

import { BREVO_SIGNUP_URL, DOWNLOAD_GATE_EVENT } from "@/lib/downloadGate";
import {
Backdrop,
CloseButton,
ConsentLabel,
DialogCard,
EmailInput,
Eyebrow,
Form,
FormNote,
GateBody,
GateTitle,
Honeypot,
Label,
SubmitButton,
} from "./DownloadGate.styles";

export function DownloadGate() {
const [open, setOpen] = useState(false);
const [consent, setConsent] = useState(false);
const emailId = useId();
const consentId = useId();
const emailRef = useRef<HTMLInputElement>(null);

const close = () => {
setOpen(false);
};

useEffect(() => {
const show = () => {
setOpen(true);
window.setTimeout(() => emailRef.current?.focus(), 40);
};

window.addEventListener(DOWNLOAD_GATE_EVENT, show);
return () => window.removeEventListener(DOWNLOAD_GATE_EVENT, show);
}, []);

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

const previousOverflow = document.body.style.overflow;
const onKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") close();
};

document.body.style.overflow = "hidden";
window.addEventListener("keydown", onKeyDown);

return () => {
document.body.style.overflow = previousOverflow;
window.removeEventListener("keydown", onKeyDown);
};
}, [open]);

if (!open) return null;

return (
<Backdrop onMouseDown={(event) => event.target === event.currentTarget && close()}>
<DialogCard role="dialog" aria-modal="true" aria-labelledby="download-gate-title">
<CloseButton type="button" aria-label="Close download form" onClick={close}>
×
</CloseButton>

<Eyebrow>Free download · Product updates</Eyebrow>
<GateTitle id="download-gate-title">Get AngelNotch.</GateTitle>
<GateBody>
Enter your email to download the macOS app and receive occasional release notes and
product updates.
</GateBody>

<Form action={BREVO_SIGNUP_URL} method="POST">
<Label htmlFor={emailId}>Email address</Label>
<EmailInput
ref={emailRef}
id={emailId}
type="email"
name="EMAIL"
placeholder="you@example.com"
autoComplete="email"
required
/>

<Honeypot aria-hidden="true">
<label htmlFor="download-email-check">Leave this field empty</label>
<input
id="download-email-check"
name="email_address_check"
tabIndex={-1}
autoComplete="off"
/>
</Honeypot>

<input type="hidden" name="locale" value="en" />
<input type="hidden" name="html_type" value="simple" />

<ConsentLabel htmlFor={consentId}>
<input
id={consentId}
type="checkbox"
checked={consent}
onChange={(event) => setConsent(event.target.checked)}
required
/>
<span>
I agree to receive AngelNotch download and product emails. See the{" "}
<a href="#privacy" onClick={close}>
privacy promise
</a>
.
</span>
</ConsentLabel>

<SubmitButton type="submit" disabled={!consent}>
Subscribe & download
</SubmitButton>
<FormNote>Brevo securely subscribes you, then starts the free download.</FormNote>
</Form>
</DialogCard>
</Backdrop>
);
}
10 changes: 7 additions & 3 deletions web/src/components/FinalCta/FinalCta.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { Button } from "@/components/ui";
import { openDownloadGate } from "@/lib/downloadGate";
import {
DOWNLOAD_NAME,
DOWNLOAD_SIZE,
DOWNLOAD_URL,
MIN_MACOS,
RELEASES_URL,
REPO_URL,
Expand Down Expand Up @@ -32,7 +31,12 @@ export function FinalCta() {
</CtaBody>

<CtaButtons>
<Button href={DOWNLOAD_URL} download={DOWNLOAD_NAME} icon="download" meta={DOWNLOAD_SIZE}>
<Button
href="#download"
onClick={openDownloadGate}
icon="download"
meta={DOWNLOAD_SIZE}
>
Download AngelNotch
</Button>
<Button
Expand Down
5 changes: 3 additions & 2 deletions web/src/components/Footer/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Wordmark } from "@/components/icons";
import { openDownloadGate } from "@/lib/downloadGate";
import {
AUTHOR_NAME,
AUTHOR_URL,
Expand Down Expand Up @@ -79,8 +80,8 @@ export function Footer() {
{column.links.map((link) => (
<FooterLink
key={link.label}
href={link.href}
{...("download" in link && link.download ? { download: link.download } : {})}
href={"download" in link ? "#download" : link.href}
{...("download" in link ? { onClick: openDownloadGate } : {})}
{...("external" in link && link.external
? { target: "_blank", rel: "noreferrer" }
: {})}
Expand Down
2 changes: 1 addition & 1 deletion web/src/components/Intro/Intro.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const PILLARS = [
{
index: "iii",
title: "Private by design",
body: "Everything stays on the Mac. No account, no analytics, no advertising, no tracking SDKs.",
body: "App data stays on the Mac. No in-app account, analytics, advertising or tracking SDKs.",
},
];

Expand Down
5 changes: 3 additions & 2 deletions web/src/components/Nav/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import { useEffect, useRef, useState } from "react";

import { Wordmark } from "@/components/icons";
import { Button } from "@/components/ui";
import { DOWNLOAD_NAME, DOWNLOAD_URL, REPO_URL } from "@/lib/site";
import { openDownloadGate } from "@/lib/downloadGate";
import { REPO_URL } from "@/lib/site";
import {
Actions,
Brand,
Expand Down Expand Up @@ -71,7 +72,7 @@ export function Nav() {
<StarLabel>Star</StarLabel>
</StarLink>

<Button href={DOWNLOAD_URL} download={DOWNLOAD_NAME} size="small" icon="download">
<Button href="#download" onClick={openDownloadGate} size="small" icon="download">
Download
</Button>
</Actions>
Expand Down
Loading