-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3 from lorenzofox3/spa__module_cart
singale page app cart and chart module
- Loading branch information
Showing
45 changed files
with
1,672 additions
and
440 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
27 changes: 27 additions & 0 deletions
27
apps/restaurant-cashier/cart/cart-product-item.component.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { compose } from '../utils/functions.js'; | ||
import { reactiveProps } from '../utils/components.js'; | ||
import { withView } from '@cofn/view'; | ||
|
||
const compositionPipeline = compose([reactiveProps(['product']), withView]); | ||
|
||
export const CartProductItem = compositionPipeline(({ html, $host }) => { | ||
return ({ product }) => { | ||
if (product.image?.url) { | ||
$host.style.setProperty('background-image', `url(${product.image.url})`); | ||
} | ||
|
||
return html` | ||
<div class="text-ellipsis text">${product.title}</div> | ||
<div aria-hidden="true" class="adorner"> | ||
<ui-icon name="check"></ui-icon> | ||
</div> | ||
<div aria-hidden="true" class="text"> | ||
<span>#${product.sku}</span> | ||
<div> | ||
<span>${product.price.amountInCents / 100}</span> | ||
<span>${product.price.currency}</span> | ||
</div> | ||
</div> | ||
`; | ||
}; | ||
}); |
44 changes: 44 additions & 0 deletions
44
apps/restaurant-cashier/cart/cart-product-list.component.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
import { compose } from '../utils/functions.js'; | ||
|
||
export const CartProductList = ({ html, cartService }) => { | ||
const handleSelectionChange = compose([ | ||
cartService.setItemQuantity, | ||
cartItemFromOption, | ||
({ detail }) => detail.option, | ||
]); | ||
return ({ products: _products, currentCart }) => { | ||
const products = Object.values(_products); | ||
const cartProductSKUs = Object.keys(currentCart.items); | ||
return html` <h2 id="product-list-header" class="visually-hidden"> | ||
Available products | ||
</h2> | ||
<ul | ||
id="available-products-listbox" | ||
is="ui-listbox" | ||
aria-labelledby="product-list-header" | ||
aria-multiselectable="true" | ||
@selection-changed="${handleSelectionChange}" | ||
> | ||
${products.map( | ||
(product) => | ||
html`${product.sku}:: | ||
<li | ||
id="${'option-' + product.sku}" | ||
is="ui-listbox-option" | ||
class="boxed" | ||
value="${product.sku}" | ||
.selected="${cartProductSKUs.includes(product.sku)}" | ||
> | ||
<app-cart-product-item | ||
.product="${product}" | ||
></app-cart-product-item> | ||
</li>`, | ||
)} | ||
</ul>`; | ||
}; | ||
}; | ||
|
||
const cartItemFromOption = ({ value, selected }) => ({ | ||
sku: value, | ||
quantity: selected ? 1 : 0, | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,81 @@ | ||
const itemQuantityFromEvent = (ev) => { | ||
const nodes = ev.composedPath(); | ||
const skuEl = nodes.find((el) => el.dataset?.id !== undefined); | ||
const actionEl = nodes.find((el) => el.dataset?.action !== undefined); | ||
if (!skuEl || !actionEl) { | ||
return undefined; | ||
} | ||
|
||
const { quantity, id: sku } = skuEl.dataset; | ||
const { action } = actionEl.dataset; | ||
return { | ||
sku, | ||
quantity: Number(quantity) + (action === 'increment' ? 1 : -1), | ||
}; | ||
}; | ||
|
||
export const Cart = ({ html, cartService, $host }) => { | ||
setTimeout(cartService.fetchCurrent, 200); // todo | ||
|
||
$host.addEventListener('click', (ev) => { | ||
ev.stopPropagation(); | ||
const itemQuantity = itemQuantityFromEvent(ev); | ||
if (itemQuantity) { | ||
cartService.setItemQuantity(itemQuantity); | ||
} | ||
}); | ||
|
||
return ({ currentCart, products }) => { | ||
const cartProducts = Object.entries(currentCart.items).map( | ||
([sku, cartItem]) => ({ | ||
...products[sku], | ||
...cartItem, | ||
}), | ||
); | ||
|
||
const hasItem = cartProducts.length > 0; | ||
|
||
return html`<h2>Your cart</h2> | ||
<ul> | ||
${cartProducts.map(({ title, sku, quantity, price }) => { | ||
return html`${'cart-item-' + sku}:: | ||
<li data-id="${sku}" data-quantity="${quantity}"> | ||
<div class="text-ellipsis title"> | ||
${title}<small>ref - ${sku}</small> | ||
</div> | ||
<div class="quantity"> | ||
<button data-action="increment"> | ||
<ui-icon name="plus"></ui-icon | ||
><span class="visually-hidden">add</span> | ||
</button> | ||
<span>${quantity}</span> | ||
<button data-action="decrement"> | ||
<ui-icon name="dash"></ui-icon | ||
><span class="visually-hidden">remove</span> | ||
</button> | ||
</div> | ||
<span>${price.amountInCents / 100 + price.currency}</span> | ||
</li>`; | ||
})} | ||
</ul> | ||
${hasItem | ||
? html`<p> | ||
For a total of | ||
<strong | ||
>${currentCart.total.amountInCents / 100 + | ||
currentCart.total.currency} | ||
</strong> | ||
</p>` | ||
: html`<p>cart is currently empty</p>`} | ||
<div class="action-bar"> | ||
<button disabled="${!hasItem}"> | ||
<ui-icon name="cart-x"></ui-icon> | ||
abandon | ||
</button> | ||
<button disabled="${!hasItem}" class="action"> | ||
<ui-icon name="coin"></ui-icon> | ||
pay | ||
</button> | ||
</div> `; | ||
}; | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import { cartEvents, cartService } from './cart.service.js'; | ||
|
||
export const createCartController = | ||
({ cartService }) => | ||
(comp) => | ||
function* ({ $signal, $host, ...rest }) { | ||
const { render } = $host; | ||
$host.render = (args = {}) => | ||
render({ | ||
...args, | ||
...cartService.getState(), | ||
}); | ||
|
||
cartService.on(cartEvents.CART_CHANGED, () => $host.render(), { | ||
signal: $signal, | ||
}); | ||
|
||
yield* comp({ | ||
$host, | ||
$signal, | ||
cartService, | ||
...rest, | ||
}); | ||
}; | ||
|
||
export const withCartController = createCartController({ cartService }); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
#cart-container { | ||
display: flex; | ||
flex-wrap: wrap; | ||
gap: var(--spacing-big); | ||
align-items: flex-start; | ||
} | ||
|
||
app-cart { | ||
--_spacing: var(--spacing-small); | ||
|
||
position: sticky; | ||
top: 1em; | ||
z-index: 99; | ||
|
||
padding: var(--_spacing); | ||
min-width: min(25em, 100%); | ||
min-height: 12em; | ||
flex-grow: 1; | ||
|
||
display: flex; | ||
flex-direction: column; | ||
gap: var(--_spacing); | ||
|
||
h2 { | ||
margin: 0; | ||
} | ||
|
||
.quantity { | ||
display: flex; | ||
align-items: flex-start; | ||
gap: 0.5em; | ||
} | ||
|
||
small { | ||
display: block; | ||
} | ||
|
||
ul { | ||
list-style: none; | ||
font-size: 0.85em; | ||
flex-grow: 1; | ||
padding: 0; | ||
display: grid; | ||
gap: 0.2em 1em; | ||
grid-template-columns: 1fr 5em minmax(4em, min-content); | ||
align-content: start; | ||
} | ||
|
||
li { | ||
display: grid; | ||
grid-column: 1 / -1; | ||
grid-template-columns: subgrid; | ||
align-items: center; | ||
border-bottom: 1px solid var(--form-border-color); | ||
|
||
> :last-child { | ||
margin-left: auto; | ||
} | ||
|
||
} | ||
|
||
p { | ||
text-align: right; | ||
font-size: 0.9em; | ||
} | ||
} | ||
|
||
app-cart-product-list { | ||
flex-grow: 3; | ||
|
||
#available-products-listbox { | ||
--_min-item-size:200px; | ||
list-style: none; | ||
padding: 0; | ||
display: grid; | ||
grid-template-columns: repeat(auto-fit, minmax(var(--_min-item-size), 1fr)); | ||
gap: var(--spacing-big); | ||
|
||
[role=option] { | ||
display: flex; | ||
flex-direction: column; | ||
cursor: pointer; | ||
outline: none; | ||
|
||
&[aria-selected=true] .adorner { | ||
--_mark-scale: 1; | ||
} | ||
|
||
&:where(:hover, :focus-visible) .adorner { | ||
--_accent-color: var(--shadow-color); | ||
--_mark-offset: 4px; | ||
} | ||
} | ||
} | ||
} | ||
|
||
|
||
.adorner { | ||
--_color: currentColor; | ||
--_accent-color: transparent; | ||
--_mark-scale: 0; | ||
--_mark-size: 2.2em; | ||
--_mark-offset: 0; | ||
|
||
position: relative; | ||
isolation: isolate; | ||
display: grid; | ||
place-items: center; | ||
|
||
|
||
&::after { | ||
content: ''; | ||
z-index: -1; | ||
position: absolute; | ||
inset: 0; | ||
margin: auto; | ||
width: var(--_mark-size); | ||
height: var(--_mark-size); | ||
border-radius: 50%; | ||
background-color: var(--_accent-color); | ||
border: 2px solid var(--_color); | ||
transition: all var(--animation-duration); | ||
box-shadow: 0 0 3px 0 black; | ||
outline: 1px solid var(--_accent-color); | ||
outline-offset: var(--_mark-offset); | ||
} | ||
|
||
> ui-icon { | ||
--size: 1.6em; | ||
transform: scale(var(--_mark-scale), var(--_mark-scale)); | ||
transition: transform var(--animation-duration); | ||
} | ||
} | ||
|
||
app-cart-product-item { | ||
font-size: 0.8em; | ||
display: grid; | ||
grid-template-rows: auto minmax(3em, 1fr) auto; | ||
background-size: cover; | ||
background-repeat: no-repeat; | ||
background-position: center; | ||
|
||
> * { | ||
padding: var(--spacing-small); | ||
background-color: rgba(10, 40, 70, 0.65); | ||
color: white; | ||
} | ||
|
||
:last-child { | ||
display: flex; | ||
align-items: center; | ||
justify-content: space-between; | ||
} | ||
} |
Oops, something went wrong.