updated labels and added welsh language description Related work items: #24302
258 lines
10 KiB
JavaScript
258 lines
10 KiB
JavaScript
import { useEffect, useMemo, useState } from "react";
|
|
import useTranslation from "next-translate/useTranslation";
|
|
import { useRouter } from "next/router";
|
|
import { getBilingualText } from "./summary/utils/helpers";
|
|
|
|
/**
|
|
* MediaDetails
|
|
* - Renders a YouTube player + a list of videos from mediaObj.value
|
|
* - Loads first item by default (no autoplay)
|
|
* - Plays only when user selects an item
|
|
* - Stops playback when leaving the "case-media" parent tab (pass whichTab)
|
|
* - Shows thumbnail with play overlay
|
|
* - Optional duration badge (if you supply v.duration)
|
|
* - "Watched" badge once a video has been played (session only)
|
|
*/
|
|
|
|
const extractYouTubeId = (input) => {
|
|
if (!input) return null;
|
|
|
|
// Already looks like a YouTube ID?
|
|
if (/^[a-zA-Z0-9_-]{11}$/.test(input)) return input;
|
|
|
|
try {
|
|
const url = new URL(input);
|
|
|
|
// youtu.be/<id>
|
|
if (url.hostname.includes("youtu.be")) {
|
|
return url.pathname.replace("/", "") || null;
|
|
}
|
|
|
|
// youtube.com/watch?v=<id>
|
|
const v = url.searchParams.get("v");
|
|
if (v) return v;
|
|
|
|
// youtube.com/embed/<id>
|
|
const embedMatch = url.pathname.match(/\/embed\/([^/]+)/);
|
|
if (embedMatch?.[1]) return embedMatch[1];
|
|
|
|
// youtube.com/shorts/<id>
|
|
const shortsMatch = url.pathname.match(/\/shorts\/([^/]+)/);
|
|
if (shortsMatch?.[1]) return shortsMatch[1];
|
|
|
|
return null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
};
|
|
|
|
const MediaDetails = ({ mediaObj, whichTab, title, useNoCookie = true }) => {
|
|
const { t } = useTranslation();
|
|
const router = useRouter();
|
|
|
|
const sourceMedia = mediaObj?.value;
|
|
const isActive = whichTab ? whichTab === "case-media" : true;
|
|
|
|
// Build list of playable items (publishable only)
|
|
const videoItems = useMemo(() => {
|
|
const isWelsh = router.locale === "cy";
|
|
|
|
return (sourceMedia || [])
|
|
.filter((x) => x?.pinswg_publishtoweb)
|
|
.map((item) => {
|
|
const link = isWelsh
|
|
? item.pinswg_videoplatformurlwelsh
|
|
: item.pinswg_videoplatformurl;
|
|
|
|
const videoId = extractYouTubeId(link);
|
|
if (!videoId) return null;
|
|
|
|
return {
|
|
key:
|
|
item.pinswg_documentid ||
|
|
item.pinswg_mediaid ||
|
|
`${item.pinswg_eventrecordingid}-${videoId}`,
|
|
videoId,
|
|
name: item.pinswg_eventrecordingname || "Video",
|
|
description: item.pinswg_description || "",
|
|
// If you later store duration in CRM, map it here (e.g. "03:42")
|
|
duration: item.pinswg_duration || null,
|
|
thumbnailHQ: `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`,
|
|
thumbnailMax: `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`,
|
|
raw: item
|
|
};
|
|
})
|
|
.filter(Boolean);
|
|
}, [sourceMedia, router.locale]);
|
|
|
|
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
|
|
// Default false so first item loads but does not play.
|
|
const [autoplay, setAutoplay] = useState(false);
|
|
|
|
// Bump this to force iframe remount (hard stop playback).
|
|
const [playerInstance, setPlayerInstance] = useState(0);
|
|
|
|
// Track which videos have been played (session only).
|
|
const [watchedIds, setWatchedIds] = useState(() => new Set());
|
|
|
|
// When list or locale changes: reset selection and do not autoplay
|
|
useEffect(() => {
|
|
setSelectedIndex(0);
|
|
setAutoplay(false);
|
|
setPlayerInstance((n) => n + 1);
|
|
}, [videoItems.length, router.locale]);
|
|
|
|
// When leaving the media tab: hard stop playback
|
|
useEffect(() => {
|
|
if (!isActive) {
|
|
setAutoplay(false);
|
|
setPlayerInstance((n) => n + 1);
|
|
}
|
|
}, [isActive]);
|
|
|
|
const selected = videoItems[selectedIndex] || null;
|
|
|
|
const domain = useNoCookie
|
|
? "https://www.youtube-nocookie.com"
|
|
: "https://www.youtube.com";
|
|
|
|
const embedSrc = selected
|
|
? `${domain}/embed/${selected.videoId}?rel=0&modestbranding=1&playsinline=1&autoplay=${
|
|
autoplay ? "1" : "0"
|
|
}`
|
|
: "";
|
|
|
|
if (!videoItems.length) return null;
|
|
|
|
return (
|
|
<div className="govuk-grid-row">
|
|
<div className="card">
|
|
<div className="card-body">
|
|
<h2 className="govuk-heading-m">{t("case:media-title")}</h2>
|
|
|
|
{/* Player: unmount when not active (guarantees playback stops) */}
|
|
{isActive && (
|
|
<div className="videoWrapper govuk-!-margin-bottom-4">
|
|
<iframe
|
|
key={`${selected?.videoId}-${playerInstance}`}
|
|
src={embedSrc}
|
|
title={selected?.name || "YouTube video"}
|
|
frameBorder="0"
|
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
|
allowFullScreen
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* Selected info */}
|
|
{selected && (
|
|
<div className="govuk-!-margin-bottom-4">
|
|
<div className="govuk-body">
|
|
<b>{t("case:event-recording-name-label")}</b>:{" "}
|
|
{selected.name}
|
|
</div>
|
|
|
|
{!!selected.description && (
|
|
<div className="govuk-body">
|
|
<b>
|
|
{t(
|
|
"case:event-recording-description-label"
|
|
)}
|
|
</b>
|
|
:{" "}
|
|
{getBilingualText(
|
|
selected.description,
|
|
router.locale
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Video list */}
|
|
<h3 className="govuk-heading-s">
|
|
{t("case:video-heading")}
|
|
</h3>
|
|
|
|
<ul className="govuk-list govuk-list--spaced">
|
|
{videoItems.map((v, idx) => {
|
|
const isSelected = idx === selectedIndex;
|
|
|
|
return (
|
|
<li key={v.key}>
|
|
<button
|
|
type="button"
|
|
className={`videoTabButton ${
|
|
isSelected ? "active" : ""
|
|
}`}
|
|
aria-current={
|
|
isSelected ? "true" : undefined
|
|
}
|
|
onClick={() => {
|
|
setSelectedIndex(idx);
|
|
|
|
// Start playback only when user selects
|
|
setAutoplay(true);
|
|
|
|
// Force remount to stop previous + start clean
|
|
setPlayerInstance((n) => n + 1);
|
|
|
|
// Mark watched
|
|
setWatchedIds((prev) => {
|
|
const next = new Set(prev);
|
|
next.add(v.videoId);
|
|
return next;
|
|
});
|
|
}}
|
|
>
|
|
<div className="videoTabContent">
|
|
<div className="thumbnailWrapper">
|
|
<img
|
|
src={v.thumbnailMax}
|
|
onError={(e) => {
|
|
// fallback for videos without maxres
|
|
e.currentTarget.src =
|
|
v.thumbnailHQ;
|
|
}}
|
|
alt=""
|
|
className="videoThumbnail"
|
|
loading="lazy"
|
|
/>
|
|
<span
|
|
className="playIcon"
|
|
aria-hidden="true"
|
|
>
|
|
▶
|
|
</span>
|
|
|
|
{!!v.duration && (
|
|
<span className="durationBadge">
|
|
{v.duration}
|
|
</span>
|
|
)}
|
|
|
|
{watchedIds.has(v.videoId) && (
|
|
<span className="watchedBadge">
|
|
Watched
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<span className="videoTitle">
|
|
{v.name}
|
|
</span>
|
|
</div>
|
|
</button>
|
|
</li>
|
|
);
|
|
})}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default MediaDetails;
|