elmeg-demo/frontend/app/videos/page.tsx
fullsizemalt 1f7f83a31a
Some checks are pending
Deploy Elmeg / deploy (push) Waiting to run
fix: Videos page links to show, hide test users from leaderboard
- Videos page now links song titles to show page (where video is displayed)
- Leaderboard hides tenwest/testuser until 12+ real users exist
2025-12-23 15:45:29 -08:00

211 lines
9.1 KiB
TypeScript

"use client"
import { useEffect, useState } from "react"
import { getApiUrl } from "@/lib/api-config"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Skeleton } from "@/components/ui/skeleton"
import { Youtube, Calendar, MapPin, Music, Film } from "lucide-react"
import Link from "next/link"
interface PerformanceVideo {
type: "performance"
id: number
youtube_link: string
show_id: number
song_id: number
song_title: string
song_slug: string
date: string
show_slug: string
venue_name: string
venue_city: string
venue_state: string | null
}
interface ShowVideo {
type: "full_show"
id: number
youtube_link: string
date: string
show_slug: string
venue_name: string
venue_city: string
venue_state: string | null
}
interface VideoStats {
performance_videos: number
full_show_videos: number
total: number
}
export default function VideosPage() {
const [performances, setPerformances] = useState<PerformanceVideo[]>([])
const [shows, setShows] = useState<ShowVideo[]>([])
const [stats, setStats] = useState<VideoStats | null>(null)
const [loading, setLoading] = useState(true)
const [activeTab, setActiveTab] = useState<"all" | "songs" | "shows">("all")
useEffect(() => {
Promise.all([
fetch(`${getApiUrl()}/videos/?limit=500`).then(r => r.json()),
fetch(`${getApiUrl()}/videos/stats`).then(r => r.json())
])
.then(([videoData, statsData]) => {
setPerformances(videoData.performances)
setShows(videoData.shows)
setStats(statsData)
})
.catch(console.error)
.finally(() => setLoading(false))
}, [])
const extractVideoId = (url: string) => {
const match = url.match(/[?&]v=([^&]+)/)
return match ? match[1] : null
}
if (loading) {
return (
<div className="container py-10 space-y-6">
<Skeleton className="h-10 w-48" />
<div className="space-y-2">
{Array.from({ length: 10 }).map((_, i) => (
<Skeleton key={i} className="h-12 w-full" />
))}
</div>
</div>
)
}
const filteredPerformances = activeTab === "shows" ? [] : performances
const filteredShows = activeTab === "songs" ? [] : shows
// Combine and sort by date
const allVideos = [
...filteredPerformances.map(p => ({ ...p, sortDate: p.date })),
...filteredShows.map(s => ({ ...s, sortDate: s.date, song_title: "Full Show" }))
].sort((a, b) => new Date(b.sortDate).getTime() - new Date(a.sortDate).getTime())
return (
<div className="container py-10 space-y-8">
<div className="flex flex-col gap-4">
<div className="flex items-center gap-3">
<Youtube className="h-8 w-8 text-red-600" />
<h1 className="text-3xl font-bold tracking-tight">Videos</h1>
</div>
{stats && (
<p className="text-muted-foreground">
{stats.total} videos available {stats.full_show_videos} full shows {stats.performance_videos} song performances
</p>
)}
</div>
{/* Filter Tabs */}
<div className="flex gap-2">
<button
onClick={() => setActiveTab("all")}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${activeTab === "all"
? "bg-primary text-primary-foreground"
: "bg-muted hover:bg-muted/80"
}`}
>
All Videos
</button>
<button
onClick={() => setActiveTab("songs")}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${activeTab === "songs"
? "bg-primary text-primary-foreground"
: "bg-muted hover:bg-muted/80"
}`}
>
<Music className="h-4 w-4 inline mr-1" />
Songs ({stats?.performance_videos})
</button>
<button
onClick={() => setActiveTab("shows")}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${activeTab === "shows"
? "bg-primary text-primary-foreground"
: "bg-muted hover:bg-muted/80"
}`}
>
<Film className="h-4 w-4 inline mr-1" />
Full Shows ({stats?.full_show_videos})
</button>
</div>
{/* Video List */}
<Card>
<CardContent className="p-0">
<div className="divide-y">
{allVideos.map((video, idx) => (
<div
key={`${video.type}-${video.id}-${idx}`}
className="flex items-center gap-4 p-4 hover:bg-muted/50 transition-colors"
>
{/* YouTube Icon/Link */}
<a
href={video.youtube_link}
target="_blank"
rel="noopener noreferrer"
className="shrink-0 text-red-600 hover:text-red-500 transition-colors"
title="Watch on YouTube"
>
<Youtube className="h-6 w-6" />
</a>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
{video.type === "full_show" ? (
<Link
href={`/shows/${video.show_slug || video.id}`}
className="font-medium hover:underline text-primary"
>
Full Show
</Link>
) : (
<Link
href={`/shows/${(video as PerformanceVideo).show_slug || (video as PerformanceVideo).show_id}`}
className="font-medium hover:underline"
>
{(video as PerformanceVideo).song_title}
</Link>
)}
<Badge variant={video.type === "full_show" ? "default" : "secondary"} className="text-xs">
{video.type === "full_show" ? "Full Show" : "Song"}
</Badge>
</div>
<div className="flex items-center gap-3 text-sm text-muted-foreground mt-1">
<span className="flex items-center gap-1">
<Calendar className="h-3 w-3" />
{new Date(video.date).toLocaleDateString(undefined, {
year: "numeric",
month: "short",
day: "numeric"
})}
</span>
<span className="flex items-center gap-1">
<MapPin className="h-3 w-3" />
{video.venue_name}, {video.venue_city}
{video.venue_state && `, ${video.venue_state}`}
</span>
</div>
</div>
{/* Show Link */}
<Link
href={`/shows/${video.show_slug || (video.type === "full_show" ? video.id : (video as PerformanceVideo).show_id)}`}
className="text-sm text-muted-foreground hover:text-foreground transition-colors shrink-0"
>
View Show
</Link>
</div>
))}
</div>
</CardContent>
</Card>
</div>
)
}