'use client'; import React from 'react'; export interface WatchSegment { startSec: number; endSec: number; watchedAt: string; } interface SegmentedProgressBarProps { segments: WatchSegment[]; duration: number; percent: number; className?: string; height?: 'sm' | 'md' | 'lg'; showTooltip?: boolean; interactive?: boolean; onSegmentClick?: (segment: WatchSegment, position: number) => void; } const heightPixels = { sm: '4px', md: '8px', lg: '12px', }; export function SegmentedProgressBar({ segments, duration, percent, className = '', height = 'md', showTooltip = true, interactive = false, onSegmentClick, }: SegmentedProgressBarProps) { const [tooltipPos, setTooltipPos] = React.useState<{ x: number; time: string } | null>(null); const containerRef = React.useRef(null); // Normalize segments: merge overlapping ranges const normalizedSegments = React.useMemo(() => { if (segments.length === 0) return []; const sorted = [...segments].sort((a, b) => a.startSec - b.startSec); const merged: WatchSegment[] = []; for (const seg of sorted) { if (merged.length === 0) { merged.push({ ...seg }); } else { const last = merged[merged.length - 1]; // Check for overlap or adjacency (within 0.5s) if (seg.startSec <= last.endSec + 0.5) { // Merge last.endSec = Math.max(last.endSec, seg.endSec); } else { // Gap, add new segment merged.push({ ...seg }); } } } return merged; }, [segments]); const handleMouseMove = React.useCallback( (e: React.MouseEvent) => { if (!showTooltip || !containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); const x = e.clientX - rect.left; const percentage = Math.max(0, Math.min(1, x / rect.width)); const time = Math.round(percentage * duration); setTooltipPos({ x, time: formatTime(time), }); }, [duration, showTooltip] ); const handleMouseLeave = () => { setTooltipPos(null); }; const handleClick = (e: React.MouseEvent) => { if (!interactive || !onSegmentClick || !containerRef.current) return; const rect = containerRef.current.getBoundingClientRect(); const x = e.clientX - rect.left; const percentage = Math.max(0, Math.min(1, x / rect.width)); const position = Math.round(percentage * duration); // Find which segment was clicked for (const segment of normalizedSegments) { if (position >= segment.startSec && position <= segment.endSec) { onSegmentClick(segment, position); break; } } }; const barHeight = heightPixels[height]; return (
{/* Main progress bar container */}
{/* Watched segments */} {normalizedSegments.map((segment, idx) => { const startPercent = (segment.startSec / duration) * 100; const endPercent = (segment.endSec / duration) * 100; const width = endPercent - startPercent; return (
{ e.currentTarget.style.backgroundColor = '#2563eb'; }} onMouseLeave={(e) => { e.currentTarget.style.backgroundColor = '#3b82f6'; }} /> ); })} {/* Current progress line */} {percent > 0 && (
)}
{/* Tooltip text - only show if showTooltip is true */} {tooltipPos && showTooltip && (
{tooltipPos.time} / {formatTime(duration)}
)}
); } function formatTime(seconds: number): string { const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); const s = Math.floor(seconds % 60); if (h > 0) { return `${h}:${String(m).padStart(2, '0')}:${String(s).padStart(2, '0')}`; } return `${m}:${String(s).padStart(2, '0')}`; }