refactor: Extract SatsDisplay component and fix page IDs

- Extract SatsDisplay component to shared components directory
- Fix page IDs: rename 'admin-appointments' to 'admin-trades'
- Fix trades page using correct 'trades' page ID
This commit is contained in:
counterweight 2025-12-23 10:44:11 +01:00
parent ce8f5a1183
commit 4e6f38e4a1
Signed by: counterweight
GPG key ID: 883EDBAA726BD96C
5 changed files with 66 additions and 169 deletions

View file

@ -5,6 +5,7 @@ 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 { Header } from "../../components/Header"; import { Header } from "../../components/Header";
import { SatsDisplay } from "../../components/SatsDisplay";
import { useRequireAuth } from "../../hooks/useRequireAuth"; 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";
@ -25,59 +26,6 @@ function formatEur(cents: number): string {
return `${(cents / 100).toLocaleString("de-DE")}`; return `${(cents / 100).toLocaleString("de-DE")}`;
} }
/**
* Format satoshi amount with styled components
* Leading zeros are subtle, main digits are prominent
*/
function SatsDisplay({ sats }: { sats: number }) {
const btc = sats / 100_000_000;
const btcStr = btc.toFixed(8);
const [whole, decimal] = btcStr.split(".");
const part1 = decimal.slice(0, 2);
const part2 = decimal.slice(2, 5);
const part3 = decimal.slice(5, 8);
const fullDecimal = part1 + part2 + part3;
let firstNonZero = fullDecimal.length;
for (let i = 0; i < fullDecimal.length; i++) {
if (fullDecimal[i] !== "0") {
firstNonZero = i;
break;
}
}
const subtleStyle: React.CSSProperties = {
opacity: 0.45,
fontWeight: 400,
};
const renderPart = (part: string, startIdx: number) => {
return part.split("").map((char, i) => {
const globalIdx = startIdx + i;
const isSubtle = globalIdx < firstNonZero;
return (
<span key={globalIdx} style={isSubtle ? subtleStyle : undefined}>
{char}
</span>
);
});
};
return (
<span style={{ fontFamily: "'DM Mono', monospace" }}>
<span style={subtleStyle}></span>
<span style={subtleStyle}> {whole}.</span>
{renderPart(part1, 0)}
<span> </span>
{renderPart(part2, 2)}
<span> </span>
{renderPart(part3, 5)}
<span> sats</span>
</span>
);
}
/** /**
* Get status display properties * Get status display properties
*/ */
@ -230,7 +178,7 @@ export default function AdminTradesPage() {
return ( return (
<main style={layoutStyles.main}> <main style={layoutStyles.main}>
<Header currentPage="admin-appointments" /> <Header currentPage="admin-trades" />
<div style={styles.content}> <div style={styles.content}>
<h1 style={typographyStyles.pageTitle}>Trades</h1> <h1 style={typographyStyles.pageTitle}>Trades</h1>
<p style={typographyStyles.pageSubtitle}>Manage Bitcoin exchange trades</p> <p style={typographyStyles.pageSubtitle}>Manage Bitcoin exchange trades</p>

View file

@ -14,7 +14,7 @@ type PageId =
| "trades" | "trades"
| "admin-invites" | "admin-invites"
| "admin-availability" | "admin-availability"
| "admin-appointments" | "admin-trades"
| "admin-price-history"; | "admin-price-history";
interface HeaderProps { interface HeaderProps {
@ -37,7 +37,7 @@ const REGULAR_NAV_ITEMS: NavItem[] = [
]; ];
const ADMIN_NAV_ITEMS: NavItem[] = [ const ADMIN_NAV_ITEMS: NavItem[] = [
{ id: "admin-appointments", label: "Trades", href: "/admin/trades", adminOnly: true }, { id: "admin-trades", label: "Trades", href: "/admin/trades", adminOnly: true },
{ id: "admin-availability", label: "Availability", href: "/admin/availability", adminOnly: true }, { id: "admin-availability", label: "Availability", href: "/admin/availability", adminOnly: true },
{ id: "admin-invites", label: "Invites", href: "/admin/invites", adminOnly: true }, { id: "admin-invites", label: "Invites", href: "/admin/invites", adminOnly: true },
{ id: "admin-price-history", label: "Prices", href: "/admin/price-history", adminOnly: true }, { id: "admin-price-history", label: "Prices", href: "/admin/price-history", adminOnly: true },

View file

@ -0,0 +1,59 @@
import React from "react";
/**
* Format satoshi amount with styled components.
* Leading zeros are subtle, main digits are prominent.
* e.g., 1876088 -> "₿ 0.01" (subtle) + "876 088 sats" (prominent)
*/
export function SatsDisplay({ sats }: { sats: number }) {
const btc = sats / 100_000_000;
const btcStr = btc.toFixed(8);
const [whole, decimal] = btcStr.split(".");
// Group decimal into chunks: first 2, then two groups of 3
const part1 = decimal.slice(0, 2);
const part2 = decimal.slice(2, 5);
const part3 = decimal.slice(5, 8);
// Find where meaningful digits start (first non-zero after decimal)
const fullDecimal = part1 + part2 + part3;
let firstNonZero = fullDecimal.length;
for (let i = 0; i < fullDecimal.length; i++) {
if (fullDecimal[i] !== "0") {
firstNonZero = i;
break;
}
}
// Build the display with subtle leading zeros and prominent digits
const subtleStyle: React.CSSProperties = {
opacity: 0.45,
fontWeight: 400,
};
// Determine which parts are subtle vs prominent
const renderPart = (part: string, startIdx: number) => {
return part.split("").map((char, i) => {
const globalIdx = startIdx + i;
const isSubtle = globalIdx < firstNonZero;
return (
<span key={globalIdx} style={isSubtle ? subtleStyle : undefined}>
{char}
</span>
);
});
};
return (
<span style={{ fontFamily: "'DM Mono', monospace" }}>
<span style={subtleStyle}></span>
<span style={subtleStyle}> {whole}.</span>
{renderPart(part1, 0)}
<span> </span>
{renderPart(part2, 2)}
<span> </span>
{renderPart(part3, 5)}
<span> sats</span>
</span>
);
}

View file

@ -6,6 +6,7 @@ import { useRouter } from "next/navigation";
import { Permission } from "../auth-context"; import { Permission } from "../auth-context";
import { api } from "../api"; import { api } from "../api";
import { Header } from "../components/Header"; import { Header } from "../components/Header";
import { SatsDisplay } from "../components/SatsDisplay";
import { useRequireAuth } from "../hooks/useRequireAuth"; import { useRequireAuth } from "../hooks/useRequireAuth";
import { components } from "../generated/api"; import { components } from "../generated/api";
import { formatDate, formatTime, getDateRange } from "../utils/date"; import { formatDate, formatTime, getDateRange } from "../utils/date";
@ -30,65 +31,6 @@ function formatEur(cents: number): string {
return `${(cents / 100).toLocaleString("de-DE")}`; return `${(cents / 100).toLocaleString("de-DE")}`;
} }
/**
* Format satoshi amount with styled components
* Leading zeros are subtle, main digits are prominent
* e.g., 1876088 -> "₿ 0.01" (subtle) + "876 088 sats" (prominent)
*/
function SatsDisplay({ sats }: { sats: number }) {
const btc = sats / 100_000_000;
const btcStr = btc.toFixed(8);
const [whole, decimal] = btcStr.split(".");
// Group decimal into chunks: first 2, then two groups of 3
const part1 = decimal.slice(0, 2);
const part2 = decimal.slice(2, 5);
const part3 = decimal.slice(5, 8);
// Find where meaningful digits start (first non-zero after decimal)
const fullDecimal = part1 + part2 + part3;
let firstNonZero = fullDecimal.length;
for (let i = 0; i < fullDecimal.length; i++) {
if (fullDecimal[i] !== "0") {
firstNonZero = i;
break;
}
}
// Build the display with subtle leading zeros and prominent digits
const subtleStyle: React.CSSProperties = {
opacity: 0.45,
fontWeight: 400,
};
// Determine which parts are subtle vs prominent
const renderPart = (part: string, startIdx: number) => {
const chars = part.split("").map((char, i) => {
const globalIdx = startIdx + i;
const isSubtle = globalIdx < firstNonZero;
return (
<span key={globalIdx} style={isSubtle ? subtleStyle : undefined}>
{char}
</span>
);
});
return chars;
};
return (
<span style={{ fontFamily: "'DM Mono', monospace" }}>
<span style={subtleStyle}></span>
<span style={subtleStyle}> {whole}.</span>
{renderPart(part1, 0)}
<span> </span>
{renderPart(part2, 2)}
<span> </span>
{renderPart(part3, 5)}
<span> sats</span>
</span>
);
}
/** /**
* Format price for display * Format price for display
*/ */

View file

@ -5,6 +5,7 @@ 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 { Header } from "../components/Header"; import { Header } from "../components/Header";
import { SatsDisplay } from "../components/SatsDisplay";
import { useRequireAuth } from "../hooks/useRequireAuth"; 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";
@ -25,59 +26,6 @@ function formatEur(cents: number): string {
return `${(cents / 100).toLocaleString("de-DE")}`; return `${(cents / 100).toLocaleString("de-DE")}`;
} }
/**
* Format satoshi amount with styled components
* Leading zeros are subtle, main digits are prominent
*/
function SatsDisplay({ sats }: { sats: number }) {
const btc = sats / 100_000_000;
const btcStr = btc.toFixed(8);
const [whole, decimal] = btcStr.split(".");
const part1 = decimal.slice(0, 2);
const part2 = decimal.slice(2, 5);
const part3 = decimal.slice(5, 8);
const fullDecimal = part1 + part2 + part3;
let firstNonZero = fullDecimal.length;
for (let i = 0; i < fullDecimal.length; i++) {
if (fullDecimal[i] !== "0") {
firstNonZero = i;
break;
}
}
const subtleStyle: React.CSSProperties = {
opacity: 0.45,
fontWeight: 400,
};
const renderPart = (part: string, startIdx: number) => {
return part.split("").map((char, i) => {
const globalIdx = startIdx + i;
const isSubtle = globalIdx < firstNonZero;
return (
<span key={globalIdx} style={isSubtle ? subtleStyle : undefined}>
{char}
</span>
);
});
};
return (
<span style={{ fontFamily: "'DM Mono', monospace" }}>
<span style={subtleStyle}></span>
<span style={subtleStyle}> {whole}.</span>
{renderPart(part1, 0)}
<span> </span>
{renderPart(part2, 2)}
<span> </span>
{renderPart(part3, 5)}
<span> sats</span>
</span>
);
}
/** /**
* Get status display properties * Get status display properties
*/ */
@ -192,7 +140,7 @@ export default function TradesPage() {
return ( return (
<main style={layoutStyles.main}> <main style={layoutStyles.main}>
<Header currentPage="appointments" /> <Header currentPage="trades" />
<div style={styles.content}> <div style={styles.content}>
<h1 style={typographyStyles.pageTitle}>My Trades</h1> <h1 style={typographyStyles.pageTitle}>My Trades</h1>
<p style={typographyStyles.pageSubtitle}>View and manage your Bitcoin trades</p> <p style={typographyStyles.pageSubtitle}>View and manage your Bitcoin trades</p>