Files
MOPC-Portal/src/components/admin/semi-finalists-content.tsx

267 lines
9.6 KiB
TypeScript
Raw Normal View History

'use client'
import { useState, useMemo } from 'react'
import Link from 'next/link'
import type { Route } from 'next'
import { useRouter } from 'next/navigation'
import { trpc } from '@/lib/trpc/client'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { CountryDisplay } from '@/components/shared/country-display'
import { Input } from '@/components/ui/input'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table'
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from '@/components/ui/tooltip'
import {
Users,
Search,
CheckCircle2,
AlertCircle,
Clock,
ArrowLeft,
Loader2,
} from 'lucide-react'
const categoryLabels: Record<string, string> = {
STARTUP: 'Startup',
BUSINESS_CONCEPT: 'Business Concept',
}
const statusConfig = {
active: { label: 'Active', color: 'bg-emerald-500', icon: CheckCircle2 },
invited: { label: 'Invited', color: 'bg-amber-500', icon: Clock },
none: { label: 'No Account', color: 'bg-red-500', icon: AlertCircle },
} as const
type SemiFinalistsContentProps = {
editionId: string
}
export function SemiFinalistsContent({ editionId }: SemiFinalistsContentProps) {
const router = useRouter()
const { data, isLoading } = trpc.dashboard.getSemiFinalistDetail.useQuery(
{ editionId },
{ enabled: !!editionId }
)
const [search, setSearch] = useState('')
const [categoryFilter, setCategoryFilter] = useState<string>('all')
const [statusFilter, setStatusFilter] = useState<string>('all')
const filtered = useMemo(() => {
if (!data) return []
let items = data
if (categoryFilter !== 'all') {
items = items.filter(p => p.category === categoryFilter)
}
if (statusFilter === 'activated') {
items = items.filter(p => p.allActivated)
} else if (statusFilter === 'pending') {
items = items.filter(p => !p.allActivated)
}
if (search.trim()) {
const q = search.toLowerCase()
items = items.filter(p =>
p.title.toLowerCase().includes(q) ||
p.teamName?.toLowerCase().includes(q) ||
p.country?.toLowerCase().includes(q) ||
p.teamMembers.some(tm =>
tm.name?.toLowerCase().includes(q) ||
tm.email.toLowerCase().includes(q)
)
)
}
return items
}, [data, search, categoryFilter, statusFilter])
const stats = useMemo(() => {
if (!data) return { total: 0, activated: 0, pending: 0 }
return {
total: data.length,
activated: data.filter(p => p.allActivated).length,
pending: data.filter(p => !p.allActivated).length,
}
}, [data])
if (isLoading) {
return (
<div className="flex items-center justify-center py-24">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>
)
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<Button variant="ghost" size="icon" className="h-8 w-8" onClick={() => router.back()}>
<ArrowLeft className="h-4 w-4" />
</Button>
<div>
<h1 className="text-xl font-bold tracking-tight md:text-2xl">
Semi-Finalists
</h1>
<p className="text-sm text-muted-foreground">
{stats.total} projects &middot; {stats.activated} fully activated &middot; {stats.pending} pending
</p>
</div>
</div>
</div>
{/* Filters */}
<Card>
<CardContent className="pt-6">
<div className="flex flex-col gap-3 sm:flex-row">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
placeholder="Search by project, team, member name or email..."
value={search}
onChange={(e) => setSearch(e.target.value)}
className="pl-9"
/>
</div>
<Select value={categoryFilter} onValueChange={setCategoryFilter}>
<SelectTrigger className="w-full sm:w-[180px]">
<SelectValue placeholder="Category" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Categories</SelectItem>
<SelectItem value="STARTUP">Startup</SelectItem>
<SelectItem value="BUSINESS_CONCEPT">Business Concept</SelectItem>
</SelectContent>
</Select>
<Select value={statusFilter} onValueChange={setStatusFilter}>
<SelectTrigger className="w-full sm:w-[180px]">
<SelectValue placeholder="Account Status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">All Statuses</SelectItem>
<SelectItem value="activated">Fully Activated</SelectItem>
<SelectItem value="pending">Pending Setup</SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* Table */}
<Card>
<CardHeader className="pb-3">
<CardTitle className="flex items-center gap-2 text-base">
<Users className="h-4 w-4 text-brand-blue" />
{filtered.length} project{filtered.length !== 1 ? 's' : ''}
</CardTitle>
</CardHeader>
<CardContent>
{filtered.length === 0 ? (
<p className="py-8 text-center text-sm text-muted-foreground">
No semi-finalist projects match your filters.
</p>
) : (
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead>Project</TableHead>
<TableHead>Category</TableHead>
<TableHead>Country</TableHead>
<TableHead>Current Round</TableHead>
<TableHead>Team Members</TableHead>
<TableHead className="text-center">Status</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filtered.map((project) => (
<TableRow key={project.projectId}>
<TableCell>
<Link
href={`/admin/projects/${project.projectId}` as Route}
className="font-medium text-brand-blue hover:underline"
>
{project.title}
</Link>
{project.teamName && (
<p className="text-xs text-muted-foreground">{project.teamName}</p>
)}
</TableCell>
<TableCell>
<Badge variant="outline" className="text-xs">
{categoryLabels[project.category ?? ''] ?? project.category}
</Badge>
</TableCell>
<TableCell className="text-sm">{project.country ? <CountryDisplay country={project.country} /> : '—'}</TableCell>
<TableCell className="text-sm">{project.currentRound}</TableCell>
<TableCell>
<TooltipProvider>
<div className="space-y-1">
{project.teamMembers.map((tm, idx) => {
const cfg = statusConfig[tm.accountStatus]
const Icon = cfg.icon
return (
<Tooltip key={idx}>
<TooltipTrigger asChild>
<div className="flex items-center gap-1.5 text-sm">
<span className={`inline-block h-2 w-2 rounded-full ${cfg.color}`} />
<span className="max-w-[180px] truncate">
{tm.name || tm.email}
</span>
</div>
</TooltipTrigger>
<TooltipContent>
<p>{tm.email}</p>
<p className="text-xs text-muted-foreground">
{cfg.label}
{tm.lastLogin && ` · Last login: ${new Date(tm.lastLogin).toLocaleDateString()}`}
</p>
</TooltipContent>
</Tooltip>
)
})}
</div>
</TooltipProvider>
</TableCell>
<TableCell className="text-center">
{project.allActivated ? (
<CheckCircle2 className="mx-auto h-4 w-4 text-emerald-500" />
) : (
<AlertCircle className="mx-auto h-4 w-4 text-amber-500" />
)}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
</div>
)
}