Skip to content
Merged
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
2 changes: 1 addition & 1 deletion app/globals.css
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap');
@import 'tailwindcss';
@plugin "@tailwindcss/typography";
@import 'tw-animate-css';

@custom-variant dark (&:is(.dark *));

@theme {
Expand Down
4 changes: 4 additions & 0 deletions app/page.tsx
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";

Check warning on line 2 in app/page.tsx

View workflow job for this annotation

GitHub Actions / build-and-lint (24.x)

'Image' is defined but never used



export default async function Home() {
return (
<div className="flex justify-center items-center h-screen">
<SignIn />
</div>


);
}
314 changes: 181 additions & 133 deletions components/bounty/bounty-card.tsx
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";

Check warning on line 12 in components/bounty/bounty-card.tsx

View workflow job for this annotation

GitHub Actions / build-and-lint (24.x)

'Users' is defined but never used
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];
Comment on lines +23 to +46
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Guard against unmapped bounty statuses.
statusConfig only covers open/claimed/closed. If the API sends another status (e.g., in‑progress/disputed per requirements), status becomes undefined and the render will throw. Add mappings for all statuses and/or a safe fallback.

🩹 Suggested hardening
-  const status = statusConfig[bounty.status];
+  const status =
+    statusConfig[bounty.status] ?? {
+      variant: "outline",
+      label: "Unknown",
+      dotColor: "bg-slate-400",
+    };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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 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",
},
};
export function BountyCard({
bounty,
onClick,
variant = "grid",
}: BountyCardProps) {
const status =
statusConfig[bounty.status] ?? {
variant: "outline",
label: "Unknown",
dotColor: "bg-slate-400",
};
🤖 Prompt for AI Agents
In `@components/bounty/bounty-card.tsx` around lines 23 - 46, The statusConfig
mapping and its use in BountyCard can return undefined for unexpected
bounty.status values; update statusConfig to include all possible statuses
(e.g., "in-progress", "disputed", etc.) and/or add a safe fallback before
rendering: compute a resolvedStatus = statusConfig[bounty.status] ?? /* default
mapping like closed/default */ and use resolvedStatus in the component. Ensure
the symbols referenced are statusConfig, BountyCard and the variable status (or
replace with resolvedStatus) so unknown API statuses won't cause a render error.

const timeLeft = bounty.updatedAt
? formatDistanceToNow(new Date(bounty.updatedAt), { addSuffix: true })
: "N/A";
Comment on lines +47 to +49
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Clock should be based on the bounty deadline, not updatedAt.
This currently shows “last updated” rather than “time left,” which breaks the countdown requirement. Please use the deadline/expiry field instead.

🧭 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
In `@components/bounty/bounty-card.tsx` around lines 47 - 49, The timeLeft
calculation uses bounty.updatedAt (showing "last updated") instead of the bounty
deadline/expiry; update the timeLeft assignment to compute
formatDistanceToNow(new Date(bounty.deadline || bounty.expiresAt), { addSuffix:
true }) (or the exact deadline field used in the bounty object) and fall back to
"N/A" if that deadline/expiry field is missing, keeping the variable name
timeLeft and preserving usage of formatDistanceToNow.


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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

List variant is still capped to max-w-xs.
With max-w-xs applied unconditionally, the list layout won’t expand to full width, which undermines the list variant’s responsive intent.

💡 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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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",
)}
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",
)}
🤖 Prompt for AI Agents
In `@components/bounty/bounty-card.tsx` around lines 99 - 104, The card always
applies "max-w-xs" which prevents the list variant from expanding; update the
className construction in bounty-card.tsx (the cn(...) call where variant ===
"list" is checked) to apply "max-w-xs" only when variant !== "list" (or
alternatively apply a full-width class like "w-full" when variant === "list"),
so the list variant can use the full available width while preserving the
compact cap for other variants.

role="button"
tabIndex={0}
onClick={onClick}
Copy link

Copilot AI Jan 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Attaching onClick to a div-based Card makes it mouse-only by default. For accessibility, either render a semantic interactive element (e.g., <button>/<a> via asChild) or add role="button", tabIndex={0}, and keyboard handlers for Enter/Space (plus an accessible label if needed).

Copilot uses AI. Check for mistakes.
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
onClick?.();
}
}}
Comment on lines +59 to +67
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid a focusable card when onClick is missing.
Right now the card is keyboard-focusable even if it does nothing. Gate role/tabIndex/keyboard handling on onClick.

♿ 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
In `@components/bounty/bounty-card.tsx` around lines 59 - 67, The card is being
made focusable and keyboard-interactive even when no onClick handler is
provided; update the JSX in bounty-card.tsx to conditionally add role="button",
tabIndex={0}, and the onKeyDown keyboard handler only when the onClick prop
exists (i.e., when onClick is truthy). Locate the element using the onClick prop
and the onKeyDown inline handler, remove the unconditional role/tabIndex, and
wrap those attributes and the Enter/Space key handling so they are only
rendered/attached if onClick is defined.

>
{/* 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Applicant count is missing from footer meta.
Acceptance criteria call for displaying applicant count (including 0). The footer currently shows only the clock.

➕ 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
In `@components/bounty/bounty-card.tsx` around lines 181 - 188, The footer
currently only renders the Clock and timeLeft; update the same metadata block in
bounty-card.tsx to also render the applicant count (showing 0 when missing) next
to the time text. Add a span (matching the existing text classes: "flex
items-center gap-1 text-slate-600 dark:text-slate-400 whitespace-nowrap
text-xs") after the timeLeft spans that displays something like
`{(applicantCount ?? 0)} applicant{(applicantCount ?? 0) !== 1 ? "s" : ""}` (use
the prop/state name used in this component for applicants — e.g., applicantCount
or applicants.length) and mirror the responsive behavior (show full text on sm
and truncated on mobile) so it appears alongside timeLeft in the same div that
contains Clock and timeLeft.

</div>
</CardFooter>
</Card>
);
}
Loading
Loading