import { hash, normalize } from "./utils";
import type { Options, Item, Result } from "./types";
type Status = "idle" | "pending" | "done";
const config: Options = {
retries: 3,
timeout: 8_000,
cache: new Map(),
};
export function clamp(n: number, lo: number, hi: number) {
return Math.max(lo, Math.min(hi, n));
}
export const slugify = (s: string) =>
s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
async function load(key: string): Promise<Result> {
const cached = config.cache.get(key);
if (cached !== undefined) return cached;
const res = await fetch("/api/items/" + key);
if (!res.ok) throw new Error("status " + res.status);
const data = await res.json();
config.cache.set(key, data);
return data;
}
function reducer(state, action) {
switch (action.type) {
case "load": return { ...state, status: "pending" };
case "ready": return { ...state, status: "done", data: action.payload };
case "reset": return initial;
default: return state;
}
}
for (const item of items) {
if (!item.active) continue;
totals[item.kind] = (totals[item.kind] ?? 0) + item.value;
}
const visible = items
.filter((it) => it.visible)
.sort((a, b) => a.order - b.order);
interface Item {
id: string;
kind: "row" | "card" | "tile";
order: number;
active: boolean;
}
export function debounce(fn, ms = 120) {
let id;
return (...args) => {
if (id) clearTimeout(id);
id = setTimeout(() => fn(...args), ms);
};
}
// sort by display order, then by id
items.sort((a, b) => a.order - b.order || a.id.localeCompare(b.id));
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 14px; }
.card[data-state="active"] { border-color: var(--accent); }
@media (min-width: 768px) {
.grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
<section class="grid">
<article class="card" data-id={id}>
<header>{title}</header>
<p>{summary}</p>
</article>
</section>
class Store {
private state = initial;
subscribe(fn) {
this.listeners.add(fn);
return () => this.listeners.delete(fn);
}
} import { hash, normalize } from "./utils";
import type { Options, Item, Result } from "./types";
type Status = "idle" | "pending" | "done";
const config: Options = {
retries: 3,
timeout: 8_000,
cache: new Map(),
};
export function clamp(n: number, lo: number, hi: number) {
return Math.max(lo, Math.min(hi, n));
}
export const slugify = (s: string) =>
s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
async function load(key: string): Promise<Result> {
const cached = config.cache.get(key);
if (cached !== undefined) return cached;
const res = await fetch("/api/items/" + key);
if (!res.ok) throw new Error("status " + res.status);
const data = await res.json();
config.cache.set(key, data);
return data;
}
function reducer(state, action) {
switch (action.type) {
case "load": return { ...state, status: "pending" };
case "ready": return { ...state, status: "done", data: action.payload };
case "reset": return initial;
default: return state;
}
}
for (const item of items) {
if (!item.active) continue;
totals[item.kind] = (totals[item.kind] ?? 0) + item.value;
}
const visible = items
.filter((it) => it.visible)
.sort((a, b) => a.order - b.order);
interface Item {
id: string;
kind: "row" | "card" | "tile";
order: number;
active: boolean;
}
export function debounce(fn, ms = 120) {
let id;
return (...args) => {
if (id) clearTimeout(id);
id = setTimeout(() => fn(...args), ms);
};
}
// sort by display order, then by id
items.sort((a, b) => a.order - b.order || a.id.localeCompare(b.id));
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 14px; }
.card[data-state="active"] { border-color: var(--accent); }
@media (min-width: 768px) {
.grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
<section class="grid">
<article class="card" data-id={id}>
<header>{title}</header>
<p>{summary}</p>
</article>
</section>
class Store {
private state = initial;
subscribe(fn) {
this.listeners.add(fn);
return () => this.listeners.delete(fn);
}
}