elmeg-demo/frontend/app/admin/musicians/page.tsx
fullsizemalt ff56e4f140
Some checks are pending
Deploy Elmeg / deploy (push) Waiting to run
feat: add Band Members (Musicians) feature - Sprint 4
2025-12-24 13:07:48 -08:00

260 lines
11 KiB
TypeScript

"use client"
import { useEffect, useState, useCallback } from "react"
import { useAuth } from "@/contexts/auth-context"
import { useRouter } from "next/navigation"
import { Card, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { Badge } from "@/components/ui/badge"
import { Search, Edit, Save, X, UserCircle, Plus } from "lucide-react"
import { getApiUrl } from "@/lib/api-config"
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog"
import { Label } from "@/components/ui/label"
import Link from "next/link"
interface Musician {
id: number
name: string
slug: string
bio: string | null
image_url: string | null
primary_instrument: string | null
}
export default function AdminMusiciansPage() {
const { user, token } = useAuth()
const router = useRouter()
const [musicians, setMusicians] = useState<Musician[]>([])
const [loading, setLoading] = useState(true)
const [search, setSearch] = useState("")
const [editingMusician, setEditingMusician] = useState<Musician | null>(null)
const [isCreating, setIsCreating] = useState(false)
const [newMusician, setNewMusician] = useState({ name: "", bio: "", image_url: "", primary_instrument: "" })
const [saving, setSaving] = useState(false)
const fetchMusicians = useCallback(async () => {
if (!token) return
try {
const res = await fetch(`${getApiUrl()}/musicians?limit=100`, {
headers: { Authorization: `Bearer ${token}` }
})
if (res.ok) setMusicians(await res.json())
} catch (e) {
console.error("Failed to fetch musicians", e)
} finally {
setLoading(false)
}
}, [token])
useEffect(() => {
if (!user) {
router.push("/login")
return
}
if (user.role !== "admin") {
router.push("/")
return
}
fetchMusicians()
}, [user, router, fetchMusicians])
const createMusician = async () => {
if (!token || !newMusician.name) return
setSaving(true)
try {
const res = await fetch(`${getApiUrl()}/musicians`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`
},
body: JSON.stringify(newMusician)
})
if (res.ok) {
fetchMusicians()
setIsCreating(false)
setNewMusician({ name: "", bio: "", image_url: "", primary_instrument: "" })
}
} catch (e) {
console.error("Failed to create musician", e)
} finally {
setSaving(false)
}
}
const filteredMusicians = musicians.filter(m =>
m.name.toLowerCase().includes(search.toLowerCase()) ||
m.primary_instrument?.toLowerCase().includes(search.toLowerCase())
)
if (loading) {
return (
<div className="space-y-4">
<div className="h-8 bg-muted rounded w-48 animate-pulse" />
<div className="grid gap-4">
{[1, 2, 3].map(i => <div key={i} className="h-20 bg-muted rounded animate-pulse" />)}
</div>
</div>
)
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-2xl font-bold flex items-center gap-2">
<UserCircle className="h-6 w-6" />
Musician Management
</h2>
<Button onClick={() => setIsCreating(true)}>
<Plus className="h-4 w-4 mr-2" />
Add Musician
</Button>
</div>
<div className="relative max-w-sm">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search musicians..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-9"
/>
</div>
<Card>
<CardContent className="p-0">
<table className="w-full">
<thead className="bg-muted/50">
<tr>
<th className="text-left p-3 font-medium">Musician</th>
<th className="text-left p-3 font-medium">Instrument</th>
<th className="text-left p-3 font-medium">Bio</th>
<th className="text-right p-3 font-medium">Actions</th>
</tr>
</thead>
<tbody>
{filteredMusicians.map(musician => (
<tr key={musician.id} className="border-t">
<td className="p-3">
<Link href={`/musicians/${musician.slug}`} className="font-medium hover:underline">
{musician.name}
</Link>
</td>
<td className="p-3 text-muted-foreground">
{musician.primary_instrument || "—"}
</td>
<td className="p-3">
{musician.bio ? (
<Badge variant="outline" className="text-green-600 border-green-600">Has Bio</Badge>
) : (
<Badge variant="outline" className="text-yellow-600 border-yellow-600">No Bio</Badge>
)}
</td>
<td className="p-3 text-right">
<Button
variant="ghost"
size="sm"
onClick={() => setEditingMusician(musician)}
>
<Edit className="h-4 w-4" />
</Button>
</td>
</tr>
))}
</tbody>
</table>
{filteredMusicians.length === 0 && (
<div className="p-8 text-center text-muted-foreground">
No musicians found. Add some to get started!
</div>
)}
</CardContent>
</Card>
{/* Create Dialog */}
<Dialog open={isCreating} onOpenChange={setIsCreating}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>Add New Musician</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<Label>Name *</Label>
<Input
placeholder="e.g., Jeff Chimenti"
value={newMusician.name}
onChange={(e) => setNewMusician({ ...newMusician, name: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label>Primary Instrument</Label>
<Input
placeholder="e.g., Keyboards"
value={newMusician.primary_instrument}
onChange={(e) => setNewMusician({ ...newMusician, primary_instrument: e.target.value })}
/>
</div>
<div className="space-y-2">
<Label>Bio</Label>
<Textarea
placeholder="Brief biography..."
value={newMusician.bio}
onChange={(e) => setNewMusician({ ...newMusician, bio: e.target.value })}
rows={3}
/>
</div>
<div className="space-y-2">
<Label>Image URL</Label>
<Input
placeholder="https://..."
value={newMusician.image_url}
onChange={(e) => setNewMusician({ ...newMusician, image_url: e.target.value })}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsCreating(false)}>
Cancel
</Button>
<Button onClick={createMusician} disabled={saving || !newMusician.name}>
<Save className="h-4 w-4 mr-2" />
{saving ? "Creating..." : "Create Musician"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Edit Dialog (placeholder - can be enhanced) */}
<Dialog open={!!editingMusician} onOpenChange={() => setEditingMusician(null)}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle>Edit: {editingMusician?.name}</DialogTitle>
</DialogHeader>
<div className="py-4 text-muted-foreground">
Edit functionality coming soon. For now, view the musician profile.
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setEditingMusician(null)}>
<X className="h-4 w-4 mr-2" />
Close
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
)
}