139 lines
4.3 KiB
TypeScript
139 lines
4.3 KiB
TypeScript
"use client";
|
|
|
|
import type { AdapterResult, RegistryAvailability } from "../types";
|
|
import { CommercialEmptyState } from "./CommercialEmptyState";
|
|
|
|
function availabilityLabel(a: RegistryAvailability): string {
|
|
switch (a) {
|
|
case "loading":
|
|
return "در حال بارگذاری…";
|
|
case "empty":
|
|
return "خالی";
|
|
case "unavailable":
|
|
return "در دسترس نیست";
|
|
case "permission_denied":
|
|
return "بدون دسترسی";
|
|
case "error":
|
|
return "خطا";
|
|
default:
|
|
return "";
|
|
}
|
|
}
|
|
|
|
/** Generic registry list — no field whitelist; renders codes + display + optional metadata keys. */
|
|
export function RegistryBrowser({
|
|
title,
|
|
subtitle,
|
|
result,
|
|
codeKey,
|
|
nameKey = "display_name",
|
|
emptyTitle,
|
|
}: {
|
|
title: string;
|
|
subtitle?: string;
|
|
result: AdapterResult<any[]> | null;
|
|
codeKey: string;
|
|
nameKey?: string;
|
|
emptyTitle?: string;
|
|
}) {
|
|
if (!result || result.availability === "loading") {
|
|
return (
|
|
<section>
|
|
<h3 className="text-sm font-semibold text-secondary">{title}</h3>
|
|
<p className="mt-2 text-xs text-gray-500">{availabilityLabel("loading")}</p>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
if (result.availability !== "ready" || !result.data.length) {
|
|
return (
|
|
<section>
|
|
<h3 className="text-sm font-semibold text-secondary">{title}</h3>
|
|
{subtitle ? <p className="mt-1 text-xs text-gray-500">{subtitle}</p> : null}
|
|
<div className="mt-2">
|
|
<CommercialEmptyState
|
|
title={emptyTitle || `${title} — ${availabilityLabel(result.availability)}`}
|
|
description={result.message}
|
|
/>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<section>
|
|
<h3 className="text-sm font-semibold text-secondary">{title}</h3>
|
|
{subtitle ? <p className="mt-1 text-xs text-gray-500">{subtitle}</p> : null}
|
|
<ul className="mt-3 grid gap-2 sm:grid-cols-2">
|
|
{result.data.map((raw, idx) => {
|
|
const item = (raw && typeof raw === "object" ? raw : {}) as Record<string, unknown>;
|
|
const code = String(item[codeKey] ?? idx);
|
|
const name = String(item[nameKey] ?? code);
|
|
const meta = item.metadata;
|
|
const extraKeys =
|
|
meta && typeof meta === "object" && !Array.isArray(meta)
|
|
? Object.keys(meta as Record<string, unknown>).slice(0, 6)
|
|
: [];
|
|
return (
|
|
<li
|
|
key={code}
|
|
className="rounded-xl border border-gray-100 bg-white px-3 py-2 text-sm shadow-sm"
|
|
>
|
|
<p className="font-medium text-secondary">{name}</p>
|
|
<p className="mt-0.5 font-mono text-[10px] text-gray-400" dir="ltr">
|
|
{code}
|
|
</p>
|
|
{extraKeys.length ? (
|
|
<p className="mt-1 text-[10px] text-gray-500" dir="ltr">
|
|
metadata: {extraKeys.join(", ")}
|
|
{Object.keys(meta as object).length > 6 ? "…" : ""}
|
|
</p>
|
|
) : null}
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
/** Renders an arbitrary metadata bag without whitelisting attribute keys. */
|
|
export function MetadataBagView({
|
|
title,
|
|
attributes,
|
|
message,
|
|
availability,
|
|
}: {
|
|
title: string;
|
|
attributes?: Record<string, unknown> | null;
|
|
message?: string;
|
|
availability?: RegistryAvailability;
|
|
}) {
|
|
const entries = attributes ? Object.entries(attributes) : [];
|
|
if (!entries.length) {
|
|
return (
|
|
<CommercialEmptyState
|
|
title={title}
|
|
description={message || availabilityLabel(availability || "empty")}
|
|
/>
|
|
);
|
|
}
|
|
return (
|
|
<div className="rounded-2xl border border-gray-100 bg-white p-4">
|
|
<h3 className="text-sm font-semibold text-secondary">{title}</h3>
|
|
<dl className="mt-3 space-y-2">
|
|
{entries.map(([key, value]) => (
|
|
<div key={key} className="flex flex-wrap gap-2 text-xs">
|
|
<dt className="font-mono text-gray-400" dir="ltr">
|
|
{key}
|
|
</dt>
|
|
<dd className="text-secondary" dir="auto">
|
|
{typeof value === "object" ? JSON.stringify(value) : String(value)}
|
|
</dd>
|
|
</div>
|
|
))}
|
|
</dl>
|
|
</div>
|
|
);
|
|
}
|