fix: add action list
This commit is contained in:
@@ -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">
|
||||||
|
|||||||
57
js/app.js
57
js/app.js
@@ -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`);
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
36
js/components/action-links-list.js
Normal file
36
js/components/action-links-list.js
Normal 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);
|
||||||
@@ -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>
|
|
||||||
`
|
`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
|
|||||||
152
js/components/icon-cta-button.js
Normal file
152
js/components/icon-cta-button.js
Normal 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);
|
||||||
130
js/components/icon-link-button.js
Normal file
130
js/components/icon-link-button.js
Normal 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
31
js/icons/cart-icon.js
Normal 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
29
js/icons/ebook-icon.js
Normal 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
31
js/icons/review-icon.js
Normal 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>
|
||||||
|
`;
|
||||||
|
}
|
||||||
@@ -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
32
js/icons/wishlist-icon.js
Normal 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
158
js/store/cart.js
Normal 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;
|
||||||
Reference in New Issue
Block a user