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:
@@ -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 “Not Invited” 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>
|
||||
|
||||
Reference in New Issue
Block a user