Compare commits
1 Commits
dev
...
changelog-
| Author | SHA1 | Date |
|---|---|---|
|
|
dda80da2c6 |
|
|
@ -90,25 +90,32 @@ export async function GET() {
|
|||
Accept: "application/vnd.github.v3+json",
|
||||
"User-Agent": "OpenCode-Console",
|
||||
},
|
||||
})
|
||||
cf: {
|
||||
cacheTtl: 60 * 5,
|
||||
cacheEverything: true,
|
||||
},
|
||||
} as RequestInit)
|
||||
|
||||
if (!response.ok) {
|
||||
return { releases: [] }
|
||||
return Response.json({ releases: [] }, { status: 502 })
|
||||
}
|
||||
|
||||
const releases = (await response.json()) as Release[]
|
||||
|
||||
return {
|
||||
releases: releases.map((release) => {
|
||||
const parsed = parseMarkdown(release.body || "")
|
||||
return {
|
||||
tag: release.tag_name,
|
||||
name: release.name,
|
||||
date: release.published_at,
|
||||
url: release.html_url,
|
||||
highlights: parsed.highlights,
|
||||
sections: parsed.sections,
|
||||
}
|
||||
}),
|
||||
}
|
||||
return Response.json(
|
||||
{
|
||||
releases: releases.map((release) => {
|
||||
const parsed = parseMarkdown(release.body || "")
|
||||
return {
|
||||
tag: release.tag_name,
|
||||
name: release.name,
|
||||
date: release.published_at,
|
||||
url: release.html_url,
|
||||
highlights: parsed.highlights,
|
||||
sections: parsed.sections,
|
||||
}
|
||||
}),
|
||||
},
|
||||
{ headers: { "Cache-Control": "public, s-maxage=300, stale-while-revalidate=600" } },
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,39 +7,6 @@ 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",
|
||||
})
|
||||
}
|
||||
|
||||
type HighlightMedia = { type: "video"; src: string } | { type: "image"; src: string; width: string; height: string }
|
||||
|
||||
type HighlightItem = {
|
||||
|
|
@ -54,68 +21,30 @@ 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 ParsedRelease = {
|
||||
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
|
||||
const getReleases = query(async () => {
|
||||
"use server"
|
||||
const response = await fetch(`${config.baseUrl}/changelog.json`)
|
||||
if (!response.ok) return []
|
||||
const data = (await response.json()) as { releases: ParsedRelease[] }
|
||||
return data.releases
|
||||
}, "releases.get")
|
||||
|
||||
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 highlights = parseHighlights(body)
|
||||
|
||||
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 }) {
|
||||
|
|
@ -216,43 +145,40 @@ 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>
|
||||
</div>
|
||||
<time dateTime={release.published_at}>{formatDate(release.published_at)}</time>
|
||||
</header>
|
||||
<div data-slot="content">
|
||||
<Show when={parsed().highlights.length > 0}>
|
||||
<div data-component="highlights">
|
||||
<For each={parsed().highlights}>{(group) => <HighlightSection group={group} />}</For>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={parsed().highlights.length > 0 && parsed().sections.length > 0}>
|
||||
<CollapsibleSections sections={parsed().sections} />
|
||||
</Show>
|
||||
<Show when={parsed().highlights.length === 0}>
|
||||
<For each={parsed().sections}>
|
||||
{(section) => (
|
||||
<div data-component="section">
|
||||
<h3>{section.title}</h3>
|
||||
<ul>
|
||||
<For each={section.items}>{(item) => <ReleaseItem item={item} />}</For>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
{(release) => (
|
||||
<article data-component="release">
|
||||
<header>
|
||||
<div data-slot="version">
|
||||
<a href={release.url} target="_blank" rel="noopener noreferrer">
|
||||
{release.tag}
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}}
|
||||
<time dateTime={release.date}>{formatDate(release.date)}</time>
|
||||
</header>
|
||||
<div data-slot="content">
|
||||
<Show when={release.highlights.length > 0}>
|
||||
<div data-component="highlights">
|
||||
<For each={release.highlights}>{(group) => <HighlightSection group={group} />}</For>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={release.highlights.length > 0 && release.sections.length > 0}>
|
||||
<CollapsibleSections sections={release.sections} />
|
||||
</Show>
|
||||
<Show when={release.highlights.length === 0}>
|
||||
<For each={release.sections}>
|
||||
{(section) => (
|
||||
<div data-component="section">
|
||||
<h3>{section.title}</h3>
|
||||
<ul>
|
||||
<For each={section.items}>{(item) => <ReleaseItem item={item} />}</For>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</Show>
|
||||
</div>
|
||||
</article>
|
||||
)}
|
||||
</For>
|
||||
</section>
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue