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

@ -3,11 +3,18 @@
import { useEffect, useState, useCallback, useRef } from "react";
import { bech32 } from "bech32";
import { api, ApiError } from "../api";
import { sharedStyles } from "../styles/shared";
import { Header } from "../components/Header";
import { useRequireAuth } from "../hooks/useRequireAuth";
import { components } from "../generated/api";
import constants from "../../../shared/constants.json";
import {
layoutStyles,
cardStyles,
formStyles,
buttonStyles,
toastStyles,
utilityStyles,
} from "../styles/shared";
// Use generated type from OpenAPI schema
type ProfileData = components["schemas"]["ProfileResponse"];
@ -253,8 +260,8 @@ export default function ProfilePage() {
if (isLoading || isLoadingProfile) {
return (
<main style={styles.main}>
<div style={styles.loader}>Loading...</div>
<main style={layoutStyles.main}>
<div style={layoutStyles.loader}>Loading...</div>
</main>
);
}
@ -266,13 +273,13 @@ export default function ProfilePage() {
const canSubmit = hasChanges() && isValid() && !isSubmitting;
return (
<main style={styles.main}>
<main style={layoutStyles.main}>
{/* Toast notification */}
{toast && (
<div
style={{
...styles.toast,
...(toast.type === "success" ? styles.toastSuccess : styles.toastError),
...toastStyles.toast,
...(toast.type === "success" ? toastStyles.toastSuccess : toastStyles.toastError),
}}
>
{toast.message}
@ -281,44 +288,46 @@ export default function ProfilePage() {
<Header currentPage="profile" />
<div style={styles.content}>
<div style={layoutStyles.contentCentered}>
<div style={styles.profileCard}>
<div style={styles.cardHeader}>
<h1 style={styles.cardTitle}>My Profile</h1>
<p style={styles.cardSubtitle}>Manage your contact information</p>
<div style={cardStyles.cardHeader}>
<h1 style={cardStyles.cardTitle}>My Profile</h1>
<p style={cardStyles.cardSubtitle}>Manage your contact information</p>
</div>
<form onSubmit={handleSubmit} style={styles.form}>
<form onSubmit={handleSubmit} style={formStyles.form}>
{/* Login email - read only */}
<div style={styles.field}>
<label style={styles.label}>
<div style={formStyles.field}>
<label style={styles.labelWithBadge}>
Login Email
<span style={styles.readOnlyBadge}>Read only</span>
<span style={utilityStyles.readOnlyBadge}>Read only</span>
</label>
<input
type="email"
value={user.email}
style={{ ...styles.input, ...styles.inputReadOnly }}
style={{ ...formStyles.input, ...formStyles.inputReadOnly }}
disabled
/>
<span style={styles.hint}>This is your login email and cannot be changed here.</span>
<span style={formStyles.hint}>
This is your login email and cannot be changed here.
</span>
</div>
{/* Godfather - shown if user was invited */}
{godfatherEmail && (
<div style={styles.field}>
<label style={styles.label}>
<div style={formStyles.field}>
<label style={styles.labelWithBadge}>
Invited By
<span style={styles.readOnlyBadge}>Read only</span>
<span style={utilityStyles.readOnlyBadge}>Read only</span>
</label>
<div style={styles.godfatherBox}>
<span style={styles.godfatherEmail}>{godfatherEmail}</span>
</div>
<span style={styles.hint}>The user who invited you to join.</span>
<span style={formStyles.hint}>The user who invited you to join.</span>
</div>
)}
<div style={styles.divider} />
<div style={utilityStyles.divider} />
<p style={styles.sectionLabel}>Contact Details</p>
<p style={styles.sectionHint}>
@ -326,8 +335,8 @@ export default function ProfilePage() {
</p>
{/* Contact email */}
<div style={styles.field}>
<label htmlFor="contact_email" style={styles.label}>
<div style={formStyles.field}>
<label htmlFor="contact_email" style={formStyles.label}>
Contact Email
</label>
<input
@ -336,17 +345,19 @@ export default function ProfilePage() {
value={formData.contact_email}
onChange={handleInputChange("contact_email")}
style={{
...styles.input,
...(errors.contact_email ? styles.inputError : {}),
...formStyles.input,
...(errors.contact_email ? formStyles.inputError : {}),
}}
placeholder="alternate@example.com"
/>
{errors.contact_email && <span style={styles.errorText}>{errors.contact_email}</span>}
{errors.contact_email && (
<span style={formStyles.errorText}>{errors.contact_email}</span>
)}
</div>
{/* Telegram */}
<div style={styles.field}>
<label htmlFor="telegram" style={styles.label}>
<div style={formStyles.field}>
<label htmlFor="telegram" style={formStyles.label}>
Telegram
</label>
<input
@ -355,17 +366,17 @@ export default function ProfilePage() {
value={formData.telegram}
onChange={handleInputChange("telegram")}
style={{
...styles.input,
...(errors.telegram ? styles.inputError : {}),
...formStyles.input,
...(errors.telegram ? formStyles.inputError : {}),
}}
placeholder="@username"
/>
{errors.telegram && <span style={styles.errorText}>{errors.telegram}</span>}
{errors.telegram && <span style={formStyles.errorText}>{errors.telegram}</span>}
</div>
{/* Signal */}
<div style={styles.field}>
<label htmlFor="signal" style={styles.label}>
<div style={formStyles.field}>
<label htmlFor="signal" style={formStyles.label}>
Signal
</label>
<input
@ -374,17 +385,17 @@ export default function ProfilePage() {
value={formData.signal}
onChange={handleInputChange("signal")}
style={{
...styles.input,
...(errors.signal ? styles.inputError : {}),
...formStyles.input,
...(errors.signal ? formStyles.inputError : {}),
}}
placeholder="username.01"
/>
{errors.signal && <span style={styles.errorText}>{errors.signal}</span>}
{errors.signal && <span style={formStyles.errorText}>{errors.signal}</span>}
</div>
{/* Nostr npub */}
<div style={styles.field}>
<label htmlFor="nostr_npub" style={styles.label}>
<div style={formStyles.field}>
<label htmlFor="nostr_npub" style={formStyles.label}>
Nostr (npub)
</label>
<input
@ -393,19 +404,20 @@ export default function ProfilePage() {
value={formData.nostr_npub}
onChange={handleInputChange("nostr_npub")}
style={{
...styles.input,
...(errors.nostr_npub ? styles.inputError : {}),
...formStyles.input,
...(errors.nostr_npub ? formStyles.inputError : {}),
}}
placeholder="npub1..."
/>
{errors.nostr_npub && <span style={styles.errorText}>{errors.nostr_npub}</span>}
{errors.nostr_npub && <span style={formStyles.errorText}>{errors.nostr_npub}</span>}
</div>
<button
type="submit"
style={{
...styles.button,
...(!canSubmit ? styles.buttonDisabled : {}),
...buttonStyles.primaryButton,
marginTop: "1rem",
...(!canSubmit ? buttonStyles.buttonDisabled : {}),
}}
disabled={!canSubmit}
>
@ -418,45 +430,14 @@ export default function ProfilePage() {
);
}
const pageStyles: Record<string, React.CSSProperties> = {
// Page-specific styles
const styles: Record<string, React.CSSProperties> = {
profileCard: {
background: "rgba(255, 255, 255, 0.03)",
backdropFilter: "blur(10px)",
border: "1px solid rgba(255, 255, 255, 0.08)",
borderRadius: "24px",
padding: "2.5rem",
...cardStyles.card,
width: "100%",
maxWidth: "480px",
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.5)",
},
cardHeader: {
marginBottom: "2rem",
},
cardTitle: {
fontFamily: "'Instrument Serif', Georgia, serif",
fontSize: "2rem",
fontWeight: 400,
color: "#fff",
margin: 0,
letterSpacing: "-0.02em",
},
cardSubtitle: {
fontFamily: "'DM Sans', system-ui, sans-serif",
color: "rgba(255, 255, 255, 0.5)",
marginTop: "0.5rem",
fontSize: "0.95rem",
},
form: {
display: "flex",
flexDirection: "column",
gap: "1.25rem",
},
field: {
display: "flex",
flexDirection: "column",
gap: "0.5rem",
},
label: {
labelWithBadge: {
fontFamily: "'DM Sans', system-ui, sans-serif",
color: "rgba(255, 255, 255, 0.7)",
fontSize: "0.875rem",
@ -465,32 +446,6 @@ const pageStyles: Record<string, React.CSSProperties> = {
alignItems: "center",
gap: "0.5rem",
},
readOnlyBadge: {
fontSize: "0.7rem",
fontWeight: 500,
color: "rgba(255, 255, 255, 0.4)",
background: "rgba(255, 255, 255, 0.08)",
padding: "0.15rem 0.5rem",
borderRadius: "4px",
textTransform: "uppercase",
letterSpacing: "0.05em",
},
input: {
fontFamily: "'DM Sans', system-ui, sans-serif",
padding: "0.875rem 1rem",
fontSize: "1rem",
background: "rgba(255, 255, 255, 0.05)",
border: "1px solid rgba(255, 255, 255, 0.1)",
borderRadius: "12px",
color: "#fff",
outline: "none",
transition: "border-color 0.2s, box-shadow 0.2s",
},
inputReadOnly: {
background: "rgba(255, 255, 255, 0.02)",
color: "rgba(255, 255, 255, 0.5)",
cursor: "not-allowed",
},
godfatherBox: {
padding: "0.875rem 1rem",
background: "rgba(99, 102, 241, 0.08)",
@ -502,26 +457,6 @@ const pageStyles: Record<string, React.CSSProperties> = {
fontSize: "1rem",
color: "rgba(129, 140, 248, 0.9)",
},
inputError: {
border: "1px solid rgba(239, 68, 68, 0.5)",
boxShadow: "0 0 0 2px rgba(239, 68, 68, 0.1)",
},
hint: {
fontFamily: "'DM Sans', system-ui, sans-serif",
fontSize: "0.75rem",
color: "rgba(255, 255, 255, 0.4)",
fontStyle: "italic",
},
errorText: {
fontFamily: "'DM Sans', system-ui, sans-serif",
fontSize: "0.75rem",
color: "#fca5a5",
},
divider: {
height: "1px",
background: "rgba(255, 255, 255, 0.08)",
margin: "0.75rem 0",
},
sectionLabel: {
fontFamily: "'DM Sans', system-ui, sans-serif",
fontSize: "0.875rem",
@ -538,46 +473,4 @@ const pageStyles: Record<string, React.CSSProperties> = {
margin: 0,
marginBottom: "0.5rem",
},
button: {
fontFamily: "'DM Sans', system-ui, sans-serif",
marginTop: "1rem",
padding: "1rem",
fontSize: "1rem",
fontWeight: 600,
background: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)",
color: "#fff",
border: "none",
borderRadius: "12px",
cursor: "pointer",
transition: "transform 0.2s, box-shadow 0.2s",
boxShadow: "0 4px 14px rgba(99, 102, 241, 0.4)",
},
buttonDisabled: {
opacity: 0.5,
cursor: "not-allowed",
boxShadow: "none",
},
toast: {
position: "fixed",
top: "1.5rem",
right: "1.5rem",
padding: "1rem 1.5rem",
borderRadius: "12px",
fontFamily: "'DM Sans', system-ui, sans-serif",
fontSize: "0.875rem",
fontWeight: 500,
zIndex: 1000,
animation: "slideIn 0.3s ease-out",
boxShadow: "0 10px 25px rgba(0, 0, 0, 0.3)",
},
toastSuccess: {
background: "rgba(34, 197, 94, 0.9)",
color: "#fff",
},
toastError: {
background: "rgba(239, 68, 68, 0.9)",
color: "#fff",
},
};
const styles = { ...sharedStyles, ...pageStyles };