forked from Mike/freno-dev
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.
93 lines
3.4 KiB
TypeScript
93 lines
3.4 KiB
TypeScript
/**
|
|
* 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;
|
|
}
|
|
}
|