Fix video player chapters, time format, and access control
## Changes ### Chapter Time Format Support - Added HH:MM:SS format support in addition to MM:SS - Updated time parsing to handle variable-length inputs - Updated time display formatter to show hours when > 0 - Updated input placeholder and validation pattern - Updated help text to mention both formats ### Video Player Improvements - Added fullscreen button to Plyr controls - Added quality/settings control to Plyr controls - Fixed accent color theming with !important CSS rules - Removed hardcoded default accent color (#f97316) - Updated Bootcamp and WebinarRecording pages to use empty string initial state ### Access Control & Security - Added transparent CSS overlay to block YouTube UI interactions - Disabled YouTube native controls (controls=0, disablekb=1, fs=0) - Set iframe pointer-events-none to prevent direct interaction - Prevents members from copying/sharing YouTube URLs directly - Preserves Plyr controls functionality through click handler ### Files Modified - src/components/admin/ChaptersEditor.tsx: Time format HH:MM:SS support - src/components/VideoPlayerWithChapters.tsx: Security overlay & theming fixes - src/pages/Bootcamp.tsx: Accent color initialization fix - src/pages/WebinarRecording.tsx: Accent color initialization fix 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -26,7 +26,7 @@ export const VideoPlayerWithChapters = forwardRef<VideoPlayerRef, VideoPlayerWit
|
||||
videoUrl,
|
||||
embedCode,
|
||||
chapters = [],
|
||||
accentColor = '#f97316', // Default orange
|
||||
accentColor,
|
||||
onChapterChange,
|
||||
onTimeUpdate,
|
||||
className = '',
|
||||
@@ -69,6 +69,7 @@ export const VideoPlayerWithChapters = forwardRef<VideoPlayerRef, VideoPlayerWit
|
||||
showinfo: 0,
|
||||
iv_load_policy: 3,
|
||||
modestbranding: 1,
|
||||
controls: 0,
|
||||
},
|
||||
controls: [
|
||||
'play-large',
|
||||
@@ -81,6 +82,7 @@ export const VideoPlayerWithChapters = forwardRef<VideoPlayerRef, VideoPlayerWit
|
||||
'settings',
|
||||
'pip',
|
||||
'airplay',
|
||||
'fullscreen',
|
||||
],
|
||||
settings: ['quality', 'speed'],
|
||||
keyboard: {
|
||||
@@ -95,16 +97,16 @@ export const VideoPlayerWithChapters = forwardRef<VideoPlayerRef, VideoPlayerWit
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.plyr--full-ui input[type=range] {
|
||||
color: ${accentColor};
|
||||
color: ${accentColor} !important;
|
||||
}
|
||||
.plyr__control--overlaid,
|
||||
.plyr__controls .plyr__control.plyr__tab-focus,
|
||||
.plyr__controls .plyr__control:hover,
|
||||
.plyr__controls .plyr__control[aria-expanded=true] {
|
||||
background: ${accentColor};
|
||||
background: ${accentColor} !important;
|
||||
}
|
||||
.plyr__progress__buffer {
|
||||
color: ${accentColor}40;
|
||||
color: ${accentColor}40 !important;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
@@ -190,14 +192,29 @@ export const VideoPlayerWithChapters = forwardRef<VideoPlayerRef, VideoPlayerWit
|
||||
const youtubeId = effectiveVideoUrl ? getYouTubeId(effectiveVideoUrl) : null;
|
||||
|
||||
return (
|
||||
<div ref={wrapperRef} className={`plyr__video-embed ${className}`}>
|
||||
{youtubeId && (
|
||||
<iframe
|
||||
src={`https://www.youtube.com/embed/${youtubeId}?origin=${window.location.origin}&iv_load_policy=3&modestbranding=1&playsinline=1`}
|
||||
allowFullScreen
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
/>
|
||||
)}
|
||||
<div className={`relative ${className}`}>
|
||||
{/* CSS Overlay to block YouTube UI interactions */}
|
||||
<div
|
||||
className="absolute inset-0 z-10 pointer-events-auto"
|
||||
style={{ background: 'transparent' }}
|
||||
onClick={(e) => {
|
||||
// Allow clicks to pass through to Plyr controls
|
||||
const plyrControls = (e.currentTarget.parentElement as HTMLElement)?.querySelector('.plyr__controls');
|
||||
if (plyrControls && plyrControls.contains(e.target as Node)) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div ref={wrapperRef} className={`plyr__video-embed`}>
|
||||
{youtubeId && (
|
||||
<iframe
|
||||
src={`https://www.youtube-nocookie.com/embed/${youtubeId}?origin=${window.location.origin}&iv_load_policy=3&modestbranding=1&playsinline=1&rel=0&showinfo=0&controls=0&disablekb=1&fs=0`}
|
||||
allowFullScreen
|
||||
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||
className="pointer-events-none"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -23,8 +23,23 @@ export function ChaptersEditor({ chapters, onChange, className = '' }: ChaptersE
|
||||
|
||||
const updateTime = (index: number, value: string) => {
|
||||
const newChapters = [...chaptersList];
|
||||
const [minutes = 0, seconds = 0] = value.split(':').map(Number);
|
||||
newChapters[index].time = minutes * 60 + seconds;
|
||||
const parts = value.split(':').map(Number);
|
||||
|
||||
let totalSeconds = 0;
|
||||
if (parts.length === 3) {
|
||||
// HH:MM:SS format
|
||||
const [hours = 0, minutes = 0, seconds = 0] = parts;
|
||||
totalSeconds = hours * 3600 + minutes * 60 + seconds;
|
||||
} else if (parts.length === 2) {
|
||||
// MM:SS format
|
||||
const [minutes = 0, seconds = 0] = parts;
|
||||
totalSeconds = minutes * 60 + seconds;
|
||||
} else {
|
||||
// Just seconds or invalid
|
||||
totalSeconds = parts[0] || 0;
|
||||
}
|
||||
|
||||
newChapters[index].time = totalSeconds;
|
||||
setChaptersList(newChapters);
|
||||
onChange(newChapters);
|
||||
};
|
||||
@@ -50,8 +65,13 @@ export function ChaptersEditor({ chapters, onChange, className = '' }: ChaptersE
|
||||
};
|
||||
|
||||
const formatTimeForInput = (seconds: number): string => {
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const hours = Math.floor(seconds / 3600);
|
||||
const minutes = Math.floor((seconds % 3600) / 60);
|
||||
const secs = seconds % 60;
|
||||
|
||||
if (hours > 0) {
|
||||
return `${hours}:${minutes.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
|
||||
}
|
||||
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
@@ -84,8 +104,8 @@ export function ChaptersEditor({ chapters, onChange, className = '' }: ChaptersE
|
||||
type="text"
|
||||
value={formatTimeForInput(chapter.time)}
|
||||
onChange={(e) => updateTime(index, e.target.value)}
|
||||
placeholder="0:00"
|
||||
pattern="[0-9]+:[0-5][0-9]"
|
||||
placeholder="0:00 or 1:23:34"
|
||||
pattern="([0-9]+:)?[0-9]+:[0-5][0-9]"
|
||||
className="font-mono"
|
||||
/>
|
||||
</div>
|
||||
@@ -118,7 +138,7 @@ export function ChaptersEditor({ chapters, onChange, className = '' }: ChaptersE
|
||||
))}
|
||||
|
||||
<div className="text-xs text-muted-foreground space-y-1 pt-2 border-t">
|
||||
<p>💡 <strong>Format:</strong> Enter time as MM:SS (e.g., 5:30 for 5 minutes 30 seconds)</p>
|
||||
<p>💡 <strong>Format:</strong> Enter time as MM:SS or HH:MM:SS (e.g., 5:30 or 1:23:34)</p>
|
||||
<p>📌 <strong>Note:</strong> Chapters only work with YouTube videos. Embed codes show static timeline.</p>
|
||||
<p>✨ <strong>Tip:</strong> Chapters are automatically sorted by time when displayed.</p>
|
||||
</div>
|
||||
|
||||
@@ -77,7 +77,7 @@ export default function Bootcamp() {
|
||||
const [userReview, setUserReview] = useState<UserReview | null>(null);
|
||||
const [reviewModalOpen, setReviewModalOpen] = useState(false);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [accentColor, setAccentColor] = useState('#f97316');
|
||||
const [accentColor, setAccentColor] = useState<string>('');
|
||||
const playerRef = useRef<VideoPlayerRef>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function WebinarRecording() {
|
||||
const [product, setProduct] = useState<Product | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [currentTime, setCurrentTime] = useState(0);
|
||||
const [accentColor, setAccentColor] = useState('#f97316');
|
||||
const [accentColor, setAccentColor] = useState<string>('');
|
||||
const playerRef = useRef<VideoPlayerRef>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user