feat: observer UX overhaul — reports, projects, charts, session & email
All checks were successful
Build and Push Docker Image / build (push) Successful in 11m2s
All checks were successful
Build and Push Docker Image / build (push) Successful in 11m2s
- Observer projects: default sort by status (rejected last), sortable status column - Observer projects: search by country, institution, geographic zone - Observer project detail: vertical timeline connectors between rounds - Fix React key warning in ExpandableJurorTable and FilteringReportTabs - Fix ScoreBadge text always white for better contrast on all backgrounds - Remove misleading /30 denominator from heatmap juror reviewed count - INTAKE stats: show Start-ups, Business Concepts, Countries (not States/Categories) - DiversityMetrics: extractCountry() for country-only display in charts - Fix nested button hydration error in filtering report mobile view - Color project titles by outcome in filtering report (green/red/amber) - Redesign CrossStageComparisonChart: funnel viz + metrics table with attrition % - Center doughnut chart in StatusBreakdownChart - Remove redundant RoundTypeStatsCards from evaluation report - Move evaluation tab bar below overview header, rename to "Juror Assignments" - Dev email override system (DEV_EMAIL_OVERRIDE env var) - Session refresh on role change without re-login - Role switcher in user dropdown menu - formatCategory() utility for consistent category display - Activity feed max height constraint Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,7 +17,7 @@ import {
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Filter, ChevronDown, ChevronUp, ChevronLeft, ChevronRight } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn, formatCategory } from '@/lib/utils'
|
||||
|
||||
type AIScreeningData = {
|
||||
meetsCriteria?: boolean
|
||||
@@ -200,7 +200,7 @@ export function FilteringPanel({ roundId }: { roundId: string }) {
|
||||
{r.project?.title ?? 'Unknown'}
|
||||
</Link>
|
||||
<p className="text-xs text-muted-foreground truncate">
|
||||
{r.project?.competitionCategory ?? ''} · {r.project?.country ?? ''}
|
||||
{formatCategory(r.project?.competitionCategory)} · {r.project?.country ?? ''}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Badge } from '@/components/ui/badge'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { AnimatedCard } from '@/components/shared/animated-container'
|
||||
import { ArrowDown, ChevronDown, ChevronUp, TrendingDown } from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { cn, formatCategory } from '@/lib/utils'
|
||||
|
||||
export function PreviousRoundSection({ currentRoundId }: { currentRoundId: string }) {
|
||||
const [collapsed, setCollapsed] = useState(false)
|
||||
@@ -76,7 +76,7 @@ export function PreviousRoundSection({ currentRoundId }: { currentRoundId: strin
|
||||
return (
|
||||
<div key={cat.category} className="space-y-1">
|
||||
<div className="flex items-center justify-between text-sm">
|
||||
<span className="font-medium truncate">{cat.category}</span>
|
||||
<span className="font-medium truncate">{formatCategory(cat.category)}</span>
|
||||
<span className="text-xs text-muted-foreground tabular-nums">
|
||||
{cat.previous} → {cat.current}
|
||||
<span className="text-rose-500 ml-1">(-{cat.eliminated})</span>
|
||||
|
||||
@@ -128,7 +128,7 @@ function RoundNode({
|
||||
return (
|
||||
<button type="button" onClick={onClick} className="text-left focus:outline-none">
|
||||
<Card className={cn(
|
||||
'w-44 shrink-0 border shadow-sm transition-all cursor-pointer hover:shadow-md',
|
||||
'w-44 shrink-0 border-2 border-border/60 shadow-sm transition-all cursor-pointer hover:shadow-md',
|
||||
isSelected && 'ring-2 ring-brand-teal shadow-md',
|
||||
)}>
|
||||
<CardContent className="p-3 space-y-2">
|
||||
@@ -219,7 +219,7 @@ function PipelineView({
|
||||
|
||||
{/* Award Tracks */}
|
||||
{awardGroups.size > 0 && (
|
||||
<div className="space-y-3 pt-1">
|
||||
<div className="space-y-3 pt-4">
|
||||
{Array.from(awardGroups.entries()).map(([awardId, group]) => (
|
||||
<div
|
||||
key={awardId}
|
||||
@@ -313,7 +313,7 @@ export function ObserverDashboardContent({ userName }: { userName?: string }) {
|
||||
<div className="grid grid-cols-3 md:grid-cols-6 divide-x divide-border">
|
||||
{[
|
||||
{ value: stats.projectCount, label: 'Projects' },
|
||||
{ value: stats.activeRoundName ?? `${stats.activeRoundCount} Active`, label: 'Active Round', isText: !!stats.activeRoundName },
|
||||
{ value: stats.activeRoundName ?? `${stats.activeRoundCount} Active`, label: 'Active Rounds', isText: !!stats.activeRoundName },
|
||||
{ value: avgScore, label: 'Avg Score' },
|
||||
{ value: `${stats.completionRate}%`, label: 'Completion' },
|
||||
{ value: stats.jurorCount, label: 'Jurors' },
|
||||
|
||||
@@ -576,9 +576,10 @@ export function ObserverProjectDetail({ projectId }: { projectId: string }) {
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<ol className="space-y-4">
|
||||
<ol className="relative">
|
||||
{competitionRounds.map((round, idx) => {
|
||||
const effectiveState = effectiveStates[idx]
|
||||
const isLast = idx === competitionRounds.length - 1
|
||||
|
||||
const roundAssignments = assignments.filter(
|
||||
(a) => a.roundId === round.id,
|
||||
@@ -589,15 +590,15 @@ export function ObserverProjectDetail({ projectId }: { projectId: string }) {
|
||||
let labelClass = 'text-muted-foreground'
|
||||
|
||||
if (effectiveState === 'PASSED' || effectiveState === 'COMPLETED') {
|
||||
icon = <CheckCircle2 className="mt-0.5 h-5 w-5 shrink-0 text-emerald-500" />
|
||||
icon = <CheckCircle2 className="h-5 w-5 shrink-0 text-emerald-500" />
|
||||
statusLabel = 'Passed'
|
||||
} else if (effectiveState === 'REJECTED') {
|
||||
icon = <XCircle className="mt-0.5 h-5 w-5 shrink-0 text-red-500" />
|
||||
icon = <XCircle className="h-5 w-5 shrink-0 text-red-500" />
|
||||
statusLabel = 'Rejected at this round'
|
||||
labelClass = 'text-red-600 font-medium'
|
||||
} else if (effectiveState === 'IN_PROGRESS') {
|
||||
icon = (
|
||||
<span className="mt-0.5 flex h-5 w-5 shrink-0 items-center justify-center">
|
||||
<span className="flex h-5 w-5 shrink-0 items-center justify-center">
|
||||
<span className="relative flex h-3 w-3">
|
||||
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-blue-400 opacity-75" />
|
||||
<span className="relative inline-flex h-3 w-3 rounded-full bg-blue-500" />
|
||||
@@ -606,22 +607,32 @@ export function ObserverProjectDetail({ projectId }: { projectId: string }) {
|
||||
)
|
||||
statusLabel = 'Active'
|
||||
} else if (effectiveState === 'NOT_REACHED') {
|
||||
icon = <Circle className="mt-0.5 h-5 w-5 shrink-0 text-muted-foreground/15" />
|
||||
icon = <Circle className="h-5 w-5 shrink-0 text-muted-foreground/15" />
|
||||
statusLabel = 'Not reached'
|
||||
labelClass = 'text-muted-foreground/50 italic'
|
||||
} else if (effectiveState === 'PENDING') {
|
||||
icon = <Circle className="mt-0.5 h-5 w-5 shrink-0 text-muted-foreground/40" />
|
||||
icon = <Circle className="h-5 w-5 shrink-0 text-muted-foreground/40" />
|
||||
statusLabel = 'Pending'
|
||||
} else {
|
||||
icon = <Circle className="mt-0.5 h-5 w-5 shrink-0 text-muted-foreground/20" />
|
||||
icon = <Circle className="h-5 w-5 shrink-0 text-muted-foreground/20" />
|
||||
}
|
||||
|
||||
return (
|
||||
<li key={round.id} className={cn(
|
||||
'flex items-start gap-3',
|
||||
'relative flex items-start gap-3 pb-6',
|
||||
isLast && 'pb-0',
|
||||
effectiveState === 'NOT_REACHED' && 'opacity-50',
|
||||
)}>
|
||||
{icon}
|
||||
{/* Connector line */}
|
||||
{!isLast && (
|
||||
<span
|
||||
className="absolute left-[9px] top-6 h-[calc(100%-8px)] w-px bg-border"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
<span className="relative z-10 flex items-center justify-center">
|
||||
{icon}
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className={cn(
|
||||
'text-sm font-medium',
|
||||
|
||||
@@ -56,7 +56,7 @@ export function ObserverProjectsContent() {
|
||||
const [debouncedSearch, setDebouncedSearch] = useState('')
|
||||
const [roundFilter, setRoundFilter] = useState('all')
|
||||
const [statusFilter, setStatusFilter] = useState('all')
|
||||
const [sortBy, setSortBy] = useState<'title' | 'score' | 'evaluations'>('title')
|
||||
const [sortBy, setSortBy] = useState<'title' | 'score' | 'evaluations' | 'status'>('status')
|
||||
const [sortDir, setSortDir] = useState<'asc' | 'desc'>('asc')
|
||||
const [page, setPage] = useState(1)
|
||||
const [perPage] = useState(20)
|
||||
@@ -86,7 +86,7 @@ export function ObserverProjectsContent() {
|
||||
setPage(1)
|
||||
}
|
||||
|
||||
const handleSort = (column: 'title' | 'score' | 'evaluations') => {
|
||||
const handleSort = (column: 'title' | 'score' | 'evaluations' | 'status') => {
|
||||
if (sortBy === column) {
|
||||
setSortDir(sortDir === 'asc' ? 'desc' : 'asc')
|
||||
} else {
|
||||
@@ -184,7 +184,7 @@ export function ObserverProjectsContent() {
|
||||
}
|
||||
}, [utils, roundFilter, debouncedSearch, statusFilter, sortBy, sortDir])
|
||||
|
||||
const SortIcon = ({ column }: { column: 'title' | 'score' | 'evaluations' }) => {
|
||||
const SortIcon = ({ column }: { column: 'title' | 'score' | 'evaluations' | 'status' }) => {
|
||||
if (sortBy !== column)
|
||||
return <ArrowUpDown className="ml-1 inline h-3 w-3 text-muted-foreground/50" />
|
||||
return sortDir === 'asc' ? (
|
||||
@@ -233,7 +233,7 @@ export function ObserverProjectsContent() {
|
||||
<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 title or team..."
|
||||
placeholder="Search by title, team, country, institution..."
|
||||
value={search}
|
||||
onChange={(e) => handleSearchChange(e.target.value)}
|
||||
className="pl-10"
|
||||
@@ -329,7 +329,16 @@ export function ObserverProjectsContent() {
|
||||
</TableHead>
|
||||
<TableHead>Country</TableHead>
|
||||
<TableHead>Round</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleSort('status')}
|
||||
className="inline-flex items-center hover:text-foreground transition-colors"
|
||||
>
|
||||
Status
|
||||
<SortIcon column="status" />
|
||||
</button>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<button
|
||||
type="button"
|
||||
|
||||
@@ -38,7 +38,6 @@ import { BarChart } from '@tremor/react'
|
||||
import { CsvExportDialog } from '@/components/shared/csv-export-dialog'
|
||||
import { ExportPdfButton } from '@/components/shared/export-pdf-button'
|
||||
import { AnimatedCard } from '@/components/shared/animated-container'
|
||||
import { RoundTypeStatsCards } from '@/components/observer/round-type-stats'
|
||||
import { ExpandableJurorTable } from './expandable-juror-table'
|
||||
|
||||
const ROUND_TYPE_LABELS: Record<string, string> = {
|
||||
@@ -139,11 +138,7 @@ function ProgressSubTab({
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between flex-wrap gap-3">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold">Progress Overview</h2>
|
||||
<p className="text-sm text-muted-foreground">Evaluation progress across rounds</p>
|
||||
</div>
|
||||
<div className="flex items-center justify-end flex-wrap gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedValue && !selectedValue.startsWith('all:') && (
|
||||
<ExportPdfButton
|
||||
@@ -214,7 +209,7 @@ function ProgressSubTab({
|
||||
<CardContent className="p-5">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium text-muted-foreground">Assignments</p>
|
||||
<p className="text-sm font-medium text-muted-foreground">Juror Assignments</p>
|
||||
<p className="text-2xl font-bold mt-1">{overviewStats.assignmentCount}</p>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
{overviewStats.projectCount > 0
|
||||
@@ -309,7 +304,7 @@ function ProgressSubTab({
|
||||
<TableHead>Type</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="text-right">Projects</TableHead>
|
||||
<TableHead className="text-right">Assignments</TableHead>
|
||||
<TableHead className="text-right">Juror Assignments</TableHead>
|
||||
<TableHead className="min-w-[140px]">Completion</TableHead>
|
||||
<TableHead className="text-right">Avg Days</TableHead>
|
||||
</TableRow>
|
||||
@@ -398,7 +393,7 @@ function ProgressSubTab({
|
||||
<p className="font-medium">{projects}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-muted-foreground text-xs">Assignments</p>
|
||||
<p className="text-muted-foreground text-xs">Juror Assignments</p>
|
||||
<p className="font-medium">{assignments}</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -749,42 +744,44 @@ export function EvaluationReportTabs({ roundId, programId, stages, selectedValue
|
||||
const stagesLoading = false // stages passed from parent already loaded
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<RoundTypeStatsCards roundId={roundId} />
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h2 className="text-base font-semibold">Evaluation Overview</h2>
|
||||
<p className="text-sm text-muted-foreground">Evaluation progress and juror performance</p>
|
||||
</div>
|
||||
<Tabs defaultValue="progress" className="space-y-6">
|
||||
<TabsList>
|
||||
<TabsTrigger value="progress" className="gap-2">
|
||||
<TrendingUp className="h-4 w-4" />
|
||||
Progress
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="jurors" className="gap-2">
|
||||
<Users className="h-4 w-4" />
|
||||
Jurors
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="scores" className="gap-2">
|
||||
<BarChart3 className="h-4 w-4" />
|
||||
Scores
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<Tabs defaultValue="progress" className="space-y-6">
|
||||
<TabsList>
|
||||
<TabsTrigger value="progress" className="gap-2">
|
||||
<TrendingUp className="h-4 w-4" />
|
||||
Progress
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="jurors" className="gap-2">
|
||||
<Users className="h-4 w-4" />
|
||||
Jurors
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="scores" className="gap-2">
|
||||
<BarChart3 className="h-4 w-4" />
|
||||
Scores
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<TabsContent value="progress">
|
||||
<ProgressSubTab
|
||||
selectedValue={selectedValue}
|
||||
stages={stages}
|
||||
stagesLoading={stagesLoading}
|
||||
selectedRound={selectedRound}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="progress">
|
||||
<ProgressSubTab
|
||||
selectedValue={selectedValue}
|
||||
stages={stages}
|
||||
stagesLoading={stagesLoading}
|
||||
selectedRound={selectedRound}
|
||||
/>
|
||||
</TabsContent>
|
||||
<TabsContent value="jurors">
|
||||
<JurorsSubTab roundId={roundId} selectedValue={selectedValue} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="jurors">
|
||||
<JurorsSubTab roundId={roundId} selectedValue={selectedValue} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="scores">
|
||||
<ScoresSubTab selectedValue={selectedValue} programId={programId} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
<TabsContent value="scores">
|
||||
<ScoresSubTab selectedValue={selectedValue} programId={programId} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
@@ -98,9 +98,8 @@ export function ExpandableJurorTable({ jurors }: ExpandableJurorTableProps) {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{jurors.map((j) => (
|
||||
<>
|
||||
<Fragment key={j.userId}>
|
||||
<TableRow
|
||||
key={j.userId}
|
||||
className="cursor-pointer hover:bg-muted/50"
|
||||
onClick={() => toggle(j.userId)}
|
||||
>
|
||||
@@ -179,7 +178,7 @@ export function ExpandableJurorTable({ jurors }: ExpandableJurorTableProps) {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</>
|
||||
</Fragment>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { Fragment, useState } from 'react'
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
import { cn, formatCategory } from '@/lib/utils'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import { Skeleton } from '@/components/ui/skeleton'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
@@ -33,6 +34,15 @@ interface FilteringReportTabsProps {
|
||||
|
||||
type OutcomeFilter = 'ALL' | 'PASSED' | 'FILTERED_OUT' | 'FLAGGED'
|
||||
|
||||
function outcomeTextColor(outcome: string): string {
|
||||
switch (outcome) {
|
||||
case 'PASSED': return 'text-emerald-700 dark:text-emerald-400'
|
||||
case 'FILTERED_OUT': return 'text-rose-700 dark:text-rose-400'
|
||||
case 'FLAGGED': return 'text-amber-700 dark:text-amber-400'
|
||||
default: return 'text-primary'
|
||||
}
|
||||
}
|
||||
|
||||
function outcomeBadge(outcome: string) {
|
||||
switch (outcome) {
|
||||
case 'PASSED':
|
||||
@@ -141,9 +151,8 @@ export function FilteringReportTabs({ roundId }: FilteringReportTabsProps) {
|
||||
const reasoning = extractReasoning(r.aiScreeningJson)
|
||||
const isExpanded = expandedId === r.id
|
||||
return (
|
||||
<>
|
||||
<Fragment key={r.id}>
|
||||
<TableRow
|
||||
key={r.id}
|
||||
className="cursor-pointer hover:bg-muted/50"
|
||||
onClick={() => toggleExpand(r.id)}
|
||||
>
|
||||
@@ -156,7 +165,7 @@ export function FilteringReportTabs({ roundId }: FilteringReportTabsProps) {
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<button
|
||||
className="font-medium text-primary hover:underline text-left"
|
||||
className={cn('font-medium hover:underline text-left', outcomeTextColor(effectiveOutcome))}
|
||||
onClick={(e) => openPreview(r.project.id, e)}
|
||||
>
|
||||
{r.project.title}
|
||||
@@ -164,7 +173,7 @@ export function FilteringReportTabs({ roundId }: FilteringReportTabsProps) {
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">{r.project.teamName}</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{r.project.competitionCategory ?? '—'}
|
||||
{formatCategory(r.project.competitionCategory) || '—'}
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{r.project.country ?? '—'}
|
||||
@@ -205,7 +214,7 @@ export function FilteringReportTabs({ roundId }: FilteringReportTabsProps) {
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</>
|
||||
</Fragment>
|
||||
)
|
||||
})}
|
||||
</TableBody>
|
||||
@@ -221,14 +230,17 @@ export function FilteringReportTabs({ roundId }: FilteringReportTabsProps) {
|
||||
return (
|
||||
<Card key={r.id}>
|
||||
<CardContent className="p-4">
|
||||
<button
|
||||
className="w-full text-left"
|
||||
<div
|
||||
className="w-full text-left cursor-pointer"
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onClick={() => toggleExpand(r.id)}
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') toggleExpand(r.id) }}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<button
|
||||
className="font-medium text-sm text-primary hover:underline text-left truncate block max-w-full"
|
||||
className={cn('font-medium text-sm hover:underline text-left truncate block max-w-full', outcomeTextColor(effectiveOutcome))}
|
||||
onClick={(e) => openPreview(r.project.id, e)}
|
||||
>
|
||||
{r.project.title}
|
||||
@@ -245,10 +257,10 @@ export function FilteringReportTabs({ roundId }: FilteringReportTabsProps) {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-3 text-xs text-muted-foreground mt-1">
|
||||
{r.project.competitionCategory && <span>{r.project.competitionCategory}</span>}
|
||||
{r.project.competitionCategory && <span>{formatCategory(r.project.competitionCategory)}</span>}
|
||||
{r.project.country && <span>{r.project.country}</span>}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="mt-3 pt-3 border-t space-y-2">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
import { formatCategory } from '@/lib/utils'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -81,7 +82,7 @@ export function ProjectPreviewDialog({ projectId, open, onOpenChange }: ProjectP
|
||||
</Badge>
|
||||
)}
|
||||
{data.project.competitionCategory && (
|
||||
<Badge variant="secondary">{data.project.competitionCategory}</Badge>
|
||||
<Badge variant="secondary">{formatCategory(data.project.competitionCategory)}</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
FileText,
|
||||
MessageSquare,
|
||||
Lock,
|
||||
Globe,
|
||||
} from 'lucide-react'
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
|
||||
@@ -80,8 +81,9 @@ export function RoundTypeStatsCards({ roundId }: RoundTypeStatsCardsProps) {
|
||||
case 'INTAKE':
|
||||
return [
|
||||
{ label: 'Total Projects', value: (stats.totalProjects as number) ?? 0, icon: Inbox, color: '#053d57' },
|
||||
{ label: 'States', value: ((stats.byState as Array<unknown>)?.length ?? 0), icon: BarChart3, color: '#557f8c' },
|
||||
{ label: 'Categories', value: ((stats.byCategory as Array<unknown>)?.length ?? 0), icon: Filter, color: '#1e7a8a' },
|
||||
{ label: 'Start-ups', value: (stats.startupCount as number) ?? 0, icon: BarChart3, color: '#1e7a8a' },
|
||||
{ label: 'Business Concepts', value: (stats.conceptCount as number) ?? 0, icon: FileText, color: '#557f8c' },
|
||||
{ label: 'Countries', value: (stats.countryCount as number) ?? 0, icon: Globe, color: '#2d8659' },
|
||||
]
|
||||
|
||||
case 'FILTERING':
|
||||
|
||||
Reference in New Issue
Block a user