Platform-wide visual overhaul, team invites, analytics improvements, and deployment hardening

UI overhaul applying jury dashboard design patterns across all pages:
- Stat cards with border-l-4 accent + icon pills on admin, observer, mentor, applicant dashboards and reports
- Card section headers with color-coded icon pills throughout
- Hover lift effects (translate-y + shadow) on cards and list items
- Gradient progress bars (brand-teal to brand-blue) platform-wide
- AnimatedCard stagger animations on all dashboard sections
- Auth pages with gradient accent strip and polished icon containers
- EmptyState component upgraded with rounded icon pill containers
- Replaced AI-looking icons (Brain/Sparkles/Bot/Wand2/Cpu) with descriptive alternatives across 12 files
- Removed gradient overlay from jury dashboard header
- Quick actions restyled as card links with group hover effects

Backend improvements:
- Team member invite emails with account setup flow and notification logging
- Analytics routers accept edition-wide queries (programId) in addition to roundId
- Round detail endpoint returns inline progress data (eliminates extra getProgress call)
- Award voting endpoints parallelized with Promise.all
- Bulk invite supports optional sendInvitation flag
- AwardVote composite index migration for query performance

Infrastructure:
- Docker entrypoint with migration retry loop (configurable retries/delay)
- docker-compose pull_policy: always for automatic image refresh
- Simplified deploy/update scripts using docker compose up -d --pull always
- Updated deployment documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-11 13:20:52 +01:00
parent 98f4a957cc
commit ce4069bf92
59 changed files with 1949 additions and 913 deletions

View File

@@ -50,6 +50,7 @@ import {
CommandItem,
CommandList,
} from '@/components/ui/command'
import { Switch } from '@/components/ui/switch'
import {
ArrowLeft,
ArrowRight,
@@ -65,6 +66,8 @@ import {
ChevronDown,
Check,
Tags,
Mail,
MailX,
} from 'lucide-react'
import { cn } from '@/lib/utils'
@@ -257,10 +260,12 @@ export default function MemberInvitePage() {
const [rows, setRows] = useState<MemberRow[]>([createEmptyRow()])
const [parsedUsers, setParsedUsers] = useState<ParsedUser[]>([])
const [sendProgress, setSendProgress] = useState(0)
const [sendInvitation, setSendInvitation] = useState(true)
const [result, setResult] = useState<{
created: number
skipped: number
assignmentsCreated?: number
invitationSent?: boolean
} | null>(null)
// Pre-assignment state
@@ -505,6 +510,7 @@ export default function MemberInvitePage() {
expertiseTags: u.expertiseTags,
assignments: u.assignments,
})),
sendInvitation,
})
setSendProgress(100)
setResult(result)
@@ -520,6 +526,7 @@ export default function MemberInvitePage() {
setParsedUsers([])
setResult(null)
setSendProgress(0)
setSendInvitation(true)
}
const hasManualData = rows.some((r) => r.email.trim() || r.name.trim())
@@ -793,6 +800,32 @@ export default function MemberInvitePage() {
</div>
)}
{/* Invitation toggle */}
<div className="rounded-lg border border-dashed p-4 bg-muted/30">
<div className="flex items-center gap-3">
{sendInvitation ? (
<Mail className="h-5 w-5 text-primary shrink-0" />
) : (
<MailX className="h-5 w-5 text-muted-foreground shrink-0" />
)}
<div className="flex-1 min-w-0">
<Label htmlFor="send-invitation" className="text-sm font-medium cursor-pointer">
Send platform invitation immediately
</Label>
<p className="text-xs text-muted-foreground">
{sendInvitation
? 'Members will receive an email invitation to create their account'
: 'Members will be created without notification — you can send invitations later from the Members page'}
</p>
</div>
<Switch
id="send-invitation"
checked={sendInvitation}
onCheckedChange={setSendInvitation}
/>
</div>
</div>
{/* Actions */}
<div className="flex justify-between pt-4">
<Button variant="outline" asChild>
@@ -844,6 +877,18 @@ export default function MemberInvitePage() {
</div>
</div>
{!sendInvitation && (
<div className="flex items-start gap-3 rounded-lg bg-blue-500/10 p-4 text-blue-700 dark:text-blue-400">
<MailX className="h-5 w-5 shrink-0 mt-0.5" />
<div>
<p className="font-medium">No invitations will be sent</p>
<p className="text-sm opacity-80">
Members will be created with &ldquo;Not Invited&rdquo; status. You can send invitations later from the Members page.
</p>
</div>
</div>
)}
{summary.invalid > 0 && (
<div className="flex items-start gap-3 rounded-lg bg-amber-500/10 p-4 text-amber-700">
<AlertCircle className="h-5 w-5 shrink-0 mt-0.5" />
@@ -924,10 +969,12 @@ export default function MemberInvitePage() {
>
{bulkCreate.isPending ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : sendInvitation ? (
<Mail className="mr-2 h-4 w-4" />
) : (
<Users className="mr-2 h-4 w-4" />
)}
Create & Invite {summary.valid} Member
{sendInvitation ? 'Create & Invite' : 'Create'} {summary.valid} Member
{summary.valid !== 1 ? 's' : ''}
</Button>
</div>
@@ -948,7 +995,7 @@ export default function MemberInvitePage() {
<CardContent className="flex flex-col items-center justify-center py-12">
<Loader2 className="h-12 w-12 animate-spin text-primary" />
<p className="mt-4 font-medium">
Creating members and sending invitations...
{sendInvitation ? 'Creating members and sending invitations...' : 'Creating members...'}
</p>
<Progress value={sendProgress} className="mt-4 w-48" />
</CardContent>
@@ -963,23 +1010,28 @@ export default function MemberInvitePage() {
<CheckCircle2 className="h-8 w-8 text-green-600" />
</div>
<p className="mt-4 text-xl font-semibold">
Invitations Sent!
{result?.invitationSent ? 'Members Created & Invited!' : 'Members Created!'}
</p>
<p className="text-muted-foreground text-center max-w-sm mt-2">
{result?.created} member{result?.created !== 1 ? 's' : ''}{' '}
created and invited.
{result?.invitationSent ? 'created and invited' : 'created'}.
{result?.skipped
? ` ${result.skipped} skipped (already exist).`
: ''}
{result?.assignmentsCreated && result.assignmentsCreated > 0
? ` ${result.assignmentsCreated} project assignment${result.assignmentsCreated !== 1 ? 's' : ''} pre-assigned.`
: ''}
{!result?.invitationSent && (
<span className="block mt-1">
You can send invitations from the Members page when ready.
</span>
)}
</p>
<div className="mt-6 flex gap-3">
<Button variant="outline" asChild>
<Link href="/admin/members">View Members</Link>
</Button>
<Button onClick={resetForm}>Invite More</Button>
<Button onClick={resetForm}>Add More</Button>
</div>
</CardContent>
</Card>