0 ? ((evalSubmitted / evalTotal) * 100).toFixed(0) : '0'
+ const stats = [
+ {
+ value: assignmentCount,
+ label: 'Assignments',
+ detail: 'Jury-project pairs',
+ accent: 'text-brand-blue',
+ },
+ {
+ value: `${evalSubmitted}/${evalTotal}`,
+ label: 'Submitted',
+ detail: `${completionPct}% complete`,
+ accent: 'text-emerald-600',
+ },
+ {
+ value: evalDraft,
+ label: 'In draft',
+ detail: evalDraft > 0 ? 'Not yet submitted' : 'No drafts',
+ accent: evalDraft > 0 ? 'text-amber-600' : 'text-emerald-600',
+ },
+ {
+ value: activeJurors,
+ label: 'Active jurors',
+ detail: 'Evaluating',
+ accent: 'text-brand-teal',
+ },
+ ]
+
return (
-
-
- {round.name} — Evaluation
+ <>
+ {/* Round label */}
+
+ {round.name} — Evaluation
-
-
-
-
-
-
-
Total Assignments
-
{assignmentCount}
-
- Jury-project pairs
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Submitted
-
- {evalSubmitted}
- /{evalTotal}
-
-
- {completionPct}% complete
-
-
-
-
-
-
-
-
-
+ {/* Mobile: horizontal data strip */}
+
+ {stats.map((s, i) => (
+ 0 ? 'border-l border-border/50' : ''}`}>
+
{s.value}
+
{s.label}
+
+ ))}
+
-
-
-
-
-
-
In Draft
-
{evalDraft}
-
- {evalDraft > 0 ? 'Not yet submitted' : 'No drafts'}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Active Jurors
-
{activeJurors}
-
- Evaluating this round
-
-
-
-
-
-
-
-
-
+ {/* Desktop: editorial stat row */}
+
+
+ {stats.map((s, i) => (
+
+ {s.value}
+ {s.label}
+ {s.detail}
+
+ ))}
+
-
+ >
)
}
diff --git a/src/components/dashboard/round-stats-filtering.tsx b/src/components/dashboard/round-stats-filtering.tsx
index 45687d3..1b09685 100644
--- a/src/components/dashboard/round-stats-filtering.tsx
+++ b/src/components/dashboard/round-stats-filtering.tsx
@@ -1,13 +1,6 @@
'use client'
-import { Card, CardContent } from '@/components/ui/card'
-import { AnimatedCard } from '@/components/shared/animated-container'
-import {
- Filter,
- CheckCircle2,
- XCircle,
- AlertTriangle,
-} from 'lucide-react'
+import { motion } from 'motion/react'
type PipelineRound = {
id: string
@@ -35,93 +28,80 @@ type RoundStatsFilteringProps = {
export function RoundStatsFiltering({ round }: RoundStatsFilteringProps) {
const { filteringPassed, filteringRejected, filteringFlagged, projectStates } = round
+ const passRate = projectStates.total > 0
+ ? ((filteringPassed / projectStates.total) * 100).toFixed(0)
+ : '0'
+ const rejectRate = projectStates.total > 0
+ ? ((filteringRejected / projectStates.total) * 100).toFixed(0)
+ : '0'
+
+ const stats = [
+ {
+ value: projectStates.total,
+ label: 'To filter',
+ detail: 'In pipeline',
+ accent: 'text-brand-blue',
+ },
+ {
+ value: filteringPassed,
+ label: 'Passed',
+ detail: `${passRate}% pass rate`,
+ accent: 'text-emerald-600',
+ },
+ {
+ value: filteringRejected,
+ label: 'Rejected',
+ detail: `${rejectRate}% rejected`,
+ accent: 'text-red-600',
+ },
+ {
+ value: filteringFlagged,
+ label: 'Flagged',
+ detail: filteringFlagged > 0 ? 'Manual review' : 'None flagged',
+ accent: filteringFlagged > 0 ? 'text-amber-600' : 'text-emerald-600',
+ },
+ ]
return (
-
-
- {round.name} — Filtering
+ <>
+ {/* Round label */}
+
+ {round.name} — Filtering
-
-
-
-
-
-
-
Projects to Filter
-
{projectStates.total}
-
- In pipeline
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
AI Passed
-
{filteringPassed}
-
- {projectStates.total > 0
- ? `${((filteringPassed / projectStates.total) * 100).toFixed(0)}% pass rate`
- : 'No results yet'}
-
-
-
-
-
-
-
-
-
+ {/* Mobile: horizontal data strip */}
+
+ {stats.map((s, i) => (
+ 0 ? 'border-l border-border/50' : ''}`}>
+
{s.value}
+
{s.label}
+
+ ))}
+
-
-
-
-
-
-
AI Rejected
-
{filteringRejected}
-
- {projectStates.total > 0
- ? `${((filteringRejected / projectStates.total) * 100).toFixed(0)}% rejected`
- : 'No results yet'}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Flagged for Review
-
{filteringFlagged}
-
- {filteringFlagged > 0 ? 'Needs manual review' : 'None flagged'}
-
-
-
-
-
-
-
+ {/* Desktop: editorial stat row */}
+
+
+ {stats.map((s, i) => (
+
+ {s.value}
+ {s.label}
+ {s.detail}
+
+ ))}
+
-
+ >
)
}
diff --git a/src/components/dashboard/round-stats-generic.tsx b/src/components/dashboard/round-stats-generic.tsx
index 3042d60..a0474fe 100644
--- a/src/components/dashboard/round-stats-generic.tsx
+++ b/src/components/dashboard/round-stats-generic.tsx
@@ -1,13 +1,6 @@
'use client'
-import { Card, CardContent } from '@/components/ui/card'
-import { AnimatedCard } from '@/components/shared/animated-container'
-import {
- ClipboardList,
- Users,
- CheckCircle2,
- AlertTriangle,
-} from 'lucide-react'
+import { motion } from 'motion/react'
type RoundStatsGenericProps = {
projectCount: number
@@ -33,89 +26,72 @@ export function RoundStatsGeneric({
const completionPct =
totalAssignments > 0 ? ((submittedCount / totalAssignments) * 100).toFixed(0) : '0'
+ const stats = [
+ {
+ value: projectCount,
+ label: 'Projects',
+ detail: newProjectsThisWeek > 0 ? `+${newProjectsThisWeek} this week` : null,
+ accent: 'text-brand-blue',
+ },
+ {
+ value: totalJurors,
+ label: 'Jurors',
+ detail: `${activeJurors} active`,
+ accent: 'text-brand-teal',
+ },
+ {
+ value: `${submittedCount}/${totalAssignments}`,
+ label: 'Evaluations',
+ detail: `${completionPct}% complete`,
+ accent: 'text-emerald-600',
+ },
+ {
+ value: actionsCount,
+ label: actionsCount === 1 ? 'Action' : 'Actions',
+ detail: actionsCount > 0 ? 'Pending' : 'All clear',
+ accent: actionsCount > 0 ? 'text-amber-600' : 'text-emerald-600',
+ },
+ ]
+
return (
-
-
-
-
-
-
-
Projects
-
{projectCount}
-
- {newProjectsThisWeek > 0 ? `+${newProjectsThisWeek} this week` : 'In edition'}
-
-
-
-
-
-
-
-
-
+ <>
+ {/* Mobile: horizontal data strip */}
+
+ {stats.map((s, i) => (
+ 0 ? 'border-l border-border/50' : ''}`}>
+
{s.value}
+
{s.label}
+
+ ))}
+
-
-
-
-
-
-
Jury
-
{totalJurors}
-
- {activeJurors} active
-
+ {/* Desktop: editorial stat row */}
+
+
+ {stats.map((s, i) => (
+
+
+ {s.value}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Evaluations
-
- {submittedCount}
- /{totalAssignments}
-
-
- {completionPct}% complete
-
-
-
-
-
-
-
-
-
-
-
- 0 ? 'border-l-amber-500' : 'border-l-emerald-400'}`}>
-
-
-
-
Actions Needed
-
{actionsCount}
-
0 ? 'text-amber-600' : 'text-emerald-600'}`}>
- {actionsCount > 0 ? 'Pending actions' : 'All clear'}
-
-
-
0 ? 'bg-amber-500/10' : 'bg-emerald-400/10'}`}>
- {actionsCount > 0
- ?
- :
- }
-
-
-
-
-
-
+
{s.label}
+ {s.detail && (
+
{s.detail}
+ )}
+
+ ))}
+
+
+ >
)
}
diff --git a/src/components/dashboard/round-stats-intake.tsx b/src/components/dashboard/round-stats-intake.tsx
index 939e7d8..dde94b2 100644
--- a/src/components/dashboard/round-stats-intake.tsx
+++ b/src/components/dashboard/round-stats-intake.tsx
@@ -1,13 +1,6 @@
'use client'
-import { Card, CardContent } from '@/components/ui/card'
-import { AnimatedCard } from '@/components/shared/animated-container'
-import {
- ClipboardList,
- CheckCircle2,
- Clock,
- TrendingUp,
-} from 'lucide-react'
+import { motion } from 'motion/react'
type PipelineRound = {
id: string
@@ -32,91 +25,77 @@ type RoundStatsIntakeProps = {
export function RoundStatsIntake({ round, newProjectsThisWeek }: RoundStatsIntakeProps) {
const { projectStates } = round
+ const completePct = projectStates.total > 0
+ ? ((projectStates.PASSED / projectStates.total) * 100).toFixed(0)
+ : '0'
+
+ const stats = [
+ {
+ value: projectStates.total,
+ label: 'Submitted',
+ detail: 'Total projects',
+ accent: 'text-brand-blue',
+ },
+ {
+ value: projectStates.PASSED,
+ label: 'Docs complete',
+ detail: `${completePct}% of total`,
+ accent: 'text-emerald-600',
+ },
+ {
+ value: projectStates.PENDING,
+ label: 'Pending',
+ detail: projectStates.PENDING > 0 ? 'Awaiting review' : 'All reviewed',
+ accent: projectStates.PENDING > 0 ? 'text-amber-600' : 'text-emerald-600',
+ },
+ {
+ value: newProjectsThisWeek,
+ label: 'This week',
+ detail: newProjectsThisWeek > 0 ? 'New submissions' : 'No new',
+ accent: 'text-brand-teal',
+ },
+ ]
return (
-
-
- {round.name} — Intake
+ <>
+ {/* Round label */}
+
+ {round.name} — Intake
-
-
-
-
-
-
-
Total Projects
-
{projectStates.total}
-
- In this round
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Documents Complete
-
{projectStates.PASSED}
-
- {projectStates.total > 0
- ? `${((projectStates.PASSED / projectStates.total) * 100).toFixed(0)}% of total`
- : 'No projects yet'}
-
-
-
-
-
-
-
-
-
+ {/* Mobile: horizontal data strip */}
+
+ {stats.map((s, i) => (
+ 0 ? 'border-l border-border/50' : ''}`}>
+
{s.value}
+
{s.label}
+
+ ))}
+
-
-
-
-
-
-
Pending Review
-
{projectStates.PENDING}
-
- {projectStates.PENDING > 0 ? 'Awaiting review' : 'All reviewed'}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
New This Week
-
{newProjectsThisWeek}
-
- {newProjectsThisWeek > 0 ? 'Recently submitted' : 'No new submissions'}
-
-
-
-
-
-
-
-
-
+ {/* Desktop: editorial stat row */}
+
+
+ {stats.map((s, i) => (
+
+ {s.value}
+ {s.label}
+ {s.detail}
+
+ ))}
+
-
+ >
)
}
diff --git a/src/components/jury/preferences-banner.tsx b/src/components/jury/preferences-banner.tsx
new file mode 100644
index 0000000..27bbf65
--- /dev/null
+++ b/src/components/jury/preferences-banner.tsx
@@ -0,0 +1,144 @@
+'use client'
+
+import { useState } from 'react'
+import { Scale, CheckCircle2, Loader2 } from 'lucide-react'
+import { toast } from 'sonner'
+import { trpc } from '@/lib/trpc/client'
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
+import { Button } from '@/components/ui/button'
+import { Label } from '@/components/ui/label'
+import { Slider } from '@/components/ui/slider'
+
+/**
+ * Shows a blocking banner when a juror has jury group memberships
+ * with unconfirmed preferences (selfServiceCap is null) linked to
+ * active rounds. The juror must confirm before proceeding.
+ */
+export function JuryPreferencesBanner() {
+ const { data: ctx, isLoading } = trpc.user.getOnboardingContext.useQuery()
+ const utils = trpc.useUtils()
+
+ const unconfirmed = (ctx?.memberships ?? []).filter(
+ (m) => m.selfServiceCap === null,
+ )
+
+ const [prefs, setPrefs] = useState