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:
parent
ce8f5a1183
commit
4e6f38e4a1
5 changed files with 66 additions and 169 deletions
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 },
|
||||||
|
|
|
||||||
59
frontend/app/components/SatsDisplay.tsx
Normal file
59
frontend/app/components/SatsDisplay.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue