POS Cart: Horizontal scrollable filters (#5391)

This commit is contained in:
d11n 2023-11-02 08:36:27 +01:00 committed by GitHub
parent e82281d273
commit c979c4774c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 196 additions and 125 deletions

View File

@ -28,8 +28,8 @@
} }
<div id="PosCart"> <div id="PosCart">
<div id="content" class="public-page-wrap"> <div id="content">
<div class="container-xl"> <div class="public-page-wrap container-xl">
<header class="sticky-top bg-body d-flex flex-column py-3 py-lg-4 gap-3"> <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"> <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> <h1 class="mb-0">@(string.IsNullOrEmpty(Model.Title) ? Model.StoreName : Model.Title)</h1>
@ -39,11 +39,13 @@
</button> </button>
</div> </div>
<input id="SearchTerm" class="form-control rounded-pill" placeholder="Search…" v-model="searchTerm"> <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"> <div id="Categories" ref="categories" v-if="allCategories" :class="{ 'scrollable': categoriesScrollable }">
<template v-for="cat in allCategories"> <nav class="btcpay-pills d-flex align-items-center gap-3" ref="categoriesNav">
<input :id="`Category-${cat.value}`" type="radio" name="category" autocomplete="off" v-model="displayCategory" :value="cat.value"> <template v-for="cat in allCategories">
<label :for="`Category-${cat.value}`" class="btcpay-pill">{{ cat.text }}</label> <input :id="`Category-${cat.value}`" type="radio" name="category" autocomplete="off" v-model="displayCategory" :value="cat.value">
</template> <label :for="`Category-${cat.value}`" class="btcpay-pill text-nowrap">{{ cat.text }}</label>
</template>
</nav>
</div> </div>
</header> </header>
<main> <main>
@ -122,126 +124,124 @@
</div> </div>
</div> </div>
<aside id="cart" ref="cart" tabindex="-1" aria-labelledby="cartLabel"> <aside id="cart" ref="cart" tabindex="-1" aria-labelledby="cartLabel">
<div class="public-page-wrap" v-cloak> <div class="public-page-wrap container-xl" 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">
<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>
<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">
<button id="CartClear" type="reset" v-on:click="clearCart" class="btn btn-text text-primary p-1" v-if="cartCount > 0"> Empty
Empty </button>
</button> <button id="CartClose" type="button" class="cart-toggle-btn" v-on:click="toggleCart" aria-controls="cart" aria-label="Close">
<button id="CartClose" type="button" class="cart-toggle-btn" v-on:click="toggleCart" aria-controls="cart" aria-label="Close"> <vc:icon symbol="close" />
<vc:icon symbol="close" /> </button>
</button> </header>
</header> <div class="offcanvas-body py-0">
<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">
<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="amount" :value="totalNumeric"> <input type="hidden" name="tip" :value="tipNumeric">
<input type="hidden" name="tip" :value="tipNumeric"> <input type="hidden" name="discount" :value="discountPercentNumeric">
<input type="hidden" name="discount" :value="discountPercentNumeric"> <input type="hidden" name="posdata" :value="posdata">
<input type="hidden" name="posdata" :value="posdata"> <table class="table table-borderless mt-0 mb-4">
<table class="table table-borderless mt-0 mb-4"> <tbody id="CartItems">
<tbody id="CartItems"> <tr v-for="item in cart" :key="item.id">
<tr v-for="item in cart" :key="item.id"> <td class="align-middle">
<td class="align-middle"> <h6 class="fw-semibold mb-1">{{ item.title }}</h6>
<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>
<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> <td class="align-middle">
<td class="align-middle"> <div class="d-flex align-items-center gap-2 justify-content-end quantity">
<div class="d-flex align-items-center gap-2 justify-content-end quantity"> <span class="badge text-bg-warning inventory" v-if="item.inventory">
<span class="badge text-bg-warning inventory" v-if="item.inventory"> {{ item.inventory > 0 ? `${item.inventory} left` : "Sold out" }}
{{ item.inventory > 0 ? `${item.inventory} left` : "Sold out" }} </span>
</span> <div class="d-flex align-items-center gap-2">
<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">
<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" />
<vc:icon symbol="minus" /> </button>
</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">
<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">
<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" />
<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 }}%
</button> </button>
</div> </div>
</th> </div>
</tr> </td>
</table> <td class="align-middle text-end">
<table class="table table-borderless mt-4 mb-0"> {{ formatCurrency(item.price||0, true) }}
<tr> </td>
<td class="align-middle">Subtotal</td> </tr>
<td class="align-middle text-end" id="CartAmount">{{ formatCurrency(amountNumeric, true) }}</td> </tbody>
</tr> </table>
<tr v-if="discountNumeric"> <table class="table table-borderless my-4" v-if="showDiscount || enableTips">
<td class="align-middle">Discount</td> <tr v-if="showDiscount">
<td class="align-middle text-end" id="CartDiscount"> <th class="align-middle">Discount</th>
<span v-if="discountPercent">{{discountPercent}}% =</span> <th class="align-middle" colspan="3">
{{ formatCurrency(discountNumeric, true) }} <div class="input-group input-group-sm w-100px pull-right">
</td> <input class="form-control hide-number-spin" type="number" min="0" step="1" max="100" id="Discount" v-model.number="discountPercent">
</tr> <span class="input-group-text">%</span>
<tr v-if="tipNumeric"> </div>
<td class="align-middle">Tip</td> </th>
<td class="align-middle text-end" id="CartTip"> </tr>
<span v-if="tipPercent">{{tipPercent}}% =</span> <tr v-if="enableTips">
{{ formatCurrency(tipNumeric, true) }} <th class="align-middle">Tip</th>
</td> <th class="align-middle" colspan="3">
</tr> <div class="btcpay-pills d-flex flex-wrap align-items-center justify-content-end gap-2" v-if="customTipPercentages">
<tr> <div class="btcpay-pill d-flex align-items-center px-3" id="Tip-Custom" :class="{ active: !tipPercent && tip }" v-on:click.prevent="tipPercent = null">
<td class="align-middle h5 border-0">Total</td> <input
<td class="align-middle h5 border-0 text-end" id="CartTotal">{{ formatCurrency(totalNumeric, true) }}</td> v-model.number="tip"
</tr> class="form-control hide-number-spin shadow-none text-reset d-block bg-transparent border-0 p-0 me-1 fw-semibold"
<tr> style="height:1.5em;min-height:auto;width:4ch"
<td colspan="2" class="pt-4"> type="number"
<button id="CartSubmit" class="btn btn-primary btn-lg w-100" :disabled="payButtonLoading" type="submit"> min="0"
<div v-if="payButtonLoading" class="spinner-border spinner-border-sm" id="pay-button-spinner" role="status"> step="@Model.Step" />
<span class="sr-only">Loading...</span> <span>@(Model.CurrencyInfo.CurrencySymbol ?? Model.CurrencyCode)</span>
</div> </div>
<template v-else>Pay</template> <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> </button>
</td> </div>
</tr> </th>
</table> </tr>
</form> </table>
<p id="CartItems" v-else class="text-muted text-center my-0">There are no items in your cart yet.</p> <table class="table table-borderless mt-4 mb-0">
</div> <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>
</div> </div>
</aside> </aside>

View File

@ -3,7 +3,13 @@
} }
#PosCart .public-page-wrap { #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 { #PosCart .offcanvas-backdrop {
@ -11,6 +17,48 @@
transition-duration: var(--btcpay-transition-duration-fast); 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 { .cart-toggle-btn {
--button-width: 40px; --button-width: 40px;
--button-height: 40px; --button-height: 40px;

View File

@ -44,6 +44,7 @@ document.addEventListener("DOMContentLoaded",function () {
displayCategory: '*', displayCategory: '*',
searchTerm: null, searchTerm: null,
cart: loadState('cart'), cart: loadState('cart'),
categoriesScrollable: false,
$cart: null $cart: null
} }
}, },
@ -179,6 +180,28 @@ document.addEventListener("DOMContentLoaded",function () {
}, },
mounted() { mounted() {
this.$cart = new bootstrap.Offcanvas(this.$refs.cart, { backdrop: false }) 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', () => { window.addEventListener('pagehide', () => {
if (this.payButtonLoading) { if (this.payButtonLoading) {