fix: add book details
This commit is contained in:
458
js/components/book-details.js
Normal file
458
js/components/book-details.js
Normal file
@@ -0,0 +1,458 @@
|
||||
/**
|
||||
* Book Details Component
|
||||
* Displays book information with cover, metadata, and purchase buttons
|
||||
*/
|
||||
import { shoppingBagIcon } from "../icons/shopping-bag.js";
|
||||
|
||||
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("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("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);
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
<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">
|
||||
<button class="btn btn-cart">
|
||||
${shoppingBagIcon({ size: 20, color: "#ffffff" })}
|
||||
<span>In winkelwagen</span>
|
||||
</button>
|
||||
${
|
||||
this.ebookAvailable
|
||||
? `
|
||||
<button class="btn btn-ebook">
|
||||
<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">
|
||||
<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>
|
||||
</svg>
|
||||
<span>Koop eBook</span>
|
||||
</button>
|
||||
`
|
||||
: ""
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("book-details", BookDetails);
|
||||
Reference in New Issue
Block a user