1/* ======================================== 2 StoryWell â Books Catalog + UPI Payment 3 Modern store experience 4 ======================================== */ 5 6(function () { 7 // ---- Configuration ---- 8 var UPI_ID = 'storywellbooks@icici'; 9 var UPI_NAME = 'StoryWell Books'; 10 var DESC_MAX_CHARS = 180; 11 // Google Sheets order backup â paste your Apps Script Web App URL below 12 // Setup: see docs/google-sheets-order-setup.md 13 var SHEETS_ENDPOINT = 'https://script.google.com/macros/s/AKfycbzTAzix1CdQMLmn0V6LBDvoFZkUrHkbFHpkNHYA-vnAgUv9rROooZjxrGINMuykzzqT1Q/exec'; 14 15 var bookGrid = document.getElementById('bookGrid'); 16 var categoryFilter = document.getElementById('categoryFilter'); 17 var loadingMsg = document.getElementById('loadingMsg'); 18 19 // UPI Modal elements 20 var upiModal = document.getElementById('upiModal'); 21 var closeModal = document.getElementById('closeModal'); 22 var modalBookTitle = document.getElementById('modalBookTitle'); 23 var modalAmount = document.getElementById('modalAmount'); 24 var modalUpiId = document.getElementById('modalUpiId'); 25 var modalQr = document.getElementById('modalQr'); 26 var upiDeepLink = document.getElementById('upiDeepLink'); 27 28 // Product detail modal elements 29 var detailModal = document.getElementById('productDetailModal'); 30 31 var allBooks = []; 32 var promoCodes = []; 33 34 // Current order state (details-first checkout) 35 var currentFinalPrice = 0; // price after promo discount 36 var currentPromo = null; // matched promo object, if applied 37 var currentOrderItems = []; // [{ title, qty, price }] 38 var currentOrderFromCart = false; 39 40 // ---- Determine base path for locale subdirectories ---- 41 var scriptEl = document.querySelector('script[src*="books.js"]'); 42 var booksPrefix = (scriptEl && scriptEl.getAttribute('src').indexOf('../') === 0) ? '../' : ''; 43 var booksJsonPath = booksPrefix + 'content/books/books.json'; 44 var promosJsonPath = booksPrefix + 'content/promos/promos.json'; 45 46 // ---- Section definitions ---- 47 var sections = [ 48 { key: 'children', title: "Children's Books", categories: ['children', 'values', 'puzzles'] }, 49 { key: 'academic', title: "Children's Non-Fiction", categories: ['academic'] }, 50 { key: 'fiction', title: 'Fiction', categories: ['fiction', 'novels'] }, 51 { key: 'nonfiction', title: 'Non-Fiction', categories: ['nonfiction'] }, 52 { key: 'stationery', title: 'Stationery & More', categories: ['stationery', 'notebooks', 'bookmarks', 'space'] } 53 ]; 54 55 // ---- Language display names ---- 56 var languageNames = { 57 'english': 'English', 58 'hindi': 'Hindi', 59 'bilingual-hi-en': 'Bilingual (Hindi & English)', 60 'bengali': 'Bengali', 61 'gujarati': 'Gujarati', 62 'marathi': 'Marathi', 63 'odia': 'Odia', 64 'tamil': 'Tamil', 65 'telugu': 'Telugu' 66 }; 67 68 // ---- Helpers ---- 69 function escapeHtml(str) { 70 var div = document.createElement('div'); 71 div.textContent = str; 72 return div.innerHTML; 73 } 74 75 function truncateText(text, max) { 76 if (!text || text.length <= max) return text || ''; 77 return text.substring(0, max).replace(/\s+\S*$/, '') + '...'; 78 } 79 80 function formatCategory(cat) { 81 var map = { 82 novels: 'Novel', children: "Children's Book", values: "Values", 83 notebooks: 'Notebook', bookmarks: 'Bookmark', puzzles: 'Puzzle & Game', 84 fiction: 'Fiction', nonfiction: 'Non-Fiction', academic: 'Academic', 85 stationery: 'Stationery', space: 'Space' 86 }; 87 return map[cat] || cat; 88 } 89
90 function renderStars(rating) { 91 var html = ''; 92 for (var i = 1; i <= 5; i++) { 93 html += i <= rating ? '★' : '☆'; 94 } 95 return html; 96 } 97 98 function getAverageRating(bookId) { 99 var reviews = getReviews(bookId); 100 if (reviews.length === 0) return { avg: 0, count: 0 }; 101 var total = 0; 102 for (var i = 0; i < reviews.length; i++) total += reviews[i].rating; 103 return { avg: Math.round(total / reviews.length), count: reviews.length }; 104 } 105 106 function buildPriceHtml(book) { 107 if (book.recommended_price && book.recommended_price > book.price) { 108 var discount = Math.round((1 - book.price / book.recommended_price) * 100); 109 return '<span class="book-price">₹' + book.price + 110 ' <span class="book-mrp">₹' + book.recommended_price + '</span>' + 111 ' <span class="book-discount">' + discount + '% off</span></span>'; 112 } 113 return '<span class="book-price">₹' + book.price + '</span>'; 114 } 115 116 function buildSpecsHtml(book) { 117 var specs = []; 118 if (book.format) specs.push(book.format.charAt(0).toUpperCase() + book.format.slice(1)); 119 if (book.pages) specs.push(book.pages + ' pages'); 120 if (book.dimensions) specs.push(book.dimensions); 121 return specs.length > 0 ? specs.join(' · ') : ''; 122 } 123 124 function buildVariantsHtml(book) { 125 if (!book.variants || book.variants.length === 0) return ''; 126 var html = '<div class="detail-variants"><label>Choose an option:</label><div class="variant-options">';
127 book.variants.forEach(function(v, i) { 128 var checked = i === 0 ? ' checked' : ''; 129 html += '<label class="variant-option">' + 130 '<input type="radio" name="variant" value="' + v.price + '" data-label="' + escapeHtml(v.label) + '"' + checked + ' onchange="updateVariantPrice(this)">' + 131 '<span>' + escapeHtml(v.label) + ' — ₹' + v.price + '</span>' + 132 '</label>'; 133 }); 134 html += '</div></div>'; 135 return html; 136 } 137 138 window.updateVariantPrice = function(radio) { 139 var price = parseInt(radio.value); 140 var label = radio.getAttribute('data-label'); 141 var btn = document.getElementById('detailBuyBtn'); 142 if (btn) { 143 var titleEl = btn.closest('.detail-info').querySelector('h2'); 144 var title = titleEl ? titleEl.textContent : ''; 145 btn.innerHTML = 'Buy Now — ₹' + price; 146 btn.setAttribute('onclick', "openUpiModal('" + encodeURIComponent(title + ' (' + label + ')') + "'," + price + ")"); 147 } 148 }; 149 150 window.openBulkInquiry = function(encodedTitle) { 151 var title = decodeURIComponent(encodedTitle); 152 var msg = 'Hi, I\'d like to request bulk pricing for "' + title + '". Please provide rates for:\n\n' + 153 '- 10 copies\n- 25 copies\n- 50 copies\n- 100+ copies\n\n' + 154 'Please reply to this email with bulk pricing. Thank you!'; 155 window.location.href = 'mailto:[email protected]?subject=' + 156 encodeURIComponent('Bulk Order Inquiry: ' + title) + 157 '&body=' + encodeURIComponent(msg); 158 }; 159 160 // ---- Reading List (localStorage) ---- 161 function getReadingList() { 162 try { return JSON.parse(localStorage.getItem('sw-reading-list')) || []; } 163 catch(e) { return []; } 164 } 165 166 function saveReadingList(list) { 167 localStorage.setItem('sw-reading-list', JSON.stringify(list)); 168 } 169 170 function isInReadingList(bookId) { 171 return getReadingList().indexOf(bookId) !== -1; 172 } 173 174 /* The save control is a ribbon on the cover, the same shape as the one in 175 the header, so pressing it and watching that icon gain a number reads as 176 one action. It used to be a third outlined button below Buy and Details, 177 where nobody found it. */ 178 function ribbonSvg(filled) { 179 return '<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" ' + 180 'fill="' + (filled ? 'currentColor' : 'none') + '" stroke="currentColor" stroke-width="2">' + 181 '<path d="M19 21l-7-5-7 5V5a2 2 0 012-2h10a2 2 0 012 2z"/></svg>'; 182 } 183 184 function ribbonHtml(bookId) { 185 var saved = isInReadingList(bookId); 186 return '<button type="button" class="rl-ribbon' + (saved ? ' rl-active' : '') + '"' + 187 ' data-book-id="' + bookId + '" aria-pressed="' + (saved ? 'true' : 'false') + '"' + 188 ' title="' + (saved ? 'In your reading list' : 'Save to reading list') + '"' + 189 ' aria-label="' + (saved ? 'Remove from reading list' : 'Save to reading list') + '"' + 190 ' onclick="event.stopPropagation();toggleReadingList(\'' + bookId + '\')">' + 191 ribbonSvg(saved) + '</button>'; 192 } 193 194 window.toggleReadingList = function(bookId) { 195 var list = getReadingList(); 196 var idx = list.indexOf(bookId); 197 if (idx === -1) { list.push(bookId); } else { list.splice(idx, 1); } 198 saveReadingList(list); 199 var inList = idx === -1; 200 201 document.querySelectorAll('.rl-ribbon[data-book-id="' + bookId + '"]').forEach(function (btn) { 202 btn.classList.toggle('rl-active', inList); 203 btn.setAttribute('aria-pressed', inList ? 'true' : 'false'); 204 btn.title = inList ? 'In your reading list' : 'Save to reading list'; 205 btn.setAttribute('aria-label', inList ? 'Remove from reading list' : 'Save to reading list'); 206 btn.innerHTML = ribbonSvg(inList); 207 }); 208 209 document.querySelectorAll('.rl-btn[data-book-id="' + bookId + '"]').forEach(function (btn) { 210 btn.classList.toggle('rl-active', inList); 211 btn.innerHTML = ribbonSvg(inList) + 212 '<span>' + (inList ? 'In your reading list' : 'Save to reading list') + '</span>'; 213 }); 214 215 updateReadingListCount(); 216 updateReadingListHint(); 217 }; 218 219 /* A one-line hint above the catalogue, for readers who have never saved a 220 book. It is not dismissible because it does not need to be: saving one 221 book is what takes it away, and by then it has said its piece. */ 222 function updateReadingListHint() { 223 var hint = document.getElementById('rlHint'); 224 if (!hint) return;
225 hint.hidden = getReadingList().length > 0; 226 } 227 228 function updateReadingListCount() { 229 var badge = document.getElementById('rlCount'); 230 if (badge) { 231 var count = getReadingList().length; 232 badge.textContent = count; 233 badge.style.display = count > 0 ? 'inline-flex' : 'none'; 234 } 235 } 236 237 // ---- Reviews (localStorage) ---- 238 function getReviews(bookId) { 239 try { return JSON.parse(localStorage.getItem('sw-reviews-' + bookId)) || []; } 240 catch(e) { return []; } 241 } 242 243 function saveReview(bookId, review) { 244 var reviews = getReviews(bookId); 245 reviews.unshift(review); 246 localStorage.setItem('sw-reviews-' + bookId, JSON.stringify(reviews)); 247 } 248 249 // ---- Load promo codes ---- 250 fetch(promosJsonPath) 251 .then(function (res) { return res.json(); }) 252 .then(function (data) { 253 promoCodes = (data.codes || []).filter(function(c) { return c.active; }); 254 }) 255 .catch(function () { promoCodes = []; }); 256 257 // ---- Load books from JSON ---- 258 fetch(booksJsonPath) 259 .then(function (res) { return res.json(); }) 260 .then(function (data) { 261 var books = data.books || data; 262 allBooks = books.filter(function(b) { return b.enabled !== false; }); 263 renderSectionedView(allBooks); 264 updateReadingListCount(); 265 updateReadingListHint(); 266 renderRecentlyViewed(); 267 handleHashRoute(); 268 }) 269 .catch(function () { 270 if (loadingMsg) loadingMsg.textContent = 'Could not load books. Please try again later.'; 271 }); 272 273 // ---- Render sectioned view (default) ----
274 function renderSectionedView(books) { 275 if (!bookGrid) return; 276 bookGrid.innerHTML = ''; 277 bookGrid.className = 'book-sections'; 278 279 sections.forEach(function(section) { 280 var sectionBooks = books.filter(function(b) { 281 return section.categories.indexOf(b.category) !== -1; 282 }); 283 if (sectionBooks.length === 0) return; 284 285 var sectionEl = document.createElement('div'); 286 sectionEl.className = 'book-section'; 287 sectionEl.id = 'section-' + section.key; 288 sectionEl.innerHTML = '<h2 class="book-section-title">' + section.title + 289 ' <span class="book-section-count">(' + sectionBooks.length + ')</span></h2>'; 290 291 var grid = document.createElement('div'); 292 grid.className = 'book-grid'; 293 sectionBooks.forEach(function(book) { grid.appendChild(createBookCard(book)); }); 294 295 sectionEl.appendChild(grid); 296 bookGrid.appendChild(sectionEl); 297 }); 298 } 299 300 // ---- Render flat filtered view ---- 301 function renderBooks(books) { 302 if (!bookGrid) return; 303 bookGrid.innerHTML = ''; 304 bookGrid.className = 'book-grid'; 305 if (books.length === 0) { 306 bookGrid.innerHTML = '<p style="text-align:center;grid-column:1/-1;color:var(--sw-muted);padding:40px;">No books found in this category.</p>'; 307 return; 308 } 309 books.forEach(function (book) { bookGrid.appendChild(createBookCard(book)); }); 310 } 311 312 // ---- Create a book card (compact, modern) ---- 313 function createBookCard(book) { 314 var card = document.createElement('div'); 315 card.className = 'book-card'; 316 card.setAttribute('data-category', book.category); 317 318 var imageHtml = book.cover 319 ? window.swCoverTag(booksPrefix, book.cover, book.title, 'thumb') 320 : '<img src="' + booksPrefix + 'images/placeholder-coming-soon.svg" alt="Coming Soon" class="placeholder-cover">'; 321 322 // Language badge 323 var langHtml = ''; 324 if (book.language && book.language !== 'english') { 325 langHtml = '<span class="book-lang-badge">' + (languageNames[book.language] || book.language) + '</span>'; 326 } 327 328 // Star ratings 329 var rating = getAverageRating(book.id); 330 var ratingHtml = ''; 331 if (rating.count > 0) { 332 ratingHtml = '<div class="book-rating"><span class="book-stars">' + renderStars(rating.avg) + '</span> <span class="book-rating-count">(' + rating.count + ')</span></div>'; 333 } else { 334 ratingHtml = '<div class="book-rating"><span class="book-stars book-stars-empty">' + renderStars(0) + '</span></div>'; 335 } 336 337 // Truncated description 338 var shortDesc = truncateText(book.description, DESC_MAX_CHARS); 339 var hasMore = book.description && book.description.length > DESC_MAX_CHARS; 340 341 // Stock / Pre-order status 342 var stockHtml = ''; 343 if (book.quantity === 0) { 344 stockHtml = '<span class="book-stock preorder">Pre-Order</span>'; 345 } else if (book.quantity > 0 && book.quantity <= 5) { 346 stockHtml = '<span class="book-stock low">Only ' + book.quantity + ' left</span>'; 347 } 348 349 // Buy button text (Pre-Order vs Buy) 350 var hasVariants = book.variants && book.variants.length > 0; 351 var buyLabel = book.quantity === 0 ? 'Pre-Order' : 'Buy ₹' + book.price; 352 // If has variants, button should open detail modal instead of quick buy 353 var buyOnclick = hasVariants 354 ? 'openProductDetail(\'' + book.id + '\')' 355 : 'openUpiModal(\'' + encodeURIComponent(book.title) + '\',' + book.price + ')'; 356 if (hasVariants) buyLabel = 'From ₹' + book.variants[0].price; 357 358 // Build card 359 card.innerHTML = 360 '<div class="book-card-image" onclick="openProductDetail(\'' + book.id + '\')">' + 361 imageHtml + 362 ribbonHtml(book.id) + 363 (book.bestseller ? '<div class="book-card-badge-top"><span class="book-badge-bestseller">Bestseller</span></div>' : '') + 364 (stockHtml ? '<div class="book-card-badge">' + stockHtml + '</div>' : '') + 365 '</div>' + 366 '<div class="book-card-body">' + 367 '<div class="book-card-meta">' + 368 '<span class="book-category">' + formatCategory(book.category) + '</span>' + 369 langHtml + 370 '</div>' + 371 '<h3 class="book-card-title" onclick="openProductDetail(\'' + book.id + '\')">' + escapeHtml(book.title) + '</h3>' + 372 '<p class="book-author">' + escapeHtml(book.author) + '</p>' + 373 ratingHtml + 374 '<div class="book-card-price">' + buildPriceHtml(book) + '</div>' + 375 '<p class="book-desc">' + escapeHtml(shortDesc) + 376 (hasMore ? ' <a href="#" onclick="openProductDetail(\'' + book.id + '\');return false;" class="read-more-link">Read more</a>' : '') + 377 '</p>' + 378 '<div class="book-card-actions">' + 379 '<button class="btn btn-buy-quick" onclick="' + buyOnclick + '">' + buyLabel + '</button>' + 380 '<button class="btn btn-details" onclick="openProductDetail(\'' + book.id + '\')">Details</button>' + 381 '</div>' + 382 '</div>'; 383 384 return card; 385 } 386
387 // ---- Product Detail Modal ---- 388 window.openProductDetail = function(bookId) { 389 var book = null; 390 for (var i = 0; i < allBooks.length; i++) { 391 if (allBooks[i].id === bookId) { book = allBooks[i]; break; } 392 } 393 if (!book || !detailModal) return; 394 395 var imageHtml = book.cover 396 ? window.swCoverTag(booksPrefix, book.cover, book.title, 'large') 397 : '<img src="' + booksPrefix + 'images/placeholder-coming-soon.svg" alt="Coming Soon" style="width:100%;border-radius:10px;">'; 398 399 var rating = getAverageRating(book.id); 400 var ratingHtml = rating.count > 0 401 ? '<div class="detail-rating"><span class="book-stars">' + renderStars(rating.avg) + '</span> <span>(' + rating.count + ' review' + (rating.count !== 1 ? 's' : '') + ')</span></div>' 402 : '<div class="detail-rating"><span class="book-stars book-stars-empty">' + renderStars(0) + '</span> <span>No reviews yet</span></div>'; 403 404 var langHtml = book.language && book.language !== 'english' 405 ? '<span class="book-lang-badge">' + (languageNames[book.language] || book.language) + '</span>' 406 : ''; 407 408 var specsText = buildSpecsHtml(book); 409 var isbnHtml = book.isbn ? '<tr><td>ISBN</td><td>' + book.isbn + '</td></tr>' : ''; 410 var weightHtml = book.weight_kg ? '<tr><td>Weight</td><td>' + book.weight_kg + ' kg</td></tr>' : ''; 411 var langRow = book.language ? '<tr><td>Language</td><td>' + (languageNames[book.language] || book.language) + '</td></tr>' : ''; 412 413 var stockLabel = ''; 414 if (book.quantity === 0) { 415 stockLabel = '<span class="book-stock preorder">Pre-Order — Print on Demand</span>'; 416 } else if (book.quantity > 0 && book.quantity <= 5) { 417 stockLabel = '<span class="book-stock low">Only ' + book.quantity + ' left in stock</span>'; 418 } else if (book.quantity > 5) { 419 stockLabel = '<span class="book-stock instock">In Stock</span>'; 420 } 421 422 var buyLabel = book.quantity === 0 ? 'Pre-Order via UPI' : 'Buy Now â ₹' + book.price; 423 424 // The modal has room for words, so here the ribbon keeps a label beside it. 425 var inList = isInReadingList(book.id); 426 var rlClass = 'rl-btn' + (inList ? ' rl-active' : ''); 427 var rlText = ribbonSvg(inList) + 428 '<span>' + (inList ? 'In your reading list' : 'Save to reading list') + '</span>'; 429 430 // Reviews 431 var reviews = getReviews(book.id); 432 var reviewsHtml = ''; 433 if (reviews.length > 0) { 434 reviewsHtml = '<div class="detail-reviews"><h4>Reviews</h4>';
435 reviews.slice(0, 5).forEach(function(r) { 436 reviewsHtml += '<div class="review-item"><div class="review-stars">' + renderStars(r.rating) + '</div>' + 437 '<p class="review-text">' + escapeHtml(r.text) + '</p>' + 438 '<p class="review-author">— ' + escapeHtml(r.name) + '</p></div>'; 439 }); 440 reviewsHtml += '</div>'; 441 } 442 443 var content = document.getElementById('productDetailContent'); 444 content.innerHTML = 445 '<div class="detail-layout">' + 446 '<div class="detail-image">' + imageHtml + '</div>' + 447 '<div class="detail-info">' + 448 '<span class="book-category">' + formatCategory(book.category) + '</span>' + 449 langHtml + 450 '<h2>' + escapeHtml(book.title) + '</h2>' + 451 '<p class="detail-author">by ' + escapeHtml(book.author) + '</p>' + 452 ratingHtml + 453 '<div class="detail-price-row">' + buildPriceHtml(book) + ' ' + stockLabel + (book.bestseller ? ' <span class="book-badge-bestseller">Amazon Bestseller</span>' : '') + '</div>' + 454 '<p class="detail-desc">' + escapeHtml(book.description || 'No description available.') + '</p>' + 455 (specsText ? '<table class="detail-specs"><tr><td>Format</td><td>' + specsText + '</td></tr>' + 456 isbnHtml + weightHtml + langRow + '</table>' : '') + 457 buildVariantsHtml(book) + 458 '<div class="detail-actions">' + 459 '<button class="btn btn-buy-now" id="detailBuyBtn" onclick="openUpiModal(\'' + 460 encodeURIComponent(book.title) + '\',' + book.price + ')">' + buyLabel + '</button>' + 461 '<button class="btn btn-add-cart" onclick="addToCart(\'' + book.id + '\',\'' + escapeHtml(book.title).replace(/'/g, "\\'") + '\',' + book.price + ')"><svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="9" cy="21" r="1"/><circle cx="20" cy="21" r="1"/><path d="M1 1h4l2.68 13.39a2 2 0 002 1.61h9.72a2 2 0 002-1.61L23 6H6"/></svg> Add to Cart</button>' + 462 '<button class="btn btn-write-review" onclick="openReviewForm(\'' + 463 encodeURIComponent(book.title) + '\',\'' + book.id + '\')">Write a Review</button>' + 464 '<button class="' + rlClass + '" data-book-id="' + book.id + '" onclick="toggleReadingList(\'' + book.id + '\')">' + rlText + '</button>' + 465 '</div>' + 466 (book.bulk_available ? '<div class="detail-bulk"><p>Need multiple copies? <a href="#" onclick="openBulkInquiry(\'' + encodeURIComponent(book.title) + '\');return false;">Request bulk pricing</a></p></div>' : '') + 467 reviewsHtml + 468 '</div>' + 469 '</div>'; 470 471 // Share buttons 472 var bookUrl = window.location.origin + window.location.pathname + '#' + book.id; 473 var shareText = book.title + ' by ' + book.author + ' â Rs.' + book.price + ' at StoryWell Books'; 474 content.innerHTML += 475 '<div class="detail-share">' + 476 '<span class="detail-share-label">Share:</span>' + 477 '<a href="https://wa.me/?text=' + encodeURIComponent(shareText + ' ' + bookUrl) + '" target="_blank" rel="noopener" class="share-btn share-wa" title="WhatsApp">' + 478 '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.148-.67.15-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.075-.297-.15-1.255-.463-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.298-.347.446-.52.149-.174.198-.298.298-.497.099-.198.05-.371-.025-.52-.075-.149-.669-1.612-.916-2.207-.242-.579-.487-.5-.669-.51-.173-.008-.371-.01-.57-.01-.198 0-.52.074-.792.372-.272.297-1.04 1.016-1.04 2.479 0 1.462 1.065 2.875 1.213 3.074.149.198 2.096 3.2 5.077 4.487.709.306 1.262.489 1.694.625.712.227 1.36.195 1.871.118.571-.085 1.758-.719 2.006-1.413.248-.694.248-1.289.173-1.413-.074-.124-.272-.198-.57-.347z"/><path d="M12 0C5.373 0 0 5.373 0 12c0 2.917 1.043 5.59 2.775 7.669L.96 24l4.477-1.77A11.94 11.94 0 0012 24c6.627 0 12-5.373 12-12S18.627 0 12 0zm0 22c-2.487 0-4.784-.803-6.656-2.163l-.474-.353-2.648 1.048.89-2.597-.387-.503A9.956 9.956 0 012 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10z"/></svg>
478' + 479 '</a>' + 480 '<a href="https://twitter.com/intent/tweet?text=' + encodeURIComponent(shareText) + '&url=' + encodeURIComponent(bookUrl) + '" target="_blank" rel="noopener" class="share-btn share-tw" title="Twitter/X">' + 481 '<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>' + 482 '</a>' + 483 '<button onclick="copyBookLink(\'' + book.id + '\')" class="share-btn share-copy" title="Copy link">' + 484 '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg>' + 485 '</button>' + 486 '</div>'; 487 488 // Related books (same category, exclude current) 489 var related = allBooks.filter(function(b) { 490 return b.category === book.category && b.id !== book.id; 491 }).slice(0, 4); 492 if (related.length > 0) { 493 var relHtml = '<div class="detail-related"><h4>You might also like</h4><div class="related-grid">'; 494 related.forEach(function(rb) { 495 var rCover = rb.cover 496 ? window.swCoverTag(booksPrefix, rb.cover, rb.title, 'thumb') 497 : '<img src="' + booksPrefix + 'images/placeholder-coming-soon.svg" alt="Coming Soon">'; 498 relHtml += '<div class="related-card" onclick="openProductDetail(\'' + rb.id + '\')">' + 499 '<div class="related-cover">' + rCover + '</div>' + 500 '<p class="related-title">' + escapeHtml(rb.title) + '</p>' + 501 '<p class="related-price">₹' + rb.price + '</p>' + 502 '</div>'; 503 }); 504 relHtml += '</div></div>'; 505 content.innerHTML += relHtml; 506 } 507 508 // Track recently viewed 509 trackRecentlyViewed(book.id); 510 511 // Update URL hash
512 history.replaceState(null, '', '#' + book.id); 513 514 detailModal.classList.add('active'); 515 document.body.style.overflow = 'hidden'; 516 }; 517 518 window.closeProductDetail = function() { 519 if (detailModal) { 520 detailModal.classList.remove('active'); 521 document.body.style.overflow = ''; 522 history.replaceState(null, '', window.location.pathname); 523 } 524 }; 525 526 window.copyBookLink = function(bookId) { 527 var url = window.location.origin + window.location.pathname + '#' + bookId; 528 navigator.clipboard.writeText(url).then(function() { 529 var btn = document.querySelector('.share-copy'); 530 if (btn) { btn.innerHTML = '✓ Copied!'; setTimeout(function() { btn.innerHTML = '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 13a5 5 0 007.54.54l3-3a5 5 0 00-7.07-7.07l-1.72 1.71"/><path d="M14 11a5 5 0 00-7.54-.54l-3 3a5 5 0 007.07 7.07l1.71-1.71"/></svg>'; }, 2000); } 531 }); 532 }; 533 534 // ---- Hash routing ---- 535 function handleHashRoute() { 536 var hash = window.location.hash.replace('#', ''); 537 if (!hash) return; 538 // Check if it's a section 539 if (hash.indexOf('section-') === 0) { 540 var el = document.getElementById(hash); 541 if (el) el.scrollIntoView({ behavior: 'smooth' }); 542 return; 543 } 544 // Otherwise try to open as a book 545 for (var i = 0; i < allBooks.length; i++) { 546 if (allBooks[i].id === hash) { 547 window.openProductDetail(hash); 548 return; 549 } 550 } 551 } 552 553 window.addEventListener('hashchange', function() { 554 handleHashRoute(); 555 }); 556 557 // ---- Recently viewed (localStorage) ---- 558 function trackRecentlyViewed(bookId) { 559 var key = 'sw-recently-viewed'; 560 var list = []; 561 try { list = JSON.parse(localStorage.getItem(key)) || []; } catch(e) {} 562 list = list.filter(function(id) { return id !== bookId; }); 563 list.unshift(bookId); 564 if (list.length > 8) list = list.slice(0, 8); 565 localStorage.setItem(key, JSON.stringify(list)); 566 } 567 568 function renderRecentlyViewed() { 569 var container = document.getElementById('recentlyViewed'); 570 if (!container) return; 571 var list = []; 572 try { list = JSON.parse(localStorage.getItem('sw-recently-viewed')) || []; } catch(e) {} 573 if (list.length === 0) { container.style.display = 'none'; return; } 574 var recentBooks = []; 575 list.forEach(function(id) { 576 for (var i = 0; i < allBooks.length; i++) { 577 if (allBooks[i].id === id) { recentBooks.push(allBooks[i]); break; } 578 } 579 }); 580 if (recentBooks.length === 0) { container.style.display = 'none'; return; } 581 container.style.display = 'block'; 582 var grid = container.querySelector('.recent-grid'); 583 if (!grid) return; 584 grid.innerHTML = ''; 585 recentBooks.slice(0, 6).forEach(function(book) { 586 var cover = book.cover 587 ? window.swCoverTag(booksPrefix, book.cover, book.title, 'thumb') 588 : '<img src="' + booksPrefix + 'images/placeholder-coming-soon.svg" alt="Coming Soon">'; 589 var card = document.createElement('div'); 590 card.className = 'related-card'; 591 card.onclick = function() { window.openProductDetail(book.id); }; 592 card.innerHTML = '<div class="related-cover">' + cover + '</div>' + 593 '<p class="related-title">' + escapeHtml(book.title) + '</p>' + 594 '<p class="related-price">₹' + book.price + '</p>'; 595 grid.appendChild(card); 596 }); 597 } 598 599 // ---- Search ---- 600 var searchInput = document.getElementById('bookSearch'); 601 if (searchInput) { 602 searchInput.addEventListener('input', function() { 603 var query = searchInput.value.toLowerCase().trim(); 604 if (query.length === 0) { 605 renderSectionedView(allBooks); 606 return; 607 } 608 var results = allBooks.filter(function(b) { 609 return b.title.toLowerCase().indexOf(query) !== -1 || 610 b.author.toLowerCase().indexOf(query) !== -1 || 611 (b.description || '').toLowerCase().indexOf(query) !== -1 || 612 (b.isbn || '').indexOf(query) !== -1 || 613 b.category.indexOf(query) !== -1; 614 }); 615 renderBooks(results); 616 }); 617 } 618 619 if (detailModal) { 620 detailModal.addEventListener('click', function(e) { 621 if (e.target === detailModal) window.closeProductDetail(); 622 }); 623 } 624 625 // ---- Category filtering ---- 626 if (categoryFilter) { 627 categoryFilter.addEventListener('click', function (e) { 628 if (e.target.tagName !== 'BUTTON') return; 629 categoryFilter.querySelectorAll('button').forEach(function (btn) { btn.classList.remove('active'); }); 630 e.target.classList.add('active'); 631 var cat = e.target.getAttribute('data-category'); 632 if (cat === 'all') { 633 renderSectionedView(allBooks); 634 } else { 635 renderBooks(allBooks.filter(function (b) { return b.category === cat; })); 636 } 637 }); 638 } 639 640 // ---- Promo code validation ---- 641 window.applyPromoCode = function(inputId, amountId, originalPrice) { 642 var input = document.getElementById(inputId); 643 var amountEl = document.getElementById(amountId); 644 if (!input || !amountEl) return; 645 646 var code = input.value.trim().toUpperCase(); 647 if (!code) { alert('Please enter a promo code.'); return; } 648 649 var now = new Date(); 650 var matched = null; 651 for (var i = 0; i < promoCodes.length; i++) { 652 if (promoCodes[i].code.toUpperCase() === code) { 653 var promo = promoCodes[i]; 654 if (promo.valid_from && new Date(promo.valid_from) > now) continue; 655 if (promo.valid_until && new Date(promo.valid_until) < now) continue; 656 if (promo.min_order && originalPrice < promo.min_order) { 657 alert('Minimum order of \u20B9' + promo.min_order + ' required for this code.'); 658 return; 659 } 660 matched = promo; 661 break; 662 } 663 } 664 665 if (!matched) { alert('Invalid or expired promo code.'); return; } 666 667 var discount = matched.type === 'percent' 668 ? Math.round(originalPrice * matched.value / 100) 669 : matched.value; 670 671 var finalPrice = Math.max(0, originalPrice - discount); 672 currentFinalPrice = finalPrice; 673 currentPromo = matched; 674 amountEl.innerHTML = '₹' + finalPrice + 675 ' <span style="text-decoration:line-through;color:var(--sw-muted);font-size:0.85em;">₹' + originalPrice + '</span>' + 676 ' <span style="color:var(--sw-teal);font-size:0.85em;">(' + matched.description + ')</span>'; 677 678 var orderAmount = document.getElementById('orderAmount'); 679 if (orderAmount) orderAmount.value = finalPrice; 680 681 input.disabled = true; 682 input.style.opacity = '0.6'; 683 }; 684 685 // ---- Review form ---- 686 window.openReviewForm = function(encodedTitle, bookId) { 687 var modal = document.getElementById('reviewModal'); 688 if (modal) { 689 document.getElementById('reviewBookTitle').textContent = decodeURIComponent(encodedTitle); 690 document.getElementById('reviewBookId').value = bookId; 691 document.getElementById('reviewRating').value = ''; 692 document.getElementById('reviewForm').reset(); 693 modal.classList.add('active'); 694 modal.querySelectorAll('.review-star-btn').forEach(function(b) { b.classList.remove('selected'); }); 695 } 696 }; 697 698 var reviewForm = document.getElementById('reviewForm'); 699 if (reviewForm) { 700 var reviewStars = document.getElementById('reviewStarsInput'); 701 if (reviewStars) { 702 reviewStars.addEventListener('click', function(e) { 703 if (e.target.tagName !== 'BUTTON') return; 704 var rating = parseInt(e.target.getAttribute('data-rating')); 705 document.getElementById('reviewRating').value = rating; 706 reviewStars.querySelectorAll('button').forEach(function(b) { 707 b.classList.toggle('selected', parseInt(b.getAttribute('data-rating')) <= rating); 708 }); 709 }); 710 } 711 712 reviewForm.addEventListener('submit', function(e) { 713 e.preventDefault(); 714 var bookId = document.getElementById('reviewBookId').value; 715 var rating = parseInt(document.getElementById('reviewRating').value); 716 var name = document.getElementById('reviewName').value.trim(); 717 var text = document.getElementById('reviewText').value.trim(); 718 719 if (!rating || !name || !text) { 720 alert('Please fill in all fields and select a rating.'); 721 return; 722 } 723 724 saveReview(bookId, { rating: rating, name: name, text: text, date: new Date().toISOString() }); 725 726 // Also submit to Netlify 727 var formData = new FormData(reviewForm); 728 fetch('/', { 729 method: 'POST', 730 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 731 body: new URLSearchParams(formData).toString() 732 }).catch(function() {}); 733 734 var modal = document.getElementById('reviewModal'); 735 if (modal) modal.classList.remove('active');
736 alert('Thank you for your review!'); 737 738 // Refresh the current view 739 renderSectionedView(allBooks); 740 }); 741 } 742 743 // Close review modal 744 var reviewModalClose = document.getElementById('reviewModalClose'); 745 if (reviewModalClose) { 746 reviewModalClose.addEventListener('click', function() { 747 document.getElementById('reviewModal').classList.remove('active'); 748 }); 749 } 750 var reviewModal = document.getElementById('reviewModal'); 751 if (reviewModal) { 752 reviewModal.addEventListener('click', function(e) { 753 if (e.target === reviewModal) reviewModal.classList.remove('active'); 754 }); 755 } 756 757 // ---- Reading List Page ---- 758 var rlGrid = document.getElementById('readingListGrid'); 759 if (rlGrid) { 760 var list = getReadingList(); 761 if (list.length === 0) { 762 rlGrid.innerHTML = '<p style="text-align:center;color:var(--sw-muted);padding:40px;">Your reading list is empty. Browse our <a href="books.html">catalog</a> to add books!</p>'; 763 } else { 764 fetch(booksJsonPath) 765 .then(function(res) { return res.json(); }) 766 .then(function(data) { 767 var books = data.books || data; 768 var listBooks = books.filter(function(b) { return list.indexOf(b.id) !== -1; }); 769 rlGrid.innerHTML = ''; 770 listBooks.forEach(function(book) { rlGrid.appendChild(createBookCard(book)); }); 771 }); 772 } 773 } 774 775 // ---- UPI Payment Modal (3-step: shipping details -> pay -> done) ---- 776 var step1 = document.getElementById('upiStep1'); 777 var step2 = document.getElementById('upiStep2'); 778 var step3 = document.getElementById('upiStep3'); 779 var toPaymentBtn = document.getElementById('toPaymentBtn'); 780 var backToDetailsBtn = document.getElementById('backToDetailsBtn'); 781 var orderForm = document.getElementById('orderForm'); 782 var orderBook = document.getElementById('orderBook'); 783 var orderAmount = document.getElementById('orderAmount'); 784 var orderSummary = document.getElementById('orderSummary'); 785 var payAmount = document.getElementById('payAmount'); 786 787 window.currentModalPrice = 0; 788 789 var MONTHS_SHORT = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; 790 function formatShortDate(d) { 791 return d.getDate() + ' ' + MONTHS_SHORT[d.getMonth()] + ' ' + d.getFullYear(); 792 } 793 794 // Delivery window: 7-10 days from date of purchase 795 function deliveryWindow(from) { 796 var start = new Date(from.getTime() + 7 * 86400000); 797 var end = new Date(from.getTime() + 10 * 86400000); 798 return formatShortDate(start) + ' \u2013 ' + formatShortDate(end); 799 } 800 801 function itemsLabel() { 802 return currentOrderItems.map(function (it) { 803 return it.title + (it.qty > 1 ? ' x' + it.qty : ''); 804 }).join(', '); 805 } 806 807 function resetModal() { 808 if (step1) step1.style.display = 'block'; 809 if (step2) step2.style.display = 'none'; 810 if (step3) step3.style.display = 'none'; 811 if (orderForm) orderForm.reset(); 812 var promoInput = document.getElementById('promoCodeInput'); 813 if (promoInput) { promoInput.value = ''; promoInput.disabled = false; promoInput.style.opacity = '1'; } 814 } 815 816 // items: optional [{ title, qty, price }] \u2014 passed by the cart; single buys build their own 817 window.openUpiModal = function (encodedTitle, price, items) { 818 var title = decodeURIComponent(encodedTitle); 819 window.currentModalPrice = price; 820 currentFinalPrice = price; 821 currentPromo = null; 822 currentOrderItems = (items && items.length) ? items : [{ title: title, qty: 1, price: price }]; 823 currentOrderFromCart = !!(items && items.length); 824 resetModal(); 825 826 // Close detail modal if open 827 if (detailModal && detailModal.classList.contains('active')) { 828 detailModal.classList.remove('active'); 829 } 830 831 modalBookTitle.textContent = title; 832 modalAmount.innerHTML = '₹' + price; 833 834 if (orderBook) orderBook.value = title; 835 if (orderAmount) orderAmount.value = price; 836 837 upiModal.classList.add('active'); 838 document.body.style.overflow = 'hidden'; 839 }; 840 841 // Build the payment screen from the current (post-promo) price 842 function showPaymentStep() { 843 var label = itemsLabel(); 844 var tn = 'StoryWell: ' + label + (currentPromo ? ' [' + currentPromo.code + ']' : ''); 845 var upiUrl = 'upi://pay?pa=' + encodeURIComponent(UPI_ID) + 846 '&pn=' + encodeURIComponent(UPI_NAME) + 847 '&am=' + currentFinalPrice + 848 '&cu=INR' + 849 '&tn=' + encodeURIComponent(tn); 850 851 if (modalUpiId) modalUpiId.textContent = UPI_ID; 852 if (payAmount) payAmount.innerHTML = '₹' + currentFinalPrice; 853 if (orderSummary) { 854 orderSummary.textContent = label + ' \u2014 \u20B9' + currentFinalPrice + 855 (currentPromo ? ' (code: ' + currentPromo.code + ')' : ''); 856 } 857 if (upiDeepLink) upiDeepLink.href = upiUrl; 858 859 if (typeof qrcode !== 'undefined' && modalQr) { 860 var qr = qrcode(0, 'M'); 861 qr.addData(upiUrl); 862 qr.make(); 863 modalQr.innerHTML = qr.createSvgTag(5); 864 } else if (modalQr) { 865 modalQr.innerHTML = '<p style="color:var(--sw-muted);font-size:0.9rem;">QR code unavailable. Use the button below.</p>'; 866 } 867 868 if (step1) step1.style.display = 'none'; 869 if (step2) step2.style.display = 'block'; 870 } 871 872 if (toPaymentBtn) { 873 toPaymentBtn.addEventListener('click', function () { 874 // Shipping details must be valid before payment is shown 875 if (orderForm && !orderForm.reportValidity()) return; 876 showPaymentStep(); 877 }); 878 } 879 880 if (backToDetailsBtn) { 881 backToDetailsBtn.addEventListener('click', function () { 882 if (step2) step2.style.display = 'none'; 883 if (step1) step1.style.display = 'block'; 884 }); 885 } 886 887 if (orderForm) { 888 orderForm.addEventListener('submit', function (e) { 889 e.preventDefault(); 890 var formData = new FormData(orderForm); 891 892 // Generate order number: SWB-YYMMDD-XXXX 893 var now = new Date(); 894 var yy = String(now.getFullYear()).slice(2); 895 var mm = String(now.getMonth() + 1).padStart(2, '0'); 896 var dd = String(now.getDate()).padStart(2, '0'); 897 var rand = String(Math.floor(1000 + Math.random() * 9000)); 898 var orderNumber = 'SWB-' + yy + mm + dd + '-' + rand; 899 900 var expectedDelivery = deliveryWindow(now); 901 var itemsSummary = currentOrderItems.map(function (it) { 902 return it.title + ' x' + it.qty + ' @ \u20B9' + it.price; 903 }).join('; '); 904 905 formData.set('order_number', orderNumber); 906 formData.set('amount', currentFinalPrice); 907 formData.set('items', itemsSummary); 908 formData.set('expected_delivery', expectedDelivery); 909
910 // Also send to Google Sheets if configured (drives invoice emails) 911 if (SHEETS_ENDPOINT) { 912 var promoInput = document.getElementById('promoCodeInput'); 913 fetch(SHEETS_ENDPOINT, { 914 method: 'POST', 915 mode: 'no-cors', 916 headers: { 'Content-Type': 'application/json' }, 917 body: JSON.stringify({ 918 order_number: orderNumber, 919 book: itemsLabel(), 920 amount: currentFinalPrice, 921 promo_code: (currentPromo && currentPromo.code) || (promoInput && promoInput.value) || '', 922 name: formData.get('name') || '', 923 email: formData.get('email') || '', 924 phone: formData.get('phone') || '', 925 address: formData.get('address') || '', 926 transaction_id: formData.get('transaction_id') || '', 927 notes: formData.get('notes') || '', 928 items: currentOrderItems, 929 order_date: now.toISOString(), 930 expected_delivery: expectedDelivery 931 }) 932 }).catch(function() {}); 933 } 934 935 // Submit to Netlify forms (primary) 936 fetch('/', { 937 method: 'POST', 938 headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, 939 body: new URLSearchParams(formData).toString() 940 }).then(function () { 941 if (step1) step1.style.display = 'none'; 942 if (step2) step2.style.display = 'none'; 943 if (step3) step3.style.display = 'block'; 944 var orderNumEl = document.getElementById('orderNumberDisplay'); 945 if (orderNumEl) orderNumEl.textContent = orderNumber; 946 var deliveryEl = document.getElementById('deliveryWindowDisplay'); 947 if (deliveryEl) deliveryEl.textContent = 'Expected delivery: ' + expectedDelivery + ' (7\u201310 days from purchase)'; 948 if (currentOrderFromCart && typeof window.clearCart === 'function') window.clearCart(); 949 }).catch(function () { 950 alert('Something went wrong. Please email [email protected] with your order details.'); 951 }); 952 }); 953 } 954 955 if (closeModal) { 956 closeModal.addEventListener('click', function () { 957 upiModal.classList.remove('active'); 958 document.body.style.overflow = ''; 959 resetModal(); 960 }); 961 } 962 963 if (upiModal) { 964 upiModal.addEventListener('click', function (e) { 965 if (e.target === upiModal) { 966 upiModal.classList.remove('active'); 967 document.body.style.overflow = ''; 968 resetModal(); 969 } 970 }); 971 } 972 973 document.addEventListener('keydown', function (e) { 974 if (e.key === 'Escape') { 975 if (detailModal && detailModal.classList.contains('active')) { 976 window.closeProductDetail(); 977 } else if (upiModal && upiModal.classList.contains('active')) { 978 upiModal.classList.remove('active'); 979 document.body.style.overflow = ''; 980 resetModal(); 981 } else if (reviewModal && reviewModal.classList.contains('active')) { 982 reviewModal.classList.remove('active'); 983 } 984 } 985 }); 986})();
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.