TorbatYar/frontend/modules/commercial/components/RegistryBrowser.tsx
2026-07-28 20:39:10 +03:30

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>
);
}