1
0
Fork 0
polaris-task-force/src/components/frontend/account/AwardListingCard.tsx

149 lines
5.2 KiB
TypeScript

import { Award, Media } from "@/payload-types";
import { Item, ItemContent, ItemDescription, ItemHeader, ItemTitle } from "@/components/ui/item";
import { convertLexicalToPlaintext } from "@payloadcms/richtext-lexical/plaintext";
import { RibbonViewer } from "@/components/frontend/account/RibbonViewer";
import { RibbonDesign } from "@/components/admin/awards/RibbonDesigner";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { AwardIcon, LockIcon } from "lucide-react";
/**
* Card shown on the awards listing page. Unlike the profile's AwardCard
* (which wraps an individual's grant), this reviews a single definable award
* type — ribbon or medal — so users can inspect the award art and its
* full requirements. Commendations are intentionally out of scope.
*
* Ribbon art is rendered at a fixed, centered width so every ribbon (and its
* container) is an identical, justified size across the grid. Medal art is
* currently hidden; only the ribbon art is shown.
*/
interface AwardListingCardProps {
award: Award;
awardImage?: Media | null;
backgroundImage?: Media | null;
showGrantButton?: boolean;
onGrantClick?: () => void;
canGrant?: boolean | null;
remainingGrants?: number | null;
}
export function AwardListingCard({
award,
awardImage,
backgroundImage,
showGrantButton = false,
onGrantClick,
canGrant,
remainingGrants,
}: AwardListingCardProps) {
const type = award.type ?? "commendation";
const isMedal = type === "medal";
const hasRibbonArt =
type === "ribbon" &&
award.ribbonDesign &&
typeof award.ribbonDesign === "object" &&
Array.isArray((award.ribbonDesign as RibbonDesign).stripes);
const hasArt = hasRibbonArt || Boolean(awardImage?.url);
const bgUrl = backgroundImage?.url;
return (
<Item
variant="outline"
className={cn(
"relative overflow-hidden min-w-0 h-full flex flex-col flex-nowrap text-center",
isMedal && "justify-center medal-card medal-card-overflow",
)}
>
{bgUrl && (
<>
<img
src={bgUrl}
alt=""
aria-hidden
className="medal-card-bg absolute inset-0 w-full h-full object-cover pointer-events-none select-none"
/>
<div
aria-hidden
className="medal-card-scrim absolute inset-0 pointer-events-none bg-gradient-to-b from-background/70 via-background/60 to-background/80"
/>
</>
)}
<ItemHeader className={cn("relative z-10 flex flex-col items-center gap-2", isMedal && "justify-center")}>
{hasArt && (
<div
className={cn(
"flex w-full items-center justify-center",
hasRibbonArt ? "h-20" : "h-16",
)}
>
{hasRibbonArt ? (
<div className="w-44 max-w-full">
<RibbonViewer
ribbonDesign={award.ribbonDesign as RibbonDesign}
showRibbonOnly={false}
/>
</div>
) : awardImage?.url ? (
<img src={awardImage.url} alt={award.name} className="size-16 object-contain" />
) : null}
</div>
)}
<ItemTitle className={cn("mt-1", isMedal && "text-lg text-shadow-lg text-shadow-white/15")}>
{award.name}
</ItemTitle>
<span className="text-[9px] uppercase tracking-wider text-muted-foreground font-mono">
{type}
</span>
</ItemHeader>
<ItemContent className="relative z-10 flex flex-col gap-2">
<ItemDescription className="min-w-0 break-words text-center text-sm leading-relaxed">
{award.description
? convertLexicalToPlaintext({ data: award.description })
: "No description available."}
</ItemDescription>
{award.experiencePoints != null && (
<span className="mt-1 font-mono text-sm text-muted-foreground">
+{award.experiencePoints} XP
</span>
)}
</ItemContent>
{showGrantButton && (
<div className="relative z-10 mt-auto w-full">
{canGrant === false ? (
<Button
variant="outline"
size="sm"
disabled
className="w-full opacity-50 cursor-not-allowed"
>
<LockIcon data-icon="inline-start" />
You cannot grant this award
</Button>
) : onGrantClick ? (
<Button
variant="outline"
size="sm"
onClick={onGrantClick}
className="w-full flex flex-col gap-0 h-full py-1.5"
>
<span className="flex flex-row gap-1">
<AwardIcon data-icon="inline-start" />
Award this
</span>
<span className="opacity-50">
{remainingGrants === undefined
? ""
: remainingGrants === null
? ` (\u221E grant${remainingGrants === 1 ? "" : "s"} remaining)`
: ` (${remainingGrants} grant${remainingGrants === 1 ? "" : "s"} remaining)`}{" "}
</span>
</Button>
) : null}
</div>
)}
</Item>
);
}