Files
opencode/packages/console/app/src/routes/changelog/index.tsx
2026-02-06 08:54:51 -06:00

176 lines
5.7 KiB
TypeScript

import "./index.css"
import { Title, Meta, Link } from "@solidjs/meta"
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 { changelog } from "~/lib/changelog"
import type { HighlightGroup } from "~/lib/changelog"
import { For, Show, createSignal } from "solid-js"
import { useI18n } from "~/context/i18n"
import { useLanguage } from "~/context/language"
function formatDate(dateString: string, locale: string) {
const date = new Date(dateString)
return date.toLocaleDateString(locale, {
year: "numeric",
month: "short",
day: "numeric",
})
}
function ReleaseItem(props: { item: string }) {
const parts = () => {
const match = props.item.match(/^(.+?)(\s*\(@([\w-]+)\))?$/)
if (match) {
return {
text: match[1],
username: match[3],
}
}
return { text: props.item, username: undefined }
}
return (
<li>
<span>{parts().text}</span>
<Show when={parts().username}>
<a data-slot="author" href={`https://github.com/${parts().username}`} target="_blank" rel="noopener noreferrer">
(@{parts().username})
</a>
</Show>
</li>
)
}
function HighlightSection(props: { group: HighlightGroup }) {
return (
<div data-component="highlight">
<h4>{props.group.source}</h4>
<hr />
<For each={props.group.items}>
{(item) => (
<div data-slot="highlight-item">
<p data-slot="title">{item.title}</p>
<p>{item.description}</p>
<Show when={item.media.type === "video"}>
<video src={item.media.src} controls autoplay loop muted playsinline />
</Show>
<Show when={item.media.type === "image"}>
<img
src={item.media.src}
alt={item.title}
width={(item.media as { width: string }).width}
height={(item.media as { height: string }).height}
/>
</Show>
</div>
)}
</For>
</div>
)
}
function CollapsibleSection(props: { section: { title: string; items: string[] } }) {
const [open, setOpen] = createSignal(false)
return (
<div data-component="collapsible-section">
<button data-slot="toggle" onClick={() => setOpen(!open())}>
<span data-slot="icon">{open() ? "▾" : "▸"}</span>
<span>{props.section.title}</span>
</button>
<Show when={open()}>
<ul>
<For each={props.section.items}>{(item) => <ReleaseItem item={item} />}</For>
</ul>
</Show>
</div>
)
}
function CollapsibleSections(props: { sections: { title: string; items: string[] }[] }) {
return (
<div data-component="collapsible-sections">
<For each={props.sections}>{(section) => <CollapsibleSection section={section} />}</For>
</div>
)
}
export default function Changelog() {
const i18n = useI18n()
const language = useLanguage()
const data = createAsync(() => changelog())
const releases = () => data() ?? []
return (
<main data-page="changelog">
<Title>{i18n.t("changelog.title")}</Title>
<Link rel="canonical" href={`${config.baseUrl}/changelog`} />
<Meta name="description" content={i18n.t("changelog.meta.description")} />
<div data-component="container">
<Header />
<div data-component="content">
<section data-component="changelog-hero">
<h1>{i18n.t("changelog.hero.title")}</h1>
<p>{i18n.t("changelog.hero.subtitle")}</p>
</section>
<section data-component="releases">
<Show when={releases().length === 0}>
<p>
{i18n.t("changelog.empty")} <a href="/changelog.json">{i18n.t("changelog.viewJson")}</a>
</p>
</Show>
<For each={releases()}>
{(release) => {
return (
<article data-component="release">
<header>
<div data-slot="version">
<a href={release.url} target="_blank" rel="noopener noreferrer">
{release.tag}
</a>
</div>
<time dateTime={release.date}>{formatDate(release.date, language.tag(language.locale()))}</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>
</div>
<Footer />
</div>
<Legal />
</main>
)
}