Skip to content

Commit

Permalink
Merge pull request #3 from lorenzofox3/spa__module_cart
Browse files Browse the repository at this point in the history
singale page app cart and chart module
  • Loading branch information
lorenzofox3 authored Feb 15, 2024
2 parents 14a06e0 + 62d7966 commit a5ec56c
Show file tree
Hide file tree
Showing 45 changed files with 1,672 additions and 440 deletions.
17 changes: 8 additions & 9 deletions apps/restaurant-cashier/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ export const createApp = ({ router }) => {
usePageLoader({ pagePath: '/users/me.page.js' }),
])
.addRoute({ pattern: 'dashboard' }, [
usePageLoader({ pagePath: '/not-available.page.js' }),
usePageLoader({ pagePath: '/dashboard/dashboard.page.js' }),
])
.addRoute({ pattern: 'products' }, [
usePageLoader({ pagePath: '/products/list/product-list.page.js' }),
Expand All @@ -108,20 +108,19 @@ export const createApp = ({ router }) => {
.addRoute({ pattern: 'products/:product-sku' }, [
usePageLoader({ pagePath: '/products/edit/edit-product.page.js' }),
])
.addRoute({ pattern: 'sales' }, [
usePageLoader({ pagePath: '/not-available.page.js' }),
.addRoute({ pattern: 'cart' }, [
usePageLoader({ pagePath: '/cart/cart.page.js' }),
])
.notFound(() => {
router.redirect('/products');
router.redirect('/dashboard');
});

// trigger initial preference state
preferencesService.emit({
type: preferencesEvents.PREFERENCES_CHANGED,
});

return {
start() {
// trigger initial preference state
preferencesService.emit({
type: preferencesEvents.PREFERENCES_CHANGED,
});
router.redirect(location.pathname + location.search + location.hash);
},
};
Expand Down
27 changes: 27 additions & 0 deletions apps/restaurant-cashier/cart/cart-product-item.component.js
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 apps/restaurant-cashier/cart/cart-product-list.component.js
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,
});
81 changes: 81 additions & 0 deletions apps/restaurant-cashier/cart/cart.component.js
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> `;
};
};
26 changes: 26 additions & 0 deletions apps/restaurant-cashier/cart/cart.controller.js
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 });
154 changes: 154 additions & 0 deletions apps/restaurant-cashier/cart/cart.css
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;
}
}
Loading

0 comments on commit a5ec56c

Please sign in to comment.