refactor: extract shared trade card styles to shared.ts

Move duplicate style definitions from trades/page.tsx and
admin/trades/page.tsx to a new tradeCardStyles export in shared.ts.

Both pages now import and use these shared styles, keeping only
page-specific styles locally. This improves maintainability and
ensures visual consistency.
This commit is contained in:
counterweight 2025-12-23 12:22:04 +01:00
parent 2efbd2c665
commit c9c36971d8
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
3 changed files with 166 additions and 210 deletions

View file

@ -16,6 +16,7 @@ import {
bannerStyles,
badgeStyles,
buttonStyles,
tradeCardStyles,
} from "../../styles/shared";
type AdminExchangeResponse = components["schemas"]["AdminExchangeResponse"];
@ -178,13 +179,13 @@ export default function AdminTradesPage() {
)}
{isLoadingTrades ? (
<div style={styles.emptyState}>Loading trades...</div>
<div style={tradeCardStyles.emptyState}>Loading trades...</div>
) : trades.length === 0 ? (
<div style={styles.emptyState}>
<div style={tradeCardStyles.emptyState}>
{activeTab === "upcoming" ? "No upcoming trades." : "No trades found."}
</div>
) : (
<div style={styles.tradeList}>
<div style={tradeCardStyles.tradeList}>
{trades.map((trade) => {
const status = getTradeStatusDisplay(trade.status);
const isBuy = trade.direction === "buy";
@ -192,10 +193,12 @@ export default function AdminTradesPage() {
const canComplete = trade.status === "booked" && isPast && activeTab === "past";
return (
<div key={trade.id} style={styles.tradeCard}>
<div style={styles.tradeHeader}>
<div style={styles.tradeInfo}>
<div style={styles.tradeTime}>{formatDateTime(trade.slot_start)}</div>
<div key={trade.id} style={tradeCardStyles.tradeCard}>
<div style={tradeCardStyles.tradeHeader}>
<div style={tradeCardStyles.tradeInfo}>
<div style={tradeCardStyles.tradeTime}>
{formatDateTime(trade.slot_start)}
</div>
{/* User Info */}
<div style={styles.userInfo}>
@ -211,10 +214,10 @@ export default function AdminTradesPage() {
</div>
{/* Trade Details */}
<div style={styles.tradeDetails}>
<div style={tradeCardStyles.tradeDetails}>
<span
style={{
...styles.directionBadge,
...tradeCardStyles.directionBadge,
background: isBuy
? "rgba(74, 222, 128, 0.15)"
: "rgba(248, 113, 113, 0.15)",
@ -223,24 +226,24 @@ export default function AdminTradesPage() {
>
{isBuy ? "BUY" : "SELL"}
</span>
<span style={styles.amount}>{formatEur(trade.eur_amount)}</span>
<span style={styles.arrow}></span>
<span style={styles.satsAmount}>
<span style={tradeCardStyles.amount}>{formatEur(trade.eur_amount)}</span>
<span style={tradeCardStyles.arrow}></span>
<span style={tradeCardStyles.satsAmount}>
<SatsDisplay sats={trade.sats_amount} />
</span>
</div>
<div style={styles.rateRow}>
<span style={styles.rateLabel}>Rate:</span>
<span style={styles.rateValue}>
<div style={tradeCardStyles.rateRow}>
<span style={tradeCardStyles.rateLabel}>Rate:</span>
<span style={tradeCardStyles.rateValue}>
{trade.agreed_price_eur.toLocaleString("de-DE", {
maximumFractionDigits: 0,
})}
/BTC
</span>
<span style={styles.rateLabel}>Market:</span>
<span style={styles.rateValue}>
<span style={tradeCardStyles.rateLabel}>Market:</span>
<span style={tradeCardStyles.rateValue}>
{trade.market_price_eur.toLocaleString("de-DE", {
maximumFractionDigits: 0,
@ -337,7 +340,7 @@ export default function AdminTradesPage() {
);
}
// Page-specific styles
// Page-specific styles (trade card styles are shared via tradeCardStyles)
const styles: Record<string, React.CSSProperties> = {
content: {
flex: 1,
@ -393,36 +396,7 @@ const styles: Record<string, React.CSSProperties> = {
fontSize: "0.875rem",
minWidth: "200px",
},
tradeList: {
display: "flex",
flexDirection: "column",
gap: "0.75rem",
},
tradeCard: {
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",
},
tradeHeader: {
display: "flex",
justifyContent: "space-between",
alignItems: "flex-start",
gap: "1rem",
},
tradeInfo: {
display: "flex",
flexDirection: "column",
gap: "0.25rem",
},
tradeTime: {
fontFamily: "'DM Sans', system-ui, sans-serif",
fontSize: "1rem",
fontWeight: 500,
color: "#fff",
marginBottom: "0.25rem",
},
// Admin-specific: user contact info
userInfo: {
display: "flex",
alignItems: "center",
@ -444,52 +418,7 @@ const styles: Record<string, React.CSSProperties> = {
borderRadius: "4px",
color: "rgba(129, 140, 248, 0.9)",
},
tradeDetails: {
display: "flex",
alignItems: "center",
gap: "0.5rem",
flexWrap: "wrap",
},
directionBadge: {
fontFamily: "'DM Sans', system-ui, sans-serif",
fontSize: "0.7rem",
fontWeight: 600,
padding: "0.2rem 0.5rem",
borderRadius: "4px",
textTransform: "uppercase",
},
amount: {
fontFamily: "'DM Mono', monospace",
fontSize: "0.95rem",
color: "#fff",
fontWeight: 500,
},
arrow: {
color: "rgba(255, 255, 255, 0.3)",
fontSize: "0.8rem",
},
satsAmount: {
fontFamily: "'DM Mono', monospace",
fontSize: "0.9rem",
color: "#f7931a",
},
rateRow: {
display: "flex",
alignItems: "center",
gap: "0.5rem",
marginTop: "0.25rem",
flexWrap: "wrap",
},
rateLabel: {
fontFamily: "'DM Sans', system-ui, sans-serif",
fontSize: "0.75rem",
color: "rgba(255, 255, 255, 0.4)",
},
rateValue: {
fontFamily: "'DM Mono', monospace",
fontSize: "0.8rem",
color: "rgba(255, 255, 255, 0.7)",
},
// Admin-specific: vertical button group
buttonGroup: {
display: "flex",
flexDirection: "column",
@ -532,10 +461,4 @@ const styles: Record<string, React.CSSProperties> = {
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",
},
};

View file

@ -659,6 +659,114 @@ export const toastStyles: StyleRecord = {
},
};
// =============================================================================
// Trade Card Styles (shared between trades and admin/trades pages)
// =============================================================================
export const tradeCardStyles: StyleRecord = {
/** Container for list of trade cards */
tradeList: {
display: "flex",
flexDirection: "column",
gap: "0.75rem",
},
/** Individual trade card */
tradeCard: {
background: tokens.surfaceBg,
border: `1px solid ${tokens.surfaceBorder}`,
borderRadius: tokens.radiusLg,
padding: "1.25rem",
transition: "all 0.2s",
},
/** Trade card header - flex row for info + actions */
tradeHeader: {
display: "flex",
justifyContent: "space-between",
alignItems: "flex-start",
gap: "1rem",
},
/** Left side info container */
tradeInfo: {
display: "flex",
flexDirection: "column",
gap: "0.25rem",
},
/** Trade date/time display */
tradeTime: {
fontFamily: tokens.fontSans,
fontSize: "1rem",
fontWeight: 500,
color: tokens.white,
marginBottom: "0.5rem",
},
/** Trade amount details row */
tradeDetails: {
display: "flex",
alignItems: "center",
gap: "0.5rem",
flexWrap: "wrap",
},
/** BUY/SELL direction badge (base - color applied inline) */
directionBadge: {
fontFamily: tokens.fontSans,
fontSize: "0.7rem",
fontWeight: 600,
padding: "0.2rem 0.5rem",
borderRadius: "4px",
textTransform: "uppercase",
},
/** EUR amount display */
amount: {
fontFamily: tokens.fontMono,
fontSize: "0.95rem",
color: tokens.white,
fontWeight: 500,
},
/** Arrow between EUR and sats */
arrow: {
color: tokens.textDisabled,
fontSize: "0.8rem",
},
/** Sats amount in Bitcoin orange */
satsAmount: {
fontFamily: tokens.fontMono,
fontSize: "0.9rem",
color: "#f7931a", // Bitcoin orange
},
/** Rate info row */
rateRow: {
display: "flex",
alignItems: "center",
gap: "0.5rem",
marginTop: "0.25rem",
flexWrap: "wrap",
},
/** Rate label text */
rateLabel: {
fontFamily: tokens.fontSans,
fontSize: "0.75rem",
color: tokens.textDim,
},
/** Rate value in monospace */
rateValue: {
fontFamily: tokens.fontMono,
fontSize: "0.8rem",
color: tokens.textSecondary,
},
/** Button group container */
buttonGroup: {
display: "flex",
gap: "0.5rem",
},
/** Empty state for no trades */
emptyState: {
fontFamily: tokens.fontSans,
color: tokens.textDim,
textAlign: "center" as const,
padding: "3rem",
},
};
// =============================================================================
// Misc Utility Styles
// =============================================================================

View file

@ -16,6 +16,7 @@ import {
bannerStyles,
badgeStyles,
buttonStyles,
tradeCardStyles,
} from "../styles/shared";
type ExchangeResponse = components["schemas"]["ExchangeResponse"];
@ -94,9 +95,9 @@ export default function TradesPage() {
{error && <div style={bannerStyles.errorBanner}>{error}</div>}
{isLoadingTrades ? (
<div style={styles.emptyState}>Loading trades...</div>
<div style={tradeCardStyles.emptyState}>Loading trades...</div>
) : trades.length === 0 ? (
<div style={styles.emptyState}>
<div style={tradeCardStyles.emptyState}>
<p>You don&apos;t have any trades yet.</p>
<a href="/exchange" style={styles.emptyStateLink}>
Start trading
@ -108,19 +109,21 @@ export default function TradesPage() {
{upcomingTrades.length > 0 && (
<div style={styles.section}>
<h2 style={styles.sectionTitle}>Upcoming ({upcomingTrades.length})</h2>
<div style={styles.tradeList}>
<div style={tradeCardStyles.tradeList}>
{upcomingTrades.map((trade) => {
const status = getTradeStatusDisplay(trade.status);
const isBuy = trade.direction === "buy";
return (
<div key={trade.id} style={styles.tradeCard}>
<div style={styles.tradeHeader}>
<div style={styles.tradeInfo}>
<div style={styles.tradeTime}>{formatDateTime(trade.slot_start)}</div>
<div style={styles.tradeDetails}>
<div key={trade.id} style={tradeCardStyles.tradeCard}>
<div style={tradeCardStyles.tradeHeader}>
<div style={tradeCardStyles.tradeInfo}>
<div style={tradeCardStyles.tradeTime}>
{formatDateTime(trade.slot_start)}
</div>
<div style={tradeCardStyles.tradeDetails}>
<span
style={{
...styles.directionBadge,
...tradeCardStyles.directionBadge,
background: isBuy
? "rgba(74, 222, 128, 0.15)"
: "rgba(248, 113, 113, 0.15)",
@ -129,15 +132,17 @@ export default function TradesPage() {
>
{isBuy ? "BUY" : "SELL"}
</span>
<span style={styles.amount}>{formatEur(trade.eur_amount)}</span>
<span style={styles.arrow}></span>
<span style={styles.satsAmount}>
<span style={tradeCardStyles.amount}>
{formatEur(trade.eur_amount)}
</span>
<span style={tradeCardStyles.arrow}></span>
<span style={tradeCardStyles.satsAmount}>
<SatsDisplay sats={trade.sats_amount} />
</span>
</div>
<div style={styles.rateRow}>
<span style={styles.rateLabel}>Rate:</span>
<span style={styles.rateValue}>
<div style={tradeCardStyles.rateRow}>
<span style={tradeCardStyles.rateLabel}>Rate:</span>
<span style={tradeCardStyles.rateValue}>
{trade.agreed_price_eur.toLocaleString("de-DE", {
maximumFractionDigits: 0,
@ -158,7 +163,7 @@ export default function TradesPage() {
</div>
{trade.status === "booked" && (
<div style={styles.buttonGroup}>
<div style={tradeCardStyles.buttonGroup}>
{confirmCancelId === trade.id ? (
<>
<button
@ -199,17 +204,22 @@ export default function TradesPage() {
<h2 style={typographyStyles.sectionTitleMuted}>
History ({pastOrFinalTrades.length})
</h2>
<div style={styles.tradeList}>
<div style={tradeCardStyles.tradeList}>
{pastOrFinalTrades.map((trade) => {
const status = getTradeStatusDisplay(trade.status);
const isBuy = trade.direction === "buy";
return (
<div key={trade.id} style={{ ...styles.tradeCard, ...styles.tradeCardPast }}>
<div style={styles.tradeTime}>{formatDateTime(trade.slot_start)}</div>
<div style={styles.tradeDetails}>
<div
key={trade.id}
style={{ ...tradeCardStyles.tradeCard, ...styles.tradeCardPast }}
>
<div style={tradeCardStyles.tradeTime}>
{formatDateTime(trade.slot_start)}
</div>
<div style={tradeCardStyles.tradeDetails}>
<span
style={{
...styles.directionBadge,
...tradeCardStyles.directionBadge,
background: isBuy
? "rgba(74, 222, 128, 0.1)"
: "rgba(248, 113, 113, 0.1)",
@ -218,9 +228,9 @@ export default function TradesPage() {
>
{isBuy ? "BUY" : "SELL"}
</span>
<span style={styles.amount}>{formatEur(trade.eur_amount)}</span>
<span style={styles.arrow}></span>
<span style={styles.satsAmount}>
<span style={tradeCardStyles.amount}>{formatEur(trade.eur_amount)}</span>
<span style={tradeCardStyles.arrow}></span>
<span style={tradeCardStyles.satsAmount}>
<SatsDisplay sats={trade.sats_amount} />
</span>
</div>
@ -247,7 +257,7 @@ export default function TradesPage() {
);
}
// Page-specific styles
// Page-specific styles (trade card styles are shared via tradeCardStyles)
const styles: Record<string, React.CSSProperties> = {
content: {
flex: 1,
@ -266,89 +276,10 @@ const styles: Record<string, React.CSSProperties> = {
color: "#fff",
marginBottom: "1rem",
},
tradeList: {
display: "flex",
flexDirection: "column",
gap: "0.75rem",
},
tradeCard: {
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",
},
tradeCardPast: {
opacity: 0.6,
background: "rgba(255, 255, 255, 0.01)",
},
tradeHeader: {
display: "flex",
justifyContent: "space-between",
alignItems: "flex-start",
gap: "1rem",
},
tradeInfo: {
display: "flex",
flexDirection: "column",
gap: "0.25rem",
},
tradeTime: {
fontFamily: "'DM Sans', system-ui, sans-serif",
fontSize: "1rem",
fontWeight: 500,
color: "#fff",
marginBottom: "0.5rem",
},
tradeDetails: {
display: "flex",
alignItems: "center",
gap: "0.5rem",
flexWrap: "wrap",
},
directionBadge: {
fontFamily: "'DM Sans', system-ui, sans-serif",
fontSize: "0.7rem",
fontWeight: 600,
padding: "0.2rem 0.5rem",
borderRadius: "4px",
textTransform: "uppercase",
},
amount: {
fontFamily: "'DM Mono', monospace",
fontSize: "0.95rem",
color: "#fff",
fontWeight: 500,
},
arrow: {
color: "rgba(255, 255, 255, 0.3)",
fontSize: "0.8rem",
},
satsAmount: {
fontFamily: "'DM Mono', monospace",
fontSize: "0.9rem",
color: "#f7931a", // Bitcoin orange
},
rateRow: {
display: "flex",
alignItems: "center",
gap: "0.5rem",
marginTop: "0.25rem",
},
rateLabel: {
fontFamily: "'DM Sans', system-ui, sans-serif",
fontSize: "0.75rem",
color: "rgba(255, 255, 255, 0.4)",
},
rateValue: {
fontFamily: "'DM Mono', monospace",
fontSize: "0.8rem",
color: "rgba(255, 255, 255, 0.7)",
},
buttonGroup: {
display: "flex",
gap: "0.5rem",
},
confirmButton: {
fontFamily: "'DM Sans', system-ui, sans-serif",
padding: "0.35rem 0.75rem",
@ -360,12 +291,6 @@ const styles: Record<string, React.CSSProperties> = {
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",