From cac0cfdc3bc4c988f5d2995bace24ffe56e66db4 Mon Sep 17 00:00:00 2001 From: Ishant5436 Date: Fri, 10 Jul 2026 01:58:42 +0530 Subject: [PATCH] feat: implement real-time BountyCountdownTimer component (#826) --- frontend/src/components/bounty/BountyCard.tsx | 8 +-- .../bounty/BountyCountdownTimer.tsx | 64 +++++++++++++++++++ .../src/components/bounty/BountyDetail.tsx | 7 +- 3 files changed, 70 insertions(+), 9 deletions(-) create mode 100644 frontend/src/components/bounty/BountyCountdownTimer.tsx diff --git a/frontend/src/components/bounty/BountyCard.tsx b/frontend/src/components/bounty/BountyCard.tsx index aa974a47..d59bd462 100644 --- a/frontend/src/components/bounty/BountyCard.tsx +++ b/frontend/src/components/bounty/BountyCard.tsx @@ -4,7 +4,8 @@ import { motion } from 'framer-motion'; 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 { formatCurrency, LANG_COLORS } from '../../lib/utils'; +import { BountyCountdownTimer } from './BountyCountdownTimer'; 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/BountyCountdownTimer.tsx b/frontend/src/components/bounty/BountyCountdownTimer.tsx new file mode 100644 index 00000000..648eb7fe --- /dev/null +++ b/frontend/src/components/bounty/BountyCountdownTimer.tsx @@ -0,0 +1,64 @@ +import React, { useState, useEffect } from 'react'; +import { Clock } from 'lucide-react'; + +interface BountyCountdownTimerProps { + deadline: string; +} + +export function BountyCountdownTimer({ deadline }: BountyCountdownTimerProps) { + const [timeLeft, setTimeLeft] = useState(calculateTimeLeft(deadline)); + + useEffect(() => { + const timer = setInterval(() => { + setTimeLeft(calculateTimeLeft(deadline)); + }, 1000); + + return () => clearInterval(timer); + }, [deadline]); + + if (timeLeft.isExpired) { + return ( + + + Expired + + ); + } + + const totalHours = timeLeft.days * 24 + timeLeft.hours; + let colorClass = 'text-text-muted'; // default + + if (totalHours < 1) { + colorClass = 'text-status-error'; // urgent (< 1 hr) + } else if (totalHours < 24) { + colorClass = 'text-status-warning'; // warning (< 24 hr) + } + + const parts = []; + if (timeLeft.days > 0) parts.push(`${timeLeft.days}d`); + if (timeLeft.hours > 0 || timeLeft.days > 0) parts.push(`${timeLeft.hours}h`); + parts.push(`${timeLeft.minutes}m`); + + return ( + + + {parts.join(' ')} + + ); +} + +function calculateTimeLeft(deadline: string) { + const difference = new Date(deadline).getTime() - new Date().getTime(); + + if (difference <= 0) { + return { days: 0, hours: 0, minutes: 0, seconds: 0, isExpired: true }; + } + + return { + days: Math.floor(difference / (1000 * 60 * 60 * 24)), + hours: Math.floor((difference / (1000 * 60 * 60)) % 24), + minutes: Math.floor((difference / 1000 / 60) % 60), + seconds: Math.floor((difference / 1000) % 60), + isExpired: false, + }; +} diff --git a/frontend/src/components/bounty/BountyDetail.tsx b/frontend/src/components/bounty/BountyDetail.tsx index 65653fa8..e16951d0 100644 --- a/frontend/src/components/bounty/BountyDetail.tsx +++ b/frontend/src/components/bounty/BountyDetail.tsx @@ -3,9 +3,10 @@ import { Link, useNavigate } from 'react-router-dom'; 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 { timeAgo, formatCurrency, LANG_COLORS } from '../../lib/utils'; import { useAuth } from '../../hooks/useAuth'; import { SubmissionForm } from './SubmissionForm'; +import { BountyCountdownTimer } from './BountyCountdownTimer'; import { fadeIn } from '../../lib/animations'; interface BountyDetailProps { @@ -138,9 +139,7 @@ export function BountyDetail({ bounty }: BountyDetailProps) { {bounty.deadline && (
Deadline - - {timeLeft(bounty.deadline)} - +
)}