Files
QMS/fe/web/teller-app/src/pages/BranchDisplayPage/AdCarousel.jsx
T

167 lines
5.6 KiB
React

import React, { useCallback, useEffect, useRef, useState } from 'react';
import { fetchData } from '../../fetching/Fetch.js';
import { SERVER_URL } from '../../constants.js';
function resolveMediaUrl(mediaUrl) {
if (!mediaUrl) return null;
return mediaUrl.startsWith('http') ? mediaUrl : `${SERVER_URL}${mediaUrl}`;
}
export default function AdCarousel({ tenantCode }) {
const [ads, setAds] = useState([]);
const [index, setIndex] = useState(0);
const [error, setError] = useState(null);
const timerRef = useRef(null);
const videoRef = useRef(null);
const clearTimer = useCallback(() => {
if (timerRef.current) {
clearTimeout(timerRef.current);
timerRef.current = null;
}
}, []);
const loadAds = useCallback(async () => {
if (!tenantCode) return;
try {
const response = await fetchData(
`${SERVER_URL}/api/v1/ads/${encodeURIComponent(tenantCode)}/active`,
'GET'
);
if (!response.success) {
setError('Could not load advertisements.');
return;
}
const next = Array.isArray(response.data) ? response.data : [];
setAds(next);
setError(null);
setIndex((current) => (next.length === 0 ? 0 : current % next.length));
} catch (err) {
console.error(err);
setError('Could not load advertisements.');
}
}, [tenantCode]);
useEffect(() => {
loadAds();
const interval = setInterval(loadAds, 60 * 1000);
return () => clearInterval(interval);
}, [loadAds]);
const goNext = useCallback(() => {
setIndex((current) => {
if (ads.length === 0) return 0;
return (current + 1) % ads.length;
});
}, [ads.length]);
const currentAd = ads[index] ?? null;
useEffect(() => {
clearTimer();
if (!currentAd) return undefined;
if (currentAd.mediaType === 'IMAGE') {
const durationMs = Math.max(1, Number(currentAd.durationSeconds) || 10) * 1000;
timerRef.current = setTimeout(goNext, durationMs);
return clearTimer;
}
// Videos advance on ended; fallback if play fails
timerRef.current = setTimeout(goNext, 5 * 60 * 1000);
return clearTimer;
}, [currentAd, goNext, clearTimer]);
useEffect(() => {
const video = videoRef.current;
if (!video || currentAd?.mediaType !== 'VIDEO') return undefined;
let removeUnmuteListener = () => {};
const onEnded = () => goNext();
const scheduleFallbackAdvance = () => {
clearTimer();
const durationMs = Math.max(5, Number(currentAd.durationSeconds) || 15) * 1000;
timerRef.current = setTimeout(goNext, durationMs);
};
video.addEventListener('ended', onEnded);
video.muted = false;
video.playsInline = true;
const playPromise = video.play();
if (playPromise?.catch) {
playPromise.catch(() => {
// Unmuted autoplay blocked — keep video playing muted, then unmute on gesture.
video.muted = true;
const mutedPlay = video.play();
if (mutedPlay?.catch) {
mutedPlay.catch(scheduleFallbackAdvance);
}
const unmute = () => {
video.muted = false;
video.play()?.catch(() => {});
};
window.addEventListener('pointerdown', unmute, { once: true });
removeUnmuteListener = () => window.removeEventListener('pointerdown', unmute);
});
}
return () => {
video.removeEventListener('ended', onEnded);
removeUnmuteListener();
};
}, [currentAd, goNext, clearTimer]);
if (error) {
return (
<section className="branch-display__ad-panel">
<h2 className="branch-display__section-title">Pengiklanan</h2>
<p className="branch-display__empty">{error}</p>
</section>
);
}
if (ads.length === 0) {
return (
<section className="branch-display__ad-panel">
<h2 className="branch-display__section-title">Pengiklanan</h2>
<p className="branch-display__empty">Tiada pengiklanan aktif.</p>
</section>
);
}
const src = resolveMediaUrl(currentAd.mediaUrl);
return (
<section className="branch-display__ad-panel">
<div className="branch-display__ad-header">
<h2 className="branch-display__section-title">Pengiklanan</h2>
</div>
<div className="branch-display__ad-stage">
{currentAd.mediaType === 'VIDEO' ? (
<video
key={currentAd.id}
ref={videoRef}
className="branch-display__ad-media"
src={src}
playsInline
autoPlay
/>
) : (
<img
key={currentAd.id}
className="branch-display__ad-media"
src={src}
alt={currentAd.title || currentAd.fileName || 'Advertisement'}
/>
)}
</div>
{currentAd.title ? (
<p className="branch-display__ad-caption">{currentAd.title}</p>
) : null}
</section>
);
}