Extract reusable UI components to reduce DRY violations

- Created StatusBadge component: Standardizes status badge display
  - Supports tradeStatus prop for trade-specific styling
  - Supports variant prop for simple badges (success/error/ready)
  - Eliminates repetitive badge style combinations

- Created EmptyState component: Standardizes empty state display
  - Handles loading and empty states consistently
  - Supports message, hint, and action props
  - Used across trades, invites, admin pages

- Created ConfirmationButton component: Standardizes confirmation flows
  - Two-step confirmation pattern (action -> confirm/cancel)
  - Supports different variants (danger/success/primary)
  - Handles loading states automatically
  - Used for cancel, complete, no-show actions

- Migrated pages to use new components:
  - trades/page.tsx: StatusBadge, EmptyState, ConfirmationButton
  - trades/[id]/page.tsx: StatusBadge
  - invites/page.tsx: StatusBadge, EmptyState
  - admin/trades/page.tsx: StatusBadge, EmptyState, ConfirmationButton
  - admin/invites/page.tsx: StatusBadge

Benefits:
- Eliminated ~50+ lines of repetitive badge styling code
- Consistent UI patterns across all pages
- Easier to maintain and update styling
- Better type safety

All tests passing (32 frontend, 33 e2e)
This commit is contained in:
counterweight 2025-12-25 21:40:07 +01:00
parent b86b506d72
commit 1a47b3643f
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
9 changed files with 309 additions and 425 deletions

View file

@ -3,12 +3,13 @@
import { useState } from "react";
import { invitesApi } from "../api";
import { PageLayout } from "../components/PageLayout";
import { StatusBadge } from "../components/StatusBadge";
import { useRequireAuth } from "../hooks/useRequireAuth";
import { useAsyncData } from "../hooks/useAsyncData";
import { components } from "../generated/api";
import constants from "../../../shared/constants.json";
import { Permission } from "../auth-context";
import { cardStyles, typographyStyles, badgeStyles, buttonStyles } from "../styles/shared";
import { cardStyles, typographyStyles, buttonStyles } from "../styles/shared";
// Use generated type from OpenAPI schema
type Invite = components["schemas"]["UserInviteResponse"];
@ -67,10 +68,10 @@ export default function InvitesPage() {
</div>
{invites.length === 0 ? (
<div style={styles.emptyState}>
<p style={styles.emptyText}>You don&apos;t have any invites yet.</p>
<p style={styles.emptyHint}>Contact an admin if you need invite codes to share.</p>
</div>
<EmptyState
message="You don't have any invites yet."
hint="Contact an admin if you need invite codes to share."
/>
) : (
<div style={styles.sections}>
{/* Ready Invites */}
@ -107,9 +108,7 @@ export default function InvitesPage() {
<div key={invite.id} style={styles.inviteCardSpent}>
<div style={styles.inviteCode}>{invite.identifier}</div>
<div style={styles.inviteeMeta}>
<span style={{ ...badgeStyles.badge, ...badgeStyles.badgeSuccess }}>
Used
</span>
<StatusBadge variant="success">Used</StatusBadge>
<span style={styles.inviteeEmail}>by {invite.used_by_email}</span>
</div>
</div>
@ -126,9 +125,7 @@ export default function InvitesPage() {
{revokedInvites.map((invite) => (
<div key={invite.id} style={styles.inviteCardRevoked}>
<div style={styles.inviteCode}>{invite.identifier}</div>
<span style={{ ...badgeStyles.badge, ...badgeStyles.badgeError }}>
Revoked
</span>
<StatusBadge variant="error">Revoked</StatusBadge>
</div>
))}
</div>