ソースを参照

feat: add annotation drawing to guest comment form on share page

Guest users can now draw annotations (pen, arrow, rect, ellipse) on
the video before posting a comment. Strokes are sent as annotations[]
in the comment payload and cleared on submit.
kingkong 1 ヶ月 前
コミット
4360db5752
1 ファイル変更76 行追加8 行削除
  1. 76 8
      src/app/share/[token]/page.tsx

+ 76 - 8
src/app/share/[token]/page.tsx

@@ -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">