Compare commits
No commits in common. "465017cda9d41ab4645d2a8e047f018c22b75f10" and "159cbc853c60f7d27a00abff1004768a30fd55ed" have entirely different histories.
465017cda9
...
159cbc853c
4 changed files with 213 additions and 246 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
import os
|
import os
|
||||||
from routers import auth, shows, venues, songs, social, tours, artists, preferences, reviews, badges, nicknames, moderation, attendance, groups, users, search, performances, notifications, feed, leaderboards, stats, admin, chase, gamification, videos, musicians, sequences, verticals, canon, on_this_day
|
from routers import auth, shows, venues, songs, social, tours, artists, preferences, reviews, badges, nicknames, moderation, attendance, groups, users, search, performances, notifications, feed, leaderboards, stats, admin, chase, gamification, videos, musicians, sequences, verticals, canon
|
||||||
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
|
@ -46,7 +46,6 @@ app.include_router(musicians.router)
|
||||||
app.include_router(sequences.router)
|
app.include_router(sequences.router)
|
||||||
app.include_router(verticals.router)
|
app.include_router(verticals.router)
|
||||||
app.include_router(canon.router)
|
app.include_router(canon.router)
|
||||||
app.include_router(on_this_day.router)
|
|
||||||
|
|
||||||
|
|
||||||
# Optional features - can be disabled via env vars
|
# Optional features - can be disabled via env vars
|
||||||
|
|
|
||||||
|
|
@ -1,125 +0,0 @@
|
||||||
"""
|
|
||||||
On This Day API endpoint - shows what happened on this date in history.
|
|
||||||
"""
|
|
||||||
from datetime import date
|
|
||||||
from typing import List, Optional
|
|
||||||
from fastapi import APIRouter, Depends, Query
|
|
||||||
from sqlmodel import Session, select
|
|
||||||
from pydantic import BaseModel
|
|
||||||
from database import get_session
|
|
||||||
from models import Show, Venue, Vertical, Performance, Song
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/on-this-day", tags=["on-this-day"])
|
|
||||||
|
|
||||||
|
|
||||||
class ShowOnThisDay(BaseModel):
|
|
||||||
id: int
|
|
||||||
date: str
|
|
||||||
slug: str | None
|
|
||||||
venue_name: str | None
|
|
||||||
venue_city: str | None
|
|
||||||
vertical_name: str
|
|
||||||
vertical_slug: str
|
|
||||||
years_ago: int
|
|
||||||
|
|
||||||
|
|
||||||
class OnThisDayResponse(BaseModel):
|
|
||||||
month: int
|
|
||||||
day: int
|
|
||||||
shows: List[ShowOnThisDay]
|
|
||||||
total_shows: int
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=OnThisDayResponse)
|
|
||||||
def get_on_this_day(
|
|
||||||
month: Optional[int] = None,
|
|
||||||
day: Optional[int] = None,
|
|
||||||
vertical: Optional[str] = None,
|
|
||||||
session: Session = Depends(get_session)
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Get all shows that happened on this day in history.
|
|
||||||
Defaults to today's date if month/day not specified.
|
|
||||||
"""
|
|
||||||
today = date.today()
|
|
||||||
target_month = month or today.month
|
|
||||||
target_day = day or today.day
|
|
||||||
|
|
||||||
# Build query
|
|
||||||
query = select(Show).where(
|
|
||||||
Show.date.isnot(None)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Filter by vertical if specified
|
|
||||||
if vertical:
|
|
||||||
vertical_obj = session.exec(
|
|
||||||
select(Vertical).where(Vertical.slug == vertical)
|
|
||||||
).first()
|
|
||||||
if vertical_obj:
|
|
||||||
query = query.where(Show.vertical_id == vertical_obj.id)
|
|
||||||
|
|
||||||
# Execute and filter by month/day in Python (SQL date functions vary)
|
|
||||||
all_shows = session.exec(query.order_by(Show.date.desc())).all()
|
|
||||||
|
|
||||||
matching_shows = []
|
|
||||||
for show in all_shows:
|
|
||||||
if show.date and show.date.month == target_month and show.date.day == target_day:
|
|
||||||
venue = session.get(Venue, show.venue_id) if show.venue_id else None
|
|
||||||
vert = session.get(Vertical, show.vertical_id)
|
|
||||||
|
|
||||||
years_ago = today.year - show.date.year
|
|
||||||
|
|
||||||
matching_shows.append(ShowOnThisDay(
|
|
||||||
id=show.id,
|
|
||||||
date=show.date.strftime("%Y-%m-%d"),
|
|
||||||
slug=show.slug,
|
|
||||||
venue_name=venue.name if venue else None,
|
|
||||||
venue_city=venue.city if venue else None,
|
|
||||||
vertical_name=vert.name if vert else "Unknown",
|
|
||||||
vertical_slug=vert.slug if vert else "unknown",
|
|
||||||
years_ago=years_ago
|
|
||||||
))
|
|
||||||
|
|
||||||
# Sort by years ago (most recent anniversary first)
|
|
||||||
matching_shows.sort(key=lambda x: x.years_ago)
|
|
||||||
|
|
||||||
return OnThisDayResponse(
|
|
||||||
month=target_month,
|
|
||||||
day=target_day,
|
|
||||||
shows=matching_shows,
|
|
||||||
total_shows=len(matching_shows)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/highlights", response_model=List[ShowOnThisDay])
|
|
||||||
def get_on_this_day_highlights(
|
|
||||||
limit: int = 5,
|
|
||||||
session: Session = Depends(get_session)
|
|
||||||
):
|
|
||||||
"""Get highlighted shows for today across all bands (limited list for homepage)"""
|
|
||||||
today = date.today()
|
|
||||||
|
|
||||||
all_shows = session.exec(
|
|
||||||
select(Show).where(Show.date.isnot(None))
|
|
||||||
).all()
|
|
||||||
|
|
||||||
matching = []
|
|
||||||
for show in all_shows:
|
|
||||||
if show.date and show.date.month == today.month and show.date.day == today.day:
|
|
||||||
venue = session.get(Venue, show.venue_id) if show.venue_id else None
|
|
||||||
vert = session.get(Vertical, show.vertical_id)
|
|
||||||
|
|
||||||
matching.append(ShowOnThisDay(
|
|
||||||
id=show.id,
|
|
||||||
date=show.date.strftime("%Y-%m-%d"),
|
|
||||||
slug=show.slug,
|
|
||||||
venue_name=venue.name if venue else None,
|
|
||||||
venue_city=venue.city if venue else None,
|
|
||||||
vertical_name=vert.name if vert else "Unknown",
|
|
||||||
vertical_slug=vert.slug if vert else "unknown",
|
|
||||||
years_ago=today.year - show.date.year
|
|
||||||
))
|
|
||||||
|
|
||||||
# Sort by anniversary and return limited
|
|
||||||
matching.sort(key=lambda x: x.years_ago)
|
|
||||||
return matching[:limit]
|
|
||||||
|
|
@ -1,143 +1,252 @@
|
||||||
import Link from "next/link"
|
import { ActivityFeed } from "@/components/feed/activity-feed"
|
||||||
|
import { XPLeaderboard } from "@/components/gamification/xp-leaderboard"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent } from "@/components/ui/card"
|
||||||
|
import Link from "next/link"
|
||||||
|
import { Trophy, Music, MapPin, Calendar, ChevronRight, Star, Youtube, Route } from "lucide-react"
|
||||||
import { getApiUrl } from "@/lib/api-config"
|
import { getApiUrl } from "@/lib/api-config"
|
||||||
|
|
||||||
interface Vertical {
|
interface Show {
|
||||||
|
id: number
|
||||||
|
slug?: string
|
||||||
|
date: string
|
||||||
|
venue?: {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
slug: string
|
slug?: string
|
||||||
description: string | null
|
city?: string
|
||||||
|
state?: string
|
||||||
}
|
}
|
||||||
|
tour?: {
|
||||||
interface Scene {
|
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
slug: string
|
slug?: string
|
||||||
description: string | null
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getVerticals(): Promise<Vertical[]> {
|
interface Song {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
slug?: string
|
||||||
|
performance_count?: number
|
||||||
|
avg_rating?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRecentShows(): Promise<Show[]> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${getApiUrl()}/verticals`, { next: { revalidate: 60 } })
|
const res = await fetch(`${getApiUrl()}/shows/recent?limit=8`, {
|
||||||
|
cache: 'no-store',
|
||||||
|
next: { revalidate: 60 }
|
||||||
|
})
|
||||||
if (!res.ok) return []
|
if (!res.ok) return []
|
||||||
return res.json()
|
return res.json()
|
||||||
} catch {
|
} catch (e) {
|
||||||
|
console.error('Failed to fetch recent shows:', e)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getScenes(): Promise<Scene[]> {
|
async function getTopSongs(): Promise<Song[]> {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${getApiUrl()}/verticals/scenes`, { next: { revalidate: 60 } })
|
const res = await fetch(`${getApiUrl()}/stats/top-songs?limit=5`, {
|
||||||
|
cache: 'no-store',
|
||||||
|
next: { revalidate: 300 }
|
||||||
|
})
|
||||||
if (!res.ok) return []
|
if (!res.ok) return []
|
||||||
return res.json()
|
return res.json()
|
||||||
} catch {
|
} catch (e) {
|
||||||
|
console.error('Failed to fetch top songs:', e)
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function HomePage() {
|
|
||||||
const [verticals, scenes] = await Promise.all([getVerticals(), getScenes()])
|
|
||||||
|
export default async function Home() {
|
||||||
|
const [recentShows, topSongs] = await Promise.all([
|
||||||
|
getRecentShows(),
|
||||||
|
getTopSongs()
|
||||||
|
])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-16">
|
<div className="flex flex-col gap-8">
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
<section className="text-center py-16 space-y-6">
|
<section className="flex flex-col items-center gap-4 py-12 text-center md:py-20 bg-gradient-to-b from-background to-accent/20 rounded-lg border">
|
||||||
<h1 className="text-5xl font-bold tracking-tight">
|
<h1 className="text-4xl font-bold tracking-tighter sm:text-5xl md:text-6xl">
|
||||||
Fediversion
|
Elmeg
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
<p className="max-w-[600px] text-lg text-muted-foreground">
|
||||||
The unified platform for the entire jam scene.
|
A comprehensive community-driven archive for Goose history.
|
||||||
One account, all your favorite bands.
|
<br />
|
||||||
|
Discover shows, share ratings, and explore the music together.
|
||||||
</p>
|
</p>
|
||||||
<div className="flex justify-center gap-4">
|
<div className="flex flex-col sm:flex-row gap-3 sm:gap-4">
|
||||||
<Button asChild size="lg">
|
<Link href="/performances">
|
||||||
<Link href="/onboarding">Get Started</Link>
|
<Button size="lg" className="gap-2 w-full sm:w-auto">
|
||||||
|
<Trophy className="h-4 w-4" />
|
||||||
|
Top Performances
|
||||||
</Button>
|
</Button>
|
||||||
<Button asChild variant="outline" size="lg">
|
</Link>
|
||||||
<Link href="/goose">Explore Shows</Link>
|
<Link href="/shows">
|
||||||
|
<Button variant="outline" size="lg" className="w-full sm:w-auto">
|
||||||
|
Browse Shows
|
||||||
</Button>
|
</Button>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Scenes Section */}
|
{/* Recent Shows */}
|
||||||
{scenes.length > 0 && (
|
<section className="space-y-4">
|
||||||
<section className="space-y-6">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-2xl font-bold text-center">Browse by Scene</h2>
|
<h2 className="text-2xl font-bold flex items-center gap-2">
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 max-w-3xl mx-auto">
|
<Calendar className="h-6 w-6 text-blue-500" />
|
||||||
{scenes.map((scene) => (
|
Recent Shows
|
||||||
<Link
|
</h2>
|
||||||
key={scene.slug}
|
<Link href="/shows" className="text-sm text-muted-foreground hover:underline flex items-center gap-1">
|
||||||
href={`/?scene=${scene.slug}`}
|
View all shows <ChevronRight className="h-4 w-4" />
|
||||||
className="block p-4 rounded-lg border bg-card hover:bg-accent transition-colors text-center"
|
|
||||||
>
|
|
||||||
<div className="font-semibold">{scene.name}</div>
|
|
||||||
{scene.description && (
|
|
||||||
<div className="text-sm text-muted-foreground mt-1 line-clamp-2">
|
|
||||||
{scene.description}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
{recentShows.length > 0 ? (
|
||||||
)}
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{recentShows.map((show) => (
|
||||||
{/* Bands Grid */}
|
<Link key={show.id} href={`/shows/${show.slug}`}>
|
||||||
<section className="space-y-6">
|
<Card className="h-full hover:bg-accent/50 transition-colors cursor-pointer">
|
||||||
<h2 className="text-2xl font-bold text-center">Featured Bands</h2>
|
<CardContent className="p-4">
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
<div className="font-semibold">
|
||||||
{verticals.map((vertical) => (
|
{new Date(show.date).toLocaleDateString('en-US', {
|
||||||
<Card key={vertical.slug} className="hover:shadow-lg transition-shadow">
|
weekday: 'short',
|
||||||
<CardHeader>
|
year: 'numeric',
|
||||||
<CardTitle>
|
month: 'short',
|
||||||
<Link href={`/${vertical.slug}`} className="hover:underline">
|
day: 'numeric'
|
||||||
{vertical.name}
|
})}
|
||||||
</Link>
|
|
||||||
</CardTitle>
|
|
||||||
{vertical.description && (
|
|
||||||
<CardDescription>{vertical.description}</CardDescription>
|
|
||||||
)}
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Button asChild variant="outline" size="sm">
|
|
||||||
<Link href={`/${vertical.slug}/shows`}>Shows</Link>
|
|
||||||
</Button>
|
|
||||||
<Button asChild variant="outline" size="sm">
|
|
||||||
<Link href={`/${vertical.slug}/songs`}>Songs</Link>
|
|
||||||
</Button>
|
|
||||||
<Button asChild variant="outline" size="sm">
|
|
||||||
<Link href={`/${vertical.slug}/venues`}>Venues</Link>
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
{show.venue && (
|
||||||
|
<div className="text-sm text-muted-foreground mt-1">
|
||||||
|
{show.venue.name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{show.venue?.city && (
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{show.venue.city}{show.venue.state ? `, ${show.venue.state}` : ''}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{show.tour && (
|
||||||
|
<div className="text-xs text-primary mt-2">
|
||||||
|
{show.tour.name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<Card className="p-8 text-center text-muted-foreground">
|
||||||
|
<p>No shows yet. Check back soon!</p>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
{/* Stats Section */}
|
<div className="grid gap-8 lg:grid-cols-3">
|
||||||
<section className="bg-muted/50 rounded-lg p-8 text-center space-y-4">
|
{/* Top Songs */}
|
||||||
<h2 className="text-2xl font-bold">Join the Community</h2>
|
<section className="space-y-4 lg:col-span-1">
|
||||||
<p className="text-muted-foreground">
|
<div className="flex items-center justify-between">
|
||||||
Track shows, rate performances, discover connections across bands.
|
<h2 className="text-xl font-bold flex items-center gap-2">
|
||||||
</p>
|
<Star className="h-5 w-5 text-yellow-500" />
|
||||||
<div className="grid grid-cols-3 gap-6 max-w-md mx-auto pt-4">
|
Top Songs
|
||||||
<div>
|
</h2>
|
||||||
<div className="text-3xl font-bold">{verticals.length}</div>
|
<Link href="/songs" className="text-sm text-muted-foreground hover:underline flex items-center gap-1">
|
||||||
<div className="text-sm text-muted-foreground">Bands</div>
|
All songs <ChevronRight className="h-4 w-4" />
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<Card>
|
||||||
<div className="text-3xl font-bold">{scenes.length}</div>
|
<CardContent className="p-0">
|
||||||
<div className="text-sm text-muted-foreground">Scenes</div>
|
{topSongs.length > 0 ? (
|
||||||
|
<ul className="divide-y">
|
||||||
|
{topSongs.map((song, idx) => (
|
||||||
|
<li key={song.id}>
|
||||||
|
<Link
|
||||||
|
href={`/songs/${song.slug}`}
|
||||||
|
className="flex items-center gap-3 p-3 hover:bg-accent/50 transition-colors"
|
||||||
|
>
|
||||||
|
<span className="text-lg font-bold text-muted-foreground w-6 text-center">
|
||||||
|
{idx + 1}
|
||||||
|
</span>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className="font-medium truncate">{song.title}</div>
|
||||||
|
{song.performance_count && (
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{song.performance_count} performances
|
||||||
</div>
|
</div>
|
||||||
<div>
|
)}
|
||||||
<div className="text-3xl font-bold">1</div>
|
|
||||||
<div className="text-sm text-muted-foreground">Account</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<div className="p-4 text-center text-muted-foreground text-sm">
|
||||||
|
No songs yet
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Activity Feed */}
|
||||||
|
<section className="space-y-4 lg:col-span-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-xl font-bold">Recent Activity</h2>
|
||||||
|
<Link href="/leaderboards" className="text-sm text-muted-foreground hover:underline flex items-center gap-1">
|
||||||
|
View all <ChevronRight className="h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<ActivityFeed />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* XP Leaderboard */}
|
||||||
|
<section className="space-y-4 lg:col-span-1">
|
||||||
|
<XPLeaderboard />
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Quick Links */}
|
||||||
|
<section className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
<Link href="/shows" className="block p-6 border rounded-lg hover:bg-accent transition-colors group">
|
||||||
|
<Calendar className="h-8 w-8 mb-2 text-blue-500 group-hover:scale-110 transition-transform" />
|
||||||
|
<h3 className="font-bold">Shows</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">Browse the complete archive</p>
|
||||||
|
</Link>
|
||||||
|
<Link href="/venues" className="block p-6 border rounded-lg hover:bg-accent transition-colors group">
|
||||||
|
<MapPin className="h-8 w-8 mb-2 text-green-500 group-hover:scale-110 transition-transform" />
|
||||||
|
<h3 className="font-bold">Venues</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">Find your favorite spots</p>
|
||||||
|
</Link>
|
||||||
|
<Link href="/songs" className="block p-6 border rounded-lg hover:bg-accent transition-colors group">
|
||||||
|
<Music className="h-8 w-8 mb-2 text-purple-500 group-hover:scale-110 transition-transform" />
|
||||||
|
<h3 className="font-bold">Songs</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">Explore the catalog</p>
|
||||||
|
</Link>
|
||||||
|
<Link href="/performances" className="block p-6 border rounded-lg hover:bg-accent transition-colors group">
|
||||||
|
<Trophy className="h-8 w-8 mb-2 text-yellow-500 group-hover:scale-110 transition-transform" />
|
||||||
|
<h3 className="font-bold">Top Performances</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">Highest rated jams</p>
|
||||||
|
</Link>
|
||||||
|
<Link href="/leaderboards" className="block p-6 border rounded-lg hover:bg-accent transition-colors group">
|
||||||
|
<Star className="h-8 w-8 mb-2 text-yellow-500 group-hover:scale-110 transition-transform" />
|
||||||
|
<h3 className="font-bold">Leaderboards</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">Top rated everything</p>
|
||||||
|
</Link>
|
||||||
|
<Link href="/tours" className="block p-6 border rounded-lg hover:bg-accent transition-colors group">
|
||||||
|
<Route className="h-8 w-8 mb-2 text-orange-500 group-hover:scale-110 transition-transform" />
|
||||||
|
<h3 className="font-bold">Tours</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">Browse by tour</p>
|
||||||
|
</Link>
|
||||||
|
<Link href="/videos" className="block p-6 border rounded-lg hover:bg-accent transition-colors group">
|
||||||
|
<Youtube className="h-8 w-8 mb-2 text-red-500 group-hover:scale-110 transition-transform" />
|
||||||
|
<h3 className="font-bold">Videos</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">Watch full shows and songs</p>
|
||||||
|
</Link>
|
||||||
</section>
|
</section>
|
||||||
</div >
|
</div >
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -25,19 +25,6 @@ async function getSong(id: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fetch cross-band versions of this song via SongCanon
|
|
||||||
async function getRelatedVersions(songId: number) {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${getApiUrl()}/canon/song/${songId}/related`, {
|
|
||||||
next: { revalidate: 60 }
|
|
||||||
})
|
|
||||||
if (!res.ok) return []
|
|
||||||
return res.json()
|
|
||||||
} catch {
|
|
||||||
return []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get top rated performances for "Heady Version" leaderboard
|
// Get top rated performances for "Heady Version" leaderboard
|
||||||
function getHeadyVersions(performances: any[]) {
|
function getHeadyVersions(performances: any[]) {
|
||||||
if (!performances || performances.length === 0) return []
|
if (!performances || performances.length === 0) return []
|
||||||
|
|
@ -58,9 +45,6 @@ export default async function SongDetailPage({ params }: { params: Promise<{ slu
|
||||||
const headyVersions = getHeadyVersions(song.performances || [])
|
const headyVersions = getHeadyVersions(song.performances || [])
|
||||||
const topPerformance = headyVersions[0]
|
const topPerformance = headyVersions[0]
|
||||||
|
|
||||||
// Fetch cross-band versions
|
|
||||||
const relatedVersions = await getRelatedVersions(song.id)
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-6">
|
<div className="flex flex-col gap-6">
|
||||||
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
<div className="flex flex-col gap-4 md:flex-row md:items-center md:justify-between">
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue