feat: round finalization with ranking-based outcomes + award pool notifications
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m0s
All checks were successful
Build and Push Docker Image / build (push) Successful in 10m0s
- processRoundClose EVALUATION uses ranking scores + advanceMode config (threshold vs count) to auto-set proposedOutcome instead of defaulting all to PASSED - Advancement emails generate invite tokens for passwordless users with "Create Your Account" CTA; rejection emails have no link - Finalization UI shows account stats (invite vs dashboard link counts) - Fixed getFinalizationSummary ranking query (was using non-existent rankingsJson) - New award pool notification system: getAwardSelectionNotificationTemplate email, notifyEligibleProjects mutation with invite token generation, "Notify Pool" button on award detail page with custom message dialog Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
133
src/components/applicant/mentoring-request-card.tsx
Normal file
133
src/components/applicant/mentoring-request-card.tsx
Normal file
@@ -0,0 +1,133 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { MessageSquare, Clock, CheckCircle, XCircle } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
interface MentoringRequestCardProps {
|
||||
projectId: string
|
||||
roundId: string
|
||||
roundName: string
|
||||
}
|
||||
|
||||
export function MentoringRequestCard({ projectId, roundId, roundName }: MentoringRequestCardProps) {
|
||||
const [timeLeft, setTimeLeft] = useState('')
|
||||
|
||||
const { data: status, isLoading } = trpc.applicant.getMentoringRequestStatus.useQuery(
|
||||
{ projectId, roundId },
|
||||
{ refetchInterval: 60_000 },
|
||||
)
|
||||
|
||||
const utils = trpc.useUtils()
|
||||
const requestMutation = trpc.applicant.requestMentoring.useMutation({
|
||||
onSuccess: (data) => {
|
||||
toast.success(data.requesting ? 'Mentoring requested' : 'Mentoring request cancelled')
|
||||
utils.applicant.getMentoringRequestStatus.invalidate({ projectId, roundId })
|
||||
utils.applicant.getMyDashboard.invalidate()
|
||||
},
|
||||
onError: (error) => toast.error(error.message),
|
||||
})
|
||||
|
||||
// Countdown timer
|
||||
useEffect(() => {
|
||||
if (!status?.deadline) return
|
||||
const update = () => {
|
||||
const now = new Date()
|
||||
const deadline = new Date(status.deadline!)
|
||||
const diff = deadline.getTime() - now.getTime()
|
||||
if (diff <= 0) {
|
||||
setTimeLeft('Expired')
|
||||
return
|
||||
}
|
||||
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
|
||||
const hours = Math.floor((diff % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60))
|
||||
const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))
|
||||
if (days > 0) setTimeLeft(`${days}d ${hours}h remaining`)
|
||||
else if (hours > 0) setTimeLeft(`${hours}h ${minutes}m remaining`)
|
||||
else setTimeLeft(`${minutes}m remaining`)
|
||||
}
|
||||
update()
|
||||
const interval = setInterval(update, 60_000)
|
||||
return () => clearInterval(interval)
|
||||
}, [status?.deadline])
|
||||
|
||||
if (isLoading || !status?.available) return null
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<CardTitle className="flex items-center gap-2 text-base">
|
||||
<MessageSquare className="h-5 w-5" />
|
||||
Mentoring — {roundName}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{status.requested ? (
|
||||
<div className="flex items-start gap-3">
|
||||
<CheckCircle className="h-5 w-5 text-green-500 mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">Mentoring requested</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{status.requestedAt
|
||||
? `Requested on ${new Date(status.requestedAt).toLocaleDateString()}`
|
||||
: 'Awaiting mentor assignment'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-start gap-3">
|
||||
<XCircle className="h-5 w-5 text-muted-foreground mt-0.5 shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">Not requesting mentoring</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
You will advance automatically without a mentoring period.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Deadline info */}
|
||||
{status.deadline && (
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<Clock className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground">Request window:</span>
|
||||
{status.canStillRequest ? (
|
||||
<Badge variant="outline" className="text-amber-600 border-amber-300">
|
||||
{timeLeft}
|
||||
</Badge>
|
||||
) : (
|
||||
<Badge variant="secondary">Closed</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Action button */}
|
||||
{status.canStillRequest && (
|
||||
<Button
|
||||
variant={status.requested ? 'outline' : 'default'}
|
||||
size="sm"
|
||||
className="w-full"
|
||||
onClick={() => requestMutation.mutate({ projectId, roundId, requesting: !status.requested })}
|
||||
disabled={requestMutation.isPending}
|
||||
>
|
||||
{requestMutation.isPending
|
||||
? 'Updating...'
|
||||
: status.requested
|
||||
? 'Cancel Mentoring Request'
|
||||
: 'Request Mentoring'}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{!status.canStillRequest && (
|
||||
<p className="text-xs text-muted-foreground text-center">
|
||||
The mentoring request window has closed.
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user