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:
parent
b86b506d72
commit
1a47b3643f
9 changed files with 309 additions and 425 deletions
97
frontend/app/components/ConfirmationButton.tsx
Normal file
97
frontend/app/components/ConfirmationButton.tsx
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { buttonStyles } from "../styles/shared";
|
||||
|
||||
interface ConfirmationButtonProps {
|
||||
/** Whether confirmation mode is active */
|
||||
isConfirming: boolean;
|
||||
/** Callback when user confirms the action */
|
||||
onConfirm: () => void;
|
||||
/** Callback when user cancels the confirmation */
|
||||
onCancel: () => void;
|
||||
/** Callback when user clicks the initial action button (to enter confirmation mode) */
|
||||
onActionClick: () => void;
|
||||
/** Label for the initial action button */
|
||||
actionLabel: string;
|
||||
/** Label for the confirm button (default: "Confirm") */
|
||||
confirmLabel?: string;
|
||||
/** Label for the cancel button (default: "No") */
|
||||
cancelLabel?: string;
|
||||
/** Whether the action is in progress (shows "..." on confirm button) */
|
||||
isLoading?: boolean;
|
||||
/** Style variant for the confirm button */
|
||||
confirmVariant?: "danger" | "success" | "primary";
|
||||
/** Custom style for the action button */
|
||||
actionButtonStyle?: React.CSSProperties;
|
||||
/** Custom style for the confirm button */
|
||||
confirmButtonStyle?: React.CSSProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Confirmation button component that shows a two-step confirmation flow.
|
||||
* Initially shows an action button. When clicked, shows Confirm/Cancel buttons.
|
||||
*/
|
||||
export function ConfirmationButton({
|
||||
isConfirming,
|
||||
onConfirm,
|
||||
onCancel,
|
||||
onActionClick,
|
||||
actionLabel,
|
||||
confirmLabel = "Confirm",
|
||||
cancelLabel = "No",
|
||||
isLoading = false,
|
||||
confirmVariant = "primary",
|
||||
actionButtonStyle,
|
||||
confirmButtonStyle,
|
||||
}: ConfirmationButtonProps) {
|
||||
if (isConfirming) {
|
||||
let confirmStyle: React.CSSProperties = buttonStyles.primaryButton;
|
||||
if (confirmVariant === "danger") {
|
||||
confirmStyle = {
|
||||
...buttonStyles.primaryButton,
|
||||
background: "rgba(239, 68, 68, 0.9)",
|
||||
borderColor: "rgba(239, 68, 68, 1)",
|
||||
};
|
||||
} else if (confirmVariant === "success") {
|
||||
confirmStyle = {
|
||||
...buttonStyles.primaryButton,
|
||||
background: "rgba(34, 197, 94, 0.9)",
|
||||
borderColor: "rgba(34, 197, 94, 1)",
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onConfirm();
|
||||
}}
|
||||
disabled={isLoading}
|
||||
style={{ ...confirmStyle, ...confirmButtonStyle }}
|
||||
>
|
||||
{isLoading ? "..." : confirmLabel}
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onCancel();
|
||||
}}
|
||||
style={buttonStyles.secondaryButton}
|
||||
>
|
||||
{cancelLabel}
|
||||
</button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onActionClick();
|
||||
}}
|
||||
style={actionButtonStyle || buttonStyles.secondaryButton}
|
||||
>
|
||||
{actionLabel}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
50
frontend/app/components/EmptyState.tsx
Normal file
50
frontend/app/components/EmptyState.tsx
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
import { utilityStyles } from "../styles/shared";
|
||||
|
||||
interface EmptyStateProps {
|
||||
/** Message to display when empty */
|
||||
message: string;
|
||||
/** Optional hint/subtitle text */
|
||||
hint?: string;
|
||||
/** Show loading state instead of empty message */
|
||||
isLoading?: boolean;
|
||||
/** Optional action element (e.g., link or button) */
|
||||
action?: React.ReactNode;
|
||||
/** Custom style override */
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standardized empty state component.
|
||||
* Displays a message when there's no data, or a loading state.
|
||||
*/
|
||||
export function EmptyState({ message, hint, isLoading, action, style }: EmptyStateProps) {
|
||||
if (isLoading) {
|
||||
return <div style={{ ...utilityStyles.emptyState, ...style }}>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ ...utilityStyles.emptyState, ...style }}>
|
||||
<p style={styles.emptyText}>{message}</p>
|
||||
{hint && <p style={styles.emptyHint}>{hint}</p>}
|
||||
{action && <div style={styles.action}>{action}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const styles: Record<string, React.CSSProperties> = {
|
||||
emptyText: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
color: "rgba(255, 255, 255, 0.6)",
|
||||
fontSize: "1rem",
|
||||
margin: 0,
|
||||
},
|
||||
emptyHint: {
|
||||
fontFamily: "'DM Sans', system-ui, sans-serif",
|
||||
color: "rgba(255, 255, 255, 0.4)",
|
||||
fontSize: "0.85rem",
|
||||
marginTop: "0.5rem",
|
||||
},
|
||||
action: {
|
||||
marginTop: "1rem",
|
||||
},
|
||||
};
|
||||
52
frontend/app/components/StatusBadge.tsx
Normal file
52
frontend/app/components/StatusBadge.tsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { badgeStyles } from "../styles/shared";
|
||||
import { getTradeStatusDisplay } from "../utils/exchange";
|
||||
|
||||
type StatusBadgeVariant = "success" | "error" | "ready";
|
||||
|
||||
interface StatusBadgeProps {
|
||||
/** Status text to display */
|
||||
children: React.ReactNode;
|
||||
/** Optional variant for simple status badges */
|
||||
variant?: StatusBadgeVariant;
|
||||
/** Trade status (uses getTradeStatusDisplay for styling) */
|
||||
tradeStatus?: string;
|
||||
/** Custom style override */
|
||||
style?: React.CSSProperties;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standardized status badge component.
|
||||
* Can be used with a variant prop for simple badges, or tradeStatus prop for trade-specific styling.
|
||||
*/
|
||||
export function StatusBadge({ children, variant, tradeStatus, style }: StatusBadgeProps) {
|
||||
let badgeStyle: React.CSSProperties = { ...badgeStyles.badge };
|
||||
|
||||
if (tradeStatus) {
|
||||
// Use trade status display utility for trade-specific badges
|
||||
const statusDisplay = getTradeStatusDisplay(tradeStatus);
|
||||
badgeStyle = {
|
||||
...badgeStyle,
|
||||
background: statusDisplay.bgColor,
|
||||
color: statusDisplay.textColor,
|
||||
};
|
||||
} else if (variant) {
|
||||
// Use variant styles for simple badges
|
||||
switch (variant) {
|
||||
case "success":
|
||||
badgeStyle = { ...badgeStyle, ...badgeStyles.badgeSuccess };
|
||||
break;
|
||||
case "error":
|
||||
badgeStyle = { ...badgeStyle, ...badgeStyles.badgeError };
|
||||
break;
|
||||
case "ready":
|
||||
badgeStyle = { ...badgeStyle, ...badgeStyles.badgeReady };
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<span style={{ ...badgeStyle, ...style }}>
|
||||
{tradeStatus ? getTradeStatusDisplay(tradeStatus).text : children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue