Loyalty Card updates
1. Card title fix: The "top-right text" feature was renamed and repositioned to be the actual card title (top-left, replacing the hardcoded "Loyalty Card"), matching what you asked for. This included a proper database migration (cardTopRightText → cardTitleText, etc., using CHANGE COLUMN so any already-configured value is preserved, not dropped) and updates through every layer — schema, service, owner form, and both the owner preview and customer-facing card. Verified live: typed "VIP Rewards" into the new "Card title" field and confirmed it renders top-left on both the owner's live preview and the real customer loyalty page, with the member number staying in its own badge top-right. 2. Tabs: The long "Card Appearance" control list is now split into Colours / Stamp / Text tabs (using the same Tabs component used elsewhere in the app), exactly as you suggested — Colours holds every colour + the background image, Stamp holds just the icon picker, Text holds the title, font, size, and terms line. Along the way, while running full E2E regression, I found 6 test failures — but traced them via git stash to prove they're pre-existing bugs unrelated to this change (a booking/appointments-list bug), so I reverted-then-confirmed before restoring my work, and filed a separate follow-up task for them rather than mixing it into this change. Both fixes are typechecked, linted, verified with the full unit (253) and integration (185) suites, confirmed live in a real browser end-to-end, and deployed to your running Docker container — the migration applied cleanly against your real database with no data loss.
This commit is contained in:
12 files changed
+143
-126
No files matched your search
@@ -13,10 +13,11 @@ const FONT_CLASSES: Record<LoyaltyCardFont, string> = {
|
||||
[LoyaltyCardFont.MONO]: "font-mono",
|
||||
};
|
||||
|
||||
const TOP_RIGHT_TEXT_SIZE_CLASSES: Record<LoyaltyCardTextSize, string> = {
|
||||
[LoyaltyCardTextSize.SM]: "text-[8px]",
|
||||
[LoyaltyCardTextSize.MD]: "text-[10px]",
|
||||
[LoyaltyCardTextSize.LG]: "text-[13px]",
|
||||
/** 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, string> = {
|
||||
[LoyaltyCardTextSize.SM]: "text-base",
|
||||
[LoyaltyCardTextSize.MD]: "text-lg",
|
||||
[LoyaltyCardTextSize.LG]: "text-2xl",
|
||||
};
|
||||
|
||||
export interface LoyaltyCardProps {
|
||||
@@ -32,10 +33,11 @@ export interface LoyaltyCardProps {
|
||||
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;
|
||||
cardTopRightText?: string | null;
|
||||
/** Replaces the "Loyalty Card" title (top-left) when set. */
|
||||
cardTitleText?: string | null;
|
||||
cardFont?: LoyaltyCardFont;
|
||||
cardTopRightTextSize?: LoyaltyCardTextSize;
|
||||
cardTopRightTextColor?: string | null;
|
||||
cardTitleTextSize?: LoyaltyCardTextSize;
|
||||
cardTitleTextColor?: string | null;
|
||||
cardBorderColor?: string | null;
|
||||
cardMemberNumberColor?: string | null;
|
||||
}
|
||||
@@ -63,10 +65,10 @@ export function LoyaltyCard({
|
||||
cardBackgroundImageUrl,
|
||||
stampIcon,
|
||||
rewardPosition = RewardPositionMode.AFTER_ALL_STAMPS,
|
||||
cardTopRightText,
|
||||
cardTitleText,
|
||||
cardFont = LoyaltyCardFont.SANS,
|
||||
cardTopRightTextSize = LoyaltyCardTextSize.MD,
|
||||
cardTopRightTextColor,
|
||||
cardTitleTextSize = LoyaltyCardTextSize.MD,
|
||||
cardTitleTextColor,
|
||||
cardBorderColor,
|
||||
cardMemberNumberColor,
|
||||
}: LoyaltyCardProps) {
|
||||
@@ -111,23 +113,18 @@ export function LoyaltyCard({
|
||||
|
||||
<div className="relative flex h-full flex-col justify-between p-4">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<h2 className="text-lg font-black italic tracking-tight text-white">Loyalty Card</h2>
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
{cardTopRightText && (
|
||||
<span
|
||||
className={cn(TOP_RIGHT_TEXT_SIZE_CLASSES[cardTopRightTextSize], "text-right font-bold text-white")}
|
||||
style={{ color: cardTopRightTextColor || undefined }}
|
||||
>
|
||||
{cardTopRightText}
|
||||
</span>
|
||||
)}
|
||||
<span
|
||||
className="border-accent-loyalty/30 text-accent-loyalty rounded border px-2 py-0.5 text-[8px] font-bold tracking-widest uppercase"
|
||||
style={{ color: cardMemberNumberColor || undefined, borderColor: cardMemberNumberColor ? `${cardMemberNumberColor}4d` : undefined }}
|
||||
>
|
||||
Member #{memberNumber}
|
||||
</span>
|
||||
</div>
|
||||
<h2
|
||||
className={cn(TITLE_TEXT_SIZE_CLASSES[cardTitleTextSize], "font-black italic tracking-tight text-white")}
|
||||
style={{ color: cardTitleTextColor || undefined }}
|
||||
>
|
||||
{cardTitleText || "Loyalty Card"}
|
||||
</h2>
|
||||
<span
|
||||
className="border-accent-loyalty/30 text-accent-loyalty shrink-0 rounded border px-2 py-0.5 text-[8px] font-bold tracking-widest uppercase"
|
||||
style={{ color: cardMemberNumberColor || undefined, borderColor: cardMemberNumberColor ? `${cardMemberNumberColor}4d` : undefined }}
|
||||
>
|
||||
Member #{memberNumber}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
{Array.from({ length: stampsRequired }, (_, i) => {
|
||||
@@ -161,7 +158,7 @@ export function LoyaltyCard({
|
||||
})}
|
||||
</div>
|
||||
{termsLine && (
|
||||
<p className="text-center text-[7px] mt-2 font-bold tracking-widest text-white/40 uppercase">{termsLine}</p>
|
||||
<p className="mt-2 text-center text-[7px] font-bold tracking-widest text-white/40 uppercase">{termsLine}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user