@gaulatti/giorgia 0.1.57

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -0
  3. package/dist/carousels.d.ts +1 -0
  4. package/dist/carousels.js +65 -0
  5. package/dist/components/live-program/LiveProgram.d.ts +9 -0
  6. package/dist/components/live-program/LiveProgram.js +751 -0
  7. package/dist/components/live-program/assets.d.ts +14 -0
  8. package/dist/components/live-program/assets.js +14 -0
  9. package/dist/components/live-program/components/Marquee.d.ts +16 -0
  10. package/dist/components/live-program/components/Marquee.js +88 -0
  11. package/dist/components/live-program/components/MarqueeCurtain.d.ts +5 -0
  12. package/dist/components/live-program/components/MarqueeCurtain.js +30 -0
  13. package/dist/components/live-program/components/WorldClocks.d.ts +19 -0
  14. package/dist/components/live-program/components/WorldClocks.js +101 -0
  15. package/dist/components/live-program/components/slides/ArticleSlide.d.ts +14 -0
  16. package/dist/components/live-program/components/slides/ArticleSlide.js +32 -0
  17. package/dist/components/live-program/components/slides/CallsignSlide.d.ts +6 -0
  18. package/dist/components/live-program/components/slides/CallsignSlide.js +49 -0
  19. package/dist/components/live-program/components/slides/LiveEventSlide.d.ts +9 -0
  20. package/dist/components/live-program/components/slides/LiveEventSlide.js +86 -0
  21. package/dist/components/live-program/components/slides/slideStyles.d.ts +1 -0
  22. package/dist/components/live-program/components/slides/slideStyles.js +64 -0
  23. package/dist/components/live-program/events.d.ts +34 -0
  24. package/dist/components/live-program/events.js +167 -0
  25. package/dist/components/live-program/hooks/useSSE.d.ts +11 -0
  26. package/dist/components/live-program/hooks/useSSE.js +107 -0
  27. package/dist/components/live-program/i18n.d.ts +4 -0
  28. package/dist/components/live-program/i18n.js +305 -0
  29. package/dist/components/live-program/segments/ArticlesSegment.d.ts +6 -0
  30. package/dist/components/live-program/segments/ArticlesSegment.js +160 -0
  31. package/dist/components/live-program/segments/EarthquakeSegment.d.ts +16 -0
  32. package/dist/components/live-program/segments/EarthquakeSegment.js +130 -0
  33. package/dist/components/live-program/segments/LiveEventSegment.d.ts +4 -0
  34. package/dist/components/live-program/segments/LiveEventSegment.js +27 -0
  35. package/dist/components/live-program/segments/MarketsSegment.d.ts +12 -0
  36. package/dist/components/live-program/segments/MarketsSegment.js +76 -0
  37. package/dist/components/live-program/segments/WeatherSegment.d.ts +15 -0
  38. package/dist/components/live-program/segments/WeatherSegment.js +119 -0
  39. package/dist/components/live-program/segments/fetchLiveEvents.d.ts +20 -0
  40. package/dist/components/live-program/segments/fetchLiveEvents.js +76 -0
  41. package/dist/components/live-program/segments/index.d.ts +8 -0
  42. package/dist/components/live-program/segments/index.js +8 -0
  43. package/dist/components/live-program/segments/types.d.ts +23 -0
  44. package/dist/components/live-program/segments/types.js +1 -0
  45. package/dist/components/live-program/segments/usePlaylistEngine.d.ts +9 -0
  46. package/dist/components/live-program/segments/usePlaylistEngine.js +108 -0
  47. package/dist/components/live-program/utils/broadcastTime.d.ts +12 -0
  48. package/dist/components/live-program/utils/broadcastTime.js +33 -0
  49. package/dist/fonts/LDIcapOFNxEwR-Bd1O9uYNmnUQomAgE25imKSbHhROjLsZBWTSrQGD_jZtU.ttf +0 -0
  50. package/dist/fonts/LDIcapOFNxEwR-Bd1O9uYNmnUQomAgE25imKSbHhROjLsZBWTSrQGFPjZtU.ttf +0 -0
  51. package/dist/fonts/LDIcapOFNxEwR-Bd1O9uYNmnUQomAgE25imKSbHhROjLsZBWTSrQGGHiZtU.ttf +0 -0
  52. package/dist/fonts/LDIcapOFNxEwR-Bd1O9uYNmnUQomAgE25imKSbHhROjLsZBWTSrQGGHjZtU.ttf +0 -0
  53. package/dist/fonts/LDIcapOFNxEwR-Bd1O9uYNmnUQomAgE25imKSbHhROjLsZBWTSrQGIbkZtU.ttf +0 -0
  54. package/dist/fonts/LDIcapOFNxEwR-Bd1O9uYNmnUQomAgE25imKSbHhROjLsZBWTSrQGL_kZtU.ttf +0 -0
  55. package/dist/fonts/LDIcapOFNxEwR-Bd1O9uYNmnUQomAgE25imKSbHhROjLsZBWTSrQGMjkZtU.ttf +0 -0
  56. package/dist/fonts/LDIcapOFNxEwR-Bd1O9uYNmnUQomAgE25imKSbHhROjLsZBWTSrQGOHjZtU.ttf +0 -0
  57. package/dist/fonts/LDIcapOFNxEwR-Bd1O9uYNmnUQomAgE25imKSbHhROjLsZBWTSrQGOHkZtU.ttf +0 -0
  58. package/dist/fonts/SlGDmQSNjdsmc35JDF1K5E55YMjF_7DPuGi-2fRUAw.ttf +0 -0
  59. package/dist/fonts/SlGDmQSNjdsmc35JDF1K5E55YMjF_7DPuGi-6_RUAw.ttf +0 -0
  60. package/dist/fonts/SlGDmQSNjdsmc35JDF1K5E55YMjF_7DPuGi-DPNUAw.ttf +0 -0
  61. package/dist/fonts/SlGDmQSNjdsmc35JDF1K5E55YMjF_7DPuGi-NfNUAw.ttf +0 -0
  62. package/dist/fonts/SlGDmQSNjdsmc35JDF1K5E55YMjF_7DPuGi-a_NUAw.ttf +0 -0
  63. package/dist/fonts/SlGFmQSNjdsmc35JDF1K5GRwUjcdlttVFm-rI7c8R496.ttf +0 -0
  64. package/dist/fonts/SlGFmQSNjdsmc35JDF1K5GRwUjcdlttVFm-rI7dbR496.ttf +0 -0
  65. package/dist/fonts/SlGFmQSNjdsmc35JDF1K5GRwUjcdlttVFm-rI7diR496.ttf +0 -0
  66. package/dist/fonts/SlGFmQSNjdsmc35JDF1K5GRwUjcdlttVFm-rI7e8QI96.ttf +0 -0
  67. package/dist/fonts/SlGFmQSNjdsmc35JDF1K5GRwUjcdlttVFm-rI7eOQI96.ttf +0 -0
  68. package/dist/fonts/TK3_WkUHHAIjg75cFRf3bXL8LICs13FvgUE.ttf +0 -0
  69. package/dist/fonts/TK3_WkUHHAIjg75cFRf3bXL8LICs169vgUE.ttf +0 -0
  70. package/dist/fonts/TK3_WkUHHAIjg75cFRf3bXL8LICs18NvgUE.ttf +0 -0
  71. package/dist/fonts/TK3_WkUHHAIjg75cFRf3bXL8LICs1_FvgUE.ttf +0 -0
  72. package/dist/fonts/TK3_WkUHHAIjg75cFRf3bXL8LICs1xZogUE.ttf +0 -0
  73. package/dist/fonts/TK3_WkUHHAIjg75cFRf3bXL8LICs1y9ogUE.ttf +0 -0
  74. package/dist/fonts/fonts.css +378 -0
  75. package/dist/fonts/j8_16_LD37rqfuwxyIuaZhE6cRXOLtm2gfTGgQ.ttf +0 -0
  76. package/dist/fonts/j8_46_LD37rqfuwxyIuaZhE6cRXOLtm2gfT-BYipBw.ttf +0 -0
  77. package/dist/fonts/j8_46_LD37rqfuwxyIuaZhE6cRXOLtm2gfT-IYmpBw.ttf +0 -0
  78. package/dist/fonts/j8_46_LD37rqfuwxyIuaZhE6cRXOLtm2gfT-PYqpBw.ttf +0 -0
  79. package/dist/fonts/j8_46_LD37rqfuwxyIuaZhE6cRXOLtm2gfT-WYupBw.ttf +0 -0
  80. package/dist/fonts/jizMREVItHgc8qDIbSTKq4XkRiUawTk7f45UM9y05oYiRNDM.ttf +0 -0
  81. package/dist/fonts/jizMREVItHgc8qDIbSTKq4XkRiUawTk7f45UM9y05oZ8RNDM.ttf +0 -0
  82. package/dist/fonts/jizMREVItHgc8qDIbSTKq4XkRiUawTk7f45UM9y05oZ8RdDM.ttf +0 -0
  83. package/dist/fonts/jizMREVItHgc8qDIbSTKq4XkRiUawTk7f45UM9y05oZORNDM.ttf +0 -0
  84. package/dist/fonts/jizMREVItHgc8qDIbSTKq4XkRiUawTk7f45UM9y05oabQ9DM.ttf +0 -0
  85. package/dist/fonts/jizMREVItHgc8qDIbSTKq4XkRiUawTk7f45UM9y05oaiQ9DM.ttf +0 -0
  86. package/dist/fonts/jizMREVItHgc8qDIbSTKq4XkRiUawTk7f45UM9y05ob8Q9DM.ttf +0 -0
  87. package/dist/fonts/jizMREVItHgc8qDIbSTKq4XkRiUawTk7f45UM9y05ob8RNDM.ttf +0 -0
  88. package/dist/fonts/jizMREVItHgc8qDIbSTKq4XkRiUawTk7f45UM9y05obVQ9DM.ttf +0 -0
  89. package/dist/fonts/jizOREVItHgc8qDIbSTKq4XkRg8T88bjFuXOnduh8MKUBw.ttf +0 -0
  90. package/dist/fonts/jizOREVItHgc8qDIbSTKq4XkRg8T88bjFuXOnduhHMWUBw.ttf +0 -0
  91. package/dist/fonts/jizOREVItHgc8qDIbSTKq4XkRg8T88bjFuXOnduhLsSUBw.ttf +0 -0
  92. package/dist/fonts/jizOREVItHgc8qDIbSTKq4XkRg8T88bjFuXOnduhLsWUBw.ttf +0 -0
  93. package/dist/fonts/jizOREVItHgc8qDIbSTKq4XkRg8T88bjFuXOnduhcMWUBw.ttf +0 -0
  94. package/dist/fonts/jizOREVItHgc8qDIbSTKq4XkRg8T88bjFuXOnduhh8KUBw.ttf +0 -0
  95. package/dist/fonts/jizOREVItHgc8qDIbSTKq4XkRg8T88bjFuXOnduhrsKUBw.ttf +0 -0
  96. package/dist/fonts/jizOREVItHgc8qDIbSTKq4XkRg8T88bjFuXOnduhrsWUBw.ttf +0 -0
  97. package/dist/fonts/jizOREVItHgc8qDIbSTKq4XkRg8T88bjFuXOnduhycKUBw.ttf +0 -0
  98. package/dist/homepage-distributor.d.ts +55 -0
  99. package/dist/homepage-distributor.js +68 -0
  100. package/dist/instagram-image-template.d.ts +9 -0
  101. package/dist/instagram-image-template.js +51 -0
  102. package/dist/live-program/main.d.ts +1 -0
  103. package/dist/live-program/main.js +10 -0
  104. package/dist/live-program-page/fifthbell/audio/pipes.ogg +0 -0
  105. package/dist/live-program-page/fifthbell/images/berlin.jpg +0 -0
  106. package/dist/live-program-page/fifthbell/images/fifthbell.png +0 -0
  107. package/dist/live-program-page/fifthbell/images/nyc.jpg +0 -0
  108. package/dist/live-program-page/fifthbell/images/nyse.jpg +0 -0
  109. package/dist/live-program-page/fifthbell/images/santiago.jpg +0 -0
  110. package/dist/live-program-page/fifthbell/images/seismograph.jpg +0 -0
  111. package/dist/live-program-page/fifthbell/images/tokyo.jpg +0 -0
  112. package/dist/live-program-page/index.html +16 -0
  113. package/dist/live-program-page/live-program.css +3 -0
  114. package/dist/live-program-page/live-program.js +76 -0
  115. package/dist/outlet-config.d.ts +29 -0
  116. package/dist/outlet-config.js +29 -0
  117. package/dist/renderer.browser.d.ts +2 -0
  118. package/dist/renderer.browser.js +132 -0
  119. package/dist/renderer.core.d.ts +9 -0
  120. package/dist/renderer.core.js +354 -0
  121. package/dist/renderer.d.ts +6 -0
  122. package/dist/renderer.js +4 -0
  123. package/dist/renderer.node.d.ts +16 -0
  124. package/dist/renderer.node.js +166 -0
  125. package/dist/types/canonical-article.d.ts +249 -0
  126. package/dist/types/canonical-article.js +236 -0
  127. package/dist/utils/sofascore.d.ts +3 -0
  128. package/dist/utils/sofascore.js +31 -0
  129. package/dist/version.d.ts +1 -0
  130. package/dist/version.js +4 -0
  131. package/package.json +89 -0
  132. package/src/partial-deps.json +53 -0
  133. package/src/styles/compiled.css +2 -0
  134. package/src/templates/layouts/404.hbs +5 -0
  135. package/src/templates/layouts/article-page.hbs +5 -0
  136. package/src/templates/layouts/category-page.hbs +5 -0
  137. package/src/templates/layouts/homepage.hbs +5 -0
  138. package/src/templates/layouts/link-in-bio.hbs +226 -0
  139. package/src/templates/layouts/live-story.hbs +5 -0
  140. package/src/templates/layouts/media-page.hbs +5 -0
  141. package/src/templates/layouts/search-page.hbs +5 -0
  142. package/src/templates/partials/blocks/audio.hbs +12 -0
  143. package/src/templates/partials/blocks/data-table.hbs +23 -0
  144. package/src/templates/partials/blocks/divider.hbs +1 -0
  145. package/src/templates/partials/blocks/heading.hbs +9 -0
  146. package/src/templates/partials/blocks/image.hbs +6 -0
  147. package/src/templates/partials/blocks/info-box.hbs +8 -0
  148. package/src/templates/partials/blocks/instagram.hbs +28 -0
  149. package/src/templates/partials/blocks/key-points.hbs +8 -0
  150. package/src/templates/partials/blocks/list.hbs +13 -0
  151. package/src/templates/partials/blocks/live-update.hbs +24 -0
  152. package/src/templates/partials/blocks/pull-quote.hbs +6 -0
  153. package/src/templates/partials/blocks/rich-text.hbs +1 -0
  154. package/src/templates/partials/blocks/tiktok.hbs +15 -0
  155. package/src/templates/partials/blocks/x.hbs +74 -0
  156. package/src/templates/partials/blocks/youtube.hbs +12 -0
  157. package/src/templates/partials/components/article-main.hbs +159 -0
  158. package/src/templates/partials/components/breaking-news/live-updates-column.hbs +29 -0
  159. package/src/templates/partials/components/breaking-news.hbs +56 -0
  160. package/src/templates/partials/components/category/header.hbs +5 -0
  161. package/src/templates/partials/components/category/main-grid.hbs +55 -0
  162. package/src/templates/partials/components/category/main.hbs +7 -0
  163. package/src/templates/partials/components/category/more-grid.hbs +26 -0
  164. package/src/templates/partials/components/editorial-hero.hbs +73 -0
  165. package/src/templates/partials/components/headline.hbs +15 -0
  166. package/src/templates/partials/components/hero-editorial.hbs +1 -0
  167. package/src/templates/partials/components/hero.hbs +1 -0
  168. package/src/templates/partials/components/home/landing.hbs +111 -0
  169. package/src/templates/partials/components/home/main.hbs +63 -0
  170. package/src/templates/partials/components/home/more-stories.hbs +23 -0
  171. package/src/templates/partials/components/home/must-read.hbs +77 -0
  172. package/src/templates/partials/components/live-story/main.hbs +229 -0
  173. package/src/templates/partials/components/media/main.hbs +220 -0
  174. package/src/templates/partials/components/not-found/main.hbs +28 -0
  175. package/src/templates/partials/components/search/main.hbs +420 -0
  176. package/src/templates/partials/components/snack.hbs +92 -0
  177. package/src/templates/partials/components/spotlight-hero.hbs +59 -0
  178. package/src/templates/partials/components/trending.hbs +14 -0
  179. package/src/templates/partials/components/ui/accordion.hbs +30 -0
  180. package/src/templates/partials/components/ui/breadcrumb.hbs +16 -0
  181. package/src/templates/partials/components/ui/icon-button.hbs +19 -0
  182. package/src/templates/partials/components/ui/loading-spinner.hbs +27 -0
  183. package/src/templates/partials/components/ui/pagination.hbs +56 -0
  184. package/src/templates/partials/components/ui/scroll-area.hbs +12 -0
  185. package/src/templates/partials/components/ui/status-badge.hbs +21 -0
  186. package/src/templates/partials/footers/footer-full.hbs +79 -0
  187. package/src/templates/partials/footers/footer-minimal.hbs +5 -0
  188. package/src/templates/partials/headers/header-main.hbs +400 -0
  189. package/src/templates/partials/headers/header-minimal.hbs +16 -0
  190. package/src/templates/partials/nav/nav-categories.hbs +5 -0
  191. package/src/templates/partials/shell/doc-end.hbs +282 -0
  192. package/src/templates/partials/shell/doc-start-404.hbs +41 -0
  193. package/src/templates/partials/shell/doc-start-standard.hbs +86 -0
  194. package/src/templates/templates/instagram-image.hbs +175 -0
@@ -0,0 +1,220 @@
1
+ <main id='main-content' class='flex-1 bg-slate-50 dark:bg-slate-950' data-media-page>
2
+ <section class='mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8'>
3
+ <div class='mb-6 flex flex-col gap-3 border-b border-slate-200 pb-5 dark:border-slate-800 md:flex-row md:items-end md:justify-between'>
4
+ <div>
5
+ <p class='text-xs font-semibold uppercase tracking-widest text-red-700 dark:text-red-400'>Assignment Media</p>
6
+ <h1 id='media-title' class='mt-2 font-serif text-4xl font-bold leading-tight text-slate-950 dark:text-white md:text-5xl'>Photos</h1>
7
+ <p id='media-meta' class='mt-2 max-w-2xl text-sm text-slate-600 dark:text-slate-400'>Loading assignment photos.</p>
8
+ </div>
9
+ <a id='media-refresh' href='' class='inline-flex items-center justify-center border border-slate-300 px-4 py-2 text-sm font-semibold text-slate-800 transition hover:border-slate-950 hover:text-slate-950 dark:border-slate-700 dark:text-slate-200 dark:hover:border-white dark:hover:text-white'>Refresh</a>
10
+ </div>
11
+ <div id='media-status' class='border border-slate-200 bg-white p-6 text-sm text-slate-600 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-300'>Loading photos.</div>
12
+ <div id='media-grid' class='hidden grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'></div>
13
+ <nav id='media-pagination' aria-label='Pagination' class='mt-8 hidden items-center justify-center gap-6'></nav>
14
+ </section>
15
+ </main>
16
+ <script>
17
+ (function () {
18
+ var PAGE_SIZE = 48;
19
+ var page = document.querySelector('[data-media-page]');
20
+ if (!page) return;
21
+
22
+ var title = document.getElementById('media-title');
23
+ var meta = document.getElementById('media-meta');
24
+ var status = document.getElementById('media-status');
25
+ var grid = document.getElementById('media-grid');
26
+ var pagination = document.getElementById('media-pagination');
27
+ var refresh = document.getElementById('media-refresh');
28
+ var loadedData = null;
29
+
30
+ function text(value, fallback) {
31
+ if (typeof value !== 'string') return fallback || '';
32
+ var trimmed = value.trim();
33
+ return trimmed || fallback || '';
34
+ }
35
+
36
+ function escapeHtml(value) {
37
+ return String(value || '')
38
+ .replace(/&/g, '&amp;')
39
+ .replace(/</g, '&lt;')
40
+ .replace(/>/g, '&gt;')
41
+ .replace(/"/g, '&quot;')
42
+ .replace(/'/g, '&#39;');
43
+ }
44
+
45
+ function assignmentIdFromPath() {
46
+ var match = window.location.pathname.match(/^\/media\/([^/]+)\/?$/);
47
+ return match ? decodeURIComponent(match[1]) : '';
48
+ }
49
+
50
+ function formatDate(value) {
51
+ if (!value) return '';
52
+ var date = new Date(value);
53
+ if (Number.isNaN(date.getTime())) return '';
54
+ return date.toLocaleString(undefined, {
55
+ month: 'short',
56
+ day: 'numeric',
57
+ year: 'numeric',
58
+ hour: 'numeric',
59
+ minute: '2-digit'
60
+ });
61
+ }
62
+
63
+ function setStatus(message) {
64
+ status.textContent = message;
65
+ status.classList.remove('hidden');
66
+ grid.classList.add('hidden');
67
+ pagination.classList.add('hidden');
68
+ pagination.classList.remove('flex');
69
+ }
70
+
71
+ function imageUrl(item) {
72
+ return text(item && (item.url || item.jpgUrl || item.originalUrl), '');
73
+ }
74
+
75
+ function currentPageFromUrl() {
76
+ var value = new URL(window.location.href).searchParams.get('page') || '1';
77
+ var pageNumber = parseInt(value, 10);
78
+ return Number.isFinite(pageNumber) && pageNumber > 0 ? pageNumber : 1;
79
+ }
80
+
81
+ function pageHref(pageNumber) {
82
+ var url = new URL(window.location.href);
83
+ if (pageNumber <= 1) {
84
+ url.searchParams.delete('page');
85
+ } else {
86
+ url.searchParams.set('page', String(pageNumber));
87
+ }
88
+ return url.pathname + url.search + url.hash;
89
+ }
90
+
91
+ function paginationPages(currentPage, totalPages) {
92
+ var pages = [];
93
+ var lastAdded = 0;
94
+
95
+ function addPage(pageNumber) {
96
+ if (pageNumber < 1 || pageNumber > totalPages || pageNumber === lastAdded) return;
97
+ if (lastAdded && pageNumber > lastAdded + 1) pages.push({ ellipsis: true });
98
+ pages.push({ label: pageNumber, page: pageNumber, current: pageNumber === currentPage });
99
+ lastAdded = pageNumber;
100
+ }
101
+
102
+ addPage(1);
103
+ for (var pageNumber = currentPage - 2; pageNumber <= currentPage + 2; pageNumber += 1) {
104
+ addPage(pageNumber);
105
+ }
106
+ addPage(totalPages);
107
+
108
+ return pages;
109
+ }
110
+
111
+ function renderPagination(currentPage, totalPages) {
112
+ if (totalPages <= 1) {
113
+ pagination.innerHTML = '';
114
+ pagination.classList.add('hidden');
115
+ pagination.classList.remove('flex');
116
+ return;
117
+ }
118
+
119
+ var prev = currentPage > 1
120
+ ? '<a href="' + escapeHtml(pageHref(currentPage - 1)) + '" class="inline-flex items-center gap-1 text-sm font-medium text-slate-500 transition-colors hover:text-[#8f0d00] dark:text-slate-400 dark:hover:text-[#ff2e1a]" aria-label="Previous page"><svg viewBox="0 0 20 20" fill="currentColor" class="h-4 w-4" aria-hidden="true"><path fill-rule="evenodd" d="M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z" clip-rule="evenodd"/></svg>Prev</a>'
121
+ : '<span class="inline-flex items-center gap-1 text-sm font-medium text-slate-300 dark:text-slate-700" aria-disabled="true"><svg viewBox="0 0 20 20" fill="currentColor" class="h-4 w-4" aria-hidden="true"><path fill-rule="evenodd" d="M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z" clip-rule="evenodd"/></svg>Prev</span>';
122
+
123
+ var items = paginationPages(currentPage, totalPages)
124
+ .map(function (item) {
125
+ if (item.ellipsis) {
126
+ return '<li><span class="inline-flex h-9 w-9 items-center justify-center text-sm text-slate-400 dark:text-slate-500">...</span></li>';
127
+ }
128
+ if (item.current) {
129
+ return '<li><span aria-current="page" class="inline-flex h-9 w-9 items-center justify-center border-b-2 border-[#b21100] text-sm font-bold text-[#8f0d00] dark:border-[#ff2e1a] dark:text-[#ff2e1a]">' + item.label + '</span></li>';
130
+ }
131
+ return '<li><a href="' + escapeHtml(pageHref(item.page)) + '" class="inline-flex h-9 w-9 items-center justify-center text-sm font-medium text-slate-600 transition-colors hover:text-[#8f0d00] dark:text-slate-300 dark:hover:text-[#ff2e1a]">' + item.label + '</a></li>';
132
+ })
133
+ .join('');
134
+
135
+ var next = currentPage < totalPages
136
+ ? '<a href="' + escapeHtml(pageHref(currentPage + 1)) + '" class="inline-flex items-center gap-1 text-sm font-medium text-slate-500 transition-colors hover:text-[#8f0d00] dark:text-slate-400 dark:hover:text-[#ff2e1a]" aria-label="Next page">Next<svg viewBox="0 0 20 20" fill="currentColor" class="h-4 w-4" aria-hidden="true"><path fill-rule="evenodd" d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 1 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd"/></svg></a>'
137
+ : '<span class="inline-flex items-center gap-1 text-sm font-medium text-slate-300 dark:text-slate-700" aria-disabled="true">Next<svg viewBox="0 0 20 20" fill="currentColor" class="h-4 w-4" aria-hidden="true"><path fill-rule="evenodd" d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 1 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z" clip-rule="evenodd"/></svg></span>';
138
+
139
+ pagination.innerHTML = prev + '<ol class="flex items-center gap-0.5">' + items + '</ol>' + next;
140
+ pagination.classList.remove('hidden');
141
+ pagination.classList.add('flex');
142
+ }
143
+
144
+ function render(data) {
145
+ loadedData = data;
146
+ var assignment = data.assignment || data;
147
+ var photos = Array.isArray(data.photos) ? data.photos : Array.isArray(data.media) ? data.media : [];
148
+ var name = text(assignment.name, 'Assignment photos');
149
+ var generatedAt = formatDate(data.generatedAt);
150
+ var totalPages = Math.max(1, Math.ceil(photos.length / PAGE_SIZE));
151
+ var currentPage = Math.min(currentPageFromUrl(), totalPages);
152
+ var start = (currentPage - 1) * PAGE_SIZE;
153
+ var visiblePhotos = photos.slice(start, start + PAGE_SIZE);
154
+
155
+ title.textContent = name;
156
+ meta.textContent = photos.length + ' photo' + (photos.length === 1 ? '' : 's') + (totalPages > 1 ? ' - Page ' + currentPage + ' of ' + totalPages : '') + (generatedAt ? ' updated ' + generatedAt : '');
157
+ refresh.href = window.location.href;
158
+
159
+ if (!photos.length) {
160
+ setStatus('No photos have been published for this assignment yet.');
161
+ return;
162
+ }
163
+
164
+ grid.innerHTML = visiblePhotos
165
+ .map(function (item) {
166
+ var url = imageUrl(item);
167
+ var alt = text(item.alt || item.caption || item.originalFilename || item.filename, 'Assignment photo');
168
+ var caption = text(item.caption || item.originalFilename || item.filename, '');
169
+ if (!url) return '';
170
+ return (
171
+ '<figure class="group overflow-hidden border border-slate-200 bg-white dark:border-slate-800 dark:bg-slate-900">' +
172
+ '<a href="' + escapeHtml(url) + '" target="_blank" rel="noopener noreferrer" class="block bg-slate-200 dark:bg-slate-800">' +
173
+ '<img src="' + escapeHtml(url) + '" alt="' + escapeHtml(alt) + '" loading="lazy" class="aspect-[4/3] h-full w-full object-cover transition duration-200 group-hover:scale-[1.02]" />' +
174
+ '</a>' +
175
+ (caption ? '<figcaption class="px-3 py-2 text-xs leading-snug text-slate-600 dark:text-slate-400">' + escapeHtml(caption) + '</figcaption>' : '') +
176
+ '</figure>'
177
+ );
178
+ })
179
+ .join('');
180
+
181
+ status.classList.add('hidden');
182
+ grid.classList.remove('hidden');
183
+ grid.classList.add('grid');
184
+ renderPagination(currentPage, totalPages);
185
+ }
186
+
187
+ var assignmentId = assignmentIdFromPath();
188
+ if (!assignmentId) {
189
+ setStatus('Missing assignment id.');
190
+ return;
191
+ }
192
+
193
+ refresh.href = window.location.href;
194
+ pagination.addEventListener('click', function (event) {
195
+ var target = event.target;
196
+ if (!(target instanceof Element)) return;
197
+ var anchor = target.closest('a[href]');
198
+ if (!(anchor instanceof HTMLAnchorElement)) return;
199
+ event.preventDefault();
200
+ window.history.pushState({}, '', anchor.getAttribute('href') || window.location.href);
201
+ render(loadedData);
202
+ });
203
+
204
+ window.addEventListener('popstate', function () {
205
+ if (loadedData) render(loadedData);
206
+ });
207
+
208
+ fetch('https://cdn.fifthbell.com/contents/assignments/' + encodeURIComponent(assignmentId) + '.json', {
209
+ cache: 'no-store'
210
+ })
211
+ .then(function (response) {
212
+ if (!response.ok) throw new Error(String(response.status));
213
+ return response.json();
214
+ })
215
+ .then(render)
216
+ .catch(function () {
217
+ setStatus('We could not load this assignment right now.');
218
+ });
219
+ })();
220
+ </script>
@@ -0,0 +1,28 @@
1
+ <main class='flex-1 container mx-auto px-4 py-16'>
2
+ <div class='max-w-2xl mx-auto text-center'>
3
+ <div class='mb-8'>
4
+ <h1 class='text-9xl font-bold text-[#8f0d00] dark:text-[#ff2e1a]' aria-label='Page not found error'>404</h1>
5
+ </div>
6
+
7
+ <h2 class='text-4xl font-bold text-slate-900 dark:text-slate-100 mb-4'>Page Not Found</h2>
8
+
9
+ <p class='text-xl text-slate-600 dark:text-slate-400 mb-8 font-serif leading-relaxed'>
10
+ We couldn't find the page you're looking for. The article or page you're trying to access may have been moved, deleted, or never existed.
11
+ </p>
12
+
13
+ <a href='{{logoLink}}' class='inline-flex items-center gap-2 bg-[#b21100] hover:bg-[#8a0d00] dark:bg-[#ff2e1a] dark:hover:bg-[#cc2515] text-white font-bold py-3 px-6 rounded transition-colors'>
14
+ <span>Go to Homepage</span>
15
+ </a>
16
+
17
+ <div class='mt-12 pt-8 border-t border-slate-200 dark:border-slate-800'>
18
+ <p class='text-sm text-slate-600 dark:text-slate-400 mb-4'>Or explore our categories:</p>
19
+ <div class='flex flex-wrap justify-center gap-4'>
20
+ {{#if navigation.categories}}
21
+ {{#each navigation.categories}}
22
+ <a href='/{{slug}}' class='text-[#8f0d00] dark:text-[#ff2e1a] hover:underline font-semibold'>{{name}}</a>
23
+ {{/each}}
24
+ {{/if}}
25
+ </div>
26
+ </div>
27
+ </div>
28
+ </main>
@@ -0,0 +1,420 @@
1
+ <main class='flex-1 container mx-auto px-4'>
2
+ <div class='mx-auto max-w-5xl py-10'>
3
+ <header class='mb-8'>
4
+ <p class='text-xs font-bold uppercase tracking-[0.2em] text-[#8f0d00]'>Search</p>
5
+ <h1 class='mt-3 text-4xl md:text-5xl font-bold text-slate-900 dark:text-slate-100 font-encode'>
6
+ Find Stories Fast
7
+ </h1>
8
+ <p class='mt-3 text-lg text-slate-600 dark:text-slate-400 font-serif'>
9
+ Search titles and excerpts across current-language coverage.
10
+ </p>
11
+ </header>
12
+
13
+ <section class='rounded-2xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 p-5 md:p-6'>
14
+ <form id='search-page-form' class='flex flex-col md:flex-row gap-3' role='search' method='get'>
15
+ <input
16
+ id='search-page-input'
17
+ name='q'
18
+ type='search'
19
+ autocomplete='off'
20
+ spellcheck='false'
21
+ placeholder='Search stories...'
22
+ class='w-full rounded-xl border border-slate-300 dark:border-slate-700 bg-white dark:bg-slate-950 px-4 py-3 text-base text-slate-900 dark:text-slate-100 placeholder:text-slate-400 dark:placeholder:text-slate-500 focus:outline-none focus:ring-2 focus:ring-[#b21100]/40'
23
+ />
24
+ <button
25
+ type='submit'
26
+ class='rounded-xl bg-[#b21100] hover:bg-[#8f0d00] text-white px-6 py-3 font-bold uppercase tracking-wide text-sm transition-colors'
27
+ >
28
+ Search
29
+ </button>
30
+ </form>
31
+ <p id='search-results-meta' class='mt-4 text-sm text-slate-500 dark:text-slate-400'></p>
32
+ </section>
33
+
34
+ <section id='search-results-list' class='mt-8 grid grid-cols-1 md:grid-cols-2 gap-6'></section>
35
+
36
+ <nav id='search-results-pagination' class='mt-8 flex flex-wrap items-center gap-2'></nav>
37
+ </div>
38
+ </main>
39
+
40
+ <script>
41
+ (function () {
42
+ const PAGE_SIZE = 20;
43
+ const CDN_BASE = 'https://cdn.fifthbell.com/content';
44
+ const cache = window.__fifthbellSearchCache || (window.__fifthbellSearchCache = new Map());
45
+
46
+ const form = document.getElementById('search-page-form');
47
+ const input = document.getElementById('search-page-input');
48
+ const meta = document.getElementById('search-results-meta');
49
+ const list = document.getElementById('search-results-list');
50
+ const pagination = document.getElementById('search-results-pagination');
51
+ if (!form || !input || !meta || !list || !pagination) return;
52
+
53
+ function getLanguage() {
54
+ const path = window.location.pathname || '/';
55
+ if (path.startsWith('/es')) return 'es';
56
+ if (path.startsWith('/it')) return 'it';
57
+ return 'en';
58
+ }
59
+
60
+ function normalizePath(path) {
61
+ if (typeof path !== 'string') return '/';
62
+ const stripped = (path.split('?')[0] || '/').trim();
63
+ if (!stripped) return '/';
64
+ const normalized = `/${stripped.replace(/^\/+/, '')}`.replace(/\/{2,}/g, '/');
65
+ if (normalized !== '/' && normalized.endsWith('/')) {
66
+ return normalized.slice(0, -1);
67
+ }
68
+ return normalized;
69
+ }
70
+
71
+ function repairMojibake(value) {
72
+ if (typeof value !== 'string' || !value) return '';
73
+ if (!/[ÃÂâ€]/.test(value)) return value;
74
+ try {
75
+ const bytes = new Uint8Array(Array.from(value, (char) => char.charCodeAt(0) & 0xff));
76
+ const decoded = new TextDecoder('utf-8', { fatal: false }).decode(bytes);
77
+ if (decoded && decoded !== value) return decoded;
78
+ } catch {
79
+ // Keep original text when repair fails.
80
+ }
81
+ return value;
82
+ }
83
+
84
+ function foldForSearch(value) {
85
+ const repaired = repairMojibake(String(value || ''));
86
+ return repaired
87
+ .normalize('NFD')
88
+ .replace(/[\u0300-\u036f]/g, '')
89
+ .toLowerCase()
90
+ .trim();
91
+ }
92
+
93
+ function escapeHtml(value) {
94
+ return String(value || '')
95
+ .replace(/&/g, '&amp;')
96
+ .replace(/</g, '&lt;')
97
+ .replace(/>/g, '&gt;')
98
+ .replace(/"/g, '&quot;')
99
+ .replace(/'/g, '&#039;');
100
+ }
101
+
102
+ function getSearchBasePath(language) {
103
+ return language === 'en' ? '/search' : `/${language}/search`;
104
+ }
105
+
106
+ function isStorybookPreview() {
107
+ const url = new URL(window.location.href);
108
+ return url.pathname.endsWith('/iframe.html') && url.searchParams.has('id');
109
+ }
110
+
111
+ function isOnSearchPage(language) {
112
+ const searchBasePath = getSearchBasePath(language);
113
+ const currentPath = normalizePath(window.location.pathname);
114
+ if (currentPath === searchBasePath) return true;
115
+
116
+ // In Storybook, pages render inside /iframe.html while still representing search-page context.
117
+ if (isStorybookPreview()) return true;
118
+
119
+ return false;
120
+ }
121
+
122
+ function parseQueryState() {
123
+ const url = new URL(window.location.href);
124
+ const q = (url.searchParams.get('q') || '').trim();
125
+ const pageRaw = Number.parseInt(url.searchParams.get('page') || '1', 10);
126
+ const page = Number.isFinite(pageRaw) && pageRaw > 0 ? pageRaw : 1;
127
+ return { q, page };
128
+ }
129
+
130
+ function toHref(language, query, page) {
131
+ const base = getSearchBasePath(language);
132
+ const params = new URLSearchParams();
133
+ params.set('q', query);
134
+ if (page > 1) params.set('page', String(page));
135
+ return `${base}?${params.toString()}`;
136
+ }
137
+
138
+ function navigateToSearchUrl(url, options = { replace: false }) {
139
+ const language = getLanguage();
140
+ if (!isOnSearchPage(language)) {
141
+ window.location.href = url;
142
+ return;
143
+ }
144
+
145
+ const currentUrl = new URL(window.location.href);
146
+ const targetUrl = new URL(url, window.location.origin);
147
+ const nextQ = (targetUrl.searchParams.get('q') || '').trim();
148
+ const nextPage = (targetUrl.searchParams.get('page') || '').trim();
149
+
150
+ if (nextQ) {
151
+ currentUrl.searchParams.set('q', nextQ);
152
+ } else {
153
+ currentUrl.searchParams.delete('q');
154
+ }
155
+
156
+ if (nextPage) {
157
+ currentUrl.searchParams.set('page', nextPage);
158
+ } else {
159
+ currentUrl.searchParams.delete('page');
160
+ }
161
+
162
+ const nextRelative = `${currentUrl.pathname}?${currentUrl.searchParams.toString()}${currentUrl.hash}`;
163
+ const method = options.replace ? 'replaceState' : 'pushState';
164
+ window.history[method](null, '', nextRelative);
165
+ runSearch();
166
+ }
167
+
168
+ async function fetchJson(url) {
169
+ const response = await fetch(url, {
170
+ cache: 'no-store',
171
+ headers: { 'Content-Type': 'application/json' }
172
+ });
173
+ if (!response.ok) {
174
+ throw new Error(`Failed to fetch ${url}: ${response.status}`);
175
+ }
176
+ return response.json();
177
+ }
178
+
179
+ async function loadIndex(language) {
180
+ const cacheKey = `index:${language}`;
181
+ if (cache.has(cacheKey)) return cache.get(cacheKey);
182
+
183
+ const manifest = await fetchJson(`${CDN_BASE}/search-manifest-${language}.json?v=${Date.now()}`);
184
+ const fileList = [];
185
+ if (manifest?.files?.currentMonth) fileList.push(manifest.files.currentMonth);
186
+ if (Array.isArray(manifest?.files?.yearly)) {
187
+ for (const file of manifest.files.yearly) {
188
+ if (typeof file === 'string') fileList.push(file);
189
+ }
190
+ }
191
+
192
+ const uniqueFiles = [...new Set(fileList)];
193
+ const payloads = await Promise.all(
194
+ uniqueFiles.map(async (file) => {
195
+ try {
196
+ return await fetchJson(`${CDN_BASE}/${file}?v=${Date.now()}`);
197
+ } catch {
198
+ return null;
199
+ }
200
+ })
201
+ );
202
+
203
+ const combined = [];
204
+ let position = 0;
205
+ for (const payload of payloads) {
206
+ const articles = Array.isArray(payload?.articles) ? payload.articles : [];
207
+ for (const article of articles) {
208
+ combined.push({
209
+ title: repairMojibake(typeof article?.title === 'string' ? article.title : ''),
210
+ slug: normalizePath(typeof article?.slug === 'string' ? article.slug : '/'),
211
+ excerpt: repairMojibake(typeof article?.excerpt === 'string' ? article.excerpt : ''),
212
+ photoUrl: typeof article?.photoUrl === 'string' ? article.photoUrl : '',
213
+ publishedAt: typeof article?.publishedAt === 'string' ? article.publishedAt : '',
214
+ position
215
+ });
216
+ position += 1;
217
+ }
218
+ }
219
+
220
+ const deduped = [];
221
+ const seen = new Set();
222
+ for (const article of combined) {
223
+ const key = `${article.slug}::${article.title}`;
224
+ if (seen.has(key)) continue;
225
+ seen.add(key);
226
+ deduped.push(article);
227
+ }
228
+
229
+ cache.set(cacheKey, deduped);
230
+ return deduped;
231
+ }
232
+
233
+ function formatPublishedAt(value, language) {
234
+ if (typeof value !== 'string' || !value) return '';
235
+ const date = new Date(value);
236
+ if (Number.isNaN(date.getTime())) return '';
237
+ const locale = language === 'es' ? 'es-ES' : language === 'it' ? 'it-IT' : 'en-US';
238
+ return date.toLocaleDateString(locale, {
239
+ month: 'short',
240
+ day: 'numeric',
241
+ year: 'numeric'
242
+ });
243
+ }
244
+
245
+ function scoreArticle(article, tokens, fullQuery) {
246
+ const title = foldForSearch(article.title || '');
247
+ const excerpt = foldForSearch(article.excerpt || '');
248
+ const haystack = `${title} ${excerpt}`;
249
+ let score = 0;
250
+
251
+ if (fullQuery && title.includes(fullQuery)) {
252
+ score += 60;
253
+ }
254
+
255
+ for (const token of tokens) {
256
+ if (!haystack.includes(token)) {
257
+ return -1;
258
+ }
259
+ if (title.includes(token)) score += 15;
260
+ if (excerpt.includes(token)) score += 5;
261
+ }
262
+
263
+ if (title.startsWith(tokens[0] || '')) {
264
+ score += 20;
265
+ }
266
+
267
+ return score;
268
+ }
269
+
270
+ function searchArticles(articles, query) {
271
+ const normalizedQuery = foldForSearch(query);
272
+ if (!normalizedQuery) return [];
273
+ const tokens = normalizedQuery.split(/\s+/g).filter(Boolean);
274
+ if (tokens.length === 0) return [];
275
+
276
+ return articles
277
+ .map((article) => {
278
+ const score = scoreArticle(article, tokens, normalizedQuery);
279
+ if (score < 0) return null;
280
+ return { ...article, score };
281
+ })
282
+ .filter(Boolean)
283
+ .sort((a, b) => {
284
+ const aPublished = Number.isNaN(Date.parse(a.publishedAt || '')) ? 0 : Date.parse(a.publishedAt || '');
285
+ const bPublished = Number.isNaN(Date.parse(b.publishedAt || '')) ? 0 : Date.parse(b.publishedAt || '');
286
+ if (bPublished !== aPublished) return bPublished - aPublished;
287
+ if (b.score !== a.score) return b.score - a.score;
288
+ return a.position - b.position;
289
+ });
290
+ }
291
+
292
+ function renderEmptyState(message) {
293
+ list.innerHTML = `<div class="col-span-full rounded-xl border border-dashed border-slate-300 dark:border-slate-700 p-8 text-center text-slate-500 dark:text-slate-400">${escapeHtml(message)}</div>`;
294
+ pagination.innerHTML = '';
295
+ }
296
+
297
+ function renderResults(language, query, page, results) {
298
+ const total = results.length;
299
+ if (!query) {
300
+ meta.textContent = 'Start typing to search in this language.';
301
+ renderEmptyState('No query yet.');
302
+ return;
303
+ }
304
+
305
+ if (total === 0) {
306
+ meta.textContent = `No matches for "${query}".`;
307
+ renderEmptyState('Try a different keyword.');
308
+ return;
309
+ }
310
+
311
+ const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE));
312
+ const currentPage = Math.min(Math.max(page, 1), totalPages);
313
+ const start = (currentPage - 1) * PAGE_SIZE;
314
+ const end = Math.min(start + PAGE_SIZE, total);
315
+ const visible = results.slice(start, end);
316
+
317
+ meta.textContent = `${total} result${total === 1 ? '' : 's'} for "${query}" • Page ${currentPage} of ${totalPages}`;
318
+
319
+ list.innerHTML = visible
320
+ .map((article) => {
321
+ const href = normalizePath(article.slug || '/');
322
+ const publishedAt = formatPublishedAt(article.publishedAt, language);
323
+ return `
324
+ <article class="group rounded-xl border border-slate-200 dark:border-slate-800 overflow-hidden bg-white dark:bg-slate-900">
325
+ <a href="${escapeHtml(href)}" class="block aspect-[16/10] bg-slate-100 dark:bg-slate-800">
326
+ <img src="${escapeHtml(article.photoUrl)}" alt="${escapeHtml(article.title)}" class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-105" loading="lazy" />
327
+ </a>
328
+ <div class="p-4">
329
+ ${publishedAt ? `<p class="text-xs uppercase tracking-wider text-slate-500 dark:text-slate-400 mb-2">${escapeHtml(publishedAt)}</p>` : ''}
330
+ <h2 class="text-xl font-bold leading-tight text-slate-900 dark:text-slate-100 font-encode">
331
+ <a href="${escapeHtml(href)}" class="hover:text-[#8f0d00] dark:hover:text-[#ff2e1a] transition-colors">${escapeHtml(article.title)}</a>
332
+ </h2>
333
+ <p class="mt-2 text-slate-600 dark:text-slate-400 font-serif line-clamp-3">${escapeHtml(article.excerpt)}</p>
334
+ </div>
335
+ </article>
336
+ `;
337
+ })
338
+ .join('');
339
+
340
+ const pageLinks = [];
341
+ if (currentPage > 1) {
342
+ pageLinks.push(
343
+ `<a class="rounded-lg border border-slate-300 dark:border-slate-700 px-3 py-2 text-sm hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors" href="${escapeHtml(toHref(language, query, currentPage - 1))}">Previous</a>`
344
+ );
345
+ }
346
+
347
+ const rangeStart = Math.max(1, currentPage - 2);
348
+ const rangeEnd = Math.min(totalPages, currentPage + 2);
349
+ for (let i = rangeStart; i <= rangeEnd; i += 1) {
350
+ if (i === currentPage) {
351
+ pageLinks.push(
352
+ `<span class="rounded-lg bg-[#b21100] text-white px-3 py-2 text-sm font-bold">${i}</span>`
353
+ );
354
+ } else {
355
+ pageLinks.push(
356
+ `<a class="rounded-lg border border-slate-300 dark:border-slate-700 px-3 py-2 text-sm hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors" href="${escapeHtml(toHref(language, query, i))}">${i}</a>`
357
+ );
358
+ }
359
+ }
360
+
361
+ if (currentPage < totalPages) {
362
+ pageLinks.push(
363
+ `<a class="rounded-lg border border-slate-300 dark:border-slate-700 px-3 py-2 text-sm hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors" href="${escapeHtml(toHref(language, query, currentPage + 1))}">Next</a>`
364
+ );
365
+ }
366
+
367
+ pagination.innerHTML = pageLinks.join('');
368
+ }
369
+
370
+ async function runSearch() {
371
+ const language = getLanguage();
372
+ const state = parseQueryState();
373
+ input.value = state.q;
374
+
375
+ if (!state.q) {
376
+ renderResults(language, '', 1, []);
377
+ return;
378
+ }
379
+
380
+ meta.textContent = 'Searching...';
381
+ list.innerHTML = '';
382
+ pagination.innerHTML = '';
383
+
384
+ try {
385
+ const articles = await loadIndex(language);
386
+ const results = searchArticles(articles, state.q);
387
+ renderResults(language, state.q, state.page, results);
388
+ } catch (error) {
389
+ meta.textContent = 'Search is temporarily unavailable.';
390
+ renderEmptyState('We could not load the search index right now.');
391
+ }
392
+ }
393
+
394
+ form.addEventListener('submit', (event) => {
395
+ event.preventDefault();
396
+ const language = getLanguage();
397
+ const query = (input.value || '').trim();
398
+ if (!query) {
399
+ navigateToSearchUrl(getSearchBasePath(language), { replace: false });
400
+ return;
401
+ }
402
+ navigateToSearchUrl(toHref(language, query, 1), { replace: false });
403
+ });
404
+
405
+ pagination.addEventListener('click', (event) => {
406
+ const target = event.target;
407
+ if (!(target instanceof Element)) return;
408
+ const anchor = target.closest('a[href]');
409
+ if (!(anchor instanceof HTMLAnchorElement)) return;
410
+ event.preventDefault();
411
+ navigateToSearchUrl(anchor.getAttribute('href') || '/', { replace: false });
412
+ });
413
+
414
+ window.addEventListener('popstate', () => {
415
+ runSearch();
416
+ });
417
+
418
+ runSearch();
419
+ })();
420
+ </script>