diff --git a/backend/routers/songs.py b/backend/routers/songs.py index a2cb920..b73dae7 100644 --- a/backend/routers/songs.py +++ b/backend/routers/songs.py @@ -37,15 +37,33 @@ def read_songs( return PaginatedResponse(data=[], meta=PaginationMeta(total=0, limit=limit, offset=offset)) if sort == "times_played": + # Select both Song and count + query = select(Song, func.count(Performance.id).label("times_played")) query = query.outerjoin(Performance).group_by(Song.id) - - # Calculate total count before pagination - total = session.exec(select(func.count()).select_from(query.subquery())).one() - - if sort == "times_played": - query = query.order_by(func.count(Performance.id).desc()) + + if vertical: + query = query.where(Song.vertical_id == vertical_entity.id) - songs = session.exec(query.offset(offset).limit(limit)).all() + # Calculate total + total_query = select(func.count()).select_from(select(Song.id).where(Song.vertical_id == vertical_entity.id) if vertical else select(Song.id)) + total = session.exec(total_query).one() + + query = query.order_by(func.count(Performance.id).desc()) + + results = session.exec(query.offset(offset).limit(limit)).all() + + # Map (Song, count) tuples to SongRead with times_played + songs = [] + for song, count in results: + song_read = SongRead.model_validate(song) + song_read.times_played = count + songs.append(song_read) + + else: + # Standard query + # Calculate total count before pagination + total = session.exec(select(func.count()).select_from(query.subquery())).one() + songs = session.exec(query.offset(offset).limit(limit)).all() return PaginatedResponse( data=songs, diff --git a/backend/schemas.py b/backend/schemas.py index 814320f..eb2455f 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -85,6 +85,7 @@ class SongRead(SongBase): tags: List["TagRead"] = [] artist: Optional["ArtistRead"] = None vertical: Optional["VerticalSimple"] = None + times_played: Optional[int] = 0 diff --git a/frontend/app/[vertical]/page.tsx b/frontend/app/[vertical]/page.tsx index db49a10..d6906ae 100644 --- a/frontend/app/[vertical]/page.tsx +++ b/frontend/app/[vertical]/page.tsx @@ -5,7 +5,15 @@ import Link from "next/link" import { Calendar, MapPin, Music, Trophy, Video, Ticket, Building, ChevronRight } from "lucide-react" import { Card, CardContent } from "@/components/ui/card" import { Button } from "@/components/ui/button" -import { VideoGallery } from "@/components/videos/video-gallery" +import { Badge } from "@/components/ui/badge" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" import { Show, Song, PaginatedResponse } from "@/types/models" interface Props { @@ -20,7 +28,8 @@ export function generateStaticParams() { async function getRecentShows(verticalSlug: string): Promise { try { - const res = await fetch(`${getApiUrl()}/shows/?vertical_slugs=${verticalSlug}&limit=8&status=past`, { + // Fetch 10 recent shows + const res = await fetch(`${getApiUrl()}/shows/?vertical_slugs=${verticalSlug}&limit=10&status=past`, { next: { revalidate: 60 } }) if (!res.ok) return [] @@ -33,7 +42,8 @@ async function getRecentShows(verticalSlug: string): Promise { async function getTopSongs(verticalSlug: string): Promise { try { - const res = await fetch(`${getApiUrl()}/songs/?vertical=${verticalSlug}&limit=5&sort=times_played`, { + // Fetch top 10 songs, assuming backend now populates times_played + const res = await fetch(`${getApiUrl()}/songs/?vertical=${verticalSlug}&limit=10&sort=times_played`, { next: { revalidate: 60 } }) if (!res.ok) return [] @@ -44,6 +54,18 @@ async function getTopSongs(verticalSlug: string): Promise { } } +async function getVerticalStats(verticalSlug: string) { + try { + const res = await fetch(`${getApiUrl()}/bands/${verticalSlug}`, { + next: { revalidate: 300 } + }) + if (!res.ok) return null + return res.json() + } catch { + return null + } +} + export default async function VerticalPage({ params }: Props) { const { vertical: verticalSlug } = await params const vertical = VERTICALS.find((v) => v.slug === verticalSlug) @@ -52,166 +74,178 @@ export default async function VerticalPage({ params }: Props) { notFound() } - const [recentShows, topSongs] = await Promise.all([ + const [recentShows, topSongs, bandProfile] = await Promise.all([ getRecentShows(verticalSlug), - getTopSongs(verticalSlug) + getTopSongs(verticalSlug), + getVerticalStats(verticalSlug) ]) - const navCards = [ - { href: `/${verticalSlug}/shows`, icon: Calendar, title: "Shows", desc: "Browse the complete archive" }, - { href: `/${verticalSlug}/venues`, icon: Building, title: "Venues", desc: "Find your favorite spots" }, - { href: `/${verticalSlug}/songs`, icon: Music, title: "Songs", desc: "Explore the catalog" }, - { href: `/${verticalSlug}/performances`, icon: Trophy, title: "Top Performances", desc: "Highest rated jams" }, - { href: `/leaderboards?band=${verticalSlug}`, icon: Trophy, title: "Leaderboards", desc: "Top rated everything" }, - { href: `/${verticalSlug}/tours`, icon: Ticket, title: "Tours", desc: "Browse by tour" }, - { href: `/videos?band=${verticalSlug}`, icon: Video, title: "Videos", desc: "Watch full shows and songs" }, - ] + const stats = bandProfile?.stats || {} return ( -
- {/* Hero Section */} -
-

- {vertical.name} -

-

- A comprehensive community-driven archive for {vertical.name} history. -
- Discover shows, share ratings, and explore the music together. -

-
- - - - - - +
+ {/* Hero Section - Compact & Utilitarian */} +
+
+
+
+

{vertical.name}

+

+ {vertical.description} +

+
+ + {/* High-Level Stats */} +
+
+
{stats.total_shows || 0}
+
Shows
+
+
+
{stats.total_songs || 0}
+
Songs
+
+
+
{stats.total_venues || 0}
+
Venues
+
+
+
+ + {/* Quick Actions Bar */} +
+ + + + + + + + + + + + + + + +
-
- {/* Recent Shows */} - {recentShows.length > 0 && ( -
+
+
+ {/* Recent Shows Table */} +

Recent Shows

- - View all shows + +
-
- {recentShows.slice(0, 8).map((show) => ( - - - -
- {new Date(show.date).toLocaleDateString('en-US', { - weekday: 'short', - month: 'short', - day: 'numeric', - year: 'numeric' - })} -
-
- {show.venue?.name || "Unknown Venue"} -
-
- - {show.venue?.city}, {show.venue?.state || show.venue?.country} -
- {/* Tour is not in strict Show model yet, omitting for strictness or need to add to model */} - {/* {show.tour?.name && ( -
- {show.tour.name} -
- )} */} -
-
- - ))} -
-
- )} - {/* Most Played Songs */} - {topSongs.length > 0 && ( -
+ + + + + Date + Venue + Location + + + + {recentShows.length === 0 ? ( + + + No shows found + + + ) : ( + recentShows.map((show) => ( + + + + {new Date(show.date).toLocaleDateString('en-US', { + month: 'short', day: 'numeric', year: 'numeric' + })} + + + + {show.venue?.name || "Unknown"} + + + {show.venue?.city}, {show.venue?.state} + + + )) + )} + +
+
+
+ + {/* Most Played Songs Table */} +

Most Played Songs

- - View all songs + +
-
- {topSongs.slice(0, 5).map((song, index) => ( - -
-
- {index + 1} -
-
-
- {song.title} -
-
-
- {song.times_played || 0} performances -
-
- - ))} -
-
- )} - {/* Videos Section */} -
- -
- - {/* Navigation Cards */} -
-

Explore

-
- {navCards.map((card) => ( - - - -
- -
-
-

- {card.title} -

-

- {card.desc} -

-
-
-
- - ))} + + + + + Title + Plays + + + + {topSongs.length === 0 ? ( + + + No songs found + + + ) : ( + topSongs.map((song) => ( + + + + {song.title} + + {song.original_artist && ( + + by {song.original_artist} + + )} + + + {song.times_played || 0} + + + )) + )} + +
+
-
+
)