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

@@ -27,7 +27,8 @@ import { Skeleton } from '@/components/ui/skeleton'
import { UserAvatar } from '@/components/shared/user-avatar'
import { UserActions, UserMobileActions } from '@/components/admin/user-actions'
import { Pagination } from '@/components/shared/pagination'
import { Plus, Users, Search } from 'lucide-react'
import { Plus, Users, Search, Mail, Loader2 } from 'lucide-react'
import { toast } from 'sonner'
import { formatRelativeTime } from '@/lib/utils'
type RoleValue = 'SUPER_ADMIN' | 'PROGRAM_ADMIN' | 'JURY_MEMBER' | 'MENTOR' | 'OBSERVER'
@@ -60,6 +61,39 @@ const roleColors: Record<string, 'default' | 'outline' | 'secondary'> = {
SUPER_ADMIN: 'destructive' as 'default',
}
function InlineSendInvite({ userId, userEmail }: { userId: string; userEmail: string }) {
const utils = trpc.useUtils()
const sendInvitation = trpc.user.sendInvitation.useMutation({
onSuccess: () => {
toast.success(`Invitation sent to ${userEmail}`)
utils.user.list.invalidate()
},
onError: (error) => {
toast.error(error.message || 'Failed to send invitation')
},
})
return (
<Button
variant="outline"
size="sm"
className="h-6 text-xs gap-1 px-2"
onClick={(e) => {
e.stopPropagation()
sendInvitation.mutate({ userId })
}}
disabled={sendInvitation.isPending}
>
{sendInvitation.isPending ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<Mail className="h-3 w-3" />
)}
Send Invite
</Button>
)
}
export function MembersContent() {
const searchParams = useSearchParams()
@@ -124,7 +158,7 @@ export function MembersContent() {
<Button asChild>
<Link href="/admin/members/invite">
<Plus className="mr-2 h-4 w-4" />
Invite Member
Add Member
</Link>
</Button>
</div>
@@ -223,9 +257,14 @@ export function MembersContent() {
</div>
</TableCell>
<TableCell>
<Badge variant={statusColors[user.status] || 'secondary'}>
{statusLabels[user.status] || user.status}
</Badge>
<div className="flex items-center gap-2">
<Badge variant={statusColors[user.status] || 'secondary'}>
{statusLabels[user.status] || user.status}
</Badge>
{user.status === 'NONE' && (
<InlineSendInvite userId={user.id} userEmail={user.email} />
)}
</div>
</TableCell>
<TableCell>
{user.lastLoginAt ? (
@@ -272,9 +311,14 @@ export function MembersContent() {
</CardDescription>
</div>
</div>
<Badge variant={statusColors[user.status] || 'secondary'}>
{statusLabels[user.status] || user.status}
</Badge>
<div className="flex flex-col items-end gap-1.5">
<Badge variant={statusColors[user.status] || 'secondary'}>
{statusLabels[user.status] || user.status}
</Badge>
{user.status === 'NONE' && (
<InlineSendInvite userId={user.id} userEmail={user.email} />
)}
</div>
</div>
</CardHeader>
<CardContent className="space-y-3">