feat: enhance Nickname Queue with status filtering
Some checks are pending
Deploy Elmeg / deploy (push) Waiting to run

This commit is contained in:
fullsizemalt 2025-12-24 13:04:35 -08:00
parent 730e92f8c9
commit 037d2aa463
2 changed files with 116 additions and 41 deletions

View file

@ -267,10 +267,14 @@ def create_report(
return db_report return db_report
@router.get("/queue/nicknames", response_model=List[PerformanceNicknameRead], dependencies=[Depends(allow_moderator)]) @router.get("/queue/nicknames", response_model=List[PerformanceNicknameRead], dependencies=[Depends(allow_moderator)])
def get_pending_nicknames(session: Session = Depends(get_session)): def get_pending_nicknames(
nicknames = session.exec( status: str = "pending",
select(PerformanceNickname).where(PerformanceNickname.status == "pending") session: Session = Depends(get_session)
).all() ):
query = select(PerformanceNickname)
if status in ("pending", "approved", "rejected"):
query = query.where(PerformanceNickname.status == status)
nicknames = session.exec(query.limit(100)).all()
return nicknames return nicknames
@router.put("/nicknames/{nickname_id}/{action}", response_model=PerformanceNicknameRead, dependencies=[Depends(allow_moderator)]) @router.put("/nicknames/{nickname_id}/{action}", response_model=PerformanceNicknameRead, dependencies=[Depends(allow_moderator)])

View file

@ -4,7 +4,10 @@ import { useEffect, useState } from "react"
import { getApiUrl } from "@/lib/api-config" import { getApiUrl } from "@/lib/api-config"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Check, X } from "lucide-react" import { Badge } from "@/components/ui/badge"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Check, X, Clock, ExternalLink } from "lucide-react"
import Link from "next/link"
interface Nickname { interface Nickname {
id: number id: number
@ -12,28 +15,34 @@ interface Nickname {
description: string description: string
performance_id: number performance_id: number
status: string status: string
song_title?: string
song_slug?: string
show_date?: string
suggested_by_username?: string
} }
export function NicknameQueue() { export function NicknameQueue() {
const [nicknames, setNicknames] = useState<Nickname[]>([]) const [nicknames, setNicknames] = useState<Nickname[]>([])
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [activeTab, setActiveTab] = useState("pending")
const fetchQueue = () => { const fetchQueue = (status: string = "pending") => {
const token = localStorage.getItem("token") const token = localStorage.getItem("token")
if (!token) return if (!token) return
fetch(`${getApiUrl()}/moderation/queue/nicknames`, { setLoading(true)
fetch(`${getApiUrl()}/moderation/queue/nicknames?status=${status}`, {
headers: { Authorization: `Bearer ${token}` } headers: { Authorization: `Bearer ${token}` }
}) })
.then(res => res.json()) .then(res => res.json())
.then(setNicknames) .then(data => setNicknames(Array.isArray(data) ? data : []))
.catch(console.error) .catch(console.error)
.finally(() => setLoading(false)) .finally(() => setLoading(false))
} }
useEffect(() => { useEffect(() => {
fetchQueue() fetchQueue(activeTab)
}, []) }, [activeTab])
const moderate = (id: number, action: "approve" | "reject") => { const moderate = (id: number, action: "approve" | "reject") => {
const token = localStorage.getItem("token") const token = localStorage.getItem("token")
@ -47,38 +56,100 @@ export function NicknameQueue() {
}) })
} }
if (loading) return <div>Loading queue...</div> const getStatusBadge = (status: string) => {
switch (status) {
case "approved":
return <Badge className="bg-green-500">Approved</Badge>
case "rejected":
return <Badge variant="destructive">Rejected</Badge>
default:
return <Badge variant="secondary">Pending</Badge>
}
}
return ( const renderNicknameCard = (nick: Nickname) => (
<div className="space-y-4">
<h2 className="text-xl font-semibold">Pending Nicknames</h2>
{nicknames.length === 0 ? (
<p className="text-muted-foreground">No pending nicknames.</p>
) : (
<div className="grid gap-4">
{nicknames.map((nick) => (
<Card key={nick.id}> <Card key={nick.id}>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2"> <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<div className="space-y-1">
<CardTitle className="text-base font-medium"> <CardTitle className="text-base font-medium">
"{nick.nickname}" &quot;{nick.nickname}&quot;
</CardTitle> </CardTitle>
<div className="flex gap-2"> {nick.song_title && (
<Button size="sm" variant="outline" className="text-green-600 hover:text-green-700 hover:bg-green-50" onClick={() => moderate(nick.id, "approve")}> <Link
href={`/songs/${nick.song_slug}`}
className="text-sm text-muted-foreground hover:underline flex items-center gap-1"
>
{nick.song_title}
<ExternalLink className="h-3 w-3" />
</Link>
)}
</div>
<div className="flex items-center gap-2">
{getStatusBadge(nick.status)}
{nick.status === "pending" && (
<div className="flex gap-1">
<Button
size="sm"
variant="outline"
className="text-green-600 hover:text-green-700 hover:bg-green-50"
onClick={() => moderate(nick.id, "approve")}
>
<Check className="h-4 w-4" /> <Check className="h-4 w-4" />
</Button> </Button>
<Button size="sm" variant="outline" className="text-red-600 hover:text-red-700 hover:bg-red-50" onClick={() => moderate(nick.id, "reject")}> <Button
size="sm"
variant="outline"
className="text-red-600 hover:text-red-700 hover:bg-red-50"
onClick={() => moderate(nick.id, "reject")}
>
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</Button> </Button>
</div> </div>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">{nick.description}</p>
<p className="text-xs text-muted-foreground mt-2">Performance ID: {nick.performance_id}</p>
</CardContent>
</Card>
))}
</div>
)} )}
</div> </div>
</CardHeader>
<CardContent>
{nick.description && (
<p className="text-sm text-muted-foreground">{nick.description}</p>
)}
<div className="flex items-center gap-4 mt-2 text-xs text-muted-foreground">
{nick.show_date && <span>{nick.show_date}</span>}
{nick.suggested_by_username && (
<span>Suggested by: {nick.suggested_by_username}</span>
)}
</div>
</CardContent>
</Card>
)
return (
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
<TabsList>
<TabsTrigger value="pending" className="flex items-center gap-1">
<Clock className="h-4 w-4" />
Pending
</TabsTrigger>
<TabsTrigger value="approved" className="flex items-center gap-1">
<Check className="h-4 w-4" />
Approved
</TabsTrigger>
<TabsTrigger value="rejected" className="flex items-center gap-1">
<X className="h-4 w-4" />
Rejected
</TabsTrigger>
</TabsList>
<TabsContent value={activeTab} className="space-y-4">
{loading ? (
<div className="text-muted-foreground">Loading...</div>
) : nicknames.length === 0 ? (
<p className="text-muted-foreground">No {activeTab} nicknames.</p>
) : (
<div className="grid gap-4">
{nicknames.map(renderNicknameCard)}
</div>
)}
</TabsContent>
</Tabs>
) )
} }