From be57110de8eaf64b325b07d10fe5143ed491d9ac Mon Sep 17 00:00:00 2001 From: fullsizemalt <106900403+fullsizemalt@users.noreply.github.com> Date: Wed, 24 Dec 2025 16:18:45 -0800 Subject: [PATCH] feat: Tour pages - add show counts, year grouping, stats card --- backend/routers/tours.py | 44 +++++++++++++---- frontend/app/tours/[slug]/page.tsx | 77 +++++++++++++++++++++++------- frontend/app/tours/page.tsx | 69 ++++++++++++++++++-------- 3 files changed, 145 insertions(+), 45 deletions(-) diff --git a/backend/routers/tours.py b/backend/routers/tours.py index 38e2f5d..33b3250 100644 --- a/backend/routers/tours.py +++ b/backend/routers/tours.py @@ -1,8 +1,8 @@ from typing import List from fastapi import APIRouter, Depends, HTTPException, Query -from sqlmodel import Session, select +from sqlmodel import Session, select, func from database import get_session -from models import Tour, User +from models import Tour, Show, User from schemas import TourCreate, TourRead, TourUpdate from auth import get_current_user @@ -20,18 +20,46 @@ def create_tour( session.refresh(db_tour) return db_tour -@router.get("/", response_model=List[TourRead]) +@router.get("/") def read_tours( offset: int = 0, - limit: int = Query(default=100, le=100), + limit: int = Query(default=100, le=500), session: Session = Depends(get_session) ): + """Get all tours with show counts""" tours = session.exec(select(Tour).offset(offset).limit(limit)).all() - return tours + + # Get show counts per tour + result = [] + for tour in tours: + show_count = session.exec( + select(func.count(Show.id)).where(Show.tour_id == tour.id) + ).one() + tour_dict = tour.model_dump() + tour_dict["show_count"] = show_count + result.append(tour_dict) + + return result -@router.get("/{slug}", response_model=TourRead) +@router.get("/{slug}") def read_tour(slug: str, session: Session = Depends(get_session)): - tour = session.exec(select(Tour).where(Tour.slug == slug)).first() + """Get tour by slug or ID""" + # Try as ID first (for backwards compatibility) + try: + tour_id = int(slug) + tour = session.get(Tour, tour_id) + except ValueError: + tour = session.exec(select(Tour).where(Tour.slug == slug)).first() + if not tour: raise HTTPException(status_code=404, detail="Tour not found") - return tour + + # Add show count + show_count = session.exec( + select(func.count(Show.id)).where(Show.tour_id == tour.id) + ).one() + tour_dict = tour.model_dump() + tour_dict["show_count"] = show_count + + return tour_dict + diff --git a/frontend/app/tours/[slug]/page.tsx b/frontend/app/tours/[slug]/page.tsx index bceab0d..1abe9f8 100644 --- a/frontend/app/tours/[slug]/page.tsx +++ b/frontend/app/tours/[slug]/page.tsx @@ -1,6 +1,7 @@ import { Button } from "@/components/ui/button" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" -import { ArrowLeft, Calendar, Music2 } from "lucide-react" +import { Badge } from "@/components/ui/badge" +import { ArrowLeft, Calendar, MapPin, Hash } from "lucide-react" import Link from "next/link" import { notFound } from "next/navigation" import { getApiUrl } from "@/lib/api-config" @@ -9,6 +10,17 @@ import { EntityRating } from "@/components/social/entity-rating" import { EntityReviews } from "@/components/reviews/entity-reviews" import { SocialWrapper } from "@/components/social/social-wrapper" +interface ShowWithVenue { + id: number + slug: string + date: string + venue?: { + name: string + city?: string + state?: string + } +} + async function getTour(id: string) { try { const res = await fetch(`${getApiUrl()}/tours/${id}`, { cache: 'no-store' }) @@ -41,11 +53,15 @@ export default async function TourDetailPage({ params }: { params: Promise<{ slu const shows = await getTourShows(tour.id) + // Calculate stats + const uniqueStates = [...new Set(shows.filter((s: ShowWithVenue) => s.venue?.state).map((s: ShowWithVenue) => s.venue!.state))] + const uniqueCities = [...new Set(shows.filter((s: ShowWithVenue) => s.venue?.city).map((s: ShowWithVenue) => s.venue!.city))] + return (
- + @@ -54,8 +70,8 @@ export default async function TourDetailPage({ params }: { params: Promise<{ slu

{tour.name}

- {tour.start_date ? new Date(tour.start_date).toLocaleDateString() : "Unknown"} - {tour.end_date && ` - ${new Date(tour.end_date).toLocaleDateString()}`} + {tour.start_date ? new Date(tour.start_date).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }) : "Unknown"} + {tour.end_date && ` - ${new Date(tour.end_date).toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" })}`}

@@ -74,19 +90,19 @@ export default async function TourDetailPage({ params }: { params: Promise<{ slu {shows.length > 0 ? (
{[...shows] - .sort((a: any, b: any) => new Date(a.date).getTime() - new Date(b.date).getTime()) - .map((show: any) => ( + .sort((a: ShowWithVenue, b: ShowWithVenue) => new Date(a.date).getTime() - new Date(b.date).getTime()) + .map((show: ShowWithVenue) => ( -
+
- {new Date(show.date).toLocaleDateString()} + {new Date(show.date).toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" })}
{show.venue && ( - - {show.venue.name}, {show.venue.city} + + {show.venue.name}, {show.venue.city}{show.venue.state ? `, ${show.venue.state}` : ""} )}
@@ -109,18 +125,47 @@ export default async function TourDetailPage({ params }: { params: Promise<{ slu
+ {/* Tour Stats */} - - Tour Details + + Tour Stats - - {tour.notes && ( -

{tour.notes}

- )} + +
+ + Shows + + {shows.length} +
+
+ + Cities + + {uniqueCities.length} +
+
+ + States + + {uniqueStates.length} +
+ + {/* Tour Notes */} + {tour.notes && ( + + + Notes + + +

{tour.notes}

+
+
+ )}
) } + diff --git a/frontend/app/tours/page.tsx b/frontend/app/tours/page.tsx index 9151ea8..1815300 100644 --- a/frontend/app/tours/page.tsx +++ b/frontend/app/tours/page.tsx @@ -3,14 +3,17 @@ import { useEffect, useState } from "react" import { getApiUrl } from "@/lib/api-config" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Badge } from "@/components/ui/badge" import Link from "next/link" -import { Map } from "lucide-react" +import { Map, Calendar } from "lucide-react" interface Tour { id: number name: string + slug: string start_date: string end_date: string + show_count: number } export default function ToursPage() { @@ -18,7 +21,7 @@ export default function ToursPage() { const [loading, setLoading] = useState(true) useEffect(() => { - fetch(`${getApiUrl()}/tours/?limit=100`) + fetch(`${getApiUrl()}/tours/?limit=200`) .then(res => res.json()) .then(data => { // Sort by start date descending @@ -31,6 +34,16 @@ export default function ToursPage() { .finally(() => setLoading(false)) }, []) + // Group tours by year + const toursByYear = tours.reduce((acc, tour) => { + const year = new Date(tour.start_date).getFullYear() + if (!acc[year]) acc[year] = [] + acc[year].push(tour) + return acc + }, {} as Record) + + const years = Object.keys(toursByYear).map(Number).sort((a, b) => b - a) + if (loading) return
Loading tours...
return ( @@ -42,25 +55,39 @@ export default function ToursPage() {

-
- {tours.map((tour) => ( - - - - - - {tour.name} - - - -

- {new Date(tour.start_date).toLocaleDateString()} - {new Date(tour.end_date).toLocaleDateString()} -

-
-
- - ))} -
+ {years.map(year => ( +
+

{year}

+
+ {toursByYear[year].map((tour) => ( + + + + + + + {tour.name} + + + {tour.show_count} shows + + + + +

+ + {new Date(tour.start_date).toLocaleDateString("en-US", { month: "short", day: "numeric" })} + {" - "} + {new Date(tour.end_date).toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })} +

+
+
+ + ))} +
+
+ ))} ) } +