feat: Add container max-width, revamp homepage with real data, add setlist import scripts
This commit is contained in:
parent
f9cdd626f4
commit
8fa04e9690
5 changed files with 587 additions and 35 deletions
128
backend/fast_import_setlists.py
Normal file
128
backend/fast_import_setlists.py
Normal file
|
|
@ -0,0 +1,128 @@
|
||||||
|
"""
|
||||||
|
Fast Setlist Importer - Skips validation checks for speed
|
||||||
|
Run after main import if it times out
|
||||||
|
"""
|
||||||
|
import requests
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
from database import engine
|
||||||
|
from models import Show, Song, Performance
|
||||||
|
|
||||||
|
BASE_URL = "https://elgoose.net/api/v2"
|
||||||
|
|
||||||
|
def fetch_json(endpoint, params=None):
|
||||||
|
url = f"{BASE_URL}/{endpoint}.json"
|
||||||
|
try:
|
||||||
|
response = requests.get(url, params=params, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
if data.get('error') == 1:
|
||||||
|
return None
|
||||||
|
return data.get('data', [])
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("FAST SETLIST IMPORTER")
|
||||||
|
print("=" * 40)
|
||||||
|
|
||||||
|
with Session(engine) as session:
|
||||||
|
# Build lookup maps
|
||||||
|
print("Building show map...")
|
||||||
|
shows = session.exec(select(Show)).all()
|
||||||
|
# Map API show_id to our show_id
|
||||||
|
# We need the API show_id. The 'notes' field might have showtitle but not API id.
|
||||||
|
# Problem: We don't store API show_id in our DB!
|
||||||
|
# We need to re-fetch shows and match by date+venue_id
|
||||||
|
|
||||||
|
show_map = {} # api_show_id -> our_show_id
|
||||||
|
song_map = {} # api_song_id -> our_song_id
|
||||||
|
|
||||||
|
# Fetch shows from API to build map
|
||||||
|
print("Fetching API shows to build map...")
|
||||||
|
page = 1
|
||||||
|
while True:
|
||||||
|
data = fetch_json("shows", {"artist": 1, "page": page})
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
for s in data:
|
||||||
|
api_show_id = s['show_id']
|
||||||
|
show_date = datetime.strptime(s['showdate'], '%Y-%m-%d')
|
||||||
|
# Find matching show in our DB
|
||||||
|
our_show = session.exec(
|
||||||
|
select(Show).where(Show.date == show_date)
|
||||||
|
).first()
|
||||||
|
if our_show:
|
||||||
|
show_map[api_show_id] = our_show.id
|
||||||
|
page += 1
|
||||||
|
if page > 50:
|
||||||
|
break
|
||||||
|
print(f"Mapped {len(show_map)} shows")
|
||||||
|
|
||||||
|
# Build song map
|
||||||
|
print("Building song map...")
|
||||||
|
songs_data = fetch_json("songs")
|
||||||
|
if songs_data:
|
||||||
|
for s in songs_data:
|
||||||
|
api_song_id = s['id']
|
||||||
|
our_song = session.exec(
|
||||||
|
select(Song).where(Song.title == s['name'])
|
||||||
|
).first()
|
||||||
|
if our_song:
|
||||||
|
song_map[api_song_id] = our_song.id
|
||||||
|
print(f"Mapped {len(song_map)} songs")
|
||||||
|
|
||||||
|
# Get existing performances to skip
|
||||||
|
print("Loading existing performances...")
|
||||||
|
existing = set()
|
||||||
|
perfs = session.exec(select(Performance.show_id, Performance.song_id, Performance.position)).all()
|
||||||
|
for p in perfs:
|
||||||
|
existing.add((p[0], p[1], p[2]))
|
||||||
|
print(f"Found {len(existing)} existing performances")
|
||||||
|
|
||||||
|
# Import setlists page by page
|
||||||
|
print("\\nImporting setlists...")
|
||||||
|
page = 1
|
||||||
|
total_added = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
data = fetch_json("setlists", {"page": page})
|
||||||
|
if not data:
|
||||||
|
print(f"No data on page {page}, done.")
|
||||||
|
break
|
||||||
|
|
||||||
|
added_this_page = 0
|
||||||
|
for perf_data in data:
|
||||||
|
our_show_id = show_map.get(perf_data.get('show_id'))
|
||||||
|
our_song_id = song_map.get(perf_data.get('song_id'))
|
||||||
|
position = perf_data.get('position', 0)
|
||||||
|
|
||||||
|
if not our_show_id or not our_song_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
key = (our_show_id, our_song_id, position)
|
||||||
|
if key in existing:
|
||||||
|
continue
|
||||||
|
|
||||||
|
perf = Performance(
|
||||||
|
show_id=our_show_id,
|
||||||
|
song_id=our_song_id,
|
||||||
|
position=position,
|
||||||
|
set_name=perf_data.get('set'),
|
||||||
|
segue=bool(perf_data.get('segue', 0)),
|
||||||
|
notes=perf_data.get('notes')
|
||||||
|
)
|
||||||
|
session.add(perf)
|
||||||
|
existing.add(key)
|
||||||
|
added_this_page += 1
|
||||||
|
total_added += 1
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
print(f"Page {page}: +{added_this_page} ({total_added} total)")
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
print(f"\\n✓ Added {total_added} performances")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
103
backend/import_by_date.py
Normal file
103
backend/import_by_date.py
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
"""
|
||||||
|
Direct Date-Based Setlist Importer
|
||||||
|
Matches setlists to shows by date
|
||||||
|
"""
|
||||||
|
import requests
|
||||||
|
from datetime import datetime
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
from database import engine
|
||||||
|
from models import Show, Song, Performance
|
||||||
|
|
||||||
|
BASE_URL = "https://elgoose.net/api/v2"
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("DATE-BASED SETLIST IMPORTER")
|
||||||
|
print("=" * 40)
|
||||||
|
|
||||||
|
with Session(engine) as session:
|
||||||
|
# Build show map by date
|
||||||
|
shows = session.exec(select(Show)).all()
|
||||||
|
show_by_date = {}
|
||||||
|
for s in shows:
|
||||||
|
date_str = s.date.strftime('%Y-%m-%d')
|
||||||
|
show_by_date[date_str] = s.id
|
||||||
|
print(f"Mapped {len(show_by_date)} shows by date")
|
||||||
|
|
||||||
|
# Build song map by title (lowercase)
|
||||||
|
songs = session.exec(select(Song)).all()
|
||||||
|
song_map = {s.title.lower().strip(): s.id for s in songs}
|
||||||
|
print(f"Mapped {len(song_map)} songs")
|
||||||
|
|
||||||
|
# Get existing performances for dedup
|
||||||
|
existing = set()
|
||||||
|
perfs = session.exec(
|
||||||
|
select(Performance.show_id, Performance.song_id, Performance.position)
|
||||||
|
).all()
|
||||||
|
for p in perfs:
|
||||||
|
existing.add((p[0], p[1], p[2]))
|
||||||
|
print(f"Found {len(existing)} existing performances")
|
||||||
|
|
||||||
|
# Fetch setlists page by page
|
||||||
|
print("\\nImporting setlists...")
|
||||||
|
page = 1
|
||||||
|
total_added = 0
|
||||||
|
|
||||||
|
while page <= 200: # Safety limit
|
||||||
|
print(f" Page {page}...", end="", flush=True)
|
||||||
|
|
||||||
|
url = f"{BASE_URL}/setlists.json"
|
||||||
|
try:
|
||||||
|
resp = requests.get(url, params={"page": page}, timeout=60)
|
||||||
|
data = resp.json().get('data', [])
|
||||||
|
except Exception as e:
|
||||||
|
print(f" Error: {e}")
|
||||||
|
break
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
print(" Done (no data)")
|
||||||
|
break
|
||||||
|
|
||||||
|
added_this_page = 0
|
||||||
|
for item in data:
|
||||||
|
# Get showdate and match to our shows
|
||||||
|
showdate = item.get('showdate')
|
||||||
|
if not showdate:
|
||||||
|
continue
|
||||||
|
|
||||||
|
our_show_id = show_by_date.get(showdate)
|
||||||
|
if not our_show_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Match song
|
||||||
|
song_name = (item.get('songname') or '').lower().strip()
|
||||||
|
song_id = song_map.get(song_name)
|
||||||
|
if not song_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
position = item.get('position', 0)
|
||||||
|
key = (our_show_id, song_id, position)
|
||||||
|
|
||||||
|
if key in existing:
|
||||||
|
continue
|
||||||
|
|
||||||
|
perf = Performance(
|
||||||
|
show_id=our_show_id,
|
||||||
|
song_id=song_id,
|
||||||
|
position=position,
|
||||||
|
set_name=item.get('setnumber'),
|
||||||
|
segue=(item.get('transition', ', ') != ', '),
|
||||||
|
notes=item.get('footnote')
|
||||||
|
)
|
||||||
|
session.add(perf)
|
||||||
|
existing.add(key)
|
||||||
|
added_this_page += 1
|
||||||
|
total_added += 1
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
print(f" +{added_this_page} ({total_added} total)")
|
||||||
|
page += 1
|
||||||
|
|
||||||
|
print(f"\\n✓ Added {total_added} performances")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
131
backend/import_per_show.py
Normal file
131
backend/import_per_show.py
Normal file
|
|
@ -0,0 +1,131 @@
|
||||||
|
"""
|
||||||
|
Per-Show Setlist Importer
|
||||||
|
Fetches setlist for each show individually
|
||||||
|
"""
|
||||||
|
import requests
|
||||||
|
from sqlmodel import Session, select
|
||||||
|
from database import engine
|
||||||
|
from models import Show, Song, Performance
|
||||||
|
|
||||||
|
BASE_URL = "https://elgoose.net/api/v2"
|
||||||
|
|
||||||
|
def fetch_show_setlist(api_show_id):
|
||||||
|
"""Fetch setlist for a specific show"""
|
||||||
|
url = f"{BASE_URL}/setlists/showid/{api_show_id}.json"
|
||||||
|
try:
|
||||||
|
response = requests.get(url, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
data = response.json()
|
||||||
|
if data.get('error') == 1:
|
||||||
|
return None
|
||||||
|
return data.get('data', [])
|
||||||
|
except Exception as e:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print("PER-SHOW SETLIST IMPORTER")
|
||||||
|
print("=" * 40)
|
||||||
|
|
||||||
|
with Session(engine) as session:
|
||||||
|
# Get all shows
|
||||||
|
shows = session.exec(select(Show)).all()
|
||||||
|
print(f"Found {len(shows)} shows in database")
|
||||||
|
|
||||||
|
# Build song map by title
|
||||||
|
songs = session.exec(select(Song)).all()
|
||||||
|
song_map = {s.title.lower(): s.id for s in songs}
|
||||||
|
print(f"Mapped {len(song_map)} songs")
|
||||||
|
|
||||||
|
# Get existing performances
|
||||||
|
existing = set()
|
||||||
|
perfs = session.exec(
|
||||||
|
select(Performance.show_id, Performance.song_id, Performance.position)
|
||||||
|
).all()
|
||||||
|
for p in perfs:
|
||||||
|
existing.add((p[0], p[1], p[2]))
|
||||||
|
print(f"Found {len(existing)} existing performances")
|
||||||
|
|
||||||
|
# We need API show IDs. The ElGoose API shows endpoint returns show_id.
|
||||||
|
# Let's fetch and correlate by date
|
||||||
|
print("Fetching API shows to get API IDs...")
|
||||||
|
api_shows = {} # date_str -> api_show_id
|
||||||
|
|
||||||
|
page = 1
|
||||||
|
while True:
|
||||||
|
url = f"{BASE_URL}/shows.json"
|
||||||
|
try:
|
||||||
|
resp = requests.get(url, params={"artist": 1, "page": page}, timeout=30)
|
||||||
|
data = resp.json().get('data', [])
|
||||||
|
if not data:
|
||||||
|
break
|
||||||
|
for s in data:
|
||||||
|
date_str = s['showdate']
|
||||||
|
api_shows[date_str] = s['show_id']
|
||||||
|
page += 1
|
||||||
|
if page > 50:
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
break
|
||||||
|
|
||||||
|
print(f"Got {len(api_shows)} API show IDs")
|
||||||
|
|
||||||
|
# Now import setlists for each show
|
||||||
|
total_added = 0
|
||||||
|
processed = 0
|
||||||
|
|
||||||
|
for show in shows:
|
||||||
|
date_str = show.date.strftime('%Y-%m-%d')
|
||||||
|
api_show_id = api_shows.get(date_str)
|
||||||
|
|
||||||
|
if not api_show_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check if we already have performances for this show
|
||||||
|
existing_for_show = session.exec(
|
||||||
|
select(Performance).where(Performance.show_id == show.id)
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if existing_for_show:
|
||||||
|
continue # Skip shows that already have performances
|
||||||
|
|
||||||
|
# Fetch setlist
|
||||||
|
setlist = fetch_show_setlist(api_show_id)
|
||||||
|
if not setlist:
|
||||||
|
continue
|
||||||
|
|
||||||
|
added = 0
|
||||||
|
for item in setlist:
|
||||||
|
song_title = item.get('songname', '').lower()
|
||||||
|
song_id = song_map.get(song_title)
|
||||||
|
|
||||||
|
if not song_id:
|
||||||
|
continue
|
||||||
|
|
||||||
|
position = item.get('position', 0)
|
||||||
|
key = (show.id, song_id, position)
|
||||||
|
|
||||||
|
if key in existing:
|
||||||
|
continue
|
||||||
|
|
||||||
|
perf = Performance(
|
||||||
|
show_id=show.id,
|
||||||
|
song_id=song_id,
|
||||||
|
position=position,
|
||||||
|
set_name=item.get('set'),
|
||||||
|
segue=bool(item.get('segue', 0)),
|
||||||
|
notes=item.get('footnote')
|
||||||
|
)
|
||||||
|
session.add(perf)
|
||||||
|
existing.add(key)
|
||||||
|
added += 1
|
||||||
|
total_added += 1
|
||||||
|
|
||||||
|
if added > 0:
|
||||||
|
session.commit()
|
||||||
|
processed += 1
|
||||||
|
print(f"Show {date_str}: +{added} songs ({total_added} total)")
|
||||||
|
|
||||||
|
print(f"\\n✓ Added {total_added} performances from {processed} shows")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
|
@ -120,3 +120,27 @@
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Container constraints for large screens */
|
||||||
|
.container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 1280px;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
padding-left: 1.5rem;
|
||||||
|
padding-right: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 640px) {
|
||||||
|
.container {
|
||||||
|
padding-left: 2rem;
|
||||||
|
padding-right: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.container {
|
||||||
|
padding-left: 4rem;
|
||||||
|
padding-right: 4rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,81 @@
|
||||||
import { ActivityFeed } from "@/components/feed/activity-feed"
|
import { ActivityFeed } from "@/components/feed/activity-feed"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { Trophy, Music, MapPin, Users } from "lucide-react"
|
import { Trophy, Music, MapPin, Calendar, ChevronRight, Star } from "lucide-react"
|
||||||
|
import { getApiUrl } from "@/lib/api-config"
|
||||||
|
|
||||||
|
interface Show {
|
||||||
|
id: number
|
||||||
|
date: string
|
||||||
|
venue?: {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
city?: string
|
||||||
|
state?: string
|
||||||
|
}
|
||||||
|
tour?: {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Song {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
times_played?: number
|
||||||
|
avg_rating?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getRecentShows(): Promise<Show[]> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${getApiUrl()}/shows?limit=8&sort=date&order=desc`, {
|
||||||
|
cache: 'no-store',
|
||||||
|
next: { revalidate: 60 }
|
||||||
|
})
|
||||||
|
if (!res.ok) return []
|
||||||
|
return res.json()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to fetch recent shows:', e)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getTopSongs(): Promise<Song[]> {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${getApiUrl()}/songs?limit=5&sort=times_played&order=desc`, {
|
||||||
|
cache: 'no-store',
|
||||||
|
next: { revalidate: 300 }
|
||||||
|
})
|
||||||
|
if (!res.ok) return []
|
||||||
|
return res.json()
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Failed to fetch top songs:', e)
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getStats() {
|
||||||
|
try {
|
||||||
|
const [showsRes, songsRes, venuesRes] = await Promise.all([
|
||||||
|
fetch(`${getApiUrl()}/shows?limit=1`, { cache: 'no-store' }),
|
||||||
|
fetch(`${getApiUrl()}/songs?limit=1`, { cache: 'no-store' }),
|
||||||
|
fetch(`${getApiUrl()}/venues?limit=1`, { cache: 'no-store' })
|
||||||
|
])
|
||||||
|
// These endpoints return arrays, we need to get counts differently
|
||||||
|
// For now we'll just show the data we have
|
||||||
|
return { shows: 0, songs: 0, venues: 0 }
|
||||||
|
} catch (e) {
|
||||||
|
return { shows: 0, songs: 0, venues: 0 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Home() {
|
||||||
|
const [recentShows, topSongs] = await Promise.all([
|
||||||
|
getRecentShows(),
|
||||||
|
getTopSongs()
|
||||||
|
])
|
||||||
|
|
||||||
export default function Home() {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-8">
|
<div className="flex flex-col gap-8">
|
||||||
{/* Hero Section */}
|
{/* Hero Section */}
|
||||||
|
|
@ -31,45 +103,139 @@ export default function Home() {
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<div className="grid gap-8 md:grid-cols-2">
|
{/* Recent Shows */}
|
||||||
{/* Activity Feed */}
|
|
||||||
<section className="space-y-4">
|
<section className="space-y-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<h2 className="text-2xl font-bold">Recent Activity</h2>
|
<h2 className="text-2xl font-bold flex items-center gap-2">
|
||||||
<Link href="/leaderboards" className="text-sm text-muted-foreground hover:underline">
|
<Calendar className="h-6 w-6 text-blue-500" />
|
||||||
View all activity
|
Recent Shows
|
||||||
|
</h2>
|
||||||
|
<Link href="/shows" className="text-sm text-muted-foreground hover:underline flex items-center gap-1">
|
||||||
|
View all shows <ChevronRight className="h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
{recentShows.length > 0 ? (
|
||||||
|
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
|
{recentShows.map((show) => (
|
||||||
|
<Link key={show.id} href={`/shows/${show.id}`}>
|
||||||
|
<Card className="h-full hover:bg-accent/50 transition-colors cursor-pointer">
|
||||||
|
<CardContent className="p-4">
|
||||||
|
<div className="font-semibold">
|
||||||
|
{new Date(show.date).toLocaleDateString('en-US', {
|
||||||
|
weekday: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
})}
|
||||||
|
</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>
|
||||||
|
</Card>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Card className="p-8 text-center text-muted-foreground">
|
||||||
|
<p>No shows yet. Check back soon!</p>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<div className="grid gap-8 lg:grid-cols-3">
|
||||||
|
{/* Top Songs */}
|
||||||
|
<section className="space-y-4 lg:col-span-1">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2 className="text-xl font-bold flex items-center gap-2">
|
||||||
|
<Star className="h-5 w-5 text-yellow-500" />
|
||||||
|
Top Songs
|
||||||
|
</h2>
|
||||||
|
<Link href="/songs" className="text-sm text-muted-foreground hover:underline flex items-center gap-1">
|
||||||
|
All songs <ChevronRight className="h-4 w-4" />
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<Card>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
{topSongs.length > 0 ? (
|
||||||
|
<ul className="divide-y">
|
||||||
|
{topSongs.map((song, idx) => (
|
||||||
|
<li key={song.id}>
|
||||||
|
<Link
|
||||||
|
href={`/songs/${song.id}`}
|
||||||
|
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.times_played && (
|
||||||
|
<div className="text-xs text-muted-foreground">
|
||||||
|
{song.times_played} performances
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<div className="p-4 text-center text-muted-foreground text-sm">
|
||||||
|
No songs yet
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Activity Feed */}
|
||||||
|
<section className="space-y-4 lg:col-span-2">
|
||||||
|
<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>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<ActivityFeed />
|
<ActivityFeed />
|
||||||
</section>
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Quick Stats / Leaderboard Preview */}
|
{/* Quick Links */}
|
||||||
<section className="space-y-4">
|
<section className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
||||||
<h2 className="text-2xl font-bold">Explore</h2>
|
<Link href="/shows" className="block p-6 border rounded-xl hover:bg-accent transition-colors group">
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<Music className="h-8 w-8 mb-2 text-blue-500 group-hover:scale-110 transition-transform" />
|
||||||
<Link href="/shows" className="block p-6 border rounded-xl hover:bg-accent transition-colors">
|
|
||||||
<Music className="h-8 w-8 mb-2 text-blue-500" />
|
|
||||||
<h3 className="font-bold">Shows</h3>
|
<h3 className="font-bold">Shows</h3>
|
||||||
<p className="text-sm text-muted-foreground">Browse the archive</p>
|
<p className="text-sm text-muted-foreground">Browse the complete archive</p>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/venues" className="block p-6 border rounded-xl hover:bg-accent transition-colors">
|
<Link href="/venues" className="block p-6 border rounded-xl hover:bg-accent transition-colors group">
|
||||||
<MapPin className="h-8 w-8 mb-2 text-green-500" />
|
<MapPin className="h-8 w-8 mb-2 text-green-500 group-hover:scale-110 transition-transform" />
|
||||||
<h3 className="font-bold">Venues</h3>
|
<h3 className="font-bold">Venues</h3>
|
||||||
<p className="text-sm text-muted-foreground">Find your favorite spots</p>
|
<p className="text-sm text-muted-foreground">Find your favorite spots</p>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/leaderboards" className="block p-6 border rounded-xl hover:bg-accent transition-colors">
|
<Link href="/songs" className="block p-6 border rounded-xl hover:bg-accent transition-colors group">
|
||||||
<Users className="h-8 w-8 mb-2 text-purple-500" />
|
<Music className="h-8 w-8 mb-2 text-purple-500 group-hover:scale-110 transition-transform" />
|
||||||
<h3 className="font-bold">Community</h3>
|
<h3 className="font-bold">Songs</h3>
|
||||||
<p className="text-sm text-muted-foreground">Join the conversation</p>
|
<p className="text-sm text-muted-foreground">Explore the catalog</p>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/leaderboards" className="block p-6 border rounded-xl hover:bg-accent transition-colors">
|
<Link href="/leaderboards" className="block p-6 border rounded-xl hover:bg-accent transition-colors group">
|
||||||
<Trophy className="h-8 w-8 mb-2 text-yellow-500" />
|
<Trophy className="h-8 w-8 mb-2 text-yellow-500 group-hover:scale-110 transition-transform" />
|
||||||
<h3 className="font-bold">Leaderboards</h3>
|
<h3 className="font-bold">Leaderboards</h3>
|
||||||
<p className="text-sm text-muted-foreground">Top rated everything</p>
|
<p className="text-sm text-muted-foreground">Top rated everything</p>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue