mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-21 22:11:48 +01:00
Redo App template editor with Vue based app (#1544)
* Redo App template editor with Vue based app * fix styles better * remove debug * support methods * make price required * fix styling for validation errors * show create or edit in title based on context * add border bottom when image present * escape/unescape * more validation fixes * fix responsive style * Toggle template field * add errors to the app * enhance validation
This commit is contained in:
parent
2bc7fa0316
commit
2d68d0da63
7 changed files with 455 additions and 414 deletions
400
BTCPayServer/Views/Apps/TemplateEditor.cshtml
Normal file
400
BTCPayServer/Views/Apps/TemplateEditor.cshtml
Normal file
|
@ -0,0 +1,400 @@
|
|||
@model (string templateId, string title)
|
||||
<style>
|
||||
.card-deck {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||
grid-gap: .5rem;
|
||||
}
|
||||
.card-deck .card{
|
||||
max-width:480px;
|
||||
}
|
||||
.card-img-top{
|
||||
min-height:247px; background-repeat: no-repeat; background-size:contain; background-position: top;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div id="template-editor-app" v-cloak>
|
||||
<div class="form-group">
|
||||
<label class="control-label mb-3">@Model.title </label>
|
||||
@if (ViewContext.ViewData.ModelState.TryGetValue(Model.templateId, out var errors))
|
||||
{
|
||||
foreach (var error in errors.Errors)
|
||||
{
|
||||
<br/>
|
||||
<span class="text-danger">@error.ErrorMessage</span>
|
||||
}
|
||||
}
|
||||
|
||||
<div class="bg-light card">
|
||||
<div class="card-body " v-bind:class="{ 'card-deck': items.length > 0}">
|
||||
<div v-for="(item, index) of items" class="card my-2 card-wrapper template-item mr-0 ml-0" v-bind:key="item.id">
|
||||
<div v-if="anyImages" class="card-img-top border-bottom" v-bind:style="getImage(item)"></div>
|
||||
<div class="card-body">
|
||||
<h6 class="card-title" v-html="item.title"></h6>
|
||||
<button type="button" class="btn btn-primary" v-on:click="editItem(index)">Edit</button>
|
||||
<button type="button" class="btn btn-danger" v-on:click="removeItem(index)">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="!items || items.length === 0" class="col-12 text-center">
|
||||
No items.<br/>
|
||||
<button type="button" class="btn btn-link" v-on:click="editItem(-1)" id="btn-add-first">
|
||||
Add your first item
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-footer text-left">
|
||||
<button type="button" class="btn btn-secondary" v-on:click="editItem(-1)" id="btn-add">
|
||||
<i class="fa fa-plus fa-fw"></i> Add
|
||||
</button>
|
||||
<button type="button" class="btn btn-secondary ml-2" v-on:click="toggleTemplateElement()">
|
||||
Toggle raw editor
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<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" v-if="editingItem">{{editingItem.index>=0? "Edit" : "Create"}} item</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close" ref="close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body" v-if="editingItem">
|
||||
<div class="mb-3">
|
||||
<span class="text-danger row m-2" v-for="error of errors">{{error}}</span>
|
||||
<div class="form-row">
|
||||
<div class="col-sm-6">
|
||||
<label>Title</label>*
|
||||
<input type="text" required pattern="[^\*#]+" class="form-control mb-2" v-model="editingItem.title" autofocus ref="txtTitle" />
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<label>Price</label>*
|
||||
<input class="form-control mb-2"
|
||||
inputmode="numeric"
|
||||
pattern="\d*"
|
||||
step="any"
|
||||
min="0"
|
||||
type="number"
|
||||
required
|
||||
v-model="editingItem.price" ref="txtPrice"/>
|
||||
</div>
|
||||
<div class="col-sm-3">
|
||||
<label>Custom price</label>
|
||||
<select class="form-control" v-model="editingItem.custom">
|
||||
<option v-bind:value="false">No</option>
|
||||
<option v-bind:value="true">Yes</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<label>Image</label>
|
||||
<input type="text" class="form-control mb-2" pattern="[^\*#]+" v-model="editingItem.image" ref="txtImage"/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<label>Description</label>
|
||||
<textarea rows="3" cols="40" class="form-control mb-2" v-model="editingItem.description" ref="txtDescription"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<label>Inventory (leave blank to not use inventory feature)</label>
|
||||
<input type="number" min="0" step="1" class="form-control mb-2" v-model="editingItem.inventory" ref="txtInventory" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="col">
|
||||
<label>Id (leave blank to generate from title)</label>
|
||||
<input type="text" required pattern="[^\*#]+" class="form-control mb-2" v-model="editingItem.id" ref="txtId"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal" v-on:click="clearEditingItem()">Close</button>
|
||||
<button type="button" class="btn btn-primary" v-on:click="saveEditingItem()">Save Changes</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function() {
|
||||
new Vue({
|
||||
el: '#template-editor-app',
|
||||
data: {
|
||||
errors: [],
|
||||
items: [],
|
||||
editingItem: null,
|
||||
elementId: "@Model.templateId"
|
||||
},
|
||||
computed: {
|
||||
anyImages: function(){
|
||||
return !!this.items.find(function(i){ return !!i.image;});
|
||||
}
|
||||
},
|
||||
mounted: function() {
|
||||
this.loadYml();
|
||||
this.getInputElement().on("input change", this.loadYml.bind(this));
|
||||
this.getModalElement().on("hide.bs.modal", this.clearEditingItem.bind(this));
|
||||
this.toggleTemplateElement();
|
||||
},
|
||||
methods: {
|
||||
toggleTemplateElement: function(){
|
||||
this.getInputElement().parent().toggle();
|
||||
},
|
||||
getImage: function(item){
|
||||
return {
|
||||
"background-image" : "url('"+(this.unEscapeKey(item.image) || "/img/img-placeholder.svg") +"')",
|
||||
"opacity": item.image? 1: 0.5
|
||||
}
|
||||
},
|
||||
getInputElement : function(){ return $("#"+this.elementId); },
|
||||
getModalElement : function(){ return $("#product-modal"); },
|
||||
loadYml: function(){
|
||||
var result = [];
|
||||
var template = this.getInputElement().val().trim();
|
||||
|
||||
var lines = [];
|
||||
var items = template.split("\n");
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
if(items[i] === ""){
|
||||
continue;
|
||||
}
|
||||
if(items[i].startsWith(" ")){
|
||||
lines[lines.length-1]+=items[i] + "\n";
|
||||
}else{
|
||||
|
||||
lines.push(items[i] + "\n");
|
||||
}
|
||||
}
|
||||
|
||||
// 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, inventory=null, paymentMethods = [];
|
||||
|
||||
var goingThroughMethods = false;
|
||||
for (var kp in product) {
|
||||
var productProperty = product[kp].trim();
|
||||
|
||||
if (kp == 0) {
|
||||
id = productProperty.replace(":", "");
|
||||
}
|
||||
if(productProperty.startsWith("-") && goingThroughMethods){
|
||||
paymentMethods.push(productProperty.substr(1));
|
||||
}else{
|
||||
goingThroughMethods = false;
|
||||
}
|
||||
|
||||
if (productProperty.indexOf('price:') !== -1) {
|
||||
price = parseFloat(productProperty.replace('price:', '').trim()).noExponents();
|
||||
}
|
||||
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 (productProperty.indexOf('inventory:') !== -1) {
|
||||
inventory = parseInt(productProperty.replace('inventory:', '').trim(),10);
|
||||
}
|
||||
if (productProperty.indexOf('payment_methods:') !== -1) {
|
||||
goingThroughMethods = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (price != null || title != null) {
|
||||
// Add product to the list
|
||||
result.push({
|
||||
id: id,
|
||||
title: title,
|
||||
price: price,
|
||||
image: image || null,
|
||||
description: description || '',
|
||||
custom: Boolean(custom),
|
||||
inventory: isNaN(inventory)? null: inventory,
|
||||
paymentMethods: paymentMethods
|
||||
});
|
||||
}
|
||||
}
|
||||
this.items = result;
|
||||
},
|
||||
toYml: function(){
|
||||
var template = '';
|
||||
// Construct template from the product list
|
||||
for (var key in this.items) {
|
||||
var product = this.items[key],
|
||||
id = product.id,
|
||||
title = product.title,
|
||||
price = product.price? product.price : 0,
|
||||
image = product.image,
|
||||
description = product.description,
|
||||
custom = product.custom,
|
||||
inventory = product.inventory,
|
||||
paymentMethods = product.paymentMethods;
|
||||
|
||||
template += id + ':\n' +
|
||||
' price: ' + parseFloat(price).noExponents() + '\n' +
|
||||
' title: ' + title + '\n';
|
||||
|
||||
if (description) {
|
||||
template += ' description: ' + description + '\n';
|
||||
}
|
||||
if (image) {
|
||||
template += ' image: ' + image + '\n';
|
||||
}
|
||||
if (custom) {
|
||||
template += ' custom: true\n';
|
||||
}
|
||||
if(paymentMethods != null && paymentMethods.length > 0){
|
||||
template+= ' payment_methods:\n';
|
||||
for (var method of paymentMethods){
|
||||
template+= ' - '+method+'\n';
|
||||
}
|
||||
}
|
||||
template += '\n';
|
||||
}
|
||||
this.getInputElement().val(template);
|
||||
},
|
||||
editItem: function(index){
|
||||
this.errors = [];
|
||||
if(index < 0){
|
||||
this.editingItem = {index:-1, id:"", title: "", price: 0, image: "", description: "", custom: false, inventory: null, paymentMethods: []};
|
||||
}else{
|
||||
this.editingItem = {...this.items[index], index};
|
||||
}
|
||||
|
||||
this.editingItem = this.unEscape(this.editingItem);
|
||||
this.getModalElement().modal("show");
|
||||
},
|
||||
removeItem: function(index){
|
||||
this.items.splice(index,1);
|
||||
this.toYml();
|
||||
},
|
||||
clearEditingItem: function(){
|
||||
this.editingItem = null;
|
||||
this.errors = [];
|
||||
},
|
||||
validate: function(){
|
||||
this.errors = [];
|
||||
if (this.editingItem.id) {
|
||||
var matchedId = this.items.findIndex((x)=> { return this.unEscapeKey(x.id) === this.editingItem.id;});
|
||||
if( matchedId>= 0 && matchedId != this.editingItem.index)
|
||||
this.errors.push("You cannot have multiple items with the same id");
|
||||
|
||||
if (!this.$refs.txtId.checkValidity()) {
|
||||
this.errors.push("Id is required and cannot have * or #");
|
||||
}
|
||||
if(this.editingItem.id.startsWith("- ")){
|
||||
this.errors.push("Id cannot start with \"- \"");
|
||||
}else if(this.editingItem.id.trim() == ""){
|
||||
this.errors.push("Id is required");
|
||||
}
|
||||
}
|
||||
|
||||
if (this.editingItem.description.indexOf("*") >= 0 || this.editingItem.description.indexOf("#") >= 0) {
|
||||
this.errors.push("Description cannot have * or #");
|
||||
}
|
||||
if(this.editingItem.description.startsWith("- ")){
|
||||
this.errors.push("Description cannot start with \"- \"");
|
||||
}
|
||||
if (!this.$refs.txtImage.checkValidity()) {
|
||||
this.errors.push("Image cannot have * or #");
|
||||
}
|
||||
if(this.editingItem.image.startsWith("- ")){
|
||||
this.errors.push("Image cannot start with \"- \"");
|
||||
}
|
||||
|
||||
if (!this.$refs.txtPrice.checkValidity()) {
|
||||
this.errors.push("Price must be a valid number");
|
||||
}
|
||||
if (!this.$refs.txtTitle.checkValidity()) {
|
||||
this.errors.push("Title is required and cannot have * or #");
|
||||
}else if(this.editingItem.title.startsWith("- ")){
|
||||
this.errors.push("Title cannot start with \"- \"");
|
||||
}else if(this.editingItem.title.trim() == ""){
|
||||
this.errors.push("Title is required");
|
||||
}
|
||||
if (!this.$refs.txtInventory.checkValidity()) {
|
||||
this.errors.push("Inventory must be blank or a a valid number (>=0)");
|
||||
}
|
||||
return this.errors.length === 0;
|
||||
},
|
||||
saveEditingItem: function(){
|
||||
if(!this.editingItem.id){
|
||||
this.editingItem.id = this.editingItem.title.toLowerCase().trim();
|
||||
this.$nextTick(this.saveEditingItem.bind(this));
|
||||
return;
|
||||
}
|
||||
if(!this.validate()){
|
||||
return;
|
||||
}
|
||||
this.editingItem = this.escape(this.editingItem);
|
||||
|
||||
if(this.editingItem.index < 0){
|
||||
this.items.push(this.editingItem);
|
||||
}else{
|
||||
this.items.splice(this.editingItem.index,1,this.editingItem);
|
||||
}
|
||||
this.toYml();
|
||||
this.getModalElement().modal("hide");
|
||||
},
|
||||
escape: function(item) {
|
||||
for(var k in item){
|
||||
if(k !== "paymentMethods" && k!=="id"){
|
||||
item[k] = $('<div/>').text(item[k]).html();
|
||||
}
|
||||
}
|
||||
return item;
|
||||
},
|
||||
unEscape: function(item){
|
||||
for(var k in item){
|
||||
if(k !== "paymentMethods" && k!=="id"){
|
||||
item[k] = this.unEscapeKey(item[k]);
|
||||
}
|
||||
}
|
||||
return item;
|
||||
},
|
||||
unEscapeKey : function(k){
|
||||
return $('<div/>').html(k).text();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Number.prototype.noExponents= function(){
|
||||
var data= String(this).split(/[eE]/);
|
||||
if(data.length== 1) return data[0];
|
||||
|
||||
var z= '', sign= this<0? '-':'',
|
||||
str= data[0].replace('.', ''),
|
||||
mag= Number(data[1])+ 1;
|
||||
|
||||
if(mag<0){
|
||||
z= sign + '0.';
|
||||
while(mag++) z += '0';
|
||||
return z + str.replace(/^\-/,'');
|
||||
}
|
||||
mag -= str.length;
|
||||
while(mag--) z += '0';
|
||||
return str + z;
|
||||
};
|
||||
|
||||
</script>
|
|
@ -5,24 +5,7 @@
|
|||
ViewData["Title"] = "Update Crowdfund";
|
||||
}
|
||||
<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">Contribution Perks Management</h5>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
</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 section-heading">
|
||||
|
@ -109,14 +92,7 @@
|
|||
</div>
|
||||
<span asp-validation-for="EndDate" class="text-danger"></span>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="control-label">Contribution Perks</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 Perk</a>
|
||||
</div>
|
||||
<div class="js-products bg-light row p-3">
|
||||
</div>
|
||||
</div>
|
||||
<partial name="TemplateEditor" model="@(nameof(Model.PerksTemplate), "Perks" )"/>
|
||||
<div class="form-group">
|
||||
<label asp-for="PerksTemplate" class="control-label"></label>
|
||||
<textarea asp-for="PerksTemplate" rows="10" cols="40" class="js-product-template form-control"></textarea>
|
||||
|
@ -230,61 +206,4 @@
|
|||
<script src="~/vendor/moment/moment.js" asp-append-version="true"></script>
|
||||
<bundle name="wwwroot/bundles/crowdfund-admin-bundle.min.js" asp-append-version="true"></bundle>
|
||||
<bundle name="wwwroot/bundles/crowdfund-admin-bundle.min.css" asp-append-version="true"></bundle>
|
||||
<script id="template-product-item" type="text/template">
|
||||
<div class="col-sm-4 col-md-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>*
|
||||
<input class="js-product-price form-control mb-2"
|
||||
inputmode="numeric"
|
||||
pattern="\d*"
|
||||
step="any"
|
||||
type="number"
|
||||
value="{price}" />
|
||||
</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 class="form-row">
|
||||
<div class="col">
|
||||
<label>Inventory (leave blank to not use inventory feature)</label>
|
||||
<input type="number" step="1" class="js-product-inventory form-control mb-2" value="{inventory}" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
}
|
||||
|
|
|
@ -4,25 +4,6 @@
|
|||
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 section-heading">
|
||||
|
@ -101,14 +82,7 @@
|
|||
<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>
|
||||
<partial name="TemplateEditor" model="@(nameof(Model.Template), "Products")" />
|
||||
<div class="form-group">
|
||||
<label asp-for="Template" class="control-label"></label>*
|
||||
<textarea asp-for="Template" rows="10" cols="40" class="js-product-template form-control"></textarea>
|
||||
|
|
|
@ -99,11 +99,11 @@
|
|||
{
|
||||
"outputFileName": "wwwroot/bundles/crowdfund-admin-bundle.min.js",
|
||||
"inputFiles": [
|
||||
"wwwroot/vendor/vuejs/vue.min.js",
|
||||
"wwwroot/vendor/babel-polyfill/polyfill.min.js",
|
||||
"wwwroot/vendor/highlightjs/highlight.min.js",
|
||||
"wwwroot/vendor/summernote/summernote-bs4.js",
|
||||
"wwwroot/crowdfund-admin/main.js",
|
||||
"wwwroot/products/js/products.js",
|
||||
"wwwroot/products/js/products.jquery.js"
|
||||
"wwwroot/crowdfund-admin/main.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
@ -116,11 +116,11 @@
|
|||
{
|
||||
"outputFileName": "wwwroot/bundles/pos-admin-bundle.min.js",
|
||||
"inputFiles": [
|
||||
"wwwroot/vendor/vuejs/vue.min.js",
|
||||
"wwwroot/vendor/babel-polyfill/polyfill.min.js",
|
||||
"wwwroot/vendor/highlightjs/highlight.min.js",
|
||||
"wwwroot/vendor/summernote/summernote-bs4.js",
|
||||
"wwwroot/pos-admin/main.js",
|
||||
"wwwroot/products/js/products.js",
|
||||
"wwwroot/products/js/products.jquery.js"
|
||||
"wwwroot/pos-admin/main.js"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
46
BTCPayServer/wwwroot/img/img-placeholder.svg
Normal file
46
BTCPayServer/wwwroot/img/img-placeholder.svg
Normal file
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Generator: Adobe Illustrator 18.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 487.482 487.482" style="enable-background:new 0 0 487.482 487.482;" xml:space="preserve">
|
||||
<g>
|
||||
<path d="M477.482,63.277H10c-5.523,0-10,4.478-10,10v340.928c0,5.522,4.477,10,10,10h467.482c5.522,0,10-4.478,10-10V73.277
|
||||
C487.482,67.755,483.005,63.277,477.482,63.277z M467.482,83.277v235.236l-104.018-80.07c-3.364-2.591-7.996-2.773-11.555-0.457
|
||||
l-68.298,44.456l-134.745-92.417c-3.192-2.189-7.362-2.34-10.706-0.384L20,258.777v-175.5H467.482z M20,404.205V281.948
|
||||
l122.786-71.841l135.024,92.608c3.337,2.286,7.723,2.34,11.111,0.134l67.959-44.235l110.602,85.138v60.453H20z"/>
|
||||
<path d="M280.707,218.281c26.063,0,47.266-21.202,47.266-47.264c0-26.058-21.203-47.258-47.266-47.258
|
||||
c-26.062,0-47.264,21.2-47.264,47.258C233.443,197.079,254.645,218.281,280.707,218.281z M280.707,143.76
|
||||
c15.034,0,27.266,12.228,27.266,27.258c0,15.033-12.231,27.264-27.266,27.264c-15.033,0-27.264-12.23-27.264-27.264
|
||||
C253.443,155.987,265.674,143.76,280.707,143.76z"/>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
<g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -1,72 +0,0 @@
|
|||
$(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(),
|
||||
inventory = parseInt($('.js-product-inventory').val(), 10);
|
||||
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 != null) {
|
||||
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() + ':');
|
||||
}
|
||||
if(inventory != null && !isNaN(inventory ))
|
||||
obj.inventory = inventory;
|
||||
|
||||
products.saveItem(obj, index);
|
||||
}
|
||||
});
|
||||
|
||||
$('.js-product-add').click(function(){
|
||||
products.itemContent();
|
||||
});
|
||||
});
|
|
@ -1,226 +0,0 @@
|
|||
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();
|
||||
|
||||
var lines = [];
|
||||
var items = template.split("\n");
|
||||
for (var i = 0; i < items.length; i++) {
|
||||
if(items[i] === ""){
|
||||
continue;
|
||||
}
|
||||
if(items[i].startsWith(" ")){
|
||||
lines[lines.length-1]+=items[i] + "\n";
|
||||
}else{
|
||||
|
||||
lines.push(items[i] + "\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, inventory=null;
|
||||
|
||||
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()).noExponents();
|
||||
}
|
||||
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 (productProperty.indexOf('inventory:') !== -1) {
|
||||
inventory = parseInt(productProperty.replace('inventory:', '').trim(),10);
|
||||
}
|
||||
}
|
||||
|
||||
if (price != null || title != null) {
|
||||
// Add product to the list
|
||||
this.products.push({
|
||||
'id': id,
|
||||
'title': title,
|
||||
'price': price,
|
||||
'image': image || null,
|
||||
'description': description || '',
|
||||
'custom': Boolean(custom),
|
||||
'inventory': isNaN(inventory)? null: inventory
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
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? product.price : 0,
|
||||
image = product.image,
|
||||
description = product.description,
|
||||
custom = product.custom,
|
||||
inventory = product.inventory;
|
||||
|
||||
template += id + '\n' +
|
||||
' price: ' + parseFloat(price).noExponents() + '\n' +
|
||||
' title: ' + title + '\n';
|
||||
|
||||
if (description) {
|
||||
template += ' description: ' + description + '\n';
|
||||
}
|
||||
if (image) {
|
||||
template += ' image: ' + image + '\n';
|
||||
}
|
||||
if (custom) {
|
||||
template += ' custom: true\n';
|
||||
}
|
||||
if(inventory != null){
|
||||
template+= ' inventory: ' + inventory + '\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 ? parseFloat(this.escape(product.price)).noExponents() : '',
|
||||
'title': product != null ? this.escape(product.title) : '',
|
||||
'description': product != null ? this.escape(product.description) : '',
|
||||
'image': product != null ? this.escape(product.image) : '',
|
||||
'inventory': product != null ? parseInt(this.escape(product.inventory),10) : '',
|
||||
'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, '>')
|
||||
;
|
||||
}
|
||||
|
||||
Number.prototype.noExponents= function(){
|
||||
var data= String(this).split(/[eE]/);
|
||||
if(data.length== 1) return data[0];
|
||||
|
||||
var z= '', sign= this<0? '-':'',
|
||||
str= data[0].replace('.', ''),
|
||||
mag= Number(data[1])+ 1;
|
||||
|
||||
if(mag<0){
|
||||
z= sign + '0.';
|
||||
while(mag++) z += '0';
|
||||
return z + str.replace(/^\-/,'');
|
||||
}
|
||||
mag -= str.length;
|
||||
while(mag--) z += '0';
|
||||
return str + z;
|
||||
};
|
Loading…
Add table
Reference in a new issue