feat(frontend): Enforce strict mode and refactor pages
Some checks failed
Deploy Fediversion / deploy (push) Failing after 1s
Some checks failed
Deploy Fediversion / deploy (push) Failing after 1s
This commit is contained in:
parent
2941fa482e
commit
60456c4737
18 changed files with 142 additions and 134 deletions
|
|
@ -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<Show[]> {
|
||||
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<Show> = await res.json()
|
||||
return data.data || []
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
async function getTopSongs(verticalSlug: string) {
|
||||
async function getTopSongs(verticalSlug: string): Promise<Song[]> {
|
||||
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<Song> = await res.json()
|
||||
return data.data || []
|
||||
} catch {
|
||||
return []
|
||||
|
|
@ -104,7 +106,7 @@ export default async function VerticalPage({ params }: Props) {
|
|||
</Link>
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-4">
|
||||
{recentShows.slice(0, 8).map((show: any) => (
|
||||
{recentShows.slice(0, 8).map((show) => (
|
||||
<Link
|
||||
key={show.id}
|
||||
href={`/${verticalSlug}/shows/${show.slug}`}
|
||||
|
|
@ -127,11 +129,12 @@ export default async function VerticalPage({ params }: Props) {
|
|||
<MapPin className="h-3 w-3" />
|
||||
{show.venue?.city}, {show.venue?.state || show.venue?.country}
|
||||
</div>
|
||||
{show.tour?.name && (
|
||||
{/* Tour is not in strict Show model yet, omitting for strictness or need to add to model */}
|
||||
{/* {show.tour?.name && (
|
||||
<div className="text-xs text-muted-foreground/80 pt-1">
|
||||
{show.tour.name}
|
||||
</div>
|
||||
)}
|
||||
)} */}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Link>
|
||||
|
|
@ -150,7 +153,7 @@ export default async function VerticalPage({ params }: Props) {
|
|||
</Link>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{topSongs.slice(0, 5).map((song: any, index: number) => (
|
||||
{topSongs.slice(0, 5).map((song, index) => (
|
||||
<Link
|
||||
key={song.id}
|
||||
href={`/${verticalSlug}/songs/${song.slug}`}
|
||||
|
|
@ -166,7 +169,7 @@ export default async function VerticalPage({ params }: Props) {
|
|||
</div>
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground whitespace-nowrap">
|
||||
{song.times_played || song.performance_count || 0} performances
|
||||
{song.times_played || 0} performances
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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<Show>) => {
|
||||
setShows(data.data || [])
|
||||
setLoading(false)
|
||||
})
|
||||
.catch(err => {
|
||||
|
|
|
|||
|
|
@ -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<Song[]>([])
|
||||
|
|
@ -20,11 +14,11 @@ export default function SongsPage() {
|
|||
useEffect(() => {
|
||||
fetch(`${getApiUrl()}/songs/?limit=1000`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
.then((data: PaginatedResponse<Song>) => {
|
||||
// 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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -180,7 +180,7 @@ export default function WelcomePage() {
|
|||
<Switch
|
||||
id="wiki-mode"
|
||||
checked={wikiMode}
|
||||
onChange={(e) => setWikiMode(e.target.checked)}
|
||||
onCheckedChange={(checked) => setWikiMode(checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -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 */}
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-lg font-bold">
|
||||
{currentVertical ? currentVertical.name : "Fediversion"}
|
||||
{current ? current.name : "Fediversion"}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
© {new Date().getFullYear()} All rights reserved
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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<any[]>([])
|
||||
const [playlists, setPlaylists] = useState<Playlist[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -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)
|
|||
<div className="space-y-1">
|
||||
<h3 className="font-semibold leading-tight">{v.name}</h3>
|
||||
<p className="text-xs text-muted-foreground font-medium">
|
||||
{v.show_count.toLocaleString()} shows
|
||||
{(v.show_count || 0).toLocaleString()} shows
|
||||
</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
|
|
|||
|
|
@ -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[]
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ export interface Performance {
|
|||
|
||||
interface PerformanceListProps {
|
||||
performances: Performance[]
|
||||
songTitle?: string
|
||||
}
|
||||
|
||||
type SortOption = "date_desc" | "date_asc" | "rating_desc"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ interface CommandDialogProps extends React.ComponentPropsWithoutRef<typeof Dialo
|
|||
description?: string
|
||||
commandProps?: React.ComponentPropsWithoutRef<typeof Command>
|
||||
showCloseButton?: boolean
|
||||
className?: string
|
||||
}
|
||||
|
||||
function CommandDialog({
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
<div key={i} className="relative">
|
||||
<Star className={cn(
|
||||
size === "sm" ? "h-3 w-3" : size === "lg" ? "h-5 w-5" : "h-4 w-4",
|
||||
"fill-muted text-muted-foreground/50"
|
||||
)} />
|
||||
{(isFull || isPartial) && (
|
||||
<div
|
||||
className="absolute inset-0 overflow-hidden"
|
||||
style={{
|
||||
clipPath: `inset(0 ${isFull ? 0 : 100 - partialFill}% 0 0)`
|
||||
// Use StarRating for visual display
|
||||
const renderStars = () => (
|
||||
<StarRating
|
||||
value={localValue}
|
||||
size={size}
|
||||
readonly={readonly}
|
||||
onChange={(v) => {
|
||||
if (!readonly) {
|
||||
setLocalValue(v)
|
||||
onChange?.(v)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Star className={cn(
|
||||
size === "sm" ? "h-3 w-3" : size === "lg" ? "h-5 w-5" : "h-4 w-4",
|
||||
"fill-yellow-500 text-yellow-500"
|
||||
)} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
precision="decimal"
|
||||
/>
|
||||
)
|
||||
}
|
||||
return stars
|
||||
}
|
||||
|
||||
if (readonly) {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div
|
||||
className={cn(
|
||||
"rounded-full flex items-center justify-center font-bold text-white shadow-md select-none",
|
||||
sizeClasses[size],
|
||||
className
|
||||
)}
|
||||
<Avatar className={cn(sizeClasses[size], className)}>
|
||||
<AvatarImage src={src || undefined} alt={username} />
|
||||
<AvatarFallback
|
||||
className="font-bold text-white"
|
||||
style={{ backgroundColor: bgColor }}
|
||||
>
|
||||
{displayText}
|
||||
</div>
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ interface AuthContextType {
|
|||
loading: boolean
|
||||
login: (token: string) => Promise<void>
|
||||
logout: () => void
|
||||
refreshUser: () => Promise<void>
|
||||
isAuthenticated: boolean
|
||||
}
|
||||
|
||||
const AuthContext = createContext<AuthContextType>({
|
||||
|
|
@ -25,6 +27,8 @@ const AuthContext = createContext<AuthContextType>({
|
|||
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 (
|
||||
<AuthContext.Provider value={{ user, token, loading, login, logout }}>
|
||||
<AuthContext.Provider value={{ user, token, loading, login, logout, refreshUser, isAuthenticated }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
)
|
||||
|
|
|
|||
63
frontend/types/models.ts
Normal file
63
frontend/types/models.ts
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
export interface PaginationMeta {
|
||||
total: number
|
||||
limit: number
|
||||
offset: number
|
||||
}
|
||||
|
||||
export interface PaginatedResponse<T> {
|
||||
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
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue