mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +01:00
work on vue and signalr fro crowdfund
This commit is contained in:
@@ -124,6 +124,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Build\" />
|
||||
<Folder Include="Views\AppsPublic\Crowdfund\Templates" />
|
||||
<Folder Include="wwwroot\vendor\clipboard.js\" />
|
||||
<Folder Include="wwwroot\vendor\highlightjs\" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Security.Claims;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Hubs;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Security;
|
||||
@@ -27,22 +28,16 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
public AppsPublicController(AppsHelper appsHelper,
|
||||
InvoiceController invoiceController,
|
||||
RateFetcher rateFetcher,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
InvoiceRepository invoiceRepository)
|
||||
CrowdfundHubStreamer crowdfundHubStreamer)
|
||||
{
|
||||
_AppsHelper = appsHelper;
|
||||
_InvoiceController = invoiceController;
|
||||
_rateFetcher = rateFetcher;
|
||||
_btcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_invoiceRepository = invoiceRepository;
|
||||
_CrowdfundHubStreamer = crowdfundHubStreamer;
|
||||
}
|
||||
|
||||
private AppsHelper _AppsHelper;
|
||||
private InvoiceController _InvoiceController;
|
||||
private readonly RateFetcher _rateFetcher;
|
||||
private readonly BTCPayNetworkProvider _btcPayNetworkProvider;
|
||||
private readonly InvoiceRepository _invoiceRepository;
|
||||
private readonly CrowdfundHubStreamer _CrowdfundHubStreamer;
|
||||
|
||||
[HttpGet]
|
||||
[Route("/apps/{appId}/pos")]
|
||||
@@ -92,10 +87,8 @@ namespace BTCPayServer.Controllers
|
||||
var app = await _AppsHelper.GetApp(appId, AppType.Crowdfund, true);
|
||||
if (app == null)
|
||||
return NotFound();
|
||||
var settings = app.GetSettings<CrowdfundSettings>();
|
||||
var currency = _AppsHelper.GetCurrencyData(settings.TargetCurrency, false);
|
||||
|
||||
return View(await CrowdfundHelper.GetInfo(app, _invoiceRepository, _rateFetcher, _btcPayNetworkProvider, statusMessage ));
|
||||
return View(await _CrowdfundHubStreamer.GetCrowdfundInfo(appId));
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
@@ -114,13 +107,15 @@ namespace BTCPayServer.Controllers
|
||||
store.AdditionalClaims.Add(new Claim(Policies.CanCreateInvoice.Key, store.Id));
|
||||
var invoice = await _InvoiceController.CreateInvoiceCore(new Invoice()
|
||||
{
|
||||
OrderId = appId,
|
||||
OrderId = $"{CrowdfundHubStreamer.CrowdfundInvoiceOrderIdPrefix}{appId}",
|
||||
Currency = settings.TargetCurrency,
|
||||
BuyerEmail = request.Email,
|
||||
Price = request.Amount,
|
||||
NotificationURL = settings.NotificationUrl,
|
||||
FullNotifications = true,
|
||||
ExtendedNotifications = true,
|
||||
RedirectURL = HttpContext.Request.GetAbsoluteRoot(),
|
||||
|
||||
}, store, HttpContext.Request.GetAbsoluteRoot());
|
||||
if (request.RedirectToCheckout)
|
||||
{
|
||||
@@ -129,10 +124,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
else
|
||||
{
|
||||
return Json(new
|
||||
{
|
||||
InvoiceId = invoice.Data.Id
|
||||
});
|
||||
return Ok(invoice.Data.Id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,94 +191,6 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
public class CrowdfundHelper
|
||||
{
|
||||
private static async Task<decimal> GetCurrentContributionAmount(InvoiceEntity[] invoices, string primaryCurrency,
|
||||
RateFetcher rateFetcher, RateRules rateRules)
|
||||
{
|
||||
decimal result = 0;
|
||||
|
||||
var groupingByCurrency = invoices.GroupBy(entity => entity.ProductInformation.Currency);
|
||||
|
||||
var ratesTask = rateFetcher.FetchRates(
|
||||
groupingByCurrency
|
||||
.Select((entities) => new CurrencyPair(entities.Key, primaryCurrency))
|
||||
.ToHashSet(),
|
||||
rateRules);
|
||||
|
||||
var finalTasks = new List<Task>();
|
||||
foreach (var rateTask in ratesTask)
|
||||
{
|
||||
finalTasks.Add(Task.Run(async () =>
|
||||
{
|
||||
var tResult = await rateTask.Value;
|
||||
var rate = tResult.BidAsk?.Bid;
|
||||
if (rate == null) return;
|
||||
var currencyGroup = groupingByCurrency.Single(entities => entities.Key == rateTask.Key.Left);
|
||||
result += currencyGroup.Sum(entity => entity.ProductInformation.Price / rate.Value);
|
||||
}));
|
||||
}
|
||||
|
||||
await Task.WhenAll(finalTasks);
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
public static async Task<ViewCrowdfundViewModel> GetInfo(AppData appData, InvoiceRepository invoiceRepository,
|
||||
RateFetcher rateFetcher, BTCPayNetworkProvider btcPayNetworkProvider, string statusMessage= null)
|
||||
{
|
||||
var settings = appData.GetSettings<CrowdfundSettings>();
|
||||
var invoices = await GetPaidInvoicesForApp(appData, invoiceRepository);
|
||||
var rateRules = appData.StoreData.GetStoreBlob().GetRateRules(btcPayNetworkProvider);
|
||||
var currentAmount = await GetCurrentContributionAmount(
|
||||
invoices,
|
||||
settings.TargetCurrency, rateFetcher, rateRules);
|
||||
var paidInvoices = invoices.Length;
|
||||
var active = (settings.StartDate == null || DateTime.UtcNow >= settings.StartDate) &&
|
||||
(settings.EndDate == null || DateTime.UtcNow <= settings.EndDate) &&
|
||||
(!settings.EnforceTargetAmount || settings.TargetAmount > currentAmount);
|
||||
|
||||
return new ViewCrowdfundViewModel()
|
||||
{
|
||||
Title = settings.Title,
|
||||
Tagline = settings.Tagline,
|
||||
Description = settings.Description,
|
||||
CustomCSSLink = settings.CustomCSSLink,
|
||||
MainImageUrl = settings.MainImageUrl,
|
||||
EmbeddedCSS = settings.EmbeddedCSS,
|
||||
StoreId = appData.StoreDataId,
|
||||
AppId = appData.Id,
|
||||
StartDate = settings.StartDate,
|
||||
EndDate = settings.EndDate,
|
||||
TargetAmount = settings.TargetAmount,
|
||||
TargetCurrency = settings.TargetCurrency,
|
||||
EnforceTargetAmount = settings.EnforceTargetAmount,
|
||||
StatusMessage = statusMessage,
|
||||
Info = new ViewCrowdfundViewModel.CrowdfundInfo()
|
||||
{
|
||||
TotalContributors = paidInvoices,
|
||||
CurrentAmount = currentAmount,
|
||||
Active = active,
|
||||
DaysLeft = settings.EndDate.HasValue? (settings.EndDate - DateTime.UtcNow).Value.Days: (int?) null,
|
||||
DaysLeftToStart = settings.StartDate.HasValue? (settings.StartDate - DateTime.UtcNow).Value.Days: (int?) null,
|
||||
ShowProgress =active && settings.TargetAmount.HasValue,
|
||||
ProgressPercentage = currentAmount/ settings.TargetAmount * 100
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<InvoiceEntity[]> GetPaidInvoicesForApp(AppData appData, InvoiceRepository invoiceRepository)
|
||||
{
|
||||
return await invoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
OrderId = appData.Id,
|
||||
Status = new string[]{ InvoiceState.ToString(InvoiceStatus.Complete)}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public class AppsHelper
|
||||
{
|
||||
ApplicationDbContextFactory _ContextFactory;
|
||||
|
||||
@@ -638,13 +638,13 @@ namespace BTCPayServer.Controllers
|
||||
if (newState == "invalid")
|
||||
{
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToInvalid(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1008, "invoice_markedInvalid"));
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1008, InvoiceEvent.MarkedInvalid));
|
||||
StatusMessage = "Invoice marked invalid";
|
||||
}
|
||||
else if(newState == "complete")
|
||||
{
|
||||
await _InvoiceRepository.UpdatePaidInvoiceToComplete(invoiceId);
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2008, "invoice_markedComplete"));
|
||||
_EventAggregator.Publish(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2008, InvoiceEvent.MarkedCompleted));
|
||||
StatusMessage = "Invoice marked complete";
|
||||
}
|
||||
return RedirectToAction(nameof(ListInvoices));
|
||||
|
||||
@@ -8,6 +8,18 @@ namespace BTCPayServer.Events
|
||||
{
|
||||
public class InvoiceEvent
|
||||
{
|
||||
public const string Created = "invoice_created";
|
||||
public const string ReceivedPayment = "invoice_receivedPayment";
|
||||
public const string MarkedCompleted = "invoice_markedComplete";
|
||||
public const string MarkedInvalid= "invoice_markedInvalid";
|
||||
public const string Expired= "invoice_expired";
|
||||
public const string ExpiredPaidPartial= "invoice_expiredPaidPartial";
|
||||
public const string PaidInFull= "invoice_paidInFull";
|
||||
public const string PaidAfterExpiration= "invoice_paidAfterExpiration";
|
||||
public const string FailedToConfirm= "invoice_failedToConfirm";
|
||||
public const string Confirmed= "invoice_confirmed";
|
||||
public const string Completed= "invoice_completed";
|
||||
|
||||
public InvoiceEvent(Models.InvoiceResponse invoice, int code, string name)
|
||||
{
|
||||
Invoice = invoice;
|
||||
|
||||
@@ -66,10 +66,10 @@ namespace BTCPayServer.HostedServices
|
||||
context.MarkDirty();
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1004, "invoice_expired"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1004, InvoiceEvent.Expired));
|
||||
invoice.Status = InvoiceStatus.Expired;
|
||||
if(invoice.ExceptionStatus == InvoiceExceptionStatus.PaidPartial)
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2000, "invoice_expiredPaidPartial"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 2000, InvoiceEvent.ExpiredPaidPartial));
|
||||
}
|
||||
|
||||
var payments = invoice.GetPayments().Where(p => p.Accounted).ToArray();
|
||||
@@ -84,7 +84,7 @@ namespace BTCPayServer.HostedServices
|
||||
{
|
||||
if (invoice.Status == InvoiceStatus.New)
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1003, "invoice_paidInFull"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1003, InvoiceEvent.PaidInFull));
|
||||
invoice.Status = InvoiceStatus.Paid;
|
||||
invoice.ExceptionStatus = accounting.Paid > accounting.TotalDue ? InvoiceExceptionStatus.PaidOver : InvoiceExceptionStatus.None;
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
@@ -93,7 +93,7 @@ namespace BTCPayServer.HostedServices
|
||||
else if (invoice.Status == InvoiceStatus.Expired && invoice.ExceptionStatus != InvoiceExceptionStatus.PaidLate)
|
||||
{
|
||||
invoice.ExceptionStatus = InvoiceExceptionStatus.PaidLate;
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1009, "invoice_paidAfterExpiration"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1009, InvoiceEvent.PaidAfterExpiration));
|
||||
context.MarkDirty();
|
||||
}
|
||||
}
|
||||
@@ -139,14 +139,14 @@ namespace BTCPayServer.HostedServices
|
||||
(confirmedAccounting.Paid < accounting.MinimumTotalDue))
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1013, "invoice_failedToConfirm"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1013, InvoiceEvent.FailedToConfirm));
|
||||
invoice.Status = InvoiceStatus.Invalid;
|
||||
context.MarkDirty();
|
||||
}
|
||||
else if (confirmedAccounting.Paid >= accounting.MinimumTotalDue)
|
||||
{
|
||||
await _InvoiceRepository.UnaffectAddress(invoice.Id);
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1005, "invoice_confirmed"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1005, InvoiceEvent.Confirmed));
|
||||
invoice.Status = InvoiceStatus.Confirmed;
|
||||
context.MarkDirty();
|
||||
}
|
||||
@@ -157,7 +157,7 @@ namespace BTCPayServer.HostedServices
|
||||
var completedAccounting = paymentMethod.Calculate(p => p.GetCryptoPaymentData().PaymentCompleted(p, network));
|
||||
if (completedAccounting.Paid >= accounting.MinimumTotalDue)
|
||||
{
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1006, "invoice_completed"));
|
||||
context.Events.Add(new InvoiceEvent(invoice.EntityToDTO(_NetworkProvider), 1006, InvoiceEvent.Completed));
|
||||
invoice.Status = InvoiceStatus.Complete;
|
||||
context.MarkDirty();
|
||||
}
|
||||
@@ -247,13 +247,13 @@ namespace BTCPayServer.HostedServices
|
||||
}));
|
||||
leases.Add(_EventAggregator.Subscribe<Events.InvoiceEvent>(async b =>
|
||||
{
|
||||
if (b.Name == "invoice_created")
|
||||
if (b.Name == InvoiceEvent.Created)
|
||||
{
|
||||
Watch(b.Invoice.Id);
|
||||
await Wait(b.Invoice.Id);
|
||||
}
|
||||
|
||||
if (b.Name == "invoice_receivedPayment")
|
||||
if (b.Name == InvoiceEvent.ReceivedPayment)
|
||||
{
|
||||
Watch(b.Invoice.Id);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,221 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Controllers;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Events;
|
||||
using BTCPayServer.Models.AppViewModels;
|
||||
using BTCPayServer.Rating;
|
||||
using BTCPayServer.Services.Apps;
|
||||
using BTCPayServer.Services.Invoices;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.Extensions.Caching.Memory;
|
||||
using Microsoft.Extensions.Primitives;
|
||||
|
||||
namespace BTCPayServer.Hubs
|
||||
{
|
||||
public class CrowdfundHub: Hub
|
||||
{
|
||||
private readonly AppsPublicController _AppsPublicController;
|
||||
|
||||
public CrowdfundHub(AppsPublicController appsPublicController)
|
||||
{
|
||||
_AppsPublicController = appsPublicController;
|
||||
}
|
||||
public async Task ListenToCrowdfundApp(string appId)
|
||||
{
|
||||
if (Context.Items.ContainsKey("app"))
|
||||
{
|
||||
await Groups.RemoveFromGroupAsync(Context.ConnectionId, Context.Items["app"].ToString());
|
||||
Context.Items.Remove("app");
|
||||
}
|
||||
Context.Items.Add("app", appId);
|
||||
await Groups.AddToGroupAsync(Context.ConnectionId, appId);
|
||||
}
|
||||
|
||||
public async Task PushUpdatedCrowdfundInfo()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public async Task<string> CreateInvoice(ContributeToCrowdfund model)
|
||||
{
|
||||
model.RedirectToCheckout = false;
|
||||
var result = await _AppsPublicController.ContributeToCrowdfund(Context.Items["app"].ToString(), model);
|
||||
return (result as OkObjectResult)?.Value.ToString();
|
||||
}
|
||||
|
||||
public async Task PaymentReceived()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public class CrowdfundHubStreamer
|
||||
{
|
||||
public const string CrowdfundInvoiceOrderIdPrefix = "crowdfund-app:";
|
||||
private readonly EventAggregator _EventAggregator;
|
||||
private readonly IHubContext<CrowdfundHub> _HubContext;
|
||||
private readonly IMemoryCache _MemoryCache;
|
||||
private readonly AppsHelper _AppsHelper;
|
||||
private readonly RateFetcher _RateFetcher;
|
||||
private readonly BTCPayNetworkProvider _BtcPayNetworkProvider;
|
||||
private readonly InvoiceRepository _InvoiceRepository;
|
||||
|
||||
private Dictionary<string, CancellationTokenSource> _CacheTokens = new Dictionary<string, CancellationTokenSource>();
|
||||
public CrowdfundHubStreamer(EventAggregator eventAggregator,
|
||||
IHubContext<CrowdfundHub> hubContext,
|
||||
IMemoryCache memoryCache,
|
||||
AppsHelper appsHelper,
|
||||
RateFetcher rateFetcher,
|
||||
BTCPayNetworkProvider btcPayNetworkProvider,
|
||||
InvoiceRepository invoiceRepository)
|
||||
{
|
||||
_EventAggregator = eventAggregator;
|
||||
_HubContext = hubContext;
|
||||
_MemoryCache = memoryCache;
|
||||
_AppsHelper = appsHelper;
|
||||
_RateFetcher = rateFetcher;
|
||||
_BtcPayNetworkProvider = btcPayNetworkProvider;
|
||||
_InvoiceRepository = invoiceRepository;
|
||||
SubscribeToEvents();
|
||||
}
|
||||
|
||||
public Task<ViewCrowdfundViewModel> GetCrowdfundInfo(string appId)
|
||||
{
|
||||
var key = GetCacheKey(appId);
|
||||
return _MemoryCache.GetOrCreateAsync(key, async entry =>
|
||||
{
|
||||
if (_CacheTokens.ContainsKey(key))
|
||||
{
|
||||
_CacheTokens.Remove(key);
|
||||
}
|
||||
|
||||
var token = new CancellationTokenSource();
|
||||
_CacheTokens.Add(key, token);
|
||||
entry.AddExpirationToken(new CancellationChangeToken(token.Token));
|
||||
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(20);
|
||||
|
||||
var app = await _AppsHelper.GetApp(appId, AppType.Crowdfund, true);
|
||||
var result = await GetInfo(app, _InvoiceRepository, _RateFetcher,
|
||||
_BtcPayNetworkProvider);
|
||||
entry.SetValue(result);
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
private void SubscribeToEvents()
|
||||
{
|
||||
|
||||
_EventAggregator.Subscribe<InvoiceEvent>(Subscription);
|
||||
}
|
||||
|
||||
private string GetCacheKey(string appId)
|
||||
{
|
||||
return $"{CrowdfundInvoiceOrderIdPrefix}:{appId}";
|
||||
}
|
||||
|
||||
private void Subscription(InvoiceEvent invoiceEvent)
|
||||
{
|
||||
if (!invoiceEvent.Invoice.OrderId.StartsWith(CrowdfundInvoiceOrderIdPrefix, StringComparison.InvariantCultureIgnoreCase))
|
||||
{
|
||||
return;
|
||||
}
|
||||
var appId = invoiceEvent.Invoice.OrderId.Replace(CrowdfundInvoiceOrderIdPrefix, "", StringComparison.InvariantCultureIgnoreCase);
|
||||
if (invoiceEvent.Name == InvoiceEvent.ReceivedPayment)
|
||||
{
|
||||
_HubContext.Clients.Group(appId).SendCoreAsync(nameof(CrowdfundHub.PaymentReceived), new object[]{ invoiceEvent.Invoice.AmountPaid } );
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<decimal> GetCurrentContributionAmount(InvoiceEntity[] invoices, string primaryCurrency,
|
||||
RateFetcher rateFetcher, RateRules rateRules)
|
||||
{
|
||||
decimal result = 0;
|
||||
|
||||
var groupingByCurrency = invoices.GroupBy(entity => entity.ProductInformation.Currency);
|
||||
|
||||
var ratesTask = rateFetcher.FetchRates(
|
||||
groupingByCurrency
|
||||
.Select((entities) => new CurrencyPair(entities.Key, primaryCurrency))
|
||||
.ToHashSet(),
|
||||
rateRules);
|
||||
|
||||
var finalTasks = new List<Task>();
|
||||
foreach (var rateTask in ratesTask)
|
||||
{
|
||||
finalTasks.Add(Task.Run(async () =>
|
||||
{
|
||||
var tResult = await rateTask.Value;
|
||||
var rate = tResult.BidAsk?.Bid;
|
||||
if (rate == null) return;
|
||||
var currencyGroup = groupingByCurrency.Single(entities => entities.Key == rateTask.Key.Left);
|
||||
result += currencyGroup.Sum(entity => entity.ProductInformation.Price / rate.Value);
|
||||
}));
|
||||
}
|
||||
|
||||
await Task.WhenAll(finalTasks);
|
||||
|
||||
return result;
|
||||
|
||||
}
|
||||
|
||||
public static async Task<ViewCrowdfundViewModel> GetInfo(AppData appData, InvoiceRepository invoiceRepository,
|
||||
RateFetcher rateFetcher, BTCPayNetworkProvider btcPayNetworkProvider, string statusMessage= null)
|
||||
{
|
||||
var settings = appData.GetSettings<AppsController.CrowdfundSettings>();
|
||||
var invoices = await GetPaidInvoicesForApp(appData, invoiceRepository);
|
||||
var rateRules = appData.StoreData.GetStoreBlob().GetRateRules(btcPayNetworkProvider);
|
||||
var currentAmount = await GetCurrentContributionAmount(
|
||||
invoices,
|
||||
settings.TargetCurrency, rateFetcher, rateRules);
|
||||
var paidInvoices = invoices.Length;
|
||||
var active = (settings.StartDate == null || DateTime.UtcNow >= settings.StartDate) &&
|
||||
(settings.EndDate == null || DateTime.UtcNow <= settings.EndDate) &&
|
||||
(!settings.EnforceTargetAmount || settings.TargetAmount > currentAmount);
|
||||
|
||||
return new ViewCrowdfundViewModel()
|
||||
{
|
||||
Title = settings.Title,
|
||||
Tagline = settings.Tagline,
|
||||
Description = settings.Description,
|
||||
CustomCSSLink = settings.CustomCSSLink,
|
||||
MainImageUrl = settings.MainImageUrl,
|
||||
EmbeddedCSS = settings.EmbeddedCSS,
|
||||
StoreId = appData.StoreDataId,
|
||||
AppId = appData.Id,
|
||||
StartDate = settings.StartDate,
|
||||
EndDate = settings.EndDate,
|
||||
TargetAmount = settings.TargetAmount,
|
||||
TargetCurrency = settings.TargetCurrency,
|
||||
EnforceTargetAmount = settings.EnforceTargetAmount,
|
||||
StatusMessage = statusMessage,
|
||||
Info = new ViewCrowdfundViewModel.CrowdfundInfo()
|
||||
{
|
||||
TotalContributors = paidInvoices,
|
||||
CurrentAmount = currentAmount,
|
||||
Active = active,
|
||||
DaysLeft = settings.EndDate.HasValue? (settings.EndDate - DateTime.UtcNow).Value.Days: (int?) null,
|
||||
DaysLeftToStart = settings.StartDate.HasValue? (settings.StartDate - DateTime.UtcNow).Value.Days: (int?) null,
|
||||
ShowProgress =active && settings.TargetAmount.HasValue,
|
||||
ProgressPercentage = currentAmount/ settings.TargetAmount * 100
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task<InvoiceEntity[]> GetPaidInvoicesForApp(AppData appData, InvoiceRepository invoiceRepository)
|
||||
{
|
||||
return await invoiceRepository.GetInvoices(new InvoiceQuery()
|
||||
{
|
||||
OrderId = appData.Id,
|
||||
Status = new string[]{ InvoiceState.ToString(InvoiceStatus.Complete)}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ namespace BTCPayServer.Payments.Lightning
|
||||
{
|
||||
leases.Add(_Aggregator.Subscribe<Events.InvoiceEvent>(async inv =>
|
||||
{
|
||||
if (inv.Name == "invoice_created")
|
||||
if (inv.Name == InvoiceEvent.Created)
|
||||
{
|
||||
await EnsureListening(inv.Invoice.Id, false);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@using BTCPayServer.Models.AppViewModels
|
||||
@model BTCPayServer.Models.AppViewModels.ViewCrowdfundViewModel
|
||||
<div class="container p-0">
|
||||
<div class="container p-0" >
|
||||
|
||||
|
||||
<div class="row h-100 w-100 py-sm-0 py-md-4 mx-0">
|
||||
@@ -87,7 +87,7 @@
|
||||
{
|
||||
<hr/>
|
||||
<h3>Contribute</h3>
|
||||
<partial name="ContributeForm" model="@(new ContributeToCrowdfund()
|
||||
<partial name="Crowdfund/ContributeForm" model="@(new ContributeToCrowdfund()
|
||||
{
|
||||
RedirectToCheckout = true,
|
||||
ViewCrowdfundViewModel = Model
|
||||
76
BTCPayServer/Views/AppsPublic/Crowdfund/VueCrowdfund.cshtml
Normal file
76
BTCPayServer/Views/AppsPublic/Crowdfund/VueCrowdfund.cshtml
Normal file
@@ -0,0 +1,76 @@
|
||||
<div class="container p-0" id="app" v-cloak>
|
||||
<div class="row h-100 w-100 py-sm-0 py-md-4 mx-0">
|
||||
<div class="card w-100 p-0 mx-0">
|
||||
<img class="card-img-top" :src="srvModel.mainImageUrl" v-if="srvModel.mainImageUrl">
|
||||
<div class="progress rounded-0 striped" style="min-height: 30px" v-if="srvModel.info.showProgres">
|
||||
<div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" :aria-valuenow="srvModel.info.progressPercentage" aria-valuemin="0" aria-valuemax="100">
|
||||
<template v-if="srvModel.info.progressPercentage > 0">
|
||||
{{srvModel.info.progressPercentage}} %
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-title row">
|
||||
<div class="col-md-9 col-sm-12">
|
||||
|
||||
<h1 >
|
||||
{{srvModel.title}}
|
||||
<h2 class="text-muted" v-if="srvModel.tagline">{{srvModel.tagline}}</h2>
|
||||
<small v-if="srvModel.info.daysLeftToStart && srvModel.info.daysLeftToStart > 0">
|
||||
{{ srvModel.info.daysLeftToStart }} days left to start
|
||||
</small>
|
||||
</h1>
|
||||
|
||||
</div>
|
||||
<ul class="list-group list-group-flush col-md-3 col-sm-12">
|
||||
<li class="list-group-item">
|
||||
<template v-if="srvModel.endDate">Ends {{srvModel.endDate}}</template>
|
||||
<template v-else>No specific end date</template>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<template v-if="srvModel.targetAmount">{{srvModel.targetAmount}} {{srvModel.targetCurrency}} Goal</template>
|
||||
<template v-else>No specific target goal</template>
|
||||
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
<template v-if="srvModel.enforceTargetAmount">Hardcap Goal</template>
|
||||
<template v-else>Softcap Goal</template>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</div>
|
||||
<div class="card-deck mb-4" v-if="srvModel.info.active">
|
||||
<div class="card shadow">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-center">{{srvModel.info.totalContributors}}</h5>
|
||||
<h6 class="card-text text-center"> Contributors</h6>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card shadow">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-center">{{srvModel.info.currentAmount}} {{srvModel.info.targetCurrency}}</h5>
|
||||
<h6 class="card-text text-center"> Raised</h6>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card shadow" v-if="srvModel.info.daysLeft && srvModel.info.daysLeft >0">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title text-center">{{srvModel.info.daysLeft}}</h5>
|
||||
<h6 class="card-text text-center">Days left</h6>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="card-text" v-html="srvModel.description"></div>
|
||||
<template v-if="srvModel.info.active"></template>
|
||||
<hr/>
|
||||
<h3>Contribute</h3>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -18,21 +18,35 @@
|
||||
{
|
||||
<link href="@Model.CustomCSSLink" rel="stylesheet"/>
|
||||
}
|
||||
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet"/>
|
||||
@if (!Context.Request.Query.ContainsKey("simple"))
|
||||
{
|
||||
<script type="text/javascript">
|
||||
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||
</script>
|
||||
<bundle name="wwwroot/bundles/crowdfund-bundle.min.js"></bundle>
|
||||
|
||||
<bundle name="wwwroot/bundles/crowdfund-bundle.min.css"></bundle>
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(Model.EmbeddedCSS))
|
||||
{
|
||||
<style>
|
||||
@Html.Raw(Model.EmbeddedCSS);
|
||||
</style>
|
||||
}
|
||||
<script type="text/javascript">
|
||||
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body class="h-100">
|
||||
<partial name="MinimalCrowdfund" model="@Model"/>
|
||||
<body>
|
||||
@if (Context.Request.Query.ContainsKey("simple"))
|
||||
{
|
||||
@await Html.PartialAsync("Crowdfund/MinimalCrowdfund", Model)
|
||||
}
|
||||
else
|
||||
{
|
||||
<noscript>
|
||||
@await Html.PartialAsync("Crowdfund/MinimalCrowdfund", Model)
|
||||
</noscript>
|
||||
|
||||
@await Html.PartialAsync("Crowdfund/VueCrowdfund", Model)
|
||||
}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
"wwwroot/checkout/**/*.js"
|
||||
]
|
||||
},
|
||||
|
||||
{
|
||||
"outputFileName": "wwwroot/bundles/cart-bundle.min.js",
|
||||
"inputFiles": [
|
||||
@@ -65,9 +66,21 @@
|
||||
{
|
||||
"outputFileName": "wwwroot/bundles/crowdfund-bundle.min.js",
|
||||
"inputFiles": [
|
||||
"wwwroot/vendor/jquery/jquery.js",
|
||||
"wwwroot/vendor/bootstrap4/js/bootstrap.js",
|
||||
"wwwroot/modal/btcpay.js"
|
||||
"wwwroot/vendor/vuejs/vue.min.js",
|
||||
"wwwroot/vendor/babel-polyfill/polyfill.min.js",
|
||||
"wwwroot/vendor/bootstrap-vue/bootstrap-vue.js",
|
||||
"wwwroot/vendor/signalr/signalr.js",
|
||||
"wwwroot/vendor/moment/moment.js",
|
||||
"wwwroot/modal/btcpay.js",
|
||||
"wwwroot/crowdfund/**/*.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
"outputFileName": "wwwroot/bundles/crowdfund-bundle.min.css",
|
||||
"inputFiles": [
|
||||
"wwwroot/vendor/font-awesome/css/font-awesome.min.css",
|
||||
"wwwroot/vendor/bootstrap-vue/bootstrap-vue.css",
|
||||
"wwwroot/crowdfund/**/*.css"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
17
BTCPayServer/wwwroot/crowdfund/app.js
Normal file
17
BTCPayServer/wwwroot/crowdfund/app.js
Normal file
@@ -0,0 +1,17 @@
|
||||
var app = null;
|
||||
window.onload = function (ev) {
|
||||
|
||||
|
||||
app = new Vue({
|
||||
el: '#app',
|
||||
data: function () {
|
||||
return {
|
||||
srvModel: window.srvModel
|
||||
}
|
||||
},
|
||||
mounted: function () {
|
||||
hubListener.connect();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
7
BTCPayServer/wwwroot/crowdfund/components/contribute.js
Normal file
7
BTCPayServer/wwwroot/crowdfund/components/contribute.js
Normal file
@@ -0,0 +1,7 @@
|
||||
Vue.component('contribute', {
|
||||
data: function () {
|
||||
return {
|
||||
}
|
||||
},
|
||||
template: ''
|
||||
});
|
||||
42
BTCPayServer/wwwroot/crowdfund/services/listener.js
Normal file
42
BTCPayServer/wwwroot/crowdfund/services/listener.js
Normal file
@@ -0,0 +1,42 @@
|
||||
|
||||
var hubListener = function(){
|
||||
|
||||
var statuses = {
|
||||
DISCONNECTED: "disconnected",
|
||||
CONNECTED: "connected",
|
||||
CONNECTING: "connecting"
|
||||
};
|
||||
var status = "disconnected";
|
||||
|
||||
|
||||
var connection = new signalR.HubConnectionBuilder().withUrl("/crowdfundHub").build();
|
||||
|
||||
connection.onclose(function(){
|
||||
this.status = statuses.DISCONNECTED;
|
||||
console.error("Connection was closed. Attempting reconnect in 2s");
|
||||
setTimeout(connect, 2000);
|
||||
});
|
||||
|
||||
|
||||
|
||||
function connect(){
|
||||
status = statuses.CONNECTING;
|
||||
connection
|
||||
.start()
|
||||
.then(function(){
|
||||
this.status = statuses.CONNECTED;
|
||||
})
|
||||
.catch(function (err) {
|
||||
this.status = statuses.DISCONNECTED;
|
||||
console.error("Could not connect to backend. Retrying in 2s", err );
|
||||
setTimeout(connect, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
statuses: statuses,
|
||||
status: status,
|
||||
connect: connect
|
||||
};
|
||||
}();
|
||||
|
||||
2
BTCPayServer/wwwroot/crowdfund/styles/main.css
Normal file
2
BTCPayServer/wwwroot/crowdfund/styles/main.css
Normal file
@@ -0,0 +1,2 @@
|
||||
[v-cloak] > * { display:none }
|
||||
[v-cloak]::before { content: "loading…" }
|
||||
4
BTCPayServer/wwwroot/vendor/babel-polyfill/polyfill.min.js
vendored
Normal file
4
BTCPayServer/wwwroot/vendor/babel-polyfill/polyfill.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
311
BTCPayServer/wwwroot/vendor/bootstrap-vue/bootstrap-vue.css
vendored
Normal file
311
BTCPayServer/wwwroot/vendor/bootstrap-vue/bootstrap-vue.css
vendored
Normal file
@@ -0,0 +1,311 @@
|
||||
.fade-enter-active, .fade-leave-active {
|
||||
transition: opacity .15s linear;
|
||||
}
|
||||
.fade-enter, .fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* workaround for https://github.com/bootstrap-vue/bootstrap-vue/issues/1560 */
|
||||
/* source: _input-group.scss */
|
||||
|
||||
.input-group > .input-group-prepend > .b-dropdown > .btn,
|
||||
.input-group > .input-group-append:not(:last-child) > .b-dropdown > .btn,
|
||||
.input-group > .input-group-append:last-child > .b-dropdown:not(:last-child):not(.dropdown-toggle) > .btn {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.input-group > .input-group-append > .b-dropdown > .btn,
|
||||
.input-group > .input-group-prepend:not(:first-child) > .b-dropdown > .btn,
|
||||
.input-group > .input-group-prepend:first-child > .b-dropdown:not(:first-child) > .btn {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
/* Special styling for type=range and type=color input */
|
||||
input.form-control[type="range"],
|
||||
input.form-control[type="color"] {
|
||||
height: 2.25rem;
|
||||
}
|
||||
input.form-control.form-control-sm[type="range"],
|
||||
input.form-control.form-control-sm[type="color"] {
|
||||
height: 1.9375rem;
|
||||
}
|
||||
input.form-control.form-control-lg[type="range"],
|
||||
input.form-control.form-control-lg[type="color"] {
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
/* Less padding on type=color */
|
||||
input.form-control[type="color"] {
|
||||
padding: 0.25rem 0.25rem;
|
||||
}
|
||||
input.form-control.form-control-sm[type="color"] {
|
||||
padding: 0.125rem 0.125rem;
|
||||
}
|
||||
|
||||
/* Add support for fixed layout table */
|
||||
table.b-table.b-table-fixed {
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
/* Busy table styling */
|
||||
table.b-table[aria-busy='false'] {
|
||||
opacity: 1;
|
||||
}
|
||||
table.b-table[aria-busy='true'] {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Sort styling */
|
||||
table.b-table > thead > tr > th,
|
||||
table.b-table > tfoot > tr > th {
|
||||
position: relative;
|
||||
}
|
||||
table.b-table > thead > tr > th.sorting,
|
||||
table.b-table > tfoot > tr > th.sorting {
|
||||
padding-right: 1.5em;
|
||||
cursor: pointer;
|
||||
}
|
||||
table.b-table > thead > tr > th.sorting::before,
|
||||
table.b-table > thead > tr > th.sorting::after,
|
||||
table.b-table > tfoot > tr > th.sorting::before,
|
||||
table.b-table > tfoot > tr > th.sorting::after {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
display: block;
|
||||
opacity: 0.4;
|
||||
padding-bottom: inherit;
|
||||
font-size: inherit;
|
||||
line-height: 180%;
|
||||
}
|
||||
table.b-table > thead > tr > th.sorting::before,
|
||||
table.b-table > tfoot > tr > th.sorting::before {
|
||||
right: 0.75em;
|
||||
content: '\2191';
|
||||
}
|
||||
table.b-table > thead > tr > th.sorting::after,
|
||||
table.b-table > tfoot > tr > th.sorting::after {
|
||||
right: 0.25em;
|
||||
content: '\2193';
|
||||
}
|
||||
table.b-table > thead > tr > th.sorting_asc::after,
|
||||
table.b-table > thead > tr > th.sorting_desc::before,
|
||||
table.b-table > tfoot > tr > th.sorting_asc::after,
|
||||
table.b-table > tfoot > tr > th.sorting_desc::before {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Stacked table layout */
|
||||
/* Derived from http://blog.adrianroselli.com/2017/11/a-responsive-accessible-table.html */
|
||||
/* Always stacked */
|
||||
table.b-table.b-table-stacked {
|
||||
width: 100%;
|
||||
}
|
||||
table.b-table.b-table-stacked,
|
||||
table.b-table.b-table-stacked > tbody,
|
||||
table.b-table.b-table-stacked > tbody > tr,
|
||||
table.b-table.b-table-stacked > tbody > tr > td,
|
||||
table.b-table.b-table-stacked > tbody > tr > th,
|
||||
table.b-table.b-table-stacked > caption {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Hide stuff we can't deal with, or shouldn't show */
|
||||
table.b-table.b-table-stacked > thead,
|
||||
table.b-table.b-table-stacked > tfoot,
|
||||
table.b-table.b-table-stacked > tbody > tr.b-table-top-row,
|
||||
table.b-table.b-table-stacked > tbody > tr.b-table-bottom-row {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* inter-row top border */
|
||||
table.b-table.b-table-stacked > tbody > tr > :first-child {
|
||||
border-top-width: 0.4rem;
|
||||
}
|
||||
|
||||
/* convert TD/TH contents to "cells". Caveat: child elements become cells! */
|
||||
table.b-table.b-table-stacked > tbody > tr > [data-label] {
|
||||
display: grid;
|
||||
grid-template-columns: 40% auto;
|
||||
grid-gap: 0.25rem 1rem;
|
||||
}
|
||||
|
||||
/* generate row cell "heading" */
|
||||
table.b-table.b-table-stacked > tbody > tr > [data-label]::before {
|
||||
content: attr(data-label);
|
||||
display: inline;
|
||||
text-align: right;
|
||||
overflow-wrap: break-word;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
|
||||
@media all and (max-width: 575.99px) {
|
||||
/* Under SM */
|
||||
table.b-table.b-table-stacked-sm {
|
||||
width: 100%;
|
||||
}
|
||||
table.b-table.b-table-stacked-sm,
|
||||
table.b-table.b-table-stacked-sm > tbody,
|
||||
table.b-table.b-table-stacked-sm > tbody > tr,
|
||||
table.b-table.b-table-stacked-sm > tbody > tr > td,
|
||||
table.b-table.b-table-stacked-sm > tbody > tr > th,
|
||||
table.b-table.b-table-stacked-sm > caption {
|
||||
display: block;
|
||||
}
|
||||
/* hide stuff we can't deal with, or shouldn't show */
|
||||
table.b-table.b-table-stacked-sm > thead,
|
||||
table.b-table.b-table-stacked-sm > tfoot,
|
||||
table.b-table.b-table-stacked-sm > tbody > tr.b-table-top-row,
|
||||
table.b-table.b-table-stacked-sm > tbody > tr.b-table-bottom-row {
|
||||
display: none;
|
||||
}
|
||||
/* inter-row top border */
|
||||
table.b-table.b-table-stacked-sm > tbody > tr > :first-child {
|
||||
border-top-width: 0.4rem;
|
||||
}
|
||||
/* convert TD/TH contents to "cells". Caveat: child elements become cells! */
|
||||
table.b-table.b-table-stacked-sm > tbody > tr > [data-label] {
|
||||
display: grid;
|
||||
grid-template-columns: 40% auto;
|
||||
grid-gap: 0.25rem 1rem;
|
||||
}
|
||||
/* generate row cell "heading" */
|
||||
table.b-table.b-table-stacked-sm > tbody > tr > [data-label]::before {
|
||||
content: attr(data-label);
|
||||
display: inline;
|
||||
text-align: right;
|
||||
overflow-wrap: break-word;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 767.99px) {
|
||||
/* under MD */
|
||||
table.b-table.b-table-stacked-md {
|
||||
width: 100%;
|
||||
}
|
||||
table.b-table.b-table-stacked-md,
|
||||
table.b-table.b-table-stacked-md > tbody,
|
||||
table.b-table.b-table-stacked-md > tbody > tr,
|
||||
table.b-table.b-table-stacked-md > tbody > tr > td,
|
||||
table.b-table.b-table-stacked-md > tbody > tr > th,
|
||||
table.b-table.b-table-stacked-md > caption {
|
||||
display: block;
|
||||
}
|
||||
/* hide stuff we can't deal with, or shouldn't show */
|
||||
table.b-table.b-table-stacked-md > thead,
|
||||
table.b-table.b-table-stacked-md > tfoot,
|
||||
table.b-table.b-table-stacked-md > tbody > tr.b-table-top-row,
|
||||
table.b-table.b-table-stacked-md > tbody > tr.b-table-bottom-row {
|
||||
display: none;
|
||||
}
|
||||
/* inter-row top border */
|
||||
table.b-table.b-table-stacked-md > tbody > tr > :first-child {
|
||||
border-top-width: 0.4rem;
|
||||
}
|
||||
/* convert TD/TH contents to "cells". Caveat: child elements become cells! */
|
||||
table.b-table.b-table-stacked-md > tbody > tr > [data-label] {
|
||||
display: grid;
|
||||
grid-template-columns: 40% auto;
|
||||
grid-gap: 0.25rem 1rem;
|
||||
}
|
||||
/* generate row cell "heading" */
|
||||
table.b-table.b-table-stacked-md > tbody > tr > [data-label]::before {
|
||||
content: attr(data-label);
|
||||
display: inline;
|
||||
text-align: right;
|
||||
overflow-wrap: break-word;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 991.99px) {
|
||||
/* under LG */
|
||||
table.b-table.b-table-stacked-lg {
|
||||
width: 100%;
|
||||
}
|
||||
table.b-table.b-table-stacked-lg,
|
||||
table.b-table.b-table-stacked-lg > tbody,
|
||||
table.b-table.b-table-stacked-lg > tbody > tr,
|
||||
table.b-table.b-table-stacked-lg > tbody > tr > td,
|
||||
table.b-table.b-table-stacked-lg > tbody > tr > th,
|
||||
table.b-table.b-table-stacked-lg > caption {
|
||||
display: block;
|
||||
}
|
||||
/* hide stuff we can't deal with, or shouldn't show */
|
||||
table.b-table.b-table-stacked-lg > thead,
|
||||
table.b-table.b-table-stacked-lg > tfoot,
|
||||
table.b-table.b-table-stacked-lg > tbody > tr.b-table-top-row,
|
||||
table.b-table.b-table-stacked-lg > tbody > tr.b-table-bottom-row {
|
||||
display: none;
|
||||
}
|
||||
/* inter-row top border */
|
||||
table.b-table.b-table-stacked-lg > tbody > tr > :first-child {
|
||||
border-top-width: 0.4rem;
|
||||
}
|
||||
/* convert TD/TH contents to "cells". Caveat: child elements become cells! */
|
||||
table.b-table.b-table-stacked-lg > tbody > tr > [data-label] {
|
||||
display: grid;
|
||||
grid-template-columns: 40% auto;
|
||||
grid-gap: 0.25rem 1rem;
|
||||
}
|
||||
/* generate row cell "heading" */
|
||||
table.b-table.b-table-stacked-lg > tbody > tr > [data-label]::before {
|
||||
content: attr(data-label);
|
||||
display: inline;
|
||||
text-align: right;
|
||||
overflow-wrap: break-word;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 1199.99px) {
|
||||
/* under XL */
|
||||
table.b-table.b-table-stacked-xl {
|
||||
width: 100%;
|
||||
}
|
||||
table.b-table.b-table-stacked-xl,
|
||||
table.b-table.b-table-stacked-xl > tbody,
|
||||
table.b-table.b-table-stacked-xl > tbody > tr,
|
||||
table.b-table.b-table-stacked-xl > tbody > tr > td,
|
||||
table.b-table.b-table-stacked-xl > tbody > tr > th,
|
||||
table.b-table.b-table-stacked-xl > caption {
|
||||
display: block;
|
||||
}
|
||||
/* hide stuff we can't deal with, or shouldn't show */
|
||||
table.b-table.b-table-stacked-xl > thead,
|
||||
table.b-table.b-table-stacked-xl > tfoot,
|
||||
table.b-table.b-table-stacked-xl > tbody > tr.b-table-top-row,
|
||||
table.b-table.b-table-stacked-xl > tbody > tr.b-table-bottom-row {
|
||||
display: none;
|
||||
}
|
||||
/* inter-row top border */
|
||||
table.b-table.b-table-stacked-xl > tbody > tr > :first-child {
|
||||
border-top-width: 0.4rem;
|
||||
}
|
||||
/* convert TD/TH contents to "cells". Caveat: child elements become cells! */
|
||||
table.b-table.b-table-stacked-xl > tbody > tr > [data-label] {
|
||||
display: grid;
|
||||
grid-template-columns: 40% auto;
|
||||
grid-gap: 0.25rem 1rem;
|
||||
}
|
||||
/* generate row cell "heading" */
|
||||
table.b-table.b-table-stacked-xl > tbody > tr > [data-label]::before {
|
||||
content: attr(data-label);
|
||||
display: inline;
|
||||
text-align: right;
|
||||
overflow-wrap: break-word;
|
||||
font-weight: bold;
|
||||
font-style: normal;
|
||||
}
|
||||
}
|
||||
|
||||
/* Details row styling */
|
||||
table.b-table > tbody > tr.b-table-details > td {
|
||||
border-top: none;
|
||||
}
|
||||
16702
BTCPayServer/wwwroot/vendor/bootstrap-vue/bootstrap-vue.js
vendored
Normal file
16702
BTCPayServer/wwwroot/vendor/bootstrap-vue/bootstrap-vue.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
4511
BTCPayServer/wwwroot/vendor/moment/moment.js
vendored
Normal file
4511
BTCPayServer/wwwroot/vendor/moment/moment.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
4088
BTCPayServer/wwwroot/vendor/signalr/signalr.js
vendored
Normal file
4088
BTCPayServer/wwwroot/vendor/signalr/signalr.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
BTCPayServer/wwwroot/vendor/vue-toasted/vue-toasted.min.js
vendored
Normal file
1
BTCPayServer/wwwroot/vendor/vue-toasted/vue-toasted.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user