diff --git a/app/page.tsx b/app/page.tsx index 2ca07ab..82489f0 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,10 +1,19 @@ import SignIn from "@/components/login/sign-in"; import Image from "next/image"; + + +/** + * Render a full-screen, centered container that displays the SignIn component. + * + * @returns A JSX element containing a full-height centered wrapper with the `SignIn` component. + */ export default async function Home() { return (
+ + ); -} +} \ No newline at end of file diff --git a/components/bounty/bounty-card.tsx b/components/bounty/bounty-card.tsx index 16825e7..a620a2b 100644 --- a/components/bounty/bounty-card.tsx +++ b/components/bounty/bounty-card.tsx @@ -1,145 +1,253 @@ -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"; -interface BountyCardProps { - bounty: Bounty -} +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"; -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" }, +interface Bounty { + id: string; + title: string; + description: string; + budget: { + amount: number; + asset: string; + }; + status: + | "open" + | "claimed" + | "in_progress" + | "under_review" + | "completed" + | "disputed"; + category: string; + claimingModel: 1 | 2 | 3 | 4; + creator: { + wallet: string; + displayName?: string; + avatar?: string; + }; + deadline: Date; + applicantCount?: number; + milestoneCount?: number; } -const difficultyColors: Record = { - beginner: "text-success-400", - intermediate: "text-warning-400", - advanced: "text-error-400", +interface BountyCardProps { + bounty: Bounty; + onClick?: () => void; + variant?: "grid" | "list"; } -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", -} +const statusConfig = { + open: { + variant: "default" as const, + label: "Open", + dotColor: "bg-emerald-500", + }, + claimed: { + variant: "secondary" as const, + label: "Claimed", + dotColor: "bg-amber-500", + }, + in_progress: { + variant: "secondary" as const, + label: "In Progress", + dotColor: "bg-blue-500", + }, + under_review: { + variant: "secondary" as const, + label: "Under Review", + dotColor: "bg-amber-500", + }, + completed: { + variant: "outline" as const, + label: "Completed", + dotColor: "bg-slate-400", + }, + disputed: { + variant: "destructive" as const, + label: "Disputed", + dotColor: "bg-red-500", + }, +}; -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} -
- ) : ( -
- {(bounty.projectName || "Unknown").substring(0, 2).toUpperCase()} -
- )} -
-

{bounty.projectName}

- - {formatDistanceToNow(new Date(bounty.createdAt), { addSuffix: true })} - -
-
- -
- - {typeInfo.icon} - {typeInfo.label} - - - {bounty.status} - -
-
+const modelNames = { + 1: "Single Claim", + 2: "Application", + 3: "Competition", + 4: "Multi-Winner", +}; -

- - {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} - )} +/** + * 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 */} + +
+
+
+ + {status.label} + +
+ + {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} +
+ ) : ( +
+ {(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