From 828b3e45f4dec92227187a33f0ae93dd02d05df2 Mon Sep 17 00:00:00 2001 From: unsiqasik Date: Fri, 29 May 2026 10:15:25 +0000 Subject: [PATCH 1/2] feat: extend Bounty type with optional UI fields and remove ad-hoc casts Add missing optional fields to the Bounty interface in types/bounty.ts: - claimCount, maxParticipants, assignedContributorId (number/null) - applications (BountyApplication[] | null) with new BountyApplication type Remove unsafe ad-hoc casts in: - bounty-detail-client.tsx: applications, assignedContributorId, submissions - use-competition-join-state.ts: submissions (updated param type) - use-bounty-cta-state.ts: prefer typed fields with fallback Acceptance criteria: - No (bounty as { ... }) casts remaining in the bounty detail tree - 81/81 tests passing - No behavioral change --- .../bounty-detail/bounty-detail-client.tsx | 20 ++++++++-------- .../bounty-detail/use-bounty-cta-state.ts | 22 ++++++++++-------- hooks/use-competition-join-state.ts | 7 +++--- pnpm-workspace.yaml | 13 +++++++++++ types/bounty.ts | 23 +++++++++++++++++++ 5 files changed, 62 insertions(+), 23 deletions(-) create mode 100644 pnpm-workspace.yaml diff --git a/components/bounty-detail/bounty-detail-client.tsx b/components/bounty-detail/bounty-detail-client.tsx index 6d6ed5c7..8e041b40 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 488872e2..4c2fe0c7 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 1dde47bb..f3c5ee21 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/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 00000000..10cebcdd --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,13 @@ +allowBuilds: + '@reown/appkit': set this to true or false + '@stellar/stellar-sdk': set this to true or false + blake-hash: set this to true or false + bufferutil: set this to true or false + esbuild: set this to true or false + protobufjs: set this to true or false + secp256k1: set this to true or false + sharp: set this to true or false + tiny-secp256k1: set this to true or false + unrs-resolver: set this to true or false + usb: set this to true or false + utf-8-validate: set this to true or false diff --git a/types/bounty.ts b/types/bounty.ts index c0c8580a..3b01e128 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; From f5c73dd55cf63c65d321bdf8ba4bb5091f536581 Mon Sep 17 00:00:00 2001 From: unsiqasik Date: Fri, 29 May 2026 12:59:47 +0000 Subject: [PATCH 2/2] revert: remove unrelated pnpm-workspace.yaml change from PR --- pnpm-workspace.yaml | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 pnpm-workspace.yaml diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml deleted file mode 100644 index 10cebcdd..00000000 --- a/pnpm-workspace.yaml +++ /dev/null @@ -1,13 +0,0 @@ -allowBuilds: - '@reown/appkit': set this to true or false - '@stellar/stellar-sdk': set this to true or false - blake-hash: set this to true or false - bufferutil: set this to true or false - esbuild: set this to true or false - protobufjs: set this to true or false - secp256k1: set this to true or false - sharp: set this to true or false - tiny-secp256k1: set this to true or false - unrs-resolver: set this to true or false - usb: set this to true or false - utf-8-validate: set this to true or false