21254 youtube media player
This commit is contained in:
@@ -0,0 +1,646 @@
|
|||||||
|
// // import { setEventDetails } from "../../store/searchOutput/action";
|
||||||
|
// // import { setCurrentPage } from "../../store/currentView/action";
|
||||||
|
// // import { connect } from "react-redux";
|
||||||
|
// // import useTranslation from "next-translate/useTranslation";
|
||||||
|
// // import { formatDates } from "../utils";
|
||||||
|
|
||||||
|
// // const MediaDetails = (props) => {
|
||||||
|
// // let { t } = useTranslation();
|
||||||
|
|
||||||
|
// // const MediaView = (mediaArr) => {
|
||||||
|
// // mediaArr = mediaArr.value;
|
||||||
|
|
||||||
|
// // return mediaArr.map((item, key) => {
|
||||||
|
// // <div>{item.pinswg_name}</div>;
|
||||||
|
// // });
|
||||||
|
// // };
|
||||||
|
|
||||||
|
// // const mediaArr = props.mediaObj?.value || [];
|
||||||
|
|
||||||
|
// // // event publishiing flag set to true displa
|
||||||
|
|
||||||
|
// // function hasOwnPropertyAndNotNull(obj, property) {
|
||||||
|
// // return obj.hasOwnProperty(property) && obj[property] !== null;
|
||||||
|
// // }
|
||||||
|
|
||||||
|
// // //console.log(eventsArr.length > 0);
|
||||||
|
|
||||||
|
// // return (
|
||||||
|
// // mediaArr.length > 0 && (
|
||||||
|
// // <div className="govuk-grid-row">
|
||||||
|
// // <div className="card">
|
||||||
|
// // <div className="card-body">
|
||||||
|
// // <h2 className="govuk-heading-m ">
|
||||||
|
// // {t("case:event-title")}Media
|
||||||
|
// // </h2>
|
||||||
|
// // {mediaArr.map(
|
||||||
|
// // (item, key) =>
|
||||||
|
// // item.pinswg_publishtoweb && (
|
||||||
|
// // <>
|
||||||
|
// // {" "}
|
||||||
|
// // <div className="">
|
||||||
|
// // <div className="">
|
||||||
|
// // <div>
|
||||||
|
// // <b>
|
||||||
|
// // {" "}
|
||||||
|
// // {t(
|
||||||
|
// // "case:event-name-label",
|
||||||
|
// // )}
|
||||||
|
// // </b>
|
||||||
|
// // : {item.pinswg_name}
|
||||||
|
// // </div>
|
||||||
|
// // {hasOwnPropertyAndNotNull(
|
||||||
|
// // item,
|
||||||
|
// // "pinswg_description",
|
||||||
|
// // ) && (
|
||||||
|
// // <div>
|
||||||
|
// // <b>
|
||||||
|
// // {t(
|
||||||
|
// // "case:event-event-name-label",
|
||||||
|
// // )}
|
||||||
|
// // </b>
|
||||||
|
// // :{" "}
|
||||||
|
// // {
|
||||||
|
// // item.pinswg_description
|
||||||
|
// // }
|
||||||
|
// // </div>
|
||||||
|
// // )}
|
||||||
|
// // </div>
|
||||||
|
// // </div>
|
||||||
|
// // </>
|
||||||
|
// // ),
|
||||||
|
// // )}{" "}
|
||||||
|
// // </div>
|
||||||
|
// // </div>
|
||||||
|
// // </div>
|
||||||
|
// // )
|
||||||
|
// // );
|
||||||
|
// // };
|
||||||
|
|
||||||
|
// // const mapStateToProps = (state) => {
|
||||||
|
// // return {
|
||||||
|
// // ...state,
|
||||||
|
// // };
|
||||||
|
// // };
|
||||||
|
|
||||||
|
// // const mapDispatchToProps = (dispatch) => {
|
||||||
|
// // return {
|
||||||
|
// // setEventDetails: (mediaObj) => {
|
||||||
|
// // dispatch(setEventDetails(mediaObj));
|
||||||
|
// // },
|
||||||
|
// // setCurrentPage: (currentPage) => {
|
||||||
|
// // dispatch(setCurrentPage(currentPage));
|
||||||
|
// // },
|
||||||
|
// // };
|
||||||
|
// // };
|
||||||
|
|
||||||
|
// // export default connect(mapStateToProps, mapDispatchToProps)(MediaDetails);
|
||||||
|
// import { useEffect, useMemo, useState } from "react";
|
||||||
|
// import { connect } from "react-redux";
|
||||||
|
// import useTranslation from "next-translate/useTranslation";
|
||||||
|
// import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
// const extractYouTubeId = (input) => {
|
||||||
|
// if (!input) return null;
|
||||||
|
// if (/^[a-zA-Z0-9_-]{11}$/.test(input)) return input;
|
||||||
|
|
||||||
|
// try {
|
||||||
|
// const url = new URL(input);
|
||||||
|
// if (url.hostname.includes("youtu.be"))
|
||||||
|
// return url.pathname.slice(1) || null;
|
||||||
|
|
||||||
|
// const v = url.searchParams.get("v");
|
||||||
|
// if (v) return v;
|
||||||
|
|
||||||
|
// const embedMatch = url.pathname.match(/\/embed\/([^/]+)/);
|
||||||
|
// if (embedMatch?.[1]) return embedMatch[1];
|
||||||
|
|
||||||
|
// const shortsMatch = url.pathname.match(/\/shorts\/([^/]+)/);
|
||||||
|
// if (shortsMatch?.[1]) return shortsMatch[1];
|
||||||
|
|
||||||
|
// return null;
|
||||||
|
// } catch {
|
||||||
|
// return null;
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// export default function MediaDetails({ mediaObj, whichTab }) {
|
||||||
|
// const router = useRouter();
|
||||||
|
// const mediaArr = mediaObj?.value || [];
|
||||||
|
|
||||||
|
// const videoItems = useMemo(() => {
|
||||||
|
// const isWelsh = router.locale === "cy";
|
||||||
|
|
||||||
|
// return (mediaArr || [])
|
||||||
|
// .filter((x) => x?.pinswg_publishtoweb)
|
||||||
|
// .map((item) => {
|
||||||
|
// const url = isWelsh
|
||||||
|
// ? item.pinswg_mediaLinkCY
|
||||||
|
// : item.pinswg_mediaLinkEN;
|
||||||
|
// const videoId = extractYouTubeId(url);
|
||||||
|
// if (!videoId) return null;
|
||||||
|
|
||||||
|
// return {
|
||||||
|
// key:
|
||||||
|
// item.pinswg_documentid ||
|
||||||
|
// `${item.pinswg_name}-${videoId}`,
|
||||||
|
// videoId,
|
||||||
|
// name: item.pinswg_name || "Video",
|
||||||
|
// description: item.pinswg_description || "",
|
||||||
|
// thumbnail: `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`,
|
||||||
|
// };
|
||||||
|
// })
|
||||||
|
// .filter(Boolean);
|
||||||
|
// }, [mediaArr, router.locale]);
|
||||||
|
|
||||||
|
// const [watchedIds, setWatchedIds] = useState(new Set());
|
||||||
|
// const [selectedIndex, setSelectedIndex] = useState(0);
|
||||||
|
|
||||||
|
// // Forces a fresh iframe instance whenever we need to hard-stop playback
|
||||||
|
// const [playerInstance, setPlayerInstance] = useState(0);
|
||||||
|
|
||||||
|
// const isActive = whichTab === "case-media";
|
||||||
|
// const selected = videoItems[selectedIndex] || null;
|
||||||
|
|
||||||
|
// // When leaving the media tab, hard-stop playback by unmounting (and bumping instance)
|
||||||
|
// useEffect(() => {
|
||||||
|
// if (!isActive) {
|
||||||
|
// setPlayerInstance((n) => n + 1);
|
||||||
|
// }
|
||||||
|
// }, [isActive]);
|
||||||
|
|
||||||
|
// // When list/locale changes, reset to first (no autoplay)
|
||||||
|
// useEffect(() => {
|
||||||
|
// setSelectedIndex(0);
|
||||||
|
// setPlayerInstance((n) => n + 1);
|
||||||
|
// }, [videoItems.length, router.locale]);
|
||||||
|
|
||||||
|
// if (!videoItems.length) return null;
|
||||||
|
|
||||||
|
// const embedBase = "https://www.youtube-nocookie.com/embed";
|
||||||
|
|
||||||
|
// // IMPORTANT:
|
||||||
|
// // - autoplay=0 for initial load
|
||||||
|
// // - autoplay=1 only when user clicks an item
|
||||||
|
// const [autoplay, setAutoplay] = useState(false);
|
||||||
|
|
||||||
|
// useEffect(() => {
|
||||||
|
// // whenever we leave tab or reset player, don't autoplay next time
|
||||||
|
// if (!isActive) setAutoplay(false);
|
||||||
|
// }, [isActive]);
|
||||||
|
|
||||||
|
// const embedSrc = selected
|
||||||
|
// ? `${embedBase}/${selected.videoId}?rel=0&modestbranding=1&playsinline=1&autoplay=${autoplay ? "1" : "0"}`
|
||||||
|
// : "";
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <div className="govuk-grid-row">
|
||||||
|
// <div className="card">
|
||||||
|
// <div className="card-body">
|
||||||
|
// <h2 className="govuk-heading-m">Media</h2>
|
||||||
|
|
||||||
|
// {/* Player: only render when tab is active */}
|
||||||
|
// {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>
|
||||||
|
// )}
|
||||||
|
|
||||||
|
// <h3 className="govuk-heading-s">Videos</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" : ""}`}
|
||||||
|
// onClick={() => {
|
||||||
|
// setSelectedIndex(idx);
|
||||||
|
// setAutoplay(true); // play only on user action
|
||||||
|
// setPlayerInstance((n) => n + 1); // ensure clean switch / stop previous
|
||||||
|
// setWatchedIds((prev) =>
|
||||||
|
// new Set(prev).add(v.videoId),
|
||||||
|
// );
|
||||||
|
// }}
|
||||||
|
// >
|
||||||
|
// <div className="videoTabContent">
|
||||||
|
// <div className="thumbnailWrapper">
|
||||||
|
// <img
|
||||||
|
// src={v.thumbnail}
|
||||||
|
// alt=""
|
||||||
|
// className="videoThumbnail"
|
||||||
|
// loading="lazy"
|
||||||
|
// />
|
||||||
|
// <span
|
||||||
|
// className="playIcon"
|
||||||
|
// aria-hidden="true"
|
||||||
|
// >
|
||||||
|
// ▶
|
||||||
|
// </span>
|
||||||
|
|
||||||
|
// {/* Duration badge (see section 2) */}
|
||||||
|
// {v.duration && (
|
||||||
|
// <span className="durationBadge">
|
||||||
|
// {v.duration}
|
||||||
|
// </span>
|
||||||
|
// )}
|
||||||
|
|
||||||
|
// {/* Watched indicator (see section 3) */}
|
||||||
|
// {watchedIds.has(v.videoId) && (
|
||||||
|
// <span className="watchedBadge">
|
||||||
|
// Watched
|
||||||
|
// </span>
|
||||||
|
// )}
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// <span className="videoTitle">
|
||||||
|
// {v.name}
|
||||||
|
// </span>
|
||||||
|
// </div>
|
||||||
|
// </button>
|
||||||
|
// </li>
|
||||||
|
// );
|
||||||
|
// })}
|
||||||
|
// </ul>
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// <style jsx>{`
|
||||||
|
// .videoWrapper {
|
||||||
|
// position: relative;
|
||||||
|
// padding-bottom: 56.25%;
|
||||||
|
// height: 0;
|
||||||
|
// overflow: hidden;
|
||||||
|
// border: 1px solid #b1b4b6;
|
||||||
|
// border-radius: 4px;
|
||||||
|
// }
|
||||||
|
// .videoWrapper iframe {
|
||||||
|
// position: absolute;
|
||||||
|
// top: 0;
|
||||||
|
// left: 0;
|
||||||
|
// width: 100%;
|
||||||
|
// height: 100%;
|
||||||
|
// }
|
||||||
|
// .videoTabButton {
|
||||||
|
// width: 100%;
|
||||||
|
// text-align: left;
|
||||||
|
// background: transparent;
|
||||||
|
// border: 1px solid #b1b4b6;
|
||||||
|
// border-radius: 4px;
|
||||||
|
// padding: 10px 12px;
|
||||||
|
// cursor: pointer;
|
||||||
|
// }
|
||||||
|
// .videoTabButton:hover {
|
||||||
|
// background: #f3f2f1;
|
||||||
|
// }
|
||||||
|
// .videoTabButton.active {
|
||||||
|
// border-color: #1d70b8;
|
||||||
|
// box-shadow: inset 0 0 0 1px #1d70b8;
|
||||||
|
// background: #eef4ff;
|
||||||
|
// }
|
||||||
|
// .videoTabContent {
|
||||||
|
// display: flex;
|
||||||
|
// gap: 12px;
|
||||||
|
// align-items: center;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .videoThumbnail {
|
||||||
|
// width: 120px;
|
||||||
|
// aspect-ratio: 16 / 9;
|
||||||
|
// object-fit: cover;
|
||||||
|
// border-radius: 4px;
|
||||||
|
// border: 1px solid #b1b4b6;
|
||||||
|
// background: #000;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .videoTitle {
|
||||||
|
// font-weight: 600;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// @media (max-width: 40.0625em) {
|
||||||
|
// .videoThumbnail {
|
||||||
|
// width: 90px;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// .thumbnailWrapper {
|
||||||
|
// position: relative;
|
||||||
|
// width: 120px;
|
||||||
|
// flex-shrink: 0;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .videoThumbnail {
|
||||||
|
// width: 100%;
|
||||||
|
// aspect-ratio: 16 / 9;
|
||||||
|
// object-fit: cover;
|
||||||
|
// border-radius: 4px;
|
||||||
|
// border: 1px solid #b1b4b6;
|
||||||
|
// background: #000;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /* ▶ Play icon */
|
||||||
|
// .playIcon {
|
||||||
|
// position: absolute;
|
||||||
|
// inset: 0;
|
||||||
|
// display: flex;
|
||||||
|
// align-items: center;
|
||||||
|
// justify-content: center;
|
||||||
|
// font-size: 32px;
|
||||||
|
// color: white;
|
||||||
|
// text-shadow: 0 0 6px rgba(0, 0, 0, 0.7);
|
||||||
|
// pointer-events: none;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /* ⏱ Duration badge */
|
||||||
|
// .durationBadge {
|
||||||
|
// position: absolute;
|
||||||
|
// bottom: 6px;
|
||||||
|
// right: 6px;
|
||||||
|
// background: rgba(0, 0, 0, 0.8);
|
||||||
|
// color: white;
|
||||||
|
// font-size: 12px;
|
||||||
|
// padding: 2px 6px;
|
||||||
|
// border-radius: 3px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// /* 👁 Watched badge */
|
||||||
|
// .watchedBadge {
|
||||||
|
// position: absolute;
|
||||||
|
// top: 6px;
|
||||||
|
// left: 6px;
|
||||||
|
// background: #00703c; /* GOV green */
|
||||||
|
// color: white;
|
||||||
|
// font-size: 11px;
|
||||||
|
// padding: 2px 6px;
|
||||||
|
// border-radius: 3px;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .videoTabContent {
|
||||||
|
// display: flex;
|
||||||
|
// gap: 12px;
|
||||||
|
// align-items: center;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .videoTitle {
|
||||||
|
// font-weight: 600;
|
||||||
|
// }
|
||||||
|
// `}</style>
|
||||||
|
// </div>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
import { useEffect, useMemo, useState } from "react";
|
||||||
|
import useTranslation from "next-translate/useTranslation";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 mediaArr = 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 (mediaArr || [])
|
||||||
|
.filter((x) => x?.pinswg_publishtoweb)
|
||||||
|
.map((item) => {
|
||||||
|
const link = isWelsh
|
||||||
|
? item.pinswg_mediaLinkCY
|
||||||
|
: item.pinswg_mediaLinkEN;
|
||||||
|
|
||||||
|
const videoId = extractYouTubeId(link);
|
||||||
|
if (!videoId) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
key:
|
||||||
|
item.pinswg_documentid ||
|
||||||
|
item.pinswg_mediaid ||
|
||||||
|
`${item.pinswg_name}-${videoId}`,
|
||||||
|
videoId,
|
||||||
|
name: item.pinswg_name || "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);
|
||||||
|
}, [mediaArr, 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-name-label")}</b>:{" "}
|
||||||
|
{selected.name}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!!selected.description && (
|
||||||
|
<div className="govuk-body">
|
||||||
|
<b>{t("case:event-event-name-label")}</b>:{" "}
|
||||||
|
{selected.description}
|
||||||
|
</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;
|
||||||
+6
-6
@@ -9,12 +9,12 @@ export function middleware(request) {
|
|||||||
process.env.NODE_ENV === "production" ? "" : `'unsafe-eval'`
|
process.env.NODE_ENV === "production" ? "" : `'unsafe-eval'`
|
||||||
};
|
};
|
||||||
style-src 'self' 'unsafe-eval' 'unsafe-inline';
|
style-src 'self' 'unsafe-eval' 'unsafe-inline';
|
||||||
img-src 'self' 'unsafe-inline' https://www.gov.wales https://gov.wales https://fonts.gstatic.com https://www.googletagmanager.com blob: data:;
|
img-src 'self' 'unsafe-inline' https://www.gov.wales https://gov.wales https://fonts.gstatic.com https://www.googletagmanager.com blob: data: https://img.youtube.com;
|
||||||
connect-src 'self' http://127.0.0.1 https://uksouth-1.in.applicationinsights.azure.com https://ukwest-0.in.applicationinsights.azure.com https://js.monitor.azure.com https://region1.google-analytics.com;
|
connect-src 'self' http://127.0.0.1 https://uksouth-1.in.applicationinsights.azure.com https://ukwest-0.in.applicationinsights.azure.com https://js.monitor.azure.com https://region1.google-analytics.com;
|
||||||
font-src 'self' https://pro.fontawesome.com/ https://fonts.gstatic.com/ ;
|
font-src 'self' https://pro.fontawesome.com/ https://fonts.gstatic.com/ ;
|
||||||
object-src 'none';
|
object-src 'none';
|
||||||
base-uri 'self';
|
base-uri 'self';
|
||||||
frame-src https://datamap.gov.wales/;
|
frame-src https://datamap.gov.wales/ https://www.youtube.com https://www.youtube-nocookie.com;
|
||||||
form-action 'self' ` +
|
form-action 'self' ` +
|
||||||
process.env.NEXTAUTH_URL +
|
process.env.NEXTAUTH_URL +
|
||||||
`;
|
`;
|
||||||
@@ -49,7 +49,7 @@ export function middleware(request) {
|
|||||||
// CSP policy
|
// CSP policy
|
||||||
requestHeaders.set(
|
requestHeaders.set(
|
||||||
"Content-Security-Policy",
|
"Content-Security-Policy",
|
||||||
contentSecurityPolicyHeaderValue
|
contentSecurityPolicyHeaderValue,
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = NextResponse.next({
|
const response = NextResponse.next({
|
||||||
@@ -59,7 +59,7 @@ export function middleware(request) {
|
|||||||
});
|
});
|
||||||
response.headers.set(
|
response.headers.set(
|
||||||
"Content-Security-Policy",
|
"Content-Security-Policy",
|
||||||
contentSecurityPolicyHeaderValue
|
contentSecurityPolicyHeaderValue,
|
||||||
);
|
);
|
||||||
|
|
||||||
// XSS protection (legacy)
|
// XSS protection (legacy)
|
||||||
@@ -77,13 +77,13 @@ export function middleware(request) {
|
|||||||
// Permissions lockdown
|
// Permissions lockdown
|
||||||
response.headers.set(
|
response.headers.set(
|
||||||
"Permissions-Policy",
|
"Permissions-Policy",
|
||||||
"geolocation=(), camera=(), microphone=(), fullscreen=(self)"
|
"geolocation=(), camera=(), microphone=(), fullscreen=(self)",
|
||||||
);
|
);
|
||||||
|
|
||||||
// Enforce HTTPS via HSTS
|
// Enforce HTTPS via HSTS
|
||||||
response.headers.set(
|
response.headers.set(
|
||||||
"Strict-Transport-Security",
|
"Strict-Transport-Security",
|
||||||
"max-age=63072000; includeSubDomains; preload"
|
"max-age=63072000; includeSubDomains; preload",
|
||||||
);
|
);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
getPersonalAccount,
|
getPersonalAccount,
|
||||||
getPortalLogin,
|
getPortalLogin,
|
||||||
getSIPSEvents,
|
getSIPSEvents,
|
||||||
|
getSIPSMedia,
|
||||||
} from "../../actions";
|
} from "../../actions";
|
||||||
import Breadcrumbs from "../../components/breadcrumbs";
|
import Breadcrumbs from "../../components/breadcrumbs";
|
||||||
import Case from "../../components/case";
|
import Case from "../../components/case";
|
||||||
@@ -24,6 +25,7 @@ import {
|
|||||||
setEventDetails,
|
setEventDetails,
|
||||||
setSearchDetails,
|
setSearchDetails,
|
||||||
setSearchResults,
|
setSearchResults,
|
||||||
|
setMediaDetails,
|
||||||
} from "../../store/searchOutput/action";
|
} from "../../store/searchOutput/action";
|
||||||
import { wrapper } from "../../store/store";
|
import { wrapper } from "../../store/store";
|
||||||
import ServiceBanner from "../../components/servicebanner";
|
import ServiceBanner from "../../components/servicebanner";
|
||||||
@@ -104,11 +106,12 @@ const CaseHome = (props) => {
|
|||||||
representationsObj={
|
representationsObj={
|
||||||
props.searchResultsObj.representationsObj
|
props.searchResultsObj.representationsObj
|
||||||
}
|
}
|
||||||
showLoginCheck={true}
|
showLoginCheck={props.showLoginCheck}
|
||||||
messagesObj={props.messagesObj}
|
messagesObj={props.messagesObj}
|
||||||
showFilteredDocs={props.showFilteredDocs}
|
showFilteredDocs={props.showFilteredDocs}
|
||||||
showMaps={props.showMaps}
|
showMaps={props.showMaps}
|
||||||
eventsObj={props.searchResultsObj.eventsObj}
|
eventsObj={props.searchResultsObj.eventsObj}
|
||||||
|
mediaObj={props.searchResultsObj.mediaObj}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Footer
|
<Footer
|
||||||
@@ -159,14 +162,22 @@ export const getServerSideProps = wrapper.getServerSideProps(
|
|||||||
const searchDetailsObj = await getSearchDetails(searchResultsObj);
|
const searchDetailsObj = await getSearchDetails(searchResultsObj);
|
||||||
|
|
||||||
var eventsObj = {};
|
var eventsObj = {};
|
||||||
|
var mediaObj = {};
|
||||||
|
|
||||||
if (searchResultsObj.value[0].pinswg_appealcasetype == 846040002) {
|
if (searchResultsObj.value[0].pinswg_appealcasetype == 846040002) {
|
||||||
eventsObj = await getSIPSEvents(
|
eventsObj = await getSIPSEvents(
|
||||||
searchDetailsObj[0].value[0].pinswg_sipsid
|
searchDetailsObj[0].value[0].pinswg_sipsid,
|
||||||
);
|
);
|
||||||
store.dispatch(setEventDetails(eventsObj));
|
store.dispatch(setEventDetails(eventsObj));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (searchResultsObj.value[0].pinswg_appealcasetype == 846040002) {
|
||||||
|
mediaObj = await getSIPSMedia(
|
||||||
|
searchDetailsObj[0].value[0].pinswg_sipsid,
|
||||||
|
);
|
||||||
|
store.dispatch(setMediaDetails(mediaObj));
|
||||||
|
}
|
||||||
|
|
||||||
//console.log(
|
//console.log(
|
||||||
// "\n======================================\n",
|
// "\n======================================\n",
|
||||||
// searchResultsObj,
|
// searchResultsObj,
|
||||||
@@ -182,7 +193,7 @@ export const getServerSideProps = wrapper.getServerSideProps(
|
|||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
searchResultsObj["@odata.count"] > 1 ||
|
searchResultsObj["@odata.count"] > 1 ||
|
||||||
searchResultsObj["@odata.count"] < 1
|
searchResultsObj["@odata.count"] < 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (searchResultsObj["@odata.count"] < 1) {
|
if (searchResultsObj["@odata.count"] < 1) {
|
||||||
@@ -223,7 +234,7 @@ export const getServerSideProps = wrapper.getServerSideProps(
|
|||||||
searchDetailsObj[0].value[0]
|
searchDetailsObj[0].value[0]
|
||||||
.pinswg_specialistcaseprocess
|
.pinswg_specialistcaseprocess
|
||||||
: "",
|
: "",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,17 +245,18 @@ export const getServerSideProps = wrapper.getServerSideProps(
|
|||||||
searchResultsObj: searchResultsObj,
|
searchResultsObj: searchResultsObj,
|
||||||
searchDetailsObj: searchDetailsObj,
|
searchDetailsObj: searchDetailsObj,
|
||||||
eventsObj: eventsObj,
|
eventsObj: eventsObj,
|
||||||
|
mediaObj: mediaObj,
|
||||||
},
|
},
|
||||||
currentType: "directResultsObj",
|
currentType: "directResultsObj",
|
||||||
docsOffline: process.env.DOCAPI_OFFLINE || false,
|
docsOffline: process.env.DOCAPI_OFFLINE || false,
|
||||||
messagesObj: await getCaseMessage(
|
messagesObj: await getCaseMessage(
|
||||||
searchResultsObj.value[0].incidentid
|
searchResultsObj.value[0].incidentid,
|
||||||
),
|
),
|
||||||
showFilteredDocs: process.env.SHOWFILTERDOCS || false,
|
showFilteredDocs: process.env.SHOWFILTERDOCS || false,
|
||||||
showMaps: process.env.SHOWMAPS,
|
showMaps: process.env.SHOWMAPS,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const mapStateToProps = (state) => {
|
const mapStateToProps = (state) => {
|
||||||
|
|||||||
@@ -3165,3 +3165,107 @@ ul.subFieldList {
|
|||||||
box-decoration-break: clone;
|
box-decoration-break: clone;
|
||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#case-media {
|
||||||
|
.videoWrapper {
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 56.25%;
|
||||||
|
height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid #b1b4b6;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoWrapper iframe {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoTabButton {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #b1b4b6;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoTabButton:hover {
|
||||||
|
background: #f3f2f1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoTabButton.active {
|
||||||
|
border-color: #1d70b8;
|
||||||
|
box-shadow: inset 0 0 0 1px #1d70b8;
|
||||||
|
background: #eef4ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoTabContent {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.thumbnailWrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 120px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoThumbnail {
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 16 / 9;
|
||||||
|
object-fit: cover;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid #b1b4b6;
|
||||||
|
background: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playIcon {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 32px;
|
||||||
|
color: white;
|
||||||
|
text-shadow: 0 0 6px rgba(0, 0, 0, 0.7);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.durationBadge {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 6px;
|
||||||
|
right: 6px;
|
||||||
|
background: rgba(0, 0, 0, 0.8);
|
||||||
|
color: white;
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.watchedBadge {
|
||||||
|
position: absolute;
|
||||||
|
top: 6px;
|
||||||
|
left: 6px;
|
||||||
|
background: #00703c;
|
||||||
|
color: white;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoTitle {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 40.0625em) {
|
||||||
|
.thumbnailWrapper {
|
||||||
|
width: 90px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user