@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,751 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { BellRing } from 'lucide-react';
4
+ import { useSSE } from './hooks/useSSE.js';
5
+ import { FIFTHBELL_ASSETS } from './assets.js';
6
+ import { MarqueeCurtain } from './components/MarqueeCurtain.js';
7
+ import Marquee from './components/Marquee.js';
8
+ import { WorldClocks, DEFAULT_WORLD_CLOCK_CITIES } from './components/WorldClocks.js';
9
+ import { CallsignSlide } from './components/slides/CallsignSlide.js';
10
+ import { slideStyles } from './components/slides/slideStyles.js';
11
+ import { fetchEvents, getCachedEvents, hasEventChanges } from './events.js';
12
+ import { createArticlesSegment, createEarthquakeSegment, createLiveEventSegment, createMarketsSegment, createWeatherSegment, fetchArticles, fetchEarthquakes, fetchLiveEvent, fetchMarketData, fetchWeatherData, usePlaylistEngine } from './segments/index.js';
13
+ const DEFAULT_LANGUAGE_ROTATION = ['en', 'es', 'en', 'it'];
14
+ const DEFAULT_CALLSIGN_PRELAUNCH_UNTIL_NYC = '2026-01-02T21:30:00';
15
+ const FIFTHBELL_COMPONENT_TYPE_CONTENT = 'fifthbell-content';
16
+ const FIFTHBELL_COMPONENT_TYPE_MARQUEE = 'fifthbell-marquee';
17
+ const FIFTHBELL_COMPONENT_TYPE_TONI_CLOCK = 'toni-clock';
18
+ const FIFTHBELL_COMPONENT_TYPE_CORNER = 'fifthbell-corner';
19
+ const FIFTHBELL_COMPONENT_TYPE_LEGACY = 'fifthbell';
20
+ const DEFAULT_FIFTHBELL_CONFIG = {
21
+ showArticles: true,
22
+ showWeather: true,
23
+ showEarthquakes: true,
24
+ showMarkets: true,
25
+ showLiveEvents: true,
26
+ showMarquee: false,
27
+ showCallsignTake: true,
28
+ weatherCities: [],
29
+ languageRotation: DEFAULT_LANGUAGE_ROTATION,
30
+ dataLoadTimeoutMs: 15000,
31
+ playlistDefaultDurationMs: 10000,
32
+ playlistUpdateIntervalMs: 100,
33
+ articlesDurationMs: 10000,
34
+ weatherDurationMs: 5000,
35
+ earthquakesDurationMs: 10000,
36
+ marketsDurationMs: 10000,
37
+ showWorldClocks: true,
38
+ showBellIcon: true,
39
+ worldClockRotateIntervalMs: 7000,
40
+ worldClockTransitionMs: 300,
41
+ worldClockShuffle: true,
42
+ worldClockWidthPx: 200,
43
+ worldClockCities: [...DEFAULT_WORLD_CLOCK_CITIES],
44
+ audioCueEnabled: true,
45
+ audioCueMinute: 59,
46
+ audioCueSecond: 55,
47
+ callsignPrelaunchUntilNyc: DEFAULT_CALLSIGN_PRELAUNCH_UNTIL_NYC,
48
+ callsignWindowStartSecond: 50,
49
+ callsignWindowEndSecond: 3,
50
+ marqueeMinPostsCount: 4,
51
+ marqueeMinAverageRelevance: 0,
52
+ marqueeMinMedianRelevance: 0,
53
+ marqueePixelsPerSecond: 150,
54
+ marqueeMinDurationSeconds: 10,
55
+ marqueeHeightPx: 72
56
+ };
57
+ function clampNumber(value, fallback, min, max) {
58
+ const numeric = typeof value === 'number' ? value : Number(value);
59
+ if (!Number.isFinite(numeric)) {
60
+ return fallback;
61
+ }
62
+ return Math.min(max, Math.max(min, numeric));
63
+ }
64
+ function normalizeBoolean(value, fallback) {
65
+ if (typeof value === 'boolean') {
66
+ return value;
67
+ }
68
+ if (typeof value === 'number') {
69
+ return value !== 0;
70
+ }
71
+ if (typeof value === 'string') {
72
+ const normalized = value.trim().toLowerCase();
73
+ if (['true', '1', 'yes', 'on'].includes(normalized)) {
74
+ return true;
75
+ }
76
+ if (['false', '0', 'no', 'off', ''].includes(normalized)) {
77
+ return false;
78
+ }
79
+ }
80
+ return fallback;
81
+ }
82
+ function normalizeStringArray(value) {
83
+ if (!Array.isArray(value)) {
84
+ return [];
85
+ }
86
+ const deduped = new Set();
87
+ for (const item of value) {
88
+ if (typeof item !== 'string') {
89
+ continue;
90
+ }
91
+ const trimmed = item.trim();
92
+ if (!trimmed) {
93
+ continue;
94
+ }
95
+ deduped.add(trimmed);
96
+ }
97
+ return [...deduped];
98
+ }
99
+ function normalizeLanguageRotation(value) {
100
+ if (!Array.isArray(value)) {
101
+ return [...DEFAULT_LANGUAGE_ROTATION];
102
+ }
103
+ const allowed = new Set(['en', 'es', 'it']);
104
+ const filtered = value.filter((item) => typeof item === 'string' && allowed.has(item));
105
+ return filtered.length > 0 ? filtered : [...DEFAULT_LANGUAGE_ROTATION];
106
+ }
107
+ function normalizeWorldClockCities(value) {
108
+ if (!Array.isArray(value)) {
109
+ return [...DEFAULT_WORLD_CLOCK_CITIES];
110
+ }
111
+ const normalized = value
112
+ .map((item) => {
113
+ if (!item || typeof item !== 'object' || Array.isArray(item)) {
114
+ return null;
115
+ }
116
+ const city = typeof item.city === 'string' ? item.city.trim() : '';
117
+ const timezone = typeof item.timezone === 'string' ? item.timezone.trim() : '';
118
+ if (!city || !timezone) {
119
+ return null;
120
+ }
121
+ return { city, timezone };
122
+ })
123
+ .filter((item) => item !== null);
124
+ return normalized.length > 0 ? normalized : [...DEFAULT_WORLD_CLOCK_CITIES];
125
+ }
126
+ function normalizeCityKey(value) {
127
+ return value
128
+ .normalize('NFD')
129
+ .replace(/[\u0300-\u036f]/g, '')
130
+ .trim()
131
+ .toLowerCase();
132
+ }
133
+ function toRecord(value) {
134
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
135
+ return value;
136
+ }
137
+ return {};
138
+ }
139
+ function parseSceneMetadata(scene) {
140
+ if (!scene || !scene.metadata) {
141
+ return {};
142
+ }
143
+ try {
144
+ const parsed = JSON.parse(scene.metadata);
145
+ return toRecord(parsed);
146
+ }
147
+ catch {
148
+ return {};
149
+ }
150
+ }
151
+ function resolveFifthBellLayerAvailability(activeComponents) {
152
+ const defaultAvailability = {
153
+ content: true,
154
+ marquee: true
155
+ };
156
+ if (!activeComponents || activeComponents.length === 0) {
157
+ return defaultAvailability;
158
+ }
159
+ if (activeComponents.includes(FIFTHBELL_COMPONENT_TYPE_LEGACY)) {
160
+ return defaultAvailability;
161
+ }
162
+ return {
163
+ content: activeComponents.includes(FIFTHBELL_COMPONENT_TYPE_CONTENT),
164
+ marquee: activeComponents.includes(FIFTHBELL_COMPONENT_TYPE_MARQUEE)
165
+ };
166
+ }
167
+ function extractConfigFromMetadata(metadataInput) {
168
+ const metadata = toRecord(metadataInput);
169
+ const legacyProps = toRecord(metadata[FIFTHBELL_COMPONENT_TYPE_LEGACY]);
170
+ const contentProps = {
171
+ ...legacyProps,
172
+ ...toRecord(metadata[FIFTHBELL_COMPONENT_TYPE_CONTENT])
173
+ };
174
+ const marqueeProps = {
175
+ ...legacyProps,
176
+ ...toRecord(metadata[FIFTHBELL_COMPONENT_TYPE_MARQUEE])
177
+ };
178
+ const cornerProps = {
179
+ ...legacyProps,
180
+ ...toRecord(metadata[FIFTHBELL_COMPONENT_TYPE_TONI_CLOCK]),
181
+ ...toRecord(metadata[FIFTHBELL_COMPONENT_TYPE_CORNER])
182
+ };
183
+ const parsedMarqueeMinPostsCount = clampNumber(marqueeProps.marqueeMinPostsCount, DEFAULT_FIFTHBELL_CONFIG.marqueeMinPostsCount, 0, 50);
184
+ let parsedMarqueeMinAverageRelevance = clampNumber(marqueeProps.marqueeMinAverageRelevance, DEFAULT_FIFTHBELL_CONFIG.marqueeMinAverageRelevance, 0, 100);
185
+ let parsedMarqueeMinMedianRelevance = clampNumber(marqueeProps.marqueeMinMedianRelevance, DEFAULT_FIFTHBELL_CONFIG.marqueeMinMedianRelevance, 0, 100);
186
+ // Compatibility: prior defaults were tuned for OR logic. Under threshold logic, treat that trio as legacy.
187
+ if (parsedMarqueeMinPostsCount === 4 && parsedMarqueeMinAverageRelevance === 5 && parsedMarqueeMinMedianRelevance === 7) {
188
+ parsedMarqueeMinAverageRelevance = 0;
189
+ parsedMarqueeMinMedianRelevance = 0;
190
+ }
191
+ return {
192
+ showArticles: normalizeBoolean(contentProps.showArticles, DEFAULT_FIFTHBELL_CONFIG.showArticles),
193
+ showWeather: normalizeBoolean(contentProps.showWeather, DEFAULT_FIFTHBELL_CONFIG.showWeather),
194
+ showEarthquakes: normalizeBoolean(contentProps.showEarthquakes, DEFAULT_FIFTHBELL_CONFIG.showEarthquakes),
195
+ showMarkets: normalizeBoolean(contentProps.showMarkets, DEFAULT_FIFTHBELL_CONFIG.showMarkets),
196
+ showLiveEvents: normalizeBoolean(contentProps.showLiveEvents, DEFAULT_FIFTHBELL_CONFIG.showLiveEvents),
197
+ showMarquee: normalizeBoolean(marqueeProps.showMarquee, DEFAULT_FIFTHBELL_CONFIG.showMarquee),
198
+ showCallsignTake: normalizeBoolean(contentProps.showCallsignTake, DEFAULT_FIFTHBELL_CONFIG.showCallsignTake),
199
+ weatherCities: normalizeStringArray(contentProps.weatherCities),
200
+ languageRotation: normalizeLanguageRotation(contentProps.languageRotation),
201
+ dataLoadTimeoutMs: clampNumber(contentProps.dataLoadTimeoutMs, DEFAULT_FIFTHBELL_CONFIG.dataLoadTimeoutMs, 1000, 120000),
202
+ playlistDefaultDurationMs: clampNumber(contentProps.playlistDefaultDurationMs, DEFAULT_FIFTHBELL_CONFIG.playlistDefaultDurationMs, 1000, 120000),
203
+ playlistUpdateIntervalMs: clampNumber(contentProps.playlistUpdateIntervalMs, DEFAULT_FIFTHBELL_CONFIG.playlistUpdateIntervalMs, 16, 5000),
204
+ articlesDurationMs: clampNumber(contentProps.articlesDurationMs, DEFAULT_FIFTHBELL_CONFIG.articlesDurationMs, 1000, 120000),
205
+ weatherDurationMs: clampNumber(contentProps.weatherDurationMs, DEFAULT_FIFTHBELL_CONFIG.weatherDurationMs, 1000, 120000),
206
+ earthquakesDurationMs: clampNumber(contentProps.earthquakesDurationMs, DEFAULT_FIFTHBELL_CONFIG.earthquakesDurationMs, 1000, 120000),
207
+ marketsDurationMs: clampNumber(contentProps.marketsDurationMs, DEFAULT_FIFTHBELL_CONFIG.marketsDurationMs, 1000, 120000),
208
+ showWorldClocks: normalizeBoolean(cornerProps.showWorldClocks, DEFAULT_FIFTHBELL_CONFIG.showWorldClocks),
209
+ showBellIcon: true,
210
+ worldClockRotateIntervalMs: clampNumber(cornerProps.worldClockRotateIntervalMs, DEFAULT_FIFTHBELL_CONFIG.worldClockRotateIntervalMs, 500, 120000),
211
+ worldClockTransitionMs: clampNumber(cornerProps.worldClockTransitionMs, DEFAULT_FIFTHBELL_CONFIG.worldClockTransitionMs, 0, 10000),
212
+ worldClockShuffle: normalizeBoolean(cornerProps.worldClockShuffle, DEFAULT_FIFTHBELL_CONFIG.worldClockShuffle),
213
+ worldClockWidthPx: clampNumber(cornerProps.worldClockWidthPx, DEFAULT_FIFTHBELL_CONFIG.worldClockWidthPx, 120, 600),
214
+ worldClockCities: normalizeWorldClockCities(cornerProps.worldClockCities),
215
+ audioCueEnabled: normalizeBoolean(contentProps.audioCueEnabled, DEFAULT_FIFTHBELL_CONFIG.audioCueEnabled),
216
+ audioCueMinute: clampNumber(contentProps.audioCueMinute, DEFAULT_FIFTHBELL_CONFIG.audioCueMinute, 0, 59),
217
+ audioCueSecond: clampNumber(contentProps.audioCueSecond, DEFAULT_FIFTHBELL_CONFIG.audioCueSecond, 0, 59),
218
+ callsignPrelaunchUntilNyc: typeof contentProps.callsignPrelaunchUntilNyc === 'string' && contentProps.callsignPrelaunchUntilNyc.trim()
219
+ ? contentProps.callsignPrelaunchUntilNyc.trim()
220
+ : DEFAULT_FIFTHBELL_CONFIG.callsignPrelaunchUntilNyc,
221
+ callsignWindowStartSecond: clampNumber(contentProps.callsignWindowStartSecond, DEFAULT_FIFTHBELL_CONFIG.callsignWindowStartSecond, 0, 59),
222
+ callsignWindowEndSecond: clampNumber(contentProps.callsignWindowEndSecond, DEFAULT_FIFTHBELL_CONFIG.callsignWindowEndSecond, 0, 59),
223
+ marqueeMinPostsCount: parsedMarqueeMinPostsCount,
224
+ marqueeMinAverageRelevance: parsedMarqueeMinAverageRelevance,
225
+ marqueeMinMedianRelevance: parsedMarqueeMinMedianRelevance,
226
+ marqueePixelsPerSecond: clampNumber(marqueeProps.marqueePixelsPerSecond, DEFAULT_FIFTHBELL_CONFIG.marqueePixelsPerSecond, 10, 1000),
227
+ marqueeMinDurationSeconds: clampNumber(marqueeProps.marqueeMinDurationSeconds, DEFAULT_FIFTHBELL_CONFIG.marqueeMinDurationSeconds, 1, 120),
228
+ marqueeHeightPx: clampNumber(marqueeProps.marqueeHeightPx, DEFAULT_FIFTHBELL_CONFIG.marqueeHeightPx, 72, 200)
229
+ };
230
+ }
231
+ function normalizeLaunchDate(rawDate) {
232
+ const parsed = new Date(rawDate);
233
+ if (Number.isNaN(parsed.getTime())) {
234
+ return new Date(DEFAULT_CALLSIGN_PRELAUNCH_UNTIL_NYC);
235
+ }
236
+ return parsed;
237
+ }
238
+ export default function LiveProgram({ embedded = false, sceneMetadata, activeComponents, apiBaseUrl }) {
239
+ const [state, setState] = useState(null);
240
+ const [showLogoSlide, setShowLogoSlide] = useState(false);
241
+ const [callsignTime, setCallsignTime] = useState(new Date());
242
+ const audioRef = useRef(null);
243
+ const audioInitialized = useRef(false);
244
+ const [languageIndex, setLanguageIndex] = useState(0);
245
+ const [articles, setArticles] = useState([]);
246
+ const [weatherData, setWeatherData] = useState([]);
247
+ const [earthquakes, setEarthquakes] = useState([]);
248
+ const [markets, setMarkets] = useState([]);
249
+ const [liveEvent, setLiveEvent] = useState(null);
250
+ const [stageEvents, setStageEvents] = useState([]);
251
+ const [programEvents, setProgramEvents] = useState([]);
252
+ const [showCurtain, setShowCurtain] = useState(false);
253
+ const [updatePending, setUpdatePending] = useState(false);
254
+ const updatePendingRef = useRef(false);
255
+ const [dataLoaded, setDataLoaded] = useState(false);
256
+ const lastFetchedItemRef = useRef(-1);
257
+ const activeInstantAudioRef = useRef(null);
258
+ const activeInstantAudiosRef = useRef(new Set());
259
+ const sceneInstantTakeSequenceRef = useRef(0);
260
+ const mixerSettingsRef = useRef({});
261
+ const songAudioRef = useRef(null);
262
+ const controlledBySceneRenderer = sceneMetadata !== undefined;
263
+ const effectiveSceneMetadata = useMemo(() => {
264
+ if (sceneMetadata !== undefined) {
265
+ return toRecord(sceneMetadata);
266
+ }
267
+ return parseSceneMetadata(state?.activeScene ?? null);
268
+ }, [sceneMetadata, state?.activeScene]);
269
+ const config = useMemo(() => extractConfigFromMetadata(effectiveSceneMetadata), [effectiveSceneMetadata]);
270
+ const layerAvailability = useMemo(() => resolveFifthBellLayerAvailability(activeComponents), [activeComponents]);
271
+ const languageRotation = config.languageRotation;
272
+ const currentLanguage = languageRotation[languageIndex] ?? languageRotation[0] ?? 'en';
273
+ const resolvedApiBaseUrl = apiBaseUrl?.replace(/\/+$/, '') ||
274
+ (() => {
275
+ if (typeof window === 'undefined')
276
+ return 'http://127.0.0.1:3000';
277
+ const hostname = window.location.hostname;
278
+ return `http://${hostname.includes(':') ? `[${hostname}]` : hostname}:3000`;
279
+ })();
280
+ useEffect(() => {
281
+ if (languageIndex >= languageRotation.length) {
282
+ setLanguageIndex(0);
283
+ }
284
+ }, [languageIndex, languageRotation.length]);
285
+ useEffect(() => {
286
+ updatePendingRef.current = updatePending;
287
+ }, [updatePending]);
288
+ useEffect(() => {
289
+ if (controlledBySceneRenderer) {
290
+ return;
291
+ }
292
+ fetch(`${resolvedApiBaseUrl}/state`)
293
+ .then((res) => res.json())
294
+ .then((data) => setState(data))
295
+ .catch((err) => console.error('Failed to fetch FifthBell program state:', err));
296
+ }, [controlledBySceneRenderer, resolvedApiBaseUrl]);
297
+ const refreshAllData = useCallback(async () => {
298
+ const [articlesData, weatherDataResult, earthquakesData, marketsData, liveEventData] = await Promise.all([
299
+ fetchArticles(currentLanguage),
300
+ fetchWeatherData(),
301
+ fetchEarthquakes(currentLanguage),
302
+ fetchMarketData(),
303
+ fetchLiveEvent(currentLanguage),
304
+ fetchEvents({
305
+ language: currentLanguage,
306
+ allowedLanguages: [currentLanguage]
307
+ })
308
+ ]);
309
+ setArticles(articlesData);
310
+ setWeatherData(weatherDataResult);
311
+ setEarthquakes(earthquakesData);
312
+ setMarkets(marketsData);
313
+ setLiveEvent(liveEventData);
314
+ const cachedEvents = getCachedEvents();
315
+ if (cachedEvents) {
316
+ setStageEvents(cachedEvents);
317
+ setProgramEvents(cachedEvents);
318
+ }
319
+ setDataLoaded(true);
320
+ }, [currentLanguage]);
321
+ useEffect(() => {
322
+ void refreshAllData();
323
+ }, [refreshAllData]);
324
+ const stopSceneInstantAudio = useCallback((fadeMs = 0) => {
325
+ const audio = activeInstantAudioRef.current;
326
+ if (!audio)
327
+ return;
328
+ if (fadeMs > 0) {
329
+ const initialVolume = audio.volume;
330
+ const startTime = performance.now();
331
+ const fadeStep = (timestamp) => {
332
+ const elapsed = timestamp - startTime;
333
+ if (elapsed >= fadeMs) {
334
+ audio.volume = 0;
335
+ audio.pause();
336
+ try {
337
+ audio.currentTime = 0;
338
+ }
339
+ catch { /* no-op */ }
340
+ audio.onended = null;
341
+ audio.onerror = null;
342
+ activeInstantAudioRef.current = null;
343
+ return;
344
+ }
345
+ audio.volume = Math.max(0, initialVolume * (1 - elapsed / fadeMs));
346
+ requestAnimationFrame(fadeStep);
347
+ };
348
+ requestAnimationFrame(fadeStep);
349
+ return;
350
+ }
351
+ audio.pause();
352
+ try {
353
+ audio.currentTime = 0;
354
+ }
355
+ catch { /* no-op */ }
356
+ audio.onended = null;
357
+ audio.onerror = null;
358
+ activeInstantAudioRef.current = null;
359
+ }, []);
360
+ useSSE({
361
+ url: `${resolvedApiBaseUrl}/events`,
362
+ enabled: !controlledBySceneRenderer,
363
+ onMessage: (data) => {
364
+ if (data.type === 'scene_staged') {
365
+ setState((prev) => {
366
+ if (!prev)
367
+ return prev;
368
+ return {
369
+ ...prev,
370
+ stagedSceneId: typeof data.stagedSceneId === 'number' && Number.isFinite(data.stagedSceneId) ? data.stagedSceneId : null,
371
+ stagedScene: data.scene && typeof data.scene === 'object' ? data.scene : null,
372
+ };
373
+ });
374
+ }
375
+ else if ((data.type === 'scene_change' || data.type === 'program_scenes_changed') && data.state) {
376
+ setState(data.state);
377
+ }
378
+ else if (data.type === 'scene_update') {
379
+ setState((prev) => {
380
+ if (!prev)
381
+ return prev;
382
+ return {
383
+ ...prev,
384
+ activeScene: data.scene ?? prev.activeScene
385
+ };
386
+ });
387
+ }
388
+ else if (data.type === 'scene_cleared') {
389
+ setState((prev) => {
390
+ if (!prev)
391
+ return prev;
392
+ return {
393
+ ...prev,
394
+ activeSceneId: null,
395
+ activeScene: null
396
+ };
397
+ });
398
+ }
399
+ else if (data.type === 'broadcast_settings_update') {
400
+ // broadcast settings stored for overlay display
401
+ }
402
+ else if (data.type === 'scene_instant_take' && data.instant?.audioUrl) {
403
+ console.log('[scene_instant_take]', data.instant.name, data.instant.audioUrl);
404
+ sceneInstantTakeSequenceRef.current += 1;
405
+ const takeSequence = sceneInstantTakeSequenceRef.current;
406
+ const ms = (mixerSettingsRef.current || {});
407
+ const masterVol = ms.sceneInstantMasterVolume ?? 1;
408
+ const muted = ms.sceneInstantMuted === true;
409
+ const baseVol = typeof data.instant.volume === 'number' ? Math.max(0, Math.min(1, data.instant.volume)) : 1;
410
+ const finalVol = muted ? 0 : Math.max(0, Math.min(1, baseVol * masterVol));
411
+ const playAudio = () => {
412
+ const audio = new Audio(data.instant.audioUrl);
413
+ audio.preload = 'auto';
414
+ audio.loop = data.loop !== false;
415
+ audio.volume = finalVol;
416
+ audio.onended = () => { activeInstantAudioRef.current = null; };
417
+ audio.onerror = () => { console.error('[scene_instant] audio error'); activeInstantAudioRef.current = null; };
418
+ activeInstantAudioRef.current = audio;
419
+ audio.play().catch((err) => { console.error('[scene_instant] play failed:', err); activeInstantAudioRef.current = null; });
420
+ };
421
+ const currentlyPlaying = activeInstantAudioRef.current;
422
+ if (currentlyPlaying && !currentlyPlaying.paused && !currentlyPlaying.ended) {
423
+ const switchFadeMs = 1500;
424
+ stopSceneInstantAudio(switchFadeMs);
425
+ window.setTimeout(() => {
426
+ if (sceneInstantTakeSequenceRef.current !== takeSequence)
427
+ return;
428
+ playAudio();
429
+ }, switchFadeMs);
430
+ return;
431
+ }
432
+ stopSceneInstantAudio();
433
+ playAudio();
434
+ }
435
+ else if (data.type === 'scene_instant_stop') {
436
+ stopSceneInstantAudio(data.fadeMs || 0);
437
+ }
438
+ else if (data.type === 'scene_instant_state') {
439
+ console.log('[scene_instant_state]', data.playback?.isPlaying ? 'playing' : 'stopped');
440
+ const playback = data.playback;
441
+ if (playback?.isPlaying && playback?.instant?.audioUrl) {
442
+ const currentlyPlaying = activeInstantAudioRef.current;
443
+ if (currentlyPlaying && !currentlyPlaying.paused && !currentlyPlaying.ended) {
444
+ return;
445
+ }
446
+ stopSceneInstantAudio(1500);
447
+ window.setTimeout(() => {
448
+ const audio = new Audio(playback.instant.audioUrl);
449
+ audio.preload = 'auto';
450
+ audio.loop = true;
451
+ audio.volume = typeof playback.instant.volume === 'number' ? Math.max(0, Math.min(1, playback.instant.volume)) : 1;
452
+ audio.onended = () => { activeInstantAudioRef.current = null; };
453
+ audio.onerror = () => { console.error('[scene_instant] audio error'); activeInstantAudioRef.current = null; };
454
+ activeInstantAudioRef.current = audio;
455
+ audio.play().catch((err) => { console.error('[scene_instant] play failed:', err); activeInstantAudioRef.current = null; });
456
+ }, 1500);
457
+ }
458
+ else {
459
+ stopSceneInstantAudio();
460
+ }
461
+ }
462
+ else if (data.type === 'instant_play' && data.instant?.audioUrl) {
463
+ console.log('[instant_play]', data.instant.name, data.instant.audioUrl);
464
+ const ms = (mixerSettingsRef.current || {});
465
+ const masterVol = ms.instantMasterVolume ?? 1;
466
+ const muted = ms.instantMuted === true;
467
+ const baseVol = typeof data.instant.volume === 'number' ? Math.max(0, Math.min(1, data.instant.volume)) : 1;
468
+ const finalVol = muted ? 0 : Math.max(0, Math.min(1, baseVol * masterVol));
469
+ const audio = new Audio(data.instant.audioUrl);
470
+ audio.preload = 'auto';
471
+ audio.volume = finalVol;
472
+ const cleanup = () => {
473
+ activeInstantAudiosRef.current.delete(audio);
474
+ };
475
+ audio.onended = cleanup;
476
+ audio.onerror = () => { console.error('[instant_play] error'); cleanup(); };
477
+ activeInstantAudiosRef.current.add(audio);
478
+ audio.play().catch((err) => { console.error('[instant_play] play failed:', err); cleanup(); });
479
+ }
480
+ else if (data.type === 'instant_stop_all') {
481
+ stopSceneInstantAudio();
482
+ for (const audio of activeInstantAudiosRef.current) {
483
+ audio.pause();
484
+ try {
485
+ audio.currentTime = 0;
486
+ }
487
+ catch { /* no-op */ }
488
+ audio.onended = null;
489
+ audio.onerror = null;
490
+ }
491
+ activeInstantAudiosRef.current.clear();
492
+ }
493
+ else if (data.type === 'audio_bus_update' && data.settings) {
494
+ const settings = data.settings;
495
+ mixerSettingsRef.current = settings.mixerSettings || {};
496
+ // Update scene instant volume
497
+ const current = activeInstantAudioRef.current;
498
+ if (current) {
499
+ const ms = mixerSettingsRef.current;
500
+ const vol = ms.sceneInstantMasterVolume ?? 1;
501
+ const muted = ms.sceneInstantMuted === true;
502
+ current.volume = muted ? 0 : Math.max(0, Math.min(1, vol));
503
+ }
504
+ // Update instant play volumes
505
+ const ms = (mixerSettingsRef.current || {});
506
+ const instantVol = ms.instantMasterVolume ?? 1;
507
+ const instantMuted = ms.instantMuted === true;
508
+ for (const audio of activeInstantAudiosRef.current) {
509
+ audio.volume = instantMuted ? 0 : Math.max(0, Math.min(1, instantVol));
510
+ }
511
+ // Handle song sequence playback
512
+ const songSeq = settings.songSequence;
513
+ const currentSong = songAudioRef.current;
514
+ const hasActiveSong = songSeq && songSeq.items && songSeq.items.length > 0 && songSeq.items.some((item) => item.audioUrl);
515
+ if (hasActiveSong) {
516
+ const activeItem = songSeq.items.find((item) => item.id === songSeq.activeItemId) || songSeq.items[0];
517
+ const songUrl = activeItem?.audioUrl;
518
+ if (songUrl) {
519
+ if (!currentSong || currentSong.src !== songUrl) {
520
+ if (currentSong) {
521
+ currentSong.pause();
522
+ currentSong.onended = null;
523
+ currentSong.onerror = null;
524
+ }
525
+ const audio = new Audio(songUrl);
526
+ audio.preload = 'auto';
527
+ audio.loop = songSeq.loop !== false;
528
+ const ms2 = (mixerSettingsRef.current || {});
529
+ const songMuted = ms2.songMuted === true;
530
+ const songVol = ms2.songMasterVolume ?? 1;
531
+ audio.volume = songMuted ? 0 : Math.max(0, Math.min(1, songVol));
532
+ audio.onerror = () => { console.error('[song] playback error'); };
533
+ songAudioRef.current = audio;
534
+ audio.play().catch((err) => { console.error('[song] play failed:', err); });
535
+ }
536
+ }
537
+ }
538
+ else if (currentSong) {
539
+ currentSong.pause();
540
+ currentSong.onended = null;
541
+ currentSong.onerror = null;
542
+ songAudioRef.current = null;
543
+ }
544
+ }
545
+ else if (data.type === 'song_off_air') {
546
+ const song = songAudioRef.current;
547
+ if (song) {
548
+ song.pause();
549
+ song.onended = null;
550
+ song.onerror = null;
551
+ songAudioRef.current = null;
552
+ }
553
+ }
554
+ else if (data.type === 'program_reload') {
555
+ if (typeof window === 'undefined')
556
+ return;
557
+ const reloadWithCacheBust = () => {
558
+ const nextUrl = new URL(window.location.href);
559
+ nextUrl.searchParams.set('_reload', Date.now().toString());
560
+ window.location.replace(nextUrl.toString());
561
+ };
562
+ if (!('caches' in window)) {
563
+ reloadWithCacheBust();
564
+ return;
565
+ }
566
+ void window.caches.keys()
567
+ .then((keys) => Promise.all(keys.map((key) => window.caches.delete(key))))
568
+ .catch((err) => { console.warn('[program_reload] cache clear failed:', err); })
569
+ .finally(() => { reloadWithCacheBust(); });
570
+ }
571
+ else if (data.type === 'program_stingers_changed') {
572
+ // stinger video URLs for scene transitions — stored for transition system
573
+ }
574
+ }
575
+ });
576
+ const refreshEvents = useCallback(async () => {
577
+ await fetchEvents({
578
+ language: currentLanguage,
579
+ allowedLanguages: [currentLanguage]
580
+ });
581
+ const cachedEvents = getCachedEvents();
582
+ if (!cachedEvents) {
583
+ return;
584
+ }
585
+ setStageEvents((prevStage) => {
586
+ const prevJson = JSON.stringify(prevStage);
587
+ const nextJson = JSON.stringify(cachedEvents);
588
+ return prevJson === nextJson ? prevStage : cachedEvents;
589
+ });
590
+ }, [currentLanguage]);
591
+ useEffect(() => {
592
+ if (stageEvents.length > 0 && programEvents.length > 0 && hasEventChanges(programEvents, stageEvents) && !showCurtain && !updatePending) {
593
+ setUpdatePending(true);
594
+ }
595
+ }, [programEvents, showCurtain, stageEvents, updatePending]);
596
+ const handleMarqueeCycleComplete = useCallback(() => {
597
+ if (updatePendingRef.current) {
598
+ setShowCurtain(true);
599
+ setUpdatePending(false);
600
+ }
601
+ }, []);
602
+ const handleCurtainComplete = useCallback(() => {
603
+ setProgramEvents(stageEvents);
604
+ setShowCurtain(false);
605
+ }, [stageEvents]);
606
+ const handlePlaylistLoop = useCallback(() => {
607
+ setLanguageIndex((prev) => (prev + 1) % Math.max(1, languageRotation.length));
608
+ }, [languageRotation.length]);
609
+ const articlesSegment = useMemo(() => {
610
+ const segment = createArticlesSegment(articles, setArticles, currentLanguage);
611
+ segment.durationMsPerItem = config.articlesDurationMs;
612
+ const originalRender = segment.render;
613
+ const originalOnEnter = segment.onEnter;
614
+ segment.onEnter = () => {
615
+ originalOnEnter?.();
616
+ lastFetchedItemRef.current = -1;
617
+ };
618
+ segment.render = (itemIndex, progress) => {
619
+ if (lastFetchedItemRef.current !== itemIndex) {
620
+ lastFetchedItemRef.current = itemIndex;
621
+ void refreshEvents();
622
+ }
623
+ return originalRender(itemIndex, progress);
624
+ };
625
+ return segment;
626
+ }, [articles, currentLanguage, refreshEvents, config.articlesDurationMs]);
627
+ const filteredWeatherData = useMemo(() => {
628
+ if (!config.weatherCities || config.weatherCities.length === 0) {
629
+ return weatherData;
630
+ }
631
+ const allowed = new Set(config.weatherCities.map(normalizeCityKey));
632
+ return weatherData
633
+ .map((region) => ({
634
+ ...region,
635
+ cities: region.cities.filter((city) => allowed.has(normalizeCityKey(city.name)))
636
+ }))
637
+ .filter((region) => region.cities.length > 0);
638
+ }, [config.weatherCities, weatherData]);
639
+ const liveEventSegment = useMemo(() => {
640
+ const segment = createLiveEventSegment(liveEvent, setLiveEvent, currentLanguage);
641
+ segment.durationMsPerItem = 15000;
642
+ return segment;
643
+ }, [liveEvent, setLiveEvent, currentLanguage]);
644
+ const weatherSegment = useMemo(() => {
645
+ const segment = createWeatherSegment(filteredWeatherData, setWeatherData, currentLanguage);
646
+ segment.durationMsPerItem = config.weatherDurationMs;
647
+ return segment;
648
+ }, [filteredWeatherData, setWeatherData, currentLanguage, config.weatherDurationMs]);
649
+ const earthquakeSegment = useMemo(() => {
650
+ const segment = createEarthquakeSegment(earthquakes, setEarthquakes, currentLanguage);
651
+ segment.durationMsPerItem = config.earthquakesDurationMs;
652
+ return segment;
653
+ }, [earthquakes, setEarthquakes, currentLanguage, config.earthquakesDurationMs]);
654
+ const marketsSegment = useMemo(() => {
655
+ const segment = createMarketsSegment(markets, setMarkets, currentLanguage);
656
+ segment.durationMsPerItem = config.marketsDurationMs;
657
+ return segment;
658
+ }, [markets, setMarkets, currentLanguage, config.marketsDurationMs]);
659
+ const segments = useMemo(() => {
660
+ const nextSegments = [];
661
+ if (config.showLiveEvents)
662
+ nextSegments.push(liveEventSegment);
663
+ if (config.showArticles)
664
+ nextSegments.push(articlesSegment);
665
+ if (config.showWeather)
666
+ nextSegments.push(weatherSegment);
667
+ if (config.showEarthquakes)
668
+ nextSegments.push(earthquakeSegment);
669
+ if (config.showMarkets)
670
+ nextSegments.push(marketsSegment);
671
+ return nextSegments;
672
+ }, [liveEventSegment, articlesSegment, weatherSegment, earthquakeSegment, marketsSegment, config.showLiveEvents, config.showArticles, config.showWeather, config.showEarthquakes, config.showMarkets]);
673
+ const { state: playlistState, currentSegment, pause, resume, reset } = usePlaylistEngine({
674
+ segments,
675
+ defaultDurationMs: config.playlistDefaultDurationMs,
676
+ updateIntervalMs: config.playlistUpdateIntervalMs,
677
+ onPlaylistLoop: handlePlaylistLoop
678
+ });
679
+ useEffect(() => {
680
+ if (!audioRef.current || audioInitialized.current) {
681
+ return;
682
+ }
683
+ audioRef.current.load();
684
+ audioInitialized.current = true;
685
+ }, []);
686
+ useEffect(() => {
687
+ if (!config.audioCueEnabled) {
688
+ return;
689
+ }
690
+ const interval = window.setInterval(() => {
691
+ const now = new Date();
692
+ if (now.getMinutes() === config.audioCueMinute && now.getSeconds() === config.audioCueSecond && audioRef.current) {
693
+ audioRef.current.currentTime = 0;
694
+ void audioRef.current.play().catch((error) => {
695
+ console.log('Audio playback prevented:', error);
696
+ });
697
+ }
698
+ }, 1000);
699
+ return () => window.clearInterval(interval);
700
+ }, [config.audioCueEnabled, config.audioCueMinute, config.audioCueSecond]);
701
+ useEffect(() => {
702
+ const checkTime = () => {
703
+ const now = new Date();
704
+ const minutes = now.getMinutes();
705
+ const seconds = now.getSeconds();
706
+ const nycTime = new Date(now.toLocaleString('en-US', { timeZone: 'America/New_York' }));
707
+ const launchDateNyc = new Date(normalizeLaunchDate(config.callsignPrelaunchUntilNyc).toLocaleString('en-US', { timeZone: 'America/New_York' }));
708
+ const isBeforeLaunch = nycTime < launchDateNyc;
709
+ const withinScheduleWindow = (minutes === 59 && seconds >= config.callsignWindowStartSecond) || (minutes === 0 && seconds <= config.callsignWindowEndSecond);
710
+ const shouldShow = config.showCallsignTake && (isBeforeLaunch || withinScheduleWindow);
711
+ if (shouldShow && !showLogoSlide) {
712
+ reset();
713
+ }
714
+ setShowLogoSlide(shouldShow);
715
+ if (shouldShow) {
716
+ setCallsignTime(now);
717
+ }
718
+ };
719
+ checkTime();
720
+ const timer = window.setInterval(checkTime, 1000);
721
+ return () => window.clearInterval(timer);
722
+ }, [reset, config.showCallsignTake, config.callsignPrelaunchUntilNyc, config.callsignWindowStartSecond, config.callsignWindowEndSecond, showLogoSlide]);
723
+ useEffect(() => {
724
+ if (showLogoSlide) {
725
+ pause();
726
+ }
727
+ else if (playlistState.isPaused) {
728
+ resume();
729
+ }
730
+ }, [pause, resume, showLogoSlide, playlistState.isPaused]);
731
+ const marqueeEnabled = layerAvailability.marquee && config.showMarquee;
732
+ const isMarqueeVisible = useMemo(() => {
733
+ if (!marqueeEnabled || showLogoSlide || segments.length === 0) {
734
+ return false;
735
+ }
736
+ return true;
737
+ }, [marqueeEnabled, showLogoSlide, segments.length]);
738
+ const stageContainerStyle = embedded
739
+ ? { width: '100%', height: '100%' }
740
+ : { width: '1920px', height: '1080px', transform: 'scale(min(1, min(100vw / 1920, 100vh / 1080)))', transformOrigin: 'center center' };
741
+ const stageContainerClass = embedded
742
+ ? 'relative bg-black text-white overflow-hidden w-full h-full'
743
+ : 'relative bg-black text-white overflow-hidden shadow-2xl';
744
+ const loadingStage = (_jsx("div", { className: stageContainerClass, style: stageContainerStyle, children: layerAvailability.content ? _jsx(CallsignSlide, { currentTime: callsignTime, audioRef: audioRef }) : _jsx("div", { className: 'absolute inset-0 bg-black' }) }));
745
+ if (!dataLoaded) {
746
+ return embedded ? (_jsx("div", { className: 'w-full h-full bg-black overflow-hidden', children: loadingStage })) : (_jsx("div", { className: 'min-h-screen bg-black flex items-center justify-center overflow-hidden', children: loadingStage }));
747
+ }
748
+ const liveStage = (_jsxs("div", { className: stageContainerClass, style: stageContainerStyle, children: [layerAvailability.content ? (showLogoSlide ? (_jsx(CallsignSlide, { currentTime: callsignTime, audioRef: audioRef })) : currentSegment ? (currentSegment.render(playlistState.currentItemIndex, playlistState.progress)) : (_jsx("div", { className: 'absolute inset-0 bg-black' }))) : (_jsx("div", { className: 'absolute inset-0 bg-black' })), isMarqueeVisible && (_jsx("div", { className: 'absolute bottom-0 left-0 right-0 z-100 transition-transform duration-1000 ease-in-out translate-y-0', children: !showLogoSlide &&
749
+ (showCurtain ? (_jsx(MarqueeCurtain, { onComplete: handleCurtainComplete })) : (_jsx(Marquee, { events: programEvents, onCycleComplete: handleMarqueeCycleComplete, minPostsCount: config.marqueeMinPostsCount, minAverageRelevance: config.marqueeMinAverageRelevance, minMedianRelevance: config.marqueeMinMedianRelevance, pixelsPerSecond: config.marqueePixelsPerSecond, minDurationSeconds: config.marqueeMinDurationSeconds, heightPx: config.marqueeHeightPx }))) })), (config.showWorldClocks || config.showBellIcon) && (_jsxs("div", { className: 'absolute top-16 right-24 z-50 flex items-start gap-6', children: [config.showWorldClocks && (_jsx("div", { className: 'flex items-start pt-1.5', children: _jsx(WorldClocks, { currentTime: callsignTime, language: currentLanguage, cities: config.worldClockCities, rotateIntervalMs: config.worldClockRotateIntervalMs, transitionDurationMs: config.worldClockTransitionMs, shuffleCities: config.worldClockShuffle, widthPx: config.worldClockWidthPx }) })), config.showBellIcon && (_jsx("div", { className: 'bg-[#b21100] text-white p-6 shadow-2xl', children: _jsx(BellRing, { size: 64, strokeWidth: 2 }) }))] }))] }));
750
+ return (_jsxs("div", { className: embedded ? 'w-full h-full bg-black overflow-hidden' : 'min-h-screen bg-black flex items-center justify-center overflow-hidden', children: [liveStage, _jsx("audio", { ref: audioRef, preload: 'auto', children: _jsx("source", { src: FIFTHBELL_ASSETS.audio.pipes, type: 'audio/ogg' }) }), _jsx("style", { children: slideStyles })] }));
751
+ }