-
Notifications
You must be signed in to change notification settings - Fork 97
Implement Multi-Winner Milestone Bounty Flow (Issue #173) #186
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
b9a1be7
e53504b
75acc86
ccc80e5
c7c22b2
7e48b34
0afb5c0
af10961
8a934e8
fdc377e
05607e9
545b484
20ef410
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,7 +11,6 @@ import { | |
| import { LeaderboardTimeframe } from "@/lib/graphql/generated"; | ||
| import { useState, useEffect } from "react"; | ||
| import { useRouter, useSearchParams } from "next/navigation"; | ||
| import { authClient } from "@/lib/auth-client"; | ||
| import { | ||
| TIMEFRAMES, | ||
| TIERS, | ||
|
|
@@ -46,9 +45,9 @@ export default function LeaderboardPage() { | |
| tags: initialTags || [], | ||
| }); | ||
|
|
||
| // Get current user ID from auth session | ||
| const { data: session } = authClient.useSession(); | ||
| const currentUserId = session?.user?.id; | ||
| // Fake current user ID for demo purposes | ||
| // In a real app this would come from auth context | ||
| const currentUserId = "user-1"; | ||
|
Comment on lines
+48
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Confirm this file used to derive currentUserId from authClient and this is a recent regression
git log --oneline -- app/leaderboard/page.tsx | head -20
git log -p -S 'authClient.useSession' -- app/leaderboard/page.tsx | head -80Repository: boundlessfi/bounties Length of output: 152 🏁 Script executed: #!/bin/bash
# Get current content of the leaderboard file
cat -n app/leaderboard/page.tsx
# Show the specific git diff for the commit mentioned
git show 8a934e8 -- app/leaderboard/page.tsxRepository: boundlessfi/bounties Length of output: 11896 🏁 Script executed: #!/bin/bash
# Search for authClient usage in the codebase
rg "authClient" --type tsx --type ts -A 3 -B 3
# Also search for useSession specifically
rg "useSession" --type tsx --type ts -A 2 -B 2Repository: boundlessfi/bounties Length of output: 120 🏁 Script executed: #!/bin/bash
# Search for authClient usage without file type restriction
rg "authClient" -A 3 -B 3 | head -100
# Also search for useSession
rg "useSession" -A 2 -B 2 | head -100
# Check if there are other uses of session/auth in the app
rg "auth.*Context|session.*Context" --type-list | head -20Repository: boundlessfi/bounties Length of output: 10034 🏁 Script executed: #!/bin/bash
# Check the commit message and surrounding context
git log --oneline -10
git show 8a934e8 --statRepository: boundlessfi/bounties Length of output: 24350 Replace hardcoded This file uses a hardcoded demo value ( 🤖 Prompt for AI Agents |
||
|
|
||
| // Debounce filters to prevent rapid API calls/URL updates | ||
| const [debouncedFilters, setDebouncedFilters] = | ||
|
|
@@ -136,7 +135,7 @@ export default function LeaderboardPage() { | |
| onLoadMore={() => fetchNextPage()} | ||
| currentUserId={currentUserId} | ||
| onRowClick={(entry) => | ||
| router.push(`/profile/${entry.contributor.userId}`) | ||
| router.push(`/user/${entry.contributor.userId}`) | ||
| } | ||
|
Comment on lines
137
to
139
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Look for the route handler/page for /user/[id] vs /profile/[id]
fd -t d -a 'user' app
fd -t d -a 'profile' app
fd -e tsx -e ts . app/user 2>/dev/null
fd -e tsx -e ts . app/profile 2>/dev/null
# Find any other links to /profile/ or /user/ to gauge consistency
rg -nP "['\"]/(?:profile|user)/" --type=ts --type=tsxRepository: boundlessfi/bounties Length of output: 411 🏁 Script executed: #!/bin/bash
# Search for page.tsx files in app/user directory
find app -path "*/user/*/page.tsx" -o -path "*/user/page.tsx" 2>/dev/null | sort
# Check if app/user directory exists and list its contents
if [ -d "app/user" ]; then
echo "=== app/user directory contents ==="
find app/user -type f \( -name "*.tsx" -o -name "*.ts" \) 2>/dev/null | head -20
fi
# Check what's in app/profile for comparison
if [ -d "app/profile" ]; then
echo "=== app/profile directory contents ==="
find app/profile -type f \( -name "*.tsx" -o -name "*.ts" \) 2>/dev/null
fiRepository: boundlessfi/bounties Length of output: 133 🏁 Script executed: #!/bin/bash
# Search for route references in the codebase (without problematic file type flags)
echo "=== References to /user/ route ==="
rg "/user/" app --max-count=20
echo "=== References to /profile/ route ==="
rg "/profile/" app --max-count=20Repository: boundlessfi/bounties Length of output: 340 🏁 Script executed: #!/bin/bash
# Get the specific leaderboard page content around lines 137-139
if [ -f "app/leaderboard/page.tsx" ]; then
echo "=== app/leaderboard/page.tsx lines 130-150 ==="
sed -n '130,150p' app/leaderboard/page.tsx
fi
# Check UserRankSidebar location and content
echo "=== Finding UserRankSidebar ==="
find app -name "*UserRankSidebar*" -o -name "*rank*sidebar*" -o -name "*Rank*" 2>/dev/null | grep -i sidebarRepository: boundlessfi/bounties Length of output: 816 🏁 Script executed: #!/bin/bash
# Search for UserRankSidebar more broadly
echo "=== Searching for UserRankSidebar component ==="
rg "UserRankSidebar" app --max-count=10
echo "=== Searching for any Sidebar component ==="
find app -name "*Sidebar*" -o -name "*sidebar*" 2>/dev/null
echo "=== Searching in components directory ==="
find app/components -type f \( -name "*.tsx" -o -name "*.ts" \) 2>/dev/null | head -20Repository: boundlessfi/bounties Length of output: 381 🏁 Script executed: #!/bin/bash
# Check if there's a components directory and search for UserRank or Rank components
rg "export.*UserRankSidebar\|class UserRankSidebar\|function UserRankSidebar" app --max-count=5
# Also search for any route navigation in components
rg "router\.push.*user\|router\.push.*profile" app --max-count=10Repository: boundlessfi/bounties Length of output: 46 🏁 Script executed: #!/bin/bash
# Find the actual components directory
echo "=== Locating components directory ==="
find . -maxdepth 3 -type d -name "components" 2>/dev/null
echo "=== Searching for user-rank-sidebar file ==="
fd "user-rank-sidebar" . 2>/dev/null
echo "=== Checking tsconfig or path aliases ==="
if [ -f "tsconfig.json" ]; then
cat tsconfig.json | grep -A 5 "paths\|baseUrl"
fi
echo "=== Full directory structure at root ==="
ls -la | head -20Repository: boundlessfi/bounties Length of output: 1561 🏁 Script executed: #!/bin/bash
# Search recursively for user-rank-sidebar
rg "user-rank-sidebar" . --type ts --type tsx 2>/dev/null
# Also try to find any file with UserRankSidebar definition
rg "class UserRankSidebar|function UserRankSidebar|const UserRankSidebar|export.*UserRankSidebar" . 2>/dev/nullRepository: boundlessfi/bounties Length of output: 179 🏁 Script executed: #!/bin/bash
# Read the user-rank-sidebar.tsx file to check for route references
echo "=== Contents of user-rank-sidebar.tsx ==="
cat components/leaderboard/user-rank-sidebar.tsx
# Also check if /app/user route exists at all
echo "=== Check for /app/user route ==="
ls -la app/ | grep userRepository: boundlessfi/bounties Length of output: 8501 Fix broken The leaderboard table's 🤖 Prompt for AI Agents |
||
| /> | ||
| )} | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,7 +15,49 @@ import { EscrowDetailPanel } from "../bounty/escrow-detail-panel"; | |
| import { RefundStatusTracker } from "../bounty/refund-status"; | ||
| import { FeeCalculator } from "../bounty/fee-calculator"; | ||
| import { useEscrowPool } from "@/hooks/use-escrow"; | ||
| import { authClient } from "@/lib/auth-client"; | ||
| import type { CancellationRecord } from "@/types/escrow"; | ||
| import { MilestoneFunnel } from "@/components/bounty/milestone-funnel"; | ||
| import { | ||
| MOCK_MODEL4_MILESTONES, | ||
| MOCK_MODEL4_CONTRIBUTORS, | ||
| } from "@/lib/mock-model4"; | ||
| import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; | ||
| import { MilestoneSubmissionCard } from "./milestone-submission-card"; | ||
| import { Model4MaintainerDashboard } from "./model4-maintainer-dashboard"; | ||
| import type { Milestone, ContributorProgress } from "@/types/bounty"; | ||
|
|
||
| type BountyData = ReturnType<typeof useBountyDetail>["data"]; | ||
|
|
||
| /** Returns milestones with mock fallback. Safe for public display since | ||
| * milestones are structural (titles/descriptions), not personal data. | ||
| */ | ||
| function getMilestones(bounty: BountyData): Milestone[] { | ||
| return bounty?.milestones ?? MOCK_MODEL4_MILESTONES; | ||
| } | ||
|
|
||
| /** Returns contributorProgress WITHOUT mock fallback — only real API data. | ||
| * Used for the public MilestoneFunnel to prevent mock users (Alice, Bob…) | ||
| * from being displayed to unauthenticated visitors. | ||
| */ | ||
| function getRealContributors(bounty: BountyData): ContributorProgress[] { | ||
| return bounty?.contributorProgress ?? []; | ||
| } | ||
|
|
||
| /** Returns full data including mock contributorProgress fallback. | ||
| * Used only in authenticated sections (contributor progress card, | ||
| * maintainer dashboard) where mocks are acceptable during prototyping. | ||
| */ | ||
| function getFullMilestoneData(bounty: BountyData): { | ||
| milestones: Milestone[]; | ||
| contributorProgress: ContributorProgress[]; | ||
| } { | ||
| return { | ||
| milestones: bounty?.milestones ?? MOCK_MODEL4_MILESTONES, | ||
| contributorProgress: | ||
| bounty?.contributorProgress ?? MOCK_MODEL4_CONTRIBUTORS, | ||
| }; | ||
| } | ||
|
|
||
| export function BountyDetailClient({ bountyId }: { bountyId: string }) { | ||
| const router = useRouter(); | ||
|
|
@@ -24,6 +66,8 @@ export function BountyDetailClient({ bountyId }: { bountyId: string }) { | |
| const [cancellationRecord, setCancellationRecord] = | ||
| useState<CancellationRecord | null>(null); | ||
|
|
||
| const { data: session } = authClient.useSession(); | ||
|
|
||
| const handleCancelled = useCallback((record: CancellationRecord) => { | ||
| setCancellationRecord(record); | ||
| }, []); | ||
|
|
@@ -87,6 +131,58 @@ export function BountyDetailClient({ bountyId }: { bountyId: string }) { | |
| <div className="flex-1 min-w-0 space-y-6"> | ||
| <HeaderCard bounty={bounty} /> | ||
| <DescriptionCard description={bounty.description} /> | ||
|
|
||
| {bounty.type === "MULTI_WINNER_MILESTONE" && ( | ||
| <Card className="border-gray-800 bg-background-card/50 backdrop-blur-sm overflow-hidden"> | ||
| <CardHeader className="border-b border-gray-800/50 pb-4"> | ||
| <CardTitle className="text-lg font-bold flex items-center gap-2"> | ||
| Milestone Funnel | ||
| <span className="text-xs font-normal text-muted-foreground bg-primary/10 text-primary px-2 py-0.5 rounded-full"> | ||
| Multi-Winner | ||
| </span> | ||
| </CardTitle> | ||
| </CardHeader> | ||
| <CardContent className="pt-6"> | ||
| {/* contributors is intentionally real-data-only: mock users | ||
| (Alice, Bob…) must not be shown to unauthenticated visitors */} | ||
| <MilestoneFunnel | ||
| milestones={getMilestones(bounty)} | ||
| contributors={getRealContributors(bounty)} | ||
| /> | ||
| </CardContent> | ||
| </Card> | ||
| )} | ||
|
|
||
| {bounty.type === "MULTI_WINNER_MILESTONE" && | ||
| session?.user?.id && | ||
| (() => { | ||
| const { milestones, contributorProgress } = | ||
| getFullMilestoneData(bounty); | ||
| const myProgress = contributorProgress.find( | ||
| (c) => c.userId === session.user.id, | ||
| ); | ||
| if (!myProgress) return null; | ||
| return ( | ||
| <MilestoneSubmissionCard | ||
| milestones={milestones} | ||
| contributorProgress={myProgress} | ||
| /> | ||
| ); | ||
| })()} | ||
|
|
||
| {bounty.type === "MULTI_WINNER_MILESTONE" && | ||
| session?.user?.id === bounty.createdBy && | ||
| (() => { | ||
| const { milestones, contributorProgress } = | ||
| getFullMilestoneData(bounty); | ||
| return ( | ||
| <Model4MaintainerDashboard | ||
| milestones={milestones} | ||
| contributors={contributorProgress} | ||
| /> | ||
| ); | ||
| })()} | ||
|
Comment on lines
+173
to
+184
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maintainer dashboard will display mock contributors when no real data is present.
🤖 Prompt for AI Agents |
||
|
|
||
| {!isCancelled && pool && <EscrowDetailPanel poolId={bountyId} />} | ||
| <RefundStatusTracker bountyId={bountyId} isCancelled={isCancelled} /> | ||
| {bounty.type !== "FIXED_PRICE" && ( | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 515
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 661
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 471
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 552
🏁 Script executed:
Repository: boundlessfi/bounties
Length of output: 92
Unsafe cast of
MULTI_WINNER_MILESTONE— invalid filter value will be sent to the backend.The generated
BountyTypeenum contains onlyCompetition,FixedPrice, andMilestoneBased. The string"MULTI_WINNER_MILESTONE"is not a valid enum member. Casting it withas unknown as BountyType(lines 45, 105) hides this type error from TypeScript, but at runtime the value is spread directly intoqueryParams.type(line 90) and sent to the GraphQL backend as part ofBountyQueryInput. The backend will reject this invalid enum value, causing the filter to silently fail or error.Add
MULTI_WINNER_MILESTONEto the GraphQL schema and regenerate theBountyTypeenum soBountyType.MultiWinnerMilestonecan be used without casts. If that cannot be done immediately, gate the filter behind a client-side check that does not round-trip to the backend, or add aTODOcomment to prevent silent shipping.🤖 Prompt for AI Agents