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
31 changes: 0 additions & 31 deletions components/bounties/SealedUntilDeadline.tsx

This file was deleted.

33 changes: 0 additions & 33 deletions components/bounties/use-deadline-passed.ts

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,6 @@ export default function BountyManagementDashboard() {
<BountySubmissionsPanel
organizationId={organizationId}
bountyId={bountyId}
submissionVisibility={overview.submissionVisibility}
submissionDeadline={overview.submissionDeadline ?? null}
rewardCurrency={overview.rewardCurrency}
staged={stagedWinners}
onToggleStage={toggleStagedWinner}
Expand Down
23 changes: 3 additions & 20 deletions components/organization/bounties/manage/BountyPayoutPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ import {
} from '@/components/ui/select';
import { BoundlessButton } from '@/components/buttons';
import EmptyState from '@/components/EmptyState';
import { SealedUntilDeadline } from '@/components/bounties/SealedUntilDeadline';
import { useDeadlinePassed } from '@/components/bounties/use-deadline-passed';
import {
useAllBountySubmissions,
ESCROW_PHASE_LABEL,
Expand Down Expand Up @@ -63,20 +61,15 @@ export default function BountyPayoutPanel({
staged: Set<string>;
}) {
const isCompleted = overview.status === 'completed';
const deadlinePassed = useDeadlinePassed(overview.submissionDeadline ?? null);
// UX gate only, mirroring the Submissions tab. Publish validation guarantees
// live competitions have a deadline, so a null deadline does not gate.
const gated =
overview.submissionVisibility === 'HIDDEN_UNTIL_DEADLINE' &&
overview.submissionDeadline != null &&
!deadlinePassed;

// Winner selection must see the COMPLETE pool, never one review page.
// Organizers always see submissions regardless of submissionVisibility;
// HIDDEN_UNTIL_DEADLINE only hides peer work from other participants.
const {
data: allSubmissions,
isLoading,
error,
} = useAllBountySubmissions(organizationId, bountyId, { enabled: !gated });
} = useAllBountySubmissions(organizationId, bountyId);
const submissions = useMemo(() => allSubmissions ?? [], [allSubmissions]);

const payout = useBountyPayout({ organizationId, bountyId });
Expand Down Expand Up @@ -168,16 +161,6 @@ export default function BountyPayoutPanel({
);
}

if (gated && overview.submissionDeadline) {
return (
<SealedUntilDeadline
deadline={overview.submissionDeadline}
title='Winner selection opens at the deadline'
description='Competition submissions stay sealed until then.'
/>
);
}

if (isLoading) {
return (
<div className='flex items-center justify-center py-16'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,9 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Badge } from '@/components/ui/badge';
import { BoundlessButton } from '@/components/buttons';
import EmptyState from '@/components/EmptyState';
import { SealedUntilDeadline } from '@/components/bounties/SealedUntilDeadline';
import { submissionStatusClass } from '@/components/bounties/statusClass';
import { useDeadlinePassed } from '@/components/bounties/use-deadline-passed';
import {
useBountySubmissions,
type BountyOperateOverview,
type OrganizerBountySubmission,
} from '@/features/bounties';
import { ordinal } from '@/lib/utils';
Expand All @@ -51,49 +48,26 @@ function formatTierAmount(amount: string): string {
export default function BountySubmissionsPanel({
organizationId,
bountyId,
submissionVisibility,
submissionDeadline,
rewardCurrency,
staged,
onToggleStage,
}: {
organizationId: string;
bountyId: string;
submissionVisibility: BountyOperateOverview['submissionVisibility'];
submissionDeadline: string | null;
rewardCurrency: string;
staged: Set<string>;
onToggleStage: (id: string) => void;
}) {
const [page, setPage] = useState(1);
const deadlinePassed = useDeadlinePassed(submissionDeadline);

// Keep competition work out of the UI until the deadline. Publish validation
// guarantees a deadline for live competitions; without one there is nothing
// to count down to, so we do not gate. NOTE: this is a UX gate only, the
// organizer endpoint itself returns submissions regardless of visibility.
const gated =
submissionVisibility === 'HIDDEN_UNTIL_DEADLINE' &&
submissionDeadline != null &&
!deadlinePassed;

// Don't fetch sealed competition work until the deadline.
// Organizers always see submissions, regardless of submissionVisibility.
// HIDDEN_UNTIL_DEADLINE only hides peer work from other participants.
const { data, isLoading, error } = useBountySubmissions(
organizationId,
bountyId,
{ params: { page, limit: PAGE_SIZE }, enabled: !gated }
{ params: { page, limit: PAGE_SIZE } }
);

if (gated && submissionDeadline) {
return (
<SealedUntilDeadline
deadline={submissionDeadline}
title='Submissions are hidden until the deadline'
description='This is a competition. Work stays sealed so review stays fair.'
/>
);
}

if (isLoading) {
return (
<div className='flex items-center justify-center py-16'>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -408,7 +408,7 @@ export default function SubmissionModelTab({
</p>
<p className='mt-0.5 text-xs text-zinc-500'>
{isCompetition
? 'Hidden until the deadline so the organizer cannot play favorites.'
? "Participants can't see each other's work until the deadline. You review submissions as they arrive."
: 'Visible to the organizer as submissions arrive.'}
</p>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export function computeBountyModeDescription(
if (entryType === 'OPEN') {
return single
? 'Anyone eligible can claim this bounty directly. The first to claim locks it, does the work, and gets paid. One worker, one reward.'
: 'Anyone can join and work in parallel. Submissions stay hidden until the deadline, then the organizer picks the winner(s).';
: 'Anyone can join and work in parallel. Submissions stay hidden from other participants until the deadline, then the organizer picks the winner(s).';
}
const depth =
entryType === 'APPLICATION_LIGHT'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,14 +97,16 @@ export function makeSubmissionModelSchema(
}
}

// Competition modes must hide submissions until the deadline.
// Competition modes must hide peer submissions from other participants
// until the deadline (organizers always see submissions).
if (
claimType === 'COMPETITION' &&
data.submissionVisibility !== 'HIDDEN_UNTIL_DEADLINE'
) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Competition submissions are hidden until the deadline',
message:
'Competition submissions are hidden from other participants until the deadline',
path: ['submissionVisibility'],
});
}
Expand Down
18 changes: 8 additions & 10 deletions features/bounties/api/use-organizer-dashboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,14 @@ export function useBountyOverview(

/**
* Submitted work on a bounty, for the reviewing organizer (#337 / #632).
* Pass `enabled: false` to keep the Submissions tab from fetching sealed
* competition work before the deadline. This is a UX gate only: the API
* returns the organizer's submissions regardless of visibility, so the
* actual seal (if required) must be enforced server-side.
* Organizers always see submissions regardless of submissionVisibility —
* HIDDEN_UNTIL_DEADLINE only hides peer work from other participants, and
* the API returns the organizer's submissions at all times.
*/
export function useBountySubmissions(
organizationId: string | undefined,
bountyId: string | undefined,
options: { params?: OrganizerSubmissionsParams; enabled?: boolean } = {}
options: { params?: OrganizerSubmissionsParams } = {}
) {
const params = options.params ?? {};
return useQuery<OrganizerBountySubmissionList>({
Expand All @@ -55,19 +54,18 @@ export function useBountySubmissions(
bountyId as string,
params
),
enabled: !!organizationId && !!bountyId && (options.enabled ?? true),
enabled: !!organizationId && !!bountyId,
});
}

/**
* The COMPLETE submission set for a bounty (pages through the capped list
* endpoint). Winner selection reads this so the payout pool is never a
* truncated page. Same UX-only caveat as useBountySubmissions.
* truncated page. Same organizer-visibility rule as useBountySubmissions.
*/
export function useAllBountySubmissions(
organizationId: string | undefined,
bountyId: string | undefined,
options: { enabled?: boolean } = {}
bountyId: string | undefined
) {
return useQuery<OrganizerBountySubmission[]>({
queryKey: bountyKeys.orgSubmissionsAll(
Expand All @@ -76,6 +74,6 @@ export function useAllBountySubmissions(
),
queryFn: () =>
listAllBountySubmissions(organizationId as string, bountyId as string),
enabled: !!organizationId && !!bountyId && (options.enabled ?? true),
enabled: !!organizationId && !!bountyId,
});
}
Loading
Loading