From 344a32e3e421f4bcf8b92239693ec357321c100c Mon Sep 17 00:00:00 2001 From: Barcode Betty Date: Tue, 26 May 2026 12:00:55 +0000 Subject: [PATCH] feat(GRO-1792): add recovery paths to booking error and cancellation pages - Add "Start a new booking" button to BookingError linking to /admin/book - Add "Book again" button to BookingCancelled linking to /admin/book - Add business contact info section to BookingError (from BUSINESS_CONTACT_INFO constant) - Replace hardcoded colors with CSS variables (--color-error, --color-cancelled, etc.) - Add page-level string constants to eliminate hardcoded strings - Add unit tests for both pages (9 tests passing) Co-Authored-By: Paperclip --- src/__tests__/BookingCancelled.test.tsx | 27 +++++++++ src/__tests__/BookingError.test.tsx | 38 ++++++++++++ src/index.css | 13 +++++ src/lib/contact.ts | 7 +++ src/pages/BookingCancelled.tsx | 68 ++++++++++++++------- src/pages/BookingError.tsx | 78 ++++++++++++++++++------- 6 files changed, 187 insertions(+), 44 deletions(-) create mode 100644 src/__tests__/BookingCancelled.test.tsx create mode 100644 src/__tests__/BookingError.test.tsx create mode 100644 src/lib/contact.ts diff --git a/src/__tests__/BookingCancelled.test.tsx b/src/__tests__/BookingCancelled.test.tsx new file mode 100644 index 0000000..2d6ada1 --- /dev/null +++ b/src/__tests__/BookingCancelled.test.tsx @@ -0,0 +1,27 @@ +import { describe, it, expect } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { BookingCancelledPage } from "../pages/BookingCancelled.tsx"; + +describe("BookingCancelledPage", () => { + it("renders the cancelled heading", () => { + render(); + expect(screen.getByRole("heading", { name: /Appointment Cancelled/i })).toBeInTheDocument(); + }); + + it("renders the cancelled body text", () => { + render(); + expect(screen.getByText(/Your appointment has been cancelled/i)).toBeInTheDocument(); + }); + + it("has a Book again link pointing to /admin/book", () => { + render(); + const link = screen.getByRole("link", { name: /Book again/i }); + expect(link).toHaveAttribute("href", "/admin/book"); + }); + + it("has a Back to Portal link pointing to /", () => { + render(); + const link = screen.getByRole("link", { name: /Back to Portal/i }); + expect(link).toHaveAttribute("href", "/"); + }); +}); diff --git a/src/__tests__/BookingError.test.tsx b/src/__tests__/BookingError.test.tsx new file mode 100644 index 0000000..0f30f6e --- /dev/null +++ b/src/__tests__/BookingError.test.tsx @@ -0,0 +1,38 @@ +import { describe, it, expect } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { BookingErrorPage } from "../pages/BookingError.tsx"; +import { BUSINESS_CONTACT_INFO } from "../lib/contact.ts"; + +describe("BookingErrorPage", () => { + it("renders the error heading", () => { + render(); + expect(screen.getByRole("heading", { name: /Link Invalid or Expired/i })).toBeInTheDocument(); + }); + + it("renders the error body text", () => { + render(); + expect(screen.getByText(/This confirmation link is invalid/i)).toBeInTheDocument(); + }); + + it("has a Start a new booking link pointing to /admin/book", () => { + render(); + const link = screen.getByRole("link", { name: /Start a new booking/i }); + expect(link).toHaveAttribute("href", "/admin/book"); + }); + + it("has a Back to Portal link pointing to /", () => { + render(); + const link = screen.getByRole("link", { name: /Back to Portal/i }); + expect(link).toHaveAttribute("href", "/"); + }); + + it("displays business contact phone", () => { + render(); + expect(screen.getByText(BUSINESS_CONTACT_INFO.phone)).toBeInTheDocument(); + }); + + it("displays business contact email", () => { + render(); + expect(screen.getByText(BUSINESS_CONTACT_INFO.email)).toBeInTheDocument(); + }); +}); diff --git a/src/index.css b/src/index.css index 61c98ed..32b3b5e 100644 --- a/src/index.css +++ b/src/index.css @@ -8,6 +8,19 @@ --color-accent-dark: color-mix(in srgb, var(--color-accent) 78%, #000); --color-accent-light: color-mix(in srgb, var(--color-accent) 18%, #fff); --color-accent-lighter: color-mix(in srgb, var(--color-accent) 9%, #fff); + + /* Semantic / booking page tokens */ + --color-error: #dc2626; + --color-error-dark: #b91c1c; + --color-error-bg: #fef2f2; + --color-cancelled: #ea580c; + --color-cancelled-dark: #c2410c; + --color-cancelled-bg: #fff7ed; + --color-success: #16a34a; + --color-success-dark: #15803d; + --color-success-bg: #f0fdf4; + --color-text-secondary: #4b5563; + --color-surface: #fff; } *, *::before, *::after { diff --git a/src/lib/contact.ts b/src/lib/contact.ts new file mode 100644 index 0000000..d2908cf --- /dev/null +++ b/src/lib/contact.ts @@ -0,0 +1,7 @@ +// Business contact information — update values to reflect actual business details. +// Used on error/cancellation pages to help customers reach the business. +export const BUSINESS_CONTACT_INFO = { + phone: "(555) 000-1234", + email: "hello@groombook.example.com", + address: "123 Main St, Anytown, USA", +} as const; diff --git a/src/pages/BookingCancelled.tsx b/src/pages/BookingCancelled.tsx index 9b2ab4a..6ded7af 100644 --- a/src/pages/BookingCancelled.tsx +++ b/src/pages/BookingCancelled.tsx @@ -1,3 +1,10 @@ +const STRINGS = { + heading: "Appointment Cancelled", + body: "Your appointment has been cancelled. If this was a mistake or you'd like to rebook, please contact us.", + bookAgain: "Book again", + backToPortal: "Back to Portal", +} as const; + export function BookingCancelledPage() { return (
-

- Appointment Cancelled +

+ {STRINGS.heading}

-

- Your appointment has been cancelled. If this was a mistake or you'd - like to rebook, please contact us. +

+ {STRINGS.body}

- - Back to Portal - + +
); diff --git a/src/pages/BookingError.tsx b/src/pages/BookingError.tsx index 62639d9..ba5d43d 100644 --- a/src/pages/BookingError.tsx +++ b/src/pages/BookingError.tsx @@ -1,3 +1,13 @@ +import { BUSINESS_CONTACT_INFO } from "../lib/contact"; + +const STRINGS = { + heading: "Link Invalid or Expired", + body: "This confirmation link is invalid, has already been used, or your appointment has already passed. Please contact us if you need help.", + newBooking: "Start a new booking", + backToPortal: "Back to Portal", + contactLabel: "Need help?", +} as const; + export function BookingErrorPage() { return (
⚠️
-

- Link Invalid or Expired +

+ {STRINGS.heading}

-

- This confirmation link is invalid, has already been used, or your - appointment has already passed. Please contact us if you need help. +

+ {STRINGS.body}

- - Back to Portal - + + + +
+

{STRINGS.contactLabel}

+

+ {BUSINESS_CONTACT_INFO.phone} · {BUSINESS_CONTACT_INFO.email} +

+
);