mirror of
https://github.com/aljazceru/cdk.git
synced 2026-02-08 22:55:54 +01:00
Redesign Lightning invoice creation and display with better UX and status handling (#1184)
--------- Co-authored-by: thesimplekid <tsk@thesimplekid.com>
This commit is contained in:
@@ -140,8 +140,8 @@ pub async fn dashboard(State(state): State<AppState>) -> Result<Html<String>, St
|
||||
|
||||
// Balance Summary as metric cards
|
||||
div class="card" {
|
||||
h2 { "Balance Summary" }
|
||||
div class="metrics-container" {
|
||||
h2 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 0;" { "Balance Summary" }
|
||||
div class="metrics-container" style="margin-top: 1.5rem;" {
|
||||
div class="metric-card" {
|
||||
div class="metric-value" { (format_sats_as_btc(balances.total_lightning_balance_sats)) }
|
||||
div class="metric-label" { "Lightning Balance" }
|
||||
@@ -209,8 +209,8 @@ pub async fn dashboard(State(state): State<AppState>) -> Result<Html<String>, St
|
||||
// Right side - Connections metrics
|
||||
aside class="node-metrics" {
|
||||
div class="card" {
|
||||
h3 { "Connections" }
|
||||
div class="metrics-container" {
|
||||
h3 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 0;" { "Connections" }
|
||||
div class="metrics-container" style="margin-top: 1.5rem;" {
|
||||
div class="metric-card" {
|
||||
div class="metric-value" { (format!("{}/{}", num_connected_peers, num_peers)) }
|
||||
div class="metric-label" { "Connected Peers" }
|
||||
@@ -224,52 +224,75 @@ pub async fn dashboard(State(state): State<AppState>) -> Result<Html<String>, St
|
||||
}
|
||||
}
|
||||
|
||||
// Lightning Network Activity as metric cards
|
||||
// Activity Sections - Side by Side Layout
|
||||
div class="card" {
|
||||
h2 { "Lightning Network Activity" }
|
||||
div class="metrics-container" {
|
||||
div class="metric-card" {
|
||||
div class="metric-value" { (format_sats_as_btc(metrics.lightning_inflow_24h)) }
|
||||
div class="metric-label" { "24h LN Inflow" }
|
||||
h2 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; padding-bottom: 1rem; margin-bottom: 0;" { "Activity Overview" }
|
||||
|
||||
div class="activity-grid" {
|
||||
// Lightning Network Activity
|
||||
div class="activity-section" {
|
||||
div class="activity-header" {
|
||||
div class="activity-icon-box" {
|
||||
svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" {
|
||||
path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z" {}
|
||||
}
|
||||
}
|
||||
h3 class="activity-title" { "Lightning Network Activity" }
|
||||
}
|
||||
|
||||
div class="activity-metrics" {
|
||||
div class="activity-metric-card" {
|
||||
div class="activity-metric-label" { "24h Inflow" }
|
||||
div class="activity-metric-value" { (format_sats_as_btc(metrics.lightning_inflow_24h)) }
|
||||
}
|
||||
div class="activity-metric-card" {
|
||||
div class="activity-metric-label" { "24h Outflow" }
|
||||
div class="activity-metric-value" { (format_sats_as_btc(metrics.lightning_outflow_24h)) }
|
||||
}
|
||||
div class="activity-metric-card" {
|
||||
div class="activity-metric-label" { "All-time Inflow" }
|
||||
div class="activity-metric-value" { (format_sats_as_btc(metrics.lightning_inflow_all_time)) }
|
||||
}
|
||||
div class="activity-metric-card" {
|
||||
div class="activity-metric-label" { "All-time Outflow" }
|
||||
div class="activity-metric-value" { (format_sats_as_btc(metrics.lightning_outflow_all_time)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
div class="metric-card" {
|
||||
div class="metric-value" { (format_sats_as_btc(metrics.lightning_outflow_24h)) }
|
||||
div class="metric-label" { "24h LN Outflow" }
|
||||
}
|
||||
div class="metric-card" {
|
||||
div class="metric-value" { (format_sats_as_btc(metrics.lightning_inflow_all_time)) }
|
||||
div class="metric-label" { "All-time LN Inflow" }
|
||||
}
|
||||
div class="metric-card" {
|
||||
div class="metric-value" { (format_sats_as_btc(metrics.lightning_outflow_all_time)) }
|
||||
div class="metric-label" { "All-time LN Outflow" }
|
||||
|
||||
// On-chain Activity
|
||||
div class="activity-section" {
|
||||
div class="activity-header" {
|
||||
div class="activity-icon-box" {
|
||||
svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" {
|
||||
path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" {}
|
||||
path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" {}
|
||||
}
|
||||
}
|
||||
h3 class="activity-title" { "On-chain Activity" }
|
||||
}
|
||||
|
||||
div class="activity-metrics" {
|
||||
div class="activity-metric-card" {
|
||||
div class="activity-metric-label" { "24h Inflow" }
|
||||
div class="activity-metric-value" { (format_sats_as_btc(metrics.onchain_inflow_24h)) }
|
||||
}
|
||||
div class="activity-metric-card" {
|
||||
div class="activity-metric-label" { "24h Outflow" }
|
||||
div class="activity-metric-value" { (format_sats_as_btc(metrics.onchain_outflow_24h)) }
|
||||
}
|
||||
div class="activity-metric-card" {
|
||||
div class="activity-metric-label" { "All-time Inflow" }
|
||||
div class="activity-metric-value" { (format_sats_as_btc(metrics.onchain_inflow_all_time)) }
|
||||
}
|
||||
div class="activity-metric-card" {
|
||||
div class="activity-metric-label" { "All-time Outflow" }
|
||||
div class="activity-metric-value" { (format_sats_as_btc(metrics.onchain_outflow_all_time)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// On-chain Activity as metric cards
|
||||
div class="card" {
|
||||
h2 { "On-chain Activity" }
|
||||
div class="metrics-container" {
|
||||
div class="metric-card" {
|
||||
div class="metric-value" { (format_sats_as_btc(metrics.onchain_inflow_24h)) }
|
||||
div class="metric-label" { "24h On-chain Inflow" }
|
||||
}
|
||||
div class="metric-card" {
|
||||
div class="metric-value" { (format_sats_as_btc(metrics.onchain_outflow_24h)) }
|
||||
div class="metric-label" { "24h On-chain Outflow" }
|
||||
}
|
||||
div class="metric-card" {
|
||||
div class="metric-value" { (format_sats_as_btc(metrics.onchain_inflow_all_time)) }
|
||||
div class="metric-label" { "All-time On-chain Inflow" }
|
||||
}
|
||||
div class="metric-card" {
|
||||
div class="metric-value" { (format_sats_as_btc(metrics.onchain_outflow_all_time)) }
|
||||
div class="metric-label" { "All-time On-chain Outflow" }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
let is_running = is_node_running(&state.node.inner);
|
||||
|
||||
@@ -10,7 +10,7 @@ use serde::Deserialize;
|
||||
use crate::web::handlers::utils::{deserialize_optional_f64, deserialize_optional_u32};
|
||||
use crate::web::handlers::AppState;
|
||||
use crate::web::templates::{
|
||||
error_message, form_card, format_sats_as_btc, info_card, is_node_running, layout_with_status,
|
||||
error_message, format_sats_as_btc, invoice_display_card, is_node_running, layout_with_status,
|
||||
success_message,
|
||||
};
|
||||
|
||||
@@ -34,54 +34,101 @@ pub struct CreateBolt12Form {
|
||||
pub async fn invoices_page(State(state): State<AppState>) -> Result<Html<String>, StatusCode> {
|
||||
let content = html! {
|
||||
h2 style="text-align: center; margin-bottom: 3rem;" { "Invoices" }
|
||||
div class="grid" {
|
||||
(form_card(
|
||||
"Create BOLT11 Invoice",
|
||||
html! {
|
||||
form method="post" action="/invoices/bolt11" {
|
||||
div class="form-group" {
|
||||
label for="amount_btc" { "Amount" }
|
||||
input type="number" id="amount_btc" name="amount_btc" required placeholder="₿0" step="0.00000001" {}
|
||||
}
|
||||
div class="form-group" {
|
||||
label for="description" { "Description (optional)" }
|
||||
input type="text" id="description" name="description" placeholder="Payment for..." {}
|
||||
}
|
||||
div class="form-group" {
|
||||
label for="expiry_seconds" { "Expiry (seconds, optional)" }
|
||||
input type="number" id="expiry_seconds" name="expiry_seconds" placeholder="3600" {}
|
||||
}
|
||||
button type="submit" { "Create BOLT11 Invoice" }
|
||||
}
|
||||
}
|
||||
))
|
||||
|
||||
(form_card(
|
||||
"Create BOLT12 Offer",
|
||||
html! {
|
||||
form method="post" action="/invoices/bolt12" {
|
||||
div class="form-group" {
|
||||
label for="amount_btc" { "Amount (optional for variable amount)" }
|
||||
input type="number" id="amount_btc" name="amount_btc" placeholder="₿0" step="0.00000001" {}
|
||||
}
|
||||
div class="form-group" {
|
||||
label for="description" { "Description (optional)" }
|
||||
input type="text" id="description" name="description" placeholder="Payment for..." {}
|
||||
}
|
||||
div class="form-group" {
|
||||
label for="expiry_seconds" { "Expiry (seconds, optional)" }
|
||||
input type="number" id="expiry_seconds" name="expiry_seconds" placeholder="3600" {}
|
||||
}
|
||||
button type="submit" { "Create BOLT12 Offer" }
|
||||
div class="card" {
|
||||
// Tab navigation
|
||||
div class="payment-tabs" style="display: flex; gap: 0.5rem; margin-bottom: 1.5rem; border-bottom: 1px solid hsl(var(--border)); padding-bottom: 0;" {
|
||||
button type="button" class="payment-tab active" onclick="switchInvoiceTab('bolt11')" data-tab="bolt11" {
|
||||
"BOLT11 Invoice"
|
||||
}
|
||||
button type="button" class="payment-tab" onclick="switchInvoiceTab('bolt12')" data-tab="bolt12" {
|
||||
"BOLT12 Offer"
|
||||
}
|
||||
}
|
||||
|
||||
// BOLT11 tab content
|
||||
div id="bolt11-content" class="tab-content active" {
|
||||
form method="post" action="/invoices/bolt11" {
|
||||
div class="form-group" {
|
||||
label for="amount_btc_bolt11" { "Amount" }
|
||||
input type="number" id="amount_btc_bolt11" name="amount_btc" required placeholder="₿0" step="0.00000001" {}
|
||||
}
|
||||
div class="form-group" {
|
||||
label for="description_bolt11" { "Description (optional)" }
|
||||
input type="text" id="description_bolt11" name="description" placeholder="Payment for..." {}
|
||||
}
|
||||
div class="form-group" {
|
||||
label for="expiry_seconds_bolt11" { "Expiry (seconds, optional)" }
|
||||
input type="number" id="expiry_seconds_bolt11" name="expiry_seconds" placeholder="3600" {}
|
||||
}
|
||||
div class="form-actions" {
|
||||
a href="/balance" { button type="button" class="button-secondary" { "Cancel" } }
|
||||
button type="submit" class="button-primary" { "Create BOLT11 Invoice" }
|
||||
}
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
// BOLT12 tab content
|
||||
div id="bolt12-content" class="tab-content" {
|
||||
form method="post" action="/invoices/bolt12" {
|
||||
div class="form-group" {
|
||||
label for="amount_btc_bolt12" { "Amount (optional for variable amount)" }
|
||||
input type="number" id="amount_btc_bolt12" name="amount_btc" placeholder="₿0" step="0.00000001" {}
|
||||
p style="font-size: 0.8125rem; color: hsl(var(--muted-foreground)); margin-top: 0.5rem;" {
|
||||
"Leave empty for variable amount offers, specify amount for fixed offers"
|
||||
}
|
||||
}
|
||||
div class="form-group" {
|
||||
label for="description_bolt12" { "Description (optional)" }
|
||||
input type="text" id="description_bolt12" name="description" placeholder="Payment for..." {}
|
||||
}
|
||||
div class="form-group" {
|
||||
label for="expiry_seconds_bolt12" { "Expiry (seconds, optional)" }
|
||||
input type="number" id="expiry_seconds_bolt12" name="expiry_seconds" placeholder="3600" {}
|
||||
}
|
||||
div class="form-actions" {
|
||||
a href="/balance" { button type="button" class="button-secondary" { "Cancel" } }
|
||||
button type="submit" class="button-primary" { "Create BOLT12 Offer" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tab switching script
|
||||
script type="text/javascript" {
|
||||
(maud::PreEscaped(r#"
|
||||
function switchInvoiceTab(tabName) {
|
||||
console.log('Switching to invoice tab:', tabName);
|
||||
|
||||
// Hide all tab contents
|
||||
const contents = document.querySelectorAll('.tab-content');
|
||||
contents.forEach(content => content.classList.remove('active'));
|
||||
|
||||
// Remove active class from all tabs
|
||||
const tabs = document.querySelectorAll('.payment-tab');
|
||||
tabs.forEach(tab => tab.classList.remove('active'));
|
||||
|
||||
// Show selected tab content
|
||||
const tabContent = document.getElementById(tabName + '-content');
|
||||
if (tabContent) {
|
||||
tabContent.classList.add('active');
|
||||
console.log('Activated invoice tab content:', tabName);
|
||||
}
|
||||
|
||||
// Add active class to selected tab
|
||||
const tabButton = document.querySelector('[data-tab="' + tabName + '"]');
|
||||
if (tabButton) {
|
||||
tabButton.classList.add('active');
|
||||
console.log('Activated invoice tab button:', tabName);
|
||||
}
|
||||
}
|
||||
"#))
|
||||
}
|
||||
};
|
||||
|
||||
let is_running = is_node_running(&state.node.inner);
|
||||
Ok(Html(
|
||||
layout_with_status("Create Invoices", content, is_running).into_string(),
|
||||
layout_with_status("s", content, is_running).into_string(),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -126,7 +173,7 @@ pub async fn post_create_bolt11(
|
||||
.status(StatusCode::BAD_REQUEST)
|
||||
.header("content-type", "text/html")
|
||||
.body(Body::from(
|
||||
layout_with_status("Create Invoice Error", content, true).into_string(),
|
||||
layout_with_status(" Error", content, true).into_string(),
|
||||
))
|
||||
.unwrap());
|
||||
}
|
||||
@@ -161,32 +208,25 @@ pub async fn post_create_bolt11(
|
||||
description_text.clone()
|
||||
};
|
||||
|
||||
let invoice_details = vec![
|
||||
("Payment Hash", invoice.payment_hash().to_string()),
|
||||
("Amount", format_sats_as_btc(form.amount_btc)),
|
||||
("Description", description_display),
|
||||
(
|
||||
"Expires At",
|
||||
format!("{}", current_time + expiry_seconds as u64),
|
||||
),
|
||||
];
|
||||
|
||||
html! {
|
||||
(success_message("BOLT11 Invoice created successfully!"))
|
||||
(info_card(
|
||||
"Invoice Details",
|
||||
vec![
|
||||
("Payment Hash", invoice.payment_hash().to_string()),
|
||||
("Amount", format_sats_as_btc(form.amount_btc)),
|
||||
("Description", description_display),
|
||||
("Expires At", format!("{}", current_time + expiry_seconds as u64)),
|
||||
]
|
||||
))
|
||||
div class="card" {
|
||||
h3 { "Invoice (copy this to share)" }
|
||||
textarea readonly style="width: 100%; height: 150px; font-family: monospace; font-size: 0.8rem;" {
|
||||
(invoice.to_string())
|
||||
}
|
||||
}
|
||||
div class="card" {
|
||||
a href="/invoices" { button { "← Create Another Invoice" } }
|
||||
}
|
||||
(invoice_display_card(&invoice.to_string(), &format_sats_as_btc(form.amount_btc), invoice_details, "/invoices"))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Web interface: Failed to create BOLT11 invoice: {}", e);
|
||||
html! {
|
||||
(error_message(&format!("Failed to create invoice: {e}")))
|
||||
(error_message(&format!("Failed to : {e}")))
|
||||
div class="card" {
|
||||
a href="/invoices" { button { "← Try Again" } }
|
||||
}
|
||||
@@ -210,15 +250,15 @@ pub async fn post_create_bolt12(
|
||||
let description_text = form.description.unwrap_or_else(|| "".to_string());
|
||||
|
||||
tracing::info!(
|
||||
"Web interface: Creating BOLT12 offer for amount={:?} btc, description={:?}, expiry={}s",
|
||||
"Web interface: Creating BOLT12 offer for amount={:?} sats, description={:?}, expiry={}s",
|
||||
form.amount_btc,
|
||||
description_text,
|
||||
expiry_seconds
|
||||
);
|
||||
|
||||
let offer_result = if let Some(amount_btc) = form.amount_btc {
|
||||
// Convert Bitcoin to millisatoshis (1 BTC = 100,000,000,000 msats)
|
||||
let amount_msats = (amount_btc * 100_000_000_000.0) as u64;
|
||||
// Convert satoshis to millisatoshis (1 sat = 1,000 msats)
|
||||
let amount_msats = (amount_btc * 1_000.0) as u64;
|
||||
state.node.inner.bolt12_payment().receive(
|
||||
amount_msats,
|
||||
&description_text,
|
||||
@@ -246,7 +286,7 @@ pub async fn post_create_bolt12(
|
||||
|
||||
let amount_display = form
|
||||
.amount_btc
|
||||
.map(|a| format_sats_as_btc((a * 100_000_000.0) as u64))
|
||||
.map(|a| format_sats_as_btc(a as u64))
|
||||
.unwrap_or_else(|| "Variable amount".to_string());
|
||||
|
||||
let description_display = if description_text.is_empty() {
|
||||
@@ -255,26 +295,19 @@ pub async fn post_create_bolt12(
|
||||
description_text
|
||||
};
|
||||
|
||||
let offer_details = vec![
|
||||
("Offer ID", offer.id().to_string()),
|
||||
("Amount", amount_display.clone()),
|
||||
("Description", description_display),
|
||||
(
|
||||
"Expires At",
|
||||
format!("{}", current_time + expiry_seconds as u64),
|
||||
),
|
||||
];
|
||||
|
||||
html! {
|
||||
(success_message("BOLT12 Offer created successfully!"))
|
||||
(info_card(
|
||||
"Offer Details",
|
||||
vec![
|
||||
("Offer ID", offer.id().to_string()),
|
||||
("Amount", amount_display),
|
||||
("Description", description_display),
|
||||
("Expires At", format!("{}", current_time + expiry_seconds as u64)),
|
||||
]
|
||||
))
|
||||
div class="card" {
|
||||
h3 { "Offer (copy this to share)" }
|
||||
textarea readonly style="width: 100%; height: 150px; font-family: monospace; font-size: 0.8rem;" {
|
||||
(offer.to_string())
|
||||
}
|
||||
}
|
||||
div class="card" {
|
||||
a href="/invoices" { button { "← Create Another Offer" } }
|
||||
}
|
||||
(invoice_display_card(&offer.to_string(), &amount_display, offer_details, "/invoices"))
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
|
||||
@@ -26,43 +26,33 @@ pub async fn balance_page(State(state): State<AppState>) -> Result<Html<String>,
|
||||
html! {
|
||||
h2 style="text-align: center; margin-bottom: 3rem;" { "Lightning" }
|
||||
|
||||
// Quick Actions section - individual cards
|
||||
div class="card" style="margin-bottom: 2rem;" {
|
||||
h2 { "Quick Actions" }
|
||||
div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" {
|
||||
// Open Channel Card
|
||||
div class="quick-action-card" {
|
||||
h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Open Channel" }
|
||||
p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Create a new Lightning Network channel to connect with another node." }
|
||||
a href="/channels/open" style="text-decoration: none;" {
|
||||
button class="button-outline" { "Open Channel" }
|
||||
}
|
||||
}
|
||||
|
||||
// Create Invoice Card
|
||||
div class="quick-action-card" {
|
||||
h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Create Invoice" }
|
||||
p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Generate a Lightning invoice to receive payments from other users or services." }
|
||||
a href="/invoices" style="text-decoration: none;" {
|
||||
button class="button-outline" { "Create Invoice" }
|
||||
}
|
||||
}
|
||||
|
||||
// Make Payment Card
|
||||
div class="quick-action-card" {
|
||||
h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Make Lightning Payment" }
|
||||
p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Send Lightning payments to other users using invoices. BOLT 11 & 12 supported." }
|
||||
a href="/invoices" style="text-decoration: none;" {
|
||||
button class="button-outline" { "Make Payment" }
|
||||
}
|
||||
// Inactive channels warning (only show if > 0)
|
||||
@if num_inactive_channels > 0 {
|
||||
div class="card" style="background-color: #fef3c7; border: 1px solid #f59e0b; margin-bottom: 2rem;" {
|
||||
h3 style="color: #92400e; margin-bottom: 0.5rem;" { "⚠️ Inactive Channels Detected" }
|
||||
p style="color: #78350f; margin: 0;" {
|
||||
"You have " (num_inactive_channels) " inactive channel(s). This may indicate a connectivity issue that requires attention."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Balance Information as metric cards
|
||||
// Balance Information with action buttons in header
|
||||
div class="card" {
|
||||
h2 { "Balance Information" }
|
||||
div class="metrics-container" {
|
||||
div style="display: flex; justify-content: space-between; align-items: center; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 0;" {
|
||||
h2 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; margin: 0;" { "Balance Information" }
|
||||
div style="display: flex; gap: 0.5rem;" {
|
||||
a href="/payments/send" style="text-decoration: none;" {
|
||||
button class="button-outline" style="padding: 0.5rem 1rem; font-size: 0.875rem;" { "Send" }
|
||||
}
|
||||
a href="/invoices" style="text-decoration: none;" {
|
||||
button class="button-outline" style="padding: 0.5rem 1rem; font-size: 0.875rem;" { "Receive" }
|
||||
}
|
||||
a href="/channels/open" style="text-decoration: none;" {
|
||||
button class="button-outline" style="padding: 0.5rem 1rem; font-size: 0.875rem;" { "Open Channel" }
|
||||
}
|
||||
}
|
||||
}
|
||||
div class="metrics-container" style="margin-top: 1.5rem;" {
|
||||
div class="metric-card" {
|
||||
div class="metric-value" { (format_sats_as_btc(balances.total_lightning_balance_sats)) }
|
||||
div class="metric-label" { "Lightning Balance" }
|
||||
@@ -75,9 +65,11 @@ pub async fn balance_page(State(state): State<AppState>) -> Result<Html<String>,
|
||||
div class="metric-value" { (format!("{}", num_active_channels)) }
|
||||
div class="metric-label" { "Active Channels" }
|
||||
}
|
||||
div class="metric-card" {
|
||||
div class="metric-value" { (format!("{}", num_inactive_channels)) }
|
||||
div class="metric-label" { "Inactive Channels" }
|
||||
@if num_inactive_channels > 0 {
|
||||
div class="metric-card" {
|
||||
div class="metric-value" style="color: #f59e0b;" { (format!("{}", num_inactive_channels)) }
|
||||
div class="metric-label" { "Inactive Channels" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -90,43 +82,33 @@ pub async fn balance_page(State(state): State<AppState>) -> Result<Html<String>,
|
||||
html! {
|
||||
h2 style="text-align: center; margin-bottom: 3rem;" { "Lightning" }
|
||||
|
||||
// Quick Actions section - individual cards
|
||||
div class="card" style="margin-bottom: 2rem;" {
|
||||
h2 { "Quick Actions" }
|
||||
div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" {
|
||||
// Open Channel Card
|
||||
div class="quick-action-card" {
|
||||
h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Open Channel" }
|
||||
p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Create a new Lightning channel by connecting with another node." }
|
||||
a href="/channels/open" style="text-decoration: none;" {
|
||||
button class="button-outline" { "Open Channel" }
|
||||
}
|
||||
}
|
||||
|
||||
// Create Invoice Card
|
||||
div class="quick-action-card" {
|
||||
h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Create Invoice" }
|
||||
p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Generate a Lightning invoice to receive payments." }
|
||||
a href="/invoices" style="text-decoration: none;" {
|
||||
button class="button-outline" { "Create Invoice" }
|
||||
}
|
||||
}
|
||||
|
||||
// Make Payment Card
|
||||
div class="quick-action-card" {
|
||||
h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Make Lightning Payment" }
|
||||
p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Send Lightning payments to other users using invoices." }
|
||||
a href="/payments/send" style="text-decoration: none;" {
|
||||
button class="button-outline" { "Make Payment" }
|
||||
}
|
||||
// Inactive channels warning (only show if > 0)
|
||||
@if num_inactive_channels > 0 {
|
||||
div class="card" style="background-color: #fef3c7; border: 1px solid #f59e0b; margin-bottom: 2rem;" {
|
||||
h3 style="color: #92400e; margin-bottom: 0.5rem;" { "⚠️ Inactive Channels Detected" }
|
||||
p style="color: #78350f; margin: 0;" {
|
||||
"You have " (num_inactive_channels) " inactive channel(s). This may indicate a connectivity issue that requires attention."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Balance Information as metric cards
|
||||
// Balance Information with action buttons in header
|
||||
div class="card" {
|
||||
h2 { "Balance Information" }
|
||||
div class="metrics-container" {
|
||||
div style="display: flex; justify-content: space-between; align-items: center; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 0;" {
|
||||
h2 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; margin: 0;" { "Balance Information" }
|
||||
div style="display: flex; gap: 0.5rem;" {
|
||||
a href="/payments/send" style="text-decoration: none;" {
|
||||
button class="button-outline" style="padding: 0.5rem 1rem; font-size: 0.875rem;" { "Send" }
|
||||
}
|
||||
a href="/invoices" style="text-decoration: none;" {
|
||||
button class="button-outline" style="padding: 0.5rem 1rem; font-size: 0.875rem;" { "Receive" }
|
||||
}
|
||||
a href="/channels/open" style="text-decoration: none;" {
|
||||
button class="button-outline" style="padding: 0.5rem 1rem; font-size: 0.875rem;" { "Open Channel" }
|
||||
}
|
||||
}
|
||||
}
|
||||
div class="metrics-container" style="margin-top: 1.5rem;" {
|
||||
div class="metric-card" {
|
||||
div class="metric-value" { (format_sats_as_btc(balances.total_lightning_balance_sats)) }
|
||||
div class="metric-label" { "Lightning Balance" }
|
||||
@@ -139,47 +121,48 @@ pub async fn balance_page(State(state): State<AppState>) -> Result<Html<String>,
|
||||
div class="metric-value" { (format!("{}", num_active_channels)) }
|
||||
div class="metric-label" { "Active Channels" }
|
||||
}
|
||||
div class="metric-card" {
|
||||
div class="metric-value" { (format!("{}", num_inactive_channels)) }
|
||||
div class="metric-label" { "Inactive Channels" }
|
||||
@if num_inactive_channels > 0 {
|
||||
div class="metric-card" {
|
||||
div class="metric-value" style="color: #f59e0b;" { (format!("{}", num_inactive_channels)) }
|
||||
div class="metric-label" { "Inactive Channels" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Channel Details header (outside card)
|
||||
h2 class="section-header" { "Channel Details" }
|
||||
h2 class="section-header" style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5;" { "Channel Details" }
|
||||
|
||||
// Channels list
|
||||
@for (index, channel) in channels.iter().enumerate() {
|
||||
// Channels list
|
||||
@for (index, channel) in channels.iter().enumerate() {
|
||||
@let node_id = channel.counterparty_node_id.to_string();
|
||||
@let channel_number = index + 1;
|
||||
|
||||
div class="channel-box" {
|
||||
// Channel number as prominent header
|
||||
div class="channel-alias" { (format!("Channel {}", channel_number)) }
|
||||
// Channel header with number on left and status badge on right
|
||||
div style="display: flex; justify-content: space-between; align-items: center; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 1.5rem;" {
|
||||
div class="channel-alias" style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; margin: 0;" { (format!("Channel {}", channel_number)) }
|
||||
@if channel.is_usable {
|
||||
span class="status-badge status-active" { "Active" }
|
||||
} @else {
|
||||
span class="status-badge status-inactive" { "Inactive" }
|
||||
}
|
||||
}
|
||||
|
||||
// Channel details in left-aligned format
|
||||
// Channel details - ordered by label length
|
||||
div class="channel-details" {
|
||||
div class="detail-row" {
|
||||
span class="detail-label" { "Node ID" }
|
||||
span class="detail-value" { (node_id) }
|
||||
}
|
||||
div class="detail-row" {
|
||||
span class="detail-label" { "Channel ID" }
|
||||
span class="detail-value-amount" { (channel.channel_id.to_string()) }
|
||||
span class="detail-value" { (channel.channel_id.to_string()) }
|
||||
}
|
||||
@if let Some(short_channel_id) = channel.short_channel_id {
|
||||
div class="detail-row" {
|
||||
span class="detail-label" { "Short Channel ID" }
|
||||
span class="detail-value-amount" { (short_channel_id.to_string()) }
|
||||
}
|
||||
}
|
||||
div class="detail-row" {
|
||||
span class="detail-label" { "Node ID" }
|
||||
span class="detail-value-amount" { (node_id) }
|
||||
}
|
||||
div class="detail-row" {
|
||||
span class="detail-label" { "Status" }
|
||||
@if channel.is_usable {
|
||||
span class="status-badge status-active" { "Active" }
|
||||
} @else {
|
||||
span class="status-badge status-inactive" { "Inactive" }
|
||||
span class="detail-value" { (short_channel_id.to_string()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,33 +33,6 @@ pub struct ConfirmOnchainForm {
|
||||
confirmed: Option<String>,
|
||||
}
|
||||
|
||||
fn quick_actions_section() -> maud::Markup {
|
||||
html! {
|
||||
div class="card" style="margin-bottom: 2rem;" {
|
||||
h2 { "Quick Actions" }
|
||||
div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 1.5rem; margin-top: 1.5rem;" {
|
||||
// Receive Bitcoin Card
|
||||
div class="quick-action-card" {
|
||||
h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Receive Bitcoin" }
|
||||
p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Generate a new Bitcoin address to receive on-chain payments from other users or services." }
|
||||
a href="/onchain?action=receive" style="text-decoration: none;" {
|
||||
button class="button-outline" { "Receive Bitcoin" }
|
||||
}
|
||||
}
|
||||
|
||||
// Send Bitcoin Card
|
||||
div class="quick-action-card" {
|
||||
h3 style="font-size: 1.125rem; font-weight: 600; margin-bottom: 0.5rem; color: var(--text-primary);" { "Send Bitcoin" }
|
||||
p style="font-size: 0.875rem; color: var(--text-muted); margin-bottom: 1rem; line-height: 1.4;" { "Send Bitcoin to another address on the blockchain. Standard on-chain transactions." }
|
||||
a href="/onchain?action=send" style="text-decoration: none;" {
|
||||
button class="button-outline" { "Send Bitcoin" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_new_address(State(state): State<AppState>) -> Result<Html<String>, StatusCode> {
|
||||
let address_result = state.node.inner.onchain_payment().new_address();
|
||||
|
||||
@@ -67,8 +40,8 @@ pub async fn get_new_address(State(state): State<AppState>) -> Result<Html<Strin
|
||||
Ok(address) => {
|
||||
html! {
|
||||
div class="card" {
|
||||
h2 { "Bitcoin Address" }
|
||||
div class="address-display" {
|
||||
h2 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 0;" { "Bitcoin Address" }
|
||||
div class="address-display" style="margin-top: 1.5rem;" {
|
||||
div class="address-container" {
|
||||
span class="address-text" { (address.to_string()) }
|
||||
}
|
||||
@@ -113,13 +86,20 @@ pub async fn onchain_page(
|
||||
let mut content = html! {
|
||||
h2 style="text-align: center; margin-bottom: 3rem;" { "On-chain" }
|
||||
|
||||
// Quick Actions section - only show on overview
|
||||
(quick_actions_section())
|
||||
|
||||
// On-chain Balance as metric cards
|
||||
// On-chain Balance with action buttons in header
|
||||
div class="card" {
|
||||
h2 { "On-chain Balance" }
|
||||
div class="metrics-container" {
|
||||
div style="display: flex; justify-content: space-between; align-items: center; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 0;" {
|
||||
h2 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; margin: 0;" { "On-chain Balance" }
|
||||
div style="display: flex; gap: 0.5rem;" {
|
||||
a href="/onchain?action=send" style="text-decoration: none;" {
|
||||
button class="button-outline" style="padding: 0.5rem 1rem; font-size: 0.875rem;" { "Send" }
|
||||
}
|
||||
a href="/onchain?action=receive" style="text-decoration: none;" {
|
||||
button class="button-outline" style="padding: 0.5rem 1rem; font-size: 0.875rem;" { "Receive" }
|
||||
}
|
||||
}
|
||||
}
|
||||
div class="metrics-container" style="margin-top: 1.5rem;" {
|
||||
div class="metric-card" {
|
||||
div class="metric-value" { (format_sats_as_btc(balances.total_onchain_balance_sats)) }
|
||||
div class="metric-label" { "Total Balance" }
|
||||
@@ -162,10 +142,20 @@ pub async fn onchain_page(
|
||||
}
|
||||
))
|
||||
|
||||
// On-chain Balance as metric cards
|
||||
// On-chain Balance with action buttons in header
|
||||
div class="card" {
|
||||
h2 { "On-chain Balance" }
|
||||
div class="metrics-container" {
|
||||
div style="display: flex; justify-content: space-between; align-items: center; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 0;" {
|
||||
h2 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; margin: 0;" { "On-chain Balance" }
|
||||
div style="display: flex; gap: 0.5rem;" {
|
||||
a href="/onchain?action=send" style="text-decoration: none;" {
|
||||
button class="button-outline" style="padding: 0.5rem 1rem; font-size: 0.875rem;" { "Send" }
|
||||
}
|
||||
a href="/onchain?action=receive" style="text-decoration: none;" {
|
||||
button class="button-outline" style="padding: 0.5rem 1rem; font-size: 0.875rem;" { "Receive" }
|
||||
}
|
||||
}
|
||||
}
|
||||
div class="metrics-container" style="margin-top: 1.5rem;" {
|
||||
div class="metric-card" {
|
||||
div class="metric-value" { (format_sats_as_btc(balances.total_onchain_balance_sats)) }
|
||||
div class="metric-label" { "Total Balance" }
|
||||
@@ -196,10 +186,20 @@ pub async fn onchain_page(
|
||||
}
|
||||
))
|
||||
|
||||
// On-chain Balance as metric cards
|
||||
// On-chain Balance with action buttons in header
|
||||
div class="card" {
|
||||
h2 { "On-chain Balance" }
|
||||
div class="metrics-container" {
|
||||
div style="display: flex; justify-content: space-between; align-items: center; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 0;" {
|
||||
h2 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; margin: 0;" { "On-chain Balance" }
|
||||
div style="display: flex; gap: 0.5rem;" {
|
||||
a href="/onchain?action=send" style="text-decoration: none;" {
|
||||
button class="button-outline" style="padding: 0.5rem 1rem; font-size: 0.875rem;" { "Send" }
|
||||
}
|
||||
a href="/onchain?action=receive" style="text-decoration: none;" {
|
||||
button class="button-outline" style="padding: 0.5rem 1rem; font-size: 0.875rem;" { "Receive" }
|
||||
}
|
||||
}
|
||||
}
|
||||
div class="metrics-container" style="margin-top: 1.5rem;" {
|
||||
div class="metric-card" {
|
||||
div class="metric-value" { (format_sats_as_btc(balances.total_onchain_balance_sats)) }
|
||||
div class="metric-label" { "Total Balance" }
|
||||
@@ -324,8 +324,8 @@ pub async fn onchain_confirm_page(
|
||||
|
||||
// Transaction Details Card
|
||||
div class="card" {
|
||||
h2 { "Transaction Details" }
|
||||
div class="transaction-details" {
|
||||
h2 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 0;" { "Transaction Details" }
|
||||
div class="transaction-details" style="margin-top: 1.5rem;" {
|
||||
div class="detail-row" {
|
||||
span class="detail-label" { "Recipient Address:" }
|
||||
span class="detail-value" { (form.address.clone()) }
|
||||
|
||||
@@ -15,7 +15,7 @@ use serde::Deserialize;
|
||||
use crate::web::handlers::utils::{deserialize_optional_u64, get_paginated_payments_streaming};
|
||||
use crate::web::handlers::AppState;
|
||||
use crate::web::templates::{
|
||||
error_message, form_card, format_msats_as_btc, format_sats_as_btc, info_card, is_node_running,
|
||||
error_message, format_msats_as_btc, format_sats_as_btc, info_card, is_node_running,
|
||||
layout_with_status, payment_list_item, success_message,
|
||||
};
|
||||
|
||||
@@ -86,7 +86,7 @@ pub async fn payments_page(
|
||||
div class="card" {
|
||||
div class="payment-list-header" {
|
||||
div {
|
||||
h2 { "Payment History" }
|
||||
h2 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 0;" { "Payment History" }
|
||||
@if total_count > 0 {
|
||||
p style="margin: 0.25rem 0 0 0; color: #666; font-size: 0.9rem;" {
|
||||
"Showing " (start_index + 1) " to " (end_index) " of " (total_count) " payments"
|
||||
@@ -116,14 +116,6 @@ pub async fn payments_page(
|
||||
PaymentDirection::Outbound => "Outbound",
|
||||
};
|
||||
|
||||
@let status_str = match payment.status {
|
||||
PaymentStatus::Pending => "Pending",
|
||||
PaymentStatus::Succeeded => "Succeeded",
|
||||
PaymentStatus::Failed => "Failed",
|
||||
};
|
||||
|
||||
@let amount_str = payment.amount_msat.map(format_msats_as_btc).unwrap_or_else(|| "Unknown".to_string());
|
||||
|
||||
@let (payment_hash, description, payment_type, preimage) = match &payment.kind {
|
||||
PaymentKind::Bolt11 { hash, preimage, .. } => {
|
||||
(Some(hash.to_string()), None::<String>, "BOLT11", preimage.map(|p| p.to_string()))
|
||||
@@ -147,6 +139,27 @@ pub async fn payments_page(
|
||||
},
|
||||
};
|
||||
|
||||
@let status_str = {
|
||||
// Helper function to determine invoice status
|
||||
fn get_invoice_status(status: PaymentStatus, direction: PaymentDirection, payment_type: &str) -> &'static str {
|
||||
match status {
|
||||
PaymentStatus::Succeeded => "Succeeded",
|
||||
PaymentStatus::Failed => "Failed",
|
||||
PaymentStatus::Pending => {
|
||||
// For inbound BOLT11 payments, show "Unpaid" instead of "Pending"
|
||||
if direction == PaymentDirection::Inbound && payment_type == "BOLT11" {
|
||||
"Unpaid"
|
||||
} else {
|
||||
"Pending"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
get_invoice_status(payment.status, payment.direction, payment_type)
|
||||
};
|
||||
|
||||
@let amount_str = payment.amount_msat.map(format_msats_as_btc).unwrap_or_else(|| "Unknown".to_string());
|
||||
|
||||
(payment_list_item(
|
||||
&payment.id.to_string(),
|
||||
direction_str,
|
||||
@@ -245,42 +258,91 @@ pub async fn payments_page(
|
||||
pub async fn send_payments_page(State(state): State<AppState>) -> Result<Html<String>, StatusCode> {
|
||||
let content = html! {
|
||||
h2 style="text-align: center; margin-bottom: 3rem;" { "Send Payment" }
|
||||
div class="grid" {
|
||||
(form_card(
|
||||
"Pay BOLT11 Invoice",
|
||||
html! {
|
||||
form method="post" action="/payments/bolt11" {
|
||||
div class="form-group" {
|
||||
label for="invoice" { "BOLT11 Invoice" }
|
||||
textarea id="invoice" name="invoice" required placeholder="lnbc..." style="height: 120px;" {}
|
||||
}
|
||||
div class="form-group" {
|
||||
label for="amount_btc" { "Amount Override (optional)" }
|
||||
input type="number" id="amount_btc" name="amount_btc" placeholder="Leave empty to use invoice amount" step="1" {}
|
||||
}
|
||||
button type="submit" { "Pay BOLT11 Invoice" }
|
||||
}
|
||||
}
|
||||
))
|
||||
|
||||
(form_card(
|
||||
"Pay BOLT12 Offer",
|
||||
html! {
|
||||
form method="post" action="/payments/bolt12" {
|
||||
div class="form-group" {
|
||||
label for="offer" { "BOLT12 Offer" }
|
||||
textarea id="offer" name="offer" required placeholder="lno..." style="height: 120px;" {}
|
||||
div class="card" {
|
||||
// Tab navigation
|
||||
div class="payment-tabs" style="display: flex; gap: 0.5rem; margin-bottom: 1.5rem; border-bottom: 1px solid hsl(var(--border)); padding-bottom: 0;" {
|
||||
button type="button" class="payment-tab active" onclick="switchTab('bolt11')" data-tab="bolt11" {
|
||||
"BOLT11 Invoice"
|
||||
}
|
||||
button type="button" class="payment-tab" onclick="switchTab('bolt12')" data-tab="bolt12" {
|
||||
"BOLT12 Offer"
|
||||
}
|
||||
}
|
||||
|
||||
// BOLT11 tab content
|
||||
div id="bolt11-content" class="tab-content active" {
|
||||
form method="post" action="/payments/bolt11" {
|
||||
div class="form-group" {
|
||||
label for="invoice" { "BOLT11 Invoice" }
|
||||
textarea id="invoice" name="invoice" required placeholder="lnbc..." rows="4" {}
|
||||
}
|
||||
div class="form-group" {
|
||||
label for="amount_btc_bolt11" { "Amount Override (optional)" }
|
||||
input type="number" id="amount_btc_bolt11" name="amount_btc" placeholder="Leave empty to use invoice amount" step="1" {}
|
||||
p style="font-size: 0.8125rem; color: hsl(var(--muted-foreground)); margin-top: 0.5rem;" {
|
||||
"Only specify an amount if you want to override the invoice amount"
|
||||
}
|
||||
div class="form-group" {
|
||||
label for="amount_btc" { "Amount (required for variable amount offers)" }
|
||||
input type="number" id="amount_btc" name="amount_btc" placeholder="Required for variable amount offers, ignored for fixed amount offers" step="1" {}
|
||||
}
|
||||
button type="submit" { "Pay BOLT12 Offer" }
|
||||
}
|
||||
div class="form-actions" {
|
||||
a href="/balance" { button type="button" class="button-secondary" { "Cancel" } }
|
||||
button type="submit" class="button-primary" { "Pay Invoice" }
|
||||
}
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
// BOLT12 tab content
|
||||
div id="bolt12-content" class="tab-content" {
|
||||
form method="post" action="/payments/bolt12" {
|
||||
div class="form-group" {
|
||||
label for="offer" { "BOLT12 Offer" }
|
||||
textarea id="offer" name="offer" required placeholder="lno..." rows="4" {}
|
||||
}
|
||||
div class="form-group" {
|
||||
label for="amount_btc_bolt12" { "Amount" }
|
||||
input type="number" id="amount_btc_bolt12" name="amount_btc" placeholder="Amount in satoshis" step="1" {}
|
||||
p style="font-size: 0.8125rem; color: hsl(var(--muted-foreground)); margin-top: 0.5rem;" {
|
||||
"Required for variable amount offers, ignored for fixed amount offers"
|
||||
}
|
||||
}
|
||||
div class="form-actions" {
|
||||
a href="/balance" { button type="button" class="button-secondary" { "Cancel" } }
|
||||
button type="submit" class="button-primary" { "Pay Offer" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tab switching script
|
||||
script type="text/javascript" {
|
||||
(maud::PreEscaped(r#"
|
||||
function switchTab(tabName) {
|
||||
console.log('Switching to tab:', tabName);
|
||||
|
||||
// Hide all tab contents
|
||||
const contents = document.querySelectorAll('.tab-content');
|
||||
contents.forEach(content => content.classList.remove('active'));
|
||||
|
||||
// Remove active class from all tabs
|
||||
const tabs = document.querySelectorAll('.payment-tab');
|
||||
tabs.forEach(tab => tab.classList.remove('active'));
|
||||
|
||||
// Show selected tab content
|
||||
const tabContent = document.getElementById(tabName + '-content');
|
||||
if (tabContent) {
|
||||
tabContent.classList.add('active');
|
||||
console.log('Activated tab content:', tabName);
|
||||
}
|
||||
|
||||
// Add active class to selected tab
|
||||
const tabButton = document.querySelector('[data-tab="' + tabName + '"]');
|
||||
if (tabButton) {
|
||||
tabButton.classList.add('active');
|
||||
console.log('Activated tab button:', tabName);
|
||||
}
|
||||
}
|
||||
"#))
|
||||
}
|
||||
};
|
||||
|
||||
let is_running = is_node_running(&state.node.inner);
|
||||
|
||||
@@ -3,11 +3,13 @@ use maud::{html, Markup};
|
||||
pub fn info_card(title: &str, items: Vec<(&str, String)>) -> Markup {
|
||||
html! {
|
||||
div class="card" {
|
||||
h2 { (title) }
|
||||
@for (label, value) in items {
|
||||
div class="info-item" {
|
||||
span class="info-label" { (label) ":" }
|
||||
span class="info-value" { (value) }
|
||||
h2 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 0;" { (title) }
|
||||
div style="margin-top: 1.5rem;" {
|
||||
@for (label, value) in items {
|
||||
div class="info-item" {
|
||||
span class="info-label" { (label) ":" }
|
||||
span class="info-value" { (value) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,8 +19,10 @@ pub fn info_card(title: &str, items: Vec<(&str, String)>) -> Markup {
|
||||
pub fn form_card(title: &str, form_content: Markup) -> Markup {
|
||||
html! {
|
||||
div class="card" {
|
||||
h2 { (title) }
|
||||
(form_content)
|
||||
h2 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 0;" { (title) }
|
||||
div style="margin-top: 1.5rem;" {
|
||||
(form_content)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -34,3 +38,50 @@ pub fn error_message(message: &str) -> Markup {
|
||||
div class="error" { (message) }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn invoice_display_card(
|
||||
invoice_text: &str,
|
||||
amount: &str,
|
||||
details: Vec<(&str, String)>,
|
||||
back_url: &str,
|
||||
) -> Markup {
|
||||
html! {
|
||||
div class="card" {
|
||||
div style="display: flex; justify-content: space-between; align-items: center; padding-bottom: 1rem; border-bottom: 1px solid hsl(var(--border)); margin-bottom: 1.5rem;" {
|
||||
h3 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; margin: 0;" { "Invoice Details" }
|
||||
}
|
||||
|
||||
// Amount highlight section at the top
|
||||
div class="invoice-amount-section" {
|
||||
div class="invoice-amount-label" { "Amount" }
|
||||
div class="invoice-amount-value" { (amount) }
|
||||
}
|
||||
|
||||
// Invoice display section - under the amount
|
||||
div class="invoice-display-section" {
|
||||
div class="invoice-label" { "Invoice" }
|
||||
div class="invoice-display-container" {
|
||||
textarea readonly class="invoice-textarea" { (invoice_text) }
|
||||
}
|
||||
}
|
||||
|
||||
// Invoice details section - after the invoice with increased spacing
|
||||
div class="invoice-details-section" style="margin-top: 2.5rem;" {
|
||||
h4 style="font-size: 0.875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.5; margin: 0 0 1rem 0;" { "Details" }
|
||||
@for (label, value) in details {
|
||||
div class="info-item" {
|
||||
span class="info-label" { (label) ":" }
|
||||
span class="info-value" { (value) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Back button at bottom left - no border lines
|
||||
div style="margin-top: 2rem;" {
|
||||
a href=(back_url) style="text-decoration: none;" {
|
||||
button class="button-outline" style="padding: 0.5rem 1rem; font-size: 0.875rem;" { "Back" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@ pub fn payment_list_item(
|
||||
"Succeeded" => "status-active",
|
||||
"Failed" => "status-inactive",
|
||||
"Pending" => "status-pending",
|
||||
"Unpaid" => "status-pending", // Use pending styling for unpaid
|
||||
_ => "status-badge",
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user