diff options
| author | adamelmore <[email protected]> | 2026-01-26 09:53:03 -0600 |
|---|---|---|
| committer | adamelmore <[email protected]> | 2026-01-26 09:53:10 -0600 |
| commit | 3dce6a6608a4745cbf05670dbb57f2312f950acc (patch) | |
| tree | 790d2dc3412522f85c456cd95e3750eb6dee3e3b /packages/console/app/src | |
| parent | 39a73d4894bf7bda69a95b7d5572d5c7c24dd7ee (diff) | |
| download | opencode-3dce6a6608a4745cbf05670dbb57f2312f950acc.tar.gz opencode-3dce6a6608a4745cbf05670dbb57f2312f950acc.zip | |
chore: gen changelog page off changelog json
Diffstat (limited to 'packages/console/app/src')
| -rw-r--r-- | packages/console/app/src/routes/changelog.json.ts | 14 | ||||
| -rw-r--r-- | packages/console/app/src/routes/changelog/index.tsx | 134 |
2 files changed, 42 insertions, 106 deletions
diff --git a/packages/console/app/src/routes/changelog.json.ts b/packages/console/app/src/routes/changelog.json.ts index a9667ac6d..9e3b75e5c 100644 --- a/packages/console/app/src/routes/changelog.json.ts +++ b/packages/console/app/src/routes/changelog.json.ts @@ -98,19 +98,23 @@ export async function GET() { cacheTtl: 60 * 5, cacheEverything: true, }, - } as any) + } as any).catch(() => undefined) - if (!response.ok) { - return new Response(JSON.stringify({ releases: [] }), { + const fail = () => + new Response(JSON.stringify({ releases: [] }), { status: 503, headers: { "Content-Type": "application/json", "Cache-Control": error, }, }) - } - const releases = (await response.json()) as Release[] + if (!response?.ok) return fail() + + const data = await response.json().catch(() => undefined) + if (!Array.isArray(data)) return fail() + + const releases = data as Release[] return new Response( JSON.stringify({ diff --git a/packages/console/app/src/routes/changelog/index.tsx b/packages/console/app/src/routes/changelog/index.tsx index 87e021ec8..e05ad42e6 100644 --- a/packages/console/app/src/routes/changelog/index.tsx +++ b/packages/console/app/src/routes/changelog/index.tsx @@ -1,44 +1,12 @@ import "./index.css" import { Title, Meta, Link } from "@solidjs/meta" -import { createAsync, query } from "@solidjs/router" +import { createAsync } from "@solidjs/router" import { Header } from "~/component/header" import { Footer } from "~/component/footer" import { Legal } from "~/component/legal" import { config } from "~/config" import { For, Show, createSignal } from "solid-js" - -type Release = { - tag_name: string - name: string - body: string - published_at: string - html_url: string -} - -const getReleases = query(async () => { - "use server" - const response = await fetch("https://api.github.com/repos/anomalyco/opencode/releases?per_page=20", { - headers: { - Accept: "application/vnd.github.v3+json", - "User-Agent": "OpenCode-Console", - }, - cf: { - cacheTtl: 60 * 5, - cacheEverything: true, - }, - } as any) - if (!response.ok) return [] - return response.json() as Promise<Release[]> -}, "releases.get") - -function formatDate(dateString: string) { - const date = new Date(dateString) - return date.toLocaleDateString("en-US", { - year: "numeric", - month: "short", - day: "numeric", - }) -} +import { getRequestEvent } from "solid-js/web" type HighlightMedia = { type: "video"; src: string } | { type: "image"; src: string; width: string; height: string } @@ -54,68 +22,33 @@ type HighlightGroup = { items: HighlightItem[] } -function parseHighlights(body: string): HighlightGroup[] { - const groups = new Map<string, HighlightItem[]>() - const regex = /<highlight\s+source="([^"]+)">([\s\S]*?)<\/highlight>/g - let match - - while ((match = regex.exec(body)) !== null) { - const source = match[1] - const content = match[2] - - const titleMatch = content.match(/<h2>([^<]+)<\/h2>/) - const pMatch = content.match(/<p(?:\s+short="([^"]*)")?>([^<]+)<\/p>/) - const imgMatch = content.match(/<img\s+width="([^"]+)"\s+height="([^"]+)"\s+alt="[^"]*"\s+src="([^"]+)"/) - const videoMatch = content.match(/^\s*(https:\/\/github\.com\/user-attachments\/assets\/[a-f0-9-]+)\s*$/m) - - let media: HighlightMedia | undefined - if (videoMatch) { - media = { type: "video", src: videoMatch[1] } - } else if (imgMatch) { - media = { type: "image", src: imgMatch[3], width: imgMatch[1], height: imgMatch[2] } - } - - if (titleMatch && media) { - const item: HighlightItem = { - title: titleMatch[1], - description: pMatch?.[2] || "", - shortDescription: pMatch?.[1], - media, - } - - if (!groups.has(source)) { - groups.set(source, []) - } - groups.get(source)!.push(item) - } - } - - return Array.from(groups.entries()).map(([source, items]) => ({ source, items })) +type ChangelogRelease = { + tag: string + name: string + date: string + url: string + highlights: HighlightGroup[] + sections: { title: string; items: string[] }[] } -function parseMarkdown(body: string) { - const lines = body.split("\n") - const sections: { title: string; items: string[] }[] = [] - let current: { title: string; items: string[] } | null = null - let skip = false +async function getReleases() { + const event = getRequestEvent() + const url = event ? new URL("/changelog.json", event.request.url).toString() : "/changelog.json" - for (const line of lines) { - if (line.startsWith("## ")) { - if (current) sections.push(current) - const title = line.slice(3).trim() - current = { title, items: [] } - skip = false - } else if (line.startsWith("**Thank you")) { - skip = true - } else if (line.startsWith("- ") && !skip) { - current?.items.push(line.slice(2).trim()) - } - } - if (current) sections.push(current) + const response = await fetch(url).catch(() => undefined) + if (!response?.ok) return [] - const highlights = parseHighlights(body) + const json = await response.json().catch(() => undefined) + return Array.isArray(json?.releases) ? (json.releases as ChangelogRelease[]) : [] +} - return { sections, highlights } +function formatDate(dateString: string) { + const date = new Date(dateString) + return date.toLocaleDateString("en-US", { + year: "numeric", + month: "short", + day: "numeric", + }) } function ReleaseItem(props: { item: string }) { @@ -217,28 +150,27 @@ export default function Changelog() { <section data-component="releases"> <For each={releases()}> {(release) => { - const parsed = () => parseMarkdown(release.body || "") return ( <article data-component="release"> <header> <div data-slot="version"> - <a href={release.html_url} target="_blank" rel="noopener noreferrer"> - {release.tag_name} + <a href={release.url} target="_blank" rel="noopener noreferrer"> + {release.tag} </a> </div> - <time dateTime={release.published_at}>{formatDate(release.published_at)}</time> + <time dateTime={release.date}>{formatDate(release.date)}</time> </header> <div data-slot="content"> - <Show when={parsed().highlights.length > 0}> + <Show when={release.highlights.length > 0}> <div data-component="highlights"> - <For each={parsed().highlights}>{(group) => <HighlightSection group={group} />}</For> + <For each={release.highlights}>{(group) => <HighlightSection group={group} />}</For> </div> </Show> - <Show when={parsed().highlights.length > 0 && parsed().sections.length > 0}> - <CollapsibleSections sections={parsed().sections} /> + <Show when={release.highlights.length > 0 && release.sections.length > 0}> + <CollapsibleSections sections={release.sections} /> </Show> - <Show when={parsed().highlights.length === 0}> - <For each={parsed().sections}> + <Show when={release.highlights.length === 0}> + <For each={release.sections}> {(section) => ( <div data-component="section"> <h3>{section.title}</h3> |
