feat: add InputHalo landing page with DMG download flow

Add a marketing landing page for inputhalo.freno.me with a hero section
(theme-aware app icon, glitch title, tagline), feature highlights, and a
download section offering a macOS DMG via the tRPC signed-S3 URL endpoint
plus an App Store link.

Extract the download orchestration into a pure, side-effect-free helper
(query/performInputHaloDownload) so the asset-name and redirect contract is
unit-tested in isolation, mirroring the existing site-aware page-head /
nav-config split. Include dark and default app-icon exports in the public
folder.
This commit is contained in:
2026-07-23 14:32:06 -04:00
parent 6fdbbbbe2b
commit 06c39b8813
5 changed files with 430 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
/**
* Pure, side-effect-free download orchestration for the InputHalo landing
* page (task 06).
*
* Extracted from the route component so the acceptance criterion —
* "download button calls `api.downloads.getDownloadUrl` with `'inputhalo'`
* and redirects to the signed S3 URL" — can be unit-tested in `bun:test`
* without importing solid-js / `@solidjs/router` / `@solidjs/meta` (the same
* pattern established by `~/components/page-head-meta.ts` and
* `~/lib/nav-config.ts`).
*
* The route component (`./index.tsx`) is a thin wrapper that supplies the
* real `api` (the tRPC client) and a `redirect` that mutates
* `window.location.href`, plus loading/error UX. The data-flow contract
* lives here.
*/
/** tRPC asset name for the InputHalo macOS DMG (matches `downloads.tsx`). */
export const INPUTHALO_ASSET_NAME = "inputhalo" as const;
/** App Store listing for InputHalo (paid variant — "coming soon"). */
export const INPUTHALO_APP_STORE_URL =
"https://apps.apple.com/us/app/inputhalo/" as const;
/** Minimum macOS version supported by InputHalo (per Info.plist). */
export const INPUTHALO_MIN_SYSTEM_VERSION = "14.6" as const;
/** Public asset paths for the app icon, switched on dark/light theme. */
export const INPUTHALO_ICON_DARK =
"/InputHalo Exports/InputHalo-iOS-Dark-1024x1024@1x.png" as const;
export const INPUTHALO_ICON_DEFAULT =
"/InputHalo Exports/InputHalo-iOS-Default-1024x1024@1x.png" as const;
/**
* Structural type of the slice of the tRPC client this helper consumes.
* Keeps the helper decoupled from the full `api` surface and testable with a
* stub.
*/
export interface DownloadQueryApi {
(input: { asset_name: string }): Promise<{ downloadURL: string }>;
}
/**
* Resolve the signed S3 download URL for the InputHalo DMG.
*
* Pure: issues the query via the supplied `api` callable and returns the
* resulting `downloadURL`. Throws if the underlying query rejects — the
* caller owns the user-facing error UX.
*
* @example
* ```ts
* const url = await queryInputHaloDownload(
* (input) => api.downloads.getDownloadUrl.query(input)
* );
* ```
*/
export async function queryInputHaloDownload(
query: DownloadQueryApi
): Promise<string> {
const data = await query({ asset_name: INPUTHALO_ASSET_NAME });
return data.downloadURL;
}
/**
* Drive the full DMG download flow: call the tRPC endpoint with the
* InputHalo asset name, then hand the signed URL to `redirect`.
*
* Returns a boolean indicating success so callers can branch their loading-
* state cleanup without a try/catch (errors are caught internally and surfaced
* via the `onError` callback — keeps the component body tidy).
*
* @param query tRPC query callable (the `api.downloads.getDownloadUrl`
* bound method).
* @param redirect Side-effect invoked with the signed S3 URL (typically
* `(url) => { window.location.href = url; }`).
* @param onError Optional error sink (defaults to `console.error`).
* @returns `true` on success, `false` if the query rejected.
*/
export async function performInputHaloDownload(
query: DownloadQueryApi,
redirect: (url: string) => void,
onError?: (error: unknown) => void
): Promise<boolean> {
try {
const url = await queryInputHaloDownload(query);
redirect(url);
return true;
} catch (error) {
(onError ?? console.error)("InputHalo download error:", error);
return false;
}
}