Files
zeekay 1bb5adf1cf The release read names its repository, so /gen can read gen's as /dev reads dev's
components/dev/Release becomes components/product/Release with a repo; /dev
passes hanzoai/dev and draws exactly what it did.
2026-10-16 12:36:19 -07:00

40 lines
1.6 KiB
TypeScript

'use client'
/**
* A repository's latest release, read from GitHub when the page opens: /dev
* names hanzoai/dev's, /gen hanzoai/gen's.
*
* No page carries a copy of a version: it is asked for here, and until GitHub
* answers, or if it does not, nothing is drawn. A reply is held to the shape it
* must have — a version tag and a page of that repository's releases — before a
* word of it reaches the page.
*/
import { useEffect, useState } from 'react'
import { More } from '@hanzo/ui/marketing'
type Latest = { tag: string; url: string }
/** The release a reply names for `repo`, or null when it is not one. */
function latest(repo: string, body: unknown): Latest | null {
if (!body || typeof body !== 'object') return null
const { tag_name: tag, html_url: url } = body as Record<string, unknown>
if (typeof tag !== 'string' || !/^v?\d+\.\d+\.\d+$/.test(tag)) return null
if (typeof url !== 'string' || !url.startsWith(`https://github.com/${repo}/releases/`)) return null
return { tag, url }
}
export function Release({ repo }: { repo: `hanzoai/${string}` }) {
const [release, setRelease] = useState<Latest | null>(null)
useEffect(() => {
const stop = new AbortController()
fetch(`https://api.github.com/repos/${repo}/releases/latest`, { headers: { accept: 'application/vnd.github+json' }, signal: stop.signal })
.then((res) => (res.ok ? res.json() : null))
.then((body: unknown) => setRelease(latest(repo, body)))
.catch(() => {})
return () => stop.abort()
}, [repo])
if (!release) return null
return <More href={release.url}>{`Latest release ${release.tag} →`}</More>
}