Skip to content
Open
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
75 changes: 70 additions & 5 deletions src/app/bounty-card/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,82 @@ import { mockBounties } from "@/data/mock-bounties";

export default function BountyCardPage() {
return (
<div className="space-y-6">
<div className="space-y-8">
<div>
<h1 className="text-2xl font-semibold">Bounty Card Component</h1>
<p className="text-slate-600">
Build and refine the bounty card UI component. Use the mock data below.
</p>
</div>
<div className="grid gap-4 md:grid-cols-2">
{mockBounties.map((bounty) => (
<BountyCard key={bounty.id} {...bounty} />
))}

{/* Desktop View */}
<div>
<h2 className="text-lg font-semibold mb-3 text-slate-800">Desktop View</h2>
<div className="hidden md:block border border-slate-200 rounded-xl p-6 bg-slate-50">
<div className="grid gap-4 md:grid-cols-2">
{mockBounties.map((bounty) => (
<BountyCard key={`${bounty.id}-desktop`} {...bounty} />
))}
</div>
</div>
</div>

{/* Mobile View */}
<div>
<h2 className="text-lg font-semibold mb-3 text-slate-800">Mobile View</h2>
<div className="md:hidden border border-slate-200 rounded-xl p-4 bg-slate-50">
<div className="space-y-4 max-w-sm mx-auto">
{mockBounties.map((bounty) => (
<BountyCard key={`${bounty.id}-mobile`} {...bounty} />
))}
</div>
</div>
<p className="text-sm text-slate-500 mt-2 md:hidden">
Scroll horizontally or zoom out to see all cards in mobile view.
</p>
</div>

{/* Props Documentation */}
<div className="border border-slate-200 rounded-xl p-6">
<h2 className="text-lg font-semibold mb-4 text-slate-800">Props</h2>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-slate-200">
<th className="text-left py-2 px-3 font-semibold text-slate-700">Prop</th>
<th className="text-left py-2 px-3 font-semibold text-slate-700">Type</th>
<th className="text-left py-2 px-3 font-semibold text-slate-700">Description</th>
</tr>
</thead>
<tbody>
<tr className="border-b border-slate-100">
<td className="py-2 px-3 font-mono text-xs text-brand-700">title</td>
<td className="py-2 px-3 font-mono text-xs text-slate-600">string</td>
<td className="py-2 px-3 text-slate-600">Bounty title</td>
</tr>
<tr className="border-b border-slate-100">
<td className="py-2 px-3 font-mono text-xs text-brand-700">reward</td>
<td className="py-2 px-3 font-mono text-xs text-slate-600">number</td>
<td className="py-2 px-3 text-slate-600">Reward amount in USD</td>
</tr>
<tr className="border-b border-slate-100">
<td className="py-2 px-3 font-mono text-xs text-brand-700">tags</td>
<td className="py-2 px-3 font-mono text-xs text-slate-600">string[]</td>
<td className="py-2 px-3 text-slate-600">Array of tag labels</td>
</tr>
<tr className="border-b border-slate-100">
<td className="py-2 px-3 font-mono text-xs text-brand-700">difficulty</td>
<td className="py-2 px-3 font-mono text-xs text-slate-600">&quot;Easy&quot; | &quot;Medium&quot; | &quot;Hard&quot;</td>
<td className="py-2 px-3 text-slate-600">Difficulty level badge</td>
</tr>
<tr>
<td className="py-2 px-3 font-mono text-xs text-brand-700">progress</td>
<td className="py-2 px-3 font-mono text-xs text-slate-600">number</td>
<td className="py-2 px-3 text-slate-600">Progress percentage (0–100)</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
);
Expand Down
79 changes: 63 additions & 16 deletions src/components/bounty-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,44 +7,91 @@ type BountyCardProps = {
};

const difficultyStyles = {
Easy: "bg-emerald-50 text-emerald-700 border-emerald-200",
Medium: "bg-amber-50 text-amber-700 border-amber-200",
Hard: "bg-rose-50 text-rose-700 border-rose-200",
Easy: "bg-emerald-50 text-emerald-700 border-emerald-200 hover:bg-emerald-100",
Medium: "bg-amber-50 text-amber-700 border-amber-200 hover:bg-amber-100",
Hard: "bg-rose-50 text-rose-700 border-rose-200 hover:bg-rose-100",
};

const progressColor = {
low: "bg-emerald-500",
medium: "bg-amber-500",
high: "bg-brand-600",
};

function getProgressColor(progress: number) {
if (progress < 35) return progressColor.low;
if (progress < 70) return progressColor.medium;
return progressColor.high;
}

export function BountyCard({ title, reward, tags, difficulty, progress }: BountyCardProps) {
return (
<div className="card p-4 sm:p-5 hover:shadow-md transition">
<div className="group relative card p-4 sm:p-5 border border-slate-200 hover:border-brand-300 hover:shadow-lg hover:shadow-brand-100 transition-all duration-200 ease-out hover:-translate-y-0.5 cursor-pointer bg-white rounded-xl">
{/* Top row: title + reward badge */}
<div className="flex items-start justify-between gap-3">
<div className="min-w-0 flex-1">
<h3 className="text-base sm:text-lg font-semibold leading-snug break-words">{title}</h3>
<div className="mt-1.5 flex flex-wrap gap-1.5">
<h3 className="text-base sm:text-lg font-semibold leading-snug text-slate-900 group-hover:text-brand-700 transition-colors duration-200 break-words">
{title}
</h3>
<div className="mt-2 flex flex-wrap gap-1.5">
{tags.map((tag) => (
<span key={tag} className="pill text-[11px] sm:text-xs px-2 py-0.5 sm:px-3 sm:py-1">
<span
key={tag}
className="pill text-[11px] sm:text-xs px-2 py-0.5 sm:px-2.5 sm:py-0.5 bg-brand-50 text-brand-700 border border-brand-200 hover:bg-brand-100 transition-colors duration-150"
>
{tag}
</span>
))}
</div>
</div>
<div className="text-right shrink-0">
<div className="text-xl sm:text-xl font-bold">${reward}</div>
<span className={`mt-1 inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] sm:text-xs font-semibold whitespace-nowrap ${difficultyStyles[difficulty]}`}>
<div className="text-right shrink-0 flex flex-col items-end gap-1.5">
<div className="text-xl sm:text-2xl font-bold text-slate-900 group-hover:text-brand-600 transition-colors duration-200 tabular-nums">
<span className="text-sm font-medium text-slate-400">$</span>
{reward.toLocaleString()}
</div>
<span
className={`inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] sm:text-xs font-semibold whitespace-nowrap transition-colors duration-150 ${difficultyStyles[difficulty]}`}
>
{difficulty === "Easy" && (
<svg className="w-2.5 h-2.5 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
)}
{difficulty === "Medium" && (
<svg className="w-2.5 h-2.5 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M5.293 9.707a1 1 0 010-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 01-1.414 1.414L9 7.414V15a1 1 0 11-2 0V7.414L6.707 9.707a1 1 0 01-1.414 0z" clipRule="evenodd" />
</svg>
)}
{difficulty === "Hard" && (
<svg className="w-2.5 h-2.5 mr-1" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M14.707 12.707a1 1 0 01-1.414 0L10 9.414l-3.293 3.293a1 1 0 01-1.414-1.414l4-4a1 1 0 011.414 0l4 4a1 1 0 010 1.414z" clipRule="evenodd" />
</svg>
)}
{difficulty}
</span>
</div>
</div>
<div className="mt-3">
<div className="flex items-center justify-between text-xs text-slate-500">
<span>Progress</span>
<span>{progress}%</span>

{/* Progress bar section */}
<div className="mt-4">
<div className="flex items-center justify-between text-xs text-slate-500 mb-1.5">
<span className="font-medium">Progress</span>
<span className="tabular-nums font-semibold text-slate-700">{progress}%</span>
</div>
<div className="mt-1.5 h-2 w-full rounded-full bg-slate-100">
<div className="h-2.5 w-full rounded-full bg-slate-100 overflow-hidden">
<div
className="h-2 rounded-full bg-brand-600"
className={`h-2.5 rounded-full ${getProgressColor(progress)} transition-all duration-500 ease-out group-hover:shadow-md group-hover:brightness-110`}
style={{ width: `${progress}%` }}
/>
</div>
</div>

{/* Hover indicator */}
<div className="absolute top-3 right-3 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
<svg className="w-4 h-4 text-brand-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</div>
</div>
);
}
17 changes: 10 additions & 7 deletions src/components/create-bounty-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export function CreateBountyForm({ onSubmit }: CreateBountyFormProps) {
const [difficulty, setDifficulty] = useState("Easy");
const [submitting, setSubmitting] = useState(false);
const [submissions, setSubmissions] = useState<string[]>([]);
const [errors, setErrors] = useState<{ title?: string; reward?: string }>({});
const isSubmittingRef = useRef(false);

const handleSubmit = async (e: React.FormEvent) => {
Expand All @@ -26,19 +27,21 @@ export function CreateBountyForm({ onSubmit }: CreateBountyFormProps) {

try {
// Validation: check for empty title and negative reward
const newErrors: { title?: string; reward?: string } = {};
if (!title.trim()) {
alert("Title is required");
isSubmittingRef.current = false;
setSubmitting(false);
return;
newErrors.title = "Title is required";
}
const rewardNum = Number(reward);
if (isNaN(rewardNum) || rewardNum <= 0) {
alert("Reward must be a positive number");
newErrors.reward = "Reward must be a positive number";
}
if (Object.keys(newErrors).length > 0) {
setErrors(newErrors);
isSubmittingRef.current = false;
setSubmitting(false);
return;
}
setErrors({});

// Simulate API delay
await new Promise((resolve) => setTimeout(resolve, 1000));
Expand Down Expand Up @@ -72,7 +75,7 @@ export function CreateBountyForm({ onSubmit }: CreateBountyFormProps) {
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
onChange={(e) => { setTitle(e.target.value); setErrors((prev) => ({ ...prev, title: undefined })); }}
className="w-full rounded-lg border border-slate-200 px-3 py-2"
placeholder="Bounty title"
required
Expand All @@ -91,7 +94,7 @@ export function CreateBountyForm({ onSubmit }: CreateBountyFormProps) {
<input
type="number"
value={reward}
onChange={(e) => setReward(e.target.value)}
onChange={(e) => { setReward(e.target.value); setErrors((prev) => ({ ...prev, reward: undefined })); }}
className="w-full rounded-lg border border-slate-200 px-3 py-2"
placeholder="100"
min="1"
Expand Down
1 change: 1 addition & 0 deletions tsconfig.tsbuildinfo

Large diffs are not rendered by default.