diff --git a/book.html b/book.html index 9217831..074f300 100644 --- a/book.html +++ b/book.html @@ -60,6 +60,75 @@ Schrijf een recensie + + + +

+ Zen is opendoen bevat een selectie van zeventig columns die Dick + Verstegen schreef tussen 2002 en 2017. Hij schreef ze als + columnist voor o.a. het boeddhistisch kwartaalblad Vorm & + Leegte, het Boeddhistisch Dagblad, Centrum Waerbeke, het Han + Fortmann Centrum en de Wijkkrant van Nijmegen-Oost. Zijn columns + wijzen zonder uitzondering naar het mysterie van het leven en + geven blijk van bewogenheid en overgave, maar de lichte toets + ontbreekt nooit. Ze gaan over zeer uiteenlopende onderwerpen, + zoals: lente, de ware stem van je hart, compassie, ontroering, + woorden, de dood van zijn vrouw, de kathedraal van Royan, het + windorgel in Vlissingen, liefde, boeddhaschap, bedelen, emoties, + relaties, management, overgave, gedachten, Nepal, leerling zijn, + theekommen, zijn vader, stilte, licht en opendoen. +

+

+ Uit zijn columns blijkt hoezeer hij zich ervan bewust is dat + zenboeddhisme geen mening is, maar een non-duale zienswijze. + Non-dualiteit is zien dat de dualiteiten geen hindernissen zijn + voor een bevrijd bestaan. (...) zo te spreken of te schrijven + dat het mysteriekarakter onaangetast blijft. Wat mij betreft is + Dick hierin volkomen geslaagd. Hoe hem dit gelukt is, is mij een + raadsel. Misschien wel dankzij zijn grote liefde voor de taal + die spreekt uit elke bladzijde van deze verzameling literaire + miniaturen. +

+

—Nico Tydeman

+
+ + + + + + Een prachtige verzameling columns die je aan het denken zet. + Dick Verstegen schrijft met zoveel warmte en wijsheid. Elk + stukje is een kleine meditatie op zich. Aanrader voor iedereen + die geïnteresseerd is in zen en het dagelijks leven. + + + Dit boek heeft mijn kijk op zen volledig veranderd. De columns + zijn toegankelijk geschreven en toch diepgaand. Ik lees elke + avond een column voor het slapen gaan. Een boek om te koesteren. + + + Mooie, poëtische teksten die je uitnodigen om stil te staan bij + het leven. Soms wat abstract, maar overall een waardevolle + toevoeging aan mijn boekenplank. De quote van Nico Tydeman op de + achterkant vat het perfect samen. + + +
diff --git a/images/book-insight.jpg b/images/book-insight.jpg new file mode 100644 index 0000000..cb6781e Binary files /dev/null and b/images/book-insight.jpg differ diff --git a/js/app.js b/js/app.js index 67dec5e..5d025f4 100644 --- a/js/app.js +++ b/js/app.js @@ -26,6 +26,11 @@ import "./components/book-details.js"; import "./components/icon-cta-button.js"; import "./components/icon-link-button.js"; import "./components/action-links-list.js"; +import "./components/content-tabs.js"; +import "./components/image-gallery.js"; +import "./components/book-description.js"; +import "./components/book-reviews.js"; +import "./components/book-review-item.js"; // Import icon components import "./icons/menu-icon.js"; diff --git a/js/components/book-description.js b/js/components/book-description.js new file mode 100644 index 0000000..1cbb6f4 --- /dev/null +++ b/js/components/book-description.js @@ -0,0 +1,53 @@ +/** + * Book Description Component + * Displays formatted book description text + */ +class BookDescription extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + } + + connectedCallback() { + this.render(); + } + + render() { + this.shadowRoot.innerHTML = ` + +
+ +
+ `; + } +} + +customElements.define("book-description", BookDescription); diff --git a/js/components/book-review-item.js b/js/components/book-review-item.js new file mode 100644 index 0000000..e1c1a1b --- /dev/null +++ b/js/components/book-review-item.js @@ -0,0 +1,128 @@ +/** + * Book Review Item Component + * Individual review with rating, author, date, and text + */ +class BookReviewItem extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + } + + connectedCallback() { + this.render(); + } + + get rating() { + return parseInt(this.getAttribute("rating") || "5", 10); + } + + get author() { + return this.getAttribute("author") || "Anoniem"; + } + + get date() { + return this.getAttribute("date") || ""; + } + + renderStars() { + const rating = Math.min(5, Math.max(0, this.rating)); + const fullStars = rating; + const emptyStars = 5 - rating; + + let stars = ""; + for (let i = 0; i < fullStars; i++) { + stars += ``; + } + for (let i = 0; i < emptyStars; i++) { + stars += ``; + } + return stars; + } + + render() { + this.shadowRoot.innerHTML = ` + +
+
+
${this.renderStars()}
+
+ ${this.author} + ${this.date ? `• ${this.date}` : ""} +
+
+

+ +

+
+ `; + } +} + +customElements.define("book-review-item", BookReviewItem); diff --git a/js/components/book-reviews.js b/js/components/book-reviews.js new file mode 100644 index 0000000..52c3869 --- /dev/null +++ b/js/components/book-reviews.js @@ -0,0 +1,47 @@ +/** + * Book Reviews Component + * Displays customer reviews for a book + */ +class BookReviews extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + } + + connectedCallback() { + this.render(); + } + + render() { + this.shadowRoot.innerHTML = ` + +
+ +

Nog geen recensies. Wees de eerste om een recensie te schrijven!

+
+
+ `; + } +} + +customElements.define("book-reviews", BookReviews); diff --git a/js/components/content-tabs.js b/js/components/content-tabs.js new file mode 100644 index 0000000..85080a7 --- /dev/null +++ b/js/components/content-tabs.js @@ -0,0 +1,144 @@ +/** + * Content Tabs Component + * Tabbed interface for displaying different content sections + */ +class ContentTabs extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + this.activeTab = 0; + } + + connectedCallback() { + this.render(); + this.setupEventListeners(); + } + + get tabs() { + const tabsAttr = this.getAttribute("tabs"); + return tabsAttr ? tabsAttr.split(",").map((t) => t.trim()) : []; + } + + setupEventListeners() { + const tabButtons = this.shadowRoot.querySelectorAll(".tab-button"); + tabButtons.forEach((button, index) => { + button.addEventListener("click", () => { + this.setActiveTab(index); + }); + }); + } + + setActiveTab(index) { + this.activeTab = index; + this.updateTabs(); + this.dispatchEvent( + new CustomEvent("tab-change", { + bubbles: true, + composed: true, + detail: { index, tab: this.tabs[index] }, + }) + ); + } + + updateTabs() { + // Update tab buttons + const tabButtons = this.shadowRoot.querySelectorAll(".tab-button"); + tabButtons.forEach((button, index) => { + button.classList.toggle("active", index === this.activeTab); + }); + + // Update panels + const panels = this.querySelectorAll("[slot^='panel-']"); + panels.forEach((panel, index) => { + panel.style.display = index === this.activeTab ? "block" : "none"; + }); + } + + render() { + const tabsHtml = this.tabs + .map( + (tab, index) => ` + + ` + ) + .join(""); + + this.shadowRoot.innerHTML = ` + +
+
+ ${tabsHtml} +
+
+ + + +
+
+ `; + + // Initialize panel visibility + setTimeout(() => this.updateTabs(), 0); + } +} + +customElements.define("content-tabs", ContentTabs); diff --git a/js/components/image-gallery.js b/js/components/image-gallery.js new file mode 100644 index 0000000..4b7fbd5 --- /dev/null +++ b/js/components/image-gallery.js @@ -0,0 +1,368 @@ +/** + * Image Gallery Component + * Grid of images that open in a fullscreen modal on click + */ +class ImageGallery extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + + // Zoom and pan state + this.currentZoom = 1; + this.translateX = 0; + this.translateY = 0; + this.isDragging = false; + this.startX = 0; + this.startY = 0; + this.lastTranslateX = 0; + this.lastTranslateY = 0; + } + + connectedCallback() { + this.render(); + this.setupEventListeners(); + } + + get images() { + const imagesAttr = this.getAttribute("images"); + return imagesAttr ? imagesAttr.split(",").map((img) => img.trim()) : []; + } + + setupEventListeners() { + const imageItems = this.shadowRoot.querySelectorAll(".gallery-image"); + imageItems.forEach((img, index) => { + img.addEventListener("click", () => { + this.openModal(index); + }); + }); + + // Modal controls + const closeBtn = this.shadowRoot.querySelector(".modal-close"); + const modal = this.shadowRoot.querySelector(".modal-overlay"); + + if (closeBtn) { + closeBtn.addEventListener("click", () => this.closeModal()); + } + + if (modal) { + modal.addEventListener("click", (e) => { + if (e.target === modal) { + this.closeModal(); + } + }); + } + + // Keyboard support + document.addEventListener("keydown", (e) => { + if (e.key === "Escape") { + this.closeModal(); + } + }); + + // Zoom controls + const zoomIn = this.shadowRoot.querySelector(".zoom-in"); + const zoomOut = this.shadowRoot.querySelector(".zoom-out"); + const resetZoom = this.shadowRoot.querySelector(".zoom-reset"); + + if (zoomIn) { + zoomIn.addEventListener("click", () => this.zoom(0.25)); + } + if (zoomOut) { + zoomOut.addEventListener("click", () => this.zoom(-0.25)); + } + if (resetZoom) { + resetZoom.addEventListener("click", () => this.resetZoom()); + } + + // Drag/pan functionality + const modalImage = this.shadowRoot.querySelector(".modal-image"); + if (modalImage) { + // Mouse events + modalImage.addEventListener("mousedown", (e) => this.startDrag(e)); + modalImage.addEventListener("mousemove", (e) => this.drag(e)); + modalImage.addEventListener("mouseup", () => this.endDrag()); + modalImage.addEventListener("mouseleave", () => this.endDrag()); + + // Touch events for mobile + modalImage.addEventListener("touchstart", (e) => this.startDrag(e), { passive: false }); + modalImage.addEventListener("touchmove", (e) => this.drag(e), { passive: false }); + modalImage.addEventListener("touchend", () => this.endDrag()); + + // Prevent default drag behavior + modalImage.addEventListener("dragstart", (e) => e.preventDefault()); + } + } + + startDrag(e) { + // Only enable drag when zoomed in + if (this.currentZoom <= 1) return; + + this.isDragging = true; + + if (e.type === "touchstart") { + this.startX = e.touches[0].clientX - this.translateX; + this.startY = e.touches[0].clientY - this.translateY; + } else { + this.startX = e.clientX - this.translateX; + this.startY = e.clientY - this.translateY; + } + + const modalImage = this.shadowRoot.querySelector(".modal-image"); + if (modalImage) { + modalImage.style.cursor = "grabbing"; + modalImage.style.transition = "none"; + } + } + + drag(e) { + if (!this.isDragging) return; + + e.preventDefault(); + + let clientX, clientY; + if (e.type === "touchmove") { + clientX = e.touches[0].clientX; + clientY = e.touches[0].clientY; + } else { + clientX = e.clientX; + clientY = e.clientY; + } + + this.translateX = clientX - this.startX; + this.translateY = clientY - this.startY; + + this.updateImageTransform(); + } + + endDrag() { + if (!this.isDragging) return; + + this.isDragging = false; + + const modalImage = this.shadowRoot.querySelector(".modal-image"); + if (modalImage) { + modalImage.style.cursor = this.currentZoom > 1 ? "grab" : "default"; + modalImage.style.transition = "transform 0.2s ease"; + } + } + + updateImageTransform() { + const modalImage = this.shadowRoot.querySelector(".modal-image"); + if (modalImage) { + modalImage.style.transform = `scale(${this.currentZoom}) translate(${this.translateX / this.currentZoom}px, ${this.translateY / this.currentZoom}px)`; + } + } + + openModal(index) { + const modal = this.shadowRoot.querySelector(".modal-overlay"); + const modalImage = this.shadowRoot.querySelector(".modal-image"); + + if (modal && modalImage && this.images[index]) { + modalImage.src = this.images[index]; + + // Reset zoom and pan + this.currentZoom = 1; + this.translateX = 0; + this.translateY = 0; + this.updateImageTransform(); + + modalImage.style.cursor = "default"; + modal.classList.add("open"); + document.body.style.overflow = "hidden"; + } + } + + closeModal() { + const modal = this.shadowRoot.querySelector(".modal-overlay"); + if (modal) { + modal.classList.remove("open"); + document.body.style.overflow = ""; + } + } + + zoom(delta) { + const modalImage = this.shadowRoot.querySelector(".modal-image"); + if (modalImage) { + const prevZoom = this.currentZoom; + this.currentZoom = Math.max(1, Math.min(4, this.currentZoom + delta)); + + // If zooming out to 1x, reset position + if (this.currentZoom === 1) { + this.translateX = 0; + this.translateY = 0; + } + + this.updateImageTransform(); + modalImage.style.cursor = this.currentZoom > 1 ? "grab" : "default"; + } + } + + resetZoom() { + const modalImage = this.shadowRoot.querySelector(".modal-image"); + if (modalImage) { + this.currentZoom = 1; + this.translateX = 0; + this.translateY = 0; + this.updateImageTransform(); + modalImage.style.cursor = "default"; + } + } + + render() { + const imagesHtml = this.images + .map( + (img, index) => ` + + ` + ) + .join(""); + + this.shadowRoot.innerHTML = ` + + + + + + `; + } +} + +customElements.define("image-gallery", ImageGallery);