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:
parent
4d9edd7fd4
commit
81cd34b0e7
9 changed files with 1148 additions and 1173 deletions
|
|
@ -3,7 +3,6 @@
|
||||||
import { useEffect, useState, useCallback } from "react";
|
import { useEffect, useState, useCallback } from "react";
|
||||||
import { Permission } from "../../auth-context";
|
import { Permission } from "../../auth-context";
|
||||||
import { api } from "../../api";
|
import { api } from "../../api";
|
||||||
import { sharedStyles } from "../../styles/shared";
|
|
||||||
import { Header } from "../../components/Header";
|
import { Header } from "../../components/Header";
|
||||||
import { useRequireAuth } from "../../hooks/useRequireAuth";
|
import { useRequireAuth } from "../../hooks/useRequireAuth";
|
||||||
import { components } from "../../generated/api";
|
import { components } from "../../generated/api";
|
||||||
|
|
@ -15,6 +14,13 @@ import {
|
||||||
formatTimeString,
|
formatTimeString,
|
||||||
isWeekend,
|
isWeekend,
|
||||||
} from "../../utils/date";
|
} from "../../utils/date";
|
||||||
|
import {
|
||||||
|
layoutStyles,
|
||||||
|
typographyStyles,
|
||||||
|
bannerStyles,
|
||||||
|
buttonStyles,
|
||||||
|
modalStyles,
|
||||||
|
} from "../../styles/shared";
|
||||||
|
|
||||||
const { slotDurationMinutes, maxAdvanceDays, minAdvanceDays } = constants.booking;
|
const { slotDurationMinutes, maxAdvanceDays, minAdvanceDays } = constants.booking;
|
||||||
|
|
||||||
|
|
@ -217,8 +223,8 @@ export default function AdminAvailabilityPage() {
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<main style={styles.main}>
|
<main style={layoutStyles.main}>
|
||||||
<div style={styles.loader}>Loading...</div>
|
<div style={layoutStyles.loader}>Loading...</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -228,15 +234,15 @@ export default function AdminAvailabilityPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main style={styles.main}>
|
<main style={layoutStyles.main}>
|
||||||
<Header currentPage="admin-availability" />
|
<Header currentPage="admin-availability" />
|
||||||
|
|
||||||
<div style={styles.content}>
|
<div style={layoutStyles.contentScrollable}>
|
||||||
<div style={styles.pageContainer}>
|
<div style={styles.pageContainer}>
|
||||||
<div style={styles.headerRow}>
|
<div style={styles.headerRow}>
|
||||||
<div>
|
<div>
|
||||||
<h1 style={styles.pageTitle}>Availability</h1>
|
<h1 style={typographyStyles.pageTitle}>Availability</h1>
|
||||||
<p style={styles.pageSubtitle}>
|
<p style={typographyStyles.pageSubtitle}>
|
||||||
Configure your available time slots for the next {maxAdvanceDays} days
|
Configure your available time slots for the next {maxAdvanceDays} days
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -246,18 +252,18 @@ export default function AdminAvailabilityPage() {
|
||||||
<button
|
<button
|
||||||
onClick={executeCopy}
|
onClick={executeCopy}
|
||||||
disabled={copyTargets.size === 0 || isCopying}
|
disabled={copyTargets.size === 0 || isCopying}
|
||||||
style={styles.copyButton}
|
style={buttonStyles.accentButton}
|
||||||
>
|
>
|
||||||
{isCopying ? "Copying..." : `Copy to ${copyTargets.size} day(s)`}
|
{isCopying ? "Copying..." : `Copy to ${copyTargets.size} day(s)`}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={cancelCopyMode} style={styles.cancelButton}>
|
<button onClick={cancelCopyMode} style={buttonStyles.secondaryButton}>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && !selectedDate && <div style={styles.errorBanner}>{error}</div>}
|
{error && !selectedDate && <div style={bannerStyles.errorBanner}>{error}</div>}
|
||||||
|
|
||||||
<div style={styles.calendar}>
|
<div style={styles.calendar}>
|
||||||
{dates.map((date) => {
|
{dates.map((date) => {
|
||||||
|
|
@ -322,11 +328,13 @@ export default function AdminAvailabilityPage() {
|
||||||
|
|
||||||
{/* Edit Modal */}
|
{/* Edit Modal */}
|
||||||
{selectedDate && (
|
{selectedDate && (
|
||||||
<div style={styles.modalOverlay} onClick={closeModal}>
|
<div style={modalStyles.modalOverlay} onClick={closeModal}>
|
||||||
<div style={styles.modal} onClick={(e) => e.stopPropagation()}>
|
<div style={modalStyles.modal} onClick={(e) => e.stopPropagation()}>
|
||||||
<h2 style={styles.modalTitle}>Edit Availability - {formatDisplayDate(selectedDate)}</h2>
|
<h2 style={modalStyles.modalTitle}>
|
||||||
|
Edit Availability - {formatDisplayDate(selectedDate)}
|
||||||
|
</h2>
|
||||||
|
|
||||||
{error && <div style={styles.modalError}>{error}</div>}
|
{error && <div style={modalStyles.modalError}>{error}</div>}
|
||||||
|
|
||||||
<div style={styles.slotsEditor}>
|
<div style={styles.slotsEditor}>
|
||||||
{editSlots.map((slot, index) => (
|
{editSlots.map((slot, index) => (
|
||||||
|
|
@ -368,15 +376,19 @@ export default function AdminAvailabilityPage() {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={styles.modalActions}>
|
<div style={modalStyles.modalActions}>
|
||||||
<button onClick={clearAvailability} disabled={isSaving} style={styles.clearButton}>
|
<button onClick={clearAvailability} disabled={isSaving} style={styles.clearButton}>
|
||||||
Clear All
|
Clear All
|
||||||
</button>
|
</button>
|
||||||
<div style={styles.modalActionsRight}>
|
<div style={modalStyles.modalActionsRight}>
|
||||||
<button onClick={closeModal} style={styles.cancelButton}>
|
<button onClick={closeModal} style={buttonStyles.secondaryButton}>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
<button onClick={saveAvailability} disabled={isSaving} style={styles.saveButton}>
|
<button
|
||||||
|
onClick={saveAvailability}
|
||||||
|
disabled={isSaving}
|
||||||
|
style={buttonStyles.accentButton}
|
||||||
|
>
|
||||||
{isSaving ? "Saving..." : "Save"}
|
{isSaving ? "Saving..." : "Save"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -388,12 +400,8 @@ export default function AdminAvailabilityPage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageStyles: Record<string, React.CSSProperties> = {
|
// Page-specific styles
|
||||||
content: {
|
const styles: Record<string, React.CSSProperties> = {
|
||||||
flex: 1,
|
|
||||||
padding: "2rem",
|
|
||||||
overflowY: "auto",
|
|
||||||
},
|
|
||||||
pageContainer: {
|
pageContainer: {
|
||||||
maxWidth: "1200px",
|
maxWidth: "1200px",
|
||||||
margin: "0 auto",
|
margin: "0 auto",
|
||||||
|
|
@ -406,19 +414,6 @@ const pageStyles: Record<string, React.CSSProperties> = {
|
||||||
flexWrap: "wrap",
|
flexWrap: "wrap",
|
||||||
gap: "1rem",
|
gap: "1rem",
|
||||||
},
|
},
|
||||||
pageTitle: {
|
|
||||||
fontFamily: "'Instrument Serif', Georgia, serif",
|
|
||||||
fontSize: "2rem",
|
|
||||||
fontWeight: 400,
|
|
||||||
color: "#fff",
|
|
||||||
margin: 0,
|
|
||||||
},
|
|
||||||
pageSubtitle: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
color: "rgba(255, 255, 255, 0.5)",
|
|
||||||
marginTop: "0.5rem",
|
|
||||||
fontSize: "0.95rem",
|
|
||||||
},
|
|
||||||
copyActions: {
|
copyActions: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|
@ -429,16 +424,6 @@ const pageStyles: Record<string, React.CSSProperties> = {
|
||||||
fontSize: "0.85rem",
|
fontSize: "0.85rem",
|
||||||
color: "rgba(255, 255, 255, 0.6)",
|
color: "rgba(255, 255, 255, 0.6)",
|
||||||
},
|
},
|
||||||
copyButton: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
fontSize: "0.85rem",
|
|
||||||
padding: "0.5rem 1rem",
|
|
||||||
background: "rgba(99, 102, 241, 0.3)",
|
|
||||||
color: "#fff",
|
|
||||||
border: "1px solid rgba(99, 102, 241, 0.5)",
|
|
||||||
borderRadius: "8px",
|
|
||||||
cursor: "pointer",
|
|
||||||
},
|
|
||||||
calendar: {
|
calendar: {
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "repeat(auto-fill, minmax(180px, 1fr))",
|
gridTemplateColumns: "repeat(auto-fill, minmax(180px, 1fr))",
|
||||||
|
|
@ -506,44 +491,6 @@ const pageStyles: Record<string, React.CSSProperties> = {
|
||||||
borderRadius: "4px",
|
borderRadius: "4px",
|
||||||
color: "rgba(129, 140, 248, 0.9)",
|
color: "rgba(129, 140, 248, 0.9)",
|
||||||
},
|
},
|
||||||
modalOverlay: {
|
|
||||||
position: "fixed",
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
background: "rgba(0, 0, 0, 0.7)",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "center",
|
|
||||||
zIndex: 1000,
|
|
||||||
},
|
|
||||||
modal: {
|
|
||||||
background: "#1a1a3e",
|
|
||||||
border: "1px solid rgba(255, 255, 255, 0.1)",
|
|
||||||
borderRadius: "16px",
|
|
||||||
padding: "2rem",
|
|
||||||
width: "90%",
|
|
||||||
maxWidth: "400px",
|
|
||||||
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.5)",
|
|
||||||
},
|
|
||||||
modalTitle: {
|
|
||||||
fontFamily: "'Instrument Serif', Georgia, serif",
|
|
||||||
fontSize: "1.5rem",
|
|
||||||
fontWeight: 400,
|
|
||||||
color: "#fff",
|
|
||||||
margin: "0 0 1.5rem 0",
|
|
||||||
},
|
|
||||||
modalError: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
fontSize: "0.85rem",
|
|
||||||
padding: "0.75rem",
|
|
||||||
background: "rgba(239, 68, 68, 0.15)",
|
|
||||||
border: "1px solid rgba(239, 68, 68, 0.3)",
|
|
||||||
borderRadius: "8px",
|
|
||||||
color: "#f87171",
|
|
||||||
marginBottom: "1rem",
|
|
||||||
},
|
|
||||||
slotsEditor: {
|
slotsEditor: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
|
@ -593,16 +540,6 @@ const pageStyles: Record<string, React.CSSProperties> = {
|
||||||
borderRadius: "6px",
|
borderRadius: "6px",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
},
|
},
|
||||||
modalActions: {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "1rem",
|
|
||||||
},
|
|
||||||
modalActionsRight: {
|
|
||||||
display: "flex",
|
|
||||||
gap: "0.75rem",
|
|
||||||
},
|
|
||||||
clearButton: {
|
clearButton: {
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||||
fontSize: "0.85rem",
|
fontSize: "0.85rem",
|
||||||
|
|
@ -613,27 +550,4 @@ const pageStyles: Record<string, React.CSSProperties> = {
|
||||||
borderRadius: "8px",
|
borderRadius: "8px",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
},
|
},
|
||||||
cancelButton: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
fontSize: "0.85rem",
|
|
||||||
padding: "0.6rem 1rem",
|
|
||||||
background: "rgba(255, 255, 255, 0.05)",
|
|
||||||
color: "rgba(255, 255, 255, 0.7)",
|
|
||||||
border: "1px solid rgba(255, 255, 255, 0.1)",
|
|
||||||
borderRadius: "8px",
|
|
||||||
cursor: "pointer",
|
|
||||||
},
|
|
||||||
saveButton: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
fontSize: "0.85rem",
|
|
||||||
fontWeight: 500,
|
|
||||||
padding: "0.6rem 1.5rem",
|
|
||||||
background: "rgba(99, 102, 241, 0.3)",
|
|
||||||
color: "#fff",
|
|
||||||
border: "1px solid rgba(99, 102, 241, 0.5)",
|
|
||||||
borderRadius: "8px",
|
|
||||||
cursor: "pointer",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = { ...sharedStyles, ...pageStyles };
|
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,20 @@
|
||||||
import { useEffect, useState, useCallback } from "react";
|
import { useEffect, useState, useCallback } from "react";
|
||||||
import { Permission } from "../../auth-context";
|
import { Permission } from "../../auth-context";
|
||||||
import { api } from "../../api";
|
import { api } from "../../api";
|
||||||
import { sharedStyles } from "../../styles/shared";
|
|
||||||
import { Header } from "../../components/Header";
|
import { Header } from "../../components/Header";
|
||||||
import { useRequireAuth } from "../../hooks/useRequireAuth";
|
import { useRequireAuth } from "../../hooks/useRequireAuth";
|
||||||
import { components } from "../../generated/api";
|
import { components } from "../../generated/api";
|
||||||
import constants from "../../../../shared/constants.json";
|
import constants from "../../../../shared/constants.json";
|
||||||
|
import {
|
||||||
|
layoutStyles,
|
||||||
|
cardStyles,
|
||||||
|
tableStyles,
|
||||||
|
paginationStyles,
|
||||||
|
formStyles,
|
||||||
|
buttonStyles,
|
||||||
|
badgeStyles,
|
||||||
|
utilityStyles,
|
||||||
|
} from "../../styles/shared";
|
||||||
|
|
||||||
const { READY, SPENT, REVOKED } = constants.inviteStatuses;
|
const { READY, SPENT, REVOKED } = constants.inviteStatuses;
|
||||||
|
|
||||||
|
|
@ -101,11 +110,11 @@ export default function AdminInvitesPage() {
|
||||||
const getStatusBadgeStyle = (status: string) => {
|
const getStatusBadgeStyle = (status: string) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case READY:
|
case READY:
|
||||||
return styles.statusReady;
|
return badgeStyles.badgeReady;
|
||||||
case SPENT:
|
case SPENT:
|
||||||
return styles.statusSpent;
|
return badgeStyles.badgeSuccess;
|
||||||
case REVOKED:
|
case REVOKED:
|
||||||
return styles.statusRevoked;
|
return badgeStyles.badgeError;
|
||||||
default:
|
default:
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
|
|
@ -113,8 +122,8 @@ export default function AdminInvitesPage() {
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<main style={styles.main}>
|
<main style={layoutStyles.main}>
|
||||||
<div style={styles.loader}>Loading...</div>
|
<div style={layoutStyles.loader}>Loading...</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -124,21 +133,21 @@ export default function AdminInvitesPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main style={styles.main}>
|
<main style={layoutStyles.main}>
|
||||||
<Header currentPage="admin-invites" />
|
<Header currentPage="admin-invites" />
|
||||||
|
|
||||||
<div style={styles.content}>
|
<div style={layoutStyles.contentScrollable}>
|
||||||
<div style={styles.pageContainer}>
|
<div style={styles.pageContainer}>
|
||||||
{/* Create Invite Section */}
|
{/* Create Invite Section */}
|
||||||
<div style={styles.createCard}>
|
<div style={styles.createCard}>
|
||||||
<h2 style={styles.createTitle}>Create Invite</h2>
|
<h2 style={styles.createTitle}>Create Invite</h2>
|
||||||
<div style={styles.createForm}>
|
<div style={styles.createForm}>
|
||||||
<div style={styles.inputGroup}>
|
<div style={formStyles.field}>
|
||||||
<label style={styles.inputLabel}>Godfather (user who can share this invite)</label>
|
<label style={styles.inputLabel}>Godfather (user who can share this invite)</label>
|
||||||
<select
|
<select
|
||||||
value={newGodfatherId}
|
value={newGodfatherId}
|
||||||
onChange={(e) => setNewGodfatherId(e.target.value)}
|
onChange={(e) => setNewGodfatherId(e.target.value)}
|
||||||
style={styles.select}
|
style={{ ...formStyles.select, maxWidth: "400px" }}
|
||||||
>
|
>
|
||||||
<option value="">Select a user...</option>
|
<option value="">Select a user...</option>
|
||||||
{users.map((u) => (
|
{users.map((u) => (
|
||||||
|
|
@ -148,7 +157,7 @@ export default function AdminInvitesPage() {
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
{users.length === 0 && (
|
{users.length === 0 && (
|
||||||
<span style={styles.inputHint}>
|
<span style={formStyles.hint}>
|
||||||
No users loaded yet. Create at least one invite to populate the list.
|
No users loaded yet. Create at least one invite to populate the list.
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
@ -158,8 +167,9 @@ export default function AdminInvitesPage() {
|
||||||
onClick={handleCreateInvite}
|
onClick={handleCreateInvite}
|
||||||
disabled={isCreating || !newGodfatherId}
|
disabled={isCreating || !newGodfatherId}
|
||||||
style={{
|
style={{
|
||||||
...styles.createButton,
|
...buttonStyles.accentButton,
|
||||||
...(!newGodfatherId ? styles.createButtonDisabled : {}),
|
alignSelf: "flex-start",
|
||||||
|
...(!newGodfatherId ? buttonStyles.buttonDisabled : {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isCreating ? "Creating..." : "Create Invite"}
|
{isCreating ? "Creating..." : "Create Invite"}
|
||||||
|
|
@ -168,10 +178,10 @@ export default function AdminInvitesPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Invites Table */}
|
{/* Invites Table */}
|
||||||
<div style={styles.tableCard}>
|
<div style={cardStyles.tableCard}>
|
||||||
<div style={styles.tableHeader}>
|
<div style={tableStyles.tableHeader}>
|
||||||
<h2 style={styles.tableTitle}>All Invites</h2>
|
<h2 style={tableStyles.tableTitle}>All Invites</h2>
|
||||||
<div style={styles.filterGroup}>
|
<div style={utilityStyles.filterGroup}>
|
||||||
<select
|
<select
|
||||||
value={statusFilter}
|
value={statusFilter}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|
@ -185,49 +195,49 @@ export default function AdminInvitesPage() {
|
||||||
<option value={SPENT}>Spent</option>
|
<option value={SPENT}>Spent</option>
|
||||||
<option value={REVOKED}>Revoked</option>
|
<option value={REVOKED}>Revoked</option>
|
||||||
</select>
|
</select>
|
||||||
<span style={styles.totalCount}>{data?.total ?? 0} invites</span>
|
<span style={tableStyles.totalCount}>{data?.total ?? 0} invites</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={styles.tableWrapper}>
|
<div style={tableStyles.tableWrapper}>
|
||||||
<table style={styles.table}>
|
<table style={tableStyles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style={styles.th}>Code</th>
|
<th style={tableStyles.th}>Code</th>
|
||||||
<th style={styles.th}>Godfather</th>
|
<th style={tableStyles.th}>Godfather</th>
|
||||||
<th style={styles.th}>Status</th>
|
<th style={tableStyles.th}>Status</th>
|
||||||
<th style={styles.th}>Used By</th>
|
<th style={tableStyles.th}>Used By</th>
|
||||||
<th style={styles.th}>Created</th>
|
<th style={tableStyles.th}>Created</th>
|
||||||
<th style={styles.th}>Actions</th>
|
<th style={tableStyles.th}>Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{error && (
|
{error && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={6} style={styles.errorRow}>
|
<td colSpan={6} style={tableStyles.errorRow}>
|
||||||
{error}
|
{error}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
{!error &&
|
{!error &&
|
||||||
data?.records.map((record) => (
|
data?.records.map((record) => (
|
||||||
<tr key={record.id} style={styles.tr}>
|
<tr key={record.id} style={tableStyles.tr}>
|
||||||
<td style={styles.tdCode}>{record.identifier}</td>
|
<td style={tableStyles.tdMono}>{record.identifier}</td>
|
||||||
<td style={styles.td}>{record.godfather_email}</td>
|
<td style={tableStyles.td}>{record.godfather_email}</td>
|
||||||
<td style={styles.td}>
|
<td style={tableStyles.td}>
|
||||||
<span
|
<span
|
||||||
style={{ ...styles.statusBadge, ...getStatusBadgeStyle(record.status) }}
|
style={{ ...badgeStyles.badge, ...getStatusBadgeStyle(record.status) }}
|
||||||
>
|
>
|
||||||
{record.status}
|
{record.status}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td style={styles.td}>{record.used_by_email || "-"}</td>
|
<td style={tableStyles.td}>{record.used_by_email || "-"}</td>
|
||||||
<td style={styles.tdDate}>{formatDate(record.created_at)}</td>
|
<td style={tableStyles.tdDate}>{formatDate(record.created_at)}</td>
|
||||||
<td style={styles.td}>
|
<td style={tableStyles.td}>
|
||||||
{record.status === READY && (
|
{record.status === READY && (
|
||||||
<button
|
<button
|
||||||
onClick={() => handleRevoke(record.id)}
|
onClick={() => handleRevoke(record.id)}
|
||||||
style={styles.revokeButton}
|
style={buttonStyles.dangerButton}
|
||||||
>
|
>
|
||||||
Revoke
|
Revoke
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -237,7 +247,7 @@ export default function AdminInvitesPage() {
|
||||||
))}
|
))}
|
||||||
{!error && (!data || data.records.length === 0) && (
|
{!error && (!data || data.records.length === 0) && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={6} style={styles.emptyRow}>
|
<td colSpan={6} style={tableStyles.emptyRow}>
|
||||||
No invites yet
|
No invites yet
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -247,21 +257,21 @@ export default function AdminInvitesPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data && data.total_pages > 1 && (
|
{data && data.total_pages > 1 && (
|
||||||
<div style={styles.pagination}>
|
<div style={paginationStyles.pagination}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
onClick={() => setPage((p) => Math.max(1, p - 1))}
|
||||||
disabled={page === 1}
|
disabled={page === 1}
|
||||||
style={styles.pageBtn}
|
style={paginationStyles.pageBtn}
|
||||||
>
|
>
|
||||||
←
|
←
|
||||||
</button>
|
</button>
|
||||||
<span style={styles.pageInfo}>
|
<span style={paginationStyles.pageInfo}>
|
||||||
{page} / {data.total_pages}
|
{page} / {data.total_pages}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setPage((p) => Math.min(data.total_pages, p + 1))}
|
onClick={() => setPage((p) => Math.min(data.total_pages, p + 1))}
|
||||||
disabled={page === data.total_pages}
|
disabled={page === data.total_pages}
|
||||||
style={styles.pageBtn}
|
style={paginationStyles.pageBtn}
|
||||||
>
|
>
|
||||||
→
|
→
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -274,12 +284,8 @@ export default function AdminInvitesPage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageStyles: Record<string, React.CSSProperties> = {
|
// Page-specific styles
|
||||||
content: {
|
const styles: Record<string, React.CSSProperties> = {
|
||||||
flex: 1,
|
|
||||||
padding: "2rem",
|
|
||||||
overflowY: "auto",
|
|
||||||
},
|
|
||||||
pageContainer: {
|
pageContainer: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
|
@ -305,82 +311,16 @@ const pageStyles: Record<string, React.CSSProperties> = {
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: "1rem",
|
gap: "1rem",
|
||||||
},
|
},
|
||||||
inputGroup: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "0.5rem",
|
|
||||||
},
|
|
||||||
inputLabel: {
|
inputLabel: {
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||||
fontSize: "0.8rem",
|
fontSize: "0.8rem",
|
||||||
color: "rgba(255, 255, 255, 0.5)",
|
color: "rgba(255, 255, 255, 0.5)",
|
||||||
},
|
},
|
||||||
select: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
fontSize: "0.9rem",
|
|
||||||
padding: "0.75rem",
|
|
||||||
background: "rgba(255, 255, 255, 0.05)",
|
|
||||||
border: "1px solid rgba(255, 255, 255, 0.1)",
|
|
||||||
borderRadius: "8px",
|
|
||||||
color: "#fff",
|
|
||||||
maxWidth: "400px",
|
|
||||||
cursor: "pointer",
|
|
||||||
},
|
|
||||||
createError: {
|
createError: {
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||||
fontSize: "0.85rem",
|
fontSize: "0.85rem",
|
||||||
color: "#f87171",
|
color: "#f87171",
|
||||||
},
|
},
|
||||||
createButton: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
fontSize: "0.9rem",
|
|
||||||
fontWeight: 500,
|
|
||||||
padding: "0.75rem 1.5rem",
|
|
||||||
background: "rgba(99, 102, 241, 0.3)",
|
|
||||||
color: "#fff",
|
|
||||||
border: "1px solid rgba(99, 102, 241, 0.5)",
|
|
||||||
borderRadius: "8px",
|
|
||||||
cursor: "pointer",
|
|
||||||
alignSelf: "flex-start",
|
|
||||||
},
|
|
||||||
createButtonDisabled: {
|
|
||||||
opacity: 0.5,
|
|
||||||
cursor: "not-allowed",
|
|
||||||
},
|
|
||||||
inputHint: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
fontSize: "0.75rem",
|
|
||||||
color: "rgba(255, 255, 255, 0.4)",
|
|
||||||
fontStyle: "italic",
|
|
||||||
},
|
|
||||||
tableCard: {
|
|
||||||
background: "rgba(255, 255, 255, 0.03)",
|
|
||||||
backdropFilter: "blur(10px)",
|
|
||||||
border: "1px solid rgba(255, 255, 255, 0.08)",
|
|
||||||
borderRadius: "20px",
|
|
||||||
padding: "1.5rem",
|
|
||||||
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.5)",
|
|
||||||
},
|
|
||||||
tableHeader: {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
marginBottom: "1rem",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
gap: "1rem",
|
|
||||||
},
|
|
||||||
tableTitle: {
|
|
||||||
fontFamily: "'Instrument Serif', Georgia, serif",
|
|
||||||
fontSize: "1.5rem",
|
|
||||||
fontWeight: 400,
|
|
||||||
color: "#fff",
|
|
||||||
margin: 0,
|
|
||||||
},
|
|
||||||
filterGroup: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "1rem",
|
|
||||||
},
|
|
||||||
filterSelect: {
|
filterSelect: {
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||||
fontSize: "0.85rem",
|
fontSize: "0.85rem",
|
||||||
|
|
@ -391,115 +331,4 @@ const pageStyles: Record<string, React.CSSProperties> = {
|
||||||
color: "#fff",
|
color: "#fff",
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
},
|
},
|
||||||
totalCount: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
fontSize: "0.875rem",
|
|
||||||
color: "rgba(255, 255, 255, 0.4)",
|
|
||||||
},
|
|
||||||
tableWrapper: {
|
|
||||||
overflowX: "auto",
|
|
||||||
},
|
|
||||||
table: {
|
|
||||||
width: "100%",
|
|
||||||
borderCollapse: "collapse",
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
},
|
|
||||||
th: {
|
|
||||||
textAlign: "left",
|
|
||||||
padding: "0.75rem 1rem",
|
|
||||||
fontSize: "0.75rem",
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "rgba(255, 255, 255, 0.4)",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
letterSpacing: "0.05em",
|
|
||||||
borderBottom: "1px solid rgba(255, 255, 255, 0.08)",
|
|
||||||
},
|
|
||||||
tr: {
|
|
||||||
borderBottom: "1px solid rgba(255, 255, 255, 0.04)",
|
|
||||||
},
|
|
||||||
td: {
|
|
||||||
padding: "0.875rem 1rem",
|
|
||||||
fontSize: "0.875rem",
|
|
||||||
color: "rgba(255, 255, 255, 0.7)",
|
|
||||||
},
|
|
||||||
tdCode: {
|
|
||||||
padding: "0.875rem 1rem",
|
|
||||||
fontSize: "0.875rem",
|
|
||||||
color: "#fff",
|
|
||||||
fontFamily: "'DM Mono', monospace",
|
|
||||||
},
|
|
||||||
tdDate: {
|
|
||||||
padding: "0.875rem 1rem",
|
|
||||||
fontSize: "0.75rem",
|
|
||||||
color: "rgba(255, 255, 255, 0.4)",
|
|
||||||
},
|
|
||||||
statusBadge: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
fontSize: "0.7rem",
|
|
||||||
fontWeight: 500,
|
|
||||||
padding: "0.25rem 0.5rem",
|
|
||||||
borderRadius: "4px",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
},
|
|
||||||
statusReady: {
|
|
||||||
background: "rgba(99, 102, 241, 0.2)",
|
|
||||||
color: "rgba(129, 140, 248, 0.9)",
|
|
||||||
},
|
|
||||||
statusSpent: {
|
|
||||||
background: "rgba(34, 197, 94, 0.2)",
|
|
||||||
color: "rgba(34, 197, 94, 0.9)",
|
|
||||||
},
|
|
||||||
statusRevoked: {
|
|
||||||
background: "rgba(239, 68, 68, 0.2)",
|
|
||||||
color: "rgba(239, 68, 68, 0.9)",
|
|
||||||
},
|
|
||||||
revokeButton: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
fontSize: "0.75rem",
|
|
||||||
padding: "0.4rem 0.75rem",
|
|
||||||
background: "rgba(239, 68, 68, 0.15)",
|
|
||||||
color: "rgba(239, 68, 68, 0.9)",
|
|
||||||
border: "1px solid rgba(239, 68, 68, 0.3)",
|
|
||||||
borderRadius: "6px",
|
|
||||||
cursor: "pointer",
|
|
||||||
},
|
|
||||||
emptyRow: {
|
|
||||||
padding: "2rem 1rem",
|
|
||||||
textAlign: "center",
|
|
||||||
color: "rgba(255, 255, 255, 0.3)",
|
|
||||||
fontSize: "0.875rem",
|
|
||||||
},
|
|
||||||
errorRow: {
|
|
||||||
padding: "2rem 1rem",
|
|
||||||
textAlign: "center",
|
|
||||||
color: "#f87171",
|
|
||||||
fontSize: "0.875rem",
|
|
||||||
},
|
|
||||||
pagination: {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "1rem",
|
|
||||||
marginTop: "1rem",
|
|
||||||
paddingTop: "1rem",
|
|
||||||
borderTop: "1px solid rgba(255, 255, 255, 0.06)",
|
|
||||||
},
|
|
||||||
pageBtn: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
padding: "0.5rem 1rem",
|
|
||||||
fontSize: "1rem",
|
|
||||||
background: "rgba(255, 255, 255, 0.05)",
|
|
||||||
color: "rgba(255, 255, 255, 0.7)",
|
|
||||||
border: "1px solid rgba(255, 255, 255, 0.1)",
|
|
||||||
borderRadius: "8px",
|
|
||||||
cursor: "pointer",
|
|
||||||
transition: "all 0.2s",
|
|
||||||
},
|
|
||||||
pageInfo: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
fontSize: "0.875rem",
|
|
||||||
color: "rgba(255, 255, 255, 0.5)",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = { ...sharedStyles, ...pageStyles };
|
|
||||||
|
|
|
||||||
|
|
@ -9,145 +9,16 @@ import { useRequireAuth } from "../hooks/useRequireAuth";
|
||||||
import { components } from "../generated/api";
|
import { components } from "../generated/api";
|
||||||
import { formatDateTime } from "../utils/date";
|
import { formatDateTime } from "../utils/date";
|
||||||
import { getStatusDisplay } from "../utils/appointment";
|
import { getStatusDisplay } from "../utils/appointment";
|
||||||
import { sharedStyles } from "../styles/shared";
|
import {
|
||||||
|
layoutStyles,
|
||||||
|
typographyStyles,
|
||||||
|
bannerStyles,
|
||||||
|
badgeStyles,
|
||||||
|
buttonStyles,
|
||||||
|
} from "../styles/shared";
|
||||||
|
|
||||||
type AppointmentResponse = components["schemas"]["AppointmentResponse"];
|
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: "800px",
|
|
||||||
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",
|
|
||||||
},
|
|
||||||
section: {
|
|
||||||
marginBottom: "2rem",
|
|
||||||
},
|
|
||||||
sectionTitle: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
fontSize: "1.1rem",
|
|
||||||
fontWeight: 500,
|
|
||||||
color: "#fff",
|
|
||||||
marginBottom: "1rem",
|
|
||||||
},
|
|
||||||
sectionTitleMuted: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
fontSize: "1.1rem",
|
|
||||||
fontWeight: 500,
|
|
||||||
color: "rgba(255, 255, 255, 0.5)",
|
|
||||||
marginBottom: "1rem",
|
|
||||||
},
|
|
||||||
appointmentList: {
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "0.75rem",
|
|
||||||
},
|
|
||||||
appointmentCard: {
|
|
||||||
background: "rgba(255, 255, 255, 0.03)",
|
|
||||||
border: "1px solid rgba(255, 255, 255, 0.08)",
|
|
||||||
borderRadius: "12px",
|
|
||||||
padding: "1.25rem",
|
|
||||||
transition: "all 0.2s",
|
|
||||||
},
|
|
||||||
appointmentCardPast: {
|
|
||||||
opacity: 0.6,
|
|
||||||
background: "rgba(255, 255, 255, 0.01)",
|
|
||||||
},
|
|
||||||
appointmentHeader: {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "flex-start",
|
|
||||||
gap: "1rem",
|
|
||||||
},
|
|
||||||
appointmentTime: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
fontSize: "1rem",
|
|
||||||
fontWeight: 500,
|
|
||||||
color: "#fff",
|
|
||||||
marginBottom: "0.25rem",
|
|
||||||
},
|
|
||||||
appointmentNote: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
fontSize: "0.875rem",
|
|
||||||
color: "rgba(255, 255, 255, 0.5)",
|
|
||||||
marginBottom: "0.5rem",
|
|
||||||
},
|
|
||||||
statusBadge: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
fontSize: "0.75rem",
|
|
||||||
fontWeight: 500,
|
|
||||||
padding: "0.25rem 0.75rem",
|
|
||||||
borderRadius: "9999px",
|
|
||||||
display: "inline-block",
|
|
||||||
},
|
|
||||||
buttonGroup: {
|
|
||||||
display: "flex",
|
|
||||||
gap: "0.5rem",
|
|
||||||
},
|
|
||||||
cancelButton: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
padding: "0.35rem 0.75rem",
|
|
||||||
fontSize: "0.75rem",
|
|
||||||
background: "rgba(255, 255, 255, 0.05)",
|
|
||||||
border: "1px solid rgba(255, 255, 255, 0.1)",
|
|
||||||
borderRadius: "6px",
|
|
||||||
color: "rgba(255, 255, 255, 0.7)",
|
|
||||||
cursor: "pointer",
|
|
||||||
transition: "all 0.2s",
|
|
||||||
},
|
|
||||||
confirmButton: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
padding: "0.35rem 0.75rem",
|
|
||||||
fontSize: "0.75rem",
|
|
||||||
background: "rgba(239, 68, 68, 0.2)",
|
|
||||||
border: "1px solid rgba(239, 68, 68, 0.3)",
|
|
||||||
borderRadius: "6px",
|
|
||||||
color: "#f87171",
|
|
||||||
cursor: "pointer",
|
|
||||||
transition: "all 0.2s",
|
|
||||||
},
|
|
||||||
emptyState: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
color: "rgba(255, 255, 255, 0.4)",
|
|
||||||
textAlign: "center",
|
|
||||||
padding: "3rem",
|
|
||||||
},
|
|
||||||
emptyStateLink: {
|
|
||||||
color: "#a78bfa",
|
|
||||||
textDecoration: "none",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const styles = { ...sharedStyles, ...pageStyles };
|
|
||||||
|
|
||||||
export default function AppointmentsPage() {
|
export default function AppointmentsPage() {
|
||||||
const { user, isLoading, isAuthorized } = useRequireAuth({
|
const { user, isLoading, isAuthorized } = useRequireAuth({
|
||||||
requiredPermission: Permission.VIEW_OWN_APPOINTMENTS,
|
requiredPermission: Permission.VIEW_OWN_APPOINTMENTS,
|
||||||
|
|
@ -195,8 +66,8 @@ export default function AppointmentsPage() {
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<main style={styles.main}>
|
<main style={layoutStyles.main}>
|
||||||
<div style={styles.loader}>Loading...</div>
|
<div style={layoutStyles.loader}>Loading...</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -213,13 +84,13 @@ export default function AppointmentsPage() {
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main style={styles.main}>
|
<main style={layoutStyles.main}>
|
||||||
<Header currentPage="appointments" />
|
<Header currentPage="appointments" />
|
||||||
<div style={styles.content}>
|
<div style={styles.content}>
|
||||||
<h1 style={styles.pageTitle}>My Appointments</h1>
|
<h1 style={typographyStyles.pageTitle}>My Appointments</h1>
|
||||||
<p style={styles.pageSubtitle}>View and manage your booked appointments</p>
|
<p style={typographyStyles.pageSubtitle}>View and manage your booked appointments</p>
|
||||||
|
|
||||||
{error && <div style={styles.errorBanner}>{error}</div>}
|
{error && <div style={bannerStyles.errorBanner}>{error}</div>}
|
||||||
|
|
||||||
{isLoadingAppointments ? (
|
{isLoadingAppointments ? (
|
||||||
<div style={styles.emptyState}>Loading appointments...</div>
|
<div style={styles.emptyState}>Loading appointments...</div>
|
||||||
|
|
@ -249,7 +120,7 @@ export default function AppointmentsPage() {
|
||||||
{apt.note && <div style={styles.appointmentNote}>{apt.note}</div>}
|
{apt.note && <div style={styles.appointmentNote}>{apt.note}</div>}
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
...styles.statusBadge,
|
...badgeStyles.badge,
|
||||||
background: status.bgColor,
|
background: status.bgColor,
|
||||||
color: status.textColor,
|
color: status.textColor,
|
||||||
}}
|
}}
|
||||||
|
|
@ -271,7 +142,7 @@ export default function AppointmentsPage() {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setConfirmCancelId(null)}
|
onClick={() => setConfirmCancelId(null)}
|
||||||
style={styles.cancelButton}
|
style={buttonStyles.secondaryButton}
|
||||||
>
|
>
|
||||||
No
|
No
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -279,7 +150,7 @@ export default function AppointmentsPage() {
|
||||||
) : (
|
) : (
|
||||||
<button
|
<button
|
||||||
onClick={() => setConfirmCancelId(apt.id)}
|
onClick={() => setConfirmCancelId(apt.id)}
|
||||||
style={styles.cancelButton}
|
style={buttonStyles.secondaryButton}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -297,7 +168,7 @@ export default function AppointmentsPage() {
|
||||||
{/* Past/Cancelled Appointments */}
|
{/* Past/Cancelled Appointments */}
|
||||||
{pastOrCancelledAppointments.length > 0 && (
|
{pastOrCancelledAppointments.length > 0 && (
|
||||||
<div style={styles.section}>
|
<div style={styles.section}>
|
||||||
<h2 style={styles.sectionTitleMuted}>
|
<h2 style={typographyStyles.sectionTitleMuted}>
|
||||||
Past & Cancelled ({pastOrCancelledAppointments.length})
|
Past & Cancelled ({pastOrCancelledAppointments.length})
|
||||||
</h2>
|
</h2>
|
||||||
<div style={styles.appointmentList}>
|
<div style={styles.appointmentList}>
|
||||||
|
|
@ -312,7 +183,7 @@ export default function AppointmentsPage() {
|
||||||
{apt.note && <div style={styles.appointmentNote}>{apt.note}</div>}
|
{apt.note && <div style={styles.appointmentNote}>{apt.note}</div>}
|
||||||
<span
|
<span
|
||||||
style={{
|
style={{
|
||||||
...styles.statusBadge,
|
...badgeStyles.badge,
|
||||||
background: status.bgColor,
|
background: status.bgColor,
|
||||||
color: status.textColor,
|
color: status.textColor,
|
||||||
}}
|
}}
|
||||||
|
|
@ -331,3 +202,84 @@ export default function AppointmentsPage() {
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Page-specific styles
|
||||||
|
const styles: Record<string, React.CSSProperties> = {
|
||||||
|
content: {
|
||||||
|
flex: 1,
|
||||||
|
padding: "2rem",
|
||||||
|
maxWidth: "800px",
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
appointmentList: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "0.75rem",
|
||||||
|
},
|
||||||
|
appointmentCard: {
|
||||||
|
background: "rgba(255, 255, 255, 0.03)",
|
||||||
|
border: "1px solid rgba(255, 255, 255, 0.08)",
|
||||||
|
borderRadius: "12px",
|
||||||
|
padding: "1.25rem",
|
||||||
|
transition: "all 0.2s",
|
||||||
|
},
|
||||||
|
appointmentCardPast: {
|
||||||
|
opacity: 0.6,
|
||||||
|
background: "rgba(255, 255, 255, 0.01)",
|
||||||
|
},
|
||||||
|
appointmentHeader: {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "flex-start",
|
||||||
|
gap: "1rem",
|
||||||
|
},
|
||||||
|
appointmentTime: {
|
||||||
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||||
|
fontSize: "1rem",
|
||||||
|
fontWeight: 500,
|
||||||
|
color: "#fff",
|
||||||
|
marginBottom: "0.25rem",
|
||||||
|
},
|
||||||
|
appointmentNote: {
|
||||||
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
color: "rgba(255, 255, 255, 0.5)",
|
||||||
|
marginBottom: "0.5rem",
|
||||||
|
},
|
||||||
|
buttonGroup: {
|
||||||
|
display: "flex",
|
||||||
|
gap: "0.5rem",
|
||||||
|
},
|
||||||
|
confirmButton: {
|
||||||
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||||
|
padding: "0.35rem 0.75rem",
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
background: "rgba(239, 68, 68, 0.2)",
|
||||||
|
border: "1px solid rgba(239, 68, 68, 0.3)",
|
||||||
|
borderRadius: "6px",
|
||||||
|
color: "#f87171",
|
||||||
|
cursor: "pointer",
|
||||||
|
transition: "all 0.2s",
|
||||||
|
},
|
||||||
|
emptyState: {
|
||||||
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||||
|
color: "rgba(255, 255, 255, 0.4)",
|
||||||
|
textAlign: "center",
|
||||||
|
padding: "3rem",
|
||||||
|
},
|
||||||
|
emptyStateLink: {
|
||||||
|
color: "#a78bfa",
|
||||||
|
textDecoration: "none",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { useEffect, useState, useCallback } from "react";
|
import { useEffect, useState, useCallback } from "react";
|
||||||
import { Permission } from "../auth-context";
|
import { Permission } from "../auth-context";
|
||||||
import { api } from "../api";
|
import { api } from "../api";
|
||||||
import { sharedStyles } from "../styles/shared";
|
import { layoutStyles, cardStyles, tableStyles, paginationStyles } from "../styles/shared";
|
||||||
import { Header } from "../components/Header";
|
import { Header } from "../components/Header";
|
||||||
import { useRequireAuth } from "../hooks/useRequireAuth";
|
import { useRequireAuth } from "../hooks/useRequireAuth";
|
||||||
import { components } from "../generated/api";
|
import { components } from "../generated/api";
|
||||||
|
|
@ -68,8 +68,8 @@ export default function AuditPage() {
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<main style={styles.main}>
|
<main style={layoutStyles.main}>
|
||||||
<div style={styles.loader}>Loading...</div>
|
<div style={layoutStyles.loader}>Loading...</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -79,47 +79,47 @@ export default function AuditPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main style={styles.main}>
|
<main style={layoutStyles.main}>
|
||||||
<Header currentPage="audit" />
|
<Header currentPage="audit" />
|
||||||
|
|
||||||
<div style={styles.content}>
|
<div style={layoutStyles.contentScrollable}>
|
||||||
<div style={styles.tablesContainer}>
|
<div style={styles.tablesContainer}>
|
||||||
{/* Counter Records Table */}
|
{/* Counter Records Table */}
|
||||||
<div style={styles.tableCard}>
|
<div style={cardStyles.tableCard}>
|
||||||
<div style={styles.tableHeader}>
|
<div style={tableStyles.tableHeader}>
|
||||||
<h2 style={styles.tableTitle}>Counter Activity</h2>
|
<h2 style={tableStyles.tableTitle}>Counter Activity</h2>
|
||||||
<span style={styles.totalCount}>{counterData?.total ?? 0} records</span>
|
<span style={tableStyles.totalCount}>{counterData?.total ?? 0} records</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.tableWrapper}>
|
<div style={tableStyles.tableWrapper}>
|
||||||
<table style={styles.table}>
|
<table style={tableStyles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style={styles.th}>User</th>
|
<th style={tableStyles.th}>User</th>
|
||||||
<th style={styles.th}>Before</th>
|
<th style={tableStyles.th}>Before</th>
|
||||||
<th style={styles.th}>After</th>
|
<th style={tableStyles.th}>After</th>
|
||||||
<th style={styles.th}>Date</th>
|
<th style={tableStyles.th}>Date</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{counterError && (
|
{counterError && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={4} style={styles.errorRow}>
|
<td colSpan={4} style={tableStyles.errorRow}>
|
||||||
{counterError}
|
{counterError}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
{!counterError &&
|
{!counterError &&
|
||||||
counterData?.records.map((record) => (
|
counterData?.records.map((record) => (
|
||||||
<tr key={record.id} style={styles.tr}>
|
<tr key={record.id} style={tableStyles.tr}>
|
||||||
<td style={styles.td}>{record.user_email}</td>
|
<td style={tableStyles.td}>{record.user_email}</td>
|
||||||
<td style={styles.tdNum}>{record.value_before}</td>
|
<td style={tableStyles.tdNum}>{record.value_before}</td>
|
||||||
<td style={styles.tdNum}>{record.value_after}</td>
|
<td style={tableStyles.tdNum}>{record.value_after}</td>
|
||||||
<td style={styles.tdDate}>{formatDate(record.created_at)}</td>
|
<td style={tableStyles.tdDate}>{formatDate(record.created_at)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{!counterError && (!counterData || counterData.records.length === 0) && (
|
{!counterError && (!counterData || counterData.records.length === 0) && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={4} style={styles.emptyRow}>
|
<td colSpan={4} style={tableStyles.emptyRow}>
|
||||||
No records yet
|
No records yet
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -128,21 +128,21 @@ export default function AuditPage() {
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{counterData && counterData.total_pages > 1 && (
|
{counterData && counterData.total_pages > 1 && (
|
||||||
<div style={styles.pagination}>
|
<div style={paginationStyles.pagination}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setCounterPage((p) => Math.max(1, p - 1))}
|
onClick={() => setCounterPage((p) => Math.max(1, p - 1))}
|
||||||
disabled={counterPage === 1}
|
disabled={counterPage === 1}
|
||||||
style={styles.pageBtn}
|
style={paginationStyles.pageBtn}
|
||||||
>
|
>
|
||||||
←
|
←
|
||||||
</button>
|
</button>
|
||||||
<span style={styles.pageInfo}>
|
<span style={paginationStyles.pageInfo}>
|
||||||
{counterPage} / {counterData.total_pages}
|
{counterPage} / {counterData.total_pages}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setCounterPage((p) => Math.min(counterData.total_pages, p + 1))}
|
onClick={() => setCounterPage((p) => Math.min(counterData.total_pages, p + 1))}
|
||||||
disabled={counterPage === counterData.total_pages}
|
disabled={counterPage === counterData.total_pages}
|
||||||
style={styles.pageBtn}
|
style={paginationStyles.pageBtn}
|
||||||
>
|
>
|
||||||
→
|
→
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -151,43 +151,43 @@ export default function AuditPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sum Records Table */}
|
{/* Sum Records Table */}
|
||||||
<div style={styles.tableCard}>
|
<div style={cardStyles.tableCard}>
|
||||||
<div style={styles.tableHeader}>
|
<div style={tableStyles.tableHeader}>
|
||||||
<h2 style={styles.tableTitle}>Sum Activity</h2>
|
<h2 style={tableStyles.tableTitle}>Sum Activity</h2>
|
||||||
<span style={styles.totalCount}>{sumData?.total ?? 0} records</span>
|
<span style={tableStyles.totalCount}>{sumData?.total ?? 0} records</span>
|
||||||
</div>
|
</div>
|
||||||
<div style={styles.tableWrapper}>
|
<div style={tableStyles.tableWrapper}>
|
||||||
<table style={styles.table}>
|
<table style={tableStyles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style={styles.th}>User</th>
|
<th style={tableStyles.th}>User</th>
|
||||||
<th style={styles.th}>A</th>
|
<th style={tableStyles.th}>A</th>
|
||||||
<th style={styles.th}>B</th>
|
<th style={tableStyles.th}>B</th>
|
||||||
<th style={styles.th}>Result</th>
|
<th style={tableStyles.th}>Result</th>
|
||||||
<th style={styles.th}>Date</th>
|
<th style={tableStyles.th}>Date</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{sumError && (
|
{sumError && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={5} style={styles.errorRow}>
|
<td colSpan={5} style={tableStyles.errorRow}>
|
||||||
{sumError}
|
{sumError}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
)}
|
)}
|
||||||
{!sumError &&
|
{!sumError &&
|
||||||
sumData?.records.map((record) => (
|
sumData?.records.map((record) => (
|
||||||
<tr key={record.id} style={styles.tr}>
|
<tr key={record.id} style={tableStyles.tr}>
|
||||||
<td style={styles.td}>{record.user_email}</td>
|
<td style={tableStyles.td}>{record.user_email}</td>
|
||||||
<td style={styles.tdNum}>{record.a}</td>
|
<td style={tableStyles.tdNum}>{record.a}</td>
|
||||||
<td style={styles.tdNum}>{record.b}</td>
|
<td style={tableStyles.tdNum}>{record.b}</td>
|
||||||
<td style={styles.tdResult}>{record.result}</td>
|
<td style={styles.tdResult}>{record.result}</td>
|
||||||
<td style={styles.tdDate}>{formatDate(record.created_at)}</td>
|
<td style={tableStyles.tdDate}>{formatDate(record.created_at)}</td>
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
{!sumError && (!sumData || sumData.records.length === 0) && (
|
{!sumError && (!sumData || sumData.records.length === 0) && (
|
||||||
<tr>
|
<tr>
|
||||||
<td colSpan={5} style={styles.emptyRow}>
|
<td colSpan={5} style={tableStyles.emptyRow}>
|
||||||
No records yet
|
No records yet
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -196,21 +196,21 @@ export default function AuditPage() {
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
{sumData && sumData.total_pages > 1 && (
|
{sumData && sumData.total_pages > 1 && (
|
||||||
<div style={styles.pagination}>
|
<div style={paginationStyles.pagination}>
|
||||||
<button
|
<button
|
||||||
onClick={() => setSumPage((p) => Math.max(1, p - 1))}
|
onClick={() => setSumPage((p) => Math.max(1, p - 1))}
|
||||||
disabled={sumPage === 1}
|
disabled={sumPage === 1}
|
||||||
style={styles.pageBtn}
|
style={paginationStyles.pageBtn}
|
||||||
>
|
>
|
||||||
←
|
←
|
||||||
</button>
|
</button>
|
||||||
<span style={styles.pageInfo}>
|
<span style={paginationStyles.pageInfo}>
|
||||||
{sumPage} / {sumData.total_pages}
|
{sumPage} / {sumData.total_pages}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => setSumPage((p) => Math.min(sumData.total_pages, p + 1))}
|
onClick={() => setSumPage((p) => Math.min(sumData.total_pages, p + 1))}
|
||||||
disabled={sumPage === sumData.total_pages}
|
disabled={sumPage === sumData.total_pages}
|
||||||
style={styles.pageBtn}
|
style={paginationStyles.pageBtn}
|
||||||
>
|
>
|
||||||
→
|
→
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -223,13 +223,8 @@ export default function AuditPage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageStyles: Record<string, React.CSSProperties> = {
|
// Page-specific styles only
|
||||||
// Override content for audit-specific layout
|
const styles: Record<string, React.CSSProperties> = {
|
||||||
content: {
|
|
||||||
flex: 1,
|
|
||||||
padding: "2rem",
|
|
||||||
overflowY: "auto",
|
|
||||||
},
|
|
||||||
tablesContainer: {
|
tablesContainer: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
|
@ -237,64 +232,6 @@ const pageStyles: Record<string, React.CSSProperties> = {
|
||||||
maxWidth: "1200px",
|
maxWidth: "1200px",
|
||||||
margin: "0 auto",
|
margin: "0 auto",
|
||||||
},
|
},
|
||||||
tableCard: {
|
|
||||||
background: "rgba(255, 255, 255, 0.03)",
|
|
||||||
backdropFilter: "blur(10px)",
|
|
||||||
border: "1px solid rgba(255, 255, 255, 0.08)",
|
|
||||||
borderRadius: "20px",
|
|
||||||
padding: "1.5rem",
|
|
||||||
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.5)",
|
|
||||||
},
|
|
||||||
tableHeader: {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
marginBottom: "1rem",
|
|
||||||
},
|
|
||||||
tableTitle: {
|
|
||||||
fontFamily: "'Instrument Serif', Georgia, serif",
|
|
||||||
fontSize: "1.5rem",
|
|
||||||
fontWeight: 400,
|
|
||||||
color: "#fff",
|
|
||||||
margin: 0,
|
|
||||||
},
|
|
||||||
totalCount: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
fontSize: "0.875rem",
|
|
||||||
color: "rgba(255, 255, 255, 0.4)",
|
|
||||||
},
|
|
||||||
tableWrapper: {
|
|
||||||
overflowX: "auto",
|
|
||||||
},
|
|
||||||
table: {
|
|
||||||
width: "100%",
|
|
||||||
borderCollapse: "collapse",
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
},
|
|
||||||
th: {
|
|
||||||
textAlign: "left",
|
|
||||||
padding: "0.75rem 1rem",
|
|
||||||
fontSize: "0.75rem",
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "rgba(255, 255, 255, 0.4)",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
letterSpacing: "0.05em",
|
|
||||||
borderBottom: "1px solid rgba(255, 255, 255, 0.08)",
|
|
||||||
},
|
|
||||||
tr: {
|
|
||||||
borderBottom: "1px solid rgba(255, 255, 255, 0.04)",
|
|
||||||
},
|
|
||||||
td: {
|
|
||||||
padding: "0.875rem 1rem",
|
|
||||||
fontSize: "0.875rem",
|
|
||||||
color: "rgba(255, 255, 255, 0.7)",
|
|
||||||
},
|
|
||||||
tdNum: {
|
|
||||||
padding: "0.875rem 1rem",
|
|
||||||
fontSize: "0.875rem",
|
|
||||||
color: "rgba(255, 255, 255, 0.9)",
|
|
||||||
fontFamily: "'DM Sans', monospace",
|
|
||||||
},
|
|
||||||
tdResult: {
|
tdResult: {
|
||||||
padding: "0.875rem 1rem",
|
padding: "0.875rem 1rem",
|
||||||
fontSize: "0.875rem",
|
fontSize: "0.875rem",
|
||||||
|
|
@ -302,48 +239,4 @@ const pageStyles: Record<string, React.CSSProperties> = {
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
fontFamily: "'DM Sans', monospace",
|
fontFamily: "'DM Sans', monospace",
|
||||||
},
|
},
|
||||||
tdDate: {
|
|
||||||
padding: "0.875rem 1rem",
|
|
||||||
fontSize: "0.75rem",
|
|
||||||
color: "rgba(255, 255, 255, 0.4)",
|
|
||||||
},
|
|
||||||
emptyRow: {
|
|
||||||
padding: "2rem 1rem",
|
|
||||||
textAlign: "center",
|
|
||||||
color: "rgba(255, 255, 255, 0.3)",
|
|
||||||
fontSize: "0.875rem",
|
|
||||||
},
|
|
||||||
errorRow: {
|
|
||||||
padding: "2rem 1rem",
|
|
||||||
textAlign: "center",
|
|
||||||
color: "#f87171",
|
|
||||||
fontSize: "0.875rem",
|
|
||||||
},
|
|
||||||
pagination: {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: "1rem",
|
|
||||||
marginTop: "1rem",
|
|
||||||
paddingTop: "1rem",
|
|
||||||
borderTop: "1px solid rgba(255, 255, 255, 0.06)",
|
|
||||||
},
|
|
||||||
pageBtn: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
padding: "0.5rem 1rem",
|
|
||||||
fontSize: "1rem",
|
|
||||||
background: "rgba(255, 255, 255, 0.05)",
|
|
||||||
color: "rgba(255, 255, 255, 0.7)",
|
|
||||||
border: "1px solid rgba(255, 255, 255, 0.1)",
|
|
||||||
borderRadius: "8px",
|
|
||||||
cursor: "pointer",
|
|
||||||
transition: "all 0.2s",
|
|
||||||
},
|
|
||||||
pageInfo: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
fontSize: "0.875rem",
|
|
||||||
color: "rgba(255, 255, 255, 0.5)",
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = { ...sharedStyles, ...pageStyles };
|
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,13 @@ import { useRequireAuth } from "../hooks/useRequireAuth";
|
||||||
import { components } from "../generated/api";
|
import { components } from "../generated/api";
|
||||||
import constants from "../../../shared/constants.json";
|
import constants from "../../../shared/constants.json";
|
||||||
import { formatDate, formatTime, getDateRange } from "../utils/date";
|
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;
|
const { slotDurationMinutes, maxAdvanceDays, minAdvanceDays, noteMaxLength } = constants.booking;
|
||||||
|
|
||||||
|
|
@ -17,205 +23,6 @@ type BookableSlot = components["schemas"]["BookableSlot"];
|
||||||
type AvailableSlotsResponse = components["schemas"]["AvailableSlotsResponse"];
|
type AvailableSlotsResponse = components["schemas"]["AvailableSlotsResponse"];
|
||||||
type AppointmentResponse = components["schemas"]["AppointmentResponse"];
|
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() {
|
export default function BookingPage() {
|
||||||
const { user, isLoading, isAuthorized } = useRequireAuth({
|
const { user, isLoading, isAuthorized } = useRequireAuth({
|
||||||
requiredPermission: Permission.BOOK_APPOINTMENT,
|
requiredPermission: Permission.BOOK_APPOINTMENT,
|
||||||
|
|
@ -345,8 +152,8 @@ export default function BookingPage() {
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<main style={styles.main}>
|
<main style={layoutStyles.main}>
|
||||||
<div style={styles.loader}>Loading...</div>
|
<div style={layoutStyles.loader}>Loading...</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -356,17 +163,17 @@ export default function BookingPage() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main style={styles.main}>
|
<main style={layoutStyles.main}>
|
||||||
<Header currentPage="booking" />
|
<Header currentPage="booking" />
|
||||||
<div style={styles.content}>
|
<div style={styles.content}>
|
||||||
<h1 style={styles.pageTitle}>Book an Appointment</h1>
|
<h1 style={typographyStyles.pageTitle}>Book an Appointment</h1>
|
||||||
<p style={styles.pageSubtitle}>
|
<p style={typographyStyles.pageSubtitle}>
|
||||||
Select a date to see available {slotDurationMinutes}-minute slots
|
Select a date to see available {slotDurationMinutes}-minute slots
|
||||||
</p>
|
</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 */}
|
{/* Date Selection */}
|
||||||
<div style={styles.section}>
|
<div style={styles.section}>
|
||||||
|
|
@ -449,17 +256,17 @@ export default function BookingPage() {
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label style={styles.inputLabel}>Note (optional, max {noteMaxLength} chars)</label>
|
<label style={formStyles.label}>Note (optional, max {noteMaxLength} chars)</label>
|
||||||
<textarea
|
<textarea
|
||||||
value={note}
|
value={note}
|
||||||
onChange={(e) => setNote(e.target.value.slice(0, noteMaxLength))}
|
onChange={(e) => setNote(e.target.value.slice(0, noteMaxLength))}
|
||||||
placeholder="Add a note about your appointment..."
|
placeholder="Add a note about your appointment..."
|
||||||
style={styles.textarea}
|
style={formStyles.textarea}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
...styles.charCount,
|
...formStyles.charCount,
|
||||||
...(note.length >= noteMaxLength ? styles.charCountWarning : {}),
|
...(note.length >= noteMaxLength ? formStyles.charCountWarning : {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{note.length}/{noteMaxLength}
|
{note.length}/{noteMaxLength}
|
||||||
|
|
@ -472,7 +279,7 @@ export default function BookingPage() {
|
||||||
disabled={isBooking}
|
disabled={isBooking}
|
||||||
style={{
|
style={{
|
||||||
...styles.bookButton,
|
...styles.bookButton,
|
||||||
...(isBooking ? styles.bookButtonDisabled : {}),
|
...(isBooking ? buttonStyles.buttonDisabled : {}),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{isBooking ? "Booking..." : "Book Appointment"}
|
{isBooking ? "Booking..." : "Book Appointment"}
|
||||||
|
|
@ -491,3 +298,131 @@ export default function BookingPage() {
|
||||||
</main>
|
</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",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,17 @@
|
||||||
|
|
||||||
import { useEffect, useState, useCallback } from "react";
|
import { useEffect, useState, useCallback } from "react";
|
||||||
import { api } from "../api";
|
import { api } from "../api";
|
||||||
import { sharedStyles } from "../styles/shared";
|
|
||||||
import { Header } from "../components/Header";
|
import { Header } from "../components/Header";
|
||||||
import { useRequireAuth } from "../hooks/useRequireAuth";
|
import { useRequireAuth } from "../hooks/useRequireAuth";
|
||||||
import { components } from "../generated/api";
|
import { components } from "../generated/api";
|
||||||
import constants from "../../../shared/constants.json";
|
import constants from "../../../shared/constants.json";
|
||||||
|
import {
|
||||||
|
layoutStyles,
|
||||||
|
cardStyles,
|
||||||
|
typographyStyles,
|
||||||
|
badgeStyles,
|
||||||
|
buttonStyles,
|
||||||
|
} from "../styles/shared";
|
||||||
|
|
||||||
// Use generated type from OpenAPI schema
|
// Use generated type from OpenAPI schema
|
||||||
type Invite = components["schemas"]["UserInviteResponse"];
|
type Invite = components["schemas"]["UserInviteResponse"];
|
||||||
|
|
@ -57,8 +63,8 @@ export default function InvitesPage() {
|
||||||
|
|
||||||
if (isLoading || isLoadingInvites) {
|
if (isLoading || isLoadingInvites) {
|
||||||
return (
|
return (
|
||||||
<main style={styles.main}>
|
<main style={layoutStyles.main}>
|
||||||
<div style={styles.loader}>Loading...</div>
|
<div style={layoutStyles.loader}>Loading...</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -73,14 +79,16 @@ export default function InvitesPage() {
|
||||||
const revokedInvites = invites.filter((i) => i.status === REVOKED);
|
const revokedInvites = invites.filter((i) => i.status === REVOKED);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main style={styles.main}>
|
<main style={layoutStyles.main}>
|
||||||
<Header currentPage="invites" />
|
<Header currentPage="invites" />
|
||||||
|
|
||||||
<div style={styles.content}>
|
<div style={layoutStyles.contentCentered}>
|
||||||
<div style={styles.pageCard}>
|
<div style={styles.pageCard}>
|
||||||
<div style={styles.cardHeader}>
|
<div style={cardStyles.cardHeader}>
|
||||||
<h1 style={styles.cardTitle}>My Invites</h1>
|
<h1 style={cardStyles.cardTitle}>My Invites</h1>
|
||||||
<p style={styles.cardSubtitle}>Share your invite codes with friends to let them join</p>
|
<p style={cardStyles.cardSubtitle}>
|
||||||
|
Share your invite codes with friends to let them join
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{invites.length === 0 ? (
|
{invites.length === 0 ? (
|
||||||
|
|
@ -93,14 +101,19 @@ export default function InvitesPage() {
|
||||||
{/* Ready Invites */}
|
{/* Ready Invites */}
|
||||||
{readyInvites.length > 0 && (
|
{readyInvites.length > 0 && (
|
||||||
<div style={styles.section}>
|
<div style={styles.section}>
|
||||||
<h2 style={styles.sectionTitle}>Available ({readyInvites.length})</h2>
|
<h2 style={typographyStyles.sectionTitle}>Available ({readyInvites.length})</h2>
|
||||||
<p style={styles.sectionHint}>Share these links with people you want to invite</p>
|
<p style={typographyStyles.sectionHint}>
|
||||||
|
Share these links with people you want to invite
|
||||||
|
</p>
|
||||||
<div style={styles.inviteList}>
|
<div style={styles.inviteList}>
|
||||||
{readyInvites.map((invite) => (
|
{readyInvites.map((invite) => (
|
||||||
<div key={invite.id} style={styles.inviteCard}>
|
<div key={invite.id} style={styles.inviteCard}>
|
||||||
<div style={styles.inviteCode}>{invite.identifier}</div>
|
<div style={styles.inviteCode}>{invite.identifier}</div>
|
||||||
<div style={styles.inviteActions}>
|
<div style={styles.inviteActions}>
|
||||||
<button onClick={() => copyToClipboard(invite)} style={styles.copyButton}>
|
<button
|
||||||
|
onClick={() => copyToClipboard(invite)}
|
||||||
|
style={buttonStyles.accentButton}
|
||||||
|
>
|
||||||
{copiedId === invite.id ? "Copied!" : "Copy Link"}
|
{copiedId === invite.id ? "Copied!" : "Copy Link"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -113,13 +126,15 @@ export default function InvitesPage() {
|
||||||
{/* Spent Invites */}
|
{/* Spent Invites */}
|
||||||
{spentInvites.length > 0 && (
|
{spentInvites.length > 0 && (
|
||||||
<div style={styles.section}>
|
<div style={styles.section}>
|
||||||
<h2 style={styles.sectionTitle}>Used ({spentInvites.length})</h2>
|
<h2 style={typographyStyles.sectionTitle}>Used ({spentInvites.length})</h2>
|
||||||
<div style={styles.inviteList}>
|
<div style={styles.inviteList}>
|
||||||
{spentInvites.map((invite) => (
|
{spentInvites.map((invite) => (
|
||||||
<div key={invite.id} style={styles.inviteCardSpent}>
|
<div key={invite.id} style={styles.inviteCardSpent}>
|
||||||
<div style={styles.inviteCode}>{invite.identifier}</div>
|
<div style={styles.inviteCode}>{invite.identifier}</div>
|
||||||
<div style={styles.inviteeMeta}>
|
<div style={styles.inviteeMeta}>
|
||||||
<span style={styles.statusBadgeSpent}>Used</span>
|
<span style={{ ...badgeStyles.badge, ...badgeStyles.badgeSuccess }}>
|
||||||
|
Used
|
||||||
|
</span>
|
||||||
<span style={styles.inviteeEmail}>by {invite.used_by_email}</span>
|
<span style={styles.inviteeEmail}>by {invite.used_by_email}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -131,12 +146,14 @@ export default function InvitesPage() {
|
||||||
{/* Revoked Invites */}
|
{/* Revoked Invites */}
|
||||||
{revokedInvites.length > 0 && (
|
{revokedInvites.length > 0 && (
|
||||||
<div style={styles.section}>
|
<div style={styles.section}>
|
||||||
<h2 style={styles.sectionTitle}>Revoked ({revokedInvites.length})</h2>
|
<h2 style={typographyStyles.sectionTitle}>Revoked ({revokedInvites.length})</h2>
|
||||||
<div style={styles.inviteList}>
|
<div style={styles.inviteList}>
|
||||||
{revokedInvites.map((invite) => (
|
{revokedInvites.map((invite) => (
|
||||||
<div key={invite.id} style={styles.inviteCardRevoked}>
|
<div key={invite.id} style={styles.inviteCardRevoked}>
|
||||||
<div style={styles.inviteCode}>{invite.identifier}</div>
|
<div style={styles.inviteCode}>{invite.identifier}</div>
|
||||||
<span style={styles.statusBadgeRevoked}>Revoked</span>
|
<span style={{ ...badgeStyles.badge, ...badgeStyles.badgeError }}>
|
||||||
|
Revoked
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -150,33 +167,12 @@ export default function InvitesPage() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageStyles: Record<string, React.CSSProperties> = {
|
// Page-specific styles
|
||||||
|
const styles: Record<string, React.CSSProperties> = {
|
||||||
pageCard: {
|
pageCard: {
|
||||||
background: "rgba(255, 255, 255, 0.03)",
|
...cardStyles.card,
|
||||||
backdropFilter: "blur(10px)",
|
|
||||||
border: "1px solid rgba(255, 255, 255, 0.08)",
|
|
||||||
borderRadius: "24px",
|
|
||||||
padding: "2.5rem",
|
|
||||||
width: "100%",
|
width: "100%",
|
||||||
maxWidth: "600px",
|
maxWidth: "600px",
|
||||||
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",
|
|
||||||
},
|
},
|
||||||
emptyState: {
|
emptyState: {
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
|
|
@ -204,21 +200,6 @@ const pageStyles: Record<string, React.CSSProperties> = {
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
gap: "0.75rem",
|
gap: "0.75rem",
|
||||||
},
|
},
|
||||||
sectionTitle: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
fontSize: "0.875rem",
|
|
||||||
fontWeight: 600,
|
|
||||||
color: "rgba(255, 255, 255, 0.8)",
|
|
||||||
margin: 0,
|
|
||||||
textTransform: "uppercase",
|
|
||||||
letterSpacing: "0.05em",
|
|
||||||
},
|
|
||||||
sectionHint: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
fontSize: "0.8rem",
|
|
||||||
color: "rgba(255, 255, 255, 0.4)",
|
|
||||||
margin: 0,
|
|
||||||
},
|
|
||||||
inviteList: {
|
inviteList: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
|
|
@ -262,48 +243,14 @@ const pageStyles: Record<string, React.CSSProperties> = {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
gap: "0.5rem",
|
gap: "0.5rem",
|
||||||
},
|
},
|
||||||
copyButton: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
fontSize: "0.8rem",
|
|
||||||
fontWeight: 500,
|
|
||||||
padding: "0.5rem 1rem",
|
|
||||||
background: "rgba(99, 102, 241, 0.3)",
|
|
||||||
color: "#fff",
|
|
||||||
border: "1px solid rgba(99, 102, 241, 0.5)",
|
|
||||||
borderRadius: "8px",
|
|
||||||
cursor: "pointer",
|
|
||||||
transition: "all 0.2s",
|
|
||||||
},
|
|
||||||
inviteeMeta: {
|
inviteeMeta: {
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "0.5rem",
|
gap: "0.5rem",
|
||||||
},
|
},
|
||||||
statusBadgeSpent: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
fontSize: "0.7rem",
|
|
||||||
fontWeight: 500,
|
|
||||||
padding: "0.25rem 0.5rem",
|
|
||||||
background: "rgba(34, 197, 94, 0.2)",
|
|
||||||
color: "rgba(34, 197, 94, 0.9)",
|
|
||||||
borderRadius: "4px",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
},
|
|
||||||
statusBadgeRevoked: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
|
||||||
fontSize: "0.7rem",
|
|
||||||
fontWeight: 500,
|
|
||||||
padding: "0.25rem 0.5rem",
|
|
||||||
background: "rgba(239, 68, 68, 0.2)",
|
|
||||||
color: "rgba(239, 68, 68, 0.9)",
|
|
||||||
borderRadius: "4px",
|
|
||||||
textTransform: "uppercase",
|
|
||||||
},
|
|
||||||
inviteeEmail: {
|
inviteeEmail: {
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||||
fontSize: "0.8rem",
|
fontSize: "0.8rem",
|
||||||
color: "rgba(255, 255, 255, 0.6)",
|
color: "rgba(255, 255, 255, 0.6)",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = { ...sharedStyles, ...pageStyles };
|
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Permission } from "./auth-context";
|
import { Permission } from "./auth-context";
|
||||||
import { api } from "./api";
|
import { api } from "./api";
|
||||||
import { sharedStyles } from "./styles/shared";
|
import { layoutStyles, cardStyles, buttonStyles } from "./styles/shared";
|
||||||
import { Header } from "./components/Header";
|
import { Header } from "./components/Header";
|
||||||
import { useRequireAuth } from "./hooks/useRequireAuth";
|
import { useRequireAuth } from "./hooks/useRequireAuth";
|
||||||
|
|
||||||
|
|
@ -29,8 +29,8 @@ export default function Home() {
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return (
|
return (
|
||||||
<main style={styles.main}>
|
<main style={layoutStyles.main}>
|
||||||
<div style={styles.loader}>Loading...</div>
|
<div style={layoutStyles.loader}>Loading...</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -40,10 +40,10 @@ export default function Home() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main style={styles.main}>
|
<main style={layoutStyles.main}>
|
||||||
<Header currentPage="counter" />
|
<Header currentPage="counter" />
|
||||||
|
|
||||||
<div style={styles.content}>
|
<div style={layoutStyles.contentCentered}>
|
||||||
<div style={styles.counterCard}>
|
<div style={styles.counterCard}>
|
||||||
<span style={styles.counterLabel}>Current Count</span>
|
<span style={styles.counterLabel}>Current Count</span>
|
||||||
<h1 style={styles.counter}>{count === null ? "..." : count}</h1>
|
<h1 style={styles.counter}>{count === null ? "..." : count}</h1>
|
||||||
|
|
@ -57,15 +57,13 @@ export default function Home() {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pageStyles: Record<string, React.CSSProperties> = {
|
// Page-specific styles only - truly unique to this page
|
||||||
|
const styles: Record<string, React.CSSProperties> = {
|
||||||
counterCard: {
|
counterCard: {
|
||||||
background: "rgba(255, 255, 255, 0.03)",
|
...cardStyles.card,
|
||||||
backdropFilter: "blur(10px)",
|
|
||||||
border: "1px solid rgba(255, 255, 255, 0.08)",
|
|
||||||
borderRadius: "32px",
|
borderRadius: "32px",
|
||||||
padding: "4rem 5rem",
|
padding: "4rem 5rem",
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.5)",
|
|
||||||
},
|
},
|
||||||
counterLabel: {
|
counterLabel: {
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||||
|
|
@ -89,26 +87,17 @@ const pageStyles: Record<string, React.CSSProperties> = {
|
||||||
backgroundClip: "text",
|
backgroundClip: "text",
|
||||||
},
|
},
|
||||||
incrementBtn: {
|
incrementBtn: {
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
...buttonStyles.primaryButton,
|
||||||
marginTop: "2.5rem",
|
marginTop: "2.5rem",
|
||||||
padding: "1rem 2.5rem",
|
padding: "1rem 2.5rem",
|
||||||
fontSize: "1.125rem",
|
fontSize: "1.125rem",
|
||||||
fontWeight: 600,
|
|
||||||
background: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)",
|
|
||||||
color: "#fff",
|
|
||||||
border: "none",
|
|
||||||
borderRadius: "16px",
|
borderRadius: "16px",
|
||||||
cursor: "pointer",
|
|
||||||
display: "inline-flex",
|
display: "inline-flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "0.5rem",
|
gap: "0.5rem",
|
||||||
transition: "transform 0.2s, box-shadow 0.2s",
|
|
||||||
boxShadow: "0 4px 14px rgba(99, 102, 241, 0.4)",
|
|
||||||
},
|
},
|
||||||
plusIcon: {
|
plusIcon: {
|
||||||
fontSize: "1.5rem",
|
fontSize: "1.5rem",
|
||||||
fontWeight: 400,
|
fontWeight: 400,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const styles = { ...sharedStyles, ...pageStyles };
|
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,18 @@
|
||||||
import { useEffect, useState, useCallback, useRef } from "react";
|
import { useEffect, useState, useCallback, useRef } from "react";
|
||||||
import { bech32 } from "bech32";
|
import { bech32 } from "bech32";
|
||||||
import { api, ApiError } from "../api";
|
import { api, ApiError } from "../api";
|
||||||
import { sharedStyles } from "../styles/shared";
|
|
||||||
import { Header } from "../components/Header";
|
import { Header } from "../components/Header";
|
||||||
import { useRequireAuth } from "../hooks/useRequireAuth";
|
import { useRequireAuth } from "../hooks/useRequireAuth";
|
||||||
import { components } from "../generated/api";
|
import { components } from "../generated/api";
|
||||||
import constants from "../../../shared/constants.json";
|
import constants from "../../../shared/constants.json";
|
||||||
|
import {
|
||||||
|
layoutStyles,
|
||||||
|
cardStyles,
|
||||||
|
formStyles,
|
||||||
|
buttonStyles,
|
||||||
|
toastStyles,
|
||||||
|
utilityStyles,
|
||||||
|
} from "../styles/shared";
|
||||||
|
|
||||||
// Use generated type from OpenAPI schema
|
// Use generated type from OpenAPI schema
|
||||||
type ProfileData = components["schemas"]["ProfileResponse"];
|
type ProfileData = components["schemas"]["ProfileResponse"];
|
||||||
|
|
@ -253,8 +260,8 @@ export default function ProfilePage() {
|
||||||
|
|
||||||
if (isLoading || isLoadingProfile) {
|
if (isLoading || isLoadingProfile) {
|
||||||
return (
|
return (
|
||||||
<main style={styles.main}>
|
<main style={layoutStyles.main}>
|
||||||
<div style={styles.loader}>Loading...</div>
|
<div style={layoutStyles.loader}>Loading...</div>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -266,13 +273,13 @@ export default function ProfilePage() {
|
||||||
const canSubmit = hasChanges() && isValid() && !isSubmitting;
|
const canSubmit = hasChanges() && isValid() && !isSubmitting;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main style={styles.main}>
|
<main style={layoutStyles.main}>
|
||||||
{/* Toast notification */}
|
{/* Toast notification */}
|
||||||
{toast && (
|
{toast && (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
...styles.toast,
|
...toastStyles.toast,
|
||||||
...(toast.type === "success" ? styles.toastSuccess : styles.toastError),
|
...(toast.type === "success" ? toastStyles.toastSuccess : toastStyles.toastError),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{toast.message}
|
{toast.message}
|
||||||
|
|
@ -281,44 +288,46 @@ export default function ProfilePage() {
|
||||||
|
|
||||||
<Header currentPage="profile" />
|
<Header currentPage="profile" />
|
||||||
|
|
||||||
<div style={styles.content}>
|
<div style={layoutStyles.contentCentered}>
|
||||||
<div style={styles.profileCard}>
|
<div style={styles.profileCard}>
|
||||||
<div style={styles.cardHeader}>
|
<div style={cardStyles.cardHeader}>
|
||||||
<h1 style={styles.cardTitle}>My Profile</h1>
|
<h1 style={cardStyles.cardTitle}>My Profile</h1>
|
||||||
<p style={styles.cardSubtitle}>Manage your contact information</p>
|
<p style={cardStyles.cardSubtitle}>Manage your contact information</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} style={styles.form}>
|
<form onSubmit={handleSubmit} style={formStyles.form}>
|
||||||
{/* Login email - read only */}
|
{/* Login email - read only */}
|
||||||
<div style={styles.field}>
|
<div style={formStyles.field}>
|
||||||
<label style={styles.label}>
|
<label style={styles.labelWithBadge}>
|
||||||
Login Email
|
Login Email
|
||||||
<span style={styles.readOnlyBadge}>Read only</span>
|
<span style={utilityStyles.readOnlyBadge}>Read only</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="email"
|
type="email"
|
||||||
value={user.email}
|
value={user.email}
|
||||||
style={{ ...styles.input, ...styles.inputReadOnly }}
|
style={{ ...formStyles.input, ...formStyles.inputReadOnly }}
|
||||||
disabled
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Godfather - shown if user was invited */}
|
{/* Godfather - shown if user was invited */}
|
||||||
{godfatherEmail && (
|
{godfatherEmail && (
|
||||||
<div style={styles.field}>
|
<div style={formStyles.field}>
|
||||||
<label style={styles.label}>
|
<label style={styles.labelWithBadge}>
|
||||||
Invited By
|
Invited By
|
||||||
<span style={styles.readOnlyBadge}>Read only</span>
|
<span style={utilityStyles.readOnlyBadge}>Read only</span>
|
||||||
</label>
|
</label>
|
||||||
<div style={styles.godfatherBox}>
|
<div style={styles.godfatherBox}>
|
||||||
<span style={styles.godfatherEmail}>{godfatherEmail}</span>
|
<span style={styles.godfatherEmail}>{godfatherEmail}</span>
|
||||||
</div>
|
</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>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div style={styles.divider} />
|
<div style={utilityStyles.divider} />
|
||||||
|
|
||||||
<p style={styles.sectionLabel}>Contact Details</p>
|
<p style={styles.sectionLabel}>Contact Details</p>
|
||||||
<p style={styles.sectionHint}>
|
<p style={styles.sectionHint}>
|
||||||
|
|
@ -326,8 +335,8 @@ export default function ProfilePage() {
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{/* Contact email */}
|
{/* Contact email */}
|
||||||
<div style={styles.field}>
|
<div style={formStyles.field}>
|
||||||
<label htmlFor="contact_email" style={styles.label}>
|
<label htmlFor="contact_email" style={formStyles.label}>
|
||||||
Contact Email
|
Contact Email
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -336,17 +345,19 @@ export default function ProfilePage() {
|
||||||
value={formData.contact_email}
|
value={formData.contact_email}
|
||||||
onChange={handleInputChange("contact_email")}
|
onChange={handleInputChange("contact_email")}
|
||||||
style={{
|
style={{
|
||||||
...styles.input,
|
...formStyles.input,
|
||||||
...(errors.contact_email ? styles.inputError : {}),
|
...(errors.contact_email ? formStyles.inputError : {}),
|
||||||
}}
|
}}
|
||||||
placeholder="alternate@example.com"
|
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>
|
</div>
|
||||||
|
|
||||||
{/* Telegram */}
|
{/* Telegram */}
|
||||||
<div style={styles.field}>
|
<div style={formStyles.field}>
|
||||||
<label htmlFor="telegram" style={styles.label}>
|
<label htmlFor="telegram" style={formStyles.label}>
|
||||||
Telegram
|
Telegram
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -355,17 +366,17 @@ export default function ProfilePage() {
|
||||||
value={formData.telegram}
|
value={formData.telegram}
|
||||||
onChange={handleInputChange("telegram")}
|
onChange={handleInputChange("telegram")}
|
||||||
style={{
|
style={{
|
||||||
...styles.input,
|
...formStyles.input,
|
||||||
...(errors.telegram ? styles.inputError : {}),
|
...(errors.telegram ? formStyles.inputError : {}),
|
||||||
}}
|
}}
|
||||||
placeholder="@username"
|
placeholder="@username"
|
||||||
/>
|
/>
|
||||||
{errors.telegram && <span style={styles.errorText}>{errors.telegram}</span>}
|
{errors.telegram && <span style={formStyles.errorText}>{errors.telegram}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Signal */}
|
{/* Signal */}
|
||||||
<div style={styles.field}>
|
<div style={formStyles.field}>
|
||||||
<label htmlFor="signal" style={styles.label}>
|
<label htmlFor="signal" style={formStyles.label}>
|
||||||
Signal
|
Signal
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -374,17 +385,17 @@ export default function ProfilePage() {
|
||||||
value={formData.signal}
|
value={formData.signal}
|
||||||
onChange={handleInputChange("signal")}
|
onChange={handleInputChange("signal")}
|
||||||
style={{
|
style={{
|
||||||
...styles.input,
|
...formStyles.input,
|
||||||
...(errors.signal ? styles.inputError : {}),
|
...(errors.signal ? formStyles.inputError : {}),
|
||||||
}}
|
}}
|
||||||
placeholder="username.01"
|
placeholder="username.01"
|
||||||
/>
|
/>
|
||||||
{errors.signal && <span style={styles.errorText}>{errors.signal}</span>}
|
{errors.signal && <span style={formStyles.errorText}>{errors.signal}</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Nostr npub */}
|
{/* Nostr npub */}
|
||||||
<div style={styles.field}>
|
<div style={formStyles.field}>
|
||||||
<label htmlFor="nostr_npub" style={styles.label}>
|
<label htmlFor="nostr_npub" style={formStyles.label}>
|
||||||
Nostr (npub)
|
Nostr (npub)
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
|
|
@ -393,19 +404,20 @@ export default function ProfilePage() {
|
||||||
value={formData.nostr_npub}
|
value={formData.nostr_npub}
|
||||||
onChange={handleInputChange("nostr_npub")}
|
onChange={handleInputChange("nostr_npub")}
|
||||||
style={{
|
style={{
|
||||||
...styles.input,
|
...formStyles.input,
|
||||||
...(errors.nostr_npub ? styles.inputError : {}),
|
...(errors.nostr_npub ? formStyles.inputError : {}),
|
||||||
}}
|
}}
|
||||||
placeholder="npub1..."
|
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>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
style={{
|
style={{
|
||||||
...styles.button,
|
...buttonStyles.primaryButton,
|
||||||
...(!canSubmit ? styles.buttonDisabled : {}),
|
marginTop: "1rem",
|
||||||
|
...(!canSubmit ? buttonStyles.buttonDisabled : {}),
|
||||||
}}
|
}}
|
||||||
disabled={!canSubmit}
|
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: {
|
profileCard: {
|
||||||
background: "rgba(255, 255, 255, 0.03)",
|
...cardStyles.card,
|
||||||
backdropFilter: "blur(10px)",
|
|
||||||
border: "1px solid rgba(255, 255, 255, 0.08)",
|
|
||||||
borderRadius: "24px",
|
|
||||||
padding: "2.5rem",
|
|
||||||
width: "100%",
|
width: "100%",
|
||||||
maxWidth: "480px",
|
maxWidth: "480px",
|
||||||
boxShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.5)",
|
|
||||||
},
|
},
|
||||||
cardHeader: {
|
labelWithBadge: {
|
||||||
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: {
|
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||||
color: "rgba(255, 255, 255, 0.7)",
|
color: "rgba(255, 255, 255, 0.7)",
|
||||||
fontSize: "0.875rem",
|
fontSize: "0.875rem",
|
||||||
|
|
@ -465,32 +446,6 @@ const pageStyles: Record<string, React.CSSProperties> = {
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
gap: "0.5rem",
|
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: {
|
godfatherBox: {
|
||||||
padding: "0.875rem 1rem",
|
padding: "0.875rem 1rem",
|
||||||
background: "rgba(99, 102, 241, 0.08)",
|
background: "rgba(99, 102, 241, 0.08)",
|
||||||
|
|
@ -502,26 +457,6 @@ const pageStyles: Record<string, React.CSSProperties> = {
|
||||||
fontSize: "1rem",
|
fontSize: "1rem",
|
||||||
color: "rgba(129, 140, 248, 0.9)",
|
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: {
|
sectionLabel: {
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||||
fontSize: "0.875rem",
|
fontSize: "0.875rem",
|
||||||
|
|
@ -538,46 +473,4 @@ const pageStyles: Record<string, React.CSSProperties> = {
|
||||||
margin: 0,
|
margin: 0,
|
||||||
marginBottom: "0.5rem",
|
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 };
|
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,113 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shared styles used across multiple pages.
|
* Design tokens - centralized values for consistency.
|
||||||
* These styles define the common layout and theming for the app.
|
|
||||||
*/
|
*/
|
||||||
export const sharedStyles: Record<string, React.CSSProperties> = {
|
const tokens = {
|
||||||
|
// Font families
|
||||||
|
fontSans: "'DM Sans', system-ui, sans-serif",
|
||||||
|
fontSerif: "'Instrument Serif', Georgia, serif",
|
||||||
|
fontMono: "'DM Mono', monospace",
|
||||||
|
|
||||||
|
// Colors
|
||||||
|
white: "#fff",
|
||||||
|
textPrimary: "#fff",
|
||||||
|
textSecondary: "rgba(255, 255, 255, 0.7)",
|
||||||
|
textMuted: "rgba(255, 255, 255, 0.5)",
|
||||||
|
textDim: "rgba(255, 255, 255, 0.4)",
|
||||||
|
textDisabled: "rgba(255, 255, 255, 0.3)",
|
||||||
|
|
||||||
|
// Accent colors
|
||||||
|
accent: "#a78bfa",
|
||||||
|
accentIndigo: "rgba(99, 102, 241, 0.9)",
|
||||||
|
accentIndigoMuted: "rgba(129, 140, 248, 0.9)",
|
||||||
|
|
||||||
|
// Status colors
|
||||||
|
success: "rgba(34, 197, 94, 0.9)",
|
||||||
|
successBg: "rgba(34, 197, 94, 0.2)",
|
||||||
|
successBorder: "rgba(34, 197, 94, 0.3)",
|
||||||
|
error: "#f87171",
|
||||||
|
errorBg: "rgba(239, 68, 68, 0.15)",
|
||||||
|
errorBorder: "rgba(239, 68, 68, 0.3)",
|
||||||
|
|
||||||
|
// Surfaces
|
||||||
|
surfaceBg: "rgba(255, 255, 255, 0.03)",
|
||||||
|
surfaceBorder: "rgba(255, 255, 255, 0.08)",
|
||||||
|
surfaceHover: "rgba(255, 255, 255, 0.05)",
|
||||||
|
inputBg: "rgba(255, 255, 255, 0.05)",
|
||||||
|
inputBorder: "rgba(255, 255, 255, 0.1)",
|
||||||
|
|
||||||
|
// Gradients
|
||||||
|
pageGradient: "linear-gradient(135deg, #0f0f23 0%, #1a1a3e 50%, #2d1b4e 100%)",
|
||||||
|
primaryGradient: "linear-gradient(135deg, #6366f1 0%, #8b5cf6 100%)",
|
||||||
|
|
||||||
|
// Shadows
|
||||||
|
cardShadow: "0 25px 50px -12px rgba(0, 0, 0, 0.5)",
|
||||||
|
buttonShadow: "0 4px 14px rgba(99, 102, 241, 0.4)",
|
||||||
|
|
||||||
|
// Border radius
|
||||||
|
radiusSm: "6px",
|
||||||
|
radiusMd: "8px",
|
||||||
|
radiusLg: "12px",
|
||||||
|
radiusXl: "16px",
|
||||||
|
radius2xl: "20px",
|
||||||
|
radius3xl: "24px",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
type StyleRecord = Record<string, React.CSSProperties>;
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Layout Styles
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const layoutStyles: StyleRecord = {
|
||||||
|
/** Full-page main container with gradient background */
|
||||||
main: {
|
main: {
|
||||||
minHeight: "100vh",
|
minHeight: "100vh",
|
||||||
background: "linear-gradient(135deg, #0f0f23 0%, #1a1a3e 50%, #2d1b4e 100%)",
|
background: tokens.pageGradient,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column",
|
flexDirection: "column",
|
||||||
},
|
},
|
||||||
|
/** Centered loading indicator */
|
||||||
loader: {
|
loader: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
fontFamily: tokens.fontSans,
|
||||||
color: "rgba(255, 255, 255, 0.5)",
|
color: tokens.textMuted,
|
||||||
fontSize: "1.125rem",
|
fontSize: "1.125rem",
|
||||||
},
|
},
|
||||||
|
/** Content area - centered (for cards/forms) */
|
||||||
|
contentCentered: {
|
||||||
|
flex: 1,
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
padding: "2rem",
|
||||||
|
},
|
||||||
|
/** Content area - scrollable (for tables/lists) */
|
||||||
|
contentScrollable: {
|
||||||
|
flex: 1,
|
||||||
|
padding: "2rem",
|
||||||
|
overflowY: "auto",
|
||||||
|
},
|
||||||
|
/** Max-width container for page content */
|
||||||
|
pageContainer: {
|
||||||
|
maxWidth: "1200px",
|
||||||
|
margin: "0 auto",
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Header Styles (for nav/app header - used by Header component)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const headerStyles: StyleRecord = {
|
||||||
header: {
|
header: {
|
||||||
padding: "1.5rem 2rem",
|
padding: "1.5rem 2rem",
|
||||||
borderBottom: "1px solid rgba(255, 255, 255, 0.06)",
|
borderBottom: `1px solid ${tokens.surfaceBorder}`,
|
||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
|
|
@ -33,8 +118,8 @@ export const sharedStyles: Record<string, React.CSSProperties> = {
|
||||||
gap: "0.75rem",
|
gap: "0.75rem",
|
||||||
},
|
},
|
||||||
navLink: {
|
navLink: {
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
fontFamily: tokens.fontSans,
|
||||||
color: "rgba(255, 255, 255, 0.5)",
|
color: tokens.textMuted,
|
||||||
fontSize: "0.875rem",
|
fontSize: "0.875rem",
|
||||||
textDecoration: "none",
|
textDecoration: "none",
|
||||||
transition: "color 0.2s",
|
transition: "color 0.2s",
|
||||||
|
|
@ -44,8 +129,8 @@ export const sharedStyles: Record<string, React.CSSProperties> = {
|
||||||
fontSize: "0.75rem",
|
fontSize: "0.75rem",
|
||||||
},
|
},
|
||||||
navCurrent: {
|
navCurrent: {
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
fontFamily: tokens.fontSans,
|
||||||
color: "#a78bfa",
|
color: tokens.accent,
|
||||||
fontSize: "0.875rem",
|
fontSize: "0.875rem",
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
},
|
},
|
||||||
|
|
@ -55,55 +140,593 @@ export const sharedStyles: Record<string, React.CSSProperties> = {
|
||||||
gap: "1rem",
|
gap: "1rem",
|
||||||
},
|
},
|
||||||
userEmail: {
|
userEmail: {
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
fontFamily: tokens.fontSans,
|
||||||
color: "rgba(255, 255, 255, 0.6)",
|
color: "rgba(255, 255, 255, 0.6)",
|
||||||
fontSize: "0.875rem",
|
fontSize: "0.875rem",
|
||||||
},
|
},
|
||||||
logoutBtn: {
|
logoutBtn: {
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
fontFamily: tokens.fontSans,
|
||||||
padding: "0.5rem 1rem",
|
padding: "0.5rem 1rem",
|
||||||
fontSize: "0.875rem",
|
fontSize: "0.875rem",
|
||||||
fontWeight: 500,
|
fontWeight: 500,
|
||||||
background: "rgba(255, 255, 255, 0.05)",
|
background: tokens.surfaceHover,
|
||||||
color: "rgba(255, 255, 255, 0.7)",
|
color: tokens.textSecondary,
|
||||||
border: "1px solid rgba(255, 255, 255, 0.1)",
|
border: `1px solid ${tokens.inputBorder}`,
|
||||||
borderRadius: "8px",
|
borderRadius: tokens.radiusMd,
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
transition: "all 0.2s",
|
transition: "all 0.2s",
|
||||||
},
|
},
|
||||||
content: {
|
};
|
||||||
flex: 1,
|
|
||||||
|
// =============================================================================
|
||||||
|
// Typography Styles
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const typographyStyles: StyleRecord = {
|
||||||
|
/** Large page title - serif font */
|
||||||
|
pageTitle: {
|
||||||
|
fontFamily: tokens.fontSerif,
|
||||||
|
fontSize: "2rem",
|
||||||
|
fontWeight: 400,
|
||||||
|
color: tokens.white,
|
||||||
|
margin: 0,
|
||||||
|
letterSpacing: "-0.02em",
|
||||||
|
},
|
||||||
|
/** Page subtitle text */
|
||||||
|
pageSubtitle: {
|
||||||
|
fontFamily: tokens.fontSans,
|
||||||
|
color: tokens.textMuted,
|
||||||
|
marginTop: "0.5rem",
|
||||||
|
fontSize: "0.95rem",
|
||||||
|
},
|
||||||
|
/** Section title - uppercase, smaller */
|
||||||
|
sectionTitle: {
|
||||||
|
fontFamily: tokens.fontSans,
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: "rgba(255, 255, 255, 0.8)",
|
||||||
|
margin: 0,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: "0.05em",
|
||||||
|
},
|
||||||
|
/** Section title - muted variant */
|
||||||
|
sectionTitleMuted: {
|
||||||
|
fontFamily: tokens.fontSans,
|
||||||
|
fontSize: "1.1rem",
|
||||||
|
fontWeight: 500,
|
||||||
|
color: tokens.textMuted,
|
||||||
|
marginBottom: "1rem",
|
||||||
|
},
|
||||||
|
/** Section hint text */
|
||||||
|
sectionHint: {
|
||||||
|
fontFamily: tokens.fontSans,
|
||||||
|
fontSize: "0.8rem",
|
||||||
|
color: tokens.textDim,
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Card Styles
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const cardStyles: StyleRecord = {
|
||||||
|
/** Standard card container */
|
||||||
|
card: {
|
||||||
|
background: tokens.surfaceBg,
|
||||||
|
backdropFilter: "blur(10px)",
|
||||||
|
border: `1px solid ${tokens.surfaceBorder}`,
|
||||||
|
borderRadius: tokens.radius3xl,
|
||||||
|
padding: "2.5rem",
|
||||||
|
boxShadow: tokens.cardShadow,
|
||||||
|
},
|
||||||
|
/** Card header section */
|
||||||
|
cardHeader: {
|
||||||
|
marginBottom: "2rem",
|
||||||
|
},
|
||||||
|
/** Card title - serif, large */
|
||||||
|
cardTitle: {
|
||||||
|
fontFamily: tokens.fontSerif,
|
||||||
|
fontSize: "2rem",
|
||||||
|
fontWeight: 400,
|
||||||
|
color: tokens.white,
|
||||||
|
margin: 0,
|
||||||
|
letterSpacing: "-0.02em",
|
||||||
|
},
|
||||||
|
/** Card subtitle */
|
||||||
|
cardSubtitle: {
|
||||||
|
fontFamily: tokens.fontSans,
|
||||||
|
color: tokens.textMuted,
|
||||||
|
marginTop: "0.5rem",
|
||||||
|
fontSize: "0.95rem",
|
||||||
|
},
|
||||||
|
/** Smaller table/section card */
|
||||||
|
tableCard: {
|
||||||
|
background: tokens.surfaceBg,
|
||||||
|
backdropFilter: "blur(10px)",
|
||||||
|
border: `1px solid ${tokens.surfaceBorder}`,
|
||||||
|
borderRadius: tokens.radius2xl,
|
||||||
|
padding: "1.5rem",
|
||||||
|
boxShadow: tokens.cardShadow,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Table Styles
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const tableStyles: StyleRecord = {
|
||||||
|
tableHeader: {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
marginBottom: "1rem",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: "1rem",
|
||||||
|
},
|
||||||
|
tableTitle: {
|
||||||
|
fontFamily: tokens.fontSerif,
|
||||||
|
fontSize: "1.5rem",
|
||||||
|
fontWeight: 400,
|
||||||
|
color: tokens.white,
|
||||||
|
margin: 0,
|
||||||
|
},
|
||||||
|
totalCount: {
|
||||||
|
fontFamily: tokens.fontSans,
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
color: tokens.textDim,
|
||||||
|
},
|
||||||
|
tableWrapper: {
|
||||||
|
overflowX: "auto",
|
||||||
|
},
|
||||||
|
table: {
|
||||||
|
width: "100%",
|
||||||
|
borderCollapse: "collapse",
|
||||||
|
fontFamily: tokens.fontSans,
|
||||||
|
},
|
||||||
|
th: {
|
||||||
|
textAlign: "left",
|
||||||
|
padding: "0.75rem 1rem",
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
fontWeight: 600,
|
||||||
|
color: tokens.textDim,
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: "0.05em",
|
||||||
|
borderBottom: `1px solid ${tokens.surfaceBorder}`,
|
||||||
|
},
|
||||||
|
tr: {
|
||||||
|
borderBottom: "1px solid rgba(255, 255, 255, 0.04)",
|
||||||
|
},
|
||||||
|
td: {
|
||||||
|
padding: "0.875rem 1rem",
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
color: tokens.textSecondary,
|
||||||
|
},
|
||||||
|
tdMono: {
|
||||||
|
padding: "0.875rem 1rem",
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
color: tokens.white,
|
||||||
|
fontFamily: tokens.fontMono,
|
||||||
|
},
|
||||||
|
tdNum: {
|
||||||
|
padding: "0.875rem 1rem",
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
color: "rgba(255, 255, 255, 0.9)",
|
||||||
|
fontFamily: tokens.fontMono,
|
||||||
|
},
|
||||||
|
tdDate: {
|
||||||
|
padding: "0.875rem 1rem",
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
color: tokens.textDim,
|
||||||
|
},
|
||||||
|
emptyRow: {
|
||||||
|
padding: "2rem 1rem",
|
||||||
|
textAlign: "center",
|
||||||
|
color: tokens.textDisabled,
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
},
|
||||||
|
errorRow: {
|
||||||
|
padding: "2rem 1rem",
|
||||||
|
textAlign: "center",
|
||||||
|
color: tokens.error,
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Pagination Styles
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const paginationStyles: StyleRecord = {
|
||||||
|
pagination: {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "1rem",
|
||||||
|
marginTop: "1rem",
|
||||||
|
paddingTop: "1rem",
|
||||||
|
borderTop: "1px solid rgba(255, 255, 255, 0.06)",
|
||||||
|
},
|
||||||
|
pageBtn: {
|
||||||
|
fontFamily: tokens.fontSans,
|
||||||
|
padding: "0.5rem 1rem",
|
||||||
|
fontSize: "1rem",
|
||||||
|
background: tokens.surfaceHover,
|
||||||
|
color: tokens.textSecondary,
|
||||||
|
border: `1px solid ${tokens.inputBorder}`,
|
||||||
|
borderRadius: tokens.radiusMd,
|
||||||
|
cursor: "pointer",
|
||||||
|
transition: "all 0.2s",
|
||||||
|
},
|
||||||
|
pageInfo: {
|
||||||
|
fontFamily: tokens.fontSans,
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
color: tokens.textMuted,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Form/Input Styles
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const formStyles: StyleRecord = {
|
||||||
|
form: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "1.25rem",
|
||||||
|
},
|
||||||
|
field: {
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
gap: "0.5rem",
|
||||||
|
},
|
||||||
|
label: {
|
||||||
|
fontFamily: tokens.fontSans,
|
||||||
|
color: tokens.textSecondary,
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
input: {
|
||||||
|
fontFamily: tokens.fontSans,
|
||||||
|
padding: "0.875rem 1rem",
|
||||||
|
fontSize: "1rem",
|
||||||
|
background: tokens.inputBg,
|
||||||
|
border: `1px solid ${tokens.inputBorder}`,
|
||||||
|
borderRadius: tokens.radiusLg,
|
||||||
|
color: tokens.white,
|
||||||
|
outline: "none",
|
||||||
|
transition: "border-color 0.2s, box-shadow 0.2s",
|
||||||
|
},
|
||||||
|
inputError: {
|
||||||
|
border: `1px solid ${tokens.errorBorder}`,
|
||||||
|
boxShadow: `0 0 0 2px ${tokens.errorBg}`,
|
||||||
|
},
|
||||||
|
inputReadOnly: {
|
||||||
|
background: "rgba(255, 255, 255, 0.02)",
|
||||||
|
color: tokens.textMuted,
|
||||||
|
cursor: "not-allowed",
|
||||||
|
},
|
||||||
|
textarea: {
|
||||||
|
fontFamily: tokens.fontSans,
|
||||||
|
width: "100%",
|
||||||
|
padding: "0.75rem",
|
||||||
|
background: tokens.inputBg,
|
||||||
|
border: `1px solid ${tokens.inputBorder}`,
|
||||||
|
borderRadius: tokens.radiusMd,
|
||||||
|
color: tokens.white,
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
minHeight: "80px",
|
||||||
|
resize: "vertical" as const,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
fontFamily: tokens.fontSans,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
padding: "0.75rem",
|
||||||
|
background: tokens.inputBg,
|
||||||
|
border: `1px solid ${tokens.inputBorder}`,
|
||||||
|
borderRadius: tokens.radiusMd,
|
||||||
|
color: tokens.white,
|
||||||
|
cursor: "pointer",
|
||||||
|
},
|
||||||
|
hint: {
|
||||||
|
fontFamily: tokens.fontSans,
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
color: tokens.textDim,
|
||||||
|
fontStyle: "italic",
|
||||||
|
},
|
||||||
|
errorText: {
|
||||||
|
fontFamily: tokens.fontSans,
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
color: "#fca5a5",
|
||||||
|
},
|
||||||
|
charCount: {
|
||||||
|
fontFamily: tokens.fontSans,
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
color: tokens.textDim,
|
||||||
|
textAlign: "right" as const,
|
||||||
|
marginTop: "0.25rem",
|
||||||
|
},
|
||||||
|
charCountWarning: {
|
||||||
|
color: tokens.error,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Button Styles
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const buttonStyles: StyleRecord = {
|
||||||
|
/** Primary action button with gradient */
|
||||||
|
primaryButton: {
|
||||||
|
fontFamily: tokens.fontSans,
|
||||||
|
padding: "1rem",
|
||||||
|
fontSize: "1rem",
|
||||||
|
fontWeight: 600,
|
||||||
|
background: tokens.primaryGradient,
|
||||||
|
color: tokens.white,
|
||||||
|
border: "none",
|
||||||
|
borderRadius: tokens.radiusLg,
|
||||||
|
cursor: "pointer",
|
||||||
|
transition: "transform 0.2s, box-shadow 0.2s",
|
||||||
|
boxShadow: tokens.buttonShadow,
|
||||||
|
},
|
||||||
|
/** Secondary/cancel button */
|
||||||
|
secondaryButton: {
|
||||||
|
fontFamily: tokens.fontSans,
|
||||||
|
fontSize: "0.85rem",
|
||||||
|
padding: "0.6rem 1rem",
|
||||||
|
background: tokens.surfaceHover,
|
||||||
|
color: tokens.textSecondary,
|
||||||
|
border: `1px solid ${tokens.inputBorder}`,
|
||||||
|
borderRadius: tokens.radiusMd,
|
||||||
|
cursor: "pointer",
|
||||||
|
transition: "all 0.2s",
|
||||||
|
},
|
||||||
|
/** Accent button (indigo tinted) */
|
||||||
|
accentButton: {
|
||||||
|
fontFamily: tokens.fontSans,
|
||||||
|
fontSize: "0.9rem",
|
||||||
|
fontWeight: 500,
|
||||||
|
padding: "0.75rem 1.5rem",
|
||||||
|
background: "rgba(99, 102, 241, 0.3)",
|
||||||
|
color: tokens.white,
|
||||||
|
border: "1px solid rgba(99, 102, 241, 0.5)",
|
||||||
|
borderRadius: tokens.radiusMd,
|
||||||
|
cursor: "pointer",
|
||||||
|
},
|
||||||
|
/** Danger/destructive button */
|
||||||
|
dangerButton: {
|
||||||
|
fontFamily: tokens.fontSans,
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
padding: "0.4rem 0.75rem",
|
||||||
|
background: tokens.errorBg,
|
||||||
|
color: "rgba(239, 68, 68, 0.9)",
|
||||||
|
border: `1px solid ${tokens.errorBorder}`,
|
||||||
|
borderRadius: tokens.radiusSm,
|
||||||
|
cursor: "pointer",
|
||||||
|
},
|
||||||
|
/** Disabled button modifier */
|
||||||
|
buttonDisabled: {
|
||||||
|
opacity: 0.5,
|
||||||
|
cursor: "not-allowed",
|
||||||
|
boxShadow: "none",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Badge/Status Styles
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const badgeStyles: StyleRecord = {
|
||||||
|
/** Base badge style */
|
||||||
|
badge: {
|
||||||
|
fontFamily: tokens.fontSans,
|
||||||
|
fontSize: "0.7rem",
|
||||||
|
fontWeight: 500,
|
||||||
|
padding: "0.25rem 0.5rem",
|
||||||
|
borderRadius: "4px",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
display: "inline-block",
|
||||||
|
},
|
||||||
|
/** Ready/primary status */
|
||||||
|
badgeReady: {
|
||||||
|
background: "rgba(99, 102, 241, 0.2)",
|
||||||
|
color: tokens.accentIndigoMuted,
|
||||||
|
},
|
||||||
|
/** Success/spent status */
|
||||||
|
badgeSuccess: {
|
||||||
|
background: tokens.successBg,
|
||||||
|
color: tokens.success,
|
||||||
|
},
|
||||||
|
/** Error/revoked status */
|
||||||
|
badgeError: {
|
||||||
|
background: "rgba(239, 68, 68, 0.2)",
|
||||||
|
color: "rgba(239, 68, 68, 0.9)",
|
||||||
|
},
|
||||||
|
/** Booked/active status */
|
||||||
|
badgeBooked: {
|
||||||
|
background: "rgba(99, 102, 241, 0.15)",
|
||||||
|
color: tokens.accentIndigoMuted,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Banner/Alert Styles
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const bannerStyles: StyleRecord = {
|
||||||
|
errorBanner: {
|
||||||
|
fontFamily: tokens.fontSans,
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
padding: "1rem",
|
||||||
|
background: tokens.errorBg,
|
||||||
|
border: `1px solid ${tokens.errorBorder}`,
|
||||||
|
borderRadius: tokens.radiusMd,
|
||||||
|
color: tokens.error,
|
||||||
|
marginBottom: "1rem",
|
||||||
|
},
|
||||||
|
successBanner: {
|
||||||
|
fontFamily: tokens.fontSans,
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
padding: "1rem",
|
||||||
|
background: "rgba(34, 197, 94, 0.15)",
|
||||||
|
border: `1px solid ${tokens.successBorder}`,
|
||||||
|
borderRadius: tokens.radiusMd,
|
||||||
|
color: "#4ade80",
|
||||||
|
marginBottom: "1rem",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Modal Styles
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const modalStyles: StyleRecord = {
|
||||||
|
modalOverlay: {
|
||||||
|
position: "fixed",
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
background: "rgba(0, 0, 0, 0.7)",
|
||||||
display: "flex",
|
display: "flex",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
padding: "2rem",
|
zIndex: 1000,
|
||||||
},
|
},
|
||||||
// Common UI component styles
|
modal: {
|
||||||
errorBanner: {
|
background: "#1a1a3e",
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
border: `1px solid ${tokens.inputBorder}`,
|
||||||
fontSize: "0.875rem",
|
borderRadius: tokens.radiusXl,
|
||||||
padding: "1rem",
|
padding: "2rem",
|
||||||
background: "rgba(239, 68, 68, 0.15)",
|
width: "90%",
|
||||||
border: "1px solid rgba(239, 68, 68, 0.3)",
|
maxWidth: "400px",
|
||||||
borderRadius: "8px",
|
boxShadow: tokens.cardShadow,
|
||||||
color: "#f87171",
|
},
|
||||||
|
modalTitle: {
|
||||||
|
fontFamily: tokens.fontSerif,
|
||||||
|
fontSize: "1.5rem",
|
||||||
|
fontWeight: 400,
|
||||||
|
color: tokens.white,
|
||||||
|
margin: "0 0 1.5rem 0",
|
||||||
|
},
|
||||||
|
modalError: {
|
||||||
|
fontFamily: tokens.fontSans,
|
||||||
|
fontSize: "0.85rem",
|
||||||
|
padding: "0.75rem",
|
||||||
|
background: tokens.errorBg,
|
||||||
|
border: `1px solid ${tokens.errorBorder}`,
|
||||||
|
borderRadius: tokens.radiusMd,
|
||||||
|
color: tokens.error,
|
||||||
marginBottom: "1rem",
|
marginBottom: "1rem",
|
||||||
},
|
},
|
||||||
cancelButton: {
|
modalActions: {
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
display: "flex",
|
||||||
fontSize: "0.85rem",
|
justifyContent: "space-between",
|
||||||
padding: "0.6rem 1rem",
|
alignItems: "center",
|
||||||
background: "rgba(255, 255, 255, 0.05)",
|
gap: "1rem",
|
||||||
color: "rgba(255, 255, 255, 0.7)",
|
|
||||||
border: "1px solid rgba(255, 255, 255, 0.1)",
|
|
||||||
borderRadius: "8px",
|
|
||||||
cursor: "pointer",
|
|
||||||
transition: "all 0.2s",
|
|
||||||
},
|
},
|
||||||
|
modalActionsRight: {
|
||||||
|
display: "flex",
|
||||||
|
gap: "0.75rem",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Toast Styles
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const toastStyles: StyleRecord = {
|
||||||
|
toast: {
|
||||||
|
position: "fixed",
|
||||||
|
top: "1.5rem",
|
||||||
|
right: "1.5rem",
|
||||||
|
padding: "1rem 1.5rem",
|
||||||
|
borderRadius: tokens.radiusLg,
|
||||||
|
fontFamily: tokens.fontSans,
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
fontWeight: 500,
|
||||||
|
zIndex: 1000,
|
||||||
|
boxShadow: "0 10px 25px rgba(0, 0, 0, 0.3)",
|
||||||
|
},
|
||||||
|
toastSuccess: {
|
||||||
|
background: "rgba(34, 197, 94, 0.9)",
|
||||||
|
color: tokens.white,
|
||||||
|
},
|
||||||
|
toastError: {
|
||||||
|
background: "rgba(239, 68, 68, 0.9)",
|
||||||
|
color: tokens.white,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Misc Utility Styles
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
export const utilityStyles: StyleRecord = {
|
||||||
|
/** Horizontal divider */
|
||||||
|
divider: {
|
||||||
|
height: "1px",
|
||||||
|
background: tokens.surfaceBorder,
|
||||||
|
margin: "0.75rem 0",
|
||||||
|
},
|
||||||
|
/** Empty state container */
|
||||||
emptyState: {
|
emptyState: {
|
||||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
fontFamily: tokens.fontSans,
|
||||||
color: "rgba(255, 255, 255, 0.4)",
|
color: tokens.textDim,
|
||||||
textAlign: "center" as const,
|
textAlign: "center" as const,
|
||||||
padding: "1rem 0",
|
padding: "1rem 0",
|
||||||
},
|
},
|
||||||
|
/** Read-only badge for form labels */
|
||||||
|
readOnlyBadge: {
|
||||||
|
fontSize: "0.7rem",
|
||||||
|
fontWeight: 500,
|
||||||
|
color: tokens.textDim,
|
||||||
|
background: tokens.surfaceBorder,
|
||||||
|
padding: "0.15rem 0.5rem",
|
||||||
|
borderRadius: "4px",
|
||||||
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: "0.05em",
|
||||||
|
},
|
||||||
|
/** Flex row with gap */
|
||||||
|
buttonRow: {
|
||||||
|
display: "flex",
|
||||||
|
gap: "0.75rem",
|
||||||
|
},
|
||||||
|
/** Filter group container */
|
||||||
|
filterGroup: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "1rem",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Combined sharedStyles (backwards compatible export)
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use individual style exports (layoutStyles, cardStyles, etc.) for better tree-shaking.
|
||||||
|
* This combined export is maintained for backwards compatibility during migration.
|
||||||
|
*/
|
||||||
|
export const sharedStyles: StyleRecord = {
|
||||||
|
// Layout
|
||||||
|
main: layoutStyles.main,
|
||||||
|
loader: layoutStyles.loader,
|
||||||
|
content: layoutStyles.contentCentered,
|
||||||
|
|
||||||
|
// Header
|
||||||
|
header: headerStyles.header,
|
||||||
|
nav: headerStyles.nav,
|
||||||
|
navLink: headerStyles.navLink,
|
||||||
|
navDivider: headerStyles.navDivider,
|
||||||
|
navCurrent: headerStyles.navCurrent,
|
||||||
|
userInfo: headerStyles.userInfo,
|
||||||
|
userEmail: headerStyles.userEmail,
|
||||||
|
logoutBtn: headerStyles.logoutBtn,
|
||||||
|
|
||||||
|
// Common
|
||||||
|
errorBanner: bannerStyles.errorBanner,
|
||||||
|
cancelButton: buttonStyles.secondaryButton,
|
||||||
|
emptyState: utilityStyles.emptyState,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue