-
Notifications
You must be signed in to change notification settings - Fork 24
feat: Built BountyCard component with shadcn composition #17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8efab73
b1bbbf2
97b9c87
7f40ae5
472495b
8b74d84
b40b19c
f487bb9
328ba0b
6a8fdc9
dc49879
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,14 @@ | ||
| import SignIn from "@/components/login/sign-in"; | ||
| import Image from "next/image"; | ||
|
|
||
|
|
||
|
|
||
| export default async function Home() { | ||
| return ( | ||
| <div className="flex justify-center items-center h-screen"> | ||
| <SignIn /> | ||
| </div> | ||
|
|
||
|
|
||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,145 +1,193 @@ | ||||||||||||||||||||||||||||||
| import Link from "next/link" | ||||||||||||||||||||||||||||||
| import Image from "next/image" | ||||||||||||||||||||||||||||||
| import { formatDistanceToNow } from "date-fns" | ||||||||||||||||||||||||||||||
| import { Github, Bug, Sparkles, FileText, RefreshCw, Circle } from "lucide-react" | ||||||||||||||||||||||||||||||
| import { Bounty, BountyType } from "@/types/bounty" | ||||||||||||||||||||||||||||||
| import { Badge } from "@/components/ui/badge" | ||||||||||||||||||||||||||||||
| import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card" | ||||||||||||||||||||||||||||||
| import { cn } from "@/lib/utils" | ||||||||||||||||||||||||||||||
| "use client"; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||
| Card, | ||||||||||||||||||||||||||||||
| CardDescription, | ||||||||||||||||||||||||||||||
| CardFooter, | ||||||||||||||||||||||||||||||
| CardHeader, | ||||||||||||||||||||||||||||||
| CardTitle, | ||||||||||||||||||||||||||||||
| } from "@/components/ui/card"; | ||||||||||||||||||||||||||||||
| import { Badge } from "@/components/ui/badge"; | ||||||||||||||||||||||||||||||
| import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; | ||||||||||||||||||||||||||||||
| import { Clock, Users } from "lucide-react"; | ||||||||||||||||||||||||||||||
| import { cn } from "@/lib/utils"; | ||||||||||||||||||||||||||||||
| import { formatDistanceToNow } from "date-fns"; | ||||||||||||||||||||||||||||||
| import { Bounty } from "@/types/bounty"; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| interface BountyCardProps { | ||||||||||||||||||||||||||||||
| bounty: Bounty | ||||||||||||||||||||||||||||||
| bounty: Bounty; | ||||||||||||||||||||||||||||||
| onClick?: () => void; | ||||||||||||||||||||||||||||||
| variant?: "grid" | "list"; | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const typeConfig: Record<BountyType, { label: string; icon: React.ReactNode; className: string }> = { | ||||||||||||||||||||||||||||||
| bug: { label: "Bug", icon: <Bug className="size-3" />, className: "bg-error-500 text-white border-transparent" }, | ||||||||||||||||||||||||||||||
| feature: { label: "Feature", icon: <Sparkles className="size-3" />, className: "bg-primary text-primary-foreground border-transparent" }, | ||||||||||||||||||||||||||||||
| documentation: { label: "Docs", icon: <FileText className="size-3" />, className: "bg-secondary-500 text-white border-transparent" }, | ||||||||||||||||||||||||||||||
| refactor: { label: "Refactor", icon: <RefreshCw className="size-3" />, className: "bg-gray-700 text-gray-100 border-transparent" }, | ||||||||||||||||||||||||||||||
| other: { label: "Other", icon: <Circle className="size-3" />, className: "bg-gray-800 text-gray-300 border-gray-600" }, | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| const statusConfig = { | ||||||||||||||||||||||||||||||
| open: { | ||||||||||||||||||||||||||||||
| variant: "default" as const, | ||||||||||||||||||||||||||||||
| label: "Open", | ||||||||||||||||||||||||||||||
| dotColor: "bg-emerald-500", | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
| claimed: { | ||||||||||||||||||||||||||||||
| variant: "secondary" as const, | ||||||||||||||||||||||||||||||
| label: "Claimed", | ||||||||||||||||||||||||||||||
| dotColor: "bg-amber-500", | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
| closed: { | ||||||||||||||||||||||||||||||
| variant: "outline" as const, | ||||||||||||||||||||||||||||||
| label: "Closed", | ||||||||||||||||||||||||||||||
| dotColor: "bg-slate-400", | ||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const difficultyColors: Record<string, string> = { | ||||||||||||||||||||||||||||||
| beginner: "text-success-400", | ||||||||||||||||||||||||||||||
| intermediate: "text-warning-400", | ||||||||||||||||||||||||||||||
| advanced: "text-error-400", | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| export function BountyCard({ | ||||||||||||||||||||||||||||||
| bounty, | ||||||||||||||||||||||||||||||
| onClick, | ||||||||||||||||||||||||||||||
| variant = "grid", | ||||||||||||||||||||||||||||||
| }: BountyCardProps) { | ||||||||||||||||||||||||||||||
| const status = statusConfig[bounty.status]; | ||||||||||||||||||||||||||||||
| const timeLeft = bounty.updatedAt | ||||||||||||||||||||||||||||||
| ? formatDistanceToNow(new Date(bounty.updatedAt), { addSuffix: true }) | ||||||||||||||||||||||||||||||
| : "N/A"; | ||||||||||||||||||||||||||||||
|
Comment on lines
+47
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clock should be based on the bounty deadline, not 🧭 Example update- const timeLeft = bounty.updatedAt
- ? formatDistanceToNow(new Date(bounty.updatedAt), { addSuffix: true })
+ const timeLeft = bounty.deadline
+ ? formatDistanceToNow(new Date(bounty.deadline), { addSuffix: true })
: "N/A";🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| const statusColors: Record<string, string> = { | ||||||||||||||||||||||||||||||
| open: "bg-success-500/10 text-success-500 border-success-500/20", | ||||||||||||||||||||||||||||||
| claimed: "bg-warning-500/10 text-warning-500 border-warning-500/20", | ||||||||||||||||||||||||||||||
| closed: "bg-gray-500/10 text-gray-500 border-gray-500/20", | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||
| <Card | ||||||||||||||||||||||||||||||
| className={cn( | ||||||||||||||||||||||||||||||
| "overflow-hidden w-full max-w-xs rounded-4xl cursor-pointer transition-all duration-300", | ||||||||||||||||||||||||||||||
| "hover:shadow-lg hover:border-primary/60 hover:scale-[1.02]", | ||||||||||||||||||||||||||||||
| "border border-slate-200 dark:border-slate-800", | ||||||||||||||||||||||||||||||
| variant === "list" && "flex flex-col", | ||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||
|
Comment on lines
53
to
58
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. List variant is still capped to 💡 Proposed fix- className={cn(
- "overflow-hidden w-full max-w-xs rounded-4xl cursor-pointer transition-all duration-300",
+ className={cn(
+ "overflow-hidden w-full rounded-4xl cursor-pointer transition-all duration-300",
+ variant === "grid" && "max-w-xs",
+ variant === "list" && "max-w-none",
"hover:shadow-lg hover:border-primary/60 hover:scale-[1.02]",
"border border-slate-200 dark:border-slate-800",
variant === "list" && "flex flex-col",
)}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| role="button" | ||||||||||||||||||||||||||||||
| tabIndex={0} | ||||||||||||||||||||||||||||||
| onClick={onClick} | ||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||
| onKeyDown={(e) => { | ||||||||||||||||||||||||||||||
| if (e.key === "Enter" || e.key === " ") { | ||||||||||||||||||||||||||||||
| e.preventDefault(); | ||||||||||||||||||||||||||||||
| onClick?.(); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
| }} | ||||||||||||||||||||||||||||||
|
Comment on lines
+59
to
+67
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid a focusable card when ♿ Suggested adjustment- role="button"
- tabIndex={0}
+ role={onClick ? "button" : undefined}
+ tabIndex={onClick ? 0 : undefined}
onClick={onClick}
onKeyDown={(e) => {
- if (e.key === "Enter" || e.key === " ") {
+ if (!onClick) return;
+ if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
- onClick?.();
+ onClick();
}
}}🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||
| {/* Main Content Section */} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| export function BountyCard({ bounty }: BountyCardProps) { | ||||||||||||||||||||||||||||||
| const typeInfo = typeConfig[bounty.type] | ||||||||||||||||||||||||||||||
| const difficultyColor = bounty.difficulty ? difficultyColors[bounty.difficulty] : "text-gray-400" | ||||||||||||||||||||||||||||||
| const statusColor = statusColors[bounty.status] || statusColors.closed | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| // Prevent card click when clicking interactive elements | ||||||||||||||||||||||||||||||
| const handleInteractiveClick = (e: React.MouseEvent) => { | ||||||||||||||||||||||||||||||
| e.stopPropagation() | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||
| <Card className="group h-full flex flex-col bg-background-card border-gray-800 transition-all duration-300 hover:border-primary/50 hover:shadow-md hover:shadow-primary/5 relative"> | ||||||||||||||||||||||||||||||
| <CardHeader className="p-5 pb-3 space-y-3"> | ||||||||||||||||||||||||||||||
| <div className="flex justify-between items-start gap-4"> | ||||||||||||||||||||||||||||||
| <div className="flex items-center gap-3"> | ||||||||||||||||||||||||||||||
| {bounty.projectLogoUrl ? ( | ||||||||||||||||||||||||||||||
| <div className="relative size-8 shrink-0 overflow-hidden rounded-md border border-gray-800"> | ||||||||||||||||||||||||||||||
| <Image | ||||||||||||||||||||||||||||||
| src={bounty.projectLogoUrl} | ||||||||||||||||||||||||||||||
| alt={bounty.projectName} | ||||||||||||||||||||||||||||||
| fill | ||||||||||||||||||||||||||||||
| className="object-cover" | ||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||
| <div className="size-8 shrink-0 rounded-md bg-gray-800 flex items-center justify-center text-xs font-bold text-gray-400"> | ||||||||||||||||||||||||||||||
| {(bounty.projectName || "Unknown").substring(0, 2).toUpperCase()} | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||
| <div> | ||||||||||||||||||||||||||||||
| <h3 className="text-sm font-medium text-gray-300 line-clamp-1">{bounty.projectName}</h3> | ||||||||||||||||||||||||||||||
| <span className="text-xs text-gray-500 flex items-center gap-1"> | ||||||||||||||||||||||||||||||
| {formatDistanceToNow(new Date(bounty.createdAt), { addSuffix: true })} | ||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <div className="flex flex-col items-end gap-1.5"> | ||||||||||||||||||||||||||||||
| <Badge variant="outline" className={cn("shrink-0 gap-1.5", typeInfo.className)}> | ||||||||||||||||||||||||||||||
| {typeInfo.icon} | ||||||||||||||||||||||||||||||
| {typeInfo.label} | ||||||||||||||||||||||||||||||
| </Badge> | ||||||||||||||||||||||||||||||
| <Badge variant="outline" className={cn("shrink-0 text-[10px] px-1.5 py-0 h-5 lowercase", statusColor)}> | ||||||||||||||||||||||||||||||
| {bounty.status} | ||||||||||||||||||||||||||||||
| </Badge> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||
| className={cn( | ||||||||||||||||||||||||||||||
| "flex-1 flex flex-col", | ||||||||||||||||||||||||||||||
| variant === "list" && "md:flex-row md:items-center", | ||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||
| <CardHeader | ||||||||||||||||||||||||||||||
| className={cn( | ||||||||||||||||||||||||||||||
| "pb-3 px-4 sm:px-5", | ||||||||||||||||||||||||||||||
| variant === "list" && "md:flex-1 md:pb-0", | ||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||
| {/* Header Row with Status and Reward */} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <div className="flex items-start justify-between gap-2 mb-3 flex-wrap"> | ||||||||||||||||||||||||||||||
| <div className="flex items-center gap-2"> | ||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||
| className={cn( | ||||||||||||||||||||||||||||||
| "w-2.5 h-2.5 rounded-full animate-pulse", | ||||||||||||||||||||||||||||||
| status.dotColor, | ||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||
| <Badge variant={status.variant} className="text-xs"> | ||||||||||||||||||||||||||||||
| {status.label} | ||||||||||||||||||||||||||||||
| </Badge> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <h2 className="text-lg font-bold text-gray-100 group-hover:text-primary transition-colors line-clamp-2 leading-tight"> | ||||||||||||||||||||||||||||||
| <Link href={`/bounty/${bounty.id}`} className="focus:outline-none after:absolute after:inset-0"> | ||||||||||||||||||||||||||||||
| {bounty.issueTitle} | ||||||||||||||||||||||||||||||
| </Link> | ||||||||||||||||||||||||||||||
| </h2> | ||||||||||||||||||||||||||||||
| </CardHeader> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <CardContent className="p-5 py-2 flex-grow"> | ||||||||||||||||||||||||||||||
| <p className="text-sm text-gray-400 line-clamp-3 mb-4"> | ||||||||||||||||||||||||||||||
| {bounty.description.replace(/[#*`_]/g, '') /* Simple stripped markdown preview */} | ||||||||||||||||||||||||||||||
| </p> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <div className="flex flex-wrap gap-2 mb-2"> | ||||||||||||||||||||||||||||||
| {bounty.tags.slice(0, 3).map(tag => ( | ||||||||||||||||||||||||||||||
| <Badge key={tag} variant="secondary" className="bg-gray-800/50 text-gray-400 border-gray-700/50 text-xs font-normal"> | ||||||||||||||||||||||||||||||
| {tag} | ||||||||||||||||||||||||||||||
| </Badge> | ||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||
| {bounty.tags.length > 3 && ( | ||||||||||||||||||||||||||||||
| <span className="text-xs text-gray-500 self-center">+{bounty.tags.length - 3}</span> | ||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||
| {variant === "grid" && bounty.rewardAmount && ( | ||||||||||||||||||||||||||||||
| <div className="text-right"> | ||||||||||||||||||||||||||||||
| <div className="text-lg font-bold text-slate-900 dark:text-slate-50"> | ||||||||||||||||||||||||||||||
| {bounty.rewardAmount.toLocaleString()} | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| </CardContent> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <CardFooter className="p-5 pt-3 mt-auto border-t border-gray-800/50 flex items-center justify-between"> | ||||||||||||||||||||||||||||||
| <div className="flex items-center gap-4"> | ||||||||||||||||||||||||||||||
| {(bounty.rewardAmount !== null && bounty.rewardAmount !== undefined) ? ( | ||||||||||||||||||||||||||||||
| <div className="flex flex-col"> | ||||||||||||||||||||||||||||||
| <span className="text-xs text-gray-500 uppercase tracking-wider font-medium">Reward</span> | ||||||||||||||||||||||||||||||
| <span className="font-bold text-primary"> | ||||||||||||||||||||||||||||||
| {bounty.rewardAmount} {bounty.rewardCurrency} | ||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| ) : ( | ||||||||||||||||||||||||||||||
| <div className="flex flex-col"> | ||||||||||||||||||||||||||||||
| <span className="text-xs text-gray-500 uppercase tracking-wider font-medium">Reward</span> | ||||||||||||||||||||||||||||||
| <span className="text-gray-400 text-sm">-</span> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| {bounty.difficulty && ( | ||||||||||||||||||||||||||||||
| <div className="flex flex-col"> | ||||||||||||||||||||||||||||||
| <span className="text-xs text-gray-500 uppercase tracking-wider font-medium">Difficulty</span> | ||||||||||||||||||||||||||||||
| <span className={cn("text-sm font-medium capitalize", difficultyColor)}> | ||||||||||||||||||||||||||||||
| {bounty.difficulty} | ||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||
| <div className="text-[10px] text-slate-400 dark:text-slate-400 font-medium"> | ||||||||||||||||||||||||||||||
| {bounty.rewardCurrency} | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| {/* Title and Description */} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <CardTitle className="text-base font-semibold line-clamp-2 text-slate-900 dark:text-slate-50 mb-1"> | ||||||||||||||||||||||||||||||
| {bounty.issueTitle} | ||||||||||||||||||||||||||||||
| </CardTitle> | ||||||||||||||||||||||||||||||
| <CardDescription className="line-clamp-2 text-xs text-slate-600 dark:text-slate-400"> | ||||||||||||||||||||||||||||||
| {bounty.description} | ||||||||||||||||||||||||||||||
| </CardDescription> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| {/* Type and Difficulty Badges */} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <div className="flex flex-wrap gap-2 mt-3"> | ||||||||||||||||||||||||||||||
| <Badge | ||||||||||||||||||||||||||||||
| variant="outline" | ||||||||||||||||||||||||||||||
| className="text-xs px-3 py-1 bg-[#f7fff0] dark:bg-slate-900 border-[#f2ffe5] dark:border-slate-700" | ||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||
| {bounty.type} | ||||||||||||||||||||||||||||||
| </Badge> | ||||||||||||||||||||||||||||||
| {bounty.difficulty && ( | ||||||||||||||||||||||||||||||
| <Badge | ||||||||||||||||||||||||||||||
| variant="outline" | ||||||||||||||||||||||||||||||
| className="text-xs px-3 py-1 bg-[#f7fff0] dark:bg-slate-900 border-[#f2ffe5] dark:border-slate-700" | ||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||
| {bounty.difficulty} | ||||||||||||||||||||||||||||||
| </Badge> | ||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||
| {bounty.tags.length > 0 && ( | ||||||||||||||||||||||||||||||
| <Badge | ||||||||||||||||||||||||||||||
| variant="outline" | ||||||||||||||||||||||||||||||
| className="text-xs px-3 py-1 bg-[#f7fff0] dark:bg-slate-900 border-[#f2ffe5] dark:border-slate-700" | ||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||
| {bounty.tags.slice(0, 1).join(", ")} | ||||||||||||||||||||||||||||||
| </Badge> | ||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| </CardHeader> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| {/* List Variant Reward Display */} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| {variant === "list" && bounty.rewardAmount && ( | ||||||||||||||||||||||||||||||
| <div className="px-4 sm:px-6 py-3 md:w-48 flex flex-col justify-center items-end border-t md:border-t-0 md:border-l border-slate-200 dark:border-slate-700 bg-slate-50/50 dark:bg-slate-900/50"> | ||||||||||||||||||||||||||||||
| <div className="text-2xl font-bold text-slate-900 dark:text-slate-50"> | ||||||||||||||||||||||||||||||
| {bounty.rewardAmount.toLocaleString()} | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| <div className="text-xs text-slate-500 dark:text-slate-400 font-medium"> | ||||||||||||||||||||||||||||||
| {bounty.rewardCurrency} | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| {/* Footer with Project and Meta Info */} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <CardFooter className="border-t border-[#f0f0f0] dark:border-slate-700 flex flex-wrap sm:items-center justify-center md:justify-between gap-3 py-3 px-4 text-xs text-slate-600 dark:text-slate-400"> | ||||||||||||||||||||||||||||||
| {/* Project Info */} | ||||||||||||||||||||||||||||||
| <div className="flex items-center gap-2 min-w-0 order-1 sm:order-none"> | ||||||||||||||||||||||||||||||
| {bounty.projectLogoUrl && ( | ||||||||||||||||||||||||||||||
| <Avatar className="h-6 w-6 border border-slate-200 dark:border-slate-700 flex-shrink-0"> | ||||||||||||||||||||||||||||||
| <AvatarImage src={bounty.projectLogoUrl} /> | ||||||||||||||||||||||||||||||
| <AvatarFallback className="text-xs font-medium"> | ||||||||||||||||||||||||||||||
| {bounty.projectName?.[0]?.toUpperCase()} | ||||||||||||||||||||||||||||||
| </AvatarFallback> | ||||||||||||||||||||||||||||||
| </Avatar> | ||||||||||||||||||||||||||||||
| )} | ||||||||||||||||||||||||||||||
| <span className="truncate text-xs font-medium text-slate-700 dark:text-slate-300"> | ||||||||||||||||||||||||||||||
| {bounty.projectName} | ||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| {/* Meta Information */} | ||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||
| <a | ||||||||||||||||||||||||||||||
| href={bounty.githubIssueUrl} | ||||||||||||||||||||||||||||||
| target="_blank" | ||||||||||||||||||||||||||||||
| rel="noopener noreferrer" | ||||||||||||||||||||||||||||||
| className="p-2 text-gray-500 hover:text-white hover:bg-gray-800 rounded-full transition-colors relative z-10" | ||||||||||||||||||||||||||||||
| onClick={handleInteractiveClick} | ||||||||||||||||||||||||||||||
| title="View GitHub Issue" | ||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||
| <Github className="size-5" /> | ||||||||||||||||||||||||||||||
| </a> | ||||||||||||||||||||||||||||||
| </CardFooter> | ||||||||||||||||||||||||||||||
| </Card> | ||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||
| <div className="flex items-center gap-2 sm:gap-3 flex-shrink-0 order-2 sm:order-none"> | ||||||||||||||||||||||||||||||
| <div className="flex items-center gap-1 text-slate-600 dark:text-slate-400 whitespace-nowrap text-xs"> | ||||||||||||||||||||||||||||||
| <Clock className="h-3.5 w-3.5 flex-shrink-0" /> | ||||||||||||||||||||||||||||||
| <span className="hidden sm:inline">{timeLeft}</span> | ||||||||||||||||||||||||||||||
| <span className="sm:hidden"> | ||||||||||||||||||||||||||||||
| {timeLeft.replace(" ago", "").replace(" from now", "")} | ||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
|
Comment on lines
+181
to
+188
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Applicant count is missing from footer meta. ➕ Suggested addition <div className="flex items-center gap-2 sm:gap-3 flex-shrink-0 order-2 sm:order-none">
<div className="flex items-center gap-1 text-slate-600 dark:text-slate-400 whitespace-nowrap text-xs">
<Clock className="h-3.5 w-3.5 flex-shrink-0" />
<span className="hidden sm:inline">{timeLeft}</span>
<span className="sm:hidden">
{timeLeft.replace(" ago", "").replace(" from now", "")}
</span>
</div>
+ {bounty.applicantCount != null && (
+ <div className="flex items-center gap-1 text-slate-600 dark:text-slate-400 whitespace-nowrap text-xs">
+ <Users className="h-3.5 w-3.5 flex-shrink-0" />
+ <span>{bounty.applicantCount}</span>
+ </div>
+ )}
</div>🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||
| </CardFooter> | ||||||||||||||||||||||||||||||
| </Card> | ||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard against unmapped bounty statuses.
statusConfigonly covers open/claimed/closed. If the API sends another status (e.g., in‑progress/disputed per requirements),statusbecomes undefined and the render will throw. Add mappings for all statuses and/or a safe fallback.🩹 Suggested hardening
📝 Committable suggestion
🤖 Prompt for AI Agents