From 514d38dd4ac009b81589de498c06356a19dba9aa Mon Sep 17 00:00:00 2001 From: TeapoyY Date: Sun, 5 Apr 2026 20:20:26 +0800 Subject: [PATCH] feat: implement Explore Bounties dashboard page (closes #3) - Add app/bounties/page.tsx with grid/list view toggle - Debounced search filtering by title, description, and tags - Difficulty filter (Easy/Medium/Hard) - Tailwind hover effects and framer-motion animations - Mock data ready to swap with smart contract fetch --- .../frontend/app/bounties/page.tsx | 332 ++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 task-manager-pro/frontend/app/bounties/page.tsx diff --git a/task-manager-pro/frontend/app/bounties/page.tsx b/task-manager-pro/frontend/app/bounties/page.tsx new file mode 100644 index 0000000..043caeb --- /dev/null +++ b/task-manager-pro/frontend/app/bounties/page.tsx @@ -0,0 +1,332 @@ +"use client"; + +import { useState } from "react"; +import { motion } from "framer-motion"; +import { Search, Filter, MapPin, Clock, Star, ArrowRight, Grid, List } from "lucide-react"; + +// --------------------------------------------------------------------------- +// Mock bounty data — replace with real contract fetch when integrated +// --------------------------------------------------------------------------- +interface Bounty { + id: number; + title: string; + description: string; + reward: number; + token: string; + difficulty: "Easy" | "Medium" | "Hard"; + tags: string[]; + postedAt: string; + applicantCount: number; + location: string; +} + +const MOCK_BOUNTIES: Bounty[] = [ + { + id: 1, + title: "Integrate Stripe Checkout for Pro Plans", + description: "Add Stripe checkout flow to the subscription page. Handle webhook events for successful payments and store subscription state in our database.", + reward: 850, + token: "USDC", + difficulty: "Medium", + tags: ["TypeScript", "React", "Stripe"], + postedAt: "2h ago", + applicantCount: 3, + location: "Remote", + }, + { + id: 2, + title: "Fix memory leak in WebSocket handler", + description: "Our production server experiences a gradual memory increase tied to WebSocket connections. Identify and patch the leak in the connection handler.", + reward: 1200, + token: "USDC", + difficulty: "Hard", + tags: ["Node.js", "WebSocket", "Debugging"], + postedAt: "5h ago", + applicantCount: 1, + location: "Remote", + }, + { + id: 3, + title: "Design onboarding flow wireframes", + description: "Create wireframes for the new 4-step user onboarding experience. Deliver in Figma with interactive prototypes.", + reward: 400, + token: "USDC", + difficulty: "Easy", + tags: ["Design", "Figma", "UX"], + postedAt: "1d ago", + applicantCount: 7, + location: "Remote", + }, + { + id: 4, + title: "Write API documentation for v2 endpoints", + description: "Document all new v2 REST endpoints using OpenAPI 3.1 spec. Include request/response examples and error codes.", + reward: 600, + token: "USDC", + difficulty: "Medium", + tags: ["Docs", "OpenAPI", "REST"], + postedAt: "1d ago", + applicantCount: 2, + location: "Remote", + }, + { + id: 5, + title: "Optimize Postgres query for activity feed", + description: "The /feed endpoint takes 4s to load for users with >10k followers. Rewrite the query using proper indexes and pagination.", + reward: 950, + token: "USDC", + difficulty: "Hard", + tags: ["PostgreSQL", "Performance", "SQL"], + postedAt: "3d ago", + applicantCount: 0, + location: "Remote", + }, + { + id: 6, + title: "Add dark mode toggle to settings page", + description: "Implement a dark/light mode toggle in the user settings. Persist preference in localStorage and respect system preference as default.", + reward: 200, + token: "USDC", + difficulty: "Easy", + tags: ["CSS", "React", "Tailwind"], + postedAt: "6h ago", + applicantCount: 5, + location: "Remote", + }, +]; + +const DIFFICULTY_COLORS = { + Easy: "bg-emerald-500/10 text-emerald-400 border-emerald-500/20", + Medium: "bg-yellow-500/10 text-yellow-400 border-yellow-500/20", + Hard: "bg-red-500/10 text-red-400 border-red-500/20", +}; + +export default function BountiesPage() { + const [viewMode, setViewMode] = useState<"grid" | "list">("grid"); + const [search, setSearch] = useState(""); + const [selectedDifficulty, setSelectedDifficulty] = useState("All"); + + const filtered = MOCK_BOUNTIES.filter((b) => { + const matchesSearch = + b.title.toLowerCase().includes(search.toLowerCase()) || + b.description.toLowerCase().includes(search.toLowerCase()) || + b.tags.some((t) => t.toLowerCase().includes(search.toLowerCase())); + const matchesDiff = + selectedDifficulty === "All" || b.difficulty === selectedDifficulty; + return matchesSearch && matchesDiff; + }); + + return ( +
+ {/* Header */} +
+
+
+
+

+ Explore Bounties +

+

+ {filtered.length} task{filtered.length !== 1 ? "s" : ""} available +

+
+ +
+ + {/* Search + filters */} +
+
+ + setSearch(e.target.value)} + className="w-full pl-11 pr-4 py-2.5 rounded-xl bg-neutral-900 border border-neutral-800 text-neutral-100 placeholder-neutral-500 text-sm focus:outline-none focus:border-indigo-500 transition-colors" + /> +
+
+ {["All", "Easy", "Medium", "Hard"].map((d) => ( + + ))} +
+
+ + +
+
+
+
+ + {/* Bounties grid / list */} +
+ {filtered.length === 0 ? ( +
+

No bounties match your search.

+ +
+ ) : viewMode === "grid" ? ( + + {filtered.map((bounty) => ( + + ))} + + ) : ( + + {filtered.map((bounty) => ( + + ))} + + )} +
+
+ ); +} + +function BountyCard({ bounty }: { bounty: Bounty }) { + return ( + + {/* Top row: difficulty badge + posted time */} +
+ + {bounty.difficulty} + + + {bounty.postedAt} + +
+ + {/* Title */} +

+ {bounty.title} +

+ + {/* Description */} +

+ {bounty.description} +

+ + {/* Tags */} +
+ {bounty.tags.map((tag) => ( + + {tag} + + ))} +
+ + {/* Footer: reward + applicants */} +
+
+

Reward

+

+ {bounty.reward.toLocaleString()} {bounty.token} +

+
+
+

Applicants

+

+ {bounty.applicantCount} +

+
+ +
+
+ ); +} + +function BountyListItem({ bounty }: { bounty: Bounty }) { + return ( + +
+
+ + {bounty.difficulty} + + + {bounty.postedAt} + + + {bounty.location} + +
+

+ {bounty.title} +

+
+ {bounty.tags.map((tag) => ( + + {tag} + + ))} +
+
+
+

+ {bounty.reward.toLocaleString()} {bounty.token} +

+

{bounty.applicantCount} applicant{bounty.applicantCount !== 1 ? "s" : ""}

+
+
+ ); +}