@fifthbell/brokaw 0.1.48 → 0.1.50
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.
- package/dist/components/live-program/LiveProgram.d.ts +2 -1
- package/dist/components/live-program/LiveProgram.js +257 -32
- package/dist/components/live-program/assets.d.ts +8 -8
- package/dist/components/live-program/assets.js +8 -8
- package/dist/components/live-program/components/slides/ArticleSlide.js +12 -2
- package/dist/components/live-program/components/slides/LiveEventSlide.d.ts +9 -0
- package/dist/components/live-program/components/slides/LiveEventSlide.js +86 -0
- package/dist/components/live-program/hooks/useSSE.js +72 -32
- package/dist/components/live-program/i18n.js +15 -0
- package/dist/components/live-program/segments/LiveEventSegment.d.ts +4 -0
- package/dist/components/live-program/segments/LiveEventSegment.js +27 -0
- package/dist/components/live-program/segments/MarketsSegment.js +3 -14
- package/dist/components/live-program/segments/WeatherSegment.js +11 -76
- package/dist/components/live-program/segments/fetchLiveEvents.d.ts +20 -0
- package/dist/components/live-program/segments/fetchLiveEvents.js +76 -0
- package/dist/components/live-program/segments/index.d.ts +2 -0
- package/dist/components/live-program/segments/index.js +2 -0
- package/dist/live-program/main.d.ts +1 -0
- package/dist/live-program/main.js +10 -0
- package/dist/live-program-page/fifthbell/audio/pipes.ogg +0 -0
- package/dist/live-program-page/fifthbell/images/berlin.jpg +0 -0
- package/dist/live-program-page/fifthbell/images/fifthbell.png +0 -0
- package/dist/live-program-page/fifthbell/images/nyc.jpg +0 -0
- package/dist/live-program-page/fifthbell/images/nyse.jpg +0 -0
- package/dist/live-program-page/fifthbell/images/santiago.jpg +0 -0
- package/dist/live-program-page/fifthbell/images/seismograph.jpg +0 -0
- package/dist/live-program-page/fifthbell/images/tokyo.jpg +0 -0
- package/dist/live-program-page/index.html +16 -0
- package/dist/live-program-page/live-program.css +3 -0
- package/dist/live-program-page/live-program.js +76 -0
- package/dist/renderer.d.ts +2 -1
- package/dist/renderer.js +1 -1
- package/dist/renderer.node.d.ts +8 -0
- package/dist/renderer.node.js +61 -0
- package/dist/utils/sofascore.d.ts +1 -1
- package/dist/utils/sofascore.js +2 -2
- package/package.json +6 -2
- package/src/styles/compiled.css +1 -1
- package/src/templates/partials/headers/header-main.hbs +7 -4
- package/src/templates/partials/headers/header-minimal.hbs +1 -1
- 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({
|
|
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({
|
|
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(
|
|
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,
|
|
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:
|
|
361
|
+
url: `${resolvedApiBaseUrl}/events`,
|
|
321
362
|
enabled: !controlledBySceneRenderer,
|
|
322
363
|
onMessage: (data) => {
|
|
323
|
-
if (
|
|
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: "
|
|
3
|
+
readonly pipes: "./fifthbell/audio/pipes.ogg";
|
|
4
4
|
};
|
|
5
5
|
readonly images: {
|
|
6
|
-
readonly logo: "
|
|
7
|
-
readonly nyc: "
|
|
8
|
-
readonly berlin: "
|
|
9
|
-
readonly santiago: "
|
|
10
|
-
readonly tokyo: "
|
|
11
|
-
readonly nyse: "
|
|
12
|
-
readonly seismograph: "
|
|
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: '
|
|
3
|
+
pipes: './fifthbell/audio/pipes.ogg'
|
|
4
4
|
},
|
|
5
5
|
images: {
|
|
6
|
-
logo: '
|
|
7
|
-
nyc: '
|
|
8
|
-
berlin: '
|
|
9
|
-
santiago: '
|
|
10
|
-
tokyo: '
|
|
11
|
-
nyse: '
|
|
12
|
-
seismograph: '
|
|
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
|
-
|
|
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: [
|
|
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
|
+
}
|