diff --git a/packages/ordinals-plus-explorer/src/components/TransactionStatusDisplay.tsx b/packages/ordinals-plus-explorer/src/components/TransactionStatusDisplay.tsx new file mode 100644 index 0000000..d995b5c --- /dev/null +++ b/packages/ordinals-plus-explorer/src/components/TransactionStatusDisplay.tsx @@ -0,0 +1,202 @@ +import React, { useEffect, useState } from 'react'; +import { + TransactionStatusTracker, + transactionTracker, + TransactionStatus, + TransactionProgressEvent, + TrackedTransaction, + TransactionType, + BitcoinNetwork, +} from 'ordinalsplus'; +import { Loader2, CheckCircle, XCircle, Clock, AlertCircle, ExternalLink } from 'lucide-react'; + +export interface TransactionStatusDisplayProps { + transactionId?: string; + network: BitcoinNetwork; + tracker?: TransactionStatusTracker; + onStatusChange?: (status: TransactionStatus) => void; + onError?: (error: Error) => void; +} + +interface StatusItem { + icon: React.ReactNode; + message: string; + timestamp: Date; + status: TransactionStatus; + link?: string; + error?: string; +} + +const TransactionStatusDisplay: React.FC = ({ + transactionId, + network, + tracker = transactionTracker, + onStatusChange, + onError, +}) => { + const [transaction, setTransaction] = useState(null); + const [events, setEvents] = useState([]); + const [childTx, setChildTx] = useState(null); + const [childEvents, setChildEvents] = useState([]); + + useEffect(() => { + if (!transactionId) return; + + const updateStatus = () => { + try { + const tx = tracker.getTransaction(transactionId); + setTransaction(tx || null); + if (tx) { + setEvents(tracker.getTransactionProgressEvents(transactionId)); + const children = tracker.getChildTransactions(transactionId); + if (children.length > 0) { + setChildTx(children[0]); + setChildEvents(tracker.getTransactionProgressEvents(children[0].id)); + } else { + setChildTx(null); + setChildEvents([]); + } + onStatusChange?.(tx.status); + } + } catch (err) { + onError?.(err as Error); + } + }; + + updateStatus(); + const id = setInterval(updateStatus, 2000); + return () => clearInterval(id); + }, [transactionId, tracker, onStatusChange, onError]); + + const createStatusItems = ( + tx: TrackedTransaction, + progress: TransactionProgressEvent[], + ): StatusItem[] => { + const items: StatusItem[] = []; + + const { status, txid, error, lastUpdatedAt } = tx; + let message = ''; + let icon: React.ReactNode = ; + + switch (status) { + case TransactionStatus.PENDING: + message = 'Transaction is being prepared'; + icon = ; + break; + case TransactionStatus.CONFIRMING: + message = 'Transaction has been sent to the network'; + icon = ; + break; + case TransactionStatus.CONFIRMED: + message = 'Transaction has been confirmed'; + icon = ; + break; + case TransactionStatus.FAILED: + message = error?.message || 'Transaction failed'; + icon = ; + break; + default: + message = `Transaction status: ${status}`; + icon = ; + } + + items.push({ + icon, + message, + timestamp: lastUpdatedAt, + status, + link: txid ? tracker.getTransactionExplorerUrl(txid, network) : undefined, + error: error?.message, + }); + + for (const ev of progress) { + items.push({ + icon: , + message: ev.message, + timestamp: ev.timestamp, + status, + }); + } + + return items.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); + }; + + const renderItems = (items: StatusItem[]) => ( +
+ {items.map((item, idx) => ( +
+
{item.icon}
+
+

{item.message}

+

+ {item.timestamp.toLocaleString()} +

+ {item.link && ( + + View in Explorer + + )} + {item.error && ( +
{item.error}
+ )} +
+
+ ))} +
+ ); + + if (!transactionId) { + return ( +
+ No transaction in progress +
+ ); + } + + if (!transaction) { + return ( +
+ Transaction not found: {transactionId} +
+ ); + } + + const statusItems = createStatusItems(transaction, events); + const childStatusItems = childTx ? createStatusItems(childTx, childEvents) : []; + const transactionTitle = transaction.type === TransactionType.COMMIT ? 'Commit Transaction' : 'Reveal Transaction'; + + return ( +
+
+

+ {transactionTitle} +

+ {renderItems(statusItems)} +
+ {childTx && ( +
+

Reveal Transaction

+ {renderItems(childStatusItems)} +
+ )} +
+ ); +}; + +export default TransactionStatusDisplay; diff --git a/packages/ordinals-plus-explorer/src/components/index.ts b/packages/ordinals-plus-explorer/src/components/index.ts new file mode 100644 index 0000000..c6a3ca9 --- /dev/null +++ b/packages/ordinals-plus-explorer/src/components/index.ts @@ -0,0 +1 @@ +export { default as TransactionStatusDisplay } from './TransactionStatusDisplay'; diff --git a/packages/ordinalsplus/src/components/TransactionStatusDisplay.css b/packages/ordinalsplus/src/components/TransactionStatusDisplay.css deleted file mode 100644 index e3c3108..0000000 --- a/packages/ordinalsplus/src/components/TransactionStatusDisplay.css +++ /dev/null @@ -1,142 +0,0 @@ -/** - * Transaction Status Display Styles - */ - -.transaction-status { - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; - max-width: 600px; - margin: 20px auto; - padding: 20px; - border-radius: 8px; - background-color: #f8f9fa; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); -} - -.transaction-status h2 { - margin-top: 0; - color: #343a40; - font-size: 1.5rem; - margin-bottom: 20px; - border-bottom: 1px solid #dee2e6; - padding-bottom: 10px; -} - -.transaction-status h3 { - margin-top: 20px; - color: #495057; - font-size: 1.25rem; - margin-bottom: 15px; -} - -.transaction-status-items { - display: flex; - flex-direction: column; - gap: 15px; -} - -.transaction-status-item { - display: flex; - padding: 12px; - border-radius: 6px; - border-left: 4px solid #6c757d; - background-color: #fff; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.transaction-status-icon { - font-size: 1.5rem; - margin-right: 15px; - display: flex; - align-items: center; -} - -.transaction-status-content { - flex: 1; -} - -.transaction-status-message { - font-weight: 500; - color: #212529; - margin-bottom: 5px; -} - -.transaction-status-timestamp { - font-size: 0.8rem; - color: #6c757d; - margin-bottom: 8px; -} - -.transaction-status-link a { - display: inline-block; - color: #0d6efd; - text-decoration: none; - font-size: 0.9rem; - padding: 4px 0; -} - -.transaction-status-link a:hover { - text-decoration: underline; -} - -.transaction-status-error { - color: #dc3545; - background-color: rgba(220, 53, 69, 0.1); - padding: 8px 12px; - border-radius: 4px; - margin-top: 8px; - font-size: 0.9rem; -} - -/* Status colors */ -.transaction-status-pending { - border-left-color: #ffc107; -} - -.transaction-status-broadcasting { - border-left-color: #17a2b8; -} - -.transaction-status-broadcasted { - border-left-color: #007bff; -} - -.transaction-status-confirmed { - border-left-color: #28a745; -} - -.transaction-status-failed { - border-left-color: #dc3545; -} - -.transaction-status-empty, -.transaction-status-not-found { - padding: 30px; - text-align: center; - color: #6c757d; - background-color: #fff; - border-radius: 8px; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); -} - -.transaction-status-child { - margin-top: 25px; - padding-top: 15px; - border-top: 1px dashed #dee2e6; -} - -/* Responsive design */ -@media (max-width: 576px) { - .transaction-status { - padding: 15px; - margin: 10px; - } - - .transaction-status-item { - padding: 10px; - } - - .transaction-status-icon { - font-size: 1.2rem; - margin-right: 10px; - } -} \ No newline at end of file diff --git a/packages/ordinalsplus/src/components/TransactionStatusDisplay.ts b/packages/ordinalsplus/src/components/TransactionStatusDisplay.ts deleted file mode 100644 index fb3b661..0000000 --- a/packages/ordinalsplus/src/components/TransactionStatusDisplay.ts +++ /dev/null @@ -1,284 +0,0 @@ -/** - * Transaction Status Display Component - * - * This component provides a UI for displaying transaction status information. - * It shows the current status of commit and reveal transactions with progress indicators - * and links to block explorers. - */ - -import { BitcoinNetwork } from '../types'; -import { - transactionTracker, - TransactionStatus, - TransactionProgressEvent, - TransactionType, - TransactionStatusTracker, - TrackedTransaction -} from '../transactions/transaction-status-tracker'; - -/** - * Transaction status display properties - */ -export interface TransactionStatusDisplayProps { - /** ID of the transaction to display status for */ - transactionId?: string; - /** Bitcoin network configuration */ - network: BitcoinNetwork; - /** Optional custom tracker instance (defaults to singleton tracker) */ - tracker?: TransactionStatusTracker; - /** Optional callback for when status changes */ - onStatusChange?: (status: TransactionStatus) => void; - /** Optional callback for when the component encounters an error */ - onError?: (error: Error) => void; -} - -/** - * Status item for rendering - */ -export interface StatusItem { - icon: string; - message: string; - timestamp: Date; - status: TransactionStatus; - link?: string; - error?: string; -} - -/** - * Transaction status display component - */ -export class TransactionStatusDisplay { - private props: TransactionStatusDisplayProps; - private tracker: TransactionStatusTracker; - private container: HTMLElement | null = null; - private statusUpdateInterval: number | null = null; - - /** - * Create a new transaction status display - */ - constructor(props: TransactionStatusDisplayProps) { - this.props = props; - this.tracker = props.tracker || transactionTracker; - } - - /** - * Mount the component to a DOM element - */ - mount(element: HTMLElement): void { - this.container = element; - this.render(); - - // Set up interval to periodically check for status updates - this.statusUpdateInterval = window.setInterval(() => { - this.render(); - }, 2000); // Check every 2 seconds - } - - /** - * Unmount and clean up the component - */ - unmount(): void { - if (this.statusUpdateInterval !== null) { - window.clearInterval(this.statusUpdateInterval); - this.statusUpdateInterval = null; - } - - if (this.container) { - this.container.innerHTML = ''; - this.container = null; - } - } - - /** - * Render the component - */ - render(): void { - if (!this.container) { - return; - } - - try { - const { transactionId } = this.props; - - // If no transaction ID is provided, display a message - if (!transactionId) { - this.container.innerHTML = ` -
-

No transaction in progress

-
- `; - return; - } - - // Get the transaction and its progress events - const transaction = this.tracker.getTransaction(transactionId); - - if (!transaction) { - this.container.innerHTML = ` -
-

Transaction not found: ${transactionId}

-
- `; - return; - } - - // Get progress events - const events = this.tracker.getTransactionProgressEvents(transactionId); - - // Get any child transactions (like reveal after commit) - const childTransactions = this.tracker.getChildTransactions(transactionId); - - // Create status items for this transaction and its children - const statusItems = this.createStatusItems(transaction, events); - - // Create HTML for the status display - const transactionTitle = transaction.type === TransactionType.COMMIT - ? 'Commit Transaction' - : 'Reveal Transaction'; - - // Generate the HTML - const statusItemsHtml = statusItems.map(item => ` -
-
${item.icon}
-
-
${item.message}
-
${this.formatDate(item.timestamp)}
- ${item.link ? `` : ''} - ${item.error ? `
${item.error}
` : ''} -
-
- `).join(''); - - // Render child transactions if any - let childTransactionsHtml = ''; - - if (childTransactions.length > 0) { - const childTx = childTransactions[0]; // For now just show the first child - const childEvents = this.tracker.getTransactionProgressEvents(childTx.id); - const childStatusItems = this.createStatusItems(childTx, childEvents); - - const childStatusItemsHtml = childStatusItems.map(item => ` -
-
${item.icon}
-
-
${item.message}
-
${this.formatDate(item.timestamp)}
- ${item.link ? `` : ''} - ${item.error ? `
${item.error}
` : ''} -
-
- `).join(''); - - childTransactionsHtml = ` -
-

Reveal Transaction

-
- ${childStatusItemsHtml} -
-
- `; - } - - // Put together the final HTML - this.container.innerHTML = ` -
-

${transactionTitle}

-
- ${statusItemsHtml} -
- ${childTransactionsHtml} -
- `; - - // Notify of status changes - if (this.props.onStatusChange && transaction) { - this.props.onStatusChange(transaction.status); - } - } catch (error) { - console.error('Error rendering transaction status:', error); - - if (this.container) { - this.container.innerHTML = ` -
-

Error displaying transaction status

-
- `; - } - - if (this.props.onError && error instanceof Error) { - this.props.onError(error); - } - } - } - - /** - * Create status items from transaction and events - */ - private createStatusItems(transaction: TrackedTransaction, events: TransactionProgressEvent[]): StatusItem[] { - const items: StatusItem[] = []; - - // Add main transaction status - const txid = transaction.txid || 'Pending'; - let statusMessage = ''; - let statusIcon = ''; - - switch (transaction.status) { - case TransactionStatus.PENDING: - statusMessage = 'Transaction is being prepared'; - statusIcon = '⏳'; - break; - case TransactionStatus.CONFIRMING: - statusMessage = 'Transaction has been sent to the network'; - statusIcon = '🔄'; - break; - case TransactionStatus.CONFIRMED: - statusMessage = 'Transaction has been confirmed'; - statusIcon = '✅'; - break; - case TransactionStatus.FAILED: - statusMessage = transaction.error?.message || 'Transaction failed'; - statusIcon = '❌'; - break; - default: - statusMessage = `Transaction status: ${transaction.status}`; - statusIcon = '❓'; - } - - // Add the main status item - items.push({ - icon: statusIcon, - message: statusMessage, - timestamp: transaction.lastUpdatedAt, - status: transaction.status, - link: transaction.txid ? this.tracker.getTransactionExplorerUrl(transaction.txid, this.props.network) : undefined, - error: transaction.error?.message - }); - - // Add progress events as status items - for (const event of events) { - items.push({ - icon: '📋', - message: event.message, - timestamp: event.timestamp, - status: transaction.status - }); - } - - // Sort by timestamp, most recent first - return items.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime()); - } - - /** - * Format a date for display - */ - private formatDate(date: Date): string { - return new Intl.DateTimeFormat('en-US', { - year: 'numeric', - month: 'short', - day: 'numeric', - hour: 'numeric', - minute: 'numeric', - second: 'numeric' - }).format(date); - } -} \ No newline at end of file diff --git a/packages/ordinalsplus/src/components/index.ts b/packages/ordinalsplus/src/components/index.ts index 09e0068..c4d2e6b 100644 --- a/packages/ordinalsplus/src/components/index.ts +++ b/packages/ordinalsplus/src/components/index.ts @@ -2,4 +2,4 @@ * Components export file */ -export * from './TransactionStatusDisplay'; \ No newline at end of file +// No UI components are currently exported from the core package. UI is handled in the explorer package. diff --git a/packages/ordinalsplus/src/index.ts b/packages/ordinalsplus/src/index.ts index ddc3b7f..474117f 100644 --- a/packages/ordinalsplus/src/index.ts +++ b/packages/ordinalsplus/src/index.ts @@ -111,7 +111,7 @@ export type { } from './transactions/transaction-status-tracker'; // --- Component Exports --- -export * from './components'; +// UI components have moved to the explorer package // --- Inscription Exports --- export {