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>
134 lines
4.6 KiB
TypeScript
134 lines
4.6 KiB
TypeScript
'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>
|
|
)
|
|
}
|