Enhance documentation and admin UI: Add detailed implementation guidelines in CLAUDE.md, introduce a referrer index in README.md, and update admin UI translations for improved user experience. Update package dependencies for better functionality and performance.

This commit is contained in:
Peter Meier
2026-03-13 10:55:33 +01:00
parent 7754d800f5
commit 606455c59b
42 changed files with 3814 additions and 421 deletions

View File

@@ -7,6 +7,9 @@ export const getBaseUrl = () =>
process.env.NEXT_PUBLIC_RUSTYCMS_API_URL || "http://127.0.0.1:3000";
const STORAGE_KEY = "rustycms_admin_api_key";
const PER_PAGE_KEY = "rustycms_per_page";
const DEFAULT_PER_PAGE = 25;
const PER_PAGE_OPTIONS = [10, 25, 50, 100] as const;
/** Client-side only: key set by login when no env key. */
let clientApiKey: string | null = null;
@@ -38,6 +41,47 @@ export function syncStoredApiKey(): void {
if (stored) clientApiKey = stored;
}
/** Items per page for content lists (stored in localStorage). */
export function getPerPage(): number {
if (typeof window === "undefined") return DEFAULT_PER_PAGE;
const v = localStorage.getItem(PER_PAGE_KEY);
const n = v ? parseInt(v, 10) : NaN;
return PER_PAGE_OPTIONS.includes(n as (typeof PER_PAGE_OPTIONS)[number])
? n
: DEFAULT_PER_PAGE;
}
export function setPerPage(n: number): void {
if (typeof window === "undefined") return;
if (PER_PAGE_OPTIONS.includes(n as (typeof PER_PAGE_OPTIONS)[number])) {
localStorage.setItem(PER_PAGE_KEY, String(n));
}
}
export { PER_PAGE_OPTIONS, DEFAULT_PER_PAGE };
/** Clear API key and all rustycms_* localStorage (e.g. for shared devices). */
export function clearSession(): void {
setApiKey(null);
if (typeof window === "undefined") return;
const keys: string[] = [];
for (let i = 0; i < localStorage.length; i++) {
const k = localStorage.key(i);
if (k?.startsWith("rustycms_")) keys.push(k);
}
keys.forEach((k) => localStorage.removeItem(k));
}
/** Check backend health (GET /health). */
export async function fetchHealth(): Promise<{ ok: boolean; status?: number }> {
try {
const res = await fetch(`${getBaseUrl()}/health`, { cache: "no-store" });
return { ok: res.ok, status: res.status };
} catch {
return { ok: false };
}
}
const getHeaders = (): HeadersInit => {
const headers: HeadersInit = {
"Content-Type": "application/json",
@@ -68,11 +112,19 @@ export type FieldDefinition = {
description?: string;
collection?: string;
collections?: string[];
/** Optional whitelist of allowed slugs for reference fields. Only these slugs are valid. */
allowedSlugs?: string[];
/** Optional whitelist of allowed content types (collections). Only these collections are valid (intersection with collection/collections). */
allowedCollections?: string[];
enum?: unknown[];
default?: unknown;
items?: FieldDefinition;
/** Optional section key for grouping fields in the admin UI (collapsible blocks). */
section?: string;
/** Optional hint for admin UI (e.g. "textarea" for string → multi-line input, "code" for code field with syntax highlighting). */
widget?: string;
/** When widget is "code", language for syntax highlighting: "css", "javascript", "json", "html". */
codeLanguage?: string;
[key: string]: unknown;
};
@@ -221,6 +273,25 @@ export async function fetchEntry<T = Record<string, unknown>>(
return res.json();
}
/** Referrer: an entry that references another (from GET .../referrers). */
export type Referrer = {
collection: string;
slug: string;
field: string;
locale?: string | null;
};
export async function fetchReferrers(
collection: string,
slug: string
): Promise<Referrer[]> {
const url = `${getBaseUrl()}/api/content/${encodeURIComponent(collection)}/${encodeURIComponent(slug)}/referrers`;
const res = await fetch(url, { headers: getHeaders() });
if (!res.ok) return [];
const data = await res.json();
return Array.isArray(data) ? data : [];
}
export async function createEntry(
collection: string,
data: Record<string, unknown>,