From 49aaaf9f3468bc11330d4e5516397f89e25b7187 Mon Sep 17 00:00:00 2001 From: Okoli Johnpaul Sochimaobi <132228270+Johnpii1@users.noreply.github.com> Date: Wed, 17 Jun 2026 21:22:33 +0100 Subject: [PATCH 1/2] Add prize pool event actions --- .../app/(authenticated)/competitions/page.tsx | 159 +++++++++++++++++- frontend/src/component/PrizePoolSummary.tsx | 58 +++++++ frontend/src/lib/eventRewards.ts | 58 +++++++ 3 files changed, 273 insertions(+), 2 deletions(-) create mode 100644 frontend/src/component/PrizePoolSummary.tsx create mode 100644 frontend/src/lib/eventRewards.ts diff --git a/frontend/src/app/(authenticated)/competitions/page.tsx b/frontend/src/app/(authenticated)/competitions/page.tsx index 14a9fa919..84962733c 100644 --- a/frontend/src/app/(authenticated)/competitions/page.tsx +++ b/frontend/src/app/(authenticated)/competitions/page.tsx @@ -1,6 +1,14 @@ "use client"; import { useEffect, useMemo, useState } from "react"; +import PrizePoolSummary from "@/component/PrizePoolSummary"; +import { useWallet } from "@/context/WalletContext"; +import { + claimPayout, + finalizeEvent, + getUserPayout, + type UserPayout, +} from "@/lib/eventRewards"; export default function CompetitionsPage() { type CompetitionStatus = "Active" | "Upcoming" | "Ended" | "Cancelled"; @@ -18,6 +26,8 @@ export default function CompetitionsPage() { endDate: string; visibility: CompetitionVisibility; joined: boolean; + isFinalized: boolean; + rewardBreakdown: { label: string; amountXlm: number; percentage: number }[]; }; const [activeTab, setActiveTab] = useState< @@ -26,6 +36,11 @@ export default function CompetitionsPage() { const [search, setSearch] = useState(""); const [page, setPage] = useState(1); const [isCreateOpen, setIsCreateOpen] = useState(false); + const { address } = useWallet(); + const [userPayouts, setUserPayouts] = useState< + Record + >({}); + const [pendingAction, setPendingAction] = useState(null); const [createForm, setCreateForm] = useState({ title: "", @@ -49,6 +64,12 @@ export default function CompetitionsPage() { endDate: "2026-04-30", visibility: "Public", joined: true, + isFinalized: false, + rewardBreakdown: [ + { label: "1st place", amountXlm: 1250, percentage: 50 }, + { label: "2nd place", amountXlm: 750, percentage: 30 }, + { label: "3rd place", amountXlm: 500, percentage: 20 }, + ], }, { id: "comp-2", @@ -63,6 +84,12 @@ export default function CompetitionsPage() { endDate: "2026-05-20", visibility: "Public", joined: false, + isFinalized: false, + rewardBreakdown: [ + { label: "1st place", amountXlm: 600, percentage: 50 }, + { label: "2nd place", amountXlm: 360, percentage: 30 }, + { label: "3rd place", amountXlm: 240, percentage: 20 }, + ], }, { id: "comp-3", @@ -77,6 +104,12 @@ export default function CompetitionsPage() { endDate: "2026-05-10", visibility: "Private", joined: false, + isFinalized: false, + rewardBreakdown: [ + { label: "1st place", amountXlm: 2500, percentage: 50 }, + { label: "2nd place", amountXlm: 1500, percentage: 30 }, + { label: "3rd place", amountXlm: 1000, percentage: 20 }, + ], }, { id: "comp-4", @@ -91,6 +124,12 @@ export default function CompetitionsPage() { endDate: "2026-03-31", visibility: "Public", joined: true, + isFinalized: false, + rewardBreakdown: [ + { label: "1st place", amountXlm: 4000, percentage: 50 }, + { label: "2nd place", amountXlm: 2400, percentage: 30 }, + { label: "3rd place", amountXlm: 1600, percentage: 20 }, + ], }, { id: "comp-5", @@ -105,6 +144,8 @@ export default function CompetitionsPage() { endDate: "2026-04-25", visibility: "Public", joined: false, + isFinalized: true, + rewardBreakdown: [], }, ]); @@ -190,6 +231,30 @@ export default function CompetitionsPage() { endDate, visibility: createForm.visibility, joined: true, + isFinalized: false, + rewardBreakdown: [ + { + label: "1st place", + amountXlm: Math.round( + (Math.max(0, Number(createForm.prizePoolXlm) || 0) * 50) / 100, + ), + percentage: 50, + }, + { + label: "2nd place", + amountXlm: Math.round( + (Math.max(0, Number(createForm.prizePoolXlm) || 0) * 30) / 100, + ), + percentage: 30, + }, + { + label: "3rd place", + amountXlm: Math.round( + (Math.max(0, Number(createForm.prizePoolXlm) || 0) * 20) / 100, + ), + percentage: 20, + }, + ], }; setCompetitions((prev) => [newCompetition, ...prev]); @@ -197,6 +262,56 @@ export default function CompetitionsPage() { setCreateForm((prev) => ({ ...prev, title: "", description: "" })); }; + useEffect(() => { + if (!address) { + setUserPayouts({}); + return; + } + + competitions + .filter((competition) => competition.isFinalized) + .forEach((competition) => { + getUserPayout(competition.id, address) + .then((payout) => { + setUserPayouts((prev) => ({ ...prev, [competition.id]: payout })); + }) + .catch(() => { + setUserPayouts((prev) => ({ ...prev, [competition.id]: null })); + }); + }); + }, [address, competitions]); + + const onFinalizeEvent = async (id: string) => { + setPendingAction(`finalize-${id}`); + try { + await finalizeEvent(id); + setCompetitions((prev) => + prev.map((competition) => + competition.id === id + ? { ...competition, isFinalized: true, status: "Ended" } + : competition, + ), + ); + } finally { + setPendingAction(null); + } + }; + + const onClaimPrize = async (id: string) => { + if (!address) return; + + setPendingAction(`claim-${id}`); + try { + const payout = await claimPayout(id, address); + setUserPayouts((prev) => ({ + ...prev, + [id]: { ...payout, claimed: true }, + })); + } finally { + setPendingAction(null); + } + }; + const tabs = [ { label: "All" as const, count: tabCounts.all }, { label: "Active" as const, count: tabCounts.active }, @@ -284,6 +399,15 @@ export default function CompetitionsPage() { {paged.map((competition) => { const isEnded = competition.status === "Ended"; const isCancelled = competition.status === "Cancelled"; + const endsAtHasPassed = + new Date(competition.endDate).getTime() <= Date.now(); + const canFinalize = + !competition.isFinalized && endsAtHasPassed && !isCancelled; + const userPayout = userPayouts[competition.id]; + const canClaimPrize = + competition.isFinalized && + Boolean(userPayout) && + !userPayout?.claimed; const canJoin = !competition.joined && !isEnded && !isCancelled; const canLeave = competition.joined && !isEnded && !isCancelled; @@ -353,8 +477,39 @@ export default function CompetitionsPage() { -
- {isEnded ? ( +
+ +
+ +
+ {canFinalize ? ( + + ) : canClaimPrize ? ( + + ) : isEnded ? ( + + {canFinalizeEvent ? ( + + ) : null} + + {canClaimPrize ? ( + + ) : null} +
+
+ + + + ); +} diff --git a/frontend/src/component/PrizePoolSummary.tsx b/frontend/src/component/PrizePoolSummary.tsx index 4b4883275..1f7fd037b 100644 --- a/frontend/src/component/PrizePoolSummary.tsx +++ b/frontend/src/component/PrizePoolSummary.tsx @@ -36,22 +36,28 @@ export default function PrizePoolSummary({
- {rewardBreakdown.map((reward) => ( -
- - {reward.label} - {typeof reward.percentage === "number" ? ( - · {reward.percentage}% - ) : null} - - - {formatXlm(reward.amountXlm)} - -
- ))} + {rewardBreakdown.length > 0 ? ( + rewardBreakdown.map((reward) => ( +
+ + {reward.label} + {typeof reward.percentage === "number" ? ( + · {reward.percentage}% + ) : null} + + + {formatXlm(reward.amountXlm)} + +
+ )) + ) : ( +

+ Reward distribution will be available after prizes are configured. +

+ )}
);