Files
freno-dev/src/routes/gaze/index.tsx
Michael Freno f2ff75e6df feat(gaze): add Gaze subdomain landing page with direct download
Move the Gaze marketing home from the legacy /marketing/gaze route to
a dedicated gaze subdomain landing page (src/routes/gaze). The new page
adds a richer hero, feature highlights, and a direct .dmg download flow
that resolves a signed S3 URL via the tRPC downloads.getDownloadUrl
endpoint.

Extract the download-resolution logic into a shared, testable
downloadAsset helper (src/lib/download-asset) so current and future
subdomain landing pages (InputHalo, Lineage, …) share one code path.
The helper is pure over an injected DownloadApi + redirect sink, keeping
it unit-testable without importing solid-js / CSRF cookie code.

Replace the old marketing/gaze route with a permanent 308 redirect to
https://gaze.freno.me so existing inbound links keep resolving to the
canonical home.

Add unit tests covering the tRPC call shape, redirect sink, and error
handling.
2026-07-23 12:15:04 -04:00

176 lines
6.6 KiB
TypeScript

import { createSignal } from "solid-js";
import { PageHead } from "~/components/PageHead";
import DownloadOnAppStoreDark from "~/components/icons/DownloadOnAppStoreDark";
import Button from "~/components/ui/Button";
import { useDarkMode } from "~/context/darkMode";
import { downloadAsset } from "~/lib/download-asset";
const GAZE_APP_STORE_URL =
"https://apps.apple.com/us/app/gaze/id6757759498";
const GAZE_MIN_MACOS = "13.0";
const FEATURES = [
{
title: "Take regular breaks",
body: "Gentle, dismissable reminders nudge you to step away from the screen so you can rest and reset."
},
{
title: "Eye health reminders",
body: "Follow the 20-20-20 rule — every 20 minutes, look at something 20 feet away for 20 seconds."
},
{
title: "Posture reminders",
body: "Periodic check-ins help you catch slouching before it becomes a habit."
},
{
title: "Lives in your menu bar",
body: "A lightweight menu bar app — no dock icon, no intrusive overlays. Just a quiet, reliable companion."
}
] as const;
export default function GazeLanding() {
const { isDark } = useDarkMode();
const [loading, setLoading] = createSignal(false);
const handleDownload = () => {
if (loading()) return;
setLoading(true);
import("~/lib/api")
.then(({ api }) =>
downloadAsset({
api,
assetName: "gaze",
onError: (error) => {
console.error("Gaze download error:", error);
alert("Failed to initiate download. Please try again.");
}
})
)
.finally(() => setLoading(false));
};
return (
<>
<PageHead
title="Eye & posture health reminder for macOS"
description="Gaze is a macOS menu bar app for eye and posture health — gentle reminders to take breaks, rest your eyes, and sit up straight. Download Gaze for macOS."
ogImage="/look-away.png"
ogTitle="Gaze — Eye and posture health reminder for macOS"
ogDescription="A macOS menu bar app that helps you remember to take breaks and rest your eyes."
/>
{/* ── Hero ─────────────────────────────────────────────────────── */}
<div class="relative flex min-h-screen flex-col">
<div class="fixed inset-0 z-0 overflow-hidden brightness-75">
<img
src="/look-away.png"
alt="Look away — Gaze hero background"
class="h-full w-full select-none object-cover"
style={{ "pointer-events": "none" }}
/>
</div>
<div class="relative z-10 flex min-h-screen flex-col items-center justify-center px-4 py-24 text-center text-white backdrop-blur-sm">
<img
src={
isDark()
? "/Gaze Exports/Gaze-iOS-Dark-1024x1024@1x.png"
: "/Gaze Exports/Gaze-iOS-Default-1024x1024@1x.png"
}
alt="Gaze App Icon"
height={128}
width={128}
class="h-32 w-32 rounded-[22%] object-cover object-center shadow-2xl"
/>
<h1 class="py-4 text-5xl font-bold tracking-tight">Gaze</h1>
<p class="mb-2 max-w-xl text-xl text-white/90">
Eye and posture health reminder for macOS
</p>
<p class="mb-8 text-sm text-white/60">
macOS {GAZE_MIN_MACOS}+ · menu bar app
</p>
<div class="flex flex-col items-center gap-4 sm:flex-row sm:space-x-4">
<Button
variant="download"
size="lg"
loading={loading()}
onClick={handleDownload}
>
download.dmg
</Button>
<a
class="my-auto transition-all duration-200 ease-out hover:scale-105 active:scale-95"
href={GAZE_APP_STORE_URL}
target="_blank"
rel="noopener noreferrer"
>
<DownloadOnAppStoreDark size={50} />
</a>
</div>
<p class="mt-3 text-xs text-white/50">
Direct download serves the latest signed macOS build.
</p>
</div>
</div>
{/* ── Feature highlights ───────────────────────────────────────── */}
<section class="bg-base relative z-20 px-4 py-20 md:px-8">
<div class="mx-auto max-w-4xl">
<h2 class="text-text mb-12 text-center text-3xl font-bold">
Small reminders, healthier habits
</h2>
<div class="grid grid-cols-1 gap-8 sm:grid-cols-2">
{FEATURES.map((feature) => (
<div
class="border-overlay0 bg-surface0 rounded-lg border p-6"
>
<h3 class="text-text mb-2 text-xl font-semibold">
{feature.title}
</h3>
<p class="text-subtext0 leading-relaxed">{feature.body}</p>
</div>
))}
</div>
</div>
</section>
{/* ── App preview / 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">
A quieter way to look after yourself
</h2>
<p class="text-subtext0 mx-auto mb-10 max-w-2xl leading-relaxed">
Gaze runs quietly in your menu bar, surfacing a gentle, dismissable
reminder when it's time to look away, stretch, or reset your
posture. No accounts, no clunky dashboards just a steady rhythm
that helps you build better habits.
</p>
<div class="flex flex-col items-center justify-center gap-4 sm:flex-row sm:space-x-4">
<Button
variant="download"
size="lg"
loading={loading()}
onClick={handleDownload}
>
download.dmg
</Button>
<a
class="my-auto transition-all duration-200 ease-out hover:scale-105 active:scale-95"
href={GAZE_APP_STORE_URL}
target="_blank"
rel="noopener noreferrer"
>
<DownloadOnAppStoreDark size={50} />
</a>
</div>
<p class="text-subtext1 mt-6 text-xs">
Requires macOS {GAZE_MIN_MACOS} or later.
</p>
</div>
</section>
</>
);
}