New PoS design
|
@ -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);
|
||||
|
||||
//
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -65,6 +65,7 @@ namespace BTCPayServer.Controllers
|
|||
ButtonText = settings.ButtonText,
|
||||
CustomButtonText = settings.CustomButtonText,
|
||||
CustomTipText = settings.CustomTipText,
|
||||
CustomTipPercentages = settings.CustomTipPercentages,
|
||||
CustomCSSLink = settings.CustomCSSLink
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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")]
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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">×</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> <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> <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>
|
||||
|
|
128
BTCPayServer/wwwroot/cart/css/style.css
Normal 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%;
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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, '&') /* 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();
|
||||
});
|
||||
}
|
BIN
BTCPayServer/wwwroot/img/icons/icon-128x128.png
Executable file
After Width: | Height: | Size: 2.9 KiB |
BIN
BTCPayServer/wwwroot/img/icons/icon-144x144.png
Executable file
After Width: | Height: | Size: 3.3 KiB |
BIN
BTCPayServer/wwwroot/img/icons/icon-152x152.png
Executable file
After Width: | Height: | Size: 3.5 KiB |
BIN
BTCPayServer/wwwroot/img/icons/icon-192x192.png
Executable file
After Width: | Height: | Size: 4.4 KiB |
BIN
BTCPayServer/wwwroot/img/icons/icon-384x384.png
Executable file
After Width: | Height: | Size: 10 KiB |
BIN
BTCPayServer/wwwroot/img/icons/icon-512x512.png
Executable file
After Width: | Height: | Size: 6.3 KiB |
BIN
BTCPayServer/wwwroot/img/icons/icon-72x72.png
Executable file
After Width: | Height: | Size: 1.7 KiB |
BIN
BTCPayServer/wwwroot/img/icons/icon-96x96.png
Executable file
After Width: | Height: | Size: 2.2 KiB |
BIN
BTCPayServer/wwwroot/img/splash.png
Normal file
After Width: | Height: | Size: 26 KiB |
50
BTCPayServer/wwwroot/manifest.json
Normal 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
|
||||
}
|