@fifthbell/brokaw 0.1.48 → 0.1.49

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 (41) hide show
  1. package/dist/components/live-program/LiveProgram.d.ts +2 -1
  2. package/dist/components/live-program/LiveProgram.js +257 -32
  3. package/dist/components/live-program/assets.d.ts +8 -8
  4. package/dist/components/live-program/assets.js +8 -8
  5. package/dist/components/live-program/components/slides/ArticleSlide.js +12 -2
  6. package/dist/components/live-program/components/slides/LiveEventSlide.d.ts +9 -0
  7. package/dist/components/live-program/components/slides/LiveEventSlide.js +86 -0
  8. package/dist/components/live-program/hooks/useSSE.js +72 -32
  9. package/dist/components/live-program/i18n.js +15 -0
  10. package/dist/components/live-program/segments/LiveEventSegment.d.ts +4 -0
  11. package/dist/components/live-program/segments/LiveEventSegment.js +27 -0
  12. package/dist/components/live-program/segments/MarketsSegment.js +3 -14
  13. package/dist/components/live-program/segments/WeatherSegment.js +11 -76
  14. package/dist/components/live-program/segments/fetchLiveEvents.d.ts +20 -0
  15. package/dist/components/live-program/segments/fetchLiveEvents.js +76 -0
  16. package/dist/components/live-program/segments/index.d.ts +2 -0
  17. package/dist/components/live-program/segments/index.js +2 -0
  18. package/dist/live-program/main.d.ts +1 -0
  19. package/dist/live-program/main.js +10 -0
  20. package/dist/live-program-page/fifthbell/audio/pipes.ogg +0 -0
  21. package/dist/live-program-page/fifthbell/images/berlin.jpg +0 -0
  22. package/dist/live-program-page/fifthbell/images/fifthbell.png +0 -0
  23. package/dist/live-program-page/fifthbell/images/nyc.jpg +0 -0
  24. package/dist/live-program-page/fifthbell/images/nyse.jpg +0 -0
  25. package/dist/live-program-page/fifthbell/images/santiago.jpg +0 -0
  26. package/dist/live-program-page/fifthbell/images/seismograph.jpg +0 -0
  27. package/dist/live-program-page/fifthbell/images/tokyo.jpg +0 -0
  28. package/dist/live-program-page/index.html +16 -0
  29. package/dist/live-program-page/live-program.css +3 -0
  30. package/dist/live-program-page/live-program.js +76 -0
  31. package/dist/renderer.d.ts +2 -1
  32. package/dist/renderer.js +1 -1
  33. package/dist/renderer.node.d.ts +8 -0
  34. package/dist/renderer.node.js +61 -0
  35. package/dist/utils/sofascore.d.ts +1 -1
  36. package/dist/utils/sofascore.js +2 -2
  37. package/package.json +6 -2
  38. package/src/styles/compiled.css +1 -1
  39. package/src/templates/partials/headers/header-main.hbs +5 -2
  40. package/src/templates/partials/headers/header-minimal.hbs +1 -1
  41. package/src/templates/partials/shell/doc-start-standard.hbs +1 -1
@@ -3,6 +3,7 @@ interface LiveProgramProps {
3
3
  embedded?: boolean;
4
4
  sceneMetadata?: Record<string, unknown> | null;
5
5
  activeComponents?: string[];
6
+ apiBaseUrl?: string;
6
7
  }
7
- export default function LiveProgram({ programId, embedded, sceneMetadata, activeComponents }: LiveProgramProps): import("react/jsx-runtime").JSX.Element;
8
+ export default function LiveProgram({ embedded, sceneMetadata, activeComponents, apiBaseUrl }: LiveProgramProps): import("react/jsx-runtime").JSX.Element;
8
9
  export {};
@@ -1,26 +1,15 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+ import { BellRing } from 'lucide-react';
3
4
  import { useSSE } from './hooks/useSSE.js';
4
- // Hardcoded API Base URL for Fifthbell
5
- function getApiBaseUrl() {
6
- if (typeof window === 'undefined')
7
- return 'http://127.0.0.1:3000';
8
- // Use the current host to dynamically target however they're accessing it
9
- const hostname = window.location.hostname;
10
- return `http://${hostname.includes(':') ? `[${hostname}]` : hostname}:3000`;
11
- }
12
- function apiUrl(path) {
13
- const normalizedPath = path.startsWith('/') ? path : `/${path}`;
14
- return `${getApiBaseUrl()}${normalizedPath}`;
15
- }
16
5
  import { FIFTHBELL_ASSETS } from './assets.js';
17
6
  import { MarqueeCurtain } from './components/MarqueeCurtain.js';
18
7
  import Marquee from './components/Marquee.js';
19
- import { DEFAULT_WORLD_CLOCK_CITIES } from './components/WorldClocks.js';
8
+ import { WorldClocks, DEFAULT_WORLD_CLOCK_CITIES } from './components/WorldClocks.js';
20
9
  import { CallsignSlide } from './components/slides/CallsignSlide.js';
21
10
  import { slideStyles } from './components/slides/slideStyles.js';
22
11
  import { fetchEvents, getCachedEvents, hasEventChanges } from './events.js';
23
- import { createArticlesSegment, createEarthquakeSegment, createMarketsSegment, createWeatherSegment, fetchArticles, fetchEarthquakes, fetchMarketData, fetchWeatherData, usePlaylistEngine } from './segments/index.js';
12
+ import { createArticlesSegment, createEarthquakeSegment, createLiveEventSegment, createMarketsSegment, createWeatherSegment, fetchArticles, fetchEarthquakes, fetchLiveEvent, fetchMarketData, fetchWeatherData, usePlaylistEngine } from './segments/index.js';
24
13
  const DEFAULT_LANGUAGE_ROTATION = ['en', 'es', 'en', 'it'];
25
14
  const DEFAULT_CALLSIGN_PRELAUNCH_UNTIL_NYC = '2026-01-02T21:30:00';
26
15
  const FIFTHBELL_COMPONENT_TYPE_CONTENT = 'fifthbell-content';
@@ -33,6 +22,7 @@ const DEFAULT_FIFTHBELL_CONFIG = {
33
22
  showWeather: true,
34
23
  showEarthquakes: true,
35
24
  showMarkets: true,
25
+ showLiveEvents: true,
36
26
  showMarquee: false,
37
27
  showCallsignTake: true,
38
28
  weatherCities: [],
@@ -203,6 +193,7 @@ function extractConfigFromMetadata(metadataInput) {
203
193
  showWeather: normalizeBoolean(contentProps.showWeather, DEFAULT_FIFTHBELL_CONFIG.showWeather),
204
194
  showEarthquakes: normalizeBoolean(contentProps.showEarthquakes, DEFAULT_FIFTHBELL_CONFIG.showEarthquakes),
205
195
  showMarkets: normalizeBoolean(contentProps.showMarkets, DEFAULT_FIFTHBELL_CONFIG.showMarkets),
196
+ showLiveEvents: normalizeBoolean(contentProps.showLiveEvents, DEFAULT_FIFTHBELL_CONFIG.showLiveEvents),
206
197
  showMarquee: normalizeBoolean(marqueeProps.showMarquee, DEFAULT_FIFTHBELL_CONFIG.showMarquee),
207
198
  showCallsignTake: normalizeBoolean(contentProps.showCallsignTake, DEFAULT_FIFTHBELL_CONFIG.showCallsignTake),
208
199
  weatherCities: normalizeStringArray(contentProps.weatherCities),
@@ -244,8 +235,7 @@ function normalizeLaunchDate(rawDate) {
244
235
  }
245
236
  return parsed;
246
237
  }
247
- export default function LiveProgram({ programId = 'fifthbell', embedded = false, sceneMetadata, activeComponents }) {
248
- const encodedProgramId = encodeURIComponent(programId);
238
+ export default function LiveProgram({ embedded = false, sceneMetadata, activeComponents, apiBaseUrl }) {
249
239
  const [state, setState] = useState(null);
250
240
  const [showLogoSlide, setShowLogoSlide] = useState(false);
251
241
  const [callsignTime, setCallsignTime] = useState(new Date());
@@ -256,6 +246,7 @@ export default function LiveProgram({ programId = 'fifthbell', embedded = false,
256
246
  const [weatherData, setWeatherData] = useState([]);
257
247
  const [earthquakes, setEarthquakes] = useState([]);
258
248
  const [markets, setMarkets] = useState([]);
249
+ const [liveEvent, setLiveEvent] = useState(null);
259
250
  const [stageEvents, setStageEvents] = useState([]);
260
251
  const [programEvents, setProgramEvents] = useState([]);
261
252
  const [showCurtain, setShowCurtain] = useState(false);
@@ -263,6 +254,11 @@ export default function LiveProgram({ programId = 'fifthbell', embedded = false,
263
254
  const updatePendingRef = useRef(false);
264
255
  const [dataLoaded, setDataLoaded] = useState(false);
265
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);
266
262
  const controlledBySceneRenderer = sceneMetadata !== undefined;
267
263
  const effectiveSceneMetadata = useMemo(() => {
268
264
  if (sceneMetadata !== undefined) {
@@ -274,6 +270,13 @@ export default function LiveProgram({ programId = 'fifthbell', embedded = false,
274
270
  const layerAvailability = useMemo(() => resolveFifthBellLayerAvailability(activeComponents), [activeComponents]);
275
271
  const languageRotation = config.languageRotation;
276
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
+ })();
277
280
  useEffect(() => {
278
281
  if (languageIndex >= languageRotation.length) {
279
282
  setLanguageIndex(0);
@@ -286,17 +289,18 @@ export default function LiveProgram({ programId = 'fifthbell', embedded = false,
286
289
  if (controlledBySceneRenderer) {
287
290
  return;
288
291
  }
289
- fetch(apiUrl(`/program/${encodedProgramId}/state`))
292
+ fetch(`${resolvedApiBaseUrl}/state`)
290
293
  .then((res) => res.json())
291
294
  .then((data) => setState(data))
292
295
  .catch((err) => console.error('Failed to fetch FifthBell program state:', err));
293
- }, [controlledBySceneRenderer, encodedProgramId]);
296
+ }, [controlledBySceneRenderer, resolvedApiBaseUrl]);
294
297
  const refreshAllData = useCallback(async () => {
295
- const [articlesData, weatherDataResult, earthquakesData, marketsData] = await Promise.all([
298
+ const [articlesData, weatherDataResult, earthquakesData, marketsData, liveEventData] = await Promise.all([
296
299
  fetchArticles(currentLanguage),
297
300
  fetchWeatherData(),
298
301
  fetchEarthquakes(currentLanguage),
299
302
  fetchMarketData(),
303
+ fetchLiveEvent(currentLanguage),
300
304
  fetchEvents({
301
305
  language: currentLanguage,
302
306
  allowedLanguages: [currentLanguage]
@@ -306,6 +310,7 @@ export default function LiveProgram({ programId = 'fifthbell', embedded = false,
306
310
  setWeatherData(weatherDataResult);
307
311
  setEarthquakes(earthquakesData);
308
312
  setMarkets(marketsData);
313
+ setLiveEvent(liveEventData);
309
314
  const cachedEvents = getCachedEvents();
310
315
  if (cachedEvents) {
311
316
  setStageEvents(cachedEvents);
@@ -316,11 +321,58 @@ export default function LiveProgram({ programId = 'fifthbell', embedded = false,
316
321
  useEffect(() => {
317
322
  void refreshAllData();
318
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
+ }, []);
319
360
  useSSE({
320
- url: apiUrl(`/program/${encodedProgramId}/events`),
361
+ url: `${resolvedApiBaseUrl}/events`,
321
362
  enabled: !controlledBySceneRenderer,
322
363
  onMessage: (data) => {
323
- if ((data.type === 'scene_change' || data.type === 'program_scenes_changed') && data.state) {
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) {
324
376
  setState(data.state);
325
377
  }
326
378
  else if (data.type === 'scene_update') {
@@ -344,17 +396,183 @@ export default function LiveProgram({ programId = 'fifthbell', embedded = false,
344
396
  };
345
397
  });
346
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
+ }
347
574
  }
348
575
  });
349
- useEffect(() => {
350
- if (dataLoaded || config.dataLoadTimeoutMs <= 0) {
351
- return;
352
- }
353
- const timeoutId = window.setTimeout(() => {
354
- window.location.reload();
355
- }, config.dataLoadTimeoutMs);
356
- return () => window.clearTimeout(timeoutId);
357
- }, [dataLoaded, config.dataLoadTimeoutMs]);
358
576
  const refreshEvents = useCallback(async () => {
359
577
  await fetchEvents({
360
578
  language: currentLanguage,
@@ -418,6 +636,11 @@ export default function LiveProgram({ programId = 'fifthbell', embedded = false,
418
636
  }))
419
637
  .filter((region) => region.cities.length > 0);
420
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]);
421
644
  const weatherSegment = useMemo(() => {
422
645
  const segment = createWeatherSegment(filteredWeatherData, setWeatherData, currentLanguage);
423
646
  segment.durationMsPerItem = config.weatherDurationMs;
@@ -435,6 +658,8 @@ export default function LiveProgram({ programId = 'fifthbell', embedded = false,
435
658
  }, [markets, setMarkets, currentLanguage, config.marketsDurationMs]);
436
659
  const segments = useMemo(() => {
437
660
  const nextSegments = [];
661
+ if (config.showLiveEvents)
662
+ nextSegments.push(liveEventSegment);
438
663
  if (config.showArticles)
439
664
  nextSegments.push(articlesSegment);
440
665
  if (config.showWeather)
@@ -444,7 +669,7 @@ export default function LiveProgram({ programId = 'fifthbell', embedded = false,
444
669
  if (config.showMarkets)
445
670
  nextSegments.push(marketsSegment);
446
671
  return nextSegments;
447
- }, [articlesSegment, weatherSegment, earthquakeSegment, marketsSegment, config.showArticles, config.showWeather, config.showEarthquakes, config.showMarkets]);
672
+ }, [liveEventSegment, articlesSegment, weatherSegment, earthquakeSegment, marketsSegment, config.showLiveEvents, config.showArticles, config.showWeather, config.showEarthquakes, config.showMarkets]);
448
673
  const { state: playlistState, currentSegment, pause, resume, reset } = usePlaylistEngine({
449
674
  segments,
450
675
  defaultDurationMs: config.playlistDefaultDurationMs,
@@ -521,6 +746,6 @@ export default function LiveProgram({ programId = 'fifthbell', embedded = false,
521
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 }));
522
747
  }
523
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 &&
524
- (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 }))) }))] }));
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 }) }))] }))] }));
525
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 })] }));
526
751
  }
@@ -1,14 +1,14 @@
1
1
  export declare const FIFTHBELL_ASSETS: {
2
2
  readonly audio: {
3
- readonly pipes: "/fifthbell/audio/pipes.ogg";
3
+ readonly pipes: "./fifthbell/audio/pipes.ogg";
4
4
  };
5
5
  readonly images: {
6
- readonly logo: "/fifthbell/images/fifthbell.png";
7
- readonly nyc: "/fifthbell/images/nyc.jpg";
8
- readonly berlin: "/fifthbell/images/berlin.jpg";
9
- readonly santiago: "/fifthbell/images/santiago.jpg";
10
- readonly tokyo: "/fifthbell/images/tokyo.jpg";
11
- readonly nyse: "/fifthbell/images/nyse.jpg";
12
- readonly seismograph: "/fifthbell/images/seismograph.jpg";
6
+ readonly logo: "./fifthbell/images/fifthbell.png";
7
+ readonly nyc: "./fifthbell/images/nyc.jpg";
8
+ readonly berlin: "./fifthbell/images/berlin.jpg";
9
+ readonly santiago: "./fifthbell/images/santiago.jpg";
10
+ readonly tokyo: "./fifthbell/images/tokyo.jpg";
11
+ readonly nyse: "./fifthbell/images/nyse.jpg";
12
+ readonly seismograph: "./fifthbell/images/seismograph.jpg";
13
13
  };
14
14
  };
@@ -1,14 +1,14 @@
1
1
  export const FIFTHBELL_ASSETS = {
2
2
  audio: {
3
- pipes: '/fifthbell/audio/pipes.ogg'
3
+ pipes: './fifthbell/audio/pipes.ogg'
4
4
  },
5
5
  images: {
6
- logo: '/fifthbell/images/fifthbell.png',
7
- nyc: '/fifthbell/images/nyc.jpg',
8
- berlin: '/fifthbell/images/berlin.jpg',
9
- santiago: '/fifthbell/images/santiago.jpg',
10
- tokyo: '/fifthbell/images/tokyo.jpg',
11
- nyse: '/fifthbell/images/nyse.jpg',
12
- seismograph: '/fifthbell/images/seismograph.jpg'
6
+ logo: './fifthbell/images/fifthbell.png',
7
+ nyc: './fifthbell/images/nyc.jpg',
8
+ berlin: './fifthbell/images/berlin.jpg',
9
+ santiago: './fifthbell/images/santiago.jpg',
10
+ tokyo: './fifthbell/images/tokyo.jpg',
11
+ nyse: './fifthbell/images/nyse.jpg',
12
+ seismograph: './fifthbell/images/seismograph.jpg'
13
13
  }
14
14
  };
@@ -1,9 +1,19 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useMemo, useState } from 'react';
3
3
  import { FastAverageColor } from 'fast-average-color';
4
+ import qrcode from 'qrcode-generator';
4
5
  const fac = new FastAverageColor();
5
6
  function buildQrCodeUrl(value) {
6
- return `https://api.qrserver.com/v1/create-qr-code/?size=150x150&data=${encodeURIComponent(value)}`;
7
+ const qr = qrcode(0, 'M');
8
+ qr.addData(value);
9
+ qr.make();
10
+ let rawSvg = qr.createSvgTag(5, 0);
11
+ // Make background transparent and foreground white so it pops on the dark parent box.
12
+ // Leave width/height/viewBox as-is — the <img> CSS (w-32 h-32) handles scaling.
13
+ rawSvg = rawSvg.replace(/fill="(?:#ffffff|white)"/i, 'fill="transparent"');
14
+ rawSvg = rawSvg.replace(/fill="(?:#000000|black)"/i, 'fill="white"');
15
+ const encodedSvg = encodeURIComponent(rawSvg);
16
+ return `data:image/svg+xml;charset=utf-8,${encodedSvg}`;
7
17
  }
8
18
  export function ArticleSlide({ newsItem, progress }) {
9
19
  const [dominantColor, setDominantColor] = useState('#b21100');
@@ -18,5 +28,5 @@ export function ArticleSlide({ newsItem, progress }) {
18
28
  console.error('Error getting average color', error);
19
29
  }
20
30
  };
21
- return (_jsxs("div", { className: 'absolute inset-0', children: [_jsx("div", { className: 'absolute inset-0', children: _jsx("img", { src: imageUrl, alt: '', crossOrigin: 'anonymous', onLoad: handleImageLoad, className: 'w-full h-full object-cover blur-xl scale-105' }, imageUrl) }), _jsx("div", { className: 'absolute inset-0 opacity-75 mix-blend-multiply transition-all duration-1000', style: { background: `linear-gradient(to bottom right, ${dominantColor}, #000000)` } }), _jsx("div", { className: 'absolute inset-0 bg-[radial-gradient(circle_at_30%_30%,rgba(255,255,255,0.1),transparent_60%)]' }), _jsxs("div", { className: 'relative z-10 grid grid-cols-12 h-full', children: [_jsxs("div", { className: 'col-span-5 flex flex-col justify-center px-24 relative', children: [_jsx("div", { className: 'w-32 h-2 bg-white mb-12' }), _jsxs("div", { className: 'animate-slide-up', children: [_jsx("h1", { className: "text-5xl font-bold leading-tight mb-8 tracking-tight line-clamp-6 font-['Encode_Sans']", children: newsItem.headline }), _jsx("p", { className: "text-4xl font-light leading-relaxed opacity-90 line-clamp-6 font-['Libre_Franklin']", children: newsItem.summary })] }, newsItem.id), newsItem.category && (_jsx("div", { className: 'absolute bottom-40 left-24 animate-slide-up', children: _jsx("span", { className: 'text-white px-4 py-2 text-2xl font-bold uppercase tracking-wider', style: { backgroundColor: dominantColor }, children: newsItem.category }) }, `${newsItem.id}-cat`))] }), _jsx("div", { className: 'col-span-7 relative h-full flex items-center justify-center p-16 pb-40', children: _jsxs("div", { className: 'relative w-full aspect-video shadow-2xl overflow-hidden border-4 border-white/10', children: [_jsx("div", { className: 'absolute top-0 left-0 h-1 bg-white/30 w-full z-20', children: _jsx("div", { className: 'h-full bg-white transition-all duration-100 ease-linear', style: { width: `${progress}%` } }) }), _jsx("img", { src: imageUrl, alt: newsItem.headline, className: 'w-full h-full object-cover', style: { animation: 'kenburns 20s infinite alternate' } }, imageUrl)] }) })] }), _jsx("div", { className: 'absolute bottom-24 left-0 w-full flex justify-between items-end px-24 z-50', children: _jsx("div", { className: 'ml-auto p-2 bg-white/10 backdrop-blur-sm', children: _jsx("img", { src: qrCodeUrl, alt: 'Article QR code', className: 'block w-37.5 h-37.5' }) }) })] }));
31
+ return (_jsxs("div", { className: 'absolute inset-0', children: [_jsx("div", { className: 'absolute inset-0', children: _jsx("img", { src: imageUrl, alt: '', crossOrigin: 'anonymous', onLoad: handleImageLoad, className: 'w-full h-full object-cover blur-xl scale-105' }, imageUrl) }), _jsx("div", { className: 'absolute inset-0 opacity-75 mix-blend-multiply transition-all duration-1000', style: { background: `linear-gradient(to bottom right, ${dominantColor}, #000000)` } }), _jsx("div", { className: 'absolute inset-0 bg-[radial-gradient(circle_at_30%_30%,rgba(255,255,255,0.1),transparent_60%)]' }), _jsxs("div", { className: 'relative z-10 grid grid-cols-12 h-full', children: [_jsx("div", { className: 'col-span-5 flex flex-col justify-center p-24 relative bg-black/35 backdrop-blur-2xl', children: _jsxs("div", { className: 'animate-slide-up flex flex-col items-start', children: [newsItem.category && (_jsx("span", { className: "text-white text-3xl font-semibold uppercase tracking-wider mb-8 font-['Encode_Sans_Condensed'] inline-block px-4 py-2", style: { backgroundColor: dominantColor }, children: newsItem.category })), _jsx("div", { className: 'w-16 h-1.5 bg-[#b21100] mb-8' }), _jsx("h1", { className: "text-5xl font-bold leading-tight mb-8 tracking-tight line-clamp-6 font-['Encode_Sans'] [text-wrap:balance]", children: newsItem.headline }), _jsx("p", { className: "text-4xl font-light leading-relaxed opacity-90 line-clamp-6 font-['Libre_Franklin']", children: newsItem.summary })] }, newsItem.id) }), _jsx("div", { className: 'col-span-7 relative h-full flex items-center justify-center p-16 pb-40', children: _jsxs("div", { className: 'relative w-full aspect-video shadow-2xl overflow-hidden border-4 border-white/10', children: [_jsx("div", { className: 'absolute top-0 left-0 h-1 bg-white/30 w-full z-20', children: _jsx("div", { className: 'h-full bg-white transition-all duration-100 ease-linear', style: { width: `${progress}%` } }) }), _jsx("img", { src: imageUrl, alt: newsItem.headline, className: 'w-full h-full object-cover', style: { animation: 'kenburns 20s infinite alternate' } }, imageUrl), _jsx("div", { className: 'absolute bottom-8 right-8 z-30', children: _jsx("div", { className: 'p-2 rounded-sm backdrop-blur-md shadow-2xl', style: { backgroundColor: 'rgba(0,0,0,0.4)' }, children: _jsx("img", { src: qrCodeUrl, alt: 'Article QR code', className: 'block w-32 h-32' }) }) })] }) })] })] }));
22
32
  }
@@ -0,0 +1,9 @@
1
+ import type { LiveEventData } from '../../segments/fetchLiveEvents.js';
2
+ import type { SupportedLanguage } from '../../i18n.js';
3
+ interface LiveEventSlideProps {
4
+ event: LiveEventData;
5
+ progress: number;
6
+ language: SupportedLanguage;
7
+ }
8
+ export declare function LiveEventSlide({ event, progress, language }: LiveEventSlideProps): import("react/jsx-runtime").JSX.Element;
9
+ export {};
@@ -0,0 +1,86 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useRef, useState, useEffect, useCallback } from 'react';
3
+ import { FastAverageColor } from 'fast-average-color';
4
+ import qrcode from 'qrcode-generator';
5
+ import { t } from '../../i18n.js';
6
+ import { buildSofascoreAttackMomentumUrl } from '../../../../utils/sofascore.js';
7
+ const fac = new FastAverageColor();
8
+ function buildQrCodeUrl(value) {
9
+ const qr = qrcode(0, 'M');
10
+ qr.addData(value);
11
+ qr.make();
12
+ let rawSvg = qr.createSvgTag(5, 0);
13
+ rawSvg = rawSvg.replace(/fill="(?:#ffffff|white)"/i, 'fill="transparent"');
14
+ rawSvg = rawSvg.replace(/fill="(?:#000000|black)"/i, 'fill="white"');
15
+ const encodedSvg = encodeURIComponent(rawSvg);
16
+ return `data:image/svg+xml;charset=utf-8,${encodedSvg}`;
17
+ }
18
+ function formatUpdateTime(update) {
19
+ if (update.timestamp) {
20
+ const d = new Date(update.timestamp);
21
+ return d.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false });
22
+ }
23
+ return update.time || '';
24
+ }
25
+ function LatestUpdates({ updates, language }) {
26
+ const latest = useMemo(() => updates.slice(0, 5), [updates]);
27
+ if (latest.length === 0) {
28
+ return (_jsx("p", { className: "text-white/50 font-['Libre_Franklin'] text-xl", children: t('liveEvent.noUpdates', language) }));
29
+ }
30
+ return (_jsx("div", { className: 'flex flex-col gap-3', children: latest.map((update, i) => (_jsxs("div", { className: 'flex gap-3 items-start', children: [_jsx("div", { className: 'w-2 h-2 rounded-full bg-[#4FC3F7] flex-shrink-0 mt-2' }), _jsxs("div", { className: 'flex flex-col gap-0.5 min-w-0', children: [formatUpdateTime(update) && (_jsx("span", { className: "text-sm font-semibold text-[#0ea5e9] font-['Libre_Franklin']", children: formatUpdateTime(update) })), update.html ? (_jsx("div", { className: "text-white/90 text-lg leading-snug font-['Libre_Franklin'] [&_img]:hidden", children: update.html.replace(/<[^>]*>/g, '') })) : update.text ? (_jsx("p", { className: "text-white/90 text-lg leading-snug font-['Libre_Franklin'] truncate", children: update.text })) : null] })] }, i))) }));
31
+ }
32
+ function ScrollingTimeline({ updates, language }) {
33
+ const [visibleStart, setVisibleStart] = useState(0);
34
+ const displayCount = 4;
35
+ const cycleRef = useRef(null);
36
+ useEffect(() => {
37
+ if (updates.length <= displayCount)
38
+ return;
39
+ cycleRef.current = setInterval(() => {
40
+ setVisibleStart((prev) => (prev + 1) % updates.length);
41
+ }, 5000);
42
+ return () => {
43
+ if (cycleRef.current)
44
+ clearInterval(cycleRef.current);
45
+ };
46
+ }, [updates.length]);
47
+ const visibleUpdates = useMemo(() => {
48
+ if (updates.length <= displayCount)
49
+ return updates;
50
+ const items = [];
51
+ for (let i = 0; i < displayCount; i++) {
52
+ items.push(updates[(visibleStart + i) % updates.length]);
53
+ }
54
+ return items;
55
+ }, [updates, visibleStart, displayCount]);
56
+ if (updates.length === 0) {
57
+ return (_jsx("div", { className: 'flex items-center justify-center h-full', children: _jsx("p", { className: "text-white/50 font-['Libre_Franklin'] text-2xl", children: t('liveEvent.noUpdates', language) }) }));
58
+ }
59
+ return (_jsx("div", { className: 'flex flex-col justify-center gap-5 h-full', children: visibleUpdates.map((update, i) => (_jsxs("div", { className: 'flex gap-4 items-start animate-slide-up', children: [_jsx("div", { className: 'w-3 h-3 rounded-full bg-[#4FC3F7] flex-shrink-0 mt-1.5 shadow-[0_0_12px_rgba(79,195,247,0.5)]' }), _jsxs("div", { className: 'flex flex-col gap-1 min-w-0 flex-1', children: [formatUpdateTime(update) && (_jsx("span", { className: "text-sm font-semibold text-[#4FC3F7] font-['Libre_Franklin'] tracking-wide", children: formatUpdateTime(update) })), update.html ? (_jsx("div", { className: "text-white text-xl leading-snug font-['Libre_Franklin'] font-light [&_img]:hidden [&_a]:text-[#4FC3F7] [&_a]:underline", children: update.html.replace(/<[^>]*>/g, '') })) : update.text ? (_jsx("p", { className: "text-white text-xl leading-snug font-['Libre_Franklin'] font-light line-clamp-2", children: update.text })) : null] })] }, `${visibleStart}-${i}`))) }));
60
+ }
61
+ export function LiveEventSlide({ event, progress, language }) {
62
+ const [dominantColor, setDominantColor] = useState('#b21100');
63
+ const isSports = typeof event.sofascore_id === 'number' && event.sofascore_id > 0;
64
+ const qrCodeUrl = useMemo(() => buildQrCodeUrl(event.liveUrl || event.url), [event.liveUrl, event.url]);
65
+ const sofascoreWidgetUrl = useMemo(() => {
66
+ if (!isSports)
67
+ return '';
68
+ return buildSofascoreAttackMomentumUrl(event.sofascore_id, 'dark');
69
+ }, [isSports, event.sofascore_id]);
70
+ const handleImageLoad = useCallback((e) => {
71
+ try {
72
+ const color = fac.getColor(e.currentTarget);
73
+ setDominantColor(color.hex);
74
+ }
75
+ catch {
76
+ setDominantColor('#b21100');
77
+ }
78
+ }, []);
79
+ if (isSports) {
80
+ return (_jsxs("div", { className: 'absolute inset-0', children: [_jsx("div", { className: 'absolute inset-0', children: _jsx("img", { src: event.image, alt: '', crossOrigin: 'anonymous', onLoad: handleImageLoad, className: 'w-full h-full object-cover blur-xl scale-105' }, event.image) }), _jsx("div", { className: 'absolute inset-0 opacity-75 mix-blend-multiply transition-all duration-1000', style: { background: `linear-gradient(to bottom right, ${dominantColor}, #000000)` } }), _jsx("div", { className: 'absolute inset-0 bg-[radial-gradient(circle_at_30%_30%,rgba(255,255,255,0.1),transparent_60%)]' }), _jsxs("div", { className: 'relative z-10 grid grid-cols-12 h-full', children: [_jsx("div", { className: 'col-span-5 flex flex-col justify-center p-16 relative bg-black/35 backdrop-blur-2xl', children: _jsxs("div", { className: 'animate-slide-up flex flex-col items-start', children: [_jsx("span", { className: "inline-flex items-center px-4 py-1.5 bg-[#cc0000] text-white text-xl font-bold tracking-[0.08em] uppercase font-['Encode_Sans_Condensed'] mb-4", children: "LIVE" }), _jsx("span", { className: "text-white/80 text-2xl font-semibold uppercase tracking-wider mb-2 font-['Encode_Sans_Condensed']", children: event.category }), _jsx("div", { className: 'w-16 h-1.5 bg-[#b21100] mb-6' }), _jsx("h1", { className: "text-4xl font-bold leading-tight mb-4 tracking-tight line-clamp-5 font-['Encode_Sans'] [text-wrap:balance] text-white", children: event.title }), event.excerpt && (_jsx("p", { className: "text-2xl font-light leading-relaxed opacity-80 line-clamp-3 font-['Libre_Franklin'] text-white mb-6", children: event.excerpt })), _jsx(LatestUpdates, { updates: event.updates, language: language })] }) }), _jsx("div", { className: 'col-span-7 relative h-full flex items-center justify-center p-12 pb-32', children: _jsxs("div", { className: 'relative w-full max-w-lg flex flex-col gap-4', children: [_jsxs("div", { className: 'relative aspect-video shadow-2xl overflow-hidden border border-white/10', children: [_jsx("div", { className: 'absolute top-0 left-0 h-1 bg-white/30 w-full z-20', children: _jsx("div", { className: 'h-full bg-white transition-all duration-100 ease-linear', style: { width: `${progress}%` } }) }), _jsx("img", { src: event.image, alt: event.alt, className: 'w-full h-full object-cover', style: { animation: 'kenburns 20s infinite alternate' }, crossOrigin: 'anonymous' }, event.image)] }), sofascoreWidgetUrl && (_jsx("div", { className: 'w-full overflow-hidden border border-white/10 bg-black/40', children: _jsx("iframe", { width: '100%', height: '180', src: sofascoreWidgetUrl, title: 'SofaScore Attack Momentum', frameBorder: '0', scrolling: 'no', loading: 'lazy', referrerPolicy: 'no-referrer-when-downgrade', style: { border: 0 } }) })), _jsx("div", { className: 'absolute bottom-4 right-4 z-30', children: _jsx("div", { className: 'p-2 rounded-sm backdrop-blur-md shadow-2xl', style: { backgroundColor: 'rgba(0,0,0,0.4)' }, children: _jsx("img", { src: qrCodeUrl, alt: 'QR code', className: 'block w-24 h-24' }) }) })] }) })] })] }));
81
+ }
82
+ return (_jsxs("div", { className: 'absolute inset-0', children: [_jsx("div", { className: 'absolute inset-0', children: _jsx("img", { src: event.image, alt: '', crossOrigin: 'anonymous', onLoad: handleImageLoad, className: 'w-full h-full object-cover blur-xl scale-105' }, event.image) }), _jsx("div", { className: 'absolute inset-0 opacity-80 mix-blend-multiply transition-all duration-1000', style: { background: `linear-gradient(to bottom right, ${dominantColor}, #000000)` } }), _jsx("div", { className: 'absolute inset-0 bg-[radial-gradient(circle_at_30%_30%,rgba(255,255,255,0.1),transparent_60%)]' }), _jsxs("div", { className: 'relative z-10 grid grid-cols-12 h-full', children: [_jsx("div", { className: 'col-span-5 flex flex-col justify-center p-16 relative bg-black/35 backdrop-blur-2xl', children: _jsxs("div", { className: 'animate-slide-up flex flex-col items-start', children: [_jsx("span", { className: "inline-flex items-center px-4 py-1.5 bg-[#cc0000] text-white text-xl font-bold tracking-[0.08em] uppercase font-['Encode_Sans_Condensed'] mb-4", children: "LIVE" }), _jsx("span", { className: "text-white/80 text-2xl font-semibold uppercase tracking-wider mb-2 font-['Encode_Sans_Condensed']", children: event.category }), _jsx("div", { className: 'w-16 h-1.5 bg-[#b21100] mb-6' }), _jsx("h1", { className: "text-4xl font-bold leading-tight mb-4 tracking-tight line-clamp-5 font-['Encode_Sans'] [text-wrap:balance] text-white", children: event.title }), event.excerpt && (_jsx("p", { className: "text-2xl font-light leading-relaxed opacity-80 line-clamp-3 font-['Libre_Franklin'] text-white", children: event.excerpt }))] }) }), _jsxs("div", { className: 'col-span-7 relative h-full flex flex-col justify-center p-16 pl-12', children: [_jsxs("div", { className: 'flex items-start gap-4 mb-8', children: [_jsx("div", { className: 'bg-[#b21100] text-white px-4 py-2 shadow-lg', children: _jsx(SecondBellIcon, { size: 32 }) }), _jsx("h2", { className: "text-white/60 text-2xl font-semibold uppercase tracking-[0.12em] font-['Encode_Sans_Condensed']", children: "LIVE UPDATES" })] }), _jsx(ScrollingTimeline, { updates: event.updates, language: language }), _jsx("div", { className: 'absolute bottom-8 right-8 z-30', children: _jsx("div", { className: 'p-2 rounded-sm backdrop-blur-md shadow-2xl', style: { backgroundColor: 'rgba(0,0,0,0.4)' }, children: _jsx("img", { src: qrCodeUrl, alt: 'QR code', className: 'block w-24 h-24' }) }) })] })] })] }));
83
+ }
84
+ function SecondBellIcon({ size }) {
85
+ return (_jsxs("svg", { width: size, height: size, viewBox: '0 0 24 24', fill: 'none', stroke: 'currentColor', strokeWidth: '2', strokeLinecap: 'round', strokeLinejoin: 'round', children: [_jsx("path", { d: 'M10.268 21a2 2 0 0 0 3.464 0' }), _jsx("path", { d: 'M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326' }), _jsx("path", { d: 'M4 2C2.8 3.7 2 5.7 2 8' }), _jsx("path", { d: 'M22 8a10 10 0 0 0-2-6' })] }));
86
+ }