21254 youtube media player

This commit is contained in:
2026-02-04 11:22:11 +00:00
parent ec067e62a5
commit 53540911cc
4 changed files with 774 additions and 12 deletions
+646
View File
@@ -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
View File
@@ -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;
+18 -6
View File
@@ -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) => {
+104
View File
@@ -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;
}
}
}