diff --git a/app/globals.css b/app/globals.css index 809a753..7d89e43 100644 --- a/app/globals.css +++ b/app/globals.css @@ -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 { diff --git a/app/page.tsx b/app/page.tsx index 2ca07ab..b4979a3 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,10 +1,14 @@ import SignIn from "@/components/login/sign-in"; import Image from "next/image"; + + export default async function Home() { return (
+ + ); } diff --git a/components/bounty/bounty-card.tsx b/components/bounty/bounty-card.tsx index 16825e7..ae62d2e 100644 --- a/components/bounty/bounty-card.tsx +++ b/components/bounty/bounty-card.tsx @@ -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 = { - 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 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 = { - 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"; -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", -} + return ( + { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + onClick?.(); + } + }} + > + {/* 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 ( - - -
-
- {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} - -
-
+
+ + {/* Header Row with Status and Reward */} + +
+
+
+ + {status.label} + +
-

- - {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} - )} + {variant === "grid" && bounty.rewardAmount && ( +
+
+ {bounty.rewardAmount.toLocaleString()}
- - - -
- {(bounty.rewardAmount !== null && bounty.rewardAmount !== undefined) ? ( -
- Reward - - {bounty.rewardAmount} {bounty.rewardCurrency} - -
- ) : ( -
- Reward - - -
- )} - - {bounty.difficulty && ( -
- Difficulty - - {bounty.difficulty} - -
- )} +
+ {bounty.rewardCurrency}
+
+ )} +
+ + {/* Title and Description */} + + + {bounty.issueTitle} + + + {bounty.description} + + + {/* Type and Difficulty Badges */} + +
+ + {bounty.type} + + {bounty.difficulty && ( + + {bounty.difficulty} + + )} + {bounty.tags.length > 0 && ( + + {bounty.tags.slice(0, 1).join(", ")} + + )} +
+ + + {/* List Variant Reward Display */} + + {variant === "list" && bounty.rewardAmount && ( +
+
+ {bounty.rewardAmount.toLocaleString()} +
+
+ {bounty.rewardCurrency} +
+
+ )} +
+ + {/* Footer with Project and Meta Info */} + + + {/* Project Info */} +
+ {bounty.projectLogoUrl && ( + + + + {bounty.projectName?.[0]?.toUpperCase()} + + + )} + + {bounty.projectName} + +
+ + {/* Meta Information */} - - - -
- - ) +
+
+ + {timeLeft} + + {timeLeft.replace(" ago", "").replace(" from now", "")} + +
+
+ + + ); } diff --git a/components/bounty/github-bounty-card.tsx b/components/bounty/github-bounty-card.tsx new file mode 100644 index 0000000..b52c7d0 --- /dev/null +++ b/components/bounty/github-bounty-card.tsx @@ -0,0 +1,147 @@ +"use client"; + +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", +} + +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