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
50 changes: 42 additions & 8 deletions src/app/discovery/page.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,43 @@
import { mockDiscovery } from "@/data/mock-discovery";

type DiscoveryBounty = typeof mockDiscovery[number];

function calculateScoreBreakdown(bounty: DiscoveryBounty) {
// Funding signals sponsor confidence. Cap the boost so one highly funded bounty
// does not overwhelm active, recent opportunities.
const funding = Math.min(bounty.fundedPercent, 100) * 0.3;

// Activity suggests market demand, but each additional claim matters a little
// less so crowded bounties do not always dominate the list.
const activity = Math.sqrt(bounty.claimedCount) * 12;

// Newer bounties should surface while they are still actionable.
const recency = Math.max(0, 14 - bounty.postedDaysAgo) * 1.5;

// Reward is important, but scaled down to avoid turning discovery into a
// simple highest-dollar sort.
const reward = bounty.reward * 0.06;

return { funding, activity, recency, reward };
}

function scoreBounty(bounty: typeof mockDiscovery[number]) {
const fundedBoost = bounty.fundedPercent >= 80 ? 20 : bounty.fundedPercent / 4;
const activityBoost = bounty.claimedCount * 5;
const recencyBoost = Math.max(0, 14 - bounty.postedDaysAgo);
return fundedBoost + activityBoost + recencyBoost + bounty.reward / 50;
const breakdown = calculateScoreBreakdown(bounty);
return Object.values(breakdown).reduce((total, value) => total + value, 0);
}

export default function DiscoveryPage() {
const ranked = [...mockDiscovery]
.map((bounty) => ({ ...bounty, score: scoreBounty(bounty) }))
.sort((a, b) => b.score - a.score);
.map((bounty) => ({
...bounty,
score: scoreBounty(bounty),
scoreBreakdown: calculateScoreBreakdown(bounty),
}))
.sort((a, b) => {
const scoreDiff = b.score - a.score;
if (scoreDiff !== 0) return scoreDiff;
return b.reward - a.reward;
});

return (
<div className="space-y-6">
Expand Down Expand Up @@ -51,8 +78,15 @@ export default function DiscoveryPage() {
Posted: <span className="font-semibold text-slate-900">{bounty.postedDaysAgo}d ago</span>
</div>
</div>
<div className="mt-3 text-xs text-slate-500">
Score: <span className="font-semibold text-brand-700">{bounty.score.toFixed(1)}</span>
<div className="mt-3 flex items-center justify-between gap-3 text-xs text-slate-500">
<span>
Score: <span className="font-semibold text-brand-700">{bounty.score.toFixed(1)}</span>
</span>
<span className="text-right">
Funding {bounty.scoreBreakdown.funding.toFixed(1)} · Activity{" "}
{bounty.scoreBreakdown.activity.toFixed(1)} · Recency{" "}
{bounty.scoreBreakdown.recency.toFixed(1)}
</span>
</div>
</div>
))}
Expand Down
32 changes: 24 additions & 8 deletions src/components/create-bounty-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@ type CreateBountyFormProps = {
onSubmit: (bounty: { title: string; reward: number; difficulty: string }) => void;
};

type FormErrors = {
title?: string;
reward?: string;
};

export function CreateBountyForm({ onSubmit }: CreateBountyFormProps) {
const [title, setTitle] = useState("");
const [reward, setReward] = useState("");
const [difficulty, setDifficulty] = useState("Easy");
const [submitting, setSubmitting] = useState(false);
const [submissions, setSubmissions] = useState<string[]>([]);
const [errors, setErrors] = useState<FormErrors>({});
const isSubmittingRef = useRef(false);

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

try {
// Validation: check for empty title and negative reward
const nextErrors: FormErrors = {};
if (!title.trim()) {
alert("Title is required");
isSubmittingRef.current = false;
setSubmitting(false);
return;
nextErrors.title = "Title is required";
}

const rewardNum = Number(reward);
if (isNaN(rewardNum) || rewardNum <= 0) {
alert("Reward must be a positive number");
nextErrors.reward = "Reward must be a positive number";
}

if (Object.keys(nextErrors).length > 0) {
setErrors(nextErrors);
isSubmittingRef.current = false;
setSubmitting(false);
return;
}
setErrors({});

// Simulate API delay
await new Promise((resolve) => setTimeout(resolve, 1000));
Expand Down Expand Up @@ -72,7 +82,10 @@ 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 +104,10 @@ 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 Expand Up @@ -141,4 +157,4 @@ export function CreateBountyForm({ onSubmit }: CreateBountyFormProps) {
)}
</div>
);
}
}