mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-01-19 05:33:31 +01:00
POS Cart: Horizontal scrollable filters (#5391)
This commit is contained in:
parent
e82281d273
commit
c979c4774c
@ -28,8 +28,8 @@
|
||||
}
|
||||
|
||||
<div id="PosCart">
|
||||
<div id="content" class="public-page-wrap">
|
||||
<div class="container-xl">
|
||||
<div id="content">
|
||||
<div class="public-page-wrap container-xl">
|
||||
<header class="sticky-top bg-body d-flex flex-column py-3 py-lg-4 gap-3">
|
||||
<div class="d-flex align-items-center justify-content-center gap-3 pe-5 position-relative">
|
||||
<h1 class="mb-0">@(string.IsNullOrEmpty(Model.Title) ? Model.StoreName : Model.Title)</h1>
|
||||
@ -39,11 +39,13 @@
|
||||
</button>
|
||||
</div>
|
||||
<input id="SearchTerm" class="form-control rounded-pill" placeholder="Search…" v-model="searchTerm">
|
||||
<div v-if="allCategories" class="btcpay-pills d-flex flex-wrap align-items-center justify-content-center gap-3">
|
||||
<template v-for="cat in allCategories">
|
||||
<input :id="`Category-${cat.value}`" type="radio" name="category" autocomplete="off" v-model="displayCategory" :value="cat.value">
|
||||
<label :for="`Category-${cat.value}`" class="btcpay-pill">{{ cat.text }}</label>
|
||||
</template>
|
||||
<div id="Categories" ref="categories" v-if="allCategories" :class="{ 'scrollable': categoriesScrollable }">
|
||||
<nav class="btcpay-pills d-flex align-items-center gap-3" ref="categoriesNav">
|
||||
<template v-for="cat in allCategories">
|
||||
<input :id="`Category-${cat.value}`" type="radio" name="category" autocomplete="off" v-model="displayCategory" :value="cat.value">
|
||||
<label :for="`Category-${cat.value}`" class="btcpay-pill text-nowrap">{{ cat.text }}</label>
|
||||
</template>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<main>
|
||||
@ -122,126 +124,124 @@
|
||||
</div>
|
||||
</div>
|
||||
<aside id="cart" ref="cart" tabindex="-1" aria-labelledby="cartLabel">
|
||||
<div class="public-page-wrap" v-cloak>
|
||||
<div class="container-xl">
|
||||
<header class="sticky-top bg-tile offcanvas-header py-3 py-lg-4 d-flex align-items-baseline justify-content-center gap-3 px-5 pe-lg-0">
|
||||
<h1 class="mb-0" id="cartLabel">Cart</h1>
|
||||
<button id="CartClear" type="reset" v-on:click="clearCart" class="btn btn-text text-primary p-1" v-if="cartCount > 0">
|
||||
Empty
|
||||
</button>
|
||||
<button id="CartClose" type="button" class="cart-toggle-btn" v-on:click="toggleCart" aria-controls="cart" aria-label="Close">
|
||||
<vc:icon symbol="close" />
|
||||
</button>
|
||||
</header>
|
||||
<div class="offcanvas-body py-0">
|
||||
<form method="post" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" v-on:submit="handleFormSubmit" v-if="cartCount !== 0">
|
||||
<input type="hidden" name="amount" :value="totalNumeric">
|
||||
<input type="hidden" name="tip" :value="tipNumeric">
|
||||
<input type="hidden" name="discount" :value="discountPercentNumeric">
|
||||
<input type="hidden" name="posdata" :value="posdata">
|
||||
<table class="table table-borderless mt-0 mb-4">
|
||||
<tbody id="CartItems">
|
||||
<tr v-for="item in cart" :key="item.id">
|
||||
<td class="align-middle">
|
||||
<h6 class="fw-semibold mb-1">{{ item.title }}</h6>
|
||||
<button type="button" v-on:click="removeFromCart(item.id)" class="btn btn-sm btn-link p-0 text-danger fw-semibold">Remove</button>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<div class="d-flex align-items-center gap-2 justify-content-end quantity">
|
||||
<span class="badge text-bg-warning inventory" v-if="item.inventory">
|
||||
{{ item.inventory > 0 ? `${item.inventory} left` : "Sold out" }}
|
||||
</span>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<button type="button" v-on:click="updateQuantity(item.id, -1)" class="btn btn-light p-1 rounded-pill d-flex align-items-center justify-content-center">
|
||||
<vc:icon symbol="minus" />
|
||||
</button>
|
||||
<input class="form-control hide-number-spin w-50px" type="number" placeholder="Qty" min="1" step="1" :max="item.inventory" v-model.number="item.count">
|
||||
<button type="button" v-on:click="updateQuantity(item.id, +1)" class="btn btn-light p-1 rounded-pill d-flex align-items-center justify-content-center">
|
||||
<vc:icon symbol="plus" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle text-end">
|
||||
{{ formatCurrency(item.price||0, true) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="table table-borderless my-4" v-if="showDiscount || enableTips">
|
||||
<tr v-if="showDiscount">
|
||||
<th class="align-middle">Discount</th>
|
||||
<th class="align-middle" colspan="3">
|
||||
<div class="input-group input-group-sm w-100px pull-right">
|
||||
<input class="form-control hide-number-spin" type="number" min="0" step="1" max="100" id="Discount" v-model.number="discountPercent">
|
||||
<span class="input-group-text">%</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
<tr v-if="enableTips">
|
||||
<th class="align-middle">Tip</th>
|
||||
<th class="align-middle" colspan="3">
|
||||
<div class="btcpay-pills d-flex flex-wrap align-items-center justify-content-end gap-2" v-if="customTipPercentages">
|
||||
<div class="btcpay-pill d-flex align-items-center px-3" id="Tip-Custom" :class="{ active: !tipPercent && tip }" v-on:click.prevent="tipPercent = null">
|
||||
<input
|
||||
v-model.number="tip"
|
||||
class="form-control hide-number-spin shadow-none text-reset d-block bg-transparent border-0 p-0 me-1 fw-semibold"
|
||||
style="height:1.5em;min-height:auto;width:4ch"
|
||||
type="number"
|
||||
min="0"
|
||||
step="@Model.Step" />
|
||||
<span>@(Model.CurrencyInfo.CurrencySymbol ?? Model.CurrencyCode)</span>
|
||||
</div>
|
||||
<button
|
||||
v-for="percentage in customTipPercentages"
|
||||
type="button"
|
||||
class="btcpay-pill px-3"
|
||||
:class="{ active: tipPercent == percentage }"
|
||||
:id="`Tip-${percentage}`"
|
||||
v-on:click.prevent="tipPercentage(percentage)">
|
||||
{{ percentage }}%
|
||||
<div class="public-page-wrap container-xl" v-cloak>
|
||||
<header class="sticky-top bg-tile offcanvas-header py-3 py-lg-4 d-flex align-items-baseline justify-content-center gap-3 px-5 pe-lg-0">
|
||||
<h1 class="mb-0" id="cartLabel">Cart</h1>
|
||||
<button id="CartClear" type="reset" v-on:click="clearCart" class="btn btn-text text-primary p-1" v-if="cartCount > 0">
|
||||
Empty
|
||||
</button>
|
||||
<button id="CartClose" type="button" class="cart-toggle-btn" v-on:click="toggleCart" aria-controls="cart" aria-label="Close">
|
||||
<vc:icon symbol="close" />
|
||||
</button>
|
||||
</header>
|
||||
<div class="offcanvas-body py-0">
|
||||
<form method="post" asp-action="ViewPointOfSale" asp-route-appId="@Model.AppId" asp-antiforgery="false" v-on:submit="handleFormSubmit" v-if="cartCount !== 0">
|
||||
<input type="hidden" name="amount" :value="totalNumeric">
|
||||
<input type="hidden" name="tip" :value="tipNumeric">
|
||||
<input type="hidden" name="discount" :value="discountPercentNumeric">
|
||||
<input type="hidden" name="posdata" :value="posdata">
|
||||
<table class="table table-borderless mt-0 mb-4">
|
||||
<tbody id="CartItems">
|
||||
<tr v-for="item in cart" :key="item.id">
|
||||
<td class="align-middle">
|
||||
<h6 class="fw-semibold mb-1">{{ item.title }}</h6>
|
||||
<button type="button" v-on:click="removeFromCart(item.id)" class="btn btn-sm btn-link p-0 text-danger fw-semibold">Remove</button>
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
<div class="d-flex align-items-center gap-2 justify-content-end quantity">
|
||||
<span class="badge text-bg-warning inventory" v-if="item.inventory">
|
||||
{{ item.inventory > 0 ? `${item.inventory} left` : "Sold out" }}
|
||||
</span>
|
||||
<div class="d-flex align-items-center gap-2">
|
||||
<button type="button" v-on:click="updateQuantity(item.id, -1)" class="btn btn-light p-1 rounded-pill d-flex align-items-center justify-content-center">
|
||||
<vc:icon symbol="minus" />
|
||||
</button>
|
||||
<input class="form-control hide-number-spin w-50px" type="number" placeholder="Qty" min="1" step="1" :max="item.inventory" v-model.number="item.count">
|
||||
<button type="button" v-on:click="updateQuantity(item.id, +1)" class="btn btn-light p-1 rounded-pill d-flex align-items-center justify-content-center">
|
||||
<vc:icon symbol="plus" />
|
||||
</button>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
<table class="table table-borderless mt-4 mb-0">
|
||||
<tr>
|
||||
<td class="align-middle">Subtotal</td>
|
||||
<td class="align-middle text-end" id="CartAmount">{{ formatCurrency(amountNumeric, true) }}</td>
|
||||
</tr>
|
||||
<tr v-if="discountNumeric">
|
||||
<td class="align-middle">Discount</td>
|
||||
<td class="align-middle text-end" id="CartDiscount">
|
||||
<span v-if="discountPercent">{{discountPercent}}% =</span>
|
||||
{{ formatCurrency(discountNumeric, true) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="tipNumeric">
|
||||
<td class="align-middle">Tip</td>
|
||||
<td class="align-middle text-end" id="CartTip">
|
||||
<span v-if="tipPercent">{{tipPercent}}% =</span>
|
||||
{{ formatCurrency(tipNumeric, true) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="align-middle h5 border-0">Total</td>
|
||||
<td class="align-middle h5 border-0 text-end" id="CartTotal">{{ formatCurrency(totalNumeric, true) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" class="pt-4">
|
||||
<button id="CartSubmit" class="btn btn-primary btn-lg w-100" :disabled="payButtonLoading" type="submit">
|
||||
<div v-if="payButtonLoading" class="spinner-border spinner-border-sm" id="pay-button-spinner" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<template v-else>Pay</template>
|
||||
</div>
|
||||
</td>
|
||||
<td class="align-middle text-end">
|
||||
{{ formatCurrency(item.price||0, true) }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<table class="table table-borderless my-4" v-if="showDiscount || enableTips">
|
||||
<tr v-if="showDiscount">
|
||||
<th class="align-middle">Discount</th>
|
||||
<th class="align-middle" colspan="3">
|
||||
<div class="input-group input-group-sm w-100px pull-right">
|
||||
<input class="form-control hide-number-spin" type="number" min="0" step="1" max="100" id="Discount" v-model.number="discountPercent">
|
||||
<span class="input-group-text">%</span>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
<tr v-if="enableTips">
|
||||
<th class="align-middle">Tip</th>
|
||||
<th class="align-middle" colspan="3">
|
||||
<div class="btcpay-pills d-flex flex-wrap align-items-center justify-content-end gap-2" v-if="customTipPercentages">
|
||||
<div class="btcpay-pill d-flex align-items-center px-3" id="Tip-Custom" :class="{ active: !tipPercent && tip }" v-on:click.prevent="tipPercent = null">
|
||||
<input
|
||||
v-model.number="tip"
|
||||
class="form-control hide-number-spin shadow-none text-reset d-block bg-transparent border-0 p-0 me-1 fw-semibold"
|
||||
style="height:1.5em;min-height:auto;width:4ch"
|
||||
type="number"
|
||||
min="0"
|
||||
step="@Model.Step" />
|
||||
<span>@(Model.CurrencyInfo.CurrencySymbol ?? Model.CurrencyCode)</span>
|
||||
</div>
|
||||
<button
|
||||
v-for="percentage in customTipPercentages"
|
||||
type="button"
|
||||
class="btcpay-pill px-3"
|
||||
:class="{ active: tipPercent == percentage }"
|
||||
:id="`Tip-${percentage}`"
|
||||
v-on:click.prevent="tipPercentage(percentage)">
|
||||
{{ percentage }}%
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
<p id="CartItems" v-else class="text-muted text-center my-0">There are no items in your cart yet.</p>
|
||||
</div>
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
<table class="table table-borderless mt-4 mb-0">
|
||||
<tr>
|
||||
<td class="align-middle">Subtotal</td>
|
||||
<td class="align-middle text-end" id="CartAmount">{{ formatCurrency(amountNumeric, true) }}</td>
|
||||
</tr>
|
||||
<tr v-if="discountNumeric">
|
||||
<td class="align-middle">Discount</td>
|
||||
<td class="align-middle text-end" id="CartDiscount">
|
||||
<span v-if="discountPercent">{{discountPercent}}% =</span>
|
||||
{{ formatCurrency(discountNumeric, true) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="tipNumeric">
|
||||
<td class="align-middle">Tip</td>
|
||||
<td class="align-middle text-end" id="CartTip">
|
||||
<span v-if="tipPercent">{{tipPercent}}% =</span>
|
||||
{{ formatCurrency(tipNumeric, true) }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="align-middle h5 border-0">Total</td>
|
||||
<td class="align-middle h5 border-0 text-end" id="CartTotal">{{ formatCurrency(totalNumeric, true) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" class="pt-4">
|
||||
<button id="CartSubmit" class="btn btn-primary btn-lg w-100" :disabled="payButtonLoading" type="submit">
|
||||
<div v-if="payButtonLoading" class="spinner-border spinner-border-sm" id="pay-button-spinner" role="status">
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
<template v-else>Pay</template>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</form>
|
||||
<p id="CartItems" v-else class="text-muted text-center my-0">There are no items in your cart yet.</p>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
|
@ -3,7 +3,13 @@
|
||||
}
|
||||
|
||||
#PosCart .public-page-wrap {
|
||||
padding: 0 0 var(--btcpay-space-l);
|
||||
padding-top: 0;
|
||||
}
|
||||
@media (max-width: 400px) {
|
||||
#PosCart .public-page-wrap {
|
||||
padding-left: var(--btcpay-space-s);
|
||||
padding-right: var(--btcpay-space-s);
|
||||
}
|
||||
}
|
||||
|
||||
#PosCart .offcanvas-backdrop {
|
||||
@ -11,6 +17,48 @@
|
||||
transition-duration: var(--btcpay-transition-duration-fast);
|
||||
}
|
||||
|
||||
#Categories nav {
|
||||
justify-content: center;
|
||||
}
|
||||
#Categories.scrollable {
|
||||
--scroll-bar-spacing: var(--btcpay-space-m);
|
||||
--scroll-indicator-spacing: var(--btcpay-space-m);
|
||||
position: relative;
|
||||
margin-bottom: calc(var(--scroll-bar-spacing) * -1);
|
||||
}
|
||||
#Categories.scrollable nav {
|
||||
justify-content: start;
|
||||
overflow: auto visible;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
margin-left: calc(var(--scroll-indicator-spacing) * -1);
|
||||
margin-right: calc(var(--scroll-indicator-spacing) * -1);
|
||||
padding: 0 var(--scroll-indicator-spacing) var(--scroll-bar-spacing);
|
||||
}
|
||||
|
||||
#Categories.scrollable nav::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Horizontal scroll indicators */
|
||||
#Categories.scrollable:before,
|
||||
#Categories.scrollable:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
width: var(--btcpay-space-m);
|
||||
}
|
||||
|
||||
#Categories.scrollable:before {
|
||||
background-image: linear-gradient(to right, var(--btcpay-body-bg), rgba(var(--btcpay-body-bg-rgb), 0));
|
||||
left: calc(var(--scroll-indicator-spacing) * -1);
|
||||
}
|
||||
|
||||
#Categories.scrollable:after {
|
||||
background-image: linear-gradient(to left, var(--btcpay-body-bg), rgba(var(--btcpay-body-bg-rgb), 0));
|
||||
right: calc(var(--scroll-indicator-spacing) * -1);
|
||||
}
|
||||
|
||||
.cart-toggle-btn {
|
||||
--button-width: 40px;
|
||||
--button-height: 40px;
|
||||
|
@ -44,6 +44,7 @@ document.addEventListener("DOMContentLoaded",function () {
|
||||
displayCategory: '*',
|
||||
searchTerm: null,
|
||||
cart: loadState('cart'),
|
||||
categoriesScrollable: false,
|
||||
$cart: null
|
||||
}
|
||||
},
|
||||
@ -179,6 +180,28 @@ document.addEventListener("DOMContentLoaded",function () {
|
||||
},
|
||||
mounted() {
|
||||
this.$cart = new bootstrap.Offcanvas(this.$refs.cart, { backdrop: false })
|
||||
|
||||
if (this.$refs.categories) {
|
||||
const getInnerNavWidth = () => {
|
||||
// set to inline display, get width to get the real inner width, then set back to flex
|
||||
this.$refs.categoriesNav.classList.remove('d-flex');
|
||||
this.$refs.categoriesNav.classList.add('d-inline-flex');
|
||||
const navWidth = this.$refs.categoriesNav.clientWidth - 32; // 32 is the margin
|
||||
this.$refs.categoriesNav.classList.remove('d-inline-flex');
|
||||
this.$refs.categoriesNav.classList.add('d-flex');
|
||||
return navWidth;
|
||||
}
|
||||
const adjustCategories = () => {
|
||||
const navWidth = getInnerNavWidth();
|
||||
Vue.set(this, 'categoriesScrollable', this.$refs.categories.clientWidth < navWidth);
|
||||
const activeEl = document.querySelector('#Categories .btcpay-pills input:checked + label')
|
||||
if (activeEl) activeEl.scrollIntoView({ block: 'end', inline: 'center' })
|
||||
}
|
||||
window.addEventListener('resize', e => {
|
||||
debounce('resize', adjustCategories, 50)
|
||||
});
|
||||
adjustCategories();
|
||||
}
|
||||
|
||||
window.addEventListener('pagehide', () => {
|
||||
if (this.payButtonLoading) {
|
||||
|
Loading…
Reference in New Issue
Block a user