Skip to content
Merged
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
15 changes: 12 additions & 3 deletions app/bounty/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,17 @@ import {
type BountyQueryInput,
} from "@/lib/graphql/generated";

const BOUNTY_TYPES: { value: BountyType; label: string }[] = [
const BOUNTY_TYPES: {
value: BountyType | "MULTI_WINNER_MILESTONE";
label: string;
}[] = [
{ value: BountyType.FixedPrice, label: "Fixed Price" },
{ value: BountyType.MilestoneBased, label: "Milestone Based" },
{ value: BountyType.Competition, label: "Competition" },
{
value: "MULTI_WINNER_MILESTONE" as unknown as BountyType,
label: "Multi-Winner Milestone",
},
];
Comment on lines +38 to 49

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether MULTI_WINNER_MILESTONE is part of the generated GraphQL BountyType enum.
fd -t f 'generated' -e ts -e tsx | head -50
rg -nP -C3 '\benum\s+BountyType\b|BountyType\s*=\s*\{' --type=ts
rg -nP -C2 'MultiWinnerMilestone|MULTI_WINNER_MILESTONE' --type=ts --type=graphql -g '!**/__tests__/**'

Repository: boundlessfi/bounties

Length of output: 515


🏁 Script executed:

# Get the complete BountyType enum definition
sed -n '789,810p' lib/graphql/generated.ts

Repository: boundlessfi/bounties

Length of output: 661


🏁 Script executed:

# Check the app/bounty/page.tsx file for context around line 90 and the queryParams usage
sed -n '85,95p' app/bounty/page.tsx

Repository: boundlessfi/bounties

Length of output: 471


🏁 Script executed:

# Search for any TODOs or comments about MULTI_WINNER_MILESTONE
rg -i 'TODO|FIXME|NOTE.*multi.*winner|multi.*winner.*milestone' app/bounty/page.tsx -B2 -A2

Repository: boundlessfi/bounties

Length of output: 552


🏁 Script executed:

# Check if MULTI_WINNER_MILESTONE is referenced anywhere else in the codebase
rg 'MULTI_WINNER_MILESTONE' --type ts --type tsx

Repository: boundlessfi/bounties

Length of output: 92


Unsafe cast of MULTI_WINNER_MILESTONE — invalid filter value will be sent to the backend.

The generated BountyType enum contains only Competition, FixedPrice, and MilestoneBased. The string "MULTI_WINNER_MILESTONE" is not a valid enum member. Casting it with as unknown as BountyType (lines 45, 105) hides this type error from TypeScript, but at runtime the value is spread directly into queryParams.type (line 90) and sent to the GraphQL backend as part of BountyQueryInput. The backend will reject this invalid enum value, causing the filter to silently fail or error.

Add MULTI_WINNER_MILESTONE to the GraphQL schema and regenerate the BountyType enum so BountyType.MultiWinnerMilestone can 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 a TODO comment to prevent silent shipping.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/bounty/page.tsx` around lines 38 - 49, The BOUNTY_TYPES entry is using an
unsafe cast ("MULTI_WINNER_MILESTONE" as unknown as BountyType) which will send
an invalid enum via queryParams.type to the backend; fix by adding
MULTI_WINNER_MILESTONE to the GraphQL schema and regenerating the TypeScript
types so you can use BountyType.MultiWinnerMilestone in BOUNTY_TYPES (replace
the cast), then ensure any usage that sets queryParams.type (where BountyType is
propagated) uses the real enum value; if you cannot update the schema right
away, gate the UI filter so selecting "Multi-Winner Milestone" is handled
client-side (does not set queryParams.type) and add a TODO comment referencing
the schema/types regen until the enum is added.


const STATUSES: { value: BountyStatus | "all"; label: string }[] = [
Expand Down Expand Up @@ -100,8 +107,10 @@ export default function BountiesPage() {
const currentPage = pagination?.page ?? page;
const totalPages = pagination?.totalPages ?? 1;

const toggleType = (type: BountyType) => {
setSelectedType((prev) => (prev === type ? "all" : type));
const toggleType = (type: BountyType | "MULTI_WINNER_MILESTONE") => {
setSelectedType((prev) =>
prev === type ? "all" : (type as unknown as BountyType),
);
setPage(1);
};

Expand Down
9 changes: 4 additions & 5 deletions app/leaderboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 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 -80

Repository: 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.tsx

Repository: 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 2

Repository: 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 -20

Repository: boundlessfi/bounties

Length of output: 10034


🏁 Script executed:

#!/bin/bash
# Check the commit message and surrounding context
git log --oneline -10
git show 8a934e8 --stat

Repository: boundlessfi/bounties

Length of output: 24350


Replace hardcoded currentUserId with actual user session derivation.

This file uses a hardcoded demo value (currentUserId = "user-1") that will break leaderboard personalization for real users—all visitors will see "user-1" highlighted in LeaderboardTable and have user-1's rank shown in UserRankSidebar. While the "[Coming soon] pattern" may be intentional for this page's current state, the leaderboard logic should derive the user ID from authClient.useSession(), as done in other components throughout the codebase (e.g., bounty-detail-client.tsx, fcfs-claim-button.tsx, use-notifications.ts).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/leaderboard/page.tsx` around lines 48 - 50, Replace the hardcoded
currentUserId variable with the actual session-derived user id by calling
authClient.useSession() in this component (same pattern used in
bounty-detail-client.tsx and fcfs-claim-button.tsx); extract the user id from
the returned session (e.g., session.user.id or equivalent) and pass that value
into LeaderboardTable and UserRankSidebar instead of the static "user-1",
handling the case where session is null/undefined (fall back to undefined/null
so components can render a non-personalized view).


// Debounce filters to prevent rapid API calls/URL updates
const [debouncedFilters, setDebouncedFilters] =
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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=tsx

Repository: 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
fi

Repository: 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=20

Repository: 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 sidebar

Repository: 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 -20

Repository: 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=10

Repository: 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 -20

Repository: 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/null

Repository: 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 user

Repository: boundlessfi/bounties

Length of output: 8501


Fix broken /user/[userId] route reference; the route does not exist.

The leaderboard table's onRowClick handler navigates to /user/${userId}, but this route is not implemented. Only /profile/[userId] exists in the app. Users clicking leaderboard entries will encounter 404 errors. Change the route back to /profile/ or implement the /user/[userId] route.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/leaderboard/page.tsx` around lines 137 - 139, The leaderboard's
onRowClick handler currently calls router.push with a non-existent
`/user/${entry.contributor.userId}` route; update the navigation to use the
existing profile route by changing the router.push target to
`/profile/${entry.contributor.userId}` (locate the onRowClick callback and the
router.push call referencing entry.contributor.userId) so clicks navigate to the
implemented /profile/[userId] page.

/>
)}
Expand Down
96 changes: 96 additions & 0 deletions components/bounty-detail/bounty-detail-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
}, []);
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Maintainer dashboard will display mock contributors when no real data is present.

getFullMilestoneData falls back to MOCK_MODEL4_CONTRIBUTORS for contributorProgress. When the bounty creator is signed in but bounty.contributorProgress is empty/undefined (real-data state), the dashboard will list Alice/Bob/etc. and expose mock-only "Release Payment"/"Advance" actions on them. The PR notes this is intentional during prototyping, but please add a clear gate (or a "Demo data" banner) before merging to a non-demo environment so creators don't act on mock contributors.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty-detail/bounty-detail-client.tsx` around lines 173 - 184,
getFullMilestoneData currently falls back to MOCK_MODEL4_CONTRIBUTORS and causes
Model4MaintainerDashboard to show fake contributors (and actions) when
bounty.contributorProgress is empty; update the rendering logic so that when
contributorProgress is derived from the MOCK_MODEL4_CONTRIBUTORS you either: (a)
do not render Model4MaintainerDashboard for real environments unless an explicit
demo flag is enabled (add and check a feature flag like isDemoMode or
process.env.SHOW_DEMO_CONTRIBUTORS), or (b) render the dashboard but pass an
explicit demo prop and surface a prominent "Demo data" banner inside
Model4MaintainerDashboard; locate getFullMilestoneData, MOCK_MODEL4_CONTRIBUTORS
and the Model4MaintainerDashboard call and implement the gate or banner to
prevent real creators from acting on mock contributors.


{!isCancelled && pool && <EscrowDetailPanel poolId={bountyId} />}
<RefundStatusTracker bountyId={bountyId} isCancelled={isCancelled} />
{bounty.type !== "FIXED_PRICE" && (
Expand Down
57 changes: 55 additions & 2 deletions components/bounty-detail/bounty-detail-sidebar-cta.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
AlertCircle,
XCircle,
Loader2,
Users,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
Expand All @@ -29,14 +30,21 @@ import { FcfsClaimButton } from "@/components/bounty/fcfs-claim-button";
import { authClient } from "@/lib/auth-client";
import type { CancellationRecord } from "@/types/escrow";
import { useCancelBountyDialog } from "@/hooks/use-cancel-bounty-dialog";
import type { Bounty } from "@/types/bounty";

/** Props accept the wider intersection returned by useBountyDetail so
* callers don't need a cast. Optional Bounty fields (maxSlots, etc.)
* are accessible without unsafe assertions. */
type SidebarBounty = BountyFieldsFragment & Partial<Bounty>;

interface SidebarCTAProps {
bounty: BountyFieldsFragment;
bounty: SidebarBounty;
onCancelled?: (record: CancellationRecord) => void;
}

export function SidebarCTA({ bounty, onCancelled }: SidebarCTAProps) {
const [copied, setCopied] = useState(false);
const [isApplying, setIsApplying] = useState(false);
const { data: session } = authClient.useSession();

const {
Expand Down Expand Up @@ -112,13 +120,58 @@ export function SidebarCTA({ bounty, onCancelled }: SidebarCTAProps) {
<span>Type</span>
<TypeBadge type={bounty.type} />
</div>
{bounty.type === "MULTI_WINNER_MILESTONE" &&
(() => {
const occupied = bounty.totalSlotsOccupied ?? 0;
const max = bounty.maxSlots ?? 5;
return (
<div className="flex items-center justify-between text-gray-400">
<span className="flex items-center gap-1.5">
<Users className="size-3.5" /> Slots
</span>
<span className="font-medium text-gray-200">
{occupied} / {max}
</span>
</div>
);
})()}
</div>

<Separator className="bg-gray-800/60" />

{/* CTA */}
{isFcfs ? (
<FcfsClaimButton bounty={bounty} />
) : bounty.type === "MULTI_WINNER_MILESTONE" ? (
(() => {
const occupied = bounty.totalSlotsOccupied ?? 0;
const max = bounty.maxSlots ?? 5;
const isFull = occupied >= max;
return (
<Button
className="w-full h-11 font-bold tracking-wide"
disabled={!canAct || isFull || isApplying}
size="lg"
onClick={async () => {
setIsApplying(true);
console.log(
"[Coming soon] Applying for slot for bounty:",
bounty.id,
);
await new Promise((resolve) => setTimeout(resolve, 1500));
setIsApplying(false);
}}
>
{isApplying ? (
<Loader2 className="size-4 animate-spin mr-2" />
) : isFull ? (
"Slots Full"
) : (
"Apply for Slot [Coming soon]"
)}
</Button>
);
})()
) : (
<Button
className="w-full h-11 font-bold tracking-wide"
Expand Down Expand Up @@ -257,7 +310,7 @@ export function SidebarCTA({ bounty, onCancelled }: SidebarCTAProps) {
}

interface MobileCTAProps {
bounty: BountyFieldsFragment;
bounty: SidebarBounty;
onCancelled?: (record: CancellationRecord) => void;
}

Expand Down
Loading