Files
ZacharyZhang-NY 49ed1f8405
CI / crate (push) Failing after 2s
CI / site (push) Successful in 30s
CI / platforms (ubuntu-24.04) (push) Failing after 1m48s
CI / platforms (macos-15-intel) (push) Canceled after 0s
CI / platforms (ubuntu-24.04-arm) (push) Canceled after 0s
CI / platforms (windows-11-arm) (push) Canceled after 0s
CI / platforms (windows-latest) (push) Failing after 15s
Site and gallery: the repo and the showcase catalog live on Gitea at git.zacharyzhang.com
Links, the header's Gitea mark, the catalog's archive download and the showcase pin
workflow (Gitea's API and commit status) move off GitHub; pinned at 6973834, which
restores TokenLedger.
2026-10-09 16:20:32 -04:00

62 lines
2.2 KiB
TypeScript

/** An app from the showcase catalog, as `scripts/showcase.mjs` writes it. */
export interface App {
id: string;
name: string;
author: string;
description: string;
category: string;
platforms: string[];
website: string | null;
source: string | null;
publishedAt: string;
previews: string[];
building?: boolean;
stars?: number;
starsUpdatedAt?: string;
/** The README, rendered; null without one. */
html: string | null;
}
export interface Catalog {
source: string;
categories: Record<string, string>;
featured: string[];
apps: App[];
}
export type Sort = "newest" | "stars";
export const SUBMIT = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Showcases#submit-an-app";
/** Newest first, then most stars; or most stars, then newest. */
export function sorted(apps: App[], sort: Sort): App[] {
const time = (a: App) => Date.parse(a.publishedAt);
const stars = (a: App) => a.stars ?? -1;
const [first, second] = sort === "newest" ? [time, stars] : [stars, time];
return [...apps].sort((a, b) => first(b) - first(a) || second(b) - second(a) || a.name.localeCompare(b.name));
}
/** The featured apps in their order, and the rest. */
export function split(catalog: Catalog): { featured: App[]; rest: App[] } {
const byId = new Map(catalog.apps.map((a) => [a.id, a]));
const featured = catalog.featured.map((id) => {
const app = byId.get(id);
if (!app) throw new Error(`showcase: featured ${id} is no app`);
return app;
});
return { featured, rest: catalog.apps.filter((a) => !catalog.featured.includes(a.id)) };
}
/** Apps in a category, or all with none, whose name, author or description holds the query. */
export function filtered(apps: App[], query: string, category: string | null): App[] {
const q = query.trim().toLowerCase();
return apps.filter((a) => (!category || a.category === category) && (!q || [a.name, a.author, a.description].some((s) => s.toLowerCase().includes(q))));
}
/** How many apps each category holds. */
export function counts(catalog: Catalog): [string, string, number][] {
return Object.entries(catalog.categories).map(([key, label]) => [key, label, catalog.apps.filter((a) => a.category === key).length]);
}
export const cover = (app: App) => `/showcase/${app.id}/${app.previews[0]}`;