- Fork elmeg-demo codebase for multi-band support - Add data importer infrastructure with base class - Create band-specific importers: - phish.py: Phish.net API v5 - grateful_dead.py: Grateful Stats API - setlistfm.py: Dead & Company, Billy Strings (Setlist.fm) - Add spec-kit configuration for Gemini - Update README with supported bands and architecture
112 lines
3.4 KiB
TypeScript
112 lines
3.4 KiB
TypeScript
"use client"
|
|
|
|
import { useState, useEffect } from "react"
|
|
import { Button } from "@/components/ui/button"
|
|
import { Target, Check, Loader2 } from "lucide-react"
|
|
import { getApiUrl } from "@/lib/api-config"
|
|
import { useAuth } from "@/contexts/auth-context"
|
|
|
|
interface ChaseSong {
|
|
id: number
|
|
song_id: number
|
|
song_title: string
|
|
caught_at: string | null
|
|
caught_show_id: number | null
|
|
}
|
|
|
|
interface MarkCaughtButtonProps {
|
|
songId: number
|
|
songTitle: string
|
|
showId: number
|
|
className?: string
|
|
}
|
|
|
|
export function MarkCaughtButton({ songId, songTitle, showId, className }: MarkCaughtButtonProps) {
|
|
const { user, token } = useAuth()
|
|
const [chaseSong, setChaseSong] = useState<ChaseSong | null>(null)
|
|
const [marking, setMarking] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (!user || !token) return
|
|
|
|
fetch(`${getApiUrl()}/chase/songs`, {
|
|
headers: { Authorization: `Bearer ${token}` }
|
|
})
|
|
.then(res => res.ok ? res.json() : [])
|
|
.then((songs: ChaseSong[]) => {
|
|
const match = songs.find(s => s.song_id === songId)
|
|
setChaseSong(match || null)
|
|
})
|
|
.catch(() => setChaseSong(null))
|
|
}, [user, token, songId])
|
|
|
|
const handleMarkCaught = async () => {
|
|
if (!chaseSong || !token) return
|
|
|
|
setMarking(true)
|
|
try {
|
|
const res = await fetch(`${getApiUrl()}/chase/songs/${chaseSong.id}/caught?show_id=${showId}`, {
|
|
method: "POST",
|
|
headers: { Authorization: `Bearer ${token}` }
|
|
})
|
|
|
|
if (!res.ok) throw new Error("Failed to mark caught")
|
|
|
|
// Update local state
|
|
setChaseSong({ ...chaseSong, caught_at: new Date().toISOString(), caught_show_id: showId })
|
|
} catch (err) {
|
|
console.error(err)
|
|
alert("Failed to mark song as caught")
|
|
} finally {
|
|
setMarking(false)
|
|
}
|
|
}
|
|
|
|
// Not logged in or not chasing this song
|
|
if (!user || !chaseSong) return null
|
|
|
|
// Already caught at THIS show
|
|
if (chaseSong.caught_show_id === showId) {
|
|
return (
|
|
<span
|
|
className="inline-flex items-center gap-1 text-xs text-green-600 dark:text-green-400 font-medium"
|
|
title={`You caught ${songTitle} at this show!`}
|
|
>
|
|
<Check className="h-3 w-3" />
|
|
Caught!
|
|
</span>
|
|
)
|
|
}
|
|
|
|
// Already caught at another show
|
|
if (chaseSong.caught_at) {
|
|
return (
|
|
<span
|
|
className="inline-flex items-center gap-1 text-xs text-muted-foreground"
|
|
title={`You already caught ${songTitle} at another show`}
|
|
>
|
|
<Check className="h-3 w-3" />
|
|
Caught
|
|
</span>
|
|
)
|
|
}
|
|
|
|
// Chasing but not yet caught
|
|
return (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={handleMarkCaught}
|
|
disabled={marking}
|
|
title={`You're chasing ${songTitle}! Mark it as caught at this show.`}
|
|
className={`h-6 px-2 text-xs gap-1 text-amber-600 hover:text-amber-700 hover:bg-amber-50 dark:text-amber-400 dark:hover:bg-amber-950/50 ${className}`}
|
|
>
|
|
{marking ? (
|
|
<Loader2 className="h-3 w-3 animate-spin" />
|
|
) : (
|
|
<Target className="h-3 w-3" />
|
|
)}
|
|
Mark Caught
|
|
</Button>
|
|
)
|
|
}
|