Skip to content

Commit 3ce26ea

Browse files
- show bundle base price
- configurable prices inc/exc tax - focus trap
1 parent 11959be commit 3ce26ea

File tree

6 files changed

+186
-42
lines changed

6 files changed

+186
-42
lines changed

src/Umbraco.Commerce.Cart/Client/src/apis/ucc.api.ts

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { processAddToCartButtons } from "../processors/ucc-add-to-cart-button.processor.ts";
2-
import { Cart, CartConfig } from "../types.ts";
2+
import {Cart, CartConfig, CartOptions} from "../types.ts";
33
import { UCC_CART_CONTEXT } from "../contexts/ucc.context.ts";
44
import { UccCartModalElement } from "../components/ucc-cart-modal.element.ts";
55
import { UccEvent } from "../events/ucc.event.ts";
@@ -15,9 +15,9 @@ export class UccApi {
1515
close_cart: 'Close Cart',
1616
checkout: 'Checkout',
1717
taxes: 'Taxes',
18-
shipping: 'Shipping',
19-
shipping_message: 'Calculated at Checkout',
18+
subtotal: 'Subtotal',
2019
total: 'Total',
20+
shipping_and_discounts_message: 'Calculate shipping and apply discounts during checkout',
2121
remove: 'Remove',
2222
cart_empty: 'Your cart is empty',
2323
},
@@ -57,7 +57,7 @@ export class UccApi {
5757
const observer = new MutationObserver((mutations) => {
5858
mutations.forEach((mutation) => {
5959
if (mutation.type === 'attributes' && mutation.attributeName === 'lang') {
60-
this._context.config.set({ lang: this._host.ownerDocument.documentElement.lang, ...this._context.config.get() });
60+
this._context.config.set({ ...this._context.config.get()!, lang: this._host.ownerDocument.documentElement.lang });
6161
}
6262
});
6363
});
@@ -102,27 +102,35 @@ export class UccApi {
102102

103103
public setStore = (store:string) => {
104104
const currentConfig = this._context.config.get()!;
105-
this._context.config.set({ store, ...currentConfig });
105+
this._context.config.set({ ...currentConfig, store });
106106
}
107107

108108
public setCheckoutUrl = (url:string) => {
109109
const currentConfig = this._context.config.get()!;
110-
this._context.config.set({ checkoutUrl: url, ...currentConfig });
110+
this._context.config.set({ ...currentConfig, checkoutUrl: url });
111111
}
112112

113113
public addLocale = (locale:string, values:Record<string, string>) => {
114114
const currentConfig = this._context.config.get()!;
115115
this._context.config.set({
116-
locales: { ...currentConfig.locales, [locale]: values },
117-
...currentConfig
116+
...currentConfig,
117+
locales: { ...currentConfig.locales, [locale]: values }
118118
});
119119
}
120120

121121
public showProperty = (property:string) => {
122122
const currentConfig = this._context.config.get()!;
123123
this._context.config.set({
124-
properties: [...currentConfig.properties!, property],
125-
...currentConfig
124+
...currentConfig,
125+
properties: [ ...(currentConfig.properties ?? []), property ]
126+
});
127+
}
128+
129+
public showPricesIncludingTax = (value:boolean) => {
130+
const currentConfig = this._context.config.get()!;
131+
this._context.config.set({
132+
...currentConfig,
133+
showPricesIncludingTax: value
126134
});
127135
}
128136

src/Umbraco.Commerce.Cart/Client/src/components/ucc-cart-modal.element.ts

Lines changed: 62 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -27,43 +27,55 @@ export class UccCartModalElement extends UccModalElement
2727
private _observerContext = () => {
2828
this._context.config.subscribe((config: CartConfig) => {
2929
if (config) {
30+
3031
this.setTitle(config.locales![config.lang].cart_title);
3132
this.setCloseButtonLabel(config.locales![config.lang].close_cart);
32-
33+
34+
this._host.querySelector<HTMLElement>('.ucc-cart-total--subtotal .ucc-cart-total-label')!.textContent = config.locales![config.lang].subtotal;
3335
this._host.querySelector<HTMLElement>('.ucc-cart-total--taxes .ucc-cart-total-label')!.textContent = config.locales![config.lang].taxes;
34-
this._host.querySelector<HTMLElement>('.ucc-cart-total--shipping .ucc-cart-total-label')!.textContent = config.locales![config.lang].shipping;
35-
this._host.querySelector<HTMLElement>('.ucc-cart-total--shipping .ucc-cart-total-value')!.textContent = config.locales![config.lang].shipping_message;
36+
this._host.querySelector<HTMLElement>('.ucc-cart-message')!.textContent = config.locales![config.lang].shipping_and_discounts_message;
3637
this._host.querySelector<HTMLElement>('.ucc-cart-total--total .ucc-cart-total-label')!.textContent = config.locales![config.lang].total;
3738
this._host.querySelector<HTMLElement>('.ucc-cart-checkout')!.textContent = config.locales![config.lang].checkout;
3839
this._host.querySelector<HTMLElement>('.ucc-cart-checkout')!.setAttribute('href', config.checkoutUrl!);
3940

40-
const cartItemRemoveBtns = this._host.querySelectorAll('.ucc-cart-item__remove');
41-
cartItemRemoveBtns.forEach((btn) => {
42-
btn.setAttribute('title', config.locales![config.lang].remove);
43-
});
44-
4541
const cartEmptyMsg = this._host.querySelector<HTMLElement>('.ucc-cart-empty__message');
4642
if (cartEmptyMsg) {
4743
cartEmptyMsg.textContent = config.locales![config.lang].cart_empty;
4844
}
45+
46+
if (config.showPricesIncludingTax) {
47+
this._host.querySelector<HTMLElement>('.ucc-cart-totals')!.classList.add('ucc-cart-totals--inc-tax');
48+
} else {
49+
this._host.querySelector<HTMLElement>('.ucc-cart-totals')!.classList.remove('ucc-cart-totals--inc-tax');
50+
}
51+
52+
this._renderCart();
4953
}
5054
});
51-
this._context.cart.subscribe((cart: Cart) => {
52-
53-
const config = this._context.config.get()!;
54-
55-
if (cart && cart.items.length > 0) {
56-
this._host.querySelector('.ucc-cart-items')!.innerHTML = cart.items.map((item) => {
57-
const propsToDisplay = Object.keys(item.properties ?? {}).filter((x) => (config.properties ?? []).map(y => y.toLowerCase()).includes(x.toLowerCase()));
58-
return `
55+
this._context.cart.subscribe(() => {
56+
this._renderCart();
57+
});
58+
}
59+
60+
private _renderCart()
61+
{
62+
const config = this._context.config.get();
63+
const cart = this._context.cart.get();
64+
65+
if (!config || !cart) return;
66+
67+
if (cart && cart.items.length > 0) {
68+
this._host.querySelector('.ucc-cart-items')!.innerHTML = cart.items.map((item) => {
69+
const propsToDisplay = Object.keys(item.properties ?? {}).filter((x) => (config.properties ?? []).map(y => y.toLowerCase()).includes(x.toLowerCase()));
70+
return `
5971
<div class="ucc-cart-item" data-id="${ item.id }">
6072
${item.imageUrl ? `
6173
<figure class="ucc-cart-item__image">
6274
<img src="${item.imageUrl}" alt="${item.name}">
6375
</figure>` : ''}
6476
<div class="ucc-cart-item__body">
6577
<div class="ucc-cart-item__content ucc-split">
66-
<div class="ucc-split__left">
78+
<div class="ucc-split__left ucc-split__item--fill">
6779
<h3 class="ucc-cart-item__title">${item.name}</h3>
6880
${item.attributes ? `
6981
<div class="ucc-cart-item__attributes">
@@ -85,6 +97,22 @@ export class UccCartModalElement extends UccModalElement
8597
`).join('')}
8698
</div>
8799
` : ''}
100+
${item.items && item.items.length ? `
101+
<div class="ucc-cart-item__bundle">
102+
<div class="ucc-cart-item__bundle-item ucc-cart-item__bundle-item--base">
103+
<span class="ucc-cart-item__bundle-item-name">${config?.locales![config.lang].base_price ?? 'Base price'}</span>
104+
<span class="ucc-cart-item__bundle-item-quantity"></span>
105+
<span class="ucc-cart-item__bundle-item-quantity">${config.showPricesIncludingTax ? item.basePrice.withTax : item.basePrice.withoutTax}</span>
106+
</div>
107+
${item.items.map((bundleItem) => `
108+
<div class="ucc-cart-item__bundle-item">
109+
<span class="ucc-cart-item__bundle-item-name">${bundleItem.name}</span>
110+
<span class="ucc-cart-item__bundle-item-quantity">x${bundleItem.quantity}</span>
111+
<span class="ucc-cart-item__bundle-item-quantity">${config.showPricesIncludingTax ? bundleItem.total.withTax : bundleItem.total.withoutTax}</span>
112+
</div>
113+
`).join('')}
114+
</div>
115+
` : ''}
88116
</div>
89117
<div class="ucc-split__right">
90118
<button class="ucc-cart-item__remove" title="${config?.locales![config.lang].remove ?? 'Remove'}"><svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-trash-2"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg></button>
@@ -95,30 +123,32 @@ export class UccCartModalElement extends UccModalElement
95123
<input type="number" value="${item.quantity}" min="1" class="ucc-cart-item__quantity">
96124
</div>
97125
<div class="ucc-split__right">
98-
<span class="ucc-cart-item__price">${item.total.withTax}</span>
126+
<span class="ucc-cart-item__price">${config.showPricesIncludingTax ? item.total.withTax : item.total.withoutTax}</span>
99127
</div>
100128
</div>
101129
</div>
102130
</div>
103131
`
104-
}).join('');
105-
this._host.querySelector<HTMLElement>('.ucc-cart-totals')!.style.display = '';
106-
this._host.querySelector<HTMLElement>('.ucc-cart-total--taxes .ucc-cart-total-value')!.textContent = cart.subtotal.tax;
107-
this._host.querySelector<HTMLElement>('.ucc-cart-total--total .ucc-cart-total-value')!.textContent = cart.subtotal.withTax;
108-
this._host.querySelector<HTMLElement>('.ucc-cart-checkout')!.classList.remove('ucc-cart-checkout--disabled');
109-
} else {
110-
this._host.querySelector<HTMLElement>('.ucc-cart-items')!.innerHTML = `
132+
}).join('');
133+
this._host.querySelector<HTMLElement>('.ucc-cart-totals')!.style.display = '';
134+
this._host.querySelector<HTMLElement>('.ucc-cart-message')!.style.display = '';
135+
this._host.querySelector<HTMLElement>('.ucc-cart-total--subtotal .ucc-cart-total-value')!.textContent = cart.subtotal.withoutTax;
136+
this._host.querySelector<HTMLElement>('.ucc-cart-total--taxes .ucc-cart-total-value')!.textContent = cart.subtotal.tax;
137+
this._host.querySelector<HTMLElement>('.ucc-cart-total--total .ucc-cart-total-value')!.textContent = cart.subtotal.withTax;
138+
this._host.querySelector<HTMLElement>('.ucc-cart-checkout')!.classList.remove('ucc-cart-checkout--disabled');
139+
} else {
140+
this._host.querySelector<HTMLElement>('.ucc-cart-items')!.innerHTML = `
111141
<div class="ucc-cart-empty">
112142
<div>
113143
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-shopping-cart"><circle cx="8" cy="21" r="1"/><circle cx="19" cy="21" r="1"/><path d="M2.05 2.05h2l2.66 12.42a2 2 0 0 0 2 1.58h9.78a2 2 0 0 0 1.95-1.57l1.65-7.43H5.12"/></svg>
114144
<div class="ucc-cart-empty__message">${config?.locales![config.lang].cart_empty ?? 'Your cart is empty'}</div>
115145
</div>
116146
</div>
117147
`;
118-
this._host.querySelector<HTMLElement>('.ucc-cart-totals')!.style.display = 'none';
119-
this._host.querySelector<HTMLElement>('.ucc-cart-checkout')!.classList.add('ucc-cart-checkout--disabled');
120-
}
121-
});
148+
this._host.querySelector<HTMLElement>('.ucc-cart-totals')!.style.display = 'none';
149+
this._host.querySelector<HTMLElement>('.ucc-cart-message')!.style.display = 'none';
150+
this._host.querySelector<HTMLElement>('.ucc-cart-checkout')!.classList.add('ucc-cart-checkout--disabled');
151+
}
122152
}
123153

124154
private _addCartItemEventListener = (event:string, selector:string, handler:Function) => {
@@ -136,11 +166,11 @@ export class UccCartModalElement extends UccModalElement
136166
`)
137167
this.setFooter(`
138168
<div class="ucc-cart-totals">
139-
<div class="ucc-cart-totals__item ucc-cart-total ucc-cart-total--taxes ucc-split">
169+
<div class="ucc-cart-totals__item ucc-cart-total ucc-cart-total--subtotal ucc-split">
140170
<span class="ucc-cart-total-label ucc-split__left"></span>
141171
<span class="ucc-cart-total-value ucc-split__right"></span>
142172
</div>
143-
<div class="ucc-cart-totals__item ucc-cart-total ucc-cart-total--shipping ucc-split">
173+
<div class="ucc-cart-totals__item ucc-cart-total ucc-cart-total--taxes ucc-split">
144174
<span class="ucc-cart-total-label ucc-split__left"></span>
145175
<span class="ucc-cart-total-value ucc-split__right"></span>
146176
</div>
@@ -149,6 +179,7 @@ export class UccCartModalElement extends UccModalElement
149179
<span class="ucc-cart-total-value ucc-split__right"></span>
150180
</div>
151181
</div>
182+
<div class="ucc-cart-message">Calculate shipping and apply discounts during checkout</div>
152183
<a class="ucc-cart-checkout" href="#"></a>
153184
`)
154185

src/Umbraco.Commerce.Cart/Client/src/components/ucc-modal.element.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import {trapFocus} from "../utils.ts";
2+
13
export class UccModalElement
24
{
35
protected readonly _host: HTMLElement;
@@ -31,14 +33,28 @@ export class UccModalElement
3133
}
3234
}
3335

36+
private _escHandler = (e: KeyboardEvent) => {
37+
if (e.key === 'Escape') {
38+
this.close();
39+
}
40+
}
41+
42+
private _trapFocus = (e: KeyboardEvent) => {
43+
trapFocus(e, '.ucc-modal');
44+
}
45+
3446
public open = () => {
3547
this._host.querySelector('.ucc-modal-container')!.classList.add('ucc-modal-container--open');
3648
this._host.ownerDocument.body.style.overflow = 'hidden';
49+
this._host.ownerDocument.addEventListener('keydown', this._escHandler);
50+
this._host.ownerDocument.addEventListener('keydown', this._trapFocus);
3751
}
3852

3953
public close = () => {
4054
this._host.querySelector('.ucc-modal-container')!.classList.remove('ucc-modal-container--open');
4155
this._host.ownerDocument.body.style.overflow = '';
56+
this._host.ownerDocument.removeEventListener('keydown', this._escHandler);
57+
this._host.ownerDocument.removeEventListener('keydown', this._trapFocus);
4258
}
4359

4460
private _attachModalTemplate()

src/Umbraco.Commerce.Cart/Client/src/styles/styles.css

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@
146146
}
147147

148148
.ucc-cart-item__title {
149+
flex: 1;
149150
font-family: var(--ucc-font-family), sans-serif;
150151
font-weight: 600;
151152
font-size: var(--ucc-text-md);
@@ -202,6 +203,37 @@
202203
content: ':';
203204
}
204205

206+
.ucc-cart-item__bundle {
207+
display: table;
208+
width: 100%;
209+
margin-top: 20px;
210+
}
211+
212+
.ucc-cart-item__bundle-item {
213+
display: table-row;
214+
font-family: var(--ucc-font-family), sans-serif;
215+
font-size: var(--ucc-text-sm);
216+
color: var(--ucc-text-color-light);
217+
text-align: right;
218+
padding: 10px 0;
219+
}
220+
221+
.ucc-cart-item__bundle-item > * {
222+
display: table-cell;
223+
padding: 10px;
224+
border-top: 1px solid var(--ucc-border-color);
225+
}
226+
227+
.ucc-cart-item__bundle-item > *:first-child {
228+
text-align: left;
229+
width: 100%;
230+
padding-left: 0;
231+
}
232+
233+
.ucc-cart-item__bundle-item > *:last-child {
234+
padding-right: 0;
235+
}
236+
205237
.ucc-cart-item__price {
206238
color: var(--ucc-text-color-dark);
207239
font-weight: bold;
@@ -214,6 +246,14 @@
214246
margin-bottom: 10px;
215247
}
216248

249+
.ucc-cart-totals.ucc-cart-totals--inc-tax {
250+
flex-direction: column-reverse;
251+
}
252+
253+
.ucc-cart-totals.ucc-cart-totals--inc-tax .ucc-cart-total--subtotal {
254+
display: none;
255+
}
256+
217257
.ucc-cart-total {
218258
font-size: var(--ucc-text-md);
219259
color: var(--ucc-text-color);
@@ -224,6 +264,15 @@
224264
font-weight: bold;
225265
}
226266

267+
.ucc-cart-message {
268+
font-family: var(--ucc-font-family), sans-serif;
269+
font-size: var(--ucc-text-md);
270+
color: var(--ucc-text-color-light);
271+
margin-bottom: 10px;
272+
text-align: center;
273+
font-style: italic;
274+
}
275+
227276
.ucc-cart-checkout {
228277
cursor: pointer;
229278
display: block;
@@ -278,4 +327,8 @@
278327

279328
.ucc-split--center {
280329
align-items: center;
330+
}
331+
332+
.ucc-split__item--fill {
333+
flex: 1;
281334
}

src/Umbraco.Commerce.Cart/Client/src/types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,14 +22,21 @@ export type CartConfig = {
2222
lang: string
2323
properties?: string[]
2424
locales?: Record<string, Record<string, string>>
25+
showPricesIncludingTax?: boolean
2526
}
2627

2728
export type Cart = {
2829
id: string
29-
items: CartItem[]
30+
items: BundlableCartItem[]
3031
subtotal: FormattedPrice
3132
}
3233

34+
export type BundlableCartItem = CartItem & {
35+
bundleReference?: string
36+
items?: CartItem[]
37+
basePrice: FormattedPrice
38+
}
39+
3340
export type CartItem = {
3441
id: string
3542
productReference: string

0 commit comments

Comments
 (0)