New PoS design

This commit is contained in:
Mario Dian 2018-12-13 21:36:19 +08:00
parent 8c5b00b1a3
commit c7e90cd7df
20 changed files with 1004 additions and 234 deletions

View file

@ -1429,6 +1429,7 @@ namespace BTCPayServer.Tests
vmpos.ButtonText = "{0} Purchase";
vmpos.CustomButtonText = "Nicolas Sexy Hair";
vmpos.CustomTipText = "Wanna tip?";
vmpos.CustomTipPercentages = "15,18,20";
vmpos.Template = @"
apple:
price: 5.0
@ -1454,6 +1455,7 @@ donation:
Assert.Equal("{0} Purchase", vmview.ButtonText);
Assert.Equal("Nicolas Sexy Hair", vmview.CustomButtonText);
Assert.Equal("Wanna tip?", vmview.CustomTipText);
Assert.Equal("15,18,20", vmview.CustomTipPercentages);
Assert.IsType<RedirectToActionResult>(publicApps.ViewPointOfSale(appId, 0, null, null, null, null, "orange").Result);
//

View file

@ -65,6 +65,9 @@ namespace BTCPayServer.Controllers
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 const string CUSTOM_TIP_PERCENTAGES = "15,18,20";
public string CustomTipPercentages { get; set; } = CUSTOM_TIP_PERCENTAGES;
public string CustomCSSLink { get; set; }
}
@ -87,6 +90,7 @@ namespace BTCPayServer.Controllers
ButtonText = settings.ButtonText ?? PointOfSaleSettings.BUTTON_TEXT_DEF,
CustomButtonText = settings.CustomButtonText ?? PointOfSaleSettings.CUSTOM_BUTTON_TEXT_DEF,
CustomTipText = settings.CustomTipText ?? PointOfSaleSettings.CUSTOM_TIP_TEXT_DEF,
CustomTipPercentages = settings.CustomTipPercentages,
CustomCSSLink = settings.CustomCSSLink
};
if (HttpContext?.Request != null)
@ -157,6 +161,7 @@ namespace BTCPayServer.Controllers
ButtonText = vm.ButtonText,
CustomButtonText = vm.CustomButtonText,
CustomTipText = vm.CustomTipText,
CustomTipPercentages = vm.CustomTipPercentages,
CustomCSSLink = vm.CustomCSSLink
});
await UpdateAppSettings(app);

View file

@ -65,6 +65,7 @@ namespace BTCPayServer.Controllers
ButtonText = settings.ButtonText,
CustomButtonText = settings.CustomButtonText,
CustomTipText = settings.CustomTipText,
CustomTipPercentages = settings.CustomTipPercentages,
CustomCSSLink = settings.CustomCSSLink
});
}

View file

@ -36,8 +36,11 @@ namespace BTCPayServer.Models.AppViewModels
public string CustomButtonText { get; set; }
[Required]
[MaxLength(30)]
[Display(Name = "Do you want to leave a tip?")]
[Display(Name = "Text to display in the tip input")]
public string CustomTipText { get; set; }
[MaxLength(30)]
[Display(Name = "Tip percentage amounts (comma separated)")]
public string CustomTipPercentages { get; set; }
[MaxLength(500)]
[Display(Name = "Custom bootstrap CSS file")]

View file

@ -45,6 +45,7 @@ namespace BTCPayServer.Models.AppViewModels
public string ButtonText { get; set; }
public string CustomButtonText { get; set; }
public string CustomTipText { get; set; }
public string CustomTipPercentages { get; set; }
public string CustomCSSLink { get; set; }
}

View file

@ -72,6 +72,11 @@
<input asp-for="CustomTipText" class="form-control" />
<span asp-validation-for="CustomTipText" class="text-danger"></span>
</div>
<div class="form-group">
<label asp-for="CustomTipPercentages" class="control-label"></label>
<input asp-for="CustomTipPercentages" class="form-control" />
<span asp-validation-for="CustomTipPercentages" class="text-danger"></span>
</div>
<div class="form-group">
<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>

View file

@ -5,6 +5,7 @@
@{
ViewData["Title"] = Model.Title;
Layout = null;
String[] tipPercentages = Model.CustomTipPercentages != null ? Model.CustomTipPercentages.Split(',') : null;
}
<!DOCTYPE html>
@ -14,6 +15,11 @@
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="apple-mobile-web-app-capable" content="yes">
<link rel="apple-touch-icon" href="~/img/icons/icon-512x512.png">
<link rel="apple-touch-startup-image" href="~/img/splash.png">
<link rel="manifest" href="~/manifest.json">
<link href="@this.Context.Request.GetAbsoluteUri(themeManager.BootstrapUri)" rel="stylesheet" />
@if (Model.CustomCSSLink != null)
{
@ -23,125 +29,332 @@
@if (Model.EnableShoppingCart)
{
<link rel="stylesheet" href="~/cart/css/style.css">
<script type="text/javascript">
var srvModel = @Html.Raw(Json.Serialize(Model));
</script>
<bundle name="wwwroot/bundles/cart-bundle.min.js" />
}
</head>
<script id="template-cart-item" type="text/template">
<tr data-id="{id}">
{image}
<td class="align-middle pr-0 pl-2" width="100%"><b>{title}</b></td>
<td class="align-middle px-0" align="right">
<a class="js-cart-item-remove btn btn-link" href="#"><i class="fa fa-trash text-muted"></i></a>
</td>
<td class="align-middle pr-0" align="right">
<div class="input-group">
<div class="input-group-prepend">
<a class="js-cart-item-minus btn btn-link px-2" href="#"><i class="fa fa-minus-circle fa-fw text-danger"></i></a>
</div>
<input class="js-cart-item-count form-control form-control-sm pull-left" type="text"name="count" placeholder="Qty" value="{count}" data-prev="{count}">
<div class="input-group-append"><a class="js-cart-item-plus btn btn-link px-2" href="#">
<i class="fa fa-plus-circle fa-fw text-success"></i></a>
</div>
</div>
</td>
<td class="align-middle text-nowrap" align="right">{price}</td>
</tr>
</script>
<script id="template-cart-item-image" type="text/template">
<td class="align-middle pr-0" width="50"><img src="{image}" width="50"></td>
</script>
<script id="template-cart-custom-amount" type="text/template">
<tr>
<td colspan="5">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-shopping-cart fa-fw"></i></span>
</div>
<input class="js-cart-custom-amount form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="Pay what you want">
<div class="input-group-append">
<a class="js-cart-custom-amount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
</div>
</div>
</td>
</tr>
</script>
<script id="template-cart-extra" type="text/template">
<tr>
<td colspan="5" class="border-0 pb-0">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-shopping-cart fa-fw"></i></span>
</div>
<input class="js-cart-custom-amount form-control" type="number" min="0" step="@Model.Step" name="amount" value="{customAmount}" placeholder="Pay what you want">
<div class="input-group-append">
<a class="js-cart-custom-amount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
</div>
</div>
</td>
</tr>
<tr>
<td colspan="5" class="border-top-0">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-percent fa-fw"></i></span>
</div>
<input class="js-cart-discount form-control" type="number" min="0" step="@Model.Step" value="{discount}" name="discount" placeholder="Discount in %">
<div class="input-group-append">
<a class="js-cart-discount-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
</div>
</div>
</td>
</tr>
</script>
<script id="template-cart-tip" type="text/template">
<tr class="h5">
<td colspan="5" class="border-top-0 pt-4">@Model.CustomTipText</td>
</tr>
<tr>
<td colspan="5" class="border-0">
<div class="input-group mb-2">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fa fa-money fa-fw"></i></span>
</div>
<input class="js-cart-tip form-control" type="number" min="0" step="@Model.Step" value="{tip}" name="tip" placeholder="Tip in @(Model.CurrencyInfo.CurrencySymbol != null ? Model.CurrencyInfo.CurrencySymbol : Model.CurrencyCode)">
<div class="input-group-append">
<a class="js-cart-tip-remove btn btn-danger" href="#"><i class="fa fa-times"></i></a>
</div>
</div>
<div class="row mb-1">
@if (tipPercentages != null && tipPercentages.Length > 0) {
@for (int i = 0; i < tipPercentages.Length; i++) {
int percentage;
@if(int.TryParse(tipPercentages[i], out percentage)) {
<div class="col">
<a class="js-cart-tip-btn btn btn-light btn-block border mb-2" href="#" data-tip="@percentage">@percentage%</a>
</div>
}
}
}
</div>
</td>
</tr>
</script>
<script id="template-cart-total" type="text/template">
<tr class="h4 table-light">
<td colspan="1" class="pb-4">Total</td>
<td colspan="4" align="right" class="pb-4">
<span class="js-cart-total">{total}</span>
</td>
</tr>
</script>
<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">&times;</span>
</button>
<div id="cartModal" class="modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header bg-primary text-white border-0">
<h5 class="modal-title">Confirmation</h5>
<button type="button" class="close text-white" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true"><i class="fa fa-times fa-fw"></i></span>
</button>
</div>
<div class="modal-body p-0">
<table id="js-cart-summary" class="table m-0">
<tbody class="my-3">
<tr class="h5">
<td colspan="2" class="border-top-0">Summary</td>
</tr>
<tr class="h6">
<td class="border-0 pb-0">Total products</td>
<td align="right" class="border-0 pb-0">
<span class="js-cart-summary-products text-nowrap"></span>
</td>
</tr>
<tr class="h6">
<td class="border-0 pb-y">Discount</td>
<td align="right" class="border-0 pb-y">
<span class="js-cart-summary-discount text-nowrap"></span>
</td>
</tr>
<tr class="h6">
<td class="border-top-0 pt-0">Tip</td>
<td align="right" class="border-top-0 pt-0">
<span class="js-cart-summary-tip text-nowrap"></span>
</td>
</tr>
<tr class="h3 table-light">
<td>Total</td>
<td align="right">
<span class="js-cart-summary-total text-nowrap"></span>
</td>
</tr>
</tbody>
</table>
</div>
<div class="modal-footer bg-light">
<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 btn-lg" type="submit"><b>@Model.CustomButtonText</b></button>
</form>
</div>
</div>
</div>
<div class="modal-body">
<table id="js-cart-list" class="table mt-2 mb-3">
</div>
<div class="wrapper">
<!-- Page Content -->
<div id="content">
<div class="p-3">
<div class="row">
<div class="col-sm-4 col-lg-3 order-sm-last text-right mb-2">
<a class="js-cart btn btn-warning text-white text-right" href="#"><i class="fa fa-shopping-basket"></i>&nbsp; <span class="badge badge-light badge-pill"><span id="js-cart-items">0</span></span></a>
</div>
<div class="col-sm-8 col-lg-9 mb-2">
<div class="input-group mb-2">
<input type="text" class="js-search form-control" placeholder="Find product">
<a class="js-search-reset btn btn-link text-black" href="#" style="position: absolute;right: 0px; z-index: 9999; display: none;"><i class="fa fa-times-circle fa-lg"></i></a>
</div>
</div>
</div>
</div>
<div id="js-pos-list" class="text-center mx-auto px-4">
<div class="row">
@for (int i = 0; i < Model.Items.Length; i++)
{
var item = Model.Items[i];
var image = item.Image;
var description = item.Description;
<div class="col-sm-6 col-lg-3 my-3 px-2 card-wrapper">
<div class="js-add-cart card" data-id="@i">
@if (!String.IsNullOrWhiteSpace(image))
{
<img class="card-img-top" src="@image" alt="Card image cap">
}
<div class="card-body p-3">
<h6 class="card-title mb-0">@item.Title</h6>
@if (!String.IsNullOrWhiteSpace(description))
{
<p class="card-text">@description</p>
}
<span class="text-muted small">@String.Format(Model.ButtonText, @item.Price.Formatted)</span>
</div>
</div>
</div>
}
</div>
</div>
</div>
<!-- Sidebar -->
<nav id="sidebar" class="bg-dark">
<div class="bg-warning p-3 clearfix">
<h3 class="text-white m-0 pull-left">Cart</h3>
<a class="js-cart btn btn-sm bg-white text-black pull-right ml-5" href="#"><i class="fa fa-times fa-lg"></i></a>
<a class="js-cart-destroy btn btn-sm bg-white text-danger pull-right" href="#" style="display: none;">Empty cart <i class="fa fa-trash fa-fw fa-lg"></i></a>
</div>
<table id="js-cart-list" class="table bg-light mt-0 mb-0">
<thead class="thead-dark">
<tr>
<th colspan="2">Product</th>
<th class="text-right" width="80">Quantity</th>
<th colspan="3" width="75%">Product</th>
<th class="text-center"><div style="width: 100px">Quantity</div></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="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>
@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>&nbsp; <span class="badge badge-light badge-pill"><span id="js-cart-items">0</span></span></a>
}
<div class="row">
@for (int i = 0; i < Model.Items.Length; i++)
{
var className = (Model.Items.Length - i) > (Model.Items.Length % 4) ? "col-sm-6 col-lg-3" : "col-md align-self-start";
var item = Model.Items[i];
var image = item.Image;
var description = item.Description;
<div class="@className my-3 px-2">
<div class="card" data-id="@i">
@if (!String.IsNullOrWhiteSpace(image))
{
<img class="card-img-top" src="@image" alt="Card image cap">
}
<div class="card-body">
<h5 class="card-title">@item.Title</h5>
@if (!String.IsNullOrWhiteSpace(description))
<table id="js-cart-extra" class="table bg-light mt-0 mb-0">
<tbody></tbody>
</table>
<button id="js-cart-confirm" data-toggle="modal" data-target="#cartModal" class="btn btn-primary btn-lg btn-block mb-3 p-3" disabled="disabled" type="submit"><b>Confirm</b></button>
<div class="text-center mb-5 pb-5">
<img src="~/img/logo-white.png" height="40">
</div>
</nav>
</div>
} else {
<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;">
<h1 class="mb-4">@Model.Title</h1>
<div class="row">
@for (int i = 0; i < Model.Items.Length; i++)
{
var className = (Model.Items.Length - i) > (Model.Items.Length % 4) ? "col-sm-6 col-lg-3" : "col-md align-self-start";
var item = Model.Items[i];
var image = item.Image;
var description = item.Description;
<div class="@className my-3 px-2">
<div class="card" data-id="@i">
@if (!String.IsNullOrWhiteSpace(image))
{
<p class="card-text">@description</p>
<img class="card-img-top" src="@image" alt="Card image cap">
}
@if (item.Custom && !Model.EnableShoppingCart)
{
<div class="card-body">
<h5 class="card-title">@item.Title</h5>
@if (!String.IsNullOrWhiteSpace(description))
{
<p class="card-text">@description</p>
}
@if (item.Custom && !Model.EnableShoppingCart)
{
<form method="post" asp-antiforgery="false" data-buy>
<input type="hidden" name="choicekey" value="@item.Id" />
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">@Model.CurrencySymbol</span>
</div>
<input class="form-control" type="number" min="@item.Price.Value" step="@Model.Step" name="amount"
value="@item.Price.Value" placeholder="Amount">
<div class="input-group-append">
<button class="btn btn-primary" type="submit">@Model.CustomButtonText</button>
</div>
</div>
</form>
}
else
{
<form method="post" asp-antiforgery="false">
<button type="submit" name="choiceKey" class="js-add-cart btn btn-primary" value="@item.Id">
@String.Format(Model.ButtonText, @item.Price.Formatted)</button>
</form>
}
</div>
</div>
</div>
}
</div>
@if (Model.ShowCustomAmount)
{
<div class="row mt-2 mb-4">
<div class="col-lg-4 offset-lg-4 col-md-6 offset-md-3 px-2">
<div class="card">
<div class="card-body">
<h5 class="card-title">Custom Amount</h5>
<p class="card-text">Create invoice to pay custom amount</p>
<form method="post" asp-antiforgery="false" data-buy>
<input type="hidden" name="choicekey" value="@item.Id" />
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">@Model.CurrencySymbol</span>
</div>
<input class="form-control" type="number" min="@item.Price.Value" step="@Model.Step" name="amount"
value="@item.Price.Value" placeholder="Amount">
<div class="input-group-append">
<button class="btn btn-primary" type="submit">@Model.CustomButtonText</button>
</div>
<input class="form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="Amount">
<div class="input-group-append"><button class="btn btn-primary" type="submit">@Model.CustomButtonText</button></div>
</div>
</form>
}
else
{
<form method="post" asp-antiforgery="false">
<button type="submit" name="choiceKey" class="js-add-cart btn btn-primary" value="@item.Id">
@String.Format(Model.ButtonText, @item.Price.Formatted)</button>
</form>
}
</div>
</div>
</div>
</div>
}
</div>
@if (Model.ShowCustomAmount)
{
<div class="row mt-2 mb-4">
<div class="col-lg-4 offset-lg-4 col-md-6 offset-md-3 px-2">
<div class="card">
<div class="card-body">
<h5 class="card-title">Custom Amount</h5>
<p class="card-text">Create invoice to pay custom amount</p>
<form method="post" asp-antiforgery="false" data-buy>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text">@Model.CurrencySymbol</span>
</div>
<input class="form-control" type="number" min="0" step="@Model.Step" name="amount" placeholder="Amount">
<div class="input-group-append"><button class="btn btn-primary" type="submit">@Model.CustomButtonText</button></div>
</div>
</form>
</div>
</div>
</div>
</div>
}
</div>
</div>
}
</body>
</html>

View file

@ -0,0 +1,128 @@
.modal-content {
border-radius: 0.5rem;
}
.modal-header {
border-top-left-radius: 0.4rem;
border-top-right-radius: 0.4rem;
}
.card-img-top {
width: 100%;
max-height: 180px;
object-fit: cover;
}
.js-cart-added {
background-color: rgba(0, 0, 0, 0.7);
border-radius: 0.25rem;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
display: none;
}
.js-cart-added .fa {
height: 50px;
position: relative;
top: 50%;
margin-top: -25px;
}
.js-add-cart:hover {
cursor: pointer;
}
#js-cart-confirm {
border-radius: 0;
}
/* ---------------------------------------------------
SIDEBAR STYLE
----------------------------------------------------- */
.wrapper {
display: flex;
width: 100%;
}
#sidebar {
width: 400px;
position: fixed;
top: 0;
right: 0;
height: 100vh;
overflow-x: hidden;
overflow-y: scroll;
z-index: 999;
background: #e1e6ea;
transition: all 0.3s;
-webkit-overflow-scrolling: touch;
}
#sidebar .js-cart {
display: none;
}
#sidebar.active {
margin-right: -400px;
}
/* ---------------------------------------------------
CONTENT STYLE
----------------------------------------------------- */
#content {
width: calc(100% - 400px);
min-height: 100vh;
transition: all 0.3s;
position: absolute;
top: 0;
left: 0;
}
#content.active {
width: 100%;
}
.bg-gray {
background-color: #aaa;
}
.text-black {
color: #000;
}
/* ---------------------------------------------------
MEDIAQUERIES
----------------------------------------------------- */
@media (max-width: 768px) {
#sidebar {
margin-right: -400px;
}
#sidebar .js-cart {
display: inline;
}
#sidebar.active {
margin-right: 0;
}
#content {
width: 100%;
}
#content.active {
width: calc(100% - 400px);
}
#sidebarCollapse span {
display: none;
}
}
@media (max-width: 575px) {
#sidebar {
width: 100%;
margin-right: -575px;
}
#content.active {
width: 100%;
}
}

View file

@ -1,28 +1,48 @@
$.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>';
if ($(this).find('.js-cart-added').length === 0) {
$(this).append('<div class="js-cart-added"><i class="fa fa-check fa-3x text-white align-middle"></i></div>');
// Animate the element
$(this).find('.js-cart-added').fadeIn(200, function(){
var self = this;
// Show it for 200ms
setTimeout(function(){
// Hide and remove
$(self).fadeOut(100, function(){
$(this).remove();
// 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;
completeCallback && completeCallback();
})
}, 200);
});
}
}
// 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();
});
};
function removeAccents(input){
var accents = 'ÀÁÂÃÄÅàáâãäåÒÓÔÕÕÖØòóôõöøÈÉÊËèéêëðÇČçčÐĎďÌÍÎÏìíîïĽľÙÚÛÜùúûüÑŇñňŠšŤťŸÿýŽž ́',
accentsOut = 'AAAAAAaaaaaaOOOOOOOooooooEEEEeeeeeCCccDDdIIIIiiiiLlUUUUuuuuNNnnSsTtYyyZz ',
output = '',
index = -1;
for( var i = 0; i < input.length; i++ ) {
index = accents.indexOf(input[i]);
if( index != -1 ) {
output += typeof accentsOut[index] != 'undefined' ? accentsOut[index] : '';
}
else {
output += typeof input[i] != 'undefined' ? input[i] : '';
}
}
return output;
}
jQuery.expr[':'].icontains = function (a, i, m) {
var string = removeAccents(jQuery(a).text().toLowerCase());
return string.indexOf(removeAccents(m[3].toLowerCase())) >= 0;
};
$(document).ready(function(){
var cart = new Cart();
@ -31,27 +51,86 @@ $(document).ready(function(){
event.preventDefault();
var $btn = $(event.target),
self = this;
id = $btn.closest('.card').data('id'),
item = srvModel.items[id];
item = srvModel.items[id],
items = cart.items;
// Is event catching disabled?
if (!$(this).hasClass('disabled')) {
// Disable catching events for this element
$(this).addClass('disabled');
// Add-to-cart animation only once
$(this).addAnimate(function(){
// Enable the event
$(self).removeClass('disabled');
});
// 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
});
});
cart.listItems();
}
});
// Destroy the cart when the "pay button is clicked"
$('#js-cart-pay').click(function(){
cart.destroy();
cart.destroy(true);
});
// Repopulate cart items in the modal when it opens
$('#cartModal').on('show.bs.modal', function () {
cart.listItems();
$('.js-cart').on('click', function () {
$('#sidebar, #content').toggleClass('active');
$('.collapse.in').toggleClass('in');
$('a[aria-expanded=true]').attr('aria-expanded', 'false');
});
$('.js-search').keyup(function(event){
var str = $(this).val();
$('#js-pos-list').find(".card-wrapper").show();
if (str.length > 1) {
var $list = $('#js-pos-list').find(".card-title:not(:icontains('" + str + "'))");
$list.parents('.card-wrapper').hide();
$('.js-search-reset').show();
}
});
$('.js-search-reset').click(function(event){
event.preventDefault();
$('.js-search').val('');
$('.js-search').trigger('keyup');
$(this).hide();
});
$('#js-cart-summary').find('tbody').prepend(cart.template($('#template-cart-tip'), {
'tip': cart.fromCents(cart.getTip()) || ''
}));
$('#cartModal').one('show.bs.modal', function () {
cart.updateDiscount();
cart.updateTip();
cart.updateSummaryProducts();
cart.updateSummaryTotal();
// Change total when tip is changed
$('.js-cart-tip').inputAmount(cart, 'tip');
// Remove tip
$('.js-cart-tip-remove').removeAmount(cart, 'tip');
$('.js-cart-tip-btn').click(function(event){
event.preventDefault();
var $tip = $('.js-cart-tip'),
discount = cart.percentage(cart.getTotalProducts(), cart.getDiscount());
$tip.val(cart.percentage(cart.getTotalProducts() - discount, parseInt($(this).data('tip'))));
$tip.trigger('input');
});
});
});

View file

@ -2,34 +2,139 @@ function Cart() {
this.items = 0;
this.totalAmount = 0;
this.content = [];
this.tip = 0;
this.loadLocalStorage();
this.itemsCount();
this.buildUI();
this.$list = $('#js-cart-list');
this.$items = $('#js-cart-items');
this.$total = $('.js-cart-total');
this.$summaryProducts = $('.js-cart-summary-products');
this.$summaryDiscount = $('.js-cart-summary-discount');
this.$summaryTotal = $('.js-cart-summary-total');
this.$summaryTip = $('.js-cart-summary-tip');
this.$destroy = $('.js-cart-destroy');
this.$confirm = $('#js-cart-confirm');
this.listItems();
this.bindEmptyCart();
this.updateItemsCount();
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++;
Cart.prototype.setCustomAmount = function(amount) {
this.customAmount = this.toNumber(amount);
if (this.customAmount > 0) {
localStorage.setItem(this.getStorageKey('cartCustomAmount'), this.customAmount);
} else {
localStorage.removeItem(this.getStorageKey('cartCustomAmount'));
}
return this.customAmount;
}
Cart.prototype.getCustomAmount = function() {
return this.toCents(this.customAmount);
}
Cart.prototype.setTip = function(amount) {
this.tip = this.toNumber(amount);
if (this.tip > 0) {
localStorage.setItem(this.getStorageKey('cartTip'), this.tip);
} else {
localStorage.removeItem(this.getStorageKey('cartTip'));
}
return this.tip;
}
Cart.prototype.getTip = function() {
return this.toCents(this.tip);
}
Cart.prototype.setDiscount = function(amount) {
this.discount = this.toNumber(amount);
if (this.discount > 0) {
localStorage.setItem(this.getStorageKey('cartDiscount'), this.discount);
} else {
localStorage.removeItem(this.getStorageKey('cartDiscount'));
}
return this.discount;
}
Cart.prototype.getDiscount = function() {
return this.toCents(this.discount);
}
Cart.prototype.getDiscountAmount = function(amount) {
return this.percentage(amount, this.getDiscount());
}
// Get total amount of products
Cart.prototype.getTotalProducts = function() {
var amount = 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);
amount += (this.content[key].count * price);
}
return obj.id === item.id
});
}
// Add custom amount
amount += this.getCustomAmount();
return amount;
}
// Get absolute total amount
Cart.prototype.getTotal = function(includeTip) {
this.totalAmount = this.getTotalProducts();
if (this.getDiscount() > 0) {
this.totalAmount -= this.getDiscountAmount(this.totalAmount);
}
if (includeTip) {
this.totalAmount += this.getTip();
}
return this.fromCents(this.totalAmount);
}
/*
* Data manipulation
*/
// Add item to the cart or update its count
Cart.prototype.addItem = function(item) {
var id = item.id,
result = this.content.filter(function(obj){
return obj.id === 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.content.push({id: id, title: item.title, price: item.price, count: 0, image: item.image});
this.emptyCartToggle();
}
// Increment item count
this.incrementItem(id);
}
Cart.prototype.incrementItem = function(id) {
// Increment the existing item count
this.content.filter(function(obj){
if (obj.id === id){
obj.count++;
}
});
this.items++;
this.saveLocalStorage();
this.itemsCount();
this.updateTotal();
this.updateAmount();
this.updateAll();
}
Cart.prototype.decrementItem = function(id) {
@ -49,79 +154,102 @@ Cart.prototype.decrementItem = function(id) {
});
this.items--;
this.saveLocalStorage();
this.itemsCount();
this.updateTotal();
this.updateAmount();
if (this.items === 0) {
this.emptyList();
}
this.updateAll();
}
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();
// Remove by item
if (id) {
this.content.filter(function(obj, index, arr){
if (obj.id === id)
{
self.removeItem(id, index, arr);
if (this.items === 0) {
this.emptyList();
for (var i = 0; i < obj.count; i++) {
self.items--;
}
}
});
} else { // Remove all
this.$list.find('tbody').empty();
self.content = [];
self.items = 0;
}
this.emptyCartToggle();
this.updateAll();
}
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();
this.$list.find('tr').eq(index+1).remove();
}
Cart.prototype.setTip = function(tip) {
return this.tip = tip;
/*
* Update DOM
*/
// Update all data elements
Cart.prototype.updateAll = function() {
this.saveLocalStorage();
this.updateItemsCount();
this.updateDiscount();
this.updateSummaryProducts();
this.updateSummaryTotal();
this.updateTotal();
this.updateAmount();
}
Cart.prototype.itemsCount = function() {
$('#js-cart-items').text(this.items);
// Update number of cart items
Cart.prototype.updateItemsCount = function() {
this.$items.text(this.items);
}
Cart.prototype.getTotal = function(plain) {
this.totalAmount = 0;
// Update total products (including the custom amount and discount) in the cart
Cart.prototype.updateTotal = function() {
this.$total.text(this.formatCurrency(this.getTotal()));
}
// 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);
}
// Update total amount in the summary
Cart.prototype.updateSummaryTotal = function() {
this.$summaryTotal.text(this.formatCurrency(this.getTotal(true)));
}
// Update total products amount in the summary
Cart.prototype.updateSummaryProducts = function() {
this.$summaryProducts.text(this.formatCurrency(this.fromCents(this.getTotalProducts())));
}
// Update discount amount in the summary
Cart.prototype.updateDiscount = function(amount) {
var discount = 0;
if (typeof amount != 'undefined') {
discount = amount;
} else {
discount = this.percentage(this.getTotalProducts(), this.getDiscount());
discount = this.fromCents(discount);
}
this.totalAmount += this.toCents(this.tip);
return this.fromCents(this.totalAmount);
this.$summaryDiscount.text((discount > 0 ? '-' : '') + this.formatCurrency(discount));
}
Cart.prototype.updateTotal = function() {
$('#js-cart-total').text(this.formatCurrency(this.getTotal(), srvModel.currencyCode, srvModel.currencySymbol));
// Update tip amount in the summary
Cart.prototype.updateTip = function(amount) {
var tip = typeof amount != 'undefined' ? amount : this.fromCents(this.getTip());
this.$summaryTip.text(this.formatCurrency(tip));
}
// Update hidden total amount value to be sent to the checkout page
Cart.prototype.updateAmount = function() {
$('#js-cart-amount').val(this.getTotal());
$('#js-cart-amount').val(this.getTotal(true));
}
// Escape html characters
Cart.prototype.escape = function(input) {
return ('' + input) /* Forces the conversion to string. */
.replace(/&/g, '&amp;') /* This MUST be the 1st replacement. */
@ -132,8 +260,51 @@ Cart.prototype.escape = function(input) {
;
}
// Load the template
Cart.prototype.template = function($template, obj) {
var template = $template.text();
for (var key in obj) {
var re = new RegExp('{' + key + '}', 'mg');
template = template.replace(re, obj[key]);
}
return template;
}
// Build the cart skeleton
Cart.prototype.buildUI = function() {
var $table = $('#js-cart-extra').find('tbody'),
list = [];
tableTemplate = this.template($('#template-cart-extra'), {
'discount': this.escape(this.fromCents(this.getDiscount()) || ''),
'customAmount': this.escape(this.fromCents(this.getCustomAmount()) || '')
});
list.push($(tableTemplate));
tableTemplate = this.template($('#template-cart-total'), {
'total': this.escape(this.formatCurrency(this.getTotal()))
});
list.push($(tableTemplate));
// Add the list to DOM
$table.append(list);
// Change total when discount is changed
$('.js-cart-discount').inputAmount(this, 'discount');
// Remove discount
$('.js-cart-discount-remove').removeAmount(this, 'discount');
// Change total when discount is changed
$('.js-cart-custom-amount').inputAmount(this, 'customAmount');
// Remove discount
$('.js-cart-custom-amount-remove').removeAmount(this, 'customAmount');
}
// List cart items and bind their events
Cart.prototype.listItems = function() {
var $table = $('#js-cart-list').find('tbody'),
var $table = this.$list.find('tbody'),
self = this,
list = [],
tableTemplate = '';
@ -142,43 +313,23 @@ Cart.prototype.listItems = function() {
// 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),
total = this.escape(this.formatCurrency(this.getTotal(), srvModel.currencyCode, srvModel.currencySymbol)),
step = this.escape(srvModel.step),
tip = this.escape(this.tip || ''),
customTipText = this.escape(srvModel.customTipText);
image = this.escape(item.image);
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>';
tableTemplate = this.template($('#template-cart-item'), {
'id': this.escape(item.id),
'image': image ? this.template($('#template-cart-item-image'), {
'image' : image
}) : '',
'title': this.escape(item.title),
'count': this.escape(item.count),
'price': this.escape(item.price.formatted)
});
list.push($(tableTemplate));
}
tableTemplate = '<tr><td colspan="4"><div class="row"><div class="col-sm-7 py-2">' + customTipText + '</div><div class="col-sm-5">' +
'<div class="input-group">' +
'<div class="input-group-prepend">' +
'<span class="input-group-text"><i class="fa fa-money"></i></span>' +
'</div>' +
'<input class="js-cart-tip form-control" type="number" min="0" step="' + step + '" value="' + tip + '" name="tip" placeholder="Amount">' +
'</div>' +
'</div></div></td></tr>';
list.push($(tableTemplate));
tableTemplate = '<tr class="bg-light h4"><td colspan="1">Total</td><td colspan="3" align="right"><span id="js-cart-total">' + total + '</span></td></tr>';
list.push($(tableTemplate));
// Add the list to DOM
$table.html(list);
list = [];
// Update the cart when number of items is changed
$('.js-cart-item-count').off().on('input', function(event){
@ -218,29 +369,64 @@ Cart.prototype.listItems = function() {
$('.js-cart-item-remove').off().on('click', function(event){
event.preventDefault();
var id = $(this).closest('tr').data('id');
self.removeItemAll(id);
self.removeItemAll($(this).closest('tr').data('id'));
});
// Change total when tip is changed
$('.js-cart-tip').off().on('input', function(event){
self.setTip($(this).val());
self.updateTotal();
self.updateAmount();
// Increment item
$('.js-cart-item-plus').off().on('click', function(event){
event.preventDefault();
var $val = $(this).parents('.input-group').find('.js-cart-item-count');
$val.val(parseInt($val.val()) + 1);
self.incrementItem($(this).closest('tr').data('id'));
});
// Decrement item
$('.js-cart-item-minus').off().on('click', function(event){
event.preventDefault();
var $val = $(this).parents('.input-group').find('.js-cart-item-count'),
id = $(this).closest('tr').data('id'),
val = parseInt($val.val());
if (val === 1) {
self.removeItemAll(id);
} else {
$val.val(val - 1);
self.decrementItem(id);
}
});
} else { // No item in the cart
self.emptyList();
}
}
Cart.prototype.emptyList = function() {
var $table = $('#js-cart-list').find('tbody');
Cart.prototype.bindEmptyCart = function() {
var self = this;
$table.html('<tr><td colspan="4">The cart is empty.</td></tr>');
this.emptyCartToggle();
this.$destroy.click(function(event){
event.preventDefault();
self.destroy();
self.emptyCartToggle();
});
}
Cart.prototype.formatCurrency = function(amount, currency, symbol) {
Cart.prototype.emptyCartToggle = function() {
if (this.content.length > 0 || this.getCustomAmount()) {
this.$destroy.show();
this.$confirm.removeAttr('disabled');
} else {
this.$destroy.hide();
this.$confirm.attr('disabled', 'disabled');
}
}
/*
* Currencies and numbers
*/
Cart.prototype.formatCurrency = function(amount) {
var amt = '',
thousandsSep = '',
decimalSep = ''
@ -252,12 +438,14 @@ Cart.prototype.formatCurrency = function(amount, currency, symbol) {
if (srvModel.currencyInfo.symbolSpace) {
prefix = prefix + ' ';
}
}
else {
postfix = srvModel.currencyInfo.currencySymbol;
if (srvModel.currencyInfo.symbolSpace) {
postfix = ' ' + postfix;
}
}
thousandsSep = srvModel.currencyInfo.thousandSeparator;
decimalSep = srvModel.currencyInfo.decimalSeparator;
@ -267,8 +455,9 @@ Cart.prototype.formatCurrency = function(amount, currency, symbol) {
var splittedAmount = amt.split('.');
amt = (splittedAmount[0] + '.').replace(/(\d)(?=(\d{3})+\.)/g, '$1' + thousandsSep);
amt = amt.substr(0, amt.length - 1);
if(splittedAmount.length == 2)
amt = amt + '.' + splittedAmount[1];
if(splittedAmount.length == 2) {
amt = amt + decimalSep + splittedAmount[1];
}
if (srvModel.currencyInfo.divisibility !== 0) {
amt[amt.length - srvModel.currencyInfo.divisibility - 1] = decimalSep;
}
@ -277,6 +466,10 @@ Cart.prototype.formatCurrency = function(amount, currency, symbol) {
return amt;
}
Cart.prototype.toNumber = function(num) {
return (num * 1) || 0;
}
Cart.prototype.toCents = function(num) {
return num * Math.pow(10, srvModel.currencyInfo.divisibility);
}
@ -285,14 +478,23 @@ Cart.prototype.fromCents = function(num) {
return num / Math.pow(10, srvModel.currencyInfo.divisibility);
}
Cart.prototype.getStorageKey = function () { return ('cart' + srvModel.appId + srvModel.currencyCode); }
Cart.prototype.percentage = function(amount, percentage) {
return this.fromCents((amount / 100) * percentage);
}
/*
* Storage
*/
Cart.prototype.getStorageKey = function (name) {
return (name + srvModel.appId + srvModel.currencyCode);
}
Cart.prototype.saveLocalStorage = function() {
localStorage.setItem(this.getStorageKey(), JSON.stringify(this.content));
localStorage.setItem(this.getStorageKey('cart'), JSON.stringify(this.content));
}
Cart.prototype.loadLocalStorage = function() {
this.content = $.parseJSON(localStorage.getItem(this.getStorageKey())) || [];
this.content = $.parseJSON(localStorage.getItem(this.getStorageKey('cart'))) || [];
// Get number of cart items
for (var key in this.content) {
@ -300,12 +502,93 @@ Cart.prototype.loadLocalStorage = function() {
this.items += this.content[key].count;
}
}
this.discount = localStorage.getItem(this.getStorageKey('cartDiscount'));
this.customAmount = localStorage.getItem(this.getStorageKey('cartCustomAmount'));
this.tip = localStorage.getItem(this.getStorageKey('cartTip'));
}
Cart.prototype.destroy = function() {
localStorage.removeItem(this.getStorageKey());
this.content = [];
this.items = 0;
this.totalAmount = 0;
this.tip = 0;
Cart.prototype.destroy = function(keepAmount) {
this.setTip(0);
this.setDiscount(0);
this.setCustomAmount(0);
// When form is sent
if (keepAmount) {
this.content = [];
this.items = 0;
} else {
this.updateDiscount(0);
this.updateTip(0);
this.removeItemAll();
$('.js-cart-discount').val('');
$('.js-cart-tip').val('');
$('.js-cart-custom-amount').val('');
}
localStorage.removeItem(this.getStorageKey('cart'));
}
/*
* jQuery helpers
*/
$.fn.inputAmount = function(obj, type) {
$(this).off().on('input', function(event){
var val = obj.toNumber($(this).val());
switch (type) {
case 'customAmount':
obj.setCustomAmount(val);
obj.updateDiscount();
obj.updateSummaryProducts();
obj.updateTotal();
break;
case 'discount':
obj.setDiscount(val);
obj.updateDiscount();
obj.updateSummaryProducts();
obj.updateTotal();
break;
case 'tip':
obj.setTip(val);
obj.updateTip();
break;
}
obj.updateSummaryTotal();
obj.updateAmount();
obj.emptyCartToggle();
});
}
$.fn.removeAmount = function(obj, type) {
$(this).off().on('click', function(event){
event.preventDefault();
switch (type) {
case 'customAmount':
obj.setCustomAmount(0);
obj.updateSummaryProducts();
$('.js-cart-custom-amount').val('');
break;
case 'discount':
obj.setDiscount(0);
obj.updateDiscount(0);
obj.updateSummaryProducts();
$('.js-cart-discount').val('');
break;
case 'tip':
obj.setTip(0);
obj.updateTip(0);
$('.js-cart-tip').val('');
break;
default:
break;
}
obj.updateTotal();
obj.updateSummaryTotal();
obj.emptyCartToggle();
});
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

View file

@ -0,0 +1,50 @@
{
"name": "BTCPayServer Point of Sale",
"short_name": "BTCPay POS",
"theme_color": "#1e7a44",
"background_color": "#ffffff",
"display": "standalone",
"icons": [
{
"src": "img/icons/icon-72x72.png",
"sizes": "72x72",
"type": "image/png"
},
{
"src": "img/icons/icon-96x96.png",
"sizes": "96x96",
"type": "image/png"
},
{
"src": "img/icons/icon-128x128.png",
"sizes": "128x128",
"type": "image/png"
},
{
"src": "img/icons/icon-144x144.png",
"sizes": "144x144",
"type": "image/png"
},
{
"src": "img/icons/icon-152x152.png",
"sizes": "152x152",
"type": "image/png"
},
{
"src": "img/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "img/icons/icon-384x384.png",
"sizes": "384x384",
"type": "image/png"
},
{
"src": "img/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"splash_pages": null
}