Files
EpicNext-Cms/src/features/housekeeping/domains/content/pages/content-page-frame.tsx
T

100 lines
2.9 KiB
TypeScript

import type { ReactNode } from "react";
import type { HousekeepingResult } from "../../../foundation/contracts";
import type { ContentQueryData } from "../queries/content-queries";
export interface ContentPageProps {
readonly title?: string;
readonly search?: string;
readonly context: import("../../../foundation/contracts").HousekeepingCapabilityContext;
readonly result?: HousekeepingResult<ContentQueryData>;
readonly routeId: import("../routes").ContentRouteId | null;
}
export function ContentPageFrame({
title,
description,
result,
forms,
}: {
readonly title: string;
readonly description: string;
readonly result?: HousekeepingResult<ContentQueryData>;
readonly forms?: ReactNode;
}) {
if (!result) {
return (
<section data-housekeeping-state="loading">
<h1>{title}</h1>
<p>{description}</p>
<p>Loading content…</p>
</section>
);
}
if (!result.ok) {
const state = result.error.code === "FORBIDDEN" ? "forbidden" : "error";
return (
<section data-housekeeping-state={state} role="alert">
<h1>{title}</h1>
<p>{result.error.messageKey}</p>
</section>
);
}
const state = result.data.items.length === 0 ? "empty" : "ready";
return (
<section data-housekeeping-state={state} className="space-y-4">
<header>
<h1>{title}</h1>
<p>{description}</p>
</header>
{forms}
{result.data.items.length === 0 ? (
<p>No matching content.</p>
) : (
<ul className="divide-y divide-[var(--admin-border)]">
{result.data.items.map((item) => (
<li key={item.id} className="py-2">
{item.href ? <a href={item.href}>{item.title}</a> : item.title}
{item.status ? <span> — {item.status}</span> : null}
{item.description ? <p>{item.description}</p> : null}
</li>
))}
</ul>
)}
</section>
);
}
export function parseContentListInput(
searchParams: Readonly<
Record<string, string | readonly string[] | undefined>
>,
) {
const first = (value: string | readonly string[] | undefined) =>
Array.isArray(value) ? value[0] : value;
const text = (value: string | readonly string[] | undefined, limit: number) =>
String(first(value) ?? "")
.normalize("NFC")
.trim()
.slice(0, limit);
const bounded = (
value: string | readonly string[] | undefined,
fallback: number,
minimum: number,
maximum: number,
) => {
const parsed = Number(first(value));
return Number.isSafeInteger(parsed)
? Math.min(maximum, Math.max(minimum, parsed))
: fallback;
};
return {
search: text(searchParams.search, 128),
status: text(searchParams.status, 32).toLocaleLowerCase(),
pageSize: bounded(searchParams.pageSize, 25, 1, 100),
offset: bounded(searchParams.offset, 0, 0, 100_000),
responseQuestionId: text(searchParams.responseQuestionId, 32),
responsePageSize: bounded(searchParams.responsePageSize, 25, 1, 50),
responseOffset: bounded(searchParams.responseOffset, 0, 0, 100_000),
};
}