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
20 changes: 10 additions & 10 deletions components/bounty-detail/bounty-detail-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ import {
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";
import type {
Milestone,
ContributorProgress,
BountyApplication,
} from "@/types/bounty";
import {
ApplicationReviewDashboard,
type Application,
Expand Down Expand Up @@ -71,11 +75,8 @@ 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 ??
[]
);
const getApplications = (bounty: BountyData): BountyApplication[] => {
return bounty?.applications ?? [];
};

export function BountyDetailClient({ bountyId }: { bountyId: string }) {
Expand Down Expand Up @@ -156,17 +157,16 @@ 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");

// 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 ?? [];
(bounty?.submissions as CompetitionSubmissionEntry[] | null | undefined) ??
[];

return (
<div className="flex flex-col lg:flex-row gap-10">
Expand Down
22 changes: 13 additions & 9 deletions components/bounty-detail/use-bounty-cta-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import { useState, useMemo } from "react";
import { useCompetitionJoinState } from "@/hooks/use-competition-join-state";
import { useCanRaiseDispute } from "@/hooks/use-can-raise-dispute";
import { useCancelBountyDialog } from "@/hooks/use-cancel-bounty-dialog";
import { useApplyToBounty, useApplyForSlot } from "@/hooks/use-bounty-application";
import {
useApplyToBounty,
useApplyForSlot,
} from "@/hooks/use-bounty-application";
import { authClient } from "@/lib/auth-client";
import type { BountyFieldsFragment } from "@/lib/graphql/generated";
import type {
ApplicationFormValues,
} from "@/components/bounty/application-dialog";
import type { ApplicationFormValues } from "@/components/bounty/application-dialog";
import type { Bounty } from "@/types/bounty";
import type { CancellationRecord } from "@/types/escrow";

Expand Down Expand Up @@ -52,7 +53,8 @@ export function useBountyCTAState({
const isFcfs = bounty.type === "FIXED_PRICE";
const isCompetition = bounty.type === "COMPETITION";
const isCreator = useMemo(
() => (session?.user as { id?: string } | undefined)?.id === bounty.createdBy,
() =>
(session?.user as { id?: string } | undefined)?.id === bounty.createdBy,
[session?.user, bounty.createdBy],
);

Expand All @@ -65,9 +67,9 @@ export function useBountyCTAState({
[isCreator, bounty.status],
);

// Fallback to _count.submissions until backend adds claimCount / maxParticipants
const claimCount = bounty._count?.submissions ?? 0;
const maxParticipants: number | null = null;
// Prefer typed fields on Bounty; fall back to _count.submissions for backward compat.
const claimCount = bounty.claimCount ?? bounty._count?.submissions ?? 0;
const maxParticipants = bounty.maxParticipants ?? null;
const deadline = bounty.bountyWindow?.endDate ?? null;
const isFinalized = bounty.status === "COMPLETED";
const submissionCount = bounty._count?.submissions ?? 0;
Expand All @@ -89,7 +91,9 @@ export function useBountyCTAState({
);

const isAlreadyJoined = useMemo(
() => bounty.contributorProgress?.some((c) => c.userId === session?.user?.id) ?? false,
() =>
bounty.contributorProgress?.some((c) => c.userId === session?.user?.id) ??
false,
[bounty.contributorProgress, session?.user?.id],
);

Expand Down
7 changes: 3 additions & 4 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,9 +39,7 @@ 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 bountySubmissions = bounty.submissions;
const serverHasJoined =
walletAddress != null &&
(bountySubmissions?.some((s) => s.submittedBy === walletAddress) ?? false);
Expand Down
23 changes: 23 additions & 0 deletions types/bounty.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,25 @@
* Frontend types aligned with the backend GraphQL schema (schema.gql).
*/

/** Application shape returned by the bounty detail query. */
export interface BountyApplication {
id: string;
applicantAddress: string;
applicantName?: string;
proposal: {
approach: string;
estimatedTimeline: string;
relevantExperience: string;
portfolioUrl?: string;
};
reputation: {
score: number;
tier: string;
completionStats: string;
};
createdAt: string;
}

export type BountyType =
| "FIXED_PRICE"
| "MILESTONE_BASED"
Expand Down Expand Up @@ -106,12 +125,16 @@ export interface Bounty {

submissions?: BountySubmission[] | null;
_count?: BountyCount | null;
applications?: BountyApplication[] | null;

milestones?: Milestone[] | null;
contributorProgress?: ContributorProgress[] | null;

maxSlots?: number | null;
totalSlotsOccupied?: number | null;
maxParticipants?: number | null;
claimCount?: number | null;
assignedContributorId?: string | null;

createdBy: string;
createdAt: string;
Expand Down