Comprehensive platform audit: security, UX, performance, and visual polish
Phase 1: Security - status transition validation, Zod tightening, DB indexes, transactions Phase 2: Admin UX - search/filter for awards, learning, partners pages Phase 3: Dashboard - Recent Activity feed, Pending Actions card, quick actions Phase 4: Jury - assignments progress/urgency, autosave indicator, divergence highlighting Phase 5: Portals - observer charts, mentor search, login/onboarding polish Phase 6: Messages preview dialog, CsvExportDialog with column selection Phase 7: Performance - query optimizations, loading skeletons, useDebounce hook Phase 8: Visual - AnimatedCard, hover effects, StatusBadge, empty state CTAs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -41,6 +41,14 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import {
|
||||
Send,
|
||||
Mail,
|
||||
@@ -51,6 +59,7 @@ import {
|
||||
AlertCircle,
|
||||
Inbox,
|
||||
CheckCircle2,
|
||||
Eye,
|
||||
} from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { formatDate } from '@/lib/utils'
|
||||
@@ -79,6 +88,7 @@ export default function MessagesPage() {
|
||||
const [deliveryChannels, setDeliveryChannels] = useState<string[]>(['EMAIL', 'IN_APP'])
|
||||
const [isScheduled, setIsScheduled] = useState(false)
|
||||
const [scheduledAt, setScheduledAt] = useState('')
|
||||
const [showPreview, setShowPreview] = useState(false)
|
||||
|
||||
const utils = trpc.useUtils()
|
||||
|
||||
@@ -152,7 +162,42 @@ export default function MessagesPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const handleSend = () => {
|
||||
const getRecipientDescription = (): string => {
|
||||
switch (recipientType) {
|
||||
case 'ALL':
|
||||
return 'All platform users'
|
||||
case 'ROLE': {
|
||||
const roleLabel = selectedRole ? selectedRole.replace(/_/g, ' ') : ''
|
||||
return roleLabel ? `All ${roleLabel}s` : 'By Role (none selected)'
|
||||
}
|
||||
case 'ROUND_JURY': {
|
||||
if (!roundId) return 'Round Jury (none selected)'
|
||||
const round = (rounds as Array<{ id: string; name: string; program?: { name: string } }> | undefined)?.find(
|
||||
(r) => r.id === roundId
|
||||
)
|
||||
return round
|
||||
? `Jury of ${round.program ? `${round.program.name} - ` : ''}${round.name}`
|
||||
: 'Round Jury'
|
||||
}
|
||||
case 'PROGRAM_TEAM': {
|
||||
if (!selectedProgramId) return 'Program Team (none selected)'
|
||||
const program = (programs as Array<{ id: string; name: string }> | undefined)?.find(
|
||||
(p) => p.id === selectedProgramId
|
||||
)
|
||||
return program ? `Team of ${program.name}` : 'Program Team'
|
||||
}
|
||||
case 'USER': {
|
||||
if (!selectedUserId) return 'Specific User (none selected)'
|
||||
const userList = (users as { users: Array<{ id: string; name: string | null; email: string }> } | undefined)?.users
|
||||
const user = userList?.find((u) => u.id === selectedUserId)
|
||||
return user ? (user.name || user.email) : 'Specific User'
|
||||
}
|
||||
default:
|
||||
return 'Unknown'
|
||||
}
|
||||
}
|
||||
|
||||
const handlePreview = () => {
|
||||
if (!subject.trim()) {
|
||||
toast.error('Subject is required')
|
||||
return
|
||||
@@ -182,6 +227,10 @@ export default function MessagesPage() {
|
||||
return
|
||||
}
|
||||
|
||||
setShowPreview(true)
|
||||
}
|
||||
|
||||
const handleActualSend = () => {
|
||||
sendMutation.mutate({
|
||||
recipientType,
|
||||
recipientFilter: buildRecipientFilter(),
|
||||
@@ -192,6 +241,7 @@ export default function MessagesPage() {
|
||||
scheduledAt: isScheduled && scheduledAt ? new Date(scheduledAt).toISOString() : undefined,
|
||||
templateId: selectedTemplateId && selectedTemplateId !== '__none__' ? selectedTemplateId : undefined,
|
||||
})
|
||||
setShowPreview(false)
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -474,13 +524,13 @@ export default function MessagesPage() {
|
||||
|
||||
{/* Send button */}
|
||||
<div className="flex justify-end">
|
||||
<Button onClick={handleSend} disabled={sendMutation.isPending}>
|
||||
<Button onClick={handlePreview} disabled={sendMutation.isPending}>
|
||||
{sendMutation.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Send className="mr-2 h-4 w-4" />
|
||||
<Eye className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{isScheduled ? 'Schedule' : 'Send Message'}
|
||||
{isScheduled ? 'Preview & Schedule' : 'Preview & Send'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -581,6 +631,68 @@ export default function MessagesPage() {
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* Preview Dialog */}
|
||||
<Dialog open={showPreview} onOpenChange={setShowPreview}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Preview Message</DialogTitle>
|
||||
<DialogDescription>Review your message before sending</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Recipients</p>
|
||||
<p className="text-sm mt-1">{getRecipientDescription()}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Subject</p>
|
||||
<p className="text-sm font-medium mt-1">{subject}</p>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Message</p>
|
||||
<div className="mt-1 rounded-lg border bg-muted/30 p-4">
|
||||
<p className="text-sm whitespace-pre-wrap">{body}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Delivery Channels</p>
|
||||
<div className="flex items-center gap-2 mt-1">
|
||||
{deliveryChannels.includes('EMAIL') && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
<Mail className="mr-1 h-3 w-3" />
|
||||
Email
|
||||
</Badge>
|
||||
)}
|
||||
{deliveryChannels.includes('IN_APP') && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
<Bell className="mr-1 h-3 w-3" />
|
||||
In-App
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isScheduled && scheduledAt && (
|
||||
<div>
|
||||
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wider">Scheduled For</p>
|
||||
<p className="text-sm mt-1">{formatDate(new Date(scheduledAt))}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowPreview(false)}>
|
||||
Edit
|
||||
</Button>
|
||||
<Button onClick={handleActualSend} disabled={sendMutation.isPending}>
|
||||
{sendMutation.isPending ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Send className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
{isScheduled ? 'Confirm & Schedule' : 'Confirm & Send'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user