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 = `
+
+
+
+
+
+
+
+ `;
+ }
+}
+
+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) => `
+
+ ${tab}
+
+ `
+ )
+ .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 = `
+
+
+
+ ${imagesHtml}
+
+
+
+
+
+
+
+
+ `;
+ }
+}
+
+customElements.define("image-gallery", ImageGallery);