feat(videos): add search, video toggle, and fix links
Some checks are pending
Deploy Elmeg / deploy (push) Waiting to run
Some checks are pending
Deploy Elmeg / deploy (push) Waiting to run
This commit is contained in:
parent
1e554f553a
commit
4aefad1eff
2 changed files with 174 additions and 102 deletions
|
|
@ -31,7 +31,9 @@ def get_all_videos(
|
||||||
Show.slug.label("show_slug"),
|
Show.slug.label("show_slug"),
|
||||||
Venue.name.label("venue_name"),
|
Venue.name.label("venue_name"),
|
||||||
Venue.city.label("venue_city"),
|
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(Song, Performance.song_id == Song.id)
|
||||||
.join(Show, Performance.show_id == Show.id)
|
.join(Show, Performance.show_id == Show.id)
|
||||||
|
|
@ -57,7 +59,9 @@ def get_all_videos(
|
||||||
"show_slug": r[7],
|
"show_slug": r[7],
|
||||||
"venue_name": r[8],
|
"venue_name": r[8],
|
||||||
"venue_city": r[9],
|
"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
|
for r in perf_results
|
||||||
]
|
]
|
||||||
|
|
@ -71,7 +75,8 @@ def get_all_videos(
|
||||||
Show.slug,
|
Show.slug,
|
||||||
Venue.name.label("venue_name"),
|
Venue.name.label("venue_name"),
|
||||||
Venue.city.label("venue_city"),
|
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)
|
.join(Venue, Show.venue_id == Venue.id)
|
||||||
.where(Show.youtube_link != None)
|
.where(Show.youtube_link != None)
|
||||||
|
|
@ -91,7 +96,8 @@ def get_all_videos(
|
||||||
"show_slug": r[3],
|
"show_slug": r[3],
|
||||||
"venue_name": r[4],
|
"venue_name": r[4],
|
||||||
"venue_city": r[5],
|
"venue_city": r[5],
|
||||||
"venue_state": r[6]
|
"venue_state": r[6],
|
||||||
|
"venue_slug": r[7]
|
||||||
}
|
}
|
||||||
for r in show_results
|
for r in show_results
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,8 @@ interface PerformanceVideo {
|
||||||
venue_name: string
|
venue_name: string
|
||||||
venue_city: string
|
venue_city: string
|
||||||
venue_state: string | null
|
venue_state: string | null
|
||||||
|
performance_slug?: string
|
||||||
|
venue_slug: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ShowVideo {
|
interface ShowVideo {
|
||||||
|
|
@ -32,6 +34,7 @@ interface ShowVideo {
|
||||||
venue_name: string
|
venue_name: string
|
||||||
venue_city: string
|
venue_city: string
|
||||||
venue_state: string | null
|
venue_state: string | null
|
||||||
|
venue_slug: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface VideoStats {
|
interface VideoStats {
|
||||||
|
|
@ -46,6 +49,8 @@ export default function VideosPage() {
|
||||||
const [stats, setStats] = useState<VideoStats | null>(null)
|
const [stats, setStats] = useState<VideoStats | null>(null)
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [activeTab, setActiveTab] = useState<"all" | "songs" | "shows">("all")
|
const [activeTab, setActiveTab] = useState<"all" | "songs" | "shows">("all")
|
||||||
|
const [searchQuery, setSearchQuery] = useState("")
|
||||||
|
const [expandedVideoId, setExpandedVideoId] = useState<string | null>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Promise.all([
|
Promise.all([
|
||||||
|
|
@ -66,6 +71,14 @@ export default function VideosPage() {
|
||||||
return match ? match[1] : null
|
return match ? match[1] : null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const toggleVideo = (uniqueId: string) => {
|
||||||
|
if (expandedVideoId === uniqueId) {
|
||||||
|
setExpandedVideoId(null)
|
||||||
|
} else {
|
||||||
|
setExpandedVideoId(uniqueId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="container py-10 space-y-6">
|
<div className="container py-10 space-y-6">
|
||||||
|
|
@ -79,14 +92,40 @@ export default function VideosPage() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const filteredPerformances = activeTab === "shows" ? [] : performances
|
// Filter logic
|
||||||
const filteredShows = activeTab === "songs" ? [] : shows
|
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
|
// Combine and sort by date
|
||||||
const allVideos = [
|
let allVideos = [
|
||||||
...filteredPerformances.map(p => ({ ...p, sortDate: p.date })),
|
...performances,
|
||||||
...filteredShows.map(s => ({ ...s, sortDate: s.date, song_title: "Full Show" }))
|
...shows.map(s => ({ ...s, song_title: "Full Show" }))
|
||||||
].sort((a, b) => new Date(b.sortDate).getTime() - new Date(a.sortDate).getTime())
|
].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 (
|
return (
|
||||||
<div className="container py-10 space-y-8">
|
<div className="container py-10 space-y-8">
|
||||||
|
|
@ -102,107 +141,134 @@ export default function VideosPage() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filter Tabs */}
|
<div className="flex flex-col md:flex-row gap-4 justify-between items-start md:items-center">
|
||||||
<div className="flex gap-2">
|
{/* Filter Tabs */}
|
||||||
<button
|
<div className="flex gap-2">
|
||||||
onClick={() => setActiveTab("all")}
|
<button
|
||||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${activeTab === "all"
|
onClick={() => setActiveTab("all")}
|
||||||
? "bg-primary text-primary-foreground"
|
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${activeTab === "all"
|
||||||
: "bg-muted hover:bg-muted/80"
|
? "bg-primary text-primary-foreground"
|
||||||
}`}
|
: "bg-muted hover:bg-muted/80"
|
||||||
>
|
}`}
|
||||||
All Videos
|
>
|
||||||
</button>
|
All
|
||||||
<button
|
</button>
|
||||||
onClick={() => setActiveTab("songs")}
|
<button
|
||||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${activeTab === "songs"
|
onClick={() => setActiveTab("songs")}
|
||||||
? "bg-primary text-primary-foreground"
|
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${activeTab === "songs"
|
||||||
: "bg-muted hover:bg-muted/80"
|
? "bg-primary text-primary-foreground"
|
||||||
}`}
|
: "bg-muted hover:bg-muted/80"
|
||||||
>
|
}`}
|
||||||
<Music className="h-4 w-4 inline mr-1" />
|
>
|
||||||
Songs ({stats?.performance_videos})
|
Songs
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setActiveTab("shows")}
|
onClick={() => setActiveTab("shows")}
|
||||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${activeTab === "shows"
|
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${activeTab === "shows"
|
||||||
? "bg-primary text-primary-foreground"
|
? "bg-primary text-primary-foreground"
|
||||||
: "bg-muted hover:bg-muted/80"
|
: "bg-muted hover:bg-muted/80"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<Film className="h-4 w-4 inline mr-1" />
|
Shows
|
||||||
Full Shows ({stats?.full_show_videos})
|
</button>
|
||||||
</button>
|
</div>
|
||||||
|
|
||||||
|
{/* Search Input */}
|
||||||
|
<div className="w-full md:w-72">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search videos..."
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Video List */}
|
{/* Video List */}
|
||||||
<Card>
|
<Card>
|
||||||
<CardContent className="p-0">
|
<CardContent className="p-0">
|
||||||
<div className="divide-y">
|
<div className="divide-y">
|
||||||
{allVideos.map((video, idx) => (
|
{allVideos.map((video, idx) => {
|
||||||
<div
|
const uniqueId = `${video.type}-${video.id}`
|
||||||
key={`${video.type}-${video.id}-${idx}`}
|
const isExpanded = expandedVideoId === uniqueId
|
||||||
className="flex items-center gap-4 p-4 hover:bg-muted/50 transition-colors"
|
const videoId = extractVideoId(video.youtube_link)
|
||||||
>
|
|
||||||
{/* 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 */}
|
return (
|
||||||
<div className="flex-1 min-w-0">
|
<div key={uniqueId} className="flex flex-col">
|
||||||
<div className="flex items-center gap-2 flex-wrap">
|
<div className="flex items-center gap-4 p-4 hover:bg-muted/50 transition-colors">
|
||||||
{video.type === "full_show" ? (
|
{/* Play Icon Trigger */}
|
||||||
<Link
|
<button
|
||||||
href={`/shows/${video.show_slug || video.id}`}
|
onClick={() => toggleVideo(uniqueId)}
|
||||||
className="font-medium hover:underline text-primary"
|
className="shrink-0 text-red-600 hover:text-red-500 transition-colors"
|
||||||
>
|
title={isExpanded ? "Close video" : "Watch video"}
|
||||||
Full Show
|
>
|
||||||
</Link>
|
<Youtube className="h-6 w-6" />
|
||||||
) : (
|
</button>
|
||||||
<Link
|
|
||||||
href={`/shows/${(video as PerformanceVideo).show_slug || (video as PerformanceVideo).show_id}`}
|
{/* Content */}
|
||||||
className="font-medium hover:underline"
|
<div className="flex-1 min-w-0">
|
||||||
>
|
<div className="flex items-center gap-2 flex-wrap">
|
||||||
{(video as PerformanceVideo).song_title}
|
{video.type === "full_show" ? (
|
||||||
</Link>
|
<Link
|
||||||
)}
|
href={`/shows/${video.show_slug}`}
|
||||||
<Badge variant={video.type === "full_show" ? "default" : "secondary"} className="text-xs">
|
className="font-medium hover:underline text-primary text-lg"
|
||||||
{video.type === "full_show" ? "Full Show" : "Song"}
|
>
|
||||||
</Badge>
|
Full Show
|
||||||
</div>
|
</Link>
|
||||||
<div className="flex items-center gap-3 text-sm text-muted-foreground mt-1">
|
) : (
|
||||||
<span className="flex items-center gap-1">
|
<Link
|
||||||
<Calendar className="h-3 w-3" />
|
href={`/performances/${(video as PerformanceVideo).performance_slug}`}
|
||||||
{new Date(video.date).toLocaleDateString(undefined, {
|
className="font-medium hover:underline text-lg"
|
||||||
year: "numeric",
|
>
|
||||||
month: "short",
|
{(video as PerformanceVideo).song_title}
|
||||||
day: "numeric"
|
</Link>
|
||||||
})}
|
)}
|
||||||
</span>
|
</div>
|
||||||
<span className="flex items-center gap-1">
|
<div className="flex items-center gap-2 text-sm text-muted-foreground mt-1">
|
||||||
<MapPin className="h-3 w-3" />
|
<Link href={`/shows/${video.show_slug}`} className="flex items-center gap-1 hover:underline hover:text-foreground">
|
||||||
{video.venue_name}, {video.venue_city}
|
<Calendar className="h-3 w-3" />
|
||||||
{video.venue_state && `, ${video.venue_state}`}
|
{new Date(video.date).toLocaleDateString(undefined, {
|
||||||
</span>
|
year: "numeric",
|
||||||
|
month: "short",
|
||||||
|
day: "numeric"
|
||||||
|
})}
|
||||||
|
</Link>
|
||||||
|
<span>•</span>
|
||||||
|
<Link href={`/venues/${video.venue_slug}`} className="flex items-center gap-1 hover:underline hover:text-foreground">
|
||||||
|
<MapPin className="h-3 w-3" />
|
||||||
|
{video.venue_name}, {video.venue_city}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Watch Button */}
|
||||||
|
<button
|
||||||
|
onClick={() => toggleVideo(uniqueId)}
|
||||||
|
className="text-sm font-medium text-primary hover:underline shrink-0"
|
||||||
|
>
|
||||||
|
{isExpanded ? "Close video" : "Watch video"}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Video Embed Accordion */}
|
||||||
|
{isExpanded && videoId && (
|
||||||
|
<div className="bg-black/5 p-4 border-t border-b">
|
||||||
|
<div className="aspect-video w-full max-w-4xl mx-auto rounded-lg overflow-hidden shadow-xl">
|
||||||
|
<iframe
|
||||||
|
width="100%"
|
||||||
|
height="100%"
|
||||||
|
src={`https://www.youtube.com/embed/${videoId}?autoplay=1`}
|
||||||
|
title="YouTube video player"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
allowFullScreen
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</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>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue