Skip to content
Closed
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
44 changes: 22 additions & 22 deletions app/api/leaderboard/route.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,29 @@
import { NextResponse } from 'next/server';
import { getMockLeaderboard } from '@/lib/mock-leaderboard';
import { LeaderboardResponse, ReputationTier } from '@/types/leaderboard';
import { NextResponse } from "next/server";
import { getMockLeaderboard } from "@/lib/mock";
import { LeaderboardResponse, ReputationTier } from "@/types/leaderboard";

export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get('page') || '1');
const limit = parseInt(searchParams.get('limit') || '10');
const tier = searchParams.get('tier') as ReputationTier | null;
const { searchParams } = new URL(request.url);
const page = parseInt(searchParams.get("page") || "1");
const limit = parseInt(searchParams.get("limit") || "10");
const tier = searchParams.get("tier") as ReputationTier | null;

// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 300));
// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 300));

const { data, total } = getMockLeaderboard(page, limit, tier || undefined);
const { data, total } = getMockLeaderboard(page, limit, tier || undefined);

const response: LeaderboardResponse = {
entries: data.map((contributor, index) => ({
rank: (page - 1) * limit + index + 1,
previousRank: null, // Mock data doesn't track history yet
rankChange: 0,
contributor,
})),
totalCount: total,
currentUserRank: null, // Only relevant if user context is provided
lastUpdatedAt: new Date().toISOString(),
};
const response: LeaderboardResponse = {
entries: data.map((contributor, index) => ({
rank: (page - 1) * limit + index + 1,
previousRank: null, // Mock data doesn't track history yet
rankChange: 0,
contributor,
})),
totalCount: total,
currentUserRank: null, // Only relevant if user context is provided
lastUpdatedAt: new Date().toISOString(),
};

return NextResponse.json(response);
return NextResponse.json(response);
}
18 changes: 9 additions & 9 deletions app/api/leaderboard/top/route.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import { NextResponse } from 'next/server';
import { getMockLeaderboard } from '@/lib/mock-leaderboard';
import { NextResponse } from "next/server";
import { getMockLeaderboard } from "@/lib/mock";

export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const count = parseInt(searchParams.get('count') || '5');
const { searchParams } = new URL(request.url);
const count = parseInt(searchParams.get("count") || "5");

// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 200));
// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 200));

// Get top N contributors
const { data } = getMockLeaderboard(1, count);
// Get top N contributors
const { data } = getMockLeaderboard(1, count);

return NextResponse.json(data);
return NextResponse.json(data);
}
32 changes: 16 additions & 16 deletions app/api/leaderboard/user/[userId]/route.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
import { NextResponse } from 'next/server';
import { getMockUserRank } from '@/lib/mock-leaderboard';
import { NextResponse } from "next/server";
import { getMockUserRank } from "@/lib/mock";

interface Params {
params: Promise<{ userId: string }>;
params: Promise<{ userId: string }>;
}

export async function GET(request: Request, { params }: Params) {
// Await params as per Next.js 15+ requirements if applicable, or good practice for future
const { userId } = await params;
// Await params as per Next.js 15+ requirements if applicable, or good practice for future
const { userId } = await params;

// Simulate network delay
await new Promise(resolve => setTimeout(resolve, 200));
// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 200));

const result = getMockUserRank(userId);
const result = getMockUserRank(userId);

if (!result) {
return NextResponse.json({ error: 'User not found' }, { status: 404 });
}
if (!result) {
return NextResponse.json({ error: "User not found" }, { status: 404 });
}

return NextResponse.json({
rank: result.rank,
contributor: result.contributor,
paramsUserId: userId
});
return NextResponse.json({
rank: result.rank,
contributor: result.contributor,
paramsUserId: userId,
});
}
5 changes: 4 additions & 1 deletion app/discover/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import { FilterPanel } from "@/components/filters/filter-panel";
import { ProjectCard } from "@/components/cards/project-card";
import { BountyCard } from "@/components/cards/bounty-card";
import { Skeleton } from "@/components/ui/skeleton";
import { mockProjects, mockBounties as rawMockBounties } from "@/lib/mock-data";
import {
mockDiscoverProjects as mockProjects,
mockDiscoverBounties as rawMockBounties,
} from "@/lib/mock";
import { FilterState, TabType } from "@/lib/types";
import { PackageOpen, Coins } from "lucide-react";
import { BountyLogic } from "@/lib/logic/bounty-logic";
Expand Down
69 changes: 38 additions & 31 deletions app/projects/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,41 +1,43 @@
import { notFound } from "next/navigation"
import type { Metadata } from "next"
import { getAllProjects, getProjectById } from "@/lib/mock-project"
import { truncateAtWordBoundary } from "@/lib/truncate"
import { ProjectLogo } from "@/components/projects/project-logo"
import { ProjectBounties } from "@/components/projects/project-bounties"
import { ProjectMaintainers } from "@/components/projects/project-maintainers"
import { ProjectSidebar } from "@/components/projects/project-sidebar"
import { Badge } from "@/components/ui/badge"
import { Separator } from "@/components/ui/separator"
import { ChevronDown, Globe, ExternalLink } from "lucide-react"
import Markdown from "react-markdown"
import { notFound } from "next/navigation";
import type { Metadata } from "next";
import { getAllProjects, getProjectById } from "@/lib/mock";
import { truncateAtWordBoundary } from "@/lib/truncate";
import { ProjectLogo } from "@/components/projects/project-logo";
import { ProjectBounties } from "@/components/projects/project-bounties";
import { ProjectMaintainers } from "@/components/projects/project-maintainers";
import { ProjectSidebar } from "@/components/projects/project-sidebar";
import { Badge } from "@/components/ui/badge";
import { Separator } from "@/components/ui/separator";
import { ChevronDown, Globe, ExternalLink } from "lucide-react";
import Markdown from "react-markdown";

interface ProjectPageProps {
params: Promise<{ id: string }>
params: Promise<{ id: string }>;
}

export async function generateStaticParams() {
return getAllProjects().map((p) => ({ id: p.id }))
return getAllProjects().map((p) => ({ id: p.id }));
}

export async function generateMetadata({ params }: ProjectPageProps): Promise<Metadata> {
const { id } = await params
const project = getProjectById(id)
export async function generateMetadata({
params,
}: ProjectPageProps): Promise<Metadata> {
const { id } = await params;
const project = getProjectById(id);

if (!project) return { title: "Project Not Found" }
if (!project) return { title: "Project Not Found" };

return {
title: `${project.name} | Projects`,
description: truncateAtWordBoundary(project.description, 160),
}
};
}

export default async function ProjectPage({ params }: ProjectPageProps) {
const { id } = await params
const project = getProjectById(id)
const { id } = await params;
const project = getProjectById(id);

if (!project) notFound()
if (!project) notFound();

return (
<div className="min-h-screen">
Expand All @@ -46,13 +48,17 @@ export default async function ProjectPage({ params }: ProjectPageProps) {
{/* Header Section */}
<header className="space-y-6">
<div className="flex items-start gap-4">
<ProjectLogo name={project.name} logoUrl={project.logoUrl} className="size-16" />
<ProjectLogo
name={project.name}
logoUrl={project.logoUrl}
className="size-16"
/>
<div className="min-w-0 flex-1">
<div className="flex flex-wrap items-center gap-3 mb-2">
<h1 className="text-3xl md:text-4xl font-bold">{project.name}</h1>
<Badge>
{project.openBountyCount} open
</Badge>
<h1 className="text-3xl md:text-4xl font-bold">
{project.name}
</h1>
<Badge>{project.openBountyCount} open</Badge>
{project.websiteUrl && (
<a
href={project.websiteUrl}
Expand All @@ -66,7 +72,9 @@ export default async function ProjectPage({ params }: ProjectPageProps) {
</a>
)}
</div>
<p className="text-lg leading-relaxed">{project.description}</p>
<p className="text-lg leading-relaxed">
{project.description}
</p>
</div>
</div>

Expand Down Expand Up @@ -119,7 +127,7 @@ export default async function ProjectPage({ params }: ProjectPageProps) {
<Markdown>{project.description}</Markdown>
</div>
<div className="pt-4">
<a
<a
href="#bounties"
className="inline-flex items-center gap-2 font-medium hover:underline"
>
Expand All @@ -144,6 +152,5 @@ export default async function ProjectPage({ params }: ProjectPageProps) {
</div>
</div>
</div>
)
);
}

11 changes: 5 additions & 6 deletions app/projects/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import { getAllProjects, getAllProjectTags } from "@/lib/mock-project"
import { ProjectsDiscovery } from "@/components/projects/projects-discovery"
import { getAllProjects, getAllProjectTags } from "@/lib/mock";
import { ProjectsDiscovery } from "@/components/projects/projects-discovery";

export default async function ProjectsPage() {
const projects = getAllProjects()
const allTags = getAllProjectTags(projects)
const projects = getAllProjects();
const allTags = getAllProjectTags(projects);

return <ProjectsDiscovery projects={projects} allTags={allTags} />
return <ProjectsDiscovery projects={projects} allTags={allTags} />;
}

2 changes: 1 addition & 1 deletion app/wallet/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { WalletInfo } from "@/types/wallet";
import { mockWalletWithAssets } from "@/lib/mock-wallet";
import { mockWalletWithAssets } from "@/lib/mock";
import { useSearchParams } from "next/navigation";
import { AlertCircle } from "lucide-react";

Expand Down
27 changes: 11 additions & 16 deletions components/bounty-detail/bounty-detail-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@ import { authClient } from "@/lib/auth-client";
import { useDeadlinePassed } from "@/hooks/use-deadline-passed";
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 { MOCK_MODEL4_MILESTONES, MOCK_MODEL4_CONTRIBUTORS } from "@/lib/mock";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { MilestoneSubmissionCard } from "./milestone-submission-card";
import { Model4MaintainerDashboard } from "./model4-maintainer-dashboard";
Expand Down Expand Up @@ -71,10 +68,7 @@ function getFullMilestoneData(bounty: BountyData): {
// Backend does not currently provide applications in the response.
// Fall back to empty array until the schema supports it.
const getApplications = (bounty: BountyData): Application[] => {
return (
(bounty as BountyData & { applications?: Application[] })?.applications ??
[]
);
return (bounty?.applications as Application[]) ?? [];

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 | ⚡ Quick win

Remove the remaining unsafe cast at Line 74.

This cast weakens the type-safety goal of this PR and can mask shape mismatches between API data and Application.

Proposed fix
-const getApplications = (bounty: BountyData): Application[] => {
-  return (bounty?.applications as Application[]) ?? [];
-};
+const getApplications = (bounty: BountyData): Application[] => {
+  return bounty?.applications ?? [];
+};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/bounty-detail/bounty-detail-client.tsx` at line 74, Remove the
unsafe cast on "bounty?.applications as Application[]": instead validate and
transform the runtime value before returning it (e.g., check
Array.isArray(bounty?.applications), and map/filter items with a small
type-guard or validator like isValidApplication to construct Application
objects), returning [] when the check fails; add or reuse a type guard function
named isValidApplication(item): item is Application and use it in
Array.prototype.filter/map so the function returns a properly-typed
Application[] without relying on an unsafe cast.

};

export function BountyDetailClient({ bountyId }: { bountyId: string }) {
Expand Down Expand Up @@ -155,17 +149,18 @@ export function BountyDetailClient({ bountyId }: { bountyId: string }) {
// Identify if the current user is the assigned contributor
// using a fallback check on submissions or assumed backend field.
const isAssignedApplicant =
(bounty as BountyData & { assignedContributorId?: string })
?.assignedContributorId === session?.user?.id ||
bounty?.assignedContributorId === session?.user?.id ||
bounty.submissions?.some((s) => s.submittedBy === session?.user?.id) ||
(!isCreator && bounty.status === "IN_PROGRESS");
Comment on lines +152 to 154

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 | ⚡ Quick win

Tighten assigned-applicant gating at Line 155.

The (!isCreator && bounty.status === "IN_PROGRESS") branch effectively marks all non-creators as assigned, and Line 156 compares submittedBy to user.id instead of wallet address.

Proposed fix
+  const sessionUserId =
+    (session?.user as { id?: string } | undefined)?.id ?? null;
   const isAssignedApplicant =
-    bounty?.assignedContributorId === session?.user?.id ||
-    bounty.submissions?.some((s) => s.submittedBy === session?.user?.id) ||
-    (!isCreator && bounty.status === "IN_PROGRESS");
+    bounty?.assignedContributorId === sessionUserId ||
+    Boolean(
+      walletAddress &&
+        bounty.submissions?.some((s) => s.submittedBy === walletAddress),
+    );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/bounty-detail/bounty-detail-client.tsx` around lines 155 - 157,
The current assigned-applicant check is too permissive and uses user id instead
of wallet address; update the logic so a user is considered assigned only if
bounty.assignedContributorId matches the current user's wallet address and/or
the bounty.status is IN_PROGRESS and assignedContributorId equals that same
wallet; also change the submissions check to compare s.submittedBy to
session.user.address (wallet) rather than session.user.id; locate and update the
expression referencing bounty?.assignedContributorId,
bounty.submissions?.some((s) => s.submittedBy === session?.user?.id), isCreator,
and bounty.status === "IN_PROGRESS" to implement these stricter checks and keep
proper null/undefined guards.


// submissions is present on BountyQuery (single-bounty query) but not on
// BountyFieldsFragment (list query). The cast is safe here because
// useBountyDetail returns BountyFieldsFragment & Partial<BountyQuery["bounty"]>.
const competitionSubmissions =
(bounty as { submissions?: CompetitionSubmissionEntry[] | null })
.submissions ?? [];
const competitionSubmissions = bounty.submissions ?? [];

// Use the casted submissions list or fallback to our placeholder
const userSubmission = competitionSubmissions.find(
(s) =>
s.submittedBy ===
(session?.user as { walletAddress?: string })?.walletAddress,
);

return (
<div className="flex flex-col lg:flex-row gap-10">
Expand Down
13 changes: 5 additions & 8 deletions hooks/use-competition-join-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from "@/hooks/use-competition-bounty";
import { useDeadlinePassed } from "@/hooks/use-deadline-passed";
import type { BountyFieldsFragment } from "@/lib/graphql/generated";
import type { Bounty } from "@/types/bounty";

interface CompetitionJoinState {
walletAddress: string | null;
Expand All @@ -19,7 +20,7 @@ interface CompetitionJoinState {
}

export function useCompetitionJoinState(
bounty: BountyFieldsFragment,
bounty: BountyFieldsFragment & Partial<Bounty>,
): CompetitionJoinState {
const { data: session } = authClient.useSession();
const joinMutation = useJoinCompetition();
Expand All @@ -38,13 +39,9 @@ export function useCompetitionJoinState(
// Derive from server payload (submissions list on BountyQuery) + local optimism.
// BountyFieldsFragment (list queries) doesn't include submissions, so falls
// back to false until the detail query resolves.
const bountySubmissions = (
bounty as { submissions?: Array<{ submittedBy: string }> | null }
).submissions;
const serverHasJoined =
walletAddress != null &&
(bountySubmissions?.some((s) => s.submittedBy === walletAddress) ?? false);
const hasJoined = serverHasJoined || localJoined;
const hasJoined = Boolean(
bounty?.submissions?.some((sub) => sub.submittedBy === walletAddress),
) || localJoined;

const handleJoin = async () => {
if (!walletAddress) {
Expand Down
Loading