From 7beab685e2d85e7c92b549c6e48b5ba6b0c79914 Mon Sep 17 00:00:00 2001 From: rubberducky Date: Thu, 15 Jan 2026 12:33:37 +0000 Subject: [PATCH] feature/book-card (#3) Co-authored-by: Tim Rijkse Reviewed-on: https://git.apps.rubberducky.studio/rubberducky/milinda-pitch/pulls/3 --- css/styles.css | 25 ++- index.html | 119 ++++++------- js/app.js | 24 +-- js/components/add-to-cart-button.js | 77 +++++++++ js/components/arrow-button.js | 15 +- js/components/book-card.js | 257 ++++++++++++++++++---------- js/components/push-box.js | 1 - js/components/section-title.js | 75 ++++++++ js/components/site-content.js | 7 +- js/components/site-footer.js | 20 +-- js/icons/arrow-right.js | 30 ++++ js/icons/mic.js | 4 +- js/icons/plus.js | 30 ++++ js/icons/search.js | 2 +- js/icons/shopping-bag.js | 6 +- js/icons/user.js | 5 +- 16 files changed, 488 insertions(+), 209 deletions(-) create mode 100644 js/components/add-to-cart-button.js create mode 100644 js/components/section-title.js create mode 100644 js/icons/arrow-right.js create mode 100644 js/icons/plus.js diff --git a/css/styles.css b/css/styles.css index e62780b..f2c6a83 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,9 @@ 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 */ + padding-bottom: var(--spacing-md, 1rem); } /* ========================================================================== diff --git a/index.html b/index.html index 148e231..d9c8b9f 100644 --- a/index.html +++ b/index.html @@ -52,8 +52,9 @@ stroke-linecap="round" stroke-linejoin="round" > - - + + + @@ -82,20 +83,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

-
- - - + +
diff --git a/js/app.js b/js/app.js index 289184c..634c3b7 100644 --- a/js/app.js +++ b/js/app.js @@ -4,17 +4,19 @@ */ // Import all components -import './components/site-header.js'; -import './components/top-bar.js'; -import './components/horizontal-scroll-nav.js'; -import './components/search-bar.js'; -import './components/site-content.js'; -import './components/site-footer.js'; -import './components/book-card.js'; -import './components/push-box.js'; -import './components/arrow-button.js'; +import "./components/site-header.js"; +import "./components/top-bar.js"; +import "./components/horizontal-scroll-nav.js"; +import "./components/search-bar.js"; +import "./components/site-content.js"; +import "./components/site-footer.js"; +import "./components/book-card.js"; +import "./components/push-box.js"; +import "./components/arrow-button.js"; +import "./components/section-title.js"; +import "./components/add-to-cart-button.js"; // App initialization (if needed) -document.addEventListener('DOMContentLoaded', () => { - 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..62f37ec --- /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/arrow-button.js b/js/components/arrow-button.js index be19ff5..f045445 100644 --- a/js/components/arrow-button.js +++ b/js/components/arrow-button.js @@ -51,16 +51,12 @@ class ArrowButton extends HTMLElement { display: flex; align-items: center; justify-content: center; - width: 24px; - height: 24px; - border: 1.5px solid currentColor; - border-radius: 50%; flex-shrink: 0; } .arrow-icon svg { - width: 12px; - height: 12px; + width: 24px; + height: 24px; stroke: currentColor; fill: none; } @@ -72,9 +68,10 @@ class ArrowButton extends HTMLElement { - - - + + + + diff --git a/js/components/book-card.js b/js/components/book-card.js index a700d77..e451d47 100644 --- a/js/components/book-card.js +++ b/js/components/book-card.js @@ -1,91 +1,96 @@ /** * 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 +105,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 +129,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 +172,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/components/site-footer.js b/js/components/site-footer.js index 35cfae3..9afd96c 100644 --- a/js/components/site-footer.js +++ b/js/components/site-footer.js @@ -5,7 +5,7 @@ class SiteFooter extends HTMLElement { constructor() { super(); - this.attachShadow({ mode: 'open' }); + this.attachShadow({ mode: "open" }); } connectedCallback() { @@ -17,7 +17,7 @@ class SiteFooter extends HTMLElement { @@ -108,4 +108,4 @@ class SiteFooter extends HTMLElement { } } -customElements.define('site-footer', SiteFooter); +customElements.define("site-footer", SiteFooter); 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/mic.js b/js/icons/mic.js index 2c96392..448ab6b 100644 --- a/js/icons/mic.js +++ b/js/icons/mic.js @@ -23,9 +23,9 @@ export function micIcon({ stroke-linecap="round" stroke-linejoin="round" > - + - + `; } 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 ` + + + + + `; +} diff --git a/js/icons/search.js b/js/icons/search.js index ff5921d..7019100 100644 --- a/js/icons/search.js +++ b/js/icons/search.js @@ -23,8 +23,8 @@ export function searchIcon({ stroke-linecap="round" stroke-linejoin="round" > + - `; } diff --git a/js/icons/shopping-bag.js b/js/icons/shopping-bag.js index a5a70d3..206c313 100644 --- a/js/icons/shopping-bag.js +++ b/js/icons/shopping-bag.js @@ -23,9 +23,9 @@ export function shoppingBagIcon({ stroke-linecap="round" stroke-linejoin="round" > - - - + + + `; } diff --git a/js/icons/user.js b/js/icons/user.js index dda51d5..02f7209 100644 --- a/js/icons/user.js +++ b/js/icons/user.js @@ -23,8 +23,9 @@ export function userIcon({ stroke-linecap="round" stroke-linejoin="round" > - - + + + `; }