All checks were successful
Build and Push Docker Image / build (push) Successful in 12m17s
Mechanical sweep of 41 files via `perl -i -pe 's{\s+dark:[\w:/\[\]\.\-]+}{}g'`.
All dark: variants were paired with light-mode counterparts already; no
elements relied on a dark:-only style.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
145 lines
5.1 KiB
TypeScript
145 lines
5.1 KiB
TypeScript
'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<Map<string, { cap: number; ratio: number }>>(new Map())
|
|
const [confirming, setConfirming] = useState(false)
|
|
|
|
const updatePref = (id: string, field: 'cap' | 'ratio', value: number) => {
|
|
setPrefs((prev) => {
|
|
const next = new Map(prev)
|
|
const existing = next.get(id) ?? { cap: 15, ratio: 0.5 }
|
|
next.set(id, { ...existing, [field]: value })
|
|
return next
|
|
})
|
|
}
|
|
|
|
const confirmMutation = trpc.user.updateJuryPreferences.useMutation({
|
|
onSuccess: () => {
|
|
toast.success('Preferences confirmed')
|
|
utils.user.getOnboardingContext.invalidate()
|
|
setConfirming(false)
|
|
},
|
|
onError: (err) => {
|
|
toast.error(err.message)
|
|
setConfirming(false)
|
|
},
|
|
})
|
|
|
|
const handleConfirm = () => {
|
|
setConfirming(true)
|
|
const preferences = unconfirmed.map((m) => {
|
|
const pref = prefs.get(m.juryGroupMemberId)
|
|
return {
|
|
juryGroupMemberId: m.juryGroupMemberId,
|
|
selfServiceCap: pref?.cap ?? m.currentCap,
|
|
selfServiceRatio: pref?.ratio ?? m.preferredStartupRatio ?? 0.5,
|
|
}
|
|
})
|
|
confirmMutation.mutate({ preferences })
|
|
}
|
|
|
|
if (isLoading || unconfirmed.length === 0) return null
|
|
|
|
return (
|
|
<Card className="border-amber-300 bg-amber-50/50">
|
|
<CardHeader className="pb-3">
|
|
<div className="flex items-center gap-2">
|
|
<Scale className="h-5 w-5 text-amber-600" />
|
|
<CardTitle className="text-base">Confirm Your Evaluation Preferences</CardTitle>
|
|
</div>
|
|
<CardDescription>
|
|
Please review and confirm your assignment preferences before evaluations begin.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{unconfirmed.map((m) => {
|
|
const pref = prefs.get(m.juryGroupMemberId)
|
|
const capValue = pref?.cap ?? m.currentCap
|
|
const ratioValue = pref?.ratio ?? m.preferredStartupRatio ?? 0.5
|
|
|
|
return (
|
|
<div key={m.juryGroupMemberId} className="rounded-lg border bg-background p-4 space-y-4">
|
|
<h4 className="font-medium text-sm">{m.juryGroupName}</h4>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="text-xs text-muted-foreground">
|
|
Maximum assignments: {capValue}
|
|
</Label>
|
|
<Slider
|
|
value={[capValue]}
|
|
onValueChange={([v]) => updatePref(m.juryGroupMemberId, 'cap', v)}
|
|
min={1}
|
|
max={50}
|
|
step={1}
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
Admin suggestion: {m.currentCap}. Adjust to match your availability.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label className="text-xs text-muted-foreground">
|
|
Category preference: {Math.round(ratioValue * 100)}% Startups / {Math.round((1 - ratioValue) * 100)}% Business Concepts
|
|
</Label>
|
|
<Slider
|
|
value={[ratioValue * 100]}
|
|
onValueChange={([v]) => updatePref(m.juryGroupMemberId, 'ratio', v / 100)}
|
|
min={0}
|
|
max={100}
|
|
step={10}
|
|
/>
|
|
<div className="flex justify-between text-xs text-muted-foreground">
|
|
<span>More Business Concepts</span>
|
|
<span>More Startups</span>
|
|
</div>
|
|
<p className="text-xs text-muted-foreground/70 italic">
|
|
This is a preference, not a guarantee. Due to the number of projects, the system will try to match your preference but exact ratios cannot be ensured.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
)
|
|
})}
|
|
|
|
<Button
|
|
onClick={handleConfirm}
|
|
disabled={confirming}
|
|
className="w-full"
|
|
>
|
|
{confirming ? (
|
|
<>
|
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
|
Confirming...
|
|
</>
|
|
) : (
|
|
<>
|
|
<CheckCircle2 className="h-4 w-4 mr-2" />
|
|
Confirm Preferences
|
|
</>
|
|
)}
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
)
|
|
}
|