Files
milinda-pitch/js/components/book-details.js
rubberducky 3dbe404443 feature/book-page (#4)
Co-authored-by: Tim Rijkse <trijkse@gmail.com>
Reviewed-on: #4
2026-01-16 08:46:03 +00:00

410 lines
10 KiB
JavaScript

/**
* Book Details Component
* Displays book information with cover, metadata, and purchase buttons
*/
class BookDetails extends HTMLElement {
static get observedAttributes() {
return [
"title",
"author",
"author-href",
"price",
"image",
"isbn",
"format",
"delivery",
"categories",
"ebook-available",
];
}
constructor() {
super();
this.attachShadow({ mode: "open" });
}
connectedCallback() {
this.render();
this.setupEventListeners();
}
attributeChangedCallback() {
if (this.shadowRoot) {
this.render();
this.setupEventListeners();
}
}
setupEventListeners() {
const addToCartBtn = this.shadowRoot?.querySelector(".btn-cart");
const ebookBtn = this.shadowRoot?.querySelector(".btn-ebook");
const favoriteBtn = this.shadowRoot?.querySelector(".favorite-btn");
if (addToCartBtn) {
addToCartBtn.addEventListener("button-click", () => {
this.dispatchEvent(
new CustomEvent("add-to-cart", {
bubbles: true,
composed: true,
detail: {
title: this.bookTitle,
price: this.price,
type: "physical",
},
})
);
});
}
if (ebookBtn) {
ebookBtn.addEventListener("button-click", () => {
this.dispatchEvent(
new CustomEvent("buy-ebook", {
bubbles: true,
composed: true,
detail: {
title: this.bookTitle,
type: "ebook",
},
})
);
});
}
if (favoriteBtn) {
favoriteBtn.addEventListener("click", () => {
favoriteBtn.classList.toggle("active");
this.dispatchEvent(
new CustomEvent("toggle-favorite", {
bubbles: true,
composed: true,
detail: {
title: this.bookTitle,
},
})
);
});
}
}
get bookTitle() {
return this.getAttribute("title") || "Book Title";
}
get author() {
return this.getAttribute("author") || "Author Name";
}
get authorHref() {
return this.getAttribute("author-href") || "#";
}
get price() {
return this.getAttribute("price") || "€ 0,00";
}
get image() {
return this.getAttribute("image") || "";
}
get isbn() {
return this.getAttribute("isbn") || "";
}
get format() {
return this.getAttribute("format") || "Paperback";
}
get delivery() {
return this.getAttribute("delivery") || "Direct leverbaar";
}
get categories() {
return this.getAttribute("categories") || "";
}
get ebookAvailable() {
return this.hasAttribute("ebook-available");
}
renderCategories() {
if (!this.categories) return "";
// Categories are comma-separated, with optional href in format: "Name|href,Name2|href2"
const cats = this.categories.split(",").map((cat) => {
const [name, href] = cat.trim().split("|");
if (href) {
return `<a href="${href}" class="category-link">${name}</a>`;
}
return `<span class="category-text">${name}</span>`;
});
return cats.join('<span class="category-separator"> / </span>');
}
render() {
const imageHtml = this.image
? `<img src="${this.image}" alt="${this.bookTitle}" class="book-cover">`
: `<div class="book-cover placeholder">
<book-open-icon size="48" color="var(--color-text-light)"></book-open-icon>
</div>`;
this.shadowRoot.innerHTML = `
<style>
:host {
display: block;
font-family: var(--font-family-outfit, "Outfit", sans-serif);
}
.book-details {
display: flex;
flex-direction: column;
gap: var(--spacing-md, 1rem);
}
/* Header Section */
.header {
display: flex;
flex-direction: column;
gap: var(--spacing-sm, 0.5rem);
}
.title-row {
display: flex;
align-items: center;
gap: var(--spacing-sm, 0.5rem);
}
.title {
font-family: var(--font-family-outfit, "Outfit", sans-serif);
font-size: 24px;
font-weight: 700;
line-height: 34px;
color: var(--color-text, #1e293b);
margin: 0;
}
.favorite-btn {
display: flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
background: none;
border: none;
cursor: pointer;
padding: 0;
flex-shrink: 0;
}
.favorite-btn svg {
width: 28px;
height: 28px;
transition: transform var(--transition-fast, 150ms ease);
}
.favorite-btn:hover svg {
transform: scale(1.1);
}
.favorite-btn.active svg {
fill: #f59e0b;
}
.author-link {
font-family: var(--font-family-outfit, "Outfit", sans-serif);
font-size: 16px;
font-weight: 300;
line-height: 24px;
color: var(--color-purple, #951d51);
text-decoration: underline;
}
.author-link:hover {
opacity: 0.8;
}
.price {
font-family: var(--font-family-outfit, "Outfit", sans-serif);
font-size: 16px;
font-weight: 700;
line-height: 24px;
color: var(--color-text, #1e293b);
margin: 0;
}
/* Content Section */
.content {
display: flex;
gap: var(--spacing-md, 1rem);
}
.cover-container {
flex-shrink: 0;
width: 110px;
height: 177px;
}
.book-cover {
width: 110px;
height: 177px;
object-fit: cover;
border-radius: var(--radius-sm, 0.25rem);
}
.book-cover.placeholder {
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-background-tertiary, #f1f5f9);
border-radius: var(--radius-sm, 0.25rem);
}
/* Details Grid */
.details {
flex: 1;
display: flex;
flex-direction: column;
gap: var(--spacing-sm, 0.5rem);
}
.detail-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: var(--spacing-sm, 0.5rem);
}
.detail-item {
display: flex;
flex-direction: column;
gap: 0;
}
.detail-item.full-width {
grid-column: 1 / -1;
}
.detail-label {
font-family: var(--font-family-outfit, "Outfit", sans-serif);
font-size: 12px;
font-weight: 200;
line-height: 24px;
color: var(--color-purple, #951d51);
}
.detail-value {
font-family: var(--font-family-outfit, "Outfit", sans-serif);
font-size: 16px;
font-weight: 300;
line-height: 24px;
color: var(--color-text, #1e293b);
}
.category-link {
color: var(--color-text, #1e293b);
text-decoration: underline;
}
.category-link:hover {
opacity: 0.8;
}
.category-text {
color: var(--color-text, #1e293b);
}
.category-separator {
color: var(--color-text, #1e293b);
}
/* Buttons Section */
.buttons {
display: flex;
flex-direction: column;
gap: var(--spacing-sm, 0.5rem);
}
</style>
<div class="book-details">
<!-- Header -->
<div class="header">
<div class="title-row">
<h1 class="title">${this.bookTitle}</h1>
<button class="favorite-btn" aria-label="Add to favorites">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#f59e0b" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
</svg>
</button>
</div>
<a href="${this.authorHref}" class="author-link">${this.author}</a>
<p class="price">${this.price}</p>
</div>
<!-- Content: Cover + Details -->
<div class="content">
<div class="cover-container">
${imageHtml}
</div>
<div class="details">
${
this.categories
? `
<div class="detail-item full-width">
<span class="detail-label">Categorieën</span>
<span class="detail-value">${this.renderCategories()}</span>
</div>
`
: ""
}
<div class="detail-row">
${
this.isbn
? `
<div class="detail-item">
<span class="detail-label">ISBN</span>
<span class="detail-value">${this.isbn}</span>
</div>
`
: ""
}
<div class="detail-item">
<span class="detail-label">Uitvoering</span>
<span class="detail-value">${this.format}</span>
</div>
</div>
<div class="detail-row">
<div class="detail-item">
<span class="detail-label">Uitvoering</span>
<span class="detail-value">${this.format}</span>
</div>
<div class="detail-item">
<span class="detail-label">Levertijd</span>
<span class="detail-value">${this.delivery}</span>
</div>
</div>
</div>
</div>
<!-- Buttons -->
<div class="buttons">
<icon-cta-button class="btn-cart" icon="cart" variant="primary">
In winkelwagen
</icon-cta-button>
${
this.ebookAvailable
? `
<icon-cta-button class="btn-ebook" icon="ebook" variant="secondary">
Koop eBook
</icon-cta-button>
`
: ""
}
</div>
</div>
`;
}
}
customElements.define("book-details", BookDetails);