diff --git a/components/bounty-detail/bounty-detail-client.tsx b/components/bounty-detail/bounty-detail-client.tsx index 6d6ed5c..8e041b4 100644 --- a/components/bounty-detail/bounty-detail-client.tsx +++ b/components/bounty-detail/bounty-detail-client.tsx @@ -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, @@ -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 }) { @@ -156,8 +157,7 @@ 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"); @@ -165,8 +165,8 @@ export function BountyDetailClient({ bountyId }: { bountyId: string }) { // BountyFieldsFragment (list query). The cast is safe here because // useBountyDetail returns BountyFieldsFragment & Partial. const competitionSubmissions = - (bounty as { submissions?: CompetitionSubmissionEntry[] | null }) - .submissions ?? []; + (bounty?.submissions as CompetitionSubmissionEntry[] | null | undefined) ?? + []; return (
diff --git a/components/bounty-detail/use-bounty-cta-state.ts b/components/bounty-detail/use-bounty-cta-state.ts index 488872e..4c2fe0c 100644 --- a/components/bounty-detail/use-bounty-cta-state.ts +++ b/components/bounty-detail/use-bounty-cta-state.ts @@ -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"; @@ -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], ); @@ -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; @@ -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], ); diff --git a/hooks/use-competition-join-state.ts b/hooks/use-competition-join-state.ts index 1dde47b..f3c5ee2 100644 --- a/hooks/use-competition-join-state.ts +++ b/hooks/use-competition-join-state.ts @@ -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; @@ -19,7 +20,7 @@ interface CompetitionJoinState { } export function useCompetitionJoinState( - bounty: BountyFieldsFragment, + bounty: BountyFieldsFragment & Partial, ): CompetitionJoinState { const { data: session } = authClient.useSession(); const joinMutation = useJoinCompetition(); @@ -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); diff --git a/types/bounty.ts b/types/bounty.ts index c0c8580..3b01e12 100644 --- a/types/bounty.ts +++ b/types/bounty.ts @@ -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" @@ -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;