work on vue and signalr fro crowdfund

This commit is contained in:
Kukks
2018-12-27 20:19:21 +01:00
parent 2b84791391
commit e97bb9c933
22 changed files with 26052 additions and 136 deletions

View File

@@ -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>

View File

@@ -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;

View File

@@ -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));

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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)}
});
}
}
}

View File

@@ -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);
}

View File

@@ -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

View 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>

View File

@@ -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>

View File

@@ -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"
]
}
]

View 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();
}
});
};

View File

@@ -0,0 +1,7 @@
Vue.component('contribute', {
data: function () {
return {
}
},
template: ''
});

View 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
};
}();

View File

@@ -0,0 +1,2 @@
[v-cloak] > * { display:none }
[v-cloak]::before { content: "loading…" }

File diff suppressed because one or more lines are too long

View 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;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long