diff --git a/frontend/app/[vertical]/page.tsx b/frontend/app/[vertical]/page.tsx index da9d33a..db49a10 100644 --- a/frontend/app/[vertical]/page.tsx +++ b/frontend/app/[vertical]/page.tsx @@ -2,10 +2,11 @@ import { notFound } from "next/navigation" import { VERTICALS } from "@/config/verticals" import { getApiUrl } from "@/lib/api-config" import Link from "next/link" -import { Calendar, MapPin, Music, Trophy, Video, Ticket, Building, ChevronRight, Play } from "lucide-react" +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 { Show, Song, PaginatedResponse } from "@/types/models" interface Props { params: Promise<{ vertical: string }> @@ -17,25 +18,26 @@ export function generateStaticParams() { })) } -async function getRecentShows(verticalSlug: string) { +async function getRecentShows(verticalSlug: string): Promise { try { const res = await fetch(`${getApiUrl()}/shows/?vertical_slugs=${verticalSlug}&limit=8&status=past`, { next: { revalidate: 60 } }) if (!res.ok) return [] - return res.json() + const data: PaginatedResponse = await res.json() + return data.data || [] } catch { return [] } } -async function getTopSongs(verticalSlug: string) { +async function getTopSongs(verticalSlug: string): Promise { try { const res = await fetch(`${getApiUrl()}/songs/?vertical=${verticalSlug}&limit=5&sort=times_played`, { next: { revalidate: 60 } }) if (!res.ok) return [] - const data = await res.json() + const data: PaginatedResponse = await res.json() return data.data || [] } catch { return [] @@ -104,7 +106,7 @@ export default async function VerticalPage({ params }: Props) {
- {recentShows.slice(0, 8).map((show: any) => ( + {recentShows.slice(0, 8).map((show) => ( {show.venue?.city}, {show.venue?.state || show.venue?.country}
- {show.tour?.name && ( + {/* Tour is not in strict Show model yet, omitting for strictness or need to add to model */} + {/* {show.tour?.name && (
{show.tour.name}
- )} + )} */} @@ -150,7 +153,7 @@ export default async function VerticalPage({ params }: Props) {
- {topSongs.slice(0, 5).map((song: any, index: number) => ( + {topSongs.slice(0, 5).map((song, index) => (
- {song.times_played || song.performance_count || 0} performances + {song.times_played || 0} performances
diff --git a/frontend/app/admin/shows/page.tsx b/frontend/app/admin/shows/page.tsx index f1a2c28..3a228ce 100644 --- a/frontend/app/admin/shows/page.tsx +++ b/frontend/app/admin/shows/page.tsx @@ -69,7 +69,7 @@ export default function AdminShowsPage() { }) if (res.ok) { const data = await res.json() - setShows(data.shows || data) + setShows(data.data || []) } } catch (e) { console.error("Failed to fetch shows", e) diff --git a/frontend/app/shows/page.tsx b/frontend/app/shows/page.tsx index a1440b9..a948955 100644 --- a/frontend/app/shows/page.tsx +++ b/frontend/app/shows/page.tsx @@ -9,32 +9,7 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs" import { DateGroupedList } from "@/components/shows/date-grouped-list" import { FilterPills } from "@/components/shows/filter-pills" import { BandGrid } from "@/components/shows/band-grid" - -interface Show { - id: number - slug?: string - date: string - youtube_link?: string - vertical?: { - name: string - slug: string - } - venue: { - id: number - name: string - city: string - state: string - } - performances?: any[] // Simplified -} - -interface Vertical { - id: number - slug: string - name: string - show_count: number - logo_url?: string | null -} +import { Show, Vertical, PaginatedResponse, Venue } from "@/types/models" function ShowsContent() { const searchParams = useSearchParams() @@ -111,15 +86,13 @@ function ShowsContent() { .then(res => { // If 401 (Unauthorized) for My Feed, we might get empty list or error if (res.status === 401 && activeView === "my-feed") { - // Redirect to login or handle? - // For now, shows API returns [] if anon try to filter by tiers. - return [] + return { data: [] as Show[], meta: { total: 0, limit: 0, offset: 0 } } } if (!res.ok) throw new Error("Failed to fetch shows") return res.json() }) - .then(data => { - setShows(data) + .then((data: PaginatedResponse) => { + setShows(data.data || []) setLoading(false) }) .catch(err => { diff --git a/frontend/app/songs/page.tsx b/frontend/app/songs/page.tsx index a769b3b..4bc0ec7 100644 --- a/frontend/app/songs/page.tsx +++ b/frontend/app/songs/page.tsx @@ -5,13 +5,7 @@ import { getApiUrl } from "@/lib/api-config" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import Link from "next/link" import { Music } from "lucide-react" - -interface Song { - id: number - title: string - slug?: string - original_artist?: string -} +import { Song, PaginatedResponse } from "@/types/models" export default function SongsPage() { const [songs, setSongs] = useState([]) @@ -20,11 +14,11 @@ export default function SongsPage() { useEffect(() => { fetch(`${getApiUrl()}/songs/?limit=1000`) .then(res => res.json()) - .then(data => { + .then((data: PaginatedResponse) => { // Handle envelope const songData = data.data || [] // Sort alphabetically - const sorted = songData.sort((a: Song, b: Song) => a.title.localeCompare(b.title)) + const sorted = songData.sort((a, b) => a.title.localeCompare(b.title)) setSongs(sorted) }) .catch(console.error) diff --git a/frontend/app/venues/[slug]/page.tsx b/frontend/app/venues/[slug]/page.tsx index bd13eda..ad636b3 100644 --- a/frontend/app/venues/[slug]/page.tsx +++ b/frontend/app/venues/[slug]/page.tsx @@ -54,7 +54,8 @@ export default function VenueDetailPage() { // Fetch shows at this venue using numeric ID const showsRes = await fetch(`${getApiUrl()}/shows/?venue_id=${venueData.id}&limit=100`) if (showsRes.ok) { - const showsData = await showsRes.json() + const showsEnvelope = await showsRes.json() + const showsData = showsEnvelope.data || [] // Sort by date descending showsData.sort((a: Show, b: Show) => new Date(b.date).getTime() - new Date(a.date).getTime() diff --git a/frontend/app/welcome/page.tsx b/frontend/app/welcome/page.tsx index 61364c2..a5ae02a 100644 --- a/frontend/app/welcome/page.tsx +++ b/frontend/app/welcome/page.tsx @@ -180,7 +180,7 @@ export default function WelcomePage() { setWikiMode(e.target.checked)} + onCheckedChange={(checked) => setWikiMode(checked)} /> diff --git a/frontend/components/layout/footer.tsx b/frontend/components/layout/footer.tsx index 2a4fc31..df6bee5 100644 --- a/frontend/components/layout/footer.tsx +++ b/frontend/components/layout/footer.tsx @@ -75,11 +75,11 @@ const DEFAULT_LINKS = { } export function Footer() { - const { currentVertical } = useVertical() + const { current } = useVertical() // Get links for current vertical or fallback - const links = (currentVertical && VERTICAL_LINKS[currentVertical.slug]) - ? VERTICAL_LINKS[currentVertical.slug] + const links = (current && VERTICAL_LINKS[current.slug]) + ? VERTICAL_LINKS[current.slug] : DEFAULT_LINKS return ( @@ -142,7 +142,7 @@ export function Footer() { {/* Brand & Copyright */}
- {currentVertical ? currentVertical.name : "Fediversion"} + {current ? current.name : "Fediversion"} © {new Date().getFullYear()} All rights reserved diff --git a/frontend/components/profile/profile-poster.tsx b/frontend/components/profile/profile-poster.tsx index c2eef05..97c4cd9 100644 --- a/frontend/components/profile/profile-poster.tsx +++ b/frontend/components/profile/profile-poster.tsx @@ -1,6 +1,6 @@ "use client" -import { PublicProfileRead } from "@/lib/types" // We'll need to define this or import generic +// import { PublicProfileRead } from "@/lib/types" // We'll need to define this or import generic import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" diff --git a/frontend/components/profile/user-playlists-list.tsx b/frontend/components/profile/user-playlists-list.tsx index c94d9a6..dcae0b8 100644 --- a/frontend/components/profile/user-playlists-list.tsx +++ b/frontend/components/profile/user-playlists-list.tsx @@ -7,9 +7,10 @@ import Link from "next/link" import { Button } from "@/components/ui/button" import { getApiUrl } from "@/lib/api-config" import { Skeleton } from "@/components/ui/skeleton" +import { Playlist } from "@/types/models" export function UserPlaylistsList({ userId, isOwner = false }: { userId: number, isOwner?: boolean }) { - const [playlists, setPlaylists] = useState([]) + const [playlists, setPlaylists] = useState([]) const [loading, setLoading] = useState(true) useEffect(() => { diff --git a/frontend/components/shows/band-grid.tsx b/frontend/components/shows/band-grid.tsx index d0f7954..50fc89c 100644 --- a/frontend/components/shows/band-grid.tsx +++ b/frontend/components/shows/band-grid.tsx @@ -3,14 +3,7 @@ import { Card, CardContent } from "@/components/ui/card" import { Music2, Check } from "lucide-react" import { Badge } from "@/components/ui/badge" - -interface Vertical { - id: number - slug: string - name: string - show_count: number - logo_url?: string | null -} +import { Vertical } from "@/types/models" interface BandGridProps { verticals: Vertical[] @@ -54,7 +47,7 @@ export function BandGrid({ verticals, selectedBands, onToggle }: BandGridProps)

{v.name}

- {v.show_count.toLocaleString()} shows + {(v.show_count || 0).toLocaleString()} shows

diff --git a/frontend/components/shows/date-grouped-list.tsx b/frontend/components/shows/date-grouped-list.tsx index b159d88..e10408e 100644 --- a/frontend/components/shows/date-grouped-list.tsx +++ b/frontend/components/shows/date-grouped-list.tsx @@ -4,24 +4,7 @@ import Link from "next/link" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Calendar, MapPin, Youtube, Music2 } from "lucide-react" -import { Button } from "@/components/ui/button" - -interface Show { - id: number - slug?: string - date: string - youtube_link?: string - vertical?: { - name: string - slug: string - } - venue: { - id: number - name: string - city: string - state: string - } -} +import { Show } from "@/types/models" interface DateGroupedListProps { shows: Show[] diff --git a/frontend/components/songs/performance-list.tsx b/frontend/components/songs/performance-list.tsx index b65dd11..bf132c5 100644 --- a/frontend/components/songs/performance-list.tsx +++ b/frontend/components/songs/performance-list.tsx @@ -28,6 +28,7 @@ export interface Performance { interface PerformanceListProps { performances: Performance[] + songTitle?: string } type SortOption = "date_desc" | "date_asc" | "rating_desc" diff --git a/frontend/components/ui/button.tsx b/frontend/components/ui/button.tsx index 6a31bd4..7197080 100644 --- a/frontend/components/ui/button.tsx +++ b/frontend/components/ui/button.tsx @@ -26,6 +26,7 @@ const buttonVariants = cva( default: "h-10 px-4 py-2", sm: "h-9 rounded-md px-3", lg: "h-11 rounded-md px-8", + xl: "h-14 px-8 text-lg rounded-full", icon: "h-10 w-10", }, }, diff --git a/frontend/components/ui/command.tsx b/frontend/components/ui/command.tsx index 7fe3881..dc727c2 100644 --- a/frontend/components/ui/command.tsx +++ b/frontend/components/ui/command.tsx @@ -34,6 +34,7 @@ interface CommandDialogProps extends React.ComponentPropsWithoutRef showCloseButton?: boolean + className?: string } function CommandDialog({ diff --git a/frontend/components/ui/rating-input.tsx b/frontend/components/ui/rating-input.tsx index a4346bb..69b1322 100644 --- a/frontend/components/ui/rating-input.tsx +++ b/frontend/components/ui/rating-input.tsx @@ -1,6 +1,7 @@ "use client" import { useState, useRef, useEffect } from "react" +import { StarRating } from "@/components/ui/star-rating" import { Star } from "lucide-react" import { cn } from "@/lib/utils" @@ -62,40 +63,21 @@ export function RatingInput({ } } - // Visual star representation (readonly display) - const renderStars = () => { - const stars = [] - const fullStars = Math.floor(localValue) - const partialFill = (localValue - fullStars) * 100 - - for (let i = 0; i < 10; i++) { - const isFull = i < fullStars - const isPartial = i === fullStars && partialFill > 0 - - stars.push( -
- - {(isFull || isPartial) && ( -
- -
- )} -
- ) - } - return stars - } + // Use StarRating for visual display + const renderStars = () => ( + { + if (!readonly) { + setLocalValue(v) + onChange?.(v) + } + }} + precision="decimal" + /> + ) if (readonly) { return ( diff --git a/frontend/components/ui/user-avatar.tsx b/frontend/components/ui/user-avatar.tsx index e7fbebd..6429c1b 100644 --- a/frontend/components/ui/user-avatar.tsx +++ b/frontend/components/ui/user-avatar.tsx @@ -1,6 +1,5 @@ -"use client" - import { cn } from "@/lib/utils" +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" interface UserAvatarProps { bgColor?: string @@ -8,6 +7,7 @@ interface UserAvatarProps { username?: string size?: "sm" | "md" | "lg" | "xl" className?: string + src?: string | null } const sizeClasses = { @@ -22,21 +22,21 @@ export function UserAvatar({ text, username = "", size = "md", - className + className, + src }: UserAvatarProps) { // If no custom text, use first letter of username const displayText = text || username.charAt(0).toUpperCase() || "?" return ( -
- {displayText} -
+ + + + {displayText} + + ) } diff --git a/frontend/contexts/auth-context.tsx b/frontend/contexts/auth-context.tsx index 78e5711..c68cb34 100644 --- a/frontend/contexts/auth-context.tsx +++ b/frontend/contexts/auth-context.tsx @@ -17,6 +17,8 @@ interface AuthContextType { loading: boolean login: (token: string) => Promise logout: () => void + refreshUser: () => Promise + isAuthenticated: boolean } const AuthContext = createContext({ @@ -25,6 +27,8 @@ const AuthContext = createContext({ loading: true, login: async () => { }, logout: () => { }, + refreshUser: async () => { }, + isAuthenticated: false, }) export function AuthProvider({ children }: { children: React.ReactNode }) { @@ -80,8 +84,16 @@ export function AuthProvider({ children }: { children: React.ReactNode }) { setUser(null) } + const refreshUser = async () => { + if (token) { + await fetchUser(token) + } + } + + const isAuthenticated = !!user + return ( - + {children} ) diff --git a/frontend/types/models.ts b/frontend/types/models.ts new file mode 100644 index 0000000..d2287c7 --- /dev/null +++ b/frontend/types/models.ts @@ -0,0 +1,63 @@ +export interface PaginationMeta { + total: number + limit: number + offset: number +} + +export interface PaginatedResponse { + data: T[] + meta: PaginationMeta +} + +export interface Vertical { + id: number + name: string + slug: string + show_count?: number + logo_url?: string | null +} + +export interface Venue { + id: number + name: string + slug: string + city: string + state: string | null + country: string + capacity?: number | null +} + +export interface Song { + id: number + title: string + slug: string + original_artist?: string | null + times_played?: number + last_played?: string | null + gap?: number | null +} + +export interface Performance { + id: number + song: Song + set_name?: string | null + position?: number + seg_audience?: boolean + transition?: boolean + notes?: string | null + youtube_link?: string | null +} + +export interface Show { + id: number + date: string + slug: string + venue_id: number + vertical_id: number + venue?: Venue + vertical?: Vertical + performances?: Performance[] + notes?: string | null + likes_count?: number + youtube_link?: string | null +}