2e99ed520f
- New analytics utility (src/lib/analytics.ts) with ANALYTICS_EVENTS constants and fireAnalyticsEvent() – thin wrapper over window.dispatchEvent, no-op safe Built for Plausible/GTM integration later. - Public booking wizard (Book.tsx): fires step-transition events at each step (service → time → contact → submit) plus booking_confirmed on the dedicated confirmation page. - Portal BookingFlow (Appointments.tsx): fires equivalent events for the portal booking flow. booking_confirmed fires via useEffect when the inline success state is shown. - BookingErrorPage: fires booking_error on mount (no PII in payload). Events include step name and flow type (public/portal) but contain no PII: no names, emails, phone numbers, or pet names in any payload. Co-Authored-By: Paperclip <noreply@paperclip.ing>
90 lines
2.8 KiB
TypeScript
90 lines
2.8 KiB
TypeScript
import { useEffect } from "react";
|
||
import { BUSINESS_CONTACT_INFO } from "../lib/contact";
|
||
import { ANALYTICS_EVENTS, fireAnalyticsEvent } from "../lib/analytics";
|
||
|
||
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() {
|
||
useEffect(() => {
|
||
fireAnalyticsEvent(ANALYTICS_EVENTS.BOOKING_ERROR, { step: "error", flow: "public" });
|
||
}, []);
|
||
|
||
return (
|
||
<div
|
||
style={{
|
||
minHeight: "100vh",
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
fontFamily: "system-ui, sans-serif",
|
||
background: "var(--color-error-bg)",
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
background: "var(--color-surface)",
|
||
borderRadius: 12,
|
||
padding: "2.5rem 3rem",
|
||
boxShadow: "0 4px 24px rgba(0,0,0,0.08)",
|
||
textAlign: "center",
|
||
maxWidth: 420,
|
||
}}
|
||
>
|
||
<div style={{ fontSize: 56, marginBottom: "0.5rem" }}>⚠️</div>
|
||
<h1 style={{ color: "var(--color-error-dark)", fontSize: 24, margin: "0 0 0.5rem" }}>
|
||
{STRINGS.heading}
|
||
</h1>
|
||
<p style={{ color: "var(--color-text-secondary)", margin: "0 0 1.5rem" }}>
|
||
{STRINGS.body}
|
||
</p>
|
||
|
||
<div style={{ display: "flex", flexDirection: "column", gap: "0.75rem", alignItems: "center" }}>
|
||
<a
|
||
href="/admin/book"
|
||
style={{
|
||
display: "inline-block",
|
||
padding: "0.6rem 1.5rem",
|
||
background: "var(--color-primary)",
|
||
color: "#fff",
|
||
borderRadius: 6,
|
||
textDecoration: "none",
|
||
fontWeight: 600,
|
||
fontSize: 14,
|
||
}}
|
||
>
|
||
{STRINGS.newBooking}
|
||
</a>
|
||
<a
|
||
href="/"
|
||
style={{
|
||
display: "inline-block",
|
||
padding: "0.6rem 1.5rem",
|
||
background: "var(--color-error)",
|
||
color: "#fff",
|
||
borderRadius: 6,
|
||
textDecoration: "none",
|
||
fontWeight: 600,
|
||
fontSize: 14,
|
||
}}
|
||
>
|
||
{STRINGS.backToPortal}
|
||
</a>
|
||
</div>
|
||
|
||
<div style={{ marginTop: "1.5rem", paddingTop: "1rem", borderTop: "1px solid #e5e7eb", fontSize: 13, color: "var(--color-text-secondary)" }}>
|
||
<p style={{ margin: "0 0 0.25rem", fontWeight: 600 }}>{STRINGS.contactLabel}</p>
|
||
<p style={{ margin: 0 }}>
|
||
{BUSINESS_CONTACT_INFO.phone} · {BUSINESS_CONTACT_INFO.email}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|