"use client"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { Gift } from "lucide-react"; import { LoyaltyCardFont, LoyaltyCardTextSize, RewardPositionMode } from "@/generated/prisma/enums"; import { findCatalogIcon } from "@/lib/icons/icon-catalog"; import { cn } from "@/lib/utils/cn"; import { resolveRewardThreshold } from "@/services/loyalty/reward-threshold"; const FONT_CLASSES: Record = { [LoyaltyCardFont.SANS]: "font-sans", [LoyaltyCardFont.SERIF]: "font-serif", [LoyaltyCardFont.MONO]: "font-mono", }; /** MD matches the size "Loyalty Card" rendered at before this was configurable, so leaving this unset doesn't change the default look. */ const TITLE_TEXT_SIZE_CLASSES: Record = { [LoyaltyCardTextSize.SM]: "text-base", [LoyaltyCardTextSize.MD]: "text-lg", [LoyaltyCardTextSize.LG]: "text-2xl", }; export interface LoyaltyCardProps { businessName: string; memberNumber: string; stampsRequired: number; stampsEarned: number; rewardLabel: string; termsLine?: string; /** Owner-configured card appearance (LoyaltyProgramme) — see the loyalty settings designer. All optional; sensible defaults apply when unset. */ cardBackgroundColor?: string | null; cardBackgroundImageUrl?: string | null; stampIcon?: string | null; /** See resolveRewardThreshold's doc comment — determines whether the final stamp position renders as a normal stamp or as the reward slot itself. */ rewardPosition?: RewardPositionMode; /** Replaces the "Loyalty Card" title (top-left) when set. */ cardTitleText?: string | null; cardFont?: LoyaltyCardFont; cardTitleTextSize?: LoyaltyCardTextSize; cardTitleTextColor?: string | null; cardBorderColor?: string | null; cardMemberNumberColor?: string | null; } /** * Static, non-flipping card — see docs/phase-9-summary.md ("Loyalty Card * Improvements"). Previously a 3D flip whose back face just repeated the * business name in a generic panel; the brief asked to drop that and only * ever show the stamped side, restyled to feel like a real plastic * loyalty card (diagonal shine + soft depth via layered CSS * gradients/shadows only — no extra image asset, so this stays cheap to * render). Also, previously, cardBackgroundColor/cardBackgroundImageUrl/ * stampIcon were stored on LoyaltyProgramme but never actually reached * this component — every business rendered the same hardcoded card * regardless of configuration. Fixed here. */ export function LoyaltyCard({ businessName, memberNumber, stampsRequired, stampsEarned, rewardLabel, termsLine, cardBackgroundColor, cardBackgroundImageUrl, stampIcon, rewardPosition = RewardPositionMode.AFTER_ALL_STAMPS, cardTitleText, cardFont = LoyaltyCardFont.SANS, cardTitleTextSize = LoyaltyCardTextSize.MD, cardTitleTextColor, cardBorderColor, cardMemberNumberColor, }: LoyaltyCardProps) { const icon = findCatalogIcon(stampIcon); const threshold = resolveRewardThreshold({ stampsRequired, rewardPosition }); // Only FINAL_SLOT_IS_REWARD sets aside a dedicated slot — under // AFTER_ALL_STAMPS every position is a plain stamp and the reward is // shown separately (see the "Reward available" banner on the loyalty // page), not by any one slot. const rewardSlotIndex = rewardPosition === RewardPositionMode.FINAL_SLOT_IS_REWARD ? stampsRequired - 1 : null; const borderColor = cardBorderColor || "var(--color-accent-loyalty)"; return (
{cardBackgroundImageUrl && ( // eslint-disable-next-line @next/next/no-img-element -- local/user-supplied URL, same pattern used app-wide )} {/* Dark scrim so stamps/text stay legible over an arbitrary background image. */} {cardBackgroundImageUrl &&