feat: enhance Nickname Queue with status filtering
Some checks are pending
Deploy Elmeg / deploy (push) Waiting to run
Some checks are pending
Deploy Elmeg / deploy (push) Waiting to run
This commit is contained in:
parent
730e92f8c9
commit
037d2aa463
2 changed files with 116 additions and 41 deletions
|
|
@ -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)])
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderNicknameCard = (nick: Nickname) => (
|
||||||
|
<Card key={nick.id}>
|
||||||
|
<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">
|
||||||
|
"{nick.nickname}"
|
||||||
|
</CardTitle>
|
||||||
|
{nick.song_title && (
|
||||||
|
<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" />
|
||||||
|
</Button>
|
||||||
|
<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" />
|
||||||
|
</Button>
|
||||||
|
</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 (
|
return (
|
||||||
<div className="space-y-4">
|
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-4">
|
||||||
<h2 className="text-xl font-semibold">Pending Nicknames</h2>
|
<TabsList>
|
||||||
{nicknames.length === 0 ? (
|
<TabsTrigger value="pending" className="flex items-center gap-1">
|
||||||
<p className="text-muted-foreground">No pending nicknames.</p>
|
<Clock className="h-4 w-4" />
|
||||||
) : (
|
Pending
|
||||||
<div className="grid gap-4">
|
</TabsTrigger>
|
||||||
{nicknames.map((nick) => (
|
<TabsTrigger value="approved" className="flex items-center gap-1">
|
||||||
<Card key={nick.id}>
|
<Check className="h-4 w-4" />
|
||||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
Approved
|
||||||
<CardTitle className="text-base font-medium">
|
</TabsTrigger>
|
||||||
"{nick.nickname}"
|
<TabsTrigger value="rejected" className="flex items-center gap-1">
|
||||||
</CardTitle>
|
<X className="h-4 w-4" />
|
||||||
<div className="flex gap-2">
|
Rejected
|
||||||
<Button size="sm" variant="outline" className="text-green-600 hover:text-green-700 hover:bg-green-50" onClick={() => moderate(nick.id, "approve")}>
|
</TabsTrigger>
|
||||||
<Check className="h-4 w-4" />
|
</TabsList>
|
||||||
</Button>
|
|
||||||
<Button size="sm" variant="outline" className="text-red-600 hover:text-red-700 hover:bg-red-50" onClick={() => moderate(nick.id, "reject")}>
|
<TabsContent value={activeTab} className="space-y-4">
|
||||||
<X className="h-4 w-4" />
|
{loading ? (
|
||||||
</Button>
|
<div className="text-muted-foreground">Loading...</div>
|
||||||
</div>
|
) : nicknames.length === 0 ? (
|
||||||
</CardHeader>
|
<p className="text-muted-foreground">No {activeTab} nicknames.</p>
|
||||||
<CardContent>
|
) : (
|
||||||
<p className="text-sm text-muted-foreground">{nick.description}</p>
|
<div className="grid gap-4">
|
||||||
<p className="text-xs text-muted-foreground mt-2">Performance ID: {nick.performance_id}</p>
|
{nicknames.map(renderNicknameCard)}
|
||||||
</CardContent>
|
</div>
|
||||||
</Card>
|
)}
|
||||||
))}
|
</TabsContent>
|
||||||
</div>
|
</Tabs>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue