mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-22 14:22:40 +01:00
Create js-only product management in PoS
This commit is contained in:
parent
f0ff47af8d
commit
d039890a9b
3 changed files with 340 additions and 1 deletions
|
@ -3,6 +3,26 @@
|
|||
ViewData["Title"] = "Update Point of Sale";
|
||||
}
|
||||
<section>
|
||||
<div class="modal" id="product-modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Product management</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Modal body text goes here.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="js-product-save btn btn-primary">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-lg-12 text-center">
|
||||
|
@ -58,9 +78,17 @@
|
|||
<input asp-for="CustomCSSLink" class="form-control" />
|
||||
<span asp-validation-for="CustomCSSLink" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Products</label>*
|
||||
<div class="mb-3">
|
||||
<a class="js-product-add btn btn-secondary" href="#" data-toggle="modal" data-target="#product-modal"><i class="fa fa-plus fa-fw"></i> Add Product</a>
|
||||
</div>
|
||||
<div class="js-products bg-light row p-3">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label asp-for="Template" class="control-label"></label>*
|
||||
<textarea asp-for="Template" rows="20" cols="40" class="form-control"></textarea>
|
||||
<textarea asp-for="Template" rows="10" cols="40" class="js-product-template form-control"></textarea>
|
||||
<span asp-validation-for="Template" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
|
@ -101,5 +129,61 @@
|
|||
<link rel="stylesheet" href="~/vendor/highlightjs/default.min.css">
|
||||
<script src="~/vendor/highlightjs/highlight.min.js"></script>
|
||||
<script>hljs.initHighlightingOnLoad();</script>
|
||||
|
||||
<script id="template-product-item" type="text/template">
|
||||
<div class="col-sm-3 mb-3">
|
||||
<div class="card">
|
||||
{image}
|
||||
<div class="card-body">
|
||||
<h6 class="card-title">{title}</h6>
|
||||
<a href="#" class="js-product-edit btn btn-primary" data-toggle="modal" data-target="#product-modal">Edit</a>
|
||||
<a href="#" class="js-product-remove btn btn-danger"><i class="fa fa-trash"></i></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script id="template-product-content" type="text/template">
|
||||
<div class="mb-3">
|
||||
<input class="js-product-id" type="hidden" name="id" value="{id}">
|
||||
<input class="js-product-index" type="hidden" name="index" value="{index}">
|
||||
<div class="form-row">
|
||||
<div class="col-sm-6">
|
||||
<label>Title</label>*
|
||||
<input type="text" class="js-product-title form-control mb-2" value="{title}" autofocus />
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<label>Price</label>*
|
||||
<div class="input-group mb-2">
|
||||
<div class="input-group-prepend">
|
||||
<span class="input-group-text"><i class="fa fa-usd"></i></span>
|
||||
</div>
|
||||
<input type="text" class="js-product-price form-control" value="{price}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<label>Custom price</label>
|
||||
<select class="js-product-custom form-control">
|
||||
{custom}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<label>Image</label>
|
||||
<input type="text" class="js-product-image form-control mb-2" value="{image}" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<label>Description</label>
|
||||
<textarea rows="3" cols="40" class="js-product-description form-control mb-2">{description}</textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script src="~/products/js/products.js"></script>
|
||||
<script src="~/products/js/products.jquery.js"></script>
|
||||
}
|
||||
|
||||
|
|
69
BTCPayServer/wwwroot/products/js/products.jquery.js
Normal file
69
BTCPayServer/wwwroot/products/js/products.jquery.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
$(document).ready(function(){
|
||||
var products = new Products(),
|
||||
delay = null;
|
||||
|
||||
$('.js-product-template').on('input', function(){
|
||||
products.loadFromTemplate();
|
||||
|
||||
clearTimeout(delay);
|
||||
|
||||
// Delay rebuilding DOM for performance reasons
|
||||
delay = setTimeout(function(){
|
||||
products.showAll();
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
$('.js-products').on('click', '.js-product-remove', function(event){
|
||||
event.preventDefault();
|
||||
|
||||
var id = $(this).closest('.card').parent().index();
|
||||
|
||||
products.removeItem(id);
|
||||
});
|
||||
|
||||
$('.js-products').on('click', '.js-product-edit', function(event){
|
||||
event.preventDefault();
|
||||
|
||||
var id = $(this).closest('.card').parent().index();
|
||||
|
||||
products.itemContent(id);
|
||||
});
|
||||
|
||||
$('.js-product-save').click(function(event){
|
||||
event.preventDefault();
|
||||
|
||||
var index = $('.js-product-index').val(),
|
||||
description = $('.js-product-description').val(),
|
||||
image = $('.js-product-image').val(),
|
||||
custom = $('.js-product-custom').val();
|
||||
obj = {
|
||||
id: products.escape($('.js-product-id').val()),
|
||||
price: products.escape($('.js-product-price').val()),
|
||||
title: products.escape($('.js-product-title').val()),
|
||||
};
|
||||
|
||||
// Only continue if price and title is provided
|
||||
if (obj.price && obj.title) {
|
||||
if (description) {
|
||||
obj.description = products.escape(description);
|
||||
}
|
||||
if (image) {
|
||||
obj.image = products.escape(image);
|
||||
}
|
||||
if (custom == 'true') {
|
||||
obj.custom = products.escape(custom);
|
||||
}
|
||||
|
||||
// Create an id from the title for a new product
|
||||
if (!Boolean(index)) {
|
||||
obj.id = products.escape(obj.title.toLowerCase() + ':');
|
||||
}
|
||||
|
||||
products.saveItem(obj, index);
|
||||
}
|
||||
});
|
||||
|
||||
$('.js-product-add').click(function(){
|
||||
products.itemContent();
|
||||
});
|
||||
});
|
186
BTCPayServer/wwwroot/products/js/products.js
Normal file
186
BTCPayServer/wwwroot/products/js/products.js
Normal file
|
@ -0,0 +1,186 @@
|
|||
function Products() {
|
||||
this.products = [];
|
||||
|
||||
// Get products from template
|
||||
this.loadFromTemplate();
|
||||
|
||||
// Show products in the DOM
|
||||
this.showAll();
|
||||
}
|
||||
|
||||
Products.prototype.loadFromTemplate = function() {
|
||||
var template = $('.js-product-template').val().trim(),
|
||||
lines = template.split("\n\n");
|
||||
|
||||
this.products = [];
|
||||
|
||||
// Split products from the template
|
||||
for (var kl in lines) {
|
||||
var line = lines[kl],
|
||||
product = line.split("\n"),
|
||||
id, price, title, description, image = null,
|
||||
custom;
|
||||
|
||||
for (var kp in product) {
|
||||
var productProperty = product[kp].trim();
|
||||
|
||||
if (kp == 0) {
|
||||
id = productProperty;
|
||||
}
|
||||
|
||||
if (productProperty.indexOf('price:') !== -1) {
|
||||
price = parseFloat(productProperty.replace('price:', '').trim());
|
||||
}
|
||||
if (productProperty.indexOf('title:') !== -1) {
|
||||
title = productProperty.replace('title:', '').trim();
|
||||
}
|
||||
if (productProperty.indexOf('description:') !== -1) {
|
||||
description = productProperty.replace('description:', '').trim();
|
||||
}
|
||||
if (productProperty.indexOf('image:') !== -1) {
|
||||
image = productProperty.replace('image:', '').trim();
|
||||
}
|
||||
if (productProperty.indexOf('custom:') !== -1) {
|
||||
custom = productProperty.replace('custom:', '').trim();
|
||||
}
|
||||
}
|
||||
|
||||
if (price != null || title != null) {
|
||||
// Add product to the list
|
||||
this.products.push({
|
||||
'id': id,
|
||||
'title': title,
|
||||
'price': price,
|
||||
'image': image || null,
|
||||
'description': description || null,
|
||||
'custom': Boolean(custom)
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Products.prototype.saveTemplate = function() {
|
||||
var template = '';
|
||||
|
||||
// Construct template from the product list
|
||||
for (var key in this.products) {
|
||||
var product = this.products[key],
|
||||
id = product.id,
|
||||
title = product.title,
|
||||
price = product.price,
|
||||
image = product.image
|
||||
description = product.description,
|
||||
custom = product.custom;
|
||||
|
||||
template += id + '\n' +
|
||||
' price: ' + price + '\n' +
|
||||
' title: ' + title + '\n';
|
||||
|
||||
if (description) {
|
||||
template += ' description: ' + description + '\n';
|
||||
}
|
||||
if (image) {
|
||||
template += ' image: ' + image + '\n';
|
||||
}
|
||||
if (custom) {
|
||||
template += ' custom: true\n';
|
||||
}
|
||||
template += '\n';
|
||||
}
|
||||
|
||||
$('.js-product-template').val(template);
|
||||
}
|
||||
|
||||
Products.prototype.showAll = function() {
|
||||
var list = [];
|
||||
|
||||
for (var key in this.products) {
|
||||
var product = this.products[key],
|
||||
image = product.image;
|
||||
|
||||
list.push(this.template($('#template-product-item'), {
|
||||
'title': this.escape(product.title),
|
||||
'image': image ? '<img class="card-img-top" src="' + this.escape(image) + '" alt="Card image cap">' : ''
|
||||
}));
|
||||
}
|
||||
|
||||
$('.js-products').html(list);
|
||||
}
|
||||
|
||||
// Load the template
|
||||
Products.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;
|
||||
}
|
||||
|
||||
Products.prototype.saveItem = function(obj, index) {
|
||||
// Edit product
|
||||
if (index) {
|
||||
this.products[index] = obj;
|
||||
} else { // Add new product
|
||||
this.products.push(obj);
|
||||
}
|
||||
|
||||
this.saveTemplate();
|
||||
this.showAll();
|
||||
this.modalEmpty();
|
||||
}
|
||||
|
||||
Products.prototype.removeItem = function(index) {
|
||||
if (this.products.length == 1) {
|
||||
this.products = [];
|
||||
$('.js-products').html('No products.');
|
||||
} else {
|
||||
this.products.splice(index, 1);
|
||||
$('.js-products').find('.card').parent().eq(index).remove();
|
||||
}
|
||||
|
||||
this.saveTemplate();
|
||||
}
|
||||
|
||||
Products.prototype.itemContent = function(index) {
|
||||
var product = null,
|
||||
custom = false;
|
||||
|
||||
// Existing product
|
||||
if (!isNaN(index)) {
|
||||
product = this.products[index];
|
||||
custom = product.custom;
|
||||
}
|
||||
|
||||
var template = this.template($('#template-product-content'), {
|
||||
'id': product != null ? this.escape(product.id) : '',
|
||||
'index': isNaN(index) ? '' : this.escape(index),
|
||||
'price': product != null ? this.escape(product.price) : '',
|
||||
'title': product != null ? this.escape(product.title) : '',
|
||||
'description': product != null ? this.escape(product.description) : '',
|
||||
'image': product != null ? this.escape(product.image) : '',
|
||||
'custom': '<option value="true"' + (custom ? ' selected' : '') + '>Yes</option><option value="false"' + (!custom ? ' selected' : '') + '>No</option>'
|
||||
});
|
||||
|
||||
$('#product-modal').find('.modal-body').html(template);
|
||||
}
|
||||
|
||||
Products.prototype.modalEmpty = function() {
|
||||
var $modal = $('#product-modal');
|
||||
|
||||
$modal.modal('hide');
|
||||
$modal.find('.modal-body').empty();
|
||||
}
|
||||
|
||||
Products.prototype.escape = function(input) {
|
||||
return ('' + input) /* Forces the conversion to string. */
|
||||
.replace(/&/g, '&') /* This MUST be the 1st replacement. */
|
||||
.replace(/'/g, ''') /* The 4 other predefined entities, required. */
|
||||
.replace(/"/g, '"')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
;
|
||||
}
|
Loading…
Add table
Reference in a new issue