refactor(frontend): consolidate shared styles into centralized style system

- Create comprehensive shared.ts with design tokens and categorized styles:
  - layoutStyles: main, loader, content variants
  - cardStyles: card, tableCard, cardHeader
  - tableStyles: complete table styling
  - paginationStyles: pagination controls
  - formStyles: inputs, labels, errors
  - buttonStyles: primary, secondary, accent, danger variants
  - badgeStyles: status badges with color variants
  - bannerStyles: error/success banners
  - modalStyles: modal overlay and content
  - toastStyles: toast notifications
  - utilityStyles: divider, emptyState, etc.

- Refactor all page components to use shared styles:
  - page.tsx (counter)
  - audit/page.tsx
  - booking/page.tsx
  - appointments/page.tsx
  - profile/page.tsx
  - invites/page.tsx
  - admin/invites/page.tsx
  - admin/availability/page.tsx

- Reduce code duplication significantly (each page now has only
  truly page-specific styles)
- Maintain backwards compatibility with sharedStyles export
This commit is contained in:
counterweight 2025-12-21 23:45:47 +01:00
parent 4d9edd7fd4
commit 81cd34b0e7
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
9 changed files with 1148 additions and 1173 deletions

View file

@ -9,7 +9,13 @@ import { useRequireAuth } from "../hooks/useRequireAuth";
import { components } from "../generated/api";
import constants from "../../../shared/constants.json";
import { formatDate, formatTime, getDateRange } from "../utils/date";
import { sharedStyles } from "../styles/shared";
import {
layoutStyles,
typographyStyles,
bannerStyles,
formStyles,
buttonStyles,
} from "../styles/shared";
const { slotDurationMinutes, maxAdvanceDays, minAdvanceDays, noteMaxLength } = constants.booking;
@ -17,205 +23,6 @@ type BookableSlot = components["schemas"]["BookableSlot"];
type AvailableSlotsResponse = components["schemas"]["AvailableSlotsResponse"];
type AppointmentResponse = components["schemas"]["AppointmentResponse"];
const pageStyles: Record<string, React.CSSProperties> = {
main: {
minHeight: "100vh",
background: "linear-gradient(135deg, #0f0f23 0%, #1a1a3e 50%, #2d1b4e 100%)",
display: "flex",
flexDirection: "column",
},
loader: {
flex: 1,
display: "flex",
alignItems: "center",
justifyContent: "center",
fontFamily: "'DM Sans', system-ui, sans-serif",
color: "rgba(255, 255, 255, 0.5)",
},
content: {
flex: 1,
padding: "2rem",
maxWidth: "900px",
margin: "0 auto",
width: "100%",
},
pageTitle: {
fontFamily: "'DM Sans', system-ui, sans-serif",
fontSize: "1.75rem",
fontWeight: 600,
color: "#fff",
marginBottom: "0.5rem",
},
pageSubtitle: {
fontFamily: "'DM Sans', system-ui, sans-serif",
color: "rgba(255, 255, 255, 0.5)",
fontSize: "0.9rem",
marginBottom: "1.5rem",
},
successBanner: {
background: "rgba(34, 197, 94, 0.15)",
border: "1px solid rgba(34, 197, 94, 0.3)",
color: "#4ade80",
padding: "1rem",
borderRadius: "8px",
marginBottom: "1rem",
fontFamily: "'DM Sans', system-ui, sans-serif",
fontSize: "0.875rem",
},
section: {
marginBottom: "2rem",
},
sectionTitle: {
fontFamily: "'DM Sans', system-ui, sans-serif",
fontSize: "1.1rem",
fontWeight: 500,
color: "#fff",
marginBottom: "1rem",
},
dateGrid: {
display: "flex",
flexWrap: "wrap",
gap: "0.5rem",
},
dateButton: {
fontFamily: "'DM Sans', system-ui, sans-serif",
padding: "0.75rem 1rem",
background: "rgba(255, 255, 255, 0.03)",
border: "1px solid rgba(255, 255, 255, 0.08)",
borderRadius: "10px",
cursor: "pointer",
minWidth: "90px",
textAlign: "center" as const,
transition: "all 0.2s",
},
dateButtonSelected: {
background: "rgba(167, 139, 250, 0.15)",
border: "1px solid #a78bfa",
},
dateButtonDisabled: {
opacity: 0.4,
cursor: "not-allowed",
background: "rgba(255, 255, 255, 0.01)",
border: "1px solid rgba(255, 255, 255, 0.04)",
},
dateWeekday: {
color: "#fff",
fontWeight: 500,
fontSize: "0.875rem",
marginBottom: "0.25rem",
},
dateDay: {
color: "rgba(255, 255, 255, 0.5)",
fontSize: "0.8rem",
},
slotGrid: {
display: "flex",
flexWrap: "wrap",
gap: "0.5rem",
},
slotButton: {
fontFamily: "'DM Sans', system-ui, sans-serif",
padding: "0.6rem 1.25rem",
background: "rgba(255, 255, 255, 0.03)",
border: "1px solid rgba(255, 255, 255, 0.08)",
borderRadius: "8px",
color: "#fff",
cursor: "pointer",
fontSize: "0.9rem",
transition: "all 0.2s",
},
slotButtonSelected: {
background: "rgba(167, 139, 250, 0.15)",
border: "1px solid #a78bfa",
},
emptyState: {
fontFamily: "'DM Sans', system-ui, sans-serif",
color: "rgba(255, 255, 255, 0.4)",
padding: "1rem 0",
},
confirmCard: {
background: "rgba(255, 255, 255, 0.03)",
border: "1px solid rgba(255, 255, 255, 0.08)",
borderRadius: "12px",
padding: "1.5rem",
maxWidth: "400px",
},
confirmTitle: {
fontFamily: "'DM Sans', system-ui, sans-serif",
fontSize: "1.1rem",
fontWeight: 500,
color: "#fff",
marginBottom: "1rem",
},
confirmTime: {
fontFamily: "'DM Sans', system-ui, sans-serif",
color: "rgba(255, 255, 255, 0.7)",
marginBottom: "1rem",
},
inputLabel: {
fontFamily: "'DM Sans', system-ui, sans-serif",
display: "block",
color: "rgba(255, 255, 255, 0.7)",
fontSize: "0.875rem",
marginBottom: "0.5rem",
},
textarea: {
fontFamily: "'DM Sans', system-ui, sans-serif",
width: "100%",
padding: "0.75rem",
background: "rgba(255, 255, 255, 0.05)",
border: "1px solid rgba(255, 255, 255, 0.1)",
borderRadius: "8px",
color: "#fff",
fontSize: "0.875rem",
minHeight: "80px",
resize: "vertical" as const,
},
charCount: {
fontFamily: "'DM Sans', system-ui, sans-serif",
fontSize: "0.75rem",
color: "rgba(255, 255, 255, 0.4)",
textAlign: "right" as const,
marginTop: "0.25rem",
},
charCountWarning: {
color: "#f87171",
},
buttonRow: {
display: "flex",
gap: "0.75rem",
marginTop: "1.5rem",
},
bookButton: {
fontFamily: "'DM Sans', system-ui, sans-serif",
flex: 1,
padding: "0.75rem",
background: "linear-gradient(135deg, #a78bfa 0%, #7c3aed 100%)",
border: "none",
borderRadius: "8px",
color: "#fff",
fontWeight: 500,
cursor: "pointer",
transition: "all 0.2s",
},
bookButtonDisabled: {
opacity: 0.5,
cursor: "not-allowed",
},
cancelButton: {
fontFamily: "'DM Sans', system-ui, sans-serif",
padding: "0.75rem 1.25rem",
background: "rgba(255, 255, 255, 0.05)",
border: "1px solid rgba(255, 255, 255, 0.1)",
borderRadius: "8px",
color: "rgba(255, 255, 255, 0.7)",
cursor: "pointer",
transition: "all 0.2s",
},
};
const styles = { ...sharedStyles, ...pageStyles };
export default function BookingPage() {
const { user, isLoading, isAuthorized } = useRequireAuth({
requiredPermission: Permission.BOOK_APPOINTMENT,
@ -345,8 +152,8 @@ export default function BookingPage() {
if (isLoading) {
return (
<main style={styles.main}>
<div style={styles.loader}>Loading...</div>
<main style={layoutStyles.main}>
<div style={layoutStyles.loader}>Loading...</div>
</main>
);
}
@ -356,17 +163,17 @@ export default function BookingPage() {
}
return (
<main style={styles.main}>
<main style={layoutStyles.main}>
<Header currentPage="booking" />
<div style={styles.content}>
<h1 style={styles.pageTitle}>Book an Appointment</h1>
<p style={styles.pageSubtitle}>
<h1 style={typographyStyles.pageTitle}>Book an Appointment</h1>
<p style={typographyStyles.pageSubtitle}>
Select a date to see available {slotDurationMinutes}-minute slots
</p>
{successMessage && <div style={styles.successBanner}>{successMessage}</div>}
{successMessage && <div style={bannerStyles.successBanner}>{successMessage}</div>}
{error && <div style={styles.errorBanner}>{error}</div>}
{error && <div style={bannerStyles.errorBanner}>{error}</div>}
{/* Date Selection */}
<div style={styles.section}>
@ -449,17 +256,17 @@ export default function BookingPage() {
</p>
<div>
<label style={styles.inputLabel}>Note (optional, max {noteMaxLength} chars)</label>
<label style={formStyles.label}>Note (optional, max {noteMaxLength} chars)</label>
<textarea
value={note}
onChange={(e) => setNote(e.target.value.slice(0, noteMaxLength))}
placeholder="Add a note about your appointment..."
style={styles.textarea}
style={formStyles.textarea}
/>
<div
style={{
...styles.charCount,
...(note.length >= noteMaxLength ? styles.charCountWarning : {}),
...formStyles.charCount,
...(note.length >= noteMaxLength ? formStyles.charCountWarning : {}),
}}
>
{note.length}/{noteMaxLength}
@ -472,7 +279,7 @@ export default function BookingPage() {
disabled={isBooking}
style={{
...styles.bookButton,
...(isBooking ? styles.bookButtonDisabled : {}),
...(isBooking ? buttonStyles.buttonDisabled : {}),
}}
>
{isBooking ? "Booking..." : "Book Appointment"}
@ -491,3 +298,131 @@ export default function BookingPage() {
</main>
);
}
// Page-specific styles
const styles: Record<string, React.CSSProperties> = {
content: {
flex: 1,
padding: "2rem",
maxWidth: "900px",
margin: "0 auto",
width: "100%",
},
section: {
marginBottom: "2rem",
},
sectionTitle: {
fontFamily: "'DM Sans', system-ui, sans-serif",
fontSize: "1.1rem",
fontWeight: 500,
color: "#fff",
marginBottom: "1rem",
},
dateGrid: {
display: "flex",
flexWrap: "wrap",
gap: "0.5rem",
},
dateButton: {
fontFamily: "'DM Sans', system-ui, sans-serif",
padding: "0.75rem 1rem",
background: "rgba(255, 255, 255, 0.03)",
border: "1px solid rgba(255, 255, 255, 0.08)",
borderRadius: "10px",
cursor: "pointer",
minWidth: "90px",
textAlign: "center" as const,
transition: "all 0.2s",
},
dateButtonSelected: {
background: "rgba(167, 139, 250, 0.15)",
border: "1px solid #a78bfa",
},
dateButtonDisabled: {
opacity: 0.4,
cursor: "not-allowed",
background: "rgba(255, 255, 255, 0.01)",
border: "1px solid rgba(255, 255, 255, 0.04)",
},
dateWeekday: {
color: "#fff",
fontWeight: 500,
fontSize: "0.875rem",
marginBottom: "0.25rem",
},
dateDay: {
color: "rgba(255, 255, 255, 0.5)",
fontSize: "0.8rem",
},
slotGrid: {
display: "flex",
flexWrap: "wrap",
gap: "0.5rem",
},
slotButton: {
fontFamily: "'DM Sans', system-ui, sans-serif",
padding: "0.6rem 1.25rem",
background: "rgba(255, 255, 255, 0.03)",
border: "1px solid rgba(255, 255, 255, 0.08)",
borderRadius: "8px",
color: "#fff",
cursor: "pointer",
fontSize: "0.9rem",
transition: "all 0.2s",
},
slotButtonSelected: {
background: "rgba(167, 139, 250, 0.15)",
border: "1px solid #a78bfa",
},
emptyState: {
fontFamily: "'DM Sans', system-ui, sans-serif",
color: "rgba(255, 255, 255, 0.4)",
padding: "1rem 0",
},
confirmCard: {
background: "rgba(255, 255, 255, 0.03)",
border: "1px solid rgba(255, 255, 255, 0.08)",
borderRadius: "12px",
padding: "1.5rem",
maxWidth: "400px",
},
confirmTitle: {
fontFamily: "'DM Sans', system-ui, sans-serif",
fontSize: "1.1rem",
fontWeight: 500,
color: "#fff",
marginBottom: "1rem",
},
confirmTime: {
fontFamily: "'DM Sans', system-ui, sans-serif",
color: "rgba(255, 255, 255, 0.7)",
marginBottom: "1rem",
},
buttonRow: {
display: "flex",
gap: "0.75rem",
marginTop: "1.5rem",
},
bookButton: {
fontFamily: "'DM Sans', system-ui, sans-serif",
flex: 1,
padding: "0.75rem",
background: "linear-gradient(135deg, #a78bfa 0%, #7c3aed 100%)",
border: "none",
borderRadius: "8px",
color: "#fff",
fontWeight: 500,
cursor: "pointer",
transition: "all 0.2s",
},
cancelButton: {
fontFamily: "'DM Sans', system-ui, sans-serif",
padding: "0.75rem 1.25rem",
background: "rgba(255, 255, 255, 0.05)",
border: "1px solid rgba(255, 255, 255, 0.1)",
borderRadius: "8px",
color: "rgba(255, 255, 255, 0.7)",
cursor: "pointer",
transition: "all 0.2s",
},
};