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:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user