From 4aefad1eff2f33c6449ed6e7749e4bb77464bb10 Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Wed, 24 Dec 2025 10:20:39 -0800 Subject: [PATCH] feat(videos): add search, video toggle, and fix links --- backend/routers/videos.py | 14 +- frontend/app/videos/page.tsx | 262 ++++++++++++++++++++++------------- 2 files changed, 174 insertions(+), 102 deletions(-) diff --git a/backend/routers/videos.py b/backend/routers/videos.py index 62961d0..65e4f6c 100644 --- a/backend/routers/videos.py +++ b/backend/routers/videos.py @@ -31,7 +31,9 @@ def get_all_videos( Show.slug.label("show_slug"), Venue.name.label("venue_name"), Venue.city.label("venue_city"), - Venue.state.label("venue_state") + Venue.state.label("venue_state"), + Performance.slug.label("performance_slug"), + Venue.slug.label("venue_slug") ) .join(Song, Performance.song_id == Song.id) .join(Show, Performance.show_id == Show.id) @@ -57,7 +59,9 @@ def get_all_videos( "show_slug": r[7], "venue_name": r[8], "venue_city": r[9], - "venue_state": r[10] + "venue_state": r[10], + "performance_slug": r[11], + "venue_slug": r[12] } for r in perf_results ] @@ -71,7 +75,8 @@ def get_all_videos( Show.slug, Venue.name.label("venue_name"), Venue.city.label("venue_city"), - Venue.state.label("venue_state") + Venue.state.label("venue_state"), + Venue.slug.label("venue_slug") ) .join(Venue, Show.venue_id == Venue.id) .where(Show.youtube_link != None) @@ -91,7 +96,8 @@ def get_all_videos( "show_slug": r[3], "venue_name": r[4], "venue_city": r[5], - "venue_state": r[6] + "venue_state": r[6], + "venue_slug": r[7] } for r in show_results ] diff --git a/frontend/app/videos/page.tsx b/frontend/app/videos/page.tsx index c4dffd3..2165780 100644 --- a/frontend/app/videos/page.tsx +++ b/frontend/app/videos/page.tsx @@ -21,6 +21,8 @@ interface PerformanceVideo { venue_name: string venue_city: string venue_state: string | null + performance_slug?: string + venue_slug: string } interface ShowVideo { @@ -32,6 +34,7 @@ interface ShowVideo { venue_name: string venue_city: string venue_state: string | null + venue_slug: string } interface VideoStats { @@ -46,6 +49,8 @@ export default function VideosPage() { const [stats, setStats] = useState(null) const [loading, setLoading] = useState(true) const [activeTab, setActiveTab] = useState<"all" | "songs" | "shows">("all") + const [searchQuery, setSearchQuery] = useState("") + const [expandedVideoId, setExpandedVideoId] = useState(null) useEffect(() => { Promise.all([ @@ -66,6 +71,14 @@ export default function VideosPage() { return match ? match[1] : null } + const toggleVideo = (uniqueId: string) => { + if (expandedVideoId === uniqueId) { + setExpandedVideoId(null) + } else { + setExpandedVideoId(uniqueId) + } + } + if (loading) { return (
@@ -79,14 +92,40 @@ export default function VideosPage() { ) } - const filteredPerformances = activeTab === "shows" ? [] : performances - const filteredShows = activeTab === "songs" ? [] : shows + // Filter logic + const query = searchQuery.toLowerCase() + const filterVideo = (v: PerformanceVideo | ShowVideo) => { + if (v.type === "full_show") { + const show = v as ShowVideo + return show.venue_name.toLowerCase().includes(query) || + show.venue_city.toLowerCase().includes(query) || + show.date.includes(query) + } else { + const perf = v as PerformanceVideo + return perf.song_title.toLowerCase().includes(query) || + perf.venue_name.toLowerCase().includes(query) || + perf.venue_city.toLowerCase().includes(query) || + perf.date.includes(query) + } + } // 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()) + let allVideos = [ + ...performances, + ...shows.map(s => ({ ...s, song_title: "Full Show" })) + ].sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()) + + // Apply Tab Filter + if (activeTab === "songs") { + allVideos = allVideos.filter(v => v.type === "performance") + } else if (activeTab === "shows") { + allVideos = allVideos.filter(v => v.type === "full_show") + } + + // Apply Search Filter + if (searchQuery) { + allVideos = allVideos.filter(filterVideo) + } return (
@@ -102,107 +141,134 @@ export default function VideosPage() { )}
- {/* Filter Tabs */} -
- - - +
+ {/* Filter Tabs */} +
+ + + +
+ + {/* Search Input */} +
+ setSearchQuery(e.target.value)} + className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50" + /> +
{/* Video List */}
- {allVideos.map((video, idx) => ( -
- {/* YouTube Icon/Link */} - - - + {allVideos.map((video, idx) => { + const uniqueId = `${video.type}-${video.id}` + const isExpanded = expandedVideoId === uniqueId + const videoId = extractVideoId(video.youtube_link) - {/* Content */} -
-
- {video.type === "full_show" ? ( - - Full Show - - ) : ( - - {(video as PerformanceVideo).song_title} - - )} - - {video.type === "full_show" ? "Full Show" : "Song"} - -
-
- - - {new Date(video.date).toLocaleDateString(undefined, { - year: "numeric", - month: "short", - day: "numeric" - })} - - - - {video.venue_name}, {video.venue_city} - {video.venue_state && `, ${video.venue_state}`} - + return ( +
+
+ {/* Play Icon Trigger */} + + + {/* Content */} +
+
+ {video.type === "full_show" ? ( + + Full Show + + ) : ( + + {(video as PerformanceVideo).song_title} + + )} +
+
+ + + {new Date(video.date).toLocaleDateString(undefined, { + year: "numeric", + month: "short", + day: "numeric" + })} + + + + + {video.venue_name}, {video.venue_city} + +
+
+ + {/* Watch Button */} +
+ + {/* Video Embed Accordion */} + {isExpanded && videoId && ( +
+
+