diff --git a/frontend/src/components/bounty/BountyCard.tsx b/frontend/src/components/bounty/BountyCard.tsx index aa974a474..395020a36 100644 --- a/frontend/src/components/bounty/BountyCard.tsx +++ b/frontend/src/components/bounty/BountyCard.tsx @@ -5,6 +5,7 @@ import { GitPullRequest, Clock } from 'lucide-react'; import type { Bounty } from '../../types/bounty'; import { cardHover } from '../../lib/animations'; import { timeLeft, formatCurrency, LANG_COLORS } from '../../lib/utils'; +import { CountdownTimer } from './CountdownTimer'; function TierBadge({ tier }: { tier: string }) { const styles: Record = { @@ -111,10 +112,7 @@ export function BountyCard({ bounty }: BountyCardProps) { {bounty.submission_count} PRs {bounty.deadline && ( - - - {timeLeft(bounty.deadline)} - + )} diff --git a/frontend/src/components/bounty/BountyDetail.tsx b/frontend/src/components/bounty/BountyDetail.tsx index 65653fa8f..de959c152 100644 --- a/frontend/src/components/bounty/BountyDetail.tsx +++ b/frontend/src/components/bounty/BountyDetail.tsx @@ -4,6 +4,7 @@ import { motion } from 'framer-motion'; import { ArrowLeft, Clock, GitPullRequest, ExternalLink, Loader2, Check, Copy } from 'lucide-react'; import type { Bounty } from '../../types/bounty'; import { timeLeft, timeAgo, formatCurrency, LANG_COLORS } from '../../lib/utils'; +import { CountdownTimer } from './CountdownTimer'; import { useAuth } from '../../hooks/useAuth'; import { SubmissionForm } from './SubmissionForm'; import { fadeIn } from '../../lib/animations'; @@ -138,9 +139,7 @@ export function BountyDetail({ bounty }: BountyDetailProps) { {bounty.deadline && (
Deadline - - {timeLeft(bounty.deadline)} - +
)}
diff --git a/frontend/src/components/bounty/CountdownTimer.tsx b/frontend/src/components/bounty/CountdownTimer.tsx new file mode 100644 index 000000000..9fecb3626 --- /dev/null +++ b/frontend/src/components/bounty/CountdownTimer.tsx @@ -0,0 +1,84 @@ +import React, { useState, useEffect } from 'react'; +import { Clock } from 'lucide-react'; + +interface CountdownTimerProps { + deadline: string; + className?: string; + icon?: boolean; + size?: 'sm' | 'md' | 'lg'; +} + +interface TimeLeft { + total: number; + days: number; + hours: number; + minutes: number; + seconds: number; + expired: boolean; + warning: boolean; + urgent: boolean; +} + +function calculateTimeLeft(deadline: string): TimeLeft { + const total = new Date(deadline).getTime() - Date.now(); + if (total <= 0) { + return { total: 0, days: 0, hours: 0, minutes: 0, seconds: 0, expired: true, warning: false, urgent: false }; + } + + const seconds = Math.floor((total / 1000) % 60); + const minutes = Math.floor((total / 1000 / 60) % 60); + const hours = Math.floor((total / 1000 / 60 / 60) % 24); + const days = Math.floor(total / 1000 / 60 / 60 / 24); + + return { + total, + days, + hours, + minutes, + seconds, + expired: false, + warning: total < 24 * 60 * 60 * 1000 && total > 60 * 60 * 1000, + urgent: total <= 60 * 60 * 1000, + }; +} + +export function CountdownTimer({ deadline, className = '', icon = true, size = 'md' }: CountdownTimerProps) { + const [time, setTime] = useState(calculateTimeLeft(deadline)); + + useEffect(() => { + const interval = setInterval(() => { + setTime(calculateTimeLeft(deadline)); + }, 1000); + return () => clearInterval(interval); + }, [deadline]); + + if (time.expired) { + const sizeClasses = { sm: 'text-xs', md: 'text-sm', lg: 'text-lg' }[size]; + return ( + + {icon && } + Expired + + ); + } + + const colorClass = time.urgent ? 'text-status-error' : time.warning ? 'text-status-warning' : 'text-text-secondary'; + const digitClass = time.urgent ? 'text-status-error' : time.warning ? 'text-status-warning' : 'text-text-primary'; + const sizeClasses = { sm: 'text-xs gap-0.5', md: 'text-sm gap-1', lg: 'text-base gap-1.5' }[size]; + const digitSizeClass = { sm: 'text-xs', md: 'text-sm', lg: 'text-lg' }[size]; + + const parts: string[] = []; + if (time.days > 0) parts.push(`${time.days}d`); + parts.push(`${String(time.hours).padStart(2, '0')}h`); + parts.push(`${String(time.minutes).padStart(2, '0')}m`); + if (size !== 'sm' || time.urgent) { + parts.push(`${String(time.seconds).padStart(2, '0')}s`); + } + + return ( + + {icon && } + {parts.join(' ')} + + ); +}