Skip to content

Commit d330ac2

Browse files
committed
Replace VideoSidebar with ThumbnailGrid and ThumbnailReel
Removed VideoSidebar and introduced new ThumbnailGrid and ThumbnailReel components for improved video selection UI. Updated admin and public share pages to use the new components, added thumbnail fetching logic, and adjusted view state handling for multi-video projects. Bumped version to 0.8.7.
1 parent a2fcb73 commit d330ac2

8 files changed

Lines changed: 574 additions & 404 deletions

File tree

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.8.6
1+
0.8.7

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "vitransfer",
3-
"version": "0.8.6",
3+
"version": "0.8.7",
44
"private": true,
55
"scripts": {
66
"dev": "next dev",

src/app/admin/projects/[id]/share/page.tsx

Lines changed: 135 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
'use client'
22

3-
import { useEffect, useRef, useState } from 'react'
3+
import { useEffect, useRef, useState, useCallback } from 'react'
44
import { useParams, useSearchParams, useRouter } from 'next/navigation'
55
import Link from 'next/link'
66
import VideoPlayer from '@/components/VideoPlayer'
77
import CommentSection from '@/components/CommentSection'
8-
import VideoSidebar from '@/components/VideoSidebar'
8+
import ThumbnailGrid from '@/components/ThumbnailGrid'
9+
import ThumbnailReel from '@/components/ThumbnailReel'
910
import ProjectInfo from '@/components/ProjectInfo'
1011
import { Card, CardContent } from '@/components/ui/card'
1112
import { Button } from '@/components/ui/button'
@@ -45,6 +46,9 @@ export default function AdminSharePage() {
4546
displayVideos: any[]
4647
displayLabel: string
4748
} | null>(null)
49+
const [viewState, setViewState] = useState<'grid' | 'player'>('grid')
50+
const [thumbnailsByName, setThumbnailsByName] = useState<Map<string, string>>(new Map())
51+
const [thumbnailsLoading, setThumbnailsLoading] = useState(true)
4852
const tokenCacheRef = useRef<Map<string, any>>(new Map())
4953
const sessionIdRef = useRef<string>(`admin:${Date.now()}`)
5054

@@ -321,11 +325,89 @@ export default function AdminSharePage() {
321325
}
322326
}, [activeVideosRaw])
323327

324-
// Handle video selection (identical to public share)
325-
const handleVideoSelect = (videoName: string) => {
328+
// Fetch thumbnails for all video groups (for grid and reel display)
329+
useEffect(() => {
330+
let isMounted = true
331+
const sessionId = sessionIdRef.current
332+
333+
async function fetchThumbnails() {
334+
if (!project?.videosByName || !id) {
335+
return
336+
}
337+
338+
setThumbnailsLoading(true)
339+
const newThumbnails = new Map<string, string>()
340+
341+
try {
342+
await Promise.all(
343+
Object.entries(project.videosByName).map(async ([name, videos]: [string, any[]]) => {
344+
// Find a video with a thumbnail
345+
const videoWithThumb = videos.find((v: any) => v.thumbnailPath)
346+
if (videoWithThumb) {
347+
const responseThumbnail = await apiFetch(
348+
`/api/admin/video-token?videoId=${videoWithThumb.id}&projectId=${id}&quality=thumbnail&sessionId=${sessionId}`
349+
)
350+
if (responseThumbnail.ok && isMounted) {
351+
const dataThumbnail = await responseThumbnail.json()
352+
newThumbnails.set(name, `/api/content/${dataThumbnail.token}`)
353+
}
354+
}
355+
})
356+
)
357+
358+
if (isMounted) {
359+
setThumbnailsByName(newThumbnails)
360+
}
361+
} catch (error) {
362+
// Failed to load thumbnails
363+
} finally {
364+
if (isMounted) {
365+
setThumbnailsLoading(false)
366+
}
367+
}
368+
}
369+
370+
fetchThumbnails()
371+
372+
return () => {
373+
isMounted = false
374+
}
375+
}, [project?.videosByName, id])
376+
377+
// Determine initial view state based on video count and URL params
378+
useEffect(() => {
379+
if (!project?.videosByName) return
380+
381+
const videoNames = Object.keys(project.videosByName)
382+
const hasMultiple = videoNames.length > 1
383+
384+
// If single video, always go to player
385+
if (!hasMultiple) {
386+
setViewState('player')
387+
return
388+
}
389+
390+
// If URL specifies a video, go to player
391+
if (urlVideoName && project.videosByName[urlVideoName]) {
392+
setViewState('player')
393+
return
394+
}
395+
396+
// Multiple videos without URL param: show grid
397+
setViewState('grid')
398+
}, [project?.videosByName, urlVideoName])
399+
400+
// Handle video selection
401+
const handleVideoSelect = useCallback((videoName: string) => {
326402
setActiveVideoName(videoName)
327403
setActiveVideosRaw(project.videosByName[videoName])
328-
}
404+
setViewState('player')
405+
}, [project?.videosByName])
406+
407+
// Handle back to grid
408+
const handleBackToGrid = useCallback(() => {
409+
setViewState('grid')
410+
}, [])
329411

330412
// Show loading state while project loads
331413
if (loading) {
@@ -380,15 +462,58 @@ export default function AdminSharePage() {
380462
return project.companyName || primaryRecipient?.name || primaryRecipient?.email || 'Client'
381463
})()
382464

465+
// Show thumbnail grid for multi-video projects when in grid view
466+
if (viewState === 'grid' && hasMultipleVideos) {
467+
return (
468+
<div className="flex-1 min-h-0 bg-background flex flex-col overflow-hidden">
469+
<div className="flex-1 overflow-y-auto">
470+
<div className="max-w-screen-2xl mx-auto w-full px-3 sm:px-4 lg:px-6 py-3 sm:py-6">
471+
{/* Header */}
472+
<div className="mb-6 flex flex-wrap items-center gap-4">
473+
<Button
474+
variant="ghost"
475+
size="default"
476+
className="px-3"
477+
onClick={() => router.push(projectUrl)}
478+
>
479+
<ArrowLeft className="w-4 h-4 mr-2" />
480+
<span className="hidden sm:inline">Back to Project</span>
481+
<span className="sm:hidden">Back</span>
482+
</Button>
483+
<div className="flex-1 min-w-0">
484+
<h1 className="text-2xl sm:text-3xl font-bold text-foreground truncate">
485+
{project.title}
486+
</h1>
487+
<p className="text-muted-foreground text-sm mt-1">
488+
Share View
489+
</p>
490+
</div>
491+
</div>
492+
493+
<ThumbnailGrid
494+
videosByName={project.videosByName}
495+
thumbnailsByName={thumbnailsByName}
496+
thumbnailsLoading={thumbnailsLoading}
497+
onVideoSelect={handleVideoSelect}
498+
projectTitle={project.title}
499+
/>
500+
</div>
501+
</div>
502+
</div>
503+
)
504+
}
505+
383506
return (
384-
<div className="flex-1 min-h-0 bg-background flex flex-col lg:flex-row overflow-hidden">
385-
{/* Video Sidebar */}
386-
{project.videosByName && hasMultipleVideos && (
387-
<VideoSidebar
507+
<div className="flex-1 min-h-0 bg-background flex flex-col overflow-hidden">
508+
{/* Thumbnail Reel for multi-video projects */}
509+
{hasMultipleVideos && (
510+
<ThumbnailReel
388511
videosByName={project.videosByName}
512+
thumbnailsByName={thumbnailsByName}
389513
activeVideoName={activeVideoName}
390514
onVideoSelect={handleVideoSelect}
391-
className="w-64 flex-shrink-0"
515+
onBackToGrid={handleBackToGrid}
516+
showBackButton={true}
392517
/>
393518
)}
394519

src/app/share/[token]/SharePageClient.tsx

Lines changed: 108 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
'use client'
22

3-
import { useEffect, useRef, useState } from 'react'
3+
import { useEffect, useRef, useState, useCallback } from 'react'
44
import { useSearchParams } from 'next/navigation'
55
import VideoPlayer from '@/components/VideoPlayer'
66
import CommentSection from '@/components/CommentSection'
7-
import VideoSidebar from '@/components/VideoSidebar'
7+
import ThumbnailGrid from '@/components/ThumbnailGrid'
8+
import ThumbnailReel from '@/components/ThumbnailReel'
89
import ProjectInfo from '@/components/ProjectInfo'
910
import { OTPInput } from '@/components/OTPInput'
1011
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
@@ -60,6 +61,9 @@ export default function SharePageClient({ token }: SharePageClientProps) {
6061
displayVideos: any[]
6162
displayLabel: string
6263
} | null>(null)
64+
const [viewState, setViewState] = useState<'grid' | 'player'>('grid')
65+
const [thumbnailsByName, setThumbnailsByName] = useState<Map<string, string>>(new Map())
66+
const [thumbnailsLoading, setThumbnailsLoading] = useState(true)
6367
const storageKey = token || ''
6468
const tokenCacheRef = useRef<Map<string, any>>(new Map())
6569

@@ -415,11 +419,85 @@ export default function SharePageClient({ token }: SharePageClientProps) {
415419
}
416420
}, [activeVideosRaw, shareToken])
417421

422+
// Fetch thumbnails for all video groups (for grid and reel display)
423+
useEffect(() => {
424+
let isMounted = true
425+
426+
async function fetchThumbnails() {
427+
if (!project?.videosByName || !shareToken) {
428+
return
429+
}
430+
431+
setThumbnailsLoading(true)
432+
const newThumbnails = new Map<string, string>()
433+
434+
try {
435+
await Promise.all(
436+
Object.entries(project.videosByName).map(async ([name, videos]: [string, any[]]) => {
437+
// Find a video with a thumbnail
438+
const videoWithThumb = videos.find((v: any) => v.thumbnailPath)
439+
if (videoWithThumb) {
440+
const thumbToken = await fetchVideoToken(videoWithThumb.id, 'thumbnail')
441+
if (thumbToken && isMounted) {
442+
newThumbnails.set(name, `/api/content/${thumbToken}`)
443+
}
444+
}
445+
})
446+
)
447+
448+
if (isMounted) {
449+
setThumbnailsByName(newThumbnails)
450+
}
451+
} catch (error) {
452+
// Failed to load thumbnails
453+
} finally {
454+
if (isMounted) {
455+
setThumbnailsLoading(false)
456+
}
457+
}
458+
}
459+
460+
fetchThumbnails()
461+
462+
return () => {
463+
isMounted = false
464+
}
465+
}, [project?.videosByName, shareToken])
466+
467+
// Determine initial view state based on video count and URL params
468+
useEffect(() => {
469+
if (!project?.videosByName) return
470+
471+
const videoNames = Object.keys(project.videosByName)
472+
const hasMultiple = videoNames.length > 1
473+
474+
// If single video, always go to player
475+
if (!hasMultiple) {
476+
setViewState('player')
477+
return
478+
}
479+
480+
// If URL specifies a video, go to player
481+
if (urlVideoName && project.videosByName[urlVideoName]) {
482+
setViewState('player')
483+
return
484+
}
485+
486+
// Multiple videos without URL param: show grid
487+
setViewState('grid')
488+
}, [project?.videosByName, urlVideoName])
489+
418490
// Handle video selection
419-
const handleVideoSelect = (videoName: string) => {
491+
const handleVideoSelect = useCallback((videoName: string) => {
420492
setActiveVideoName(videoName)
421493
setActiveVideosRaw(project.videosByName[videoName])
422-
}
494+
setViewState('player')
495+
}, [project?.videosByName])
496+
497+
// Handle back to grid
498+
const handleBackToGrid = useCallback(() => {
499+
setViewState('grid')
500+
}, [])
423501

424502
async function handleSendOtp(e: React.FormEvent) {
425503
e.preventDefault()
@@ -758,15 +836,36 @@ export default function SharePageClient({ token }: SharePageClientProps) {
758836
return !comment.videoId || activeVideoIds.has(comment.videoId)
759837
})
760838

839+
// Show thumbnail grid for multi-video projects when in grid view
840+
if (viewState === 'grid' && hasMultipleVideos) {
841+
return (
842+
<div className="flex-1 min-h-0 bg-background flex flex-col overflow-hidden">
843+
<div className="flex-1 overflow-y-auto">
844+
<div className="w-full px-4 sm:px-6 lg:px-8 py-6 sm:py-8">
845+
<ThumbnailGrid
846+
videosByName={project.videosByName}
847+
thumbnailsByName={thumbnailsByName}
848+
thumbnailsLoading={thumbnailsLoading}
849+
onVideoSelect={handleVideoSelect}
850+
projectTitle={project.title}
851+
/>
852+
</div>
853+
</div>
854+
</div>
855+
)
856+
}
857+
761858
return (
762-
<div className="flex-1 min-h-0 bg-background flex flex-col lg:flex-row overflow-hidden">
763-
{/* Video Sidebar - contains both desktop and mobile versions internally */}
764-
{project.videosByName && (
765-
<VideoSidebar
859+
<div className="flex-1 min-h-0 bg-background flex flex-col overflow-hidden">
860+
{/* Thumbnail Reel for multi-video projects */}
861+
{hasMultipleVideos && (
862+
<ThumbnailReel
766863
videosByName={project.videosByName}
864+
thumbnailsByName={thumbnailsByName}
767865
activeVideoName={activeVideoName}
768866
onVideoSelect={handleVideoSelect}
769-
className="w-64 flex-shrink-0"
867+
onBackToGrid={handleBackToGrid}
868+
showBackButton={true}
770869
/>
771870
)}
772871

0 commit comments

Comments
 (0)