|
|
@@ -1,6 +1,8 @@
|
|
|
'use client';
|
|
|
|
|
|
import { useState, useEffect, useRef } from 'react';
|
|
|
+import { AnnotationCanvas, COLORS, Tool } from '@/components/video-player/AnnotationCanvas';
|
|
|
+import { AnnotationData } from '@/lib/api';
|
|
|
import { useRouter } from 'next/navigation';
|
|
|
import { useAuth } from '@/lib/auth-context';
|
|
|
import { useParams } from 'next/navigation';
|
|
|
@@ -37,6 +39,23 @@ export default function SharePage() {
|
|
|
const [commentSubmitting, setCommentSubmitting] = useState(false);
|
|
|
const [commentError, setCommentError] = useState<string | null>(null);
|
|
|
const [currentTime, setCurrentTime] = useState(0);
|
|
|
+ const [drawMode, setDrawMode] = useState(false);
|
|
|
+ const [drawTool, setDrawTool] = useState<Tool>('arrow');
|
|
|
+ const [drawColor, setDrawColor] = useState('#ef4444');
|
|
|
+ const [pendingStrokes, setPendingStrokes] = useState<AnnotationData[]>([]);
|
|
|
+ const videoContainerRef = useRef<HTMLDivElement>(null);
|
|
|
+ const [videoDims, setVideoDims] = useState({ width: 0, height: 0 });
|
|
|
+
|
|
|
+ // ── Video container resize observer ───────────────────────────────────────
|
|
|
+ useEffect(() => {
|
|
|
+ const el = videoContainerRef.current;
|
|
|
+ if (!el) return;
|
|
|
+ const obs = new ResizeObserver(() => {
|
|
|
+ setVideoDims({ width: el.offsetWidth, height: el.offsetHeight });
|
|
|
+ });
|
|
|
+ obs.observe(el);
|
|
|
+ return () => obs.disconnect();
|
|
|
+ }, []);
|
|
|
|
|
|
// ── Load guest name from localStorage ───────────────────────────────────────
|
|
|
useEffect(() => {
|
|
|
@@ -153,7 +172,9 @@ export default function SharePage() {
|
|
|
guestName: guestName.trim(),
|
|
|
content: commentText.trim(),
|
|
|
timestamp: currentTime > 0 ? currentTime : undefined,
|
|
|
+ annotations: pendingStrokes.length > 0 ? pendingStrokes : undefined,
|
|
|
});
|
|
|
+ setPendingStrokes([]);
|
|
|
setComments(prev => [...prev, data.comment]);
|
|
|
setCommentsTotal(prev => prev + 1);
|
|
|
setCommentText('');
|
|
|
@@ -397,13 +418,60 @@ export default function SharePage() {
|
|
|
<div className="flex-1 flex items-center justify-center" style={{ background: '#000', minHeight: 0 }}>
|
|
|
{streamUrl ? (
|
|
|
<div className="w-full max-w-6xl mx-auto" style={{ aspectRatio: '16/9', maxHeight: '85vh' }}>
|
|
|
- {isHls ? (
|
|
|
- <video ref={videoRef} className="w-full h-full" controls playsInline
|
|
|
- onTimeUpdate={e => setCurrentTime(e.currentTarget.currentTime)} />
|
|
|
- ) : (
|
|
|
- <video ref={videoRef} src={streamUrl} className="w-full h-full" controls playsInline
|
|
|
- onTimeUpdate={e => setCurrentTime(e.currentTarget.currentTime)} />
|
|
|
- )}
|
|
|
+ <div ref={videoContainerRef} className="relative w-full h-full">
|
|
|
+ {isHls ? (
|
|
|
+ <video ref={videoRef} className="w-full h-full" controls playsInline
|
|
|
+ onTimeUpdate={e => setCurrentTime(e.currentTarget.currentTime)} />
|
|
|
+ ) : (
|
|
|
+ <video ref={videoRef} src={streamUrl} className="w-full h-full" controls playsInline
|
|
|
+ onTimeUpdate={e => setCurrentTime(e.currentTarget.currentTime)} />
|
|
|
+ )}
|
|
|
+ {/* Annotation drawing canvas */}
|
|
|
+ <AnnotationCanvas
|
|
|
+ isActive={drawMode}
|
|
|
+ tool={drawTool}
|
|
|
+ color={drawColor}
|
|
|
+ width={videoDims.width}
|
|
|
+ height={videoDims.height}
|
|
|
+ pendingStrokes={pendingStrokes}
|
|
|
+ onStrokeComplete={(stroke) => setPendingStrokes(prev => [...prev, stroke])}
|
|
|
+ />
|
|
|
+ {/* Draw toolbar */}
|
|
|
+ {drawMode && (
|
|
|
+ <div className="absolute top-2 right-2 z-30 flex flex-wrap gap-1.5 p-2 rounded-xl"
|
|
|
+ style={{ background: 'rgba(15,17,28,0.97)', border: '1px solid rgba(167,139,250,0.3)', backdropFilter: 'blur(12px)' }}>
|
|
|
+ {(['pen', 'arrow', 'rect', 'ellipse'] as Tool[]).map(t => (
|
|
|
+ <button key={t} onClick={() => setDrawTool(t)}
|
|
|
+ className={`px-2 py-1 rounded text-xs font-medium transition-colors ${
|
|
|
+ drawTool === t ? 'bg-indigo-600 text-white' : 'bg-white/10 text-white/80 hover:bg-white/20'
|
|
|
+ }`}>{t.charAt(0).toUpperCase() + t.slice(1)}</button>
|
|
|
+ ))}
|
|
|
+ <div className="w-px h-5 bg-white/10" />
|
|
|
+ {COLORS.map(c => (
|
|
|
+ <button key={c.value} onClick={() => setDrawColor(c.value)}
|
|
|
+ className={`w-5 h-5 rounded-full border-2 transition-transform hover:scale-125 ${
|
|
|
+ drawColor === c.value ? 'border-white scale-125' : 'border-transparent'
|
|
|
+ }`} style={{ backgroundColor: c.value }} />
|
|
|
+ ))}
|
|
|
+ <div className="w-px h-5 bg-white/10" />
|
|
|
+ <button onClick={() => { setDrawMode(false); setPendingStrokes([]); }}
|
|
|
+ className="text-xs text-white/60 hover:text-white px-2 py-1 rounded bg-white/10 hover:bg-white/20 transition-colors">Clear</button>
|
|
|
+ <button onClick={() => setDrawMode(false)}
|
|
|
+ className="text-xs text-white/60 hover:text-white px-2 py-1 rounded bg-white/10 hover:bg-white/20 transition-colors">Done</button>
|
|
|
+ </div>
|
|
|
+ )}
|
|
|
+ {/* Draw mode toggle */}
|
|
|
+ {!drawMode && (
|
|
|
+ <button onClick={() => { videoRef.current?.pause(); setDrawMode(true); }}
|
|
|
+ className="absolute top-2 right-2 z-20 p-1.5 rounded-lg transition-colors"
|
|
|
+ style={{ background: 'rgba(0,0,0,0.5)', color: 'rgba(255,255,255,0.8)' }}
|
|
|
+ title="Draw annotation">
|
|
|
+ <svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
|
+ <path strokeLinecap="round" strokeLinejoin="round" d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
</div>
|
|
|
) : !linkInfo?.asset.videoReady ? (
|
|
|
<div className="flex flex-col items-center justify-center" style={{ aspectRatio: '16/9', maxHeight: '85vh' }}>
|
|
|
@@ -434,7 +502,7 @@ export default function SharePage() {
|
|
|
<svg className="w-3 h-3" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}>
|
|
|
<path strokeLinecap="round" strokeLinejoin="round" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
|
</svg>
|
|
|
- Comment at {formatTimecode(currentTime, 30, currentTime)}
|
|
|
+ Comment at {formatTimecode(currentTime, 30, currentTime)}{pendingStrokes.length > 0 && ` (${pendingStrokes.length} annotation${pendingStrokes.length > 1 ? 's' : ''})`}
|
|
|
</div>
|
|
|
)}
|
|
|
<div className="flex justify-end mt-2">
|