- {bounty.description.replace(/[#*`_]/g, '') /* Simple stripped markdown preview */}
-
-
-
- {bounty.tags.slice(0, 3).map(tag => (
-
- {tag}
-
- ))}
- {bounty.tags.length > 3 && (
-
+{bounty.tags.length - 3}
- )}
+/**
+ * Render a clickable card summarizing a bounty, including status, budget, category, model, creator, deadline, and applicant info.
+ *
+ * @param bounty - The bounty data to display (id, title, description, budget, status, category, claimingModel, creator, deadline, applicantCount, milestoneCount)
+ * @param onClick - Optional handler invoked when the card is clicked or activated via keyboard
+ * @param variant - Layout variant; either `"grid"` (default) or `"list"`
+ * @returns A JSX element representing the styled bounty card
+ */
+export function BountyCard({
+ bounty,
+ onClick,
+ variant = "grid",
+}: BountyCardProps) {
+ const status = statusConfig[bounty.status];
+ const timeLeft = formatDistanceToNow(bounty.deadline, { addSuffix: true });
+
+ return (
+
{
+ if (e.key === "Enter" || e.key === " ") {
+ e.preventDefault();
+ onClick?.();
+ }
+ }}
+ >
+ {/* Main Content Section */}
+
+
+
+ {/* Header Row with Status and Budget */}
+
+
+
+
+ {variant === "grid" && (
+
+
+ {bounty.budget.amount.toLocaleString()}
-
-
-
-
- {(bounty.rewardAmount !== null && bounty.rewardAmount !== undefined) ? (
-
- Reward
-
- {bounty.rewardAmount} {bounty.rewardCurrency}
-
-
- ) : (
-
- Reward
- -
-
- )}
-
- {bounty.difficulty && (
-
- Difficulty
-
- {bounty.difficulty}
-
-
- )}
+
+ {bounty.budget.asset}
+
+ )}
+
-
-
-
-
-
- )
-}
+ {/* Title and Description */}
+
+
+ {bounty.title}
+
+
+ {bounty.description}
+
+
+ {/* Category and Model Badges */}
+
+
+
+ {bounty.category}
+
+
+ {modelNames[bounty.claimingModel]}
+
+ {bounty.milestoneCount && bounty.milestoneCount > 1 && (
+
+ {bounty.milestoneCount} milestones
+
+ )}
+
+
+
+ {/* List Variant Budget Display */}
+
+ {variant === "list" && (
+
+
+ {bounty.budget.amount.toLocaleString()}
+
+
+ {bounty.budget.asset}
+
+
+ )}
+
+
+ {/* Footer with Creator and Meta Info */}
+
+
+ {/* Creator Info */}
+
+
+
+
+ {bounty.creator.displayName?.[0]?.toUpperCase() ||
+ bounty.creator.wallet.slice(0, 2).toUpperCase()}
+
+
+
+ {bounty.creator.displayName ||
+ `${bounty.creator.wallet.slice(0, 8)}...`}
+
+
+
+ {/* Meta Information */}
+
+
+ {bounty.deadline && (
+
+
+ {timeLeft}
+
+ {timeLeft.replace(" ago", "").replace(" from now", "")}
+
+
+ )}
+ {bounty.applicantCount != null && bounty.applicantCount > 0 && (
+
+
+ {bounty.applicantCount}
+
+ )}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/bounty/github-bounty-card.tsx b/components/bounty/github-bounty-card.tsx
new file mode 100644
index 0000000..fe55919
--- /dev/null
+++ b/components/bounty/github-bounty-card.tsx
@@ -0,0 +1,155 @@
+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"
+
+interface BountyCardProps {
+ bounty: Bounty
+}
+
+const typeConfig: Record = {
+ bug: { label: "Bug", icon: , className: "bg-error-500 text-white border-transparent" },
+ feature: { label: "Feature", icon: , className: "bg-primary text-primary-foreground border-transparent" },
+ documentation: { label: "Docs", icon: , className: "bg-secondary-500 text-white border-transparent" },
+ refactor: { label: "Refactor", icon: , className: "bg-gray-700 text-gray-100 border-transparent" },
+ other: { label: "Other", icon: , className: "bg-gray-800 text-gray-300 border-gray-600" },
+}
+
+const difficultyColors: Record = {
+ beginner: "text-success-400",
+ intermediate: "text-warning-400",
+ advanced: "text-error-400",
+}
+
+const statusColors: Record = {
+ 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",
+}
+
+/**
+ * Render a stylized card summarizing a bounty with metadata, badges, and action links.
+ *
+ * Displays project branding (logo or initials), relative creation time, type and status badges,
+ * the issue title (linking to the bounty page), a short markdown-stripped description preview,
+ * up to three tags (with overflow count), reward and optional difficulty, and a link to the GitHub issue.
+ *
+ * @param bounty - The bounty data to display
+ * @returns A JSX element representing the bounty card
+ */
+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 (
+
+
+
+
+ {bounty.projectLogoUrl ? (
+
+
+
+ ) : (
+
+ {(bounty.projectName || "Unknown").substring(0, 2).toUpperCase()}
+
+ )}
+
+
{bounty.projectName}
+
+ {formatDistanceToNow(new Date(bounty.createdAt), { addSuffix: true })}
+
+
+
+
+
+
+ {typeInfo.icon}
+ {typeInfo.label}
+
+
+ {bounty.status}
+
+
+
+
+
+
+ {bounty.issueTitle}
+
+
+
+
+
+
+ {bounty.description.replace(/[#*`_]/g, '') /* Simple stripped markdown preview */}
+
+
+
+ {bounty.tags.slice(0, 3).map(tag => (
+
+ {tag}
+
+ ))}
+ {bounty.tags.length > 3 && (
+ +{bounty.tags.length - 3}
+ )}
+
+
+
+
+
+ {(bounty.rewardAmount !== null && bounty.rewardAmount !== undefined) ? (
+
+ Reward
+
+ {bounty.rewardAmount} {bounty.rewardCurrency}
+
+
+ ) : (
+
+ Reward
+ -
+
+ )}
+
+ {bounty.difficulty && (
+
+ Difficulty
+
+ {bounty.difficulty}
+
+
+ )}
+
+
+
+
+
+
+
+ )
+}
\ No newline at end of file