diff --git a/css/styles.css b/css/styles.css index e62780b..a5b0ce8 100644 --- a/css/styles.css +++ b/css/styles.css @@ -189,6 +189,7 @@ table { --color-text: #1e293b; --color-text-light: #64748b; --color-text-inverse: #ffffff; + --color-black: #000000; --color-background: #ffffff; --color-background-secondary: #f8fafc; @@ -198,6 +199,8 @@ table { --color-border-light: #f1f5f9; --color-push-box-bg: #ebeef4; + --color-card-dark-bg: #ebeef4; + --color-button-primary: #951d51; /* Layout */ --site-header-height: 210px; @@ -375,13 +378,28 @@ site-content { overflow: hidden; } +.content-padding { + padding-left: var(--spacing-md, 1rem); + padding-right: var(--spacing-md, 1rem); +} + /* ========================================================================== Page Components ========================================================================== */ /* Section */ .section { - margin-bottom: var(--spacing-xl); + margin-top: var(--spacing-md); + margin-bottom: var(--spacing-md); + /* Full-width within container */ + width: 100%; + /* Padding inside section */ + padding-left: var(--spacing-md, 1rem); + padding-right: var(--spacing-md, 1rem); +} + +.section-dark { + background-color: var(--color-card-dark-bg); } .section-title { @@ -401,8 +419,8 @@ site-content { /* Book Grid */ .book-grid { display: grid; - grid-template-columns: repeat(2, 1fr); - gap: var(--spacing-md); + grid-template-columns: 1fr; + gap: var(--spacing-md, 1rem); /* 16px */ } /* ========================================================================== diff --git a/index.html b/index.html index 148e231..6db8ec6 100644 --- a/index.html +++ b/index.html @@ -82,20 +82,23 @@ - - Asoka Logo -

- Gespecialiseerd op het vlak van boeddhisme en aanverwante Oost-West - thema's -

- Meer over Asoka -
+
+ + Asoka Logo +

+ Gespecialiseerd op het vlak van boeddhisme en aanverwante + Oost-West thema's +

+ Meer over Asoka +
+
-
-

Featured Books

+
+
-

New Releases

+
-

Best Sellers

+
{ - console.log('BookStore app initialized'); +document.addEventListener("DOMContentLoaded", () => { + console.log("BookStore app initialized"); }); diff --git a/js/components/add-to-cart-button.js b/js/components/add-to-cart-button.js new file mode 100644 index 0000000..904b29a --- /dev/null +++ b/js/components/add-to-cart-button.js @@ -0,0 +1,77 @@ +/** + * Add to Cart Button Component + * Button with plus icon and shopping bag icon + */ +import { plusIcon } from "../icons/plus.js"; +import { shoppingBagIcon } from "../icons/shopping-bag.js"; + +class AddToCartButton extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + } + + connectedCallback() { + this.render(); + this.setupEventListeners(); + } + + setupEventListeners() { + const button = this.shadowRoot.querySelector(".add-to-cart-button"); + if (button) { + button.addEventListener("click", () => { + this.dispatchEvent( + new CustomEvent("add-to-cart", { + bubbles: true, + composed: true, + }) + ); + }); + } + } + + render() { + this.shadowRoot.innerHTML = ` + + + `; + } +} + +customElements.define("add-to-cart-button", AddToCartButton); diff --git a/js/components/book-card.js b/js/components/book-card.js index a700d77..b5c9258 100644 --- a/js/components/book-card.js +++ b/js/components/book-card.js @@ -1,91 +1,95 @@ /** * Book Card Component - * Reusable card displaying book thumbnail, title, author, and price + * Reusable card displaying book thumbnail, title, description, author, and price + * Horizontal layout with image on left and content on right */ class BookCard extends HTMLElement { static get observedAttributes() { - return ['title', 'author', 'price', 'image', 'href', 'rating']; + return [ + "title", + "author", + "description", + "price", + "image", + "href", + "theme", + ]; } constructor() { super(); - this.attachShadow({ mode: 'open' }); + this.attachShadow({ mode: "open" }); } connectedCallback() { this.render(); + this.setupEventListeners(); + } + + setupEventListeners() { + const addToCartButton = this.shadowRoot?.querySelector("add-to-cart-button"); + if (addToCartButton) { + addToCartButton.addEventListener("add-to-cart", (e) => { + e.stopPropagation(); + // Re-dispatch with book details + this.dispatchEvent( + new CustomEvent("add-to-cart", { + bubbles: true, + composed: true, + detail: { + title: this.title, + author: this.author, + price: this.price, + }, + }) + ); + }); + } } attributeChangedCallback() { if (this.shadowRoot) { this.render(); + this.setupEventListeners(); } } get title() { - return this.getAttribute('title') || 'Book Title'; + return this.getAttribute("title") || "Book Title"; } get author() { - return this.getAttribute('author') || 'Author Name'; + return this.getAttribute("author") || "Author Name"; + } + + get description() { + return this.getAttribute("description") || ""; } get price() { - return this.getAttribute('price') || '$0.00'; + return this.getAttribute("price") || "$0.00"; } get image() { - return this.getAttribute('image') || ''; + return this.getAttribute("image") || ""; } get href() { - return this.getAttribute('href') || 'book.html'; + return this.getAttribute("href") || "book.html"; } - get rating() { - return parseFloat(this.getAttribute('rating')) || 0; - } - - renderStars(rating) { - const fullStars = Math.floor(rating); - const hasHalf = rating % 1 >= 0.5; - const emptyStars = 5 - fullStars - (hasHalf ? 1 : 0); - - let stars = ''; - - // Full stars - for (let i = 0; i < fullStars; i++) { - stars += ` - - `; - } - - // Half star - if (hasHalf) { - stars += ` - - - - - - - - `; - } - - // Empty stars - for (let i = 0; i < emptyStars; i++) { - stars += ` - - `; - } - - return stars; + get theme() { + return this.getAttribute("theme") || "light"; // 'light' or 'dark' } render() { + const backgroundColor = + this.theme === "dark" + ? "var(--color-card-dark-bg, #ebeef4)" + : "var(--color-background, #ffffff)"; + // Generate placeholder image if none provided - const imageHtml = this.image + const imageHtml = this.image ? `${this.title}` : `
@@ -100,14 +104,18 @@ class BookCard extends HTMLElement { } .card { - display: block; - text-decoration: none; + display: flex; + flex-direction: row; + align-items: stretch; + gap: var(--spacing-md, 1rem); color: inherit; - background-color: var(--color-background, #ffffff); - border-radius: var(--radius-lg, 0.75rem); - overflow: hidden; + background-color: ${backgroundColor}; + border-radius: var(--radius-sm, 0.25rem); + padding: var(--spacing-md, 1rem); transition: transform var(--transition-fast, 150ms ease), box-shadow var(--transition-fast, 150ms ease); + min-width: 0; + overflow: hidden; } .card:hover { @@ -120,16 +128,34 @@ class BookCard extends HTMLElement { } .image-container { - position: relative; - aspect-ratio: 3 / 4; + width: 102px; + min-width: 102px; + max-width: 102px; + height: 165px; background-color: var(--color-background-tertiary, #f1f5f9); overflow: hidden; + border-radius: var(--radius-sm, 0.25rem); + } + + .image-link { + display: block; + height: 100%; + text-decoration: none; + color: inherit; + } + + .image-link:visited, + .image-link:hover, + .image-link:active { + color: inherit; + text-decoration: none; } .book-image { width: 100%; height: 100%; object-fit: cover; + display: block; } .book-image.placeholder { @@ -145,67 +171,107 @@ class BookCard extends HTMLElement { opacity: 0.5; } - .content { - padding: var(--spacing-sm, 0.5rem); + .content-wrapper { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; } - .title { - font-size: var(--font-size-sm, 0.875rem); - font-weight: var(--font-weight-semibold, 600); - color: var(--color-text, #1e293b); - margin-bottom: var(--spacing-xs, 0.25rem); + .content { + height: 100%; + flex: 1; + display: flex; + flex-direction: column; + gap: var(--spacing-sm, 0.5rem); /* 8px gap between elements */ + justify-content: center; + } + + .title-link { + font-family: var(--font-family-outfit, "Outfit", sans-serif); + font-size: var(--font-size-xl, 1.25rem); /* 20px */ + font-weight: var(--font-weight-bold, 700); + line-height: var(--line-height-24, 24px); + color: var(--color-black, #000000); + text-decoration: underline; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; - line-height: var(--line-height-tight, 1.25); } - .author { - font-size: var(--font-size-xs, 0.75rem); - color: var(--color-text-light, #64748b); - margin-bottom: var(--spacing-xs, 0.25rem); + .title-link:hover { + opacity: 0.8; + } + + .description { + margin: 0; + font-family: var(--font-family-outfit, "Outfit", sans-serif); + font-size: var(--font-size-base, 1rem); /* 16px */ + font-weight: var(--font-weight-light, 300); + line-height: var(--line-height-24, 24px); + color: var(--color-black, #000000); + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + } + + .author-link { + font-family: var(--font-family-outfit, "Outfit", sans-serif); + font-size: var(--font-size-base, 1rem); /* 16px */ + font-weight: var(--font-weight-light, 300); + line-height: var(--line-height-24, 24px); + color: var(--color-button-primary, #951d51); + text-decoration: underline; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } - .rating { - display: flex; - align-items: center; - gap: 2px; - margin-bottom: var(--spacing-xs, 0.25rem); - } - - .star { - width: 12px; - height: 12px; - color: var(--color-accent, #f59e0b); - } - - .star-empty { - color: var(--color-border, #e2e8f0); + .author-link:hover { + opacity: 0.8; } .price { - font-size: var(--font-size-base, 1rem); + margin: 0; + font-family: var(--font-family-outfit, "Outfit", sans-serif); + font-size: var(--font-size-md, 1rem); /* 16px */ font-weight: var(--font-weight-bold, 700); - color: var(--color-primary, #2563eb); + line-height: var(--line-height-24, 24px); + color: var(--color-text, #1e293b); + } + + .actions { + display: flex; + justify-content: flex-end; + margin-top: auto; } - -
- ${imageHtml} +
+ +
+ ${imageHtml} +
+
+
+
+ ${this.title} + ${ + this.description + ? `

${this.description}

` + : "" + } + ${this.author} +

${this.price}

+
+
+ +
-
-

${this.title}

-

${this.author}

- ${this.rating > 0 ? `
${this.renderStars(this.rating)}
` : ''} -

${this.price}

-
- +
`; } } -customElements.define('book-card', BookCard); +customElements.define("book-card", BookCard); diff --git a/js/components/push-box.js b/js/components/push-box.js index 531bc55..a2cefc1 100644 --- a/js/components/push-box.js +++ b/js/components/push-box.js @@ -41,7 +41,6 @@ class PushBox extends HTMLElement { .push-box { background-color: ${this.backgroundColor}; - margin-bottom: 16px; padding: 48px 16px; display: flex; flex-direction: column; diff --git a/js/components/section-title.js b/js/components/section-title.js new file mode 100644 index 0000000..a873892 --- /dev/null +++ b/js/components/section-title.js @@ -0,0 +1,75 @@ +/** + * Section Title Component + * Displays a title with an arrow right icon on the right side + */ +import { arrowRightIcon } from "../icons/arrow-right.js"; + +class SectionTitle extends HTMLElement { + static get observedAttributes() { + return ["text"]; + } + + constructor() { + super(); + this.attachShadow({ mode: "open" }); + } + + connectedCallback() { + this.render(); + } + + attributeChangedCallback() { + if (this.shadowRoot) { + this.render(); + } + } + + get text() { + return this.getAttribute("text") || this.textContent || "Section Title"; + } + + render() { + this.shadowRoot.innerHTML = ` + +
+

${this.text}

+
+ ${arrowRightIcon({ size: 24, color: "currentColor" })} +
+
+ `; + } +} + +customElements.define("section-title", SectionTitle); diff --git a/js/components/site-content.js b/js/components/site-content.js index 81ec2ca..de55f16 100644 --- a/js/components/site-content.js +++ b/js/components/site-content.js @@ -5,7 +5,7 @@ class SiteContent extends HTMLElement { constructor() { super(); - this.attachShadow({ mode: 'open' }); + this.attachShadow({ mode: "open" }); } connectedCallback() { @@ -22,7 +22,8 @@ class SiteContent extends HTMLElement { } .content { - padding: var(--spacing-md, 1rem); + padding-top: var(--spacing-md); + padding-bottom: var(--spacing-md); }
@@ -32,4 +33,4 @@ class SiteContent extends HTMLElement { } } -customElements.define('site-content', SiteContent); +customElements.define("site-content", SiteContent); diff --git a/js/icons/arrow-right.js b/js/icons/arrow-right.js new file mode 100644 index 0000000..8c64630 --- /dev/null +++ b/js/icons/arrow-right.js @@ -0,0 +1,30 @@ +/** + * Arrow Right Icon (Lucide) + * @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 arrowRightIcon({ + size = 24, + color = "currentColor", + strokeWidth = 2, +} = {}) { + return ` + + + + + `; +} diff --git a/js/icons/plus.js b/js/icons/plus.js new file mode 100644 index 0000000..05ee285 --- /dev/null +++ b/js/icons/plus.js @@ -0,0 +1,30 @@ +/** + * Plus Icon (Lucide) + * @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 plusIcon({ + size = 24, + color = "currentColor", + strokeWidth = 2, +} = {}) { + return ` + + + + + `; +}