diff --git a/prisma/migrations/20260805120000_phase11_loyalty_card_title_rename/migration.sql b/prisma/migrations/20260805120000_phase11_loyalty_card_title_rename/migration.sql new file mode 100644 index 0000000..72beed6 --- /dev/null +++ b/prisma/migrations/20260805120000_phase11_loyalty_card_title_rename/migration.sql @@ -0,0 +1,9 @@ +-- Phase 11: the loyalty card's owner-configurable text now replaces the +-- hardcoded "Loyalty Card" title (top-left), not a secondary badge next to +-- the member number (top-right) — renamed to match, using CHANGE COLUMN so +-- any value an owner already configured (e.g. a custom title) survives the +-- rename instead of being dropped. +ALTER TABLE `LoyaltyProgramme` + CHANGE COLUMN `cardTopRightText` `cardTitleText` VARCHAR(191) NULL, + CHANGE COLUMN `cardTopRightTextSize` `cardTitleTextSize` ENUM('SM', 'MD', 'LG') NOT NULL DEFAULT 'MD', + CHANGE COLUMN `cardTopRightTextColor` `cardTitleTextColor` VARCHAR(191) NULL; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index c120c86..e46ffcc 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -1214,19 +1214,18 @@ model LoyaltyProgramme { stampIcon String? termsLine String? - /// Phase 10 — free text shown in the card's top-right area, above the - /// member number. Separate from the member number itself (always - /// shown, has its own colour below) — null/empty means nothing renders - /// there beyond the member number, same as today. - cardTopRightText String? - /// Card-wide — applies to all card text, not just the top-right text. - /// See LoyaltyCardFont's doc comment for why this is card-wide rather - /// than a control per element. - cardFont LoyaltyCardFont @default(SANS) - cardTopRightTextSize LoyaltyCardTextSize @default(MD) - cardTopRightTextColor String? - cardBorderColor String? - cardMemberNumberColor String? + /// Phase 11 — replaces the hardcoded "Loyalty Card" title shown top-left + /// on the card. Null/empty falls back to "Loyalty Card", same as before + /// this was configurable. + cardTitleText String? + /// Card-wide — applies to all card text, not just the title. See + /// LoyaltyCardFont's doc comment for why this is card-wide rather than a + /// control per element. + cardFont LoyaltyCardFont @default(SANS) + cardTitleTextSize LoyaltyCardTextSize @default(MD) + cardTitleTextColor String? + cardBorderColor String? + cardMemberNumberColor String? updatedAt DateTime @updatedAt diff --git a/src/app/(customer)/home/page.tsx b/src/app/(customer)/home/page.tsx index 33ce909..d5854eb 100644 --- a/src/app/(customer)/home/page.tsx +++ b/src/app/(customer)/home/page.tsx @@ -143,17 +143,17 @@ export default async function CustomerHomePage() {
{/* Hero — an owner-managed gallery (see /owner/settings/gallery), matching the content width rather than bleeding edge-to-edge. */} {heroGalleryImages.length > 0 && ( -
+
)}
- {session?.user && ( + {/* {session?.user && (

Good to see you, {session.user.name?.split(" ")[0] ?? "there"}

- )} + )} */}

{business.name}

{location && (

@@ -176,9 +176,11 @@ export default async function CustomerHomePage() { {/* Upcoming appointment */} {upcomingAppointment && (

+ {session?.user && (

- Your Next Visit -

+ {session.user.name?.split(" ")[0] ?? "Your Next Appointment"}, Your Next Appointment + + )} s.service.name).join(" + ")} diff --git a/src/app/(customer)/loyalty/page.tsx b/src/app/(customer)/loyalty/page.tsx index 2a5e410..2343f0f 100644 --- a/src/app/(customer)/loyalty/page.tsx +++ b/src/app/(customer)/loyalty/page.tsx @@ -86,10 +86,10 @@ export default async function LoyaltyPage() { cardBackgroundImageUrl={loyalty.cardBackgroundImageUrl} stampIcon={loyalty.stampIcon} rewardPosition={loyalty.rewardPosition} - cardTopRightText={loyalty.cardTopRightText} + cardTitleText={loyalty.cardTitleText} cardFont={loyalty.cardFont} - cardTopRightTextSize={loyalty.cardTopRightTextSize} - cardTopRightTextColor={loyalty.cardTopRightTextColor} + cardTitleTextSize={loyalty.cardTitleTextSize} + cardTitleTextColor={loyalty.cardTitleTextColor} cardBorderColor={loyalty.cardBorderColor} cardMemberNumberColor={loyalty.cardMemberNumberColor} /> diff --git a/src/app/(owner)/owner/settings/loyalty/page.tsx b/src/app/(owner)/owner/settings/loyalty/page.tsx index 81d2a1a..e420629 100644 --- a/src/app/(owner)/owner/settings/loyalty/page.tsx +++ b/src/app/(owner)/owner/settings/loyalty/page.tsx @@ -51,10 +51,10 @@ export default async function OwnerLoyaltyConfigPage() { cardBackgroundImageUrl: programme.cardBackgroundImageUrl ?? undefined, stampIcon: programme.stampIcon ?? undefined, termsLine: programme.termsLine ?? undefined, - cardTopRightText: programme.cardTopRightText ?? undefined, + cardTitleText: programme.cardTitleText ?? undefined, cardFont: programme.cardFont, - cardTopRightTextSize: programme.cardTopRightTextSize, - cardTopRightTextColor: programme.cardTopRightTextColor ?? undefined, + cardTitleTextSize: programme.cardTitleTextSize, + cardTitleTextColor: programme.cardTitleTextColor ?? undefined, cardBorderColor: programme.cardBorderColor ?? undefined, cardMemberNumberColor: programme.cardMemberNumberColor ?? undefined, } @@ -68,7 +68,7 @@ export default async function OwnerLoyaltyConfigPage() { earnOnCancelled: false, earnOnNoShow: false, cardFont: LoyaltyCardFont.SANS, - cardTopRightTextSize: LoyaltyCardTextSize.MD, + cardTitleTextSize: LoyaltyCardTextSize.MD, }; return ( diff --git a/src/components/ui/loyalty-card.tsx b/src/components/ui/loyalty-card.tsx index 00023d5..faf54cb 100644 --- a/src/components/ui/loyalty-card.tsx +++ b/src/components/ui/loyalty-card.tsx @@ -13,10 +13,11 @@ const FONT_CLASSES: Record = { [LoyaltyCardFont.MONO]: "font-mono", }; -const TOP_RIGHT_TEXT_SIZE_CLASSES: Record = { - [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.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({
-

Loyalty Card

-
- {cardTopRightText && ( - - {cardTopRightText} - - )} - - Member #{memberNumber} - -
+

+ {cardTitleText || "Loyalty Card"} +

+ + Member #{memberNumber} +
{Array.from({ length: stampsRequired }, (_, i) => { @@ -161,7 +158,7 @@ export function LoyaltyCard({ })}
{termsLine && ( -

{termsLine}

+

{termsLine}

)}
diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx index f1f2aa0..d07d49c 100644 --- a/src/components/ui/tabs.tsx +++ b/src/components/ui/tabs.tsx @@ -14,7 +14,7 @@ export const Tabs = TabsPrimitive.Root; export function TabsList({ className, ...props }: TabsPrimitive.TabsListProps) { return ( ); @@ -24,7 +24,7 @@ export function TabsTrigger({ className, ...props }: TabsPrimitive.TabsTriggerPr return (
-
- set("cardBackgroundColor", hex)} - /> - set("cardBackgroundImageUrl", url ?? undefined)} - category="loyalty" - /> - set("cardBorderColor", hex)} - /> - set("stampIcon", iconName)} /> - set("termsLine", e.target.value)} - placeholder="Valid for 12 months. One reward per visit." - /> +
+ + + Colours + Stamp + Text + -
-

Top-right card text

-
+ + set("cardBackgroundColor", hex)} + /> + set("cardBackgroundImageUrl", url ?? undefined)} + category="loyalty" + /> + set("cardBorderColor", hex)} + /> + set("cardTitleTextColor", hex)} + /> + set("cardMemberNumberColor", hex)} + /> + + + + set("stampIcon", iconName)} /> + + + set("cardTopRightText", e.target.value)} - placeholder="e.g. VIP Member" - hint="Shown above the member number. Leave blank to show only the member number, as today." + label="Card title" + value={config.cardTitleText ?? ""} + onChange={(e) => set("cardTitleText", e.target.value)} + placeholder="Loyalty Card" + hint={`Shown top-left on the card, in place of the default "Loyalty Card". Leave blank to keep the default.`} />
set("cardTopRightTextSize", e.target.value as ProgrammeConfigInput["cardTopRightTextSize"])} + label="Title size" + value={config.cardTitleTextSize} + onChange={(e) => set("cardTitleTextSize", e.target.value as ProgrammeConfigInput["cardTitleTextSize"])} > - {TEXT_SIZE_OPTIONS.map((s) => ( + {TITLE_SIZE_OPTIONS.map((s) => ( ))}
-

Font applies to the whole card, not just this text.

- set("cardTopRightTextColor", hex)} +

Font applies to the whole card, not just the title.

+ set("termsLine", e.target.value)} + placeholder="Valid for 12 months. One reward per visit." /> -
-
- - set("cardMemberNumberColor", hex)} - /> + +
diff --git a/src/services/loyalty/__tests__/loyalty-config.test.ts b/src/services/loyalty/__tests__/loyalty-config.test.ts index fc478ea..07b6e5a 100644 --- a/src/services/loyalty/__tests__/loyalty-config.test.ts +++ b/src/services/loyalty/__tests__/loyalty-config.test.ts @@ -14,7 +14,7 @@ const base: ProgrammeConfigInput = { earnOnCancelled: false, earnOnNoShow: false, cardFont: LoyaltyCardFont.SANS, - cardTopRightTextSize: LoyaltyCardTextSize.MD, + cardTitleTextSize: LoyaltyCardTextSize.MD, }; describe("validateProgrammeConfig", () => { diff --git a/src/services/loyalty/__tests__/loyalty-owner.integration.test.ts b/src/services/loyalty/__tests__/loyalty-owner.integration.test.ts index 3c10fea..5ff10d3 100644 --- a/src/services/loyalty/__tests__/loyalty-owner.integration.test.ts +++ b/src/services/loyalty/__tests__/loyalty-owner.integration.test.ts @@ -41,7 +41,7 @@ describe("LoyaltyService — owner configuration and redemption (integration)", earnOnCancelled: false, earnOnNoShow: false, cardFont: LoyaltyCardFont.SANS, - cardTopRightTextSize: LoyaltyCardTextSize.MD, + cardTitleTextSize: LoyaltyCardTextSize.MD, }, }); expect(first.success).toBe(true); @@ -63,7 +63,7 @@ describe("LoyaltyService — owner configuration and redemption (integration)", earnOnCancelled: false, earnOnNoShow: false, cardFont: LoyaltyCardFont.SANS, - cardTopRightTextSize: LoyaltyCardTextSize.MD, + cardTitleTextSize: LoyaltyCardTextSize.MD, }, }); expect(second.success).toBe(true); @@ -92,7 +92,7 @@ describe("LoyaltyService — owner configuration and redemption (integration)", earnOnCancelled: false, earnOnNoShow: false, cardFont: LoyaltyCardFont.SANS, - cardTopRightTextSize: LoyaltyCardTextSize.MD, + cardTitleTextSize: LoyaltyCardTextSize.MD, }, }); expect(result.success).toBe(false); diff --git a/src/services/loyalty/loyalty.service.ts b/src/services/loyalty/loyalty.service.ts index 11e60dd..903d076 100644 --- a/src/services/loyalty/loyalty.service.ts +++ b/src/services/loyalty/loyalty.service.ts @@ -56,10 +56,10 @@ export interface LoyaltySummary { cardBackgroundColor: string | null; cardBackgroundImageUrl: string | null; termsLine: string | null; - cardTopRightText: string | null; + cardTitleText: string | null; cardFont: LoyaltyCardFont; - cardTopRightTextSize: LoyaltyCardTextSize; - cardTopRightTextColor: string | null; + cardTitleTextSize: LoyaltyCardTextSize; + cardTitleTextColor: string | null; cardBorderColor: string | null; cardMemberNumberColor: string | null; } @@ -105,10 +105,10 @@ export async function getLoyaltySummary( cardBackgroundColor: programme.cardBackgroundColor, cardBackgroundImageUrl: programme.cardBackgroundImageUrl, termsLine: programme.termsLine, - cardTopRightText: programme.cardTopRightText, + cardTitleText: programme.cardTitleText, cardFont: programme.cardFont, - cardTopRightTextSize: programme.cardTopRightTextSize, - cardTopRightTextColor: programme.cardTopRightTextColor, + cardTitleTextSize: programme.cardTitleTextSize, + cardTitleTextColor: programme.cardTitleTextColor, cardBorderColor: programme.cardBorderColor, cardMemberNumberColor: programme.cardMemberNumberColor, }; @@ -546,11 +546,11 @@ export interface ProgrammeConfigInput { cardBackgroundImageUrl?: string; stampIcon?: string; termsLine?: string; - /** Free text shown in the card's top-right area — see schema.prisma's cardTopRightText doc comment. */ - cardTopRightText?: string; + /** Replaces the "Loyalty Card" title (top-left) when set — see schema.prisma's cardTitleText doc comment. */ + cardTitleText?: string; cardFont: LoyaltyCardFont; - cardTopRightTextSize: LoyaltyCardTextSize; - cardTopRightTextColor?: string; + cardTitleTextSize: LoyaltyCardTextSize; + cardTitleTextColor?: string; cardBorderColor?: string; cardMemberNumberColor?: string; } @@ -627,10 +627,10 @@ export async function updateProgrammeConfig( cardBackgroundImageUrl: params.input.cardBackgroundImageUrl || null, stampIcon: params.input.stampIcon || null, termsLine: params.input.termsLine?.trim() || null, - cardTopRightText: params.input.cardTopRightText?.trim() || null, + cardTitleText: params.input.cardTitleText?.trim() || null, cardFont: params.input.cardFont, - cardTopRightTextSize: params.input.cardTopRightTextSize, - cardTopRightTextColor: params.input.cardTopRightTextColor || null, + cardTitleTextSize: params.input.cardTitleTextSize, + cardTitleTextColor: params.input.cardTitleTextColor || null, cardBorderColor: params.input.cardBorderColor || null, cardMemberNumberColor: params.input.cardMemberNumberColor || null, }; diff --git a/src/services/reporting/owner-reporting.service.ts b/src/services/reporting/owner-reporting.service.ts index e9e901e..cd807fb 100644 --- a/src/services/reporting/owner-reporting.service.ts +++ b/src/services/reporting/owner-reporting.service.ts @@ -227,7 +227,7 @@ export async function getConfigurationWarnings(prisma: PrismaClient, businessId: earnOnCancelled: loyaltyProgramme.earnOnCancelled, earnOnNoShow: loyaltyProgramme.earnOnNoShow, cardFont: loyaltyProgramme.cardFont, - cardTopRightTextSize: loyaltyProgramme.cardTopRightTextSize, + cardTitleTextSize: loyaltyProgramme.cardTitleTextSize, }); if (!validation.valid) { warnings.push({