forked from Mike/freno-dev
[gpt-6.1-sol] The meetings, mixer and charging previews had drifted from the current app, while slogans obscured what the modules do. Match the shipped controls and behavior, reuse the existing demo machinery, and explain the landing-page features plainly without changing routing or purchase flows. Tests: bun test src/routes/nook src/components/nook (36 passed) NOOK-1037
244 lines
9.6 KiB
TypeScript
244 lines
9.6 KiB
TypeScript
import { PageHead } from "~/components/PageHead";
|
|
import { EdgeCacheHeaders } from "~/components/EdgeCacheHeaders";
|
|
import SubdomainHeader from "~/components/SubdomainHeader";
|
|
import FeatureCollage from "~/components/nook/FeatureCollage";
|
|
import FeatureBreakdowns from "~/components/nook/FeatureBreakdowns";
|
|
import IslandExtras from "~/components/nook/IslandExtras";
|
|
import PhoneBeta from "~/components/nook/PhoneBeta";
|
|
import Button from "~/components/ui/Button";
|
|
import Input from "~/components/ui/Input";
|
|
import { createSignal, Show } from "solid-js";
|
|
import { useDarkMode } from "~/context/darkMode";
|
|
import { useSite } from "~/context/SiteContext";
|
|
|
|
/*
|
|
* Downloads are logged and resolved server-side. `/api/the-nook/download`
|
|
* records the click in the analytics buffer, then 302s to the latest
|
|
* release dmg the appcast names. Pointing the button straight at the
|
|
* enclosure would skip the funnel's second step entirely.
|
|
*/
|
|
function downloadNook() {
|
|
window.location.href = "/api/the-nook/download";
|
|
}
|
|
|
|
export default function NookLanding() {
|
|
const site = useSite();
|
|
const { isDark } = useDarkMode();
|
|
const brandColor = () =>
|
|
isDark() ? (site().brandColorDark ?? site().brandColor) : site().brandColor;
|
|
const [resendEmail, setResendEmail] = createSignal("");
|
|
const [resendSending, setResendSending] = createSignal(false);
|
|
const [resendSent, setResendSent] = createSignal(false);
|
|
const [resendError, setResendError] = createSignal(false);
|
|
|
|
const resendLicense = async () => {
|
|
if (resendSending()) return;
|
|
const email = resendEmail().trim();
|
|
if (!email) return;
|
|
setResendSending(true);
|
|
setResendSent(false);
|
|
setResendError(false);
|
|
try {
|
|
const res = await fetch("/api/the-nook/resend-license", {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json" },
|
|
body: JSON.stringify({ email })
|
|
});
|
|
if (!res.ok) throw new Error("resend failed");
|
|
setResendSent(true);
|
|
} catch {
|
|
setResendError(true);
|
|
} finally {
|
|
setResendSending(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<EdgeCacheHeaders maxAge={300} />
|
|
<PageHead
|
|
title="Agent control for your Mac"
|
|
ogTitle="The Nook — agent control for your Mac"
|
|
description="Monitor coding agents, respond to permission requests, and manage your Mac\'s cooling, battery, and audio with The Nook."
|
|
ogImage={`https://${site().domain}/nook/og.png`}
|
|
ogDescription="A macOS app for coding agents, fan control, battery charging, and per-app audio. Includes a 14-day trial and a one-time license."
|
|
/>
|
|
|
|
<SubdomainHeader />
|
|
|
|
{/* ── Hero ─────────────────────────────────────────────────────── */}
|
|
{/* -mt-14 pulls the hero under the sticky header; pt-14 re-clears
|
|
content, so the gradient fills the full viewport incl. the bar */}
|
|
<div class="relative -mt-14 flex min-h-svh flex-col overflow-hidden pt-14">
|
|
<div
|
|
class="fixed inset-0 z-0"
|
|
style={{
|
|
background: isDark()
|
|
? "radial-gradient(ellipse at top, #123945 0%, #0b0b10 70%)"
|
|
: "radial-gradient(ellipse at top, #d7eef5 0%, #f5f5f5 70%)"
|
|
}}
|
|
/>
|
|
<div class="relative z-10 flex flex-1 flex-col items-center justify-center px-4 py-12 text-center sm:py-24">
|
|
<img
|
|
src="/nook/icon.png"
|
|
alt="The Nook App Icon"
|
|
height={128}
|
|
width={128}
|
|
class="mb-4 h-24 w-24 rounded-[22%] object-cover object-center shadow-2xl sm:mb-6 sm:h-32 sm:w-32"
|
|
/>
|
|
<div
|
|
class="text-text/90 mb-6 rounded-2xl px-5 py-3 text-sm font-semibold tracking-wide backdrop-blur-sm"
|
|
style={{
|
|
border: "1px solid var(--color-overlay0)",
|
|
background: "var(--color-surface0)"
|
|
}}
|
|
>
|
|
The Nook
|
|
</div>
|
|
<h1 class="text-text mb-4 text-4xl font-bold tracking-tight sm:text-5xl">
|
|
Manage your coding agents and your Mac
|
|
</h1>
|
|
<p class="text-subtext0 mb-2 max-w-xl text-xl">
|
|
Follow your agents, respond to requests, and manage cooling,
|
|
charging, and audio from the top of your screen.
|
|
</p>
|
|
<p class="text-subtext1 mb-8 text-sm">
|
|
macOS 14.2+ · 14-day free trial · up to 3 devices
|
|
</p>
|
|
|
|
<div class="flex flex-col items-center gap-4 sm:flex-row sm:space-x-4">
|
|
<Button
|
|
variant="download"
|
|
size="lg"
|
|
color={brandColor()}
|
|
onClick={downloadNook}
|
|
>
|
|
Download trial
|
|
</Button>
|
|
<Button
|
|
variant="secondary"
|
|
size="lg"
|
|
onClick={() => (window.location.href = "/checkout")}
|
|
>
|
|
Buy
|
|
</Button>
|
|
</div>
|
|
<div class="mt-3 flex flex-col items-center gap-1.5">
|
|
<span class="border-overlay0 bg-surface0 text-subtext0 rounded-full border px-2.5 py-0.5 text-[11px] font-semibold tracking-wide">
|
|
Beta pricing · 33% off
|
|
</span>
|
|
<p class="text-subtext1 text-xs">
|
|
<span class="text-subtext0 line-through">$15</span>{" "}
|
|
<span class="text-text font-bold">$10</span> one-time
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ── Feature collage ─────────────────────────────────────────── */}
|
|
<FeatureCollage />
|
|
|
|
{/* ── Feature breakdowns: the features in action ─────────────── */}
|
|
<section class="bg-base relative z-20 px-4 py-16 md:px-8">
|
|
<div class="mx-auto max-w-5xl">
|
|
<p class="text-subtext1 mb-2 text-center text-xs font-semibold tracking-[0.18em] uppercase">
|
|
In action
|
|
</p>
|
|
<h2 class="text-text mb-16 text-center text-3xl font-bold">
|
|
What you can do with The Nook
|
|
</h2>
|
|
<FeatureBreakdowns />
|
|
</div>
|
|
</section>
|
|
|
|
{/* ── Everything else: marquee + live module bento ───────────── */}
|
|
<IslandExtras />
|
|
|
|
{/* ── iPhone + Watch companion, TestFlight beta ──────────────── */}
|
|
<PhoneBeta brandColor={brandColor()} />
|
|
|
|
{/* ── CTA ──────────────────────────────────────────────────────── */}
|
|
<section class="bg-surface0 relative z-20 px-4 py-20 md:px-8">
|
|
<div class="mx-auto max-w-4xl text-center">
|
|
<h2 class="text-text mb-4 text-3xl font-bold">
|
|
Try it free for 14 days
|
|
</h2>
|
|
<p class="text-subtext0 mx-auto mb-10 max-w-2xl leading-relaxed">
|
|
The trial includes every module. After 14 days, a one-time
|
|
purchase lets you keep using The Nook on up to three of your Macs.
|
|
There is no subscription.
|
|
</p>
|
|
<div class="flex flex-col items-center justify-center gap-4 sm:flex-row sm:space-x-4">
|
|
<Button
|
|
variant="download"
|
|
size="lg"
|
|
color={brandColor()}
|
|
onClick={downloadNook}
|
|
>
|
|
Download trial
|
|
</Button>
|
|
<a
|
|
class="text-subtext1 my-auto text-sm underline decoration-dotted hover:opacity-80"
|
|
href="/checkout"
|
|
>
|
|
Buy a license → $10{" "}
|
|
<span class="text-subtext0 line-through">$15</span>
|
|
</a>
|
|
</div>
|
|
<p class="text-subtext1 mt-6 text-xs">
|
|
The beta price is $10. The regular price will be $15. One license covers up to 3 of
|
|
your own Macs.
|
|
</p>
|
|
</div>
|
|
</section>
|
|
|
|
{/* ── Resend license ─────────────────────────────────────────── */}
|
|
<section class="bg-base relative z-20 px-4 py-16 md:px-8">
|
|
<div class="border-overlay0 bg-surface0 mx-auto max-w-md rounded-xl border p-8">
|
|
<h2 class="text-text mb-2 text-center text-2xl font-bold">
|
|
Lost your license?
|
|
</h2>
|
|
<p class="text-subtext0 mb-6 text-center text-sm">
|
|
Enter the email you purchased with and we'll resend your key.
|
|
</p>
|
|
<form
|
|
class="flex flex-col gap-3"
|
|
onSubmit={(e) => {
|
|
e.preventDefault();
|
|
resendLicense();
|
|
}}
|
|
>
|
|
<Input
|
|
type="email"
|
|
required
|
|
label="Email"
|
|
placeholder="you@example.com"
|
|
disabled={resendSending()}
|
|
value={resendEmail()}
|
|
onInput={(e) => setResendEmail(e.currentTarget.value)}
|
|
/>
|
|
<Button
|
|
type="submit"
|
|
variant="primary"
|
|
color={brandColor()}
|
|
loading={resendSending()}
|
|
>
|
|
Resend license
|
|
</Button>
|
|
</form>
|
|
<Show when={resendSent()}>
|
|
<p class="text-subtext1 mt-4 text-center text-sm">
|
|
If a license is registered to that email, we'll send you the key.
|
|
</p>
|
|
</Show>
|
|
<Show when={resendError()}>
|
|
<p class="mt-4 text-center text-sm text-red-500">
|
|
Something went wrong. Please try again.
|
|
</p>
|
|
</Show>
|
|
</div>
|
|
</section>
|
|
</>
|
|
);
|
|
}
|