mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-19 06:54:19 +01:00
Added toggle button to switch to store default currency (#3752)
* Added toggle button to switch to store default currency * Replaced ["usd"] with the currency variable * Fix indentation and improve JS part * Update script and display * Improve chart display * Improve rate display * Address code review comments Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
This commit is contained in:
@@ -1,17 +1,42 @@
|
|||||||
@using BTCPayServer.Services.Wallets
|
@using BTCPayServer.Services.Wallets
|
||||||
@model BTCPayServer.Components.StoreWalletBalance.StoreWalletBalanceViewModel
|
@model BTCPayServer.Components.StoreWalletBalance.StoreWalletBalanceViewModel
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#DefaultCurrencyToggle .btn {
|
||||||
|
background-color: var(--btcpay-bg-tile);
|
||||||
|
border-color: var(--btcpay-body-border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
#DefaultCurrencyToggle input:not(:checked) + .btn {
|
||||||
|
color: var(--btcpay-body-text-muted);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
<div id="StoreWalletBalance-@Model.Store.Id" class="widget store-wallet-balance">
|
<div id="StoreWalletBalance-@Model.Store.Id" class="widget store-wallet-balance">
|
||||||
<h6 class="mb-2">Wallet Balance</h6>
|
<div class="d-flex gap-3 align-items-center justify-content-between mb-2">
|
||||||
|
<h6>Wallet Balance</h6>
|
||||||
|
@if (Model.CryptoCode != Model.DefaultCurrency)
|
||||||
|
{
|
||||||
|
<div class="btn-group btn-group-sm gap-0" role="group" id="DefaultCurrencyToggle">
|
||||||
|
<input type="radio" class="btn-check" name="currency" id="currency_@Model.CryptoCode" value="@Model.CryptoCode" autocomplete="off" checked>
|
||||||
|
<label class="btn btn-outline-secondary px-2 py-1" for="currency_@Model.CryptoCode">@Model.CryptoCode</label>
|
||||||
|
<input type="radio" class="btn-check" name="currency" id="currency_@Model.DefaultCurrency" value="@Model.DefaultCurrency" autocomplete="off">
|
||||||
|
<label class="btn btn-outline-secondary px-2 py-1" for="currency_@Model.DefaultCurrency">@Model.DefaultCurrency</label>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
<header class="mb-3">
|
<header class="mb-3">
|
||||||
@if (Model.Balance is not null)
|
@if (Model.Balance is not null)
|
||||||
{
|
{
|
||||||
<div class="balance">
|
<div class="balance" id="Balance-@Model.CryptoCode">
|
||||||
<h3 class="d-inline-block me-1">@Model.Balance</h3>
|
<h3 class="d-inline-block me-1">@Model.Balance</h3>
|
||||||
<span class="text-secondary fw-semibold">@Model.CryptoCode</span>
|
<span class="text-secondary fw-semibold">@Model.CryptoCode</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
<div class="balance" id="Balance-@Model.DefaultCurrency">
|
||||||
<div class="btn-group mt-1" role="group" aria-label="Filter">
|
<h3 class="d-inline-block" id="DefaultCurrencyBalance"></h3>
|
||||||
|
<span class="text-secondary fw-semibold">@Model.DefaultCurrency</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
<div class="btn-group mt-1" role="group" aria-label="Filter">
|
||||||
<input type="radio" class="btn-check" name="filter" id="filter-week" value="week" @(Model.Type == WalletHistogramType.Week ? "checked" : "")>
|
<input type="radio" class="btn-check" name="filter" id="filter-week" value="week" @(Model.Type == WalletHistogramType.Week ? "checked" : "")>
|
||||||
<label class="btn btn-link" for="filter-week">1W</label>
|
<label class="btn btn-link" for="filter-week">1W</label>
|
||||||
<input type="radio" class="btn-check" name="filter" id="filter-month" value="month" @(Model.Type == WalletHistogramType.Month ? "checked" : "")>
|
<input type="radio" class="btn-check" name="filter" id="filter-month" value="month" @(Model.Type == WalletHistogramType.Month ? "checked" : "")>
|
||||||
@@ -20,50 +45,112 @@
|
|||||||
<label class="btn btn-link" for="filter-year">1Y</label>
|
<label class="btn btn-link" for="filter-year">1Y</label>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="ct-chart ct-major-eleventh"></div>
|
<div class="ct-chart"></div>
|
||||||
<script>
|
<script>
|
||||||
(function () {
|
(function () {
|
||||||
const id = 'StoreWalletBalance-@Model.Store.Id';
|
const balance = @Safe.Json(Model.Balance);
|
||||||
|
const storeId = @Safe.Json(Model.Store.Id);
|
||||||
|
const cryptoCode = @Safe.Json(Model.CryptoCode);
|
||||||
|
const defaultCurrency = @Safe.Json(Model.DefaultCurrency);
|
||||||
|
const divisibility = @Safe.Json(Model.CurrencyData.Divisibility);
|
||||||
|
const pathBase = @Safe.Json(Context.Request.PathBase);
|
||||||
|
let data = { series: @Safe.Json(Model.Series), labels: @Safe.Json(Model.Labels), balance: @Safe.Json(Model.Balance) };
|
||||||
|
let rate = null;
|
||||||
|
|
||||||
|
const $cryptoBalance = document.getElementById(`Balance-${cryptoCode}`);
|
||||||
|
const $defaultBalance = document.getElementById(`Balance-${defaultCurrency}`);
|
||||||
|
const $defaultCurrencyBalance = document.getElementById('DefaultCurrencyBalance');
|
||||||
|
|
||||||
|
$defaultBalance.style.display = 'none';
|
||||||
|
|
||||||
|
const id = `StoreWalletBalance-${storeId}`;
|
||||||
const baseUrl = @Safe.Json(Url.Action("WalletHistogram", "UIWallets", new { walletId = Model.WalletId, type = WalletHistogramType.Week }));
|
const baseUrl = @Safe.Json(Url.Action("WalletHistogram", "UIWallets", new { walletId = Model.WalletId, type = WalletHistogramType.Week }));
|
||||||
|
const chartOpts = {
|
||||||
|
fullWidth: true,
|
||||||
|
showArea: true,
|
||||||
|
axisY: {
|
||||||
|
labelInterpolationFnc: value => rate
|
||||||
|
? displayDefaultCurrency(value, defaultCurrency).toString()
|
||||||
|
: value
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const render = data => {
|
const render = data => {
|
||||||
const { series, labels, balance } = data;
|
let { series, labels, balance } = data;
|
||||||
|
if (rate)
|
||||||
|
series = data.series.map(i => toDefaultCurrency(i, rate));
|
||||||
if (balance)
|
if (balance)
|
||||||
document.querySelector(`#${id} h3`).innerText = balance;
|
document.querySelector(`#${id} h3`).innerText = balance;
|
||||||
const min = Math.min(...series);
|
const min = Math.min(...series);
|
||||||
const max = Math.max(...series);
|
const max = Math.max(...series);
|
||||||
const low = Math.max(min - ((max - min) / 5), 0);
|
const low = Math.max(min - ((max - min) / 5), 0);
|
||||||
|
const renderOpts = Object.assign({}, chartOpts, { low });
|
||||||
const chart = new Chartist.Line(`#${id} .ct-chart`, {
|
const chart = new Chartist.Line(`#${id} .ct-chart`, {
|
||||||
labels,
|
labels,
|
||||||
series: [series]
|
series: [series]
|
||||||
}, {
|
}, renderOpts);
|
||||||
low,
|
|
||||||
fullWidth: true,
|
|
||||||
showArea: true
|
|
||||||
});
|
|
||||||
// prevent y-axis labels from getting cut off
|
// prevent y-axis labels from getting cut off
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
const yLabels = [...document.querySelectorAll('.ct-label.ct-vertical.ct-start')];
|
const yLabels = [...document.querySelectorAll('.ct-label.ct-vertical.ct-start')];
|
||||||
if (yLabels) {
|
if (yLabels) {
|
||||||
const width = Math.max(...(yLabels.map(l => l.innerText.length * 8)))
|
const factor = rate ? 6 : 8;
|
||||||
chart.update(null, { axisY: { offset: width } })
|
const width = Math.max(...(yLabels.map(l => l.innerText.length * factor)));
|
||||||
|
const opts = Object.assign({}, renderOpts, {
|
||||||
|
axisY: Object.assign({}, renderOpts.axisY, { offset: width })
|
||||||
|
});
|
||||||
|
chart.update(null, opts);
|
||||||
}
|
}
|
||||||
}, 0)
|
}, 0)
|
||||||
};
|
};
|
||||||
|
|
||||||
const update = async type => {
|
const update = async type => {
|
||||||
const url = baseUrl.replace(/\/week$/gi, `/${type}`);
|
const url = baseUrl.replace(/\/week$/gi, `/${type}`);
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const json = await response.json();
|
data = await response.json();
|
||||||
render(json);
|
render(data);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
render({ series: @Safe.Json(Model.Series), labels: @Safe.Json(Model.Labels), balance: @Safe.Json(Model.Balance) });
|
|
||||||
|
const toDefaultCurrency = (value, rate) => {
|
||||||
|
return Math.round((value * rate) * 100) / 100;
|
||||||
|
};
|
||||||
|
|
||||||
|
const displayDefaultCurrency = (value, currency) => {
|
||||||
|
const locale = currency === "USD" ? 'en-US' : navigator.language;
|
||||||
|
const opts = { currency, style: 'decimal', minimumFractionDigits: divisibility };
|
||||||
|
return new Intl.NumberFormat(locale, opts).format(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
render(data);
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
delegate('change', `#${id} [name="filter"]`, async e => {
|
delegate('change', `#${id} [name="filter"]`, async e => {
|
||||||
const type = e.target.value;
|
const type = e.target.value;
|
||||||
await update(type);
|
await update(type);
|
||||||
})
|
})
|
||||||
})
|
delegate('change', '#DefaultCurrencyToggle input', async e => {
|
||||||
|
const { target } = e;
|
||||||
|
if (target.value === defaultCurrency) {
|
||||||
|
const currencyPair = `${cryptoCode}_${defaultCurrency}`;
|
||||||
|
const response = await fetch(`${pathBase}/api/rates?storeId=${storeId}¤cyPairs=${currencyPair}`);
|
||||||
|
const json = await response.json();
|
||||||
|
rate = json[0] && json[0].rate;
|
||||||
|
if (rate) {
|
||||||
|
const value = toDefaultCurrency(balance, rate);
|
||||||
|
$defaultCurrencyBalance.innerText = displayDefaultCurrency(value, defaultCurrency);
|
||||||
|
render(data);
|
||||||
|
} else {
|
||||||
|
console.warn(`Fetching rate for ${currencyPair} failed.`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rate = null;
|
||||||
|
render(data);
|
||||||
|
}
|
||||||
|
$cryptoBalance.style.display = rate ? 'none' : 'block';
|
||||||
|
$defaultBalance.style.display = rate ? 'block' : 'none';
|
||||||
|
});
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
using BTCPayServer.Services;
|
using BTCPayServer.Services;
|
||||||
|
using BTCPayServer.Services.Rates;
|
||||||
using BTCPayServer.Services.Stores;
|
using BTCPayServer.Services.Stores;
|
||||||
using BTCPayServer.Services.Wallets;
|
using BTCPayServer.Services.Wallets;
|
||||||
using Dapper;
|
using Dapper;
|
||||||
@@ -18,28 +19,37 @@ namespace BTCPayServer.Components.StoreWalletBalance;
|
|||||||
|
|
||||||
public class StoreWalletBalance : ViewComponent
|
public class StoreWalletBalance : ViewComponent
|
||||||
{
|
{
|
||||||
private string CryptoCode;
|
private string _cryptoCode;
|
||||||
private const WalletHistogramType DefaultType = WalletHistogramType.Week;
|
private const WalletHistogramType DefaultType = WalletHistogramType.Week;
|
||||||
|
|
||||||
private readonly StoreRepository _storeRepo;
|
private readonly StoreRepository _storeRepo;
|
||||||
|
private readonly CurrencyNameTable _currencies;
|
||||||
private readonly WalletHistogramService _walletHistogramService;
|
private readonly WalletHistogramService _walletHistogramService;
|
||||||
|
|
||||||
public StoreWalletBalance(StoreRepository storeRepo, WalletHistogramService walletHistogramService, BTCPayNetworkProvider networkProvider)
|
public StoreWalletBalance(
|
||||||
|
StoreRepository storeRepo,
|
||||||
|
CurrencyNameTable currencies,
|
||||||
|
WalletHistogramService walletHistogramService,
|
||||||
|
BTCPayNetworkProvider networkProvider)
|
||||||
{
|
{
|
||||||
_storeRepo = storeRepo;
|
_storeRepo = storeRepo;
|
||||||
|
_currencies = currencies;
|
||||||
_walletHistogramService = walletHistogramService;
|
_walletHistogramService = walletHistogramService;
|
||||||
CryptoCode = networkProvider.DefaultNetwork.CryptoCode;
|
_cryptoCode = networkProvider.DefaultNetwork.CryptoCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<IViewComponentResult> InvokeAsync(StoreData store)
|
public async Task<IViewComponentResult> InvokeAsync(StoreData store)
|
||||||
{
|
{
|
||||||
var walletId = new WalletId(store.Id, CryptoCode);
|
var walletId = new WalletId(store.Id, _cryptoCode);
|
||||||
var data = await _walletHistogramService.GetHistogram(store, walletId, DefaultType);
|
var data = await _walletHistogramService.GetHistogram(store, walletId, DefaultType);
|
||||||
|
var defaultCurrency = store.GetStoreBlob().DefaultCurrency;
|
||||||
|
|
||||||
var vm = new StoreWalletBalanceViewModel
|
var vm = new StoreWalletBalanceViewModel
|
||||||
{
|
{
|
||||||
Store = store,
|
Store = store,
|
||||||
CryptoCode = CryptoCode,
|
CryptoCode = _cryptoCode,
|
||||||
|
CurrencyData = _currencies.GetCurrencyData(defaultCurrency, true),
|
||||||
|
DefaultCurrency = defaultCurrency,
|
||||||
WalletId = walletId,
|
WalletId = walletId,
|
||||||
Series = data?.Series,
|
Series = data?.Series,
|
||||||
Labels = data?.Labels,
|
Labels = data?.Labels,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using BTCPayServer.Data;
|
using BTCPayServer.Data;
|
||||||
|
using BTCPayServer.Services.Rates;
|
||||||
using BTCPayServer.Services.Wallets;
|
using BTCPayServer.Services.Wallets;
|
||||||
|
|
||||||
namespace BTCPayServer.Components.StoreWalletBalance;
|
namespace BTCPayServer.Components.StoreWalletBalance;
|
||||||
@@ -8,6 +9,8 @@ public class StoreWalletBalanceViewModel
|
|||||||
{
|
{
|
||||||
public decimal? Balance { get; set; }
|
public decimal? Balance { get; set; }
|
||||||
public string CryptoCode { get; set; }
|
public string CryptoCode { get; set; }
|
||||||
|
public string DefaultCurrency { get; set; }
|
||||||
|
public CurrencyData CurrencyData { get; set; }
|
||||||
public StoreData Store { get; set; }
|
public StoreData Store { get; set; }
|
||||||
public WalletId WalletId { get; set; }
|
public WalletId WalletId { get; set; }
|
||||||
public WalletHistogramType Type { get; set; }
|
public WalletHistogramType Type { get; set; }
|
||||||
|
|||||||
Reference in New Issue
Block a user