150 lines
5.3 KiB
TypeScript
150 lines
5.3 KiB
TypeScript
"use client";
|
||
|
||
import Link from "next/link";
|
||
import type { CommercialProduct } from "../types";
|
||
import { resolveProductLaunchUrl } from "../types";
|
||
import { Button } from "@/components/ui";
|
||
import { FeatureLock } from "./FeatureLock";
|
||
import { pushRecent } from "../launcher-prefs";
|
||
|
||
export function ProductCard({
|
||
product,
|
||
recommendationScore,
|
||
tenantId,
|
||
favorite,
|
||
pinned,
|
||
onToggleFavorite,
|
||
onTogglePinned,
|
||
}: {
|
||
product: CommercialProduct;
|
||
recommendationScore?: number;
|
||
tenantId?: string;
|
||
favorite?: boolean;
|
||
pinned?: boolean;
|
||
onToggleFavorite?: () => void;
|
||
onTogglePinned?: () => void;
|
||
}) {
|
||
const visibilityLocked = product.visibility === "hidden" || product.status === "inactive";
|
||
const launchUrl = resolveProductLaunchUrl(product);
|
||
const strategy = product.launch_strategy || "route";
|
||
const requiredCap = product.required_capabilities?.[0];
|
||
const setupHref = `/setup/${encodeURIComponent(product.product_code)}`;
|
||
|
||
const markRecent = () => {
|
||
pushRecent(product.product_code);
|
||
};
|
||
|
||
const launchButton =
|
||
launchUrl && !visibilityLocked && strategy !== "disabled" ? (
|
||
strategy === "external" ? (
|
||
<a href={launchUrl} target="_blank" rel="noreferrer" onClick={markRecent}>
|
||
<Button size="sm" variant="outline" className="w-full">
|
||
باز کردن
|
||
</Button>
|
||
</a>
|
||
) : (
|
||
<Link href={launchUrl} onClick={markRecent}>
|
||
<Button size="sm" variant="outline" className="w-full">
|
||
باز کردن
|
||
</Button>
|
||
</Link>
|
||
)
|
||
) : null;
|
||
|
||
return (
|
||
<div
|
||
className={`rounded-2xl border p-4 transition hover:shadow-md ${
|
||
visibilityLocked
|
||
? "border-gray-100 bg-gray-50 opacity-70 dark:border-gray-800 dark:bg-gray-900"
|
||
: "border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-900"
|
||
}`}
|
||
>
|
||
<div className="flex items-start gap-3">
|
||
<div
|
||
className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl text-lg font-bold text-white"
|
||
style={{ background: product.color || "var(--color-primary)" }}
|
||
>
|
||
{product.icon || (product.display_name?.charAt(0) ?? "P")}
|
||
</div>
|
||
<div className="min-w-0 flex-1">
|
||
<div className="flex flex-wrap items-center gap-2">
|
||
<h3 className="font-semibold text-secondary dark:text-gray-100">{product.display_name}</h3>
|
||
{product.category ? (
|
||
<span className="text-[10px] text-gray-400">{product.category}</span>
|
||
) : null}
|
||
{typeof recommendationScore === "number" ? (
|
||
<span className="text-[10px] text-primary">امتیاز {recommendationScore}</span>
|
||
) : null}
|
||
</div>
|
||
{product.description ? (
|
||
<p className="mt-1 text-xs leading-5 text-gray-500">{product.description}</p>
|
||
) : null}
|
||
<p className="mt-1 font-mono text-[10px] text-gray-400" dir="ltr">
|
||
{product.product_slug || product.product_code}
|
||
{product.version ? ` @ ${product.version}` : ""}
|
||
{strategy ? ` · ${strategy}` : ""}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
<div className="mt-3 flex flex-wrap gap-2">
|
||
{onToggleFavorite ? (
|
||
<button
|
||
type="button"
|
||
aria-pressed={favorite}
|
||
aria-label={favorite ? "حذف از علاقهمندی" : "افزودن به علاقهمندی"}
|
||
className={`rounded-lg px-2 py-1 text-xs ${
|
||
favorite ? "bg-amber-100 text-amber-800" : "bg-gray-100 text-gray-600 dark:bg-gray-800"
|
||
}`}
|
||
onClick={onToggleFavorite}
|
||
>
|
||
{favorite ? "★" : "☆"}
|
||
</button>
|
||
) : null}
|
||
{onTogglePinned ? (
|
||
<button
|
||
type="button"
|
||
aria-pressed={pinned}
|
||
aria-label={pinned ? "برداشتن پین" : "پین کردن"}
|
||
className={`rounded-lg px-2 py-1 text-xs ${
|
||
pinned ? "bg-sky-100 text-sky-800" : "bg-gray-100 text-gray-600 dark:bg-gray-800"
|
||
}`}
|
||
onClick={onTogglePinned}
|
||
>
|
||
{pinned ? "📌" : "Pin"}
|
||
</button>
|
||
) : null}
|
||
<Link href={setupHref} className="rounded-lg bg-gray-100 px-2 py-1 text-xs text-gray-700 dark:bg-gray-800 dark:text-gray-300">
|
||
راهاندازی
|
||
</Link>
|
||
</div>
|
||
{launchButton ? (
|
||
<div className="mt-3">
|
||
{tenantId && requiredCap ? (
|
||
<FeatureLock
|
||
tenantId={tenantId}
|
||
featureKey={requiredCap}
|
||
fallback={
|
||
<Button size="sm" variant="outline" className="w-full">
|
||
قفل — ارتقا لازم
|
||
</Button>
|
||
}
|
||
>
|
||
{launchButton}
|
||
</FeatureLock>
|
||
) : (
|
||
launchButton
|
||
)}
|
||
</div>
|
||
) : (
|
||
<div className="mt-3">
|
||
<Link href={setupHref}>
|
||
<Button size="sm" variant="outline" className="w-full">
|
||
شروع راهاندازی
|
||
</Button>
|
||
</Link>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|