feature/book-page #4

Merged
rubberducky merged 4 commits from feature/book-page into main 2026-01-16 08:46:04 +00:00
12 changed files with 761 additions and 75 deletions
Showing only changes of commit 5755d43cfc - Show all commits

View File

@@ -51,6 +51,15 @@
categories="Zen|#,Integrale spiritualiteit|#" categories="Zen|#,Integrale spiritualiteit|#"
ebook-available ebook-available
></book-details> ></book-details>
<action-links-list>
<icon-link-button icon="wishlist" href="#">
Voeg toe aan verlanglijstje
</icon-link-button>
<icon-link-button icon="review" href="#">
Schrijf een recensie
</icon-link-button>
</action-links-list>
</section> </section>
<div class="content-padding"> <div class="content-padding">

View File

@@ -3,6 +3,9 @@
* Imports and registers all web components * Imports and registers all web components
*/ */
// Import cart store (must be first to set up window.cartStore)
import cart from "./store/cart.js";
// Import all components // Import all components
import "./components/site-header.js"; import "./components/site-header.js";
import "./components/top-bar.js"; import "./components/top-bar.js";
@@ -20,6 +23,9 @@ import "./components/cta-button.js";
import "./components/category-card.js"; import "./components/category-card.js";
import "./components/newsletter-signup.js"; import "./components/newsletter-signup.js";
import "./components/book-details.js"; import "./components/book-details.js";
import "./components/icon-cta-button.js";
import "./components/icon-link-button.js";
import "./components/action-links-list.js";
// Import icon components // Import icon components
import "./icons/menu-icon.js"; import "./icons/menu-icon.js";
@@ -30,7 +36,56 @@ import "./icons/book-open-icon.js";
import "./icons/clipboard-icon.js"; import "./icons/clipboard-icon.js";
import "./icons/chevron-down-icon.js"; import "./icons/chevron-down-icon.js";
// App initialization (if needed) // App initialization
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
console.log("BookStore app initialized"); console.log("BookStore app initialized");
// Initialize cart badge on page load
const count = cart.getItemCount();
if (count > 0) {
window.dispatchEvent(
new CustomEvent("cart-updated", {
detail: {
items: cart.getItems(),
count: count,
total: cart.getTotal(),
},
})
);
}
// Listen for add-to-cart events from book-card and book-details components
document.addEventListener("add-to-cart", (event) => {
const { title, author, price, type, image } = event.detail || {};
if (title) {
cart.addItem({
title,
author: author || "",
price: price || "€ 0,00",
type: type || "physical",
image: image || "",
});
// Optional: Show feedback to user
console.log(`Added "${title}" to cart`);
}
});
// Listen for buy-ebook events from book-details component
document.addEventListener("buy-ebook", (event) => {
const { title } = event.detail || {};
if (title) {
cart.addItem({
title,
author: "",
price: "€ 0,00", // eBook price would come from component
type: "ebook",
image: "",
});
console.log(`Added eBook "${title}" to cart`);
}
});
}); });

View File

@@ -0,0 +1,36 @@
/**
* Action Links List Component
* A container for icon link buttons with dividers
*/
class ActionLinksList extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: "open" });
}
connectedCallback() {
this.render();
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
}
.action-links-list {
display: flex;
flex-direction: column;
margin-top: var(--spacing-md, 1rem);
gap: var(--spacing-md, 1rem);
}
</style>
<div class="action-links-list">
<slot></slot>
</div>
`;
}
}
customElements.define("action-links-list", ActionLinksList);

View File

@@ -2,7 +2,6 @@
* Book Details Component * Book Details Component
* Displays book information with cover, metadata, and purchase buttons * Displays book information with cover, metadata, and purchase buttons
*/ */
import { shoppingBagIcon } from "../icons/shopping-bag.js";
class BookDetails extends HTMLElement { class BookDetails extends HTMLElement {
static get observedAttributes() { static get observedAttributes() {
@@ -43,7 +42,7 @@ class BookDetails extends HTMLElement {
const favoriteBtn = this.shadowRoot?.querySelector(".favorite-btn"); const favoriteBtn = this.shadowRoot?.querySelector(".favorite-btn");
if (addToCartBtn) { if (addToCartBtn) {
addToCartBtn.addEventListener("click", () => { addToCartBtn.addEventListener("button-click", () => {
this.dispatchEvent( this.dispatchEvent(
new CustomEvent("add-to-cart", { new CustomEvent("add-to-cart", {
bubbles: true, bubbles: true,
@@ -59,7 +58,7 @@ class BookDetails extends HTMLElement {
} }
if (ebookBtn) { if (ebookBtn) {
ebookBtn.addEventListener("click", () => { ebookBtn.addEventListener("button-click", () => {
this.dispatchEvent( this.dispatchEvent(
new CustomEvent("buy-ebook", { new CustomEvent("buy-ebook", {
bubbles: true, bubbles: true,
@@ -325,50 +324,6 @@ class BookDetails extends HTMLElement {
flex-direction: column; flex-direction: column;
gap: var(--spacing-sm, 0.5rem); gap: var(--spacing-sm, 0.5rem);
} }
.btn {
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-sm, 0.5rem);
padding: 14px var(--spacing-md, 1rem);
border: none;
border-radius: var(--radius-lg, 0.75rem);
font-family: var(--font-family-outfit, "Outfit", sans-serif);
font-size: 16px;
font-weight: 400;
line-height: 24px;
cursor: pointer;
transition: opacity var(--transition-fast, 150ms ease);
}
.btn:hover {
opacity: 0.9;
}
.btn:active {
opacity: 0.8;
}
.btn-cart {
background-color: var(--color-purple, #951d51);
color: var(--color-text-inverse, #ffffff);
}
.btn-cart svg {
width: 20px;
height: 20px;
}
.btn-ebook {
background-color: var(--color-push-box-bg, #EBEEF4);
color: var(--color-text, #1e293b);
}
.btn-ebook svg {
width: 20px;
height: 20px;
}
</style> </style>
<div class="book-details"> <div class="book-details">
@@ -433,19 +388,15 @@ class BookDetails extends HTMLElement {
<!-- Buttons --> <!-- Buttons -->
<div class="buttons"> <div class="buttons">
<button class="btn btn-cart"> <icon-cta-button class="btn-cart" icon="cart" variant="primary">
${shoppingBagIcon({ size: 20, color: "#ffffff" })} In winkelwagen
<span>In winkelwagen</span> </icon-cta-button>
</button>
${ ${
this.ebookAvailable this.ebookAvailable
? ` ? `
<button class="btn btn-ebook"> <icon-cta-button class="btn-ebook" icon="ebook" variant="secondary">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> Koop eBook
<path d="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25"></path> </icon-cta-button>
</svg>
<span>Koop eBook</span>
</button>
` `
: "" : ""
} }

View File

@@ -0,0 +1,152 @@
/**
* Icon CTA Button Component
* Full-width button with icon and text, supports primary (purple) and secondary (gray) variants
* Extends the CTA button pattern with icon support
*/
import { cartIcon } from "../icons/cart-icon.js";
import { ebookIcon } from "../icons/ebook-icon.js";
class IconCtaButton extends HTMLElement {
static get observedAttributes() {
return ["href", "variant", "icon"];
}
constructor() {
super();
this.attachShadow({ mode: "open" });
}
connectedCallback() {
this.render();
this.setupEventListeners();
}
attributeChangedCallback() {
if (this.shadowRoot) {
this.render();
this.setupEventListeners();
}
}
setupEventListeners() {
const button = this.shadowRoot?.querySelector(".icon-cta-button");
if (button && !this.hasAttribute("href")) {
button.addEventListener("click", () => {
this.dispatchEvent(
new CustomEvent("button-click", {
bubbles: true,
composed: true,
})
);
});
}
}
get href() {
return this.getAttribute("href") || "";
}
get variant() {
return this.getAttribute("variant") || "primary"; // 'primary' or 'secondary'
}
get icon() {
return this.getAttribute("icon") || ""; // 'cart', 'ebook', or empty
}
getIconHtml() {
const isPrimary = this.variant === "primary";
const iconColor = isPrimary ? "#ffffff" : "currentColor";
switch (this.icon) {
case "cart":
return cartIcon({ size: 20, color: iconColor });
case "ebook":
return ebookIcon({ size: 20, color: iconColor });
default:
return "";
}
}
render() {
const isLink = this.hasAttribute("href") && this.href;
const tag = isLink ? "a" : "button";
const hrefAttr = isLink ? `href="${this.href}"` : "";
const typeAttr = isLink ? "" : 'type="button"';
const isPrimary = this.variant === "primary";
const iconHtml = this.getIconHtml();
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
width: 100%;
}
.icon-cta-button {
display: flex;
align-items: center;
justify-content: center;
gap: var(--spacing-sm, 0.5rem);
width: 100%;
padding: 14px var(--spacing-md, 1rem);
border: none;
border-radius: var(--radius-sm, 0.25rem);
font-family: var(--font-family-outfit, "Outfit", sans-serif);
font-size: 16px;
font-weight: 400;
line-height: 24px;
text-decoration: none;
cursor: pointer;
transition: opacity var(--transition-fast, 150ms ease);
}
.icon-cta-button:hover {
opacity: 0.9;
}
.icon-cta-button:active {
opacity: 0.8;
}
.icon-cta-button.primary {
background-color: var(--color-purple, #951d51);
color: var(--color-text-inverse, #ffffff);
}
.icon-cta-button.secondary {
background-color: var(--color-push-box-bg, #EBEEF4);
color: var(--color-text, #1e293b);
}
.icon {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.icon svg {
width: 20px;
height: 20px;
}
.button-text {
text-decoration: none;
}
</style>
<${tag}
class="icon-cta-button ${isPrimary ? "primary" : "secondary"}"
${hrefAttr}
${typeAttr}
>
${iconHtml ? `<span class="icon">${iconHtml}</span>` : ""}
<span class="button-text">
<slot></slot>
</span>
</${tag}>
`;
}
}
customElements.define("icon-cta-button", IconCtaButton);

View File

@@ -0,0 +1,130 @@
/**
* Icon Link Button Component
* A text link with an icon on the left
* Used for actions like "Add to wishlist", "Write a review"
*/
import { wishlistIcon } from "../icons/wishlist-icon.js";
import { reviewIcon } from "../icons/review-icon.js";
class IconLinkButton extends HTMLElement {
static get observedAttributes() {
return ["href", "icon"];
}
constructor() {
super();
this.attachShadow({ mode: "open" });
}
connectedCallback() {
this.render();
this.setupEventListeners();
}
attributeChangedCallback() {
if (this.shadowRoot) {
this.render();
this.setupEventListeners();
}
}
setupEventListeners() {
const button = this.shadowRoot?.querySelector(".icon-link-button");
if (button && !this.hasAttribute("href")) {
button.addEventListener("click", () => {
this.dispatchEvent(
new CustomEvent("link-click", {
bubbles: true,
composed: true,
})
);
});
}
}
get href() {
return this.getAttribute("href") || "";
}
get icon() {
return this.getAttribute("icon") || "";
}
getIconHtml() {
switch (this.icon) {
case "wishlist":
return wishlistIcon({ size: 24, color: "currentColor" });
case "review":
return reviewIcon({ size: 24, color: "currentColor" });
default:
return "";
}
}
render() {
const isLink = this.hasAttribute("href") && this.href;
const tag = isLink ? "a" : "button";
const hrefAttr = isLink ? `href="${this.href}"` : "";
const typeAttr = isLink ? "" : 'type="button"';
const iconHtml = this.getIconHtml();
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
}
.icon-link-button {
display: flex;
align-items: center;
gap: var(--spacing-md, 1rem);
background: none;
border: none;
font-family: var(--font-family-outfit, "Outfit", sans-serif);
font-size: 16px;
font-weight: 400;
line-height: 24px;
color: var(--color-text, #1e293b);
text-decoration: none;
cursor: pointer;
transition: opacity var(--transition-fast, 150ms ease);
}
.icon-link-button:hover {
opacity: 0.7;
}
.icon {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
width: 24px;
height: 24px;
}
.icon svg {
width: 24px;
height: 24px;
}
.button-text {
text-decoration: underline;
text-underline-offset: 3px;
}
</style>
<${tag}
class="icon-link-button"
${hrefAttr}
${typeAttr}
>
${iconHtml ? `<span class="icon">${iconHtml}</span>` : ""}
<span class="button-text">
<slot></slot>
</span>
</${tag}>
`;
}
}
customElements.define("icon-link-button", IconLinkButton);

31
js/icons/cart-icon.js Normal file
View File

@@ -0,0 +1,31 @@
/**
* Cart Icon (Lucide style shopping cart)
* @param {Object} props - Icon properties
* @param {number} props.size - Icon size (default: 20)
* @param {string} props.color - Icon color (default: currentColor)
* @param {number} props.strokeWidth - Stroke width (default: 2)
* @returns {string} SVG string
*/
export function cartIcon({
size = 20,
color = "currentColor",
strokeWidth = 2,
} = {}) {
return `
<svg
xmlns="http://www.w3.org/2000/svg"
width="${size}"
height="${size}"
viewBox="0 0 24 24"
fill="none"
stroke="${color}"
stroke-width="${strokeWidth}"
stroke-linecap="round"
stroke-linejoin="round"
>
<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>
`;
}

29
js/icons/ebook-icon.js Normal file
View File

@@ -0,0 +1,29 @@
/**
* eBook Icon (Open book style)
* @param {Object} props - Icon properties
* @param {number} props.size - Icon size (default: 20)
* @param {string} props.color - Icon color (default: currentColor)
* @param {number} props.strokeWidth - Stroke width (default: 2)
* @returns {string} SVG string
*/
export function ebookIcon({
size = 20,
color = "currentColor",
strokeWidth = 2,
} = {}) {
return `
<svg
xmlns="http://www.w3.org/2000/svg"
width="${size}"
height="${size}"
viewBox="0 0 24 24"
fill="none"
stroke="${color}"
stroke-width="${strokeWidth}"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25"/>
</svg>
`;
}

31
js/icons/review-icon.js Normal file
View File

@@ -0,0 +1,31 @@
/**
* Review Icon (person with star)
* @param {Object} props - Icon properties
* @param {number} props.size - Icon size (default: 24)
* @param {string} props.color - Icon color (default: currentColor)
* @param {number} props.strokeWidth - Stroke width (default: 2)
* @returns {string} SVG string
*/
export function reviewIcon({
size = 24,
color = "currentColor",
strokeWidth = 2,
} = {}) {
return `
<svg
xmlns="http://www.w3.org/2000/svg"
width="${size}"
height="${size}"
viewBox="0 0 24 24"
fill="none"
stroke="${color}"
stroke-width="${strokeWidth}"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="9" cy="7" r="4"/>
<path d="M3 21v-2a4 4 0 0 1 4-4h4a4 4 0 0 1 4 4v2"/>
<polygon points="19 8 20.5 11 24 11.5 21.5 14 22 17.5 19 16 16 17.5 16.5 14 14 11.5 17.5 11 19 8"/>
</svg>
`;
}

View File

@@ -1,19 +1,49 @@
/** /**
* Shopping Bag Icon Web Component * Shopping Bag Icon Web Component
* A reusable shopping bag/cart icon element * A reusable shopping bag/cart icon element with optional badge count
*/ */
class ShoppingBagIcon extends HTMLElement { class ShoppingBagIcon extends HTMLElement {
static get observedAttributes() { static get observedAttributes() {
return ["size", "color", "stroke-width"]; return ["size", "color", "stroke-width", "count"];
} }
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: "open" }); this.attachShadow({ mode: "open" });
this.handleCartUpdate = this.handleCartUpdate.bind(this);
} }
connectedCallback() { connectedCallback() {
this.render(); this.render();
// Listen for cart updates
window.addEventListener("cart-updated", this.handleCartUpdate);
// Initialize count from cart store if available
this.initializeCount();
}
disconnectedCallback() {
window.removeEventListener("cart-updated", this.handleCartUpdate);
}
initializeCount() {
// Wait for cart store to be available
setTimeout(() => {
if (window.cartStore) {
const count = window.cartStore.getItemCount();
if (count > 0) {
this.setAttribute("count", count.toString());
}
}
}, 0);
}
handleCartUpdate(event) {
const { count } = event.detail;
if (count > 0) {
this.setAttribute("count", count.toString());
} else {
this.removeAttribute("count");
}
} }
attributeChangedCallback() { attributeChangedCallback() {
@@ -32,18 +62,58 @@ class ShoppingBagIcon extends HTMLElement {
return this.getAttribute("stroke-width") || "2"; return this.getAttribute("stroke-width") || "2";
} }
get count() {
const countAttr = this.getAttribute("count");
return countAttr ? parseInt(countAttr, 10) : 0;
}
render() { render() {
const showBadge = this.count > 0;
const displayCount = this.count > 99 ? "99+" : this.count.toString();
this.shadowRoot.innerHTML = ` this.shadowRoot.innerHTML = `
<style> <style>
:host { :host {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
position: relative;
} }
.icon-wrapper {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
}
svg { svg {
display: block; display: block;
} }
.badge {
position: absolute;
top: -6px;
right: -8px;
min-width: 18px;
height: 18px;
padding: 0 5px;
color: var(--color-purple, #951d51);
background-color: #ffffff;
font-family: var(--font-family-outfit, "Outfit", sans-serif);
font-size: 11px;
font-weight: 700;
line-height: 18px;
text-align: center;
border-radius: 9px;
box-sizing: border-box;
}
.badge.hidden {
display: none;
}
</style> </style>
<div class="icon-wrapper">
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
width="${this.size}" width="${this.size}"
@@ -59,6 +129,8 @@ class ShoppingBagIcon extends HTMLElement {
<circle cx="19" cy="21" r="1"></circle> <circle cx="19" cy="21" r="1"></circle>
<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"></path> <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"></path>
</svg> </svg>
<span class="badge ${showBadge ? "" : "hidden"}">${displayCount}</span>
</div>
`; `;
} }
} }

32
js/icons/wishlist-icon.js Normal file
View File

@@ -0,0 +1,32 @@
/**
* Wishlist Icon (tablet with heart)
* @param {Object} props - Icon properties
* @param {number} props.size - Icon size (default: 24)
* @param {string} props.color - Icon color (default: currentColor)
* @param {number} props.strokeWidth - Stroke width (default: 2)
* @returns {string} SVG string
*/
export function wishlistIcon({
size = 24,
color = "currentColor",
strokeWidth = 2,
} = {}) {
return `
<svg
xmlns="http://www.w3.org/2000/svg"
width="${size}"
height="${size}"
viewBox="0 0 24 24"
fill="none"
stroke="${color}"
stroke-width="${strokeWidth}"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="5" y="2" width="14" height="20" rx="2" ry="2"/>
<path d="M12 18h.01"/>
<path d="M12 8l1.5 1.5L12 11l-1.5-1.5L12 8z" fill="${color}" stroke="none"/>
<path d="M12 6.5c-.5-.5-1.5-.5-2 0s-.5 1.5 0 2l2 2 2-2c.5-.5.5-1.5 0-2s-1.5-.5-2 0"/>
</svg>
`;
}

158
js/store/cart.js Normal file
View File

@@ -0,0 +1,158 @@
/**
* Cart Store
* Manages shopping cart state with localStorage persistence
* Dispatches 'cart-updated' events when cart changes
*/
const CART_STORAGE_KEY = "milinda-cart";
class CartStore {
constructor() {
this.items = this.loadFromStorage();
}
/**
* Load cart from localStorage
*/
loadFromStorage() {
try {
const stored = localStorage.getItem(CART_STORAGE_KEY);
return stored ? JSON.parse(stored) : [];
} catch (e) {
console.error("Failed to load cart from storage:", e);
return [];
}
}
/**
* Save cart to localStorage
*/
saveToStorage() {
try {
localStorage.setItem(CART_STORAGE_KEY, JSON.stringify(this.items));
} catch (e) {
console.error("Failed to save cart to storage:", e);
}
}
/**
* Dispatch cart-updated event
*/
notifyUpdate() {
window.dispatchEvent(
new CustomEvent("cart-updated", {
detail: {
items: this.items,
count: this.getItemCount(),
total: this.getTotal(),
},
})
);
}
/**
* Add item to cart
* @param {Object} item - Item to add { title, author, price, type, image }
*/
addItem(item) {
// Check if item already exists (by title and type)
const existingIndex = this.items.findIndex(
(i) => i.title === item.title && i.type === item.type
);
if (existingIndex >= 0) {
// Increment quantity
this.items[existingIndex].quantity += 1;
} else {
// Add new item
this.items.push({
...item,
quantity: 1,
addedAt: Date.now(),
});
}
this.saveToStorage();
this.notifyUpdate();
return this.items;
}
/**
* Remove item from cart
* @param {number} index - Index of item to remove
*/
removeItem(index) {
if (index >= 0 && index < this.items.length) {
this.items.splice(index, 1);
this.saveToStorage();
this.notifyUpdate();
}
return this.items;
}
/**
* Update item quantity
* @param {number} index - Index of item
* @param {number} quantity - New quantity
*/
updateQuantity(index, quantity) {
if (index >= 0 && index < this.items.length) {
if (quantity <= 0) {
this.removeItem(index);
} else {
this.items[index].quantity = quantity;
this.saveToStorage();
this.notifyUpdate();
}
}
return this.items;
}
/**
* Clear all items from cart
*/
clear() {
this.items = [];
this.saveToStorage();
this.notifyUpdate();
return this.items;
}
/**
* Get all items
*/
getItems() {
return this.items;
}
/**
* Get total item count (sum of quantities)
*/
getItemCount() {
return this.items.reduce((sum, item) => sum + item.quantity, 0);
}
/**
* Get cart total price
*/
getTotal() {
return this.items.reduce((sum, item) => {
// Parse price like "€ 24,95" to number
const priceStr = item.price || "0";
const price = parseFloat(
priceStr.replace(/[€$£\s]/g, "").replace(",", ".")
);
return sum + price * item.quantity;
}, 0);
}
}
// Create singleton instance
const cart = new CartStore();
// Export for module use
export default cart;
// Also attach to window for global access
window.cartStore = cart;