mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 14:34:23 +01:00
Enable shopping cart, add items to cart, enable tips (#410)
Modal cart, remove items, checkout Fix removal and adding of cart items Improve cart UI Add cart bundle, remove unused js files from the view when cart isn't used Do not enable cart by default Do not put modal into the view when the cart is disabled Escape js properties Work with amounts as cents Make animation speed look constant Enable tips in the cart Fix cart UI
This commit is contained in:
committed by
Nicolas Dorier
parent
e144d2479b
commit
1831692761
@@ -1463,6 +1463,7 @@ namespace BTCPayServer.Tests
|
|||||||
vmpos.Currency = "CAD";
|
vmpos.Currency = "CAD";
|
||||||
vmpos.ButtonText = "{0} Purchase";
|
vmpos.ButtonText = "{0} Purchase";
|
||||||
vmpos.CustomButtonText = "Nicolas Sexy Hair";
|
vmpos.CustomButtonText = "Nicolas Sexy Hair";
|
||||||
|
vmpos.CustomTipText = "Wanna tip?";
|
||||||
vmpos.Template = @"
|
vmpos.Template = @"
|
||||||
apple:
|
apple:
|
||||||
price: 5.0
|
price: 5.0
|
||||||
@@ -1487,6 +1488,7 @@ donation:
|
|||||||
Assert.Equal("$5.00", vmview.Items[0].Price.Formatted);
|
Assert.Equal("$5.00", vmview.Items[0].Price.Formatted);
|
||||||
Assert.Equal("{0} Purchase", vmview.ButtonText);
|
Assert.Equal("{0} Purchase", vmview.ButtonText);
|
||||||
Assert.Equal("Nicolas Sexy Hair", vmview.CustomButtonText);
|
Assert.Equal("Nicolas Sexy Hair", vmview.CustomButtonText);
|
||||||
|
Assert.Equal("Wanna tip?", vmview.CustomTipText);
|
||||||
Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result);
|
Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result);
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -50,17 +50,21 @@ namespace BTCPayServer.Controllers
|
|||||||
" description: The Tibetan Himalayas, the land is majestic and beautiful—a spiritual place where, despite the perilous environment, many journey seeking enlightenment. Pay us what you want!\n" +
|
" description: The Tibetan Himalayas, the land is majestic and beautiful—a spiritual place where, despite the perilous environment, many journey seeking enlightenment. Pay us what you want!\n" +
|
||||||
" image: https://cdn.pixabay.com/photo/2016/09/16/11/24/darts-1673812__480.jpg\n" +
|
" image: https://cdn.pixabay.com/photo/2016/09/16/11/24/darts-1673812__480.jpg\n" +
|
||||||
" custom: true";
|
" custom: true";
|
||||||
|
EnableShoppingCart = false;
|
||||||
ShowCustomAmount = true;
|
ShowCustomAmount = true;
|
||||||
}
|
}
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
public string Currency { get; set; }
|
public string Currency { get; set; }
|
||||||
public string Template { get; set; }
|
public string Template { get; set; }
|
||||||
|
public bool EnableShoppingCart { get; set; }
|
||||||
public bool ShowCustomAmount { get; set; }
|
public bool ShowCustomAmount { get; set; }
|
||||||
|
|
||||||
public const string BUTTON_TEXT_DEF = "Buy for {0}";
|
public const string BUTTON_TEXT_DEF = "Buy for {0}";
|
||||||
public string ButtonText { get; set; } = BUTTON_TEXT_DEF;
|
public string ButtonText { get; set; } = BUTTON_TEXT_DEF;
|
||||||
public const string CUSTOM_BUTTON_TEXT_DEF = "Pay";
|
public const string CUSTOM_BUTTON_TEXT_DEF = "Pay";
|
||||||
public string CustomButtonText { get; set; } = CUSTOM_BUTTON_TEXT_DEF;
|
public string CustomButtonText { get; set; } = CUSTOM_BUTTON_TEXT_DEF;
|
||||||
|
public const string CUSTOM_TIP_TEXT_DEF = "Do you want to leave a tip?";
|
||||||
|
public string CustomTipText { get; set; } = CUSTOM_TIP_TEXT_DEF;
|
||||||
|
|
||||||
public string CustomCSSLink { get; set; }
|
public string CustomCSSLink { get; set; }
|
||||||
}
|
}
|
||||||
@@ -76,11 +80,13 @@ namespace BTCPayServer.Controllers
|
|||||||
var vm = new UpdatePointOfSaleViewModel()
|
var vm = new UpdatePointOfSaleViewModel()
|
||||||
{
|
{
|
||||||
Title = settings.Title,
|
Title = settings.Title,
|
||||||
|
EnableShoppingCart = settings.EnableShoppingCart,
|
||||||
ShowCustomAmount = settings.ShowCustomAmount,
|
ShowCustomAmount = settings.ShowCustomAmount,
|
||||||
Currency = settings.Currency,
|
Currency = settings.Currency,
|
||||||
Template = settings.Template,
|
Template = settings.Template,
|
||||||
ButtonText = settings.ButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
|
ButtonText = settings.ButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
|
||||||
CustomButtonText = settings.CustomButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
|
CustomButtonText = settings.CustomButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
|
||||||
|
CustomTipText = settings.CustomTipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
|
||||||
CustomCSSLink = settings.CustomCSSLink
|
CustomCSSLink = settings.CustomCSSLink
|
||||||
};
|
};
|
||||||
if (HttpContext?.Request != null)
|
if (HttpContext?.Request != null)
|
||||||
@@ -144,11 +150,13 @@ namespace BTCPayServer.Controllers
|
|||||||
app.SetSettings(new PointOfSaleSettings()
|
app.SetSettings(new PointOfSaleSettings()
|
||||||
{
|
{
|
||||||
Title = vm.Title,
|
Title = vm.Title,
|
||||||
|
EnableShoppingCart = vm.EnableShoppingCart,
|
||||||
ShowCustomAmount = vm.ShowCustomAmount,
|
ShowCustomAmount = vm.ShowCustomAmount,
|
||||||
Currency = vm.Currency.ToUpperInvariant(),
|
Currency = vm.Currency.ToUpperInvariant(),
|
||||||
Template = vm.Template,
|
Template = vm.Template,
|
||||||
ButtonText = vm.ButtonText,
|
ButtonText = vm.ButtonText,
|
||||||
CustomButtonText = vm.CustomButtonText,
|
CustomButtonText = vm.CustomButtonText,
|
||||||
|
CustomTipText = vm.CustomTipText,
|
||||||
CustomCSSLink = vm.CustomCSSLink
|
CustomCSSLink = vm.CustomCSSLink
|
||||||
});
|
});
|
||||||
await UpdateAppSettings(app);
|
await UpdateAppSettings(app);
|
||||||
|
|||||||
@@ -45,11 +45,13 @@ namespace BTCPayServer.Controllers
|
|||||||
{
|
{
|
||||||
Title = settings.Title,
|
Title = settings.Title,
|
||||||
Step = step.ToString(CultureInfo.InvariantCulture),
|
Step = step.ToString(CultureInfo.InvariantCulture),
|
||||||
|
EnableShoppingCart = settings.EnableShoppingCart,
|
||||||
ShowCustomAmount = settings.ShowCustomAmount,
|
ShowCustomAmount = settings.ShowCustomAmount,
|
||||||
CurrencySymbol = currency.Symbol,
|
CurrencySymbol = currency.Symbol,
|
||||||
Items = _AppsHelper.Parse(settings.Template, settings.Currency),
|
Items = _AppsHelper.Parse(settings.Template, settings.Currency),
|
||||||
ButtonText = settings.ButtonText,
|
ButtonText = settings.ButtonText,
|
||||||
CustomButtonText = settings.CustomButtonText,
|
CustomButtonText = settings.CustomButtonText,
|
||||||
|
CustomTipText = settings.CustomTipText,
|
||||||
CustomCSSLink = settings.CustomCSSLink
|
CustomCSSLink = settings.CustomCSSLink
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -74,6 +76,10 @@ namespace BTCPayServer.Controllers
|
|||||||
if (app == null)
|
if (app == null)
|
||||||
return NotFound();
|
return NotFound();
|
||||||
var settings = app.GetSettings<PointOfSaleSettings>();
|
var settings = app.GetSettings<PointOfSaleSettings>();
|
||||||
|
if (string.IsNullOrEmpty(choiceKey) && !settings.EnableShoppingCart)
|
||||||
|
{
|
||||||
|
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
|
||||||
|
}
|
||||||
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount)
|
if (string.IsNullOrEmpty(choiceKey) && !settings.ShowCustomAmount)
|
||||||
{
|
{
|
||||||
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
|
return RedirectToAction(nameof(ViewPointOfSale), new { appId = appId });
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ namespace BTCPayServer.Models.AppViewModels
|
|||||||
[MaxLength(5000)]
|
[MaxLength(5000)]
|
||||||
public string Template { get; set; }
|
public string Template { get; set; }
|
||||||
|
|
||||||
|
[Display(Name = "Enable shopping cart")]
|
||||||
|
public bool EnableShoppingCart { get; set; }
|
||||||
[Display(Name = "User can input custom amount")]
|
[Display(Name = "User can input custom amount")]
|
||||||
public bool ShowCustomAmount { get; set; }
|
public bool ShowCustomAmount { get; set; }
|
||||||
public string Example1 { get; internal set; }
|
public string Example1 { get; internal set; }
|
||||||
@@ -32,6 +34,10 @@ namespace BTCPayServer.Models.AppViewModels
|
|||||||
[MaxLength(30)]
|
[MaxLength(30)]
|
||||||
[Display(Name = "Text to display on buttons next to the input allowing the user to enter a custom amount")]
|
[Display(Name = "Text to display on buttons next to the input allowing the user to enter a custom amount")]
|
||||||
public string CustomButtonText { get; set; }
|
public string CustomButtonText { get; set; }
|
||||||
|
[Required]
|
||||||
|
[MaxLength(30)]
|
||||||
|
[Display(Name = "Do you want to leave a tip?")]
|
||||||
|
public string CustomTipText { get; set; }
|
||||||
|
|
||||||
[MaxLength(500)]
|
[MaxLength(500)]
|
||||||
[Display(Name = "Custom bootstrap CSS file")]
|
[Display(Name = "Custom bootstrap CSS file")]
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ namespace BTCPayServer.Models.AppViewModels
|
|||||||
public bool Custom { get; set; }
|
public bool Custom { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool EnableShoppingCart { get; set; }
|
||||||
public bool ShowCustomAmount { get; set; }
|
public bool ShowCustomAmount { get; set; }
|
||||||
public string Step { get; set; }
|
public string Step { get; set; }
|
||||||
public string Title { get; set; }
|
public string Title { get; set; }
|
||||||
@@ -30,6 +31,7 @@ namespace BTCPayServer.Models.AppViewModels
|
|||||||
|
|
||||||
public string ButtonText { get; set; }
|
public string ButtonText { get; set; }
|
||||||
public string CustomButtonText { get; set; }
|
public string CustomButtonText { get; set; }
|
||||||
|
public string CustomTipText { get; set; }
|
||||||
|
|
||||||
public string CustomCSSLink { get; set; }
|
public string CustomCSSLink { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,10 @@
|
|||||||
<input asp-for="Currency" class="form-control" />
|
<input asp-for="Currency" class="form-control" />
|
||||||
<span asp-validation-for="Currency" class="text-danger"></span>
|
<span asp-validation-for="Currency" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="EnableShoppingCart"></label>
|
||||||
|
<input asp-for="EnableShoppingCart" type="checkbox" class="form-check" />
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="ShowCustomAmount"></label>
|
<label asp-for="ShowCustomAmount"></label>
|
||||||
<input asp-for="ShowCustomAmount" type="checkbox" class="form-check" />
|
<input asp-for="ShowCustomAmount" type="checkbox" class="form-check" />
|
||||||
@@ -43,6 +47,11 @@
|
|||||||
<input asp-for="CustomButtonText" class="form-control" />
|
<input asp-for="CustomButtonText" class="form-control" />
|
||||||
<span asp-validation-for="CustomButtonText" class="text-danger"></span>
|
<span asp-validation-for="CustomButtonText" class="text-danger"></span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label asp-for="CustomTipText" class="control-label"></label>*
|
||||||
|
<input asp-for="CustomTipText" class="form-control" />
|
||||||
|
<span asp-validation-for="CustomTipText" class="text-danger"></span>
|
||||||
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label asp-for="CustomCSSLink" class="control-label"></label>
|
<label asp-for="CustomCSSLink" class="control-label"></label>
|
||||||
<a href="https://docs.btcpayserver.org/development/theme#bootstrap-themes" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
|
<a href="https://docs.btcpayserver.org/development/theme#bootstrap-themes" target="_blank"><span class="fa fa-question-circle-o" title="More information..."></span></a>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
|
@addTagHelper *, Meziantou.AspNetCore.BundleTagHelpers
|
||||||
|
@inject BTCPayServer.HostedServices.CssThemeManager themeManager
|
||||||
|
|
||||||
@model BTCPayServer.Models.AppViewModels.ViewPointOfSaleViewModel
|
@model BTCPayServer.Models.AppViewModels.ViewPointOfSaleViewModel
|
||||||
@{
|
@{
|
||||||
@@ -18,11 +19,59 @@
|
|||||||
{
|
{
|
||||||
<link href="@Model.CustomCSSLink" rel="stylesheet" />
|
<link href="@Model.CustomCSSLink" rel="stylesheet" />
|
||||||
}
|
}
|
||||||
|
<link href="~/vendor/font-awesome/css/font-awesome.min.css" rel="stylesheet" />
|
||||||
|
|
||||||
|
@if (Model.EnableShoppingCart)
|
||||||
|
{
|
||||||
|
<script type="text/javascript">
|
||||||
|
var srvModel = @Html.Raw(Json.Serialize(Model));
|
||||||
|
</script>
|
||||||
|
<bundle name="wwwroot/bundles/cart-bundle.min.js" />
|
||||||
|
}
|
||||||
</head>
|
</head>
|
||||||
<body class="h-100">
|
<body class="h-100">
|
||||||
|
@if (Model.EnableShoppingCart)
|
||||||
|
{
|
||||||
|
<div id="cartModal" class="modal" tabindex="-1" role="dialog">
|
||||||
|
<div class="modal-dialog modal-lg" role="document">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title">Shopping cart</h5>
|
||||||
|
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<table id="js-cart-list" class="table mt-2 mb-3">
|
||||||
|
<thead class="thead-dark">
|
||||||
|
<tr>
|
||||||
|
<th colspan="2">Product</th>
|
||||||
|
<th class="text-right" width="80">Quantity</th>
|
||||||
|
<th class="text-right" width="25%">Price</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||||
|
<form method="post" asp-antiforgery="false" data-buy>
|
||||||
|
<input id="js-cart-amount" class="form-control" type="hidden" name="amount">
|
||||||
|
<button id="js-cart-pay" class="btn btn-primary" type="submit"><b>@Model.CustomButtonText</b></button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div class="container d-flex h-100">
|
<div class="container d-flex h-100">
|
||||||
<div class="justify-content-center align-self-center text-center mx-auto px-2 py-3 w-100" style="margin: auto;">
|
<div class="justify-content-center align-self-center text-center mx-auto px-2 py-3 w-100" style="margin: auto;">
|
||||||
<h1 class="mb-4">@Model.Title</h1>
|
<h1 class="mb-4">@Model.Title</h1>
|
||||||
|
@if (Model.EnableShoppingCart)
|
||||||
|
{
|
||||||
|
<a id="js-cart" class="btn btn-warning text-white text-right" href="#" data-toggle="modal" data-target="#cartModal"><i class="fa fa-shopping-basket"></i> <span class="badge badge-light badge-pill"><span id="js-cart-items">0</span></span></a>
|
||||||
|
}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@for (int i = 0; i < Model.Items.Length; i++)
|
@for (int i = 0; i < Model.Items.Length; i++)
|
||||||
{
|
{
|
||||||
@@ -31,7 +80,7 @@
|
|||||||
var image = item.Image;
|
var image = item.Image;
|
||||||
var description = item.Description;
|
var description = item.Description;
|
||||||
<div class="@className my-3 px-2">
|
<div class="@className my-3 px-2">
|
||||||
<div class="card">
|
<div class="card" data-id="@i">
|
||||||
@if (!String.IsNullOrWhiteSpace(image))
|
@if (!String.IsNullOrWhiteSpace(image))
|
||||||
{
|
{
|
||||||
<img class="card-img-top" src="@image" alt="Card image cap">
|
<img class="card-img-top" src="@image" alt="Card image cap">
|
||||||
@@ -42,7 +91,7 @@
|
|||||||
{
|
{
|
||||||
<p class="card-text">@description</p>
|
<p class="card-text">@description</p>
|
||||||
}
|
}
|
||||||
@if (item.Custom)
|
@if (item.Custom && !Model.EnableShoppingCart)
|
||||||
{
|
{
|
||||||
<form method="post" asp-antiforgery="false" data-buy>
|
<form method="post" asp-antiforgery="false" data-buy>
|
||||||
<input type="hidden" name="choicekey" value="@item.Id" />
|
<input type="hidden" name="choicekey" value="@item.Id" />
|
||||||
@@ -61,7 +110,7 @@
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
<form method="post" asp-antiforgery="false">
|
<form method="post" asp-antiforgery="false">
|
||||||
<button type="submit" name="choiceKey" class="btn btn-primary" value="@item.Id">
|
<button type="submit" name="choiceKey" class="js-add-cart btn btn-primary" value="@item.Id">
|
||||||
@String.Format(Model.ButtonText, @item.Price.Formatted)</button>
|
@String.Format(Model.ButtonText, @item.Price.Formatted)</button>
|
||||||
</form>
|
</form>
|
||||||
}
|
}
|
||||||
@@ -94,7 +143,5 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script src="~/vendor/jquery/jquery.js"></script>
|
|
||||||
<script src="~/vendor/bootstrap4/js/bootstrap.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -52,5 +52,14 @@
|
|||||||
"wwwroot/vendor/vex/js/vex.combined.min.js",
|
"wwwroot/vendor/vex/js/vex.combined.min.js",
|
||||||
"wwwroot/checkout/**/*.js"
|
"wwwroot/checkout/**/*.js"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"outputFileName": "wwwroot/bundles/cart-bundle.min.js",
|
||||||
|
"inputFiles": [
|
||||||
|
"wwwroot/vendor/jquery/jquery.js",
|
||||||
|
"wwwroot/vendor/bootstrap4/js/bootstrap.js",
|
||||||
|
"wwwroot/cart/js/cart.js",
|
||||||
|
"wwwroot/cart/js/cart.jquery.js"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
57
BTCPayServer/wwwroot/cart/js/cart.jquery.js
Normal file
57
BTCPayServer/wwwroot/cart/js/cart.jquery.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
$.fn.addAnimate = function(completeCallback) {
|
||||||
|
var documentHeight = $(document).height(),
|
||||||
|
itemPos = $(this).offset(),
|
||||||
|
itemY = itemPos.top,
|
||||||
|
cartPos = $('#js-cart').find('.badge').position();
|
||||||
|
tempItem = '<span id="js-cart-temp-item" class="badge badge-primary text-white badge-pill " style="' +
|
||||||
|
'position: absolute;' +
|
||||||
|
'top: ' + itemPos.top + 'px;' +
|
||||||
|
'left: ' + (itemPos.left + 50) + 'px;">'+
|
||||||
|
'<i class="fa fa-shopping-basket"></i></span>';
|
||||||
|
|
||||||
|
// Make animation speed look constant regardless of how far the object is from the cart
|
||||||
|
var animationSpeed = (Math.log(itemY) * (documentHeight / Math.log2(documentHeight - itemY))) / 2;
|
||||||
|
|
||||||
|
// Add the cart item badge and animate it
|
||||||
|
$('body').after(tempItem);
|
||||||
|
$('#js-cart-temp-item').animate({
|
||||||
|
easing: 'swing',
|
||||||
|
top: cartPos.top,
|
||||||
|
left: cartPos.left
|
||||||
|
}, animationSpeed, function() {
|
||||||
|
$(this).remove();
|
||||||
|
completeCallback && completeCallback();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
$(document).ready(function(){
|
||||||
|
var cart = new Cart();
|
||||||
|
|
||||||
|
$('.js-add-cart').click(function(event){
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
var $btn = $(event.target),
|
||||||
|
id = $btn.closest('.card').data('id'),
|
||||||
|
item = srvModel.items[id];
|
||||||
|
|
||||||
|
// Animate adding and then add then save
|
||||||
|
$(this).addAnimate(function(){
|
||||||
|
cart.addItem({
|
||||||
|
id: id,
|
||||||
|
title: item.title,
|
||||||
|
price: item.price,
|
||||||
|
image: typeof item.image != 'underfined' ? item.image : null
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Destroy the cart when the "pay button is clicked"
|
||||||
|
$('#js-cart-pay').click(function(){
|
||||||
|
cart.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Repopulate cart items in the modal when it opens
|
||||||
|
$('#cartModal').on('show.bs.modal', function () {
|
||||||
|
cart.listItems();
|
||||||
|
});
|
||||||
|
});
|
||||||
282
BTCPayServer/wwwroot/cart/js/cart.js
Normal file
282
BTCPayServer/wwwroot/cart/js/cart.js
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
function Cart() {
|
||||||
|
this.items = 0;
|
||||||
|
this.totalAmount = 0;
|
||||||
|
this.content = [];
|
||||||
|
this.tip = 0;
|
||||||
|
|
||||||
|
this.loadLocalStorage();
|
||||||
|
this.itemsCount();
|
||||||
|
this.listItems();
|
||||||
|
this.updateAmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart.prototype.addItem = function(item) {
|
||||||
|
// Increment the existing item count
|
||||||
|
var result = this.content.filter(function(obj){
|
||||||
|
if (obj.id === item.id){
|
||||||
|
obj.count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj.id === item.id
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add new item because it doesn't exist yet
|
||||||
|
if (!result.length) {
|
||||||
|
this.content.push({id: item.id, title: item.title, price: item.price, count: 1, image: item.image})
|
||||||
|
}
|
||||||
|
|
||||||
|
this.items++;
|
||||||
|
this.saveLocalStorage();
|
||||||
|
this.itemsCount();
|
||||||
|
this.updateTotal();
|
||||||
|
this.updateAmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart.prototype.decrementItem = function(id) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
// Decrement the existing item count
|
||||||
|
this.content.filter(function(obj, index, arr){
|
||||||
|
if (obj.id === id)
|
||||||
|
{
|
||||||
|
obj.count--;
|
||||||
|
|
||||||
|
// It's the last item with the same ID, remove it
|
||||||
|
if (obj.count === 0) {
|
||||||
|
self.removeItem(id, index, arr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.items--;
|
||||||
|
this.saveLocalStorage();
|
||||||
|
this.itemsCount();
|
||||||
|
this.updateTotal();
|
||||||
|
this.updateAmount();
|
||||||
|
|
||||||
|
if (this.items === 0) {
|
||||||
|
this.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart.prototype.removeItemAll = function(id) {
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
this.content.filter(function(obj, index, arr){
|
||||||
|
if (obj.id === id)
|
||||||
|
{
|
||||||
|
self.removeItem(id, index, arr);
|
||||||
|
|
||||||
|
for (var i = 0; i < obj.count; i++) {
|
||||||
|
self.items--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.saveLocalStorage();
|
||||||
|
this.itemsCount();
|
||||||
|
this.updateTotal();
|
||||||
|
this.updateAmount();
|
||||||
|
|
||||||
|
if (this.items === 0) {
|
||||||
|
this.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart.prototype.removeItem = function(id, index, arr) {
|
||||||
|
// Remove from the array
|
||||||
|
arr.splice(index, 1);
|
||||||
|
// Remove from the DOM
|
||||||
|
$('#js-cart-list').find('tr').eq(index+1).remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart.prototype.setTip = function(tip) {
|
||||||
|
return this.tip = tip;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart.prototype.itemsCount = function() {
|
||||||
|
$('#js-cart-items').text(this.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart.prototype.getTotal = function(plain) {
|
||||||
|
this.totalAmount = 0;
|
||||||
|
|
||||||
|
// Always calculate the total amount based on the cart content
|
||||||
|
for (var key in this.content) {
|
||||||
|
if (this.content.hasOwnProperty(key) && typeof this.content[key] != 'undefined') {
|
||||||
|
var price = this.toCents(this.content[key].price.value);
|
||||||
|
this.totalAmount += (this.content[key].count * price);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.totalAmount += this.toCents(this.tip);
|
||||||
|
|
||||||
|
return this.fromCents(this.totalAmount);
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart.prototype.updateTotal = function() {
|
||||||
|
$('#js-cart-total').text(this.formatCurrency(this.getTotal()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart.prototype.updateAmount = function() {
|
||||||
|
$('#js-cart-amount').val(this.getTotal());
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart.prototype.escape = function(input) {
|
||||||
|
return ('' + input) /* Forces the conversion to string. */
|
||||||
|
.replace(/&/g, '&') /* This MUST be the 1st replacement. */
|
||||||
|
.replace(/'/g, ''') /* The 4 other predefined entities, required. */
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart.prototype.listItems = function() {
|
||||||
|
var $table = $('#js-cart-list').find('tbody'),
|
||||||
|
self = this,
|
||||||
|
list = []
|
||||||
|
tableTemplate = '';
|
||||||
|
|
||||||
|
if (this.content.length > 0) {
|
||||||
|
// Prepare the list of items in the cart
|
||||||
|
for (var key in this.content) {
|
||||||
|
var item = this.content[key],
|
||||||
|
id = this.escape(item.id),
|
||||||
|
title = this.escape(item.title),
|
||||||
|
image = this.escape(item.image),
|
||||||
|
count = this.escape(item.count),
|
||||||
|
price = this.escape(item.price.formatted),
|
||||||
|
currencySymbol = this.escape(srvModel.currencySymbol),
|
||||||
|
step = this.escape(srvModel.step),
|
||||||
|
customTipText = this.escape(srvModel.customTipText);
|
||||||
|
|
||||||
|
tableTemplate = '<tr data-id="' + id + '">' +
|
||||||
|
(image !== null ? '<td class="align-middle pr-0" width="60"><img src="' + image + '" width="100%"></td>' : '') +
|
||||||
|
'<td class="align-middle pr-0"><b>' + title + '</b></td>' +
|
||||||
|
'<td class="align-middle pr-0" align="right"><div class="input-group">' +
|
||||||
|
' <input class="js-cart-item-count form-control form-control-sm pull-left" type="number" min="0" step="1" name="count" placeholder="Qty" value="' + count + '" data-prev="' + count + '">' +
|
||||||
|
' <div class="input-group-append"><a class="js-cart-item-remove btn btn-danger btn-sm" href="#"><i class="fa fa-remove"></i></a></div>' +
|
||||||
|
'</div></td>' +
|
||||||
|
'<td class="align-middle" align="right">' + price + '</td>' +
|
||||||
|
'</tr>';
|
||||||
|
list.push($(tableTemplate));
|
||||||
|
}
|
||||||
|
|
||||||
|
tableTemplate = '<tr><td colspan="4"><div class="row"><div class="col-sm-8 py-2">' + customTipText + '</div><div class="col-sm-4">' +
|
||||||
|
'<div class="input-group">' +
|
||||||
|
'<div class="input-group-prepend">' +
|
||||||
|
'<span class="input-group-text">' + currencySymbol + '</span>' +
|
||||||
|
'</div>' +
|
||||||
|
'<input class="js-cart-tip form-control" type="number" min="0" step="' + step + '" name="tip" placeholder="Amount">' +
|
||||||
|
'</div>' +
|
||||||
|
'</div></div></td></tr>';
|
||||||
|
list.push($(tableTemplate));
|
||||||
|
|
||||||
|
tableTemplate = '<tr class="bg-light h4"><td colspan="2">Total</td><td colspan="2" align="right"><span id="js-cart-total">' + this.formatCurrency(this.getTotal()) + '</span></td></tr>';
|
||||||
|
list.push($(tableTemplate));
|
||||||
|
|
||||||
|
// Add the list to DOM
|
||||||
|
$table.html(list);
|
||||||
|
|
||||||
|
// Update the cart when number of items is changed
|
||||||
|
$('.js-cart-item-count').off().on('input', function(event){
|
||||||
|
var _this = this,
|
||||||
|
id = $(this).closest('tr').data('id'),
|
||||||
|
count = parseInt($(this).val()),
|
||||||
|
prevCount = parseInt($(this).data('prev')),
|
||||||
|
increased = count > prevCount;
|
||||||
|
|
||||||
|
// User hasn't inputed any number so stop here
|
||||||
|
if (isNaN(count)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$(this).data('prev', count);
|
||||||
|
|
||||||
|
var item = self.content.filter(function(obj){
|
||||||
|
return obj.id === id
|
||||||
|
});
|
||||||
|
|
||||||
|
// Must be in the loop because user may change the count manually by more than 1
|
||||||
|
for (var i = 0; i < Math.abs(count - prevCount); i++) {
|
||||||
|
if (increased) {
|
||||||
|
self.addItem({
|
||||||
|
id: id,
|
||||||
|
title: item.title,
|
||||||
|
price: item.price,
|
||||||
|
image: typeof item.image != 'underfined' ? item.image : null
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
self.decrementItem(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Remove item from the cart
|
||||||
|
$('.js-cart-item-remove').off().on('click', function(event){
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
var id = $(this).closest('tr').data('id');
|
||||||
|
|
||||||
|
self.removeItemAll(id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Change total when tip is changed
|
||||||
|
$('.js-cart-tip').off().on('input', function(event){
|
||||||
|
self.setTip($(this).val());
|
||||||
|
self.updateTotal();
|
||||||
|
self.updateAmount();
|
||||||
|
});
|
||||||
|
} else { // No item in the cart
|
||||||
|
self.emptyList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart.prototype.emptyList = function() {
|
||||||
|
var $table = $('#js-cart-list').find('tbody');
|
||||||
|
|
||||||
|
$table.html('<tr><td colspan="4">The cart is empty.</td></tr>');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Get the currency symbol from an existing amount and use it with the new amount*/
|
||||||
|
Cart.prototype.formatCurrency = function(amount, example) {
|
||||||
|
var regex = /([0-9.]+)/gm;
|
||||||
|
|
||||||
|
// Get the first item's formated price
|
||||||
|
if (typeof example == 'undefined' && typeof srvModel != 'undefined') {
|
||||||
|
example = srvModel.items[0].price.formatted;
|
||||||
|
}
|
||||||
|
|
||||||
|
return example.replace(regex, amount.toFixed(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart.prototype.toCents = function(num) {
|
||||||
|
return num * 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart.prototype.fromCents = function(num) {
|
||||||
|
return num / 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart.prototype.saveLocalStorage = function() {
|
||||||
|
localStorage.setItem('cart', JSON.stringify(this.content));
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart.prototype.loadLocalStorage = function() {
|
||||||
|
this.content = $.parseJSON(localStorage.getItem('cart')) || [];
|
||||||
|
|
||||||
|
// Get number of cart items
|
||||||
|
for (var key in this.content) {
|
||||||
|
if (this.content.hasOwnProperty(key) && typeof this.content[key] != 'undefined' && this.content[key] != null) {
|
||||||
|
this.items += this.content[key].count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Cart.prototype.destroy = function() {
|
||||||
|
localStorage.removeItem('cart');
|
||||||
|
this.content = [];
|
||||||
|
this.items = 0;
|
||||||
|
this.totalAmount = 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user