Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions frontend/src/components/bounty/BountyCard.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { motion } from 'framer-motion';
import { GitPullRequest, Clock } from 'lucide-react';
import { GitPullRequest } 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 { BountyCountdown } from './BountyCountdown';

function TierBadge({ tier }: { tier: string }) {
const styles: Record<string, string> = {
Expand Down Expand Up @@ -110,12 +111,7 @@ export function BountyCard({ bounty }: BountyCardProps) {
<GitPullRequest className="w-3.5 h-3.5" />
{bounty.submission_count} PRs
</span>
{bounty.deadline && (
<span className="inline-flex items-center gap-1">
<Clock className="w-3.5 h-3.5" />
{timeLeft(bounty.deadline)}
</span>
)}
<BountyCountdown deadline={bounty.deadline} />
</div>
</div>

Expand Down
39 changes: 39 additions & 0 deletions frontend/src/components/bounty/BountyCountdown.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import React, { useEffect, useState } from 'react';
import { Clock } from 'lucide-react';
import { deadlineUrgency, timeLeft } from '../../lib/utils';

interface BountyCountdownProps {
deadline?: string | null;
className?: string;
}

export function BountyCountdown({ deadline, className = '' }: BountyCountdownProps) {
const [, setTick] = useState(0);

useEffect(() => {
if (!deadline) return;
const id = window.setInterval(() => setTick((tick) => tick + 1), 60000);
return () => window.clearInterval(id);
}, [deadline]);

if (!deadline) return null;

const urgency = deadlineUrgency(deadline);
const color = {
normal: 'text-text-muted',
warning: 'text-status-warning',
urgent: 'text-status-error',
expired: 'text-text-muted',
}[urgency];

return (
<span
className={`inline-flex items-center gap-1 ${color} ${className}`}
data-testid={urgency === 'urgent' ? 'urgent-indicator' : undefined}
aria-label={`Bounty deadline: ${timeLeft(deadline)}`}
>
<Clock className="w-3.5 h-3.5" />
{timeLeft(deadline)}
</span>
);
}
9 changes: 4 additions & 5 deletions frontend/src/components/bounty/BountyDetail.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import React, { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { motion } from 'framer-motion';
import { ArrowLeft, Clock, GitPullRequest, ExternalLink, Loader2, Check, Copy } from 'lucide-react';
import { ArrowLeft, 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 { BountyCountdown } from './BountyCountdown';
import { useAuth } from '../../hooks/useAuth';
import { SubmissionForm } from './SubmissionForm';
import { fadeIn } from '../../lib/animations';
Expand Down Expand Up @@ -138,9 +139,7 @@ export function BountyDetail({ bounty }: BountyDetailProps) {
{bounty.deadline && (
<div className="flex items-center justify-between text-sm">
<span className="text-text-muted">Deadline</span>
<span className="font-mono text-status-warning inline-flex items-center gap-1">
<Clock className="w-3.5 h-3.5" /> {timeLeft(bounty.deadline)}
</span>
<BountyCountdown deadline={bounty.deadline} className="font-mono" />
</div>
)}
<div className="flex items-center justify-between text-sm">
Expand Down
28 changes: 28 additions & 0 deletions frontend/src/lib/animations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export const fadeIn = {
initial: { opacity: 0, y: 8 },
animate: { opacity: 1, y: 0, transition: { duration: 0.18 } },
};

export const pageTransition = fadeIn;

export const cardHover = {
rest: { y: 0 },
hover: { y: -2, transition: { duration: 0.16 } },
};

export const buttonHover = {
whileHover: { scale: 1.02 },
whileTap: { scale: 0.98 },
};

export const staggerContainer = {
initial: {},
animate: { transition: { staggerChildren: 0.06 } },
};

export const staggerItem = fadeIn;

export const slideInRight = {
initial: { opacity: 0, x: 16 },
animate: { opacity: 1, x: 0, transition: { duration: 0.2 } },
};
41 changes: 41 additions & 0 deletions frontend/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
export const LANG_COLORS: Record<string, string> = {
TypeScript: '#3178c6', JavaScript: '#f7df1e', React: '#61dafb', Rust: '#dea584', Solana: '#14f195', Solidity: '#627eea', Python: '#3776ab', Go: '#00add8', CSS: '#563d7c', HTML: '#e34c26', TS: '#3178c6'
};

export function formatCurrency(amount: number, token = 'USDC'): string {
const value = Number(amount ?? 0);
const compact = value >= 1000 ? `${Number((value / 1000).toFixed(1))}k` : `${value}`;
return `${compact} ${token}`;
}

export function timeAgo(input: string): string {
const diff = Date.now() - new Date(input).getTime();
if (!Number.isFinite(diff)) return 'unknown';
const mins = Math.floor(diff / 60000);
if (mins < 1) return 'just now';
if (mins < 60) return `${mins}m ago`;
const hours = Math.floor(mins / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
return `${days}d ago`;
}

export function timeLeft(input: string): string {
const diff = new Date(input).getTime() - Date.now();
if (!Number.isFinite(diff) || diff <= 0) return 'Expired';
const minutes = Math.floor(diff / 60000) % 60;
const hours = Math.floor(diff / 3600000) % 24;
const days = Math.floor(diff / 86400000);
if (days > 0) return `${days}d ${hours}h`;
if (hours > 0) return `${hours}h ${minutes}m`;
return `${minutes}m`;
}

export function deadlineUrgency(input?: string | null): 'normal' | 'warning' | 'urgent' | 'expired' {
if (!input) return 'normal';
const diff = new Date(input).getTime() - Date.now();
if (!Number.isFinite(diff) || diff <= 0) return 'expired';
if (diff < 3600000) return 'urgent';
if (diff < 86400000) return 'warning';
return 'normal';
}