TorbatYar/frontend/modules/commercial/components/RegistryBrowser.tsx

185 lines
6.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import type { AdapterResult, RegistryAvailability } from "../types";
import { CommercialEmptyState } from "./CommercialEmptyState";
import {
fieldLabel,
friendlyMessage,
humanizeCommercialCode,
isTechnicalField,
presentValue,
} from "../presentation";
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 "";
}
}
function PresentedValue({ value }: { value: unknown }) {
const presented = presentValue(value);
if (Array.isArray(presented)) {
return (
<div className="flex flex-wrap gap-1.5">
{presented.map((item, index) => (
<span key={`${item}-${index}`} className="rounded-full bg-gray-100 px-2.5 py-1 text-xs text-gray-700">
{item}
</span>
))}
</div>
);
}
if (presented && typeof presented === "object") {
const entries = Object.entries(presented).filter(([key]) => !isTechnicalField(key));
return (
<details className="rounded-xl border border-gray-100 bg-gray-50 p-3">
<summary className="cursor-pointer text-xs font-semibold text-secondary">مشاهده جزئیات</summary>
<dl className="mt-3 grid gap-3 sm:grid-cols-2">
{entries.map(([key, nested]) => (
<div key={key}>
<dt className="text-[11px] text-gray-500">{fieldLabel(key)}</dt>
<dd className="mt-1 text-xs text-secondary"><PresentedValue value={nested} /></dd>
</div>
))}
</dl>
</details>
);
}
return <>{presented}</>;
}
/** Generic human-facing catalog list. Implementation identifiers are deliberately hidden. */
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={friendlyMessage(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 explicitName = typeof item[nameKey] === "string" ? String(item[nameKey]) : null;
const name = humanizeCommercialCode(code, explicitName);
const meta = item.metadata;
const detailEntries = Object.entries(item).filter(
([key, value]) =>
key !== codeKey &&
key !== nameKey &&
key !== "metadata" &&
!isTechnicalField(key) &&
value != null
);
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>
{detailEntries.length ? (
<dl className="mt-3 space-y-2">
{detailEntries.slice(0, 4).map(([key, value]) => (
<div key={key} className="flex items-start justify-between gap-3 text-xs">
<dt className="text-gray-500">{fieldLabel(key)}</dt>
<dd className="text-left text-secondary"><PresentedValue value={value} /></dd>
</div>
))}
</dl>
) : null}
{meta && typeof meta === "object" && !Array.isArray(meta) ? (
<div className="mt-3"><PresentedValue value={meta} /></div>
) : 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={friendlyMessage(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.filter(([key]) => !isTechnicalField(key)).map(([key, value]) => (
<div key={key} className="rounded-xl bg-gray-50 p-3 text-xs">
<dt className="text-gray-500">{fieldLabel(key)}</dt>
<dd className="mt-2 text-secondary">
<PresentedValue value={value} />
</dd>
</div>
))}
</dl>
</div>
);
}