272 lines
7.0 KiB
JavaScript
272 lines
7.0 KiB
JavaScript
/**
|
|
* Book Card Component
|
|
* 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",
|
|
"description",
|
|
"price",
|
|
"image",
|
|
"href",
|
|
"theme",
|
|
];
|
|
}
|
|
|
|
constructor() {
|
|
super();
|
|
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";
|
|
}
|
|
|
|
get author() {
|
|
return this.getAttribute("author") || "Author Name";
|
|
}
|
|
|
|
get description() {
|
|
return this.getAttribute("description") || "";
|
|
}
|
|
|
|
get price() {
|
|
return this.getAttribute("price") || "$0.00";
|
|
}
|
|
|
|
get image() {
|
|
return this.getAttribute("image") || "";
|
|
}
|
|
|
|
get href() {
|
|
return this.getAttribute("href") || "book.html";
|
|
}
|
|
|
|
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
|
|
? `<img src="${this.image}" alt="${this.title}" class="book-image">`
|
|
: `<div class="book-image placeholder">
|
|
<book-open-icon size="48"></book-open-icon>
|
|
</div>`;
|
|
|
|
this.shadowRoot.innerHTML = `
|
|
<style>
|
|
:host {
|
|
display: block;
|
|
}
|
|
|
|
.card {
|
|
display: flex;
|
|
flex-direction: row;
|
|
align-items: stretch;
|
|
gap: var(--spacing-md, 1rem);
|
|
color: inherit;
|
|
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 {
|
|
transform: translateY(-2px);
|
|
box-shadow: var(--shadow-md, 0 4px 6px -1px rgb(0 0 0 / 0.1));
|
|
}
|
|
|
|
.card:active {
|
|
transform: translateY(0);
|
|
}
|
|
|
|
.image-container {
|
|
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 {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--color-text-light, #64748b);
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.content-wrapper {
|
|
flex: 1;
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
.author-link:hover {
|
|
opacity: 0.8;
|
|
}
|
|
|
|
.price {
|
|
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);
|
|
line-height: var(--line-height-24, 24px);
|
|
color: var(--color-text, #1e293b);
|
|
}
|
|
|
|
.actions {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
margin-top: auto;
|
|
}
|
|
</style>
|
|
<div class="card">
|
|
<a href="${this.href}" class="image-link">
|
|
<div class="image-container">
|
|
${imageHtml}
|
|
</div>
|
|
</a>
|
|
<div class="content-wrapper">
|
|
<div class="content">
|
|
<a href="${this.href}" class="title-link">${this.title}</a>
|
|
${
|
|
this.description
|
|
? `<p class="description">${this.description}</p>`
|
|
: ""
|
|
}
|
|
<a href="${this.href}" class="author-link">${this.author}</a>
|
|
<p class="price">${this.price}</p>
|
|
</div>
|
|
<div class="actions">
|
|
<add-to-cart-button></add-to-cart-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
}
|
|
|
|
customElements.define("book-card", BookCard);
|