Skip to content

Comments

Reward settings#389

Merged
Benjtalkshow merged 119 commits intoboundlessfi:mainfrom
Benjtalkshow:reward-settings
Feb 22, 2026
Merged

Reward settings#389
Benjtalkshow merged 119 commits intoboundlessfi:mainfrom
Benjtalkshow:reward-settings

Conversation

@Benjtalkshow
Copy link
Collaborator

@Benjtalkshow Benjtalkshow commented Feb 22, 2026

Summary by CodeRabbit

  • New Features

    • Reward distribution status banner and dedicated distribution UI with refresh.
  • Improvements

    • Timeline shows judging window and winner announcement dates.
    • Deadline-aware submissions: creation/register menus disable after deadline.
    • Dynamic hackathon loading with richer prize and submission displays.
    • Settings: slug and tagline editable; registration-closed disables participant controls.
  • Refinements

    • Rewards flow: "Reward Winners" wording, wallets step removed, compact podium/winners layout, announcement truncation with View More/Less, tightened winner card sizing.

@vercel
Copy link

vercel bot commented Feb 22, 2026

@Benjtalkshow is attempting to deploy a commit to the Threadflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Feb 22, 2026

📝 Walkthrough

Walkthrough

This PR adds backend-driven reward distribution (status fetch and trigger), extends hackathon types and draft preview mapping with judging/winner timeline and organization fields, loads live hackathon data in settings, removes client-side wallet/milestone flows, and adds deadline-aware submission UI and multiple rewards UI/data-shape updates.

Changes

Cohort / File(s) Summary
API Types & Endpoints
lib/api/hackathons.ts, lib/api/hackathons/judging.ts
Extended Hackathon type with judgingStart/judgingEnd/winnersAnnouncedAt/resultsPublished/metadata. Added reward-distribution types and endpoints (getRewardDistributionStatus, triggerRewardDistribution). getJudgingSubmissions accepts optional status.
Reward Distribution Core
hooks/use-reward-distribution-status.ts, components/organization/hackathons/rewards/RewardDistributionStatusBanner.tsx
New hook to fetch/refresh distribution status and a new banner component rendering status, timestamps, totals, winners, notes, and refresh control.
Rewards Page Integration
components/organization/hackathons/rewards/RewardsPageContent.tsx, app/(landing)/organizations/[id]/hackathons/[hackathonId]/rewards/page.tsx
Rewards page/content accept and surface distributionStatus, loading and refresh callback; UI adds status badge/sheet and trigger distribution controls; publish flow now triggers backend distribution and refetch behavior.
Removed Wallet/Milestone Flow
hooks/use-wallet-addresses.ts, hooks/use-milestone-creation.ts, components/organization/hackathons/rewards/CreateMilestonesButton.tsx, components/organization/hackathons/rewards/WalletsStep.tsx
Deleted client-side wallet-address hook, milestone-creation hook/component and wallets wizard step; publish flow simplified to backend distribution trigger.
Publish Flow & Hooks
hooks/use-publish-winners.ts, hooks/use-hackathon-rewards.ts, hooks/use-wizard-steps.ts
usePublishWinners now uses idempotency keys and triggerRewardDistribution; useHackathonRewards enriched with refetchHackathon/resultsPublished/hackathon; wizard steps reduced to announcement→preview.
Winners UI/Data Model
components/organization/hackathons/rewards/*, components/organization/hackathons/rewards/types.ts, components/organization/hackathons/rewards/WinnersPreviewPage.tsx, components/organization/hackathons/rewards/WinnersGrid.tsx
Prize APIs now return structured {amount,currency,label}; WinnerCard props relaxed; submission model adds category and commentCount; grid/layout and rendering logic updated to display tiers with winners only.
Preview & Organization Mapping
app/(landing)/hackathons/preview/[orgId]/[draftId]/page.tsx, components/landing-page/Hero2.tsx
Draft preview now fetches organization data (name/logo) and maps timeline fields (judging*, winnersAnnouncedAt). Hero sample cards updated with concrete ISO datetimes and new timeline fields.
Settings & Dynamic Data
app/(landing)/organizations/[id]/hackathons/[hackathonId]/settings/page.tsx, components/organization/hackathons/settings/*
Settings page now loads hackathon via API, maps into tab forms, resets forms on data changes, and saves per-section to appropriate endpoints with onSaveSuccess/refetch behavior.
Participant & Registration Controls
components/organization/hackathons/new/tabs/ParticipantTab.tsx, components/organization/hackathons/settings/ParticipantSettingsTab.tsx, components/hackathons/hackathonStickyCard.tsx
Added isRegistrationClosed prop propagation and disabled UI states for participant controls; sticky card shows explicit "Hackathon Ended" disabled button.
Submission Deadline Awareness
components/hackathons/submissions/submissionTab.tsx, components/hackathons/submissions/submissionCard.tsx
Introduced useHackathonStatus usage and derived isDeadlinePassed; disables "Create Submission" and hides per-submission dropdowns after deadline.
Rewards Mapping & Utilities
lib/utils/rewards-data-mapper.ts, lib/utils/renderHtml.ts, components/organization/hackathons/rewards/AnnouncementSection.tsx
Made rewards mapping defensive (name/avatar/project fallbacks), added category/commentCount fields, changed sanitizeHtml fallback to empty, and updated announcement truncation/UX and sanitization usage.
Schemas & Validation
components/organization/hackathons/new/tabs/schemas/*
Slug schema tightened (length/regex); prize-tier schema adds required integer rank, updating inferred types.
Dependencies
package.json
Added date-fns-tz, p-limit, uuid and their types.
Misc UI/Styling
several components under components/organization/hackathons/rewards/*, components/organization/hackathons/settings/*
Multiple styling, spacing, text, and small API surface prop changes across rewards/setting components (e.g., PreviewStep getPrizeForRank return type, WinnerCard sizing, Announcement UI).

Sequence Diagram

sequenceDiagram
    participant User
    participant RewardsPage as "Rewards Page"
    participant Hook as "useRewardDistributionStatus"
    participant API as "Backend API"
    participant Banner as "RewardDistributionStatusBanner"

    User->>RewardsPage: open rewards page
    RewardsPage->>Hook: fetch distribution status (orgId, hackathonId)
    Hook->>API: GET /organizations/:orgId/hackathons/:hackathonId/distribution-status
    alt 200 OK
        API-->>Hook: RewardDistributionStatusResponse
    else 404
        API-->>Hook: 404
        Hook-->>RewardsPage: NOT_TRIGGERED placeholder
    end
    Hook-->>RewardsPage: distributionStatus, isLoading, refetch
    RewardsPage->>Banner: render status banner

    User->>RewardsPage: click "Trigger Distribution"
    RewardsPage->>API: POST /organizations/:orgId/hackathons/:hackathonId/trigger-distribution (idempotencyKey)
    API->>API: create distribution / start processing
    API-->>RewardsPage: RewardDistributionStatusResponse
    RewardsPage->>Hook: refetch()
    Hook->>API: GET /distribution-status (updated)
    API-->>Hook: updated status
    Hook-->>RewardsPage: updated distributionStatus
    RewardsPage->>Banner: show updated status/timestamps
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • 0xdevcollins

"I hopped along the code today,
Made distributions find their way,
Removed the wallets, tuned the clock,
Banners bloom and deadlines lock,
A rabbit cheers: rewards now play!" 🐇✨

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Reward settings' is vague and overly generic, failing to convey the specific nature of the extensive changes across reward distribution, hackathon settings, and submission management. Use a more descriptive title that captures the primary changes, such as 'Add reward distribution status tracking and publish flow refactor' or 'Implement reward distribution workflow with status management'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 19

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (7)
components/organization/hackathons/rewards/WizardFooter.tsx (1)

57-61: ⚠️ Potential issue | 🟡 Minor

Stale loading text — update "Publishing..." to match "Reward Winners".

The action button now reads "Reward Winners" but its loading state still says "Publishing...", creating an inconsistent user-facing message mid-submission.

✏️ Proposed fix
            {isPublishing ? (
              <>
                <Loader2 className='h-4 w-4 animate-spin' />
-               Publishing...
+               Rewarding...
              </>
            ) : (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/rewards/WizardFooter.tsx` around lines 57
- 61, In WizardFooter.tsx update the loading text to match the action label:
inside the isPublishing conditional (where Loader2 is rendered) replace the
string "Publishing..." with the matching action text (e.g., "Reward Winners" or
"Rewarding...") so the Loader2 + text reflects the button label consistently.
components/organization/hackathons/new/tabs/ParticipantTab.tsx (2)

435-448: ⚠️ Potential issue | 🟡 Minor

teamMax NumberInput is missing disabled={isRegistrationClosed}.

teamMin at line 407 correctly receives disabled={isRegistrationClosed}, but teamMax does not. This lets an organizer modify the maximum team size even when registration is closed, leading to an inconsistent read-only state.

🐛 Proposed fix
  <NumberInput
    value={field.value || 5}
+   disabled={isRegistrationClosed}
    onIncrement={() => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/new/tabs/ParticipantTab.tsx` around lines
435 - 448, The teamMax NumberInput is missing the disabled prop which allows
edits when registration should be closed; update the NumberInput for teamMax
(the component using field.value/onIncrement/onDecrement for teamMax) to pass
disabled={isRegistrationClosed} (same pattern used by the teamMin NumberInput)
so that both increment/decrement handlers and manual edits are disabled when
isRegistrationClosed is true; ensure you reference the same isRegistrationClosed
variable and preserve existing onIncrement/onDecrement logic that uses
form.getValues('teamMin') and field.onChange.

336-384: ⚠️ Potential issue | 🟡 Minor

Custom deadline calendar can still be changed when registration is closed.

The registration-policy buttons are correctly disabled when isRegistrationClosed is true, but the PopoverTrigger / Button that opens the calendar date picker has no corresponding disabled state. If the current policy is already 'custom', the organizer can still open the popover and pick a new date while registration is closed.

🐛 Proposed fix
  <Button
    variant='outline'
+   disabled={isRegistrationClosed}
    className={cn(
      'h-12 w-full rounded-lg border border-zinc-800 bg-zinc-900/30 p-4 text-left font-normal hover:bg-zinc-900/50',
-     !field.value && 'text-zinc-500'
+     !field.value && 'text-zinc-500',
+     isRegistrationClosed && 'cursor-not-allowed opacity-60'
    )}
  >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/new/tabs/ParticipantTab.tsx` around lines
336 - 384, The custom-deadline popover remains interactive even when
registration is closed; disable the popover trigger and prevent date selection
when isRegistrationClosed is true. Update the PopoverTrigger/Button used inside
the FormField render (the Button rendered as child of PopoverTrigger) to pass a
disabled prop when isRegistrationClosed is true and add a guard in the Calendar
onSelect handler (and/or set Calendar to read-only) so it does not call
field.onChange if isRegistrationClosed is true; keep existing UI styling but
ensure the trigger is non-interactive and the Calendar cannot change the value
while isRegistrationClosed is true.
lib/api/hackathons/judging.ts (2)

361-383: ⚠️ Potential issue | 🟠 Major

organizationId is declared but never used in the request URL.

Every other scoped function in this file (addJudge, removeJudge, submitGrade) correctly includes organizationId in an /organizations/${organizationId}/hackathons/... pattern. This function calls /hackathons/${hackathonId}/submissions directly, silently dropping organizationId.

Call sites in hooks/use-hackathon-rewards.ts and app/(landing)/organizations/[id]/hackathons/[hackathonId]/judging/page.tsx explicitly pass organizationId, expecting scope enforcement.

Either:

  • The endpoint genuinely requires no org scoping → remove the organizationId parameter from the signature to avoid misleading callers, or
  • The endpoint should be org-scoped → update the URL to /organizations/${organizationId}/hackathons/${hackathonId}/submissions.

Additionally, this function is duplicated identically in both lib/api/hackathons/judging.ts and lib/api/hackathons.ts, creating a maintenance risk where the two copies can diverge.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/api/hackathons/judging.ts` around lines 361 - 383, The
getJudgingSubmissions function declares organizationId but omits it from the
request URL; update the function (getJudgingSubmissions) to use the org-scoped
endpoint
`/organizations/${organizationId}/hackathons/${hackathonId}/submissions`
(matching other functions like addJudge/removeJudge/submitGrade) or remove the
organizationId parameter from the signature if the endpoint truly isn't
org-scoped—then update all call sites accordingly; also remove or consolidate
the duplicate implementation present in the other module (the copy in the other
file) so there is a single source of truth for getJudgingSubmissions.

358-383: 🛠️ Refactor suggestion | 🟠 Major

Consolidate duplicate function — getJudgingSubmissions exported identically from both lib/api/hackathons.ts and lib/api/hackathons/judging.ts.

Both files contain byte-for-byte identical implementations. Worse, different parts of the codebase import from different paths: hooks/use-hackathon-rewards.ts imports from @/lib/api/hackathons while app/(landing)/organizations/[id]/hackathons/[hackathonId]/judging/page.tsx imports from @/lib/api/hackathons/judging. Updating one without the other silently diverges behavior for different callers.

Keep the implementation in lib/api/hackathons/judging.ts and either re-export it from lib/api/hackathons.ts or update all callers to import from a single source.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/api/hackathons/judging.ts` around lines 358 - 383, The
getJudgingSubmissions implementation is duplicated; remove the duplicate from
lib/api/hackathons.ts and re-export the single source from
lib/api/hackathons/judging.ts so all callers use the same function, or
alternatively update all imports to point to lib/api/hackathons/judging.ts;
specifically keep the implementation in the exported function
getJudgingSubmissions in lib/api/hackathons/judging.ts and add an export {
getJudgingSubmissions } from './hackathons/judging' (or update import sites like
hooks/use-hackathon-rewards.ts and app/.../judging/page.tsx to import
getJudgingSubmissions from '@/lib/api/hackathons/judging') to consolidate the
symbol.
package.json (1)

136-146: ⚠️ Potential issue | 🟡 Minor

Remove @types/uuid from devDependencies — it is redundant.

uuid@7+ bundles its own TypeScript declarations, making @types/uuid unnecessary. Since the project uses uuid@^13.0.0, the stub package only adds clutter and potential for duplicate/competing type definitions. The codebase correctly uses ESM syntax (import { v4 } from 'uuid'), and uuid@13 is ESM-only with no CommonJS concerns.

🔧 Proposed fix
-    "@types/uuid": "^10.0.0",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` around lines 136 - 146, Remove the redundant devDependency
"@types/uuid" from package.json since the installed "uuid" (uuid@^13.0.0)
includes its own TypeScript declarations; edit the "devDependencies" block to
delete the "@types/uuid" entry, then run your package manager (npm/yarn/pnpm) to
update node_modules and the lockfile so the dependency graph is consistent.
components/organization/hackathons/rewards/PublishWinnersWizard.tsx (1)

89-97: ⚠️ Potential issue | 🟠 Major

Unsafe as any cast bypasses type checking for non-existent rank property.

PrizeTier does not have a rank property (it has place, prizeAmount, description, currency, passMark, id). The cast (tier as any).rank || 0 defaults every tier to rank 0, causing getPrizeForRank() to never match winners to rewards.

♻️ Suggested approach
  const mappedPrizeTiers = useMemo(
    () =>
-     prizeTiers.map(tier => ({
-       rank: (tier as any).rank || 0,
+     prizeTiers.map((tier, index) => ({
+       rank: index + 1,
        prizeAmount: tier.prizeAmount,
        currency: tier.currency,
      })),
    [prizeTiers]
  );

Tiers are ordered by position in the array, so using index + 1 gives correct rank assignments.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/rewards/PublishWinnersWizard.tsx` around
lines 89 - 97, The mappedPrizeTiers code is using an unsafe (tier as any).rank
which always falls back to 0; update the useMemo to derive rank from the array
index so ranks reflect position (use prizeTiers.map((tier, index) => ...) and
set rank = index + 1), preserve prizeAmount and currency, and ensure any
consumers like getPrizeForRank() will receive correct ranks from
mappedPrizeTiers.
🧹 Nitpick comments (32)
components/organization/hackathons/settings/AdvancedSettingsTab.tsx (2)

103-103: Prefer unknown over any for the caught error.

Using catch (error: any) disables all type-checking on the error object. Typing it as unknown and narrowing (or using a type guard / utility like isAxiosError) is safer and more idiomatic TypeScript.

Proposed fix
-    } catch (error: any) {
-      const message = error.message || error.response?.data?.message;
+    } catch (error: unknown) {
+      const err = error as Record<string, any>;
+      const message =
+        err?.response?.data?.message || err?.message;
       const errorMessage = Array.isArray(message) ? message[0] : message;

Alternatively, if axios is available:

import { isAxiosError } from 'axios';

} catch (error: unknown) {
  let message: string | string[] | undefined;
  if (isAxiosError(error)) {
    message = error.response?.data?.message;
  }
  if (!message && error instanceof Error) {
    message = error.message;
  }
  const errorMessage = Array.isArray(message) ? message[0] : message;
  ...
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/settings/AdvancedSettingsTab.tsx` at line
103, Change the catch clause in AdvancedSettingsTab (currently "catch (error:
any)") to use "error: unknown" and narrow it before accessing properties: use
axios's isAxiosError(error) to extract error.response?.data?.message when
available, fall back to checking "error instanceof Error" for error.message,
normalize array messages to a single string, and then use that computed
errorMessage in the existing error handling logic (preserving the same UI/update
paths that follow the catch).

90-112: onSubmit handler naming.

Per coding guidelines, event handlers should start with handle (e.g., handleSubmit). While onSubmit is conventional in react-hook-form examples, renaming to handleSubmit (and adjusting the form binding to form.handleSubmit(handleSubmitForm) or similar to avoid shadowing) would align with the project convention. Low priority given this is a common react-hook-form pattern.

As per coding guidelines: "Event handlers should start with 'handle' prefix (e.g., handleClick, handleSubmit)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/settings/AdvancedSettingsTab.tsx` around
lines 90 - 112, Rename the onSubmit function to follow the project's "handle"
prefix convention (e.g., handleAdvancedSettingsSubmit) and update the form
binding so it calls form.handleSubmit(handleAdvancedSettingsSubmit) (or similar)
to avoid shadowing react-hook-form's handleSubmit; keep the same implementation
(setIsSaving, api.patch, toast, form.reset, onSaveSuccess) but reference the new
function name wherever onSubmit was used. Ensure imports/props referencing
onSubmit are updated and no other identifiers are changed.
components/organization/hackathons/settings/TimelineSettingsTab.tsx (6)

51-56: Prefer const arrow function with explicit type annotation.

The component is exported as a function declaration. The project guidelines prefer const arrow functions with explicit type annotations.

Suggested change
-export default function TimelineSettingsTab({
-  organizationId,
-  hackathonId,
-  initialData,
-  onSaveSuccess,
-}: TimelineSettingsTabProps) {
+const TimelineSettingsTab: React.FC<TimelineSettingsTabProps> = ({
+  organizationId,
+  hackathonId,
+  initialData,
+  onSaveSuccess,
+}) => {

And at the bottom of the file:

export default TimelineSettingsTab;

As per coding guidelines, "Prefer const arrow functions with explicit type annotations over function declarations".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/settings/TimelineSettingsTab.tsx` around
lines 51 - 56, The component is currently a function declaration; change it to a
const arrow function with an explicit React/TypeScript type annotation and
export it as the default at the bottom. Replace the function declaration
"function TimelineSettingsTab({ organizationId, hackathonId, initialData,
onSaveSuccess, }: TimelineSettingsTabProps)" with a const arrow like "const
TimelineSettingsTab: React.FC<TimelineSettingsTabProps> = ({ organizationId,
hackathonId, initialData, onSaveSuccess }) => { ... }" and then add "export
default TimelineSettingsTab;" after the component to match project guidelines.

104-104: Event handler naming: prefer handle prefix.

onSubmit reads like a callback prop name. Rename to e.g. handleSave to follow the project convention and avoid confusion with form.handleSubmit.

As per coding guidelines, "Event handlers should start with 'handle' prefix (e.g., handleClick, handleSubmit)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/settings/TimelineSettingsTab.tsx` at line
104, Rename the event handler function currently declared as onSubmit to follow
the project's "handle" prefix convention (e.g., handleSave); update the function
name declaration and all internal references/usages (including where it's passed
into form.handleSubmit or any JSX props) so nothing breaks—specifically change
the identifier onSubmit to handleSave in the TimelineSettingsTab component and
replace every call/site that references onSubmit (such as
form.handleSubmit(onSubmit) or any prop forwarding) to use handleSave instead.

58-85: Duplicated form default values shape.

The same field mapping is written twice — once in defaultValues (lines 60–69) and again in the useEffect reset (lines 74–82). Extract a helper to avoid drift when fields are added or renamed.

Suggested refactor
+const buildFormDefaults = (data?: Partial<TimelineFormData>): TimelineFormData => ({
+  startDate: data?.startDate || undefined,
+  submissionDeadline: data?.submissionDeadline || undefined,
+  judgingStart: data?.judgingStart || undefined,
+  endDate: data?.endDate || undefined,
+  judgingEnd: data?.judgingEnd || undefined,
+  winnersAnnouncedAt: data?.winnersAnnouncedAt || undefined,
+  timezone: data?.timezone || 'UTC',
+  phases: data?.phases || [],
+});
+
 const form = useForm<TimelineFormData>({
   resolver: zodResolver(timelineSchema),
-  defaultValues: {
-    startDate: initialData?.startDate || undefined,
-    ...
-  },
+  defaultValues: buildFormDefaults(initialData),
 });

 useEffect(() => {
   if (initialData) {
-    form.reset({
-      startDate: initialData.startDate || undefined,
-      ...
-    });
+    form.reset(buildFormDefaults(initialData));
   }
 }, [initialData, form]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/settings/TimelineSettingsTab.tsx` around
lines 58 - 85, Extract a single helper (e.g.,
buildTimelineFormValues(initialData?: Partial<TimelineFormData>):
TimelineFormData) that maps incoming initialData to the exact shape used by the
form (startDate, submissionDeadline, judgingStart, endDate, judgingEnd,
winnersAnnouncedAt, timezone with default 'UTC', phases default []), then use
that helper for useForm's defaultValues and inside the useEffect call to
form.reset; update references to TimelineFormData, timelineSchema, useForm and
form.reset so both places derive values from the same function to avoid
duplication and drift.

609-618: Consider disabling save when the form is not dirty.

The button is only disabled during save. You could also disable it when no changes have been made (!form.formState.isDirty) to avoid unnecessary API calls and give clearer UX feedback.

 <BoundlessButton
   type='submit'
   variant='default'
   size='lg'
-  disabled={isSaving}
+  disabled={isSaving || !form.formState.isDirty}
   className='min-w-[120px]'
 >
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/settings/TimelineSettingsTab.tsx` around
lines 609 - 618, The Save button currently only uses isSaving to determine
disable state; update the BoundlessButton disabled prop to also be true when the
form has no changes by combining isSaving with the form dirty flag (e.g.,
disabled={isSaving || !form.formState.isDirty}). Locate the BoundlessButton in
TimelineSettingsTab and reference the existing form instance
(form.formState.isDirty) to prevent submitting unchanged data and improve UX.

166-398: Heavy duplication across date-picker fields.

The four date-picker blocks (startDate, submissionDeadline, judgingStart, endDate) are near-identical — same structure, styling, and behavior — differing only in field name, label, and placeholder. The optional judgingEnd and winnersAnnouncedAt blocks (lines 429–576) follow the same pattern. Consider extracting a DateTimePickerField component to DRY this up.

Sketch of extracted component
interface DateTimePickerFieldProps {
  name: keyof TimelineFormData;
  label: string;
  placeholder: string;
  required?: boolean;
  control: Control<TimelineFormData>;
  formatTimeValue: (date?: Date) => string;
  applyTimeToDate: (date: Date, timeValue: string) => Date;
}

const DateTimePickerField: React.FC<DateTimePickerFieldProps> = ({
  name, label, placeholder, required, control, formatTimeValue, applyTimeToDate,
}) => (
  <FormField
    control={control}
    name={name}
    render={({ field }) => (
      <FormItem className='gap-3'>
        <FormLabel className='text-sm'>
          {label} {required && <span className='text-error-400'>*</span>}
        </FormLabel>
        {/* ... calendar + time input (same markup) ... */}
      </FormItem>
    )}
  />
);

Usage:

<DateTimePickerField
  name="startDate"
  label="Start Date"
  placeholder="Select start date"
  required
  control={form.control}
  formatTimeValue={formatTimeValue}
  applyTimeToDate={applyTimeToDate}
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/settings/TimelineSettingsTab.tsx` around
lines 166 - 398, There is heavy duplication across the four nearly identical
date-picker FormField blocks (startDate, submissionDeadline, judgingStart,
endDate) — extract a reusable DateTimePickerField React component that accepts
props: name (keyof TimelineFormData), label, placeholder, required?, control
(use form.control), formatTimeValue, and applyTimeToDate; have
DateTimePickerField render the shared FormField/FormItem/Popover/Calendar/Input
markup and wire up field.onChange, field.value, disabled logic, and FormMessage,
then replace each duplicated block with a DateTimePickerField instance (also
reuse for judgingEnd and winnersAnnouncedAt) to DRY up the file.

141-141: Prefer unknown over any in catch blocks.

Using error: any bypasses type safety. Use unknown and narrow with type guards for safer error handling.

Suggested fix
-    } catch (error: any) {
-      const message = error.message || error.response?.data?.message;
+    } catch (error: unknown) {
+      const message =
+        error instanceof Error
+          ? error.message
+          : typeof error === 'object' && error !== null && 'response' in error
+            ? (error as { response?: { data?: { message?: string } } }).response?.data?.message
+            : undefined;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/settings/TimelineSettingsTab.tsx` at line
141, Replace the catch parameter typed as any with unknown (change catch (error:
any) to catch (error: unknown)) and narrow it before use: check if error is an
instance of Error (or use a type guard) to safely access error.message,
otherwise fallback to String(error) when logging or showing UI messages; update
the catch block inside the TimelineSettingsTab component's async handler so all
usages of the caught value use the narrowed form.
components/organization/hackathons/rewards/AnnouncementSection.tsx (2)

8-16: Two guideline violations: onEdit prop name and function declaration style

  1. onEdit prop: Per coding guidelines, event handlers should start with handle. The prop should be onEdithandleEdit (and updated at the call-site).

  2. Component declaration: The component uses export default function instead of a const arrow function with an explicit type annotation, which is the preferred style for .tsx files.

♻️ Proposed refactor
- interface AnnouncementSectionProps {
+ interface AnnouncementSectionProps {
    announcement: string;
-   onEdit: () => void;
+   handleEdit: () => void;
  }

- export default function AnnouncementSection({
+ const AnnouncementSection = ({
    announcement,
-   onEdit,
+   handleEdit,
  }: AnnouncementSectionProps) => {
    // ...
    <Button
      variant='ghost'
      size='sm'
-     onClick={onEdit}
+     onClick={handleEdit}
      className='text-primary hover:bg-primary/10 h-6 px-2 py-0 text-xs'
    >
  // ...
- }
+ };
+
+ export default AnnouncementSection;

As per coding guidelines: "Event handlers should start with 'handle' prefix" and "Prefer const arrow functions with explicit type annotations over function declarations."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/rewards/AnnouncementSection.tsx` around
lines 8 - 16, The AnnouncementSection component and props should be renamed and
re-typed: rename the prop onEdit to handleEdit in AnnouncementSectionProps and
in the destructured props of AnnouncementSection (and update all call-sites to
pass handleEdit instead of onEdit); convert the component declaration from a
function declaration to a const arrow with an explicit type annotation, e.g.
declare const AnnouncementSection: React.FC<AnnouncementSectionProps> = ({
announcement, handleEdit }) => { ... } and then export default
AnnouncementSection; ensure you update any references to
AnnouncementSectionProps, AnnouncementSection, and the handler prop names across
the codebase.

52-57: Extract anonymous onClick to a named handleToggleExpand handler

The inline arrow on onClick is an anonymous function with no named handler, violating the coding guideline that event handlers must start with the handle prefix.

♻️ Proposed refactor
+ const handleToggleExpand = () => setIsExpanded(!isExpanded);

  <button
-   onClick={() => setIsExpanded(!isExpanded)}
+   onClick={handleToggleExpand}
    className='text-primary mt-1 text-xs hover:underline'
  >

As per coding guidelines: "Event handlers should start with 'handle' prefix (e.g., handleClick, handleSubmit)."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/rewards/AnnouncementSection.tsx` around
lines 52 - 57, Extract the inline anonymous onClick handler into a named handler
function called handleToggleExpand; move the logic currently in the button's
onClick (which toggles setIsExpanded(!isExpanded)) into handleToggleExpand and
use that handler on the button, keeping the existing props and className; ensure
the handler references setIsExpanded and isExpanded (or uses functional updater
setIsExpanded(prev => !prev)) and update the button to
onClick={handleToggleExpand} to satisfy the "handle" prefix guideline.
components/hackathons/submissions/submissionTab.tsx (2)

73-78: status variable at line 74 is shadowed inside the .map() callback at line 293.

At line 74, status is 'upcoming' | 'ongoing' | 'ended' (hackathon lifecycle). Inside the .map() at line 293, a new const status is declared with type 'Approved' | 'Rejected' | 'Pending'. Both scopes work correctly, but the name collision is confusing and will be flagged by eslint/no-shadow.

♻️ Rename inner variable to avoid shadowing
- const status =
+ const submissionStatus =
    submission.status?.toLowerCase() === 'shortlisted'
      ? 'Approved'
      : submission.status?.toLowerCase() === 'disqualified'
        ? 'Rejected'
        : 'Pending';

  return (
    <SubmissionCard
      ...
-     status={status}
+     status={submissionStatus}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/hackathons/submissions/submissionTab.tsx` around lines 73 - 78,
The top-level hackathon lifecycle variable named status (returned from
useHackathonStatus and stored in currentHackathon-related code) is being
shadowed by a different const status inside the .map() callback (the
per-submission approval state); rename the inner variable (for example to
submissionStatus or approvalStatus) and update all references inside that .map()
callback to use the new name so eslint no-shadow is satisfied while keeping the
outer status unchanged.

73-73: useHackathonData() is called redundantly in SubmissionTabContent.

SubmissionTab (line 400) already holds currentHackathon and could pass isDeadlinePassed (or the raw date fields) as a prop to SubmissionTabContent, eliminating the extra context read and keeping SubmissionTabContent's dependencies explicit.

♻️ Thread `isDeadlinePassed` as a prop instead

In SubmissionTabContentProps:

 interface SubmissionTabContentProps extends SubmissionTabProps {
+  isDeadlinePassed: boolean;
   mySubmission: any;
   ...

In SubmissionTab (around line 438):

+ const { status } = useHackathonStatus(
+   currentHackathon?.startDate,
+   currentHackathon?.submissionDeadline
+ );
+ const isDeadlinePassed = status === 'ended';

  <SubmissionTabContent
+   isDeadlinePassed={isDeadlinePassed}
    ...

Then remove lines 73–78 from SubmissionTabContent entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/hackathons/submissions/submissionTab.tsx` at line 73,
SubmissionTabContent currently re-reads context via useHackathonData()
redundantly; instead, modify SubmissionTab to compute and pass the needed
deadline info (e.g., isDeadlinePassed or the raw deadline dates from
currentHackathon) into SubmissionTabContent via its props, update
SubmissionTabContentProps to accept that prop, and remove the useHackathonData()
call and related lines (the block around lines 73–78) from SubmissionTabContent
so it no longer reads context directly; reference SubmissionTab and
SubmissionTabContentProps to locate the changes and ensure all uses inside
SubmissionTabContent now read the passed prop.
components/organization/hackathons/new/tabs/ParticipantTab.tsx (4)

143-147: Use an early return and rename per project conventions.

The current block wraps the happy-path in an if body instead of early-returning on the falsy branch. The project guideline requires early returns for clarity. Additionally, the guideline requires event handlers to be prefixed with handle; onSubmit should be handleSubmit (or handleFormSubmit to avoid shadowing form.handleSubmit).

♻️ Proposed refactor
-const onSubmit = async (data: ParticipantFormData) => {
-  if (onSave) {
-    await onSave(data);
-  }
-};
+const handleFormSubmit = async (data: ParticipantFormData) => {
+  if (!onSave) return;
+  await onSave(data);
+};

Update the form's onSubmit prop accordingly:

-<form onSubmit={form.handleSubmit(onSubmit)} …>
+<form onSubmit={form.handleSubmit(handleFormSubmit)} …>

As per coding guidelines: "Use early returns to improve code clarity" and "Event handlers should start with 'handle' prefix".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/new/tabs/ParticipantTab.tsx` around lines
143 - 147, Rename the event handler function `onSubmit` to `handleSubmit` (or
`handleFormSubmit` to avoid shadowing `form.handleSubmit`) and change its
implementation to use an early return: if `onSave` is falsy return immediately,
otherwise await `onSave(data)`; also update the form's `onSubmit` prop to
reference the new handler name so the component uses the renamed handler.

225-228: !isRegistrationClosed && guard inside onClick is redundant.

When disabled={isRegistrationClosed} is set on the button, browsers will not fire the click event at all, so the inline guard provides no additional safety. Removing it reduces noise without any functional change.

♻️ Proposed simplification (shown for participant-type buttons; apply identically to policy buttons)
  onClick={() =>
-   !isRegistrationClosed && field.onChange(value)
+   field.onChange(value)
  }
  disabled={isRegistrationClosed}

Also applies to: 291-294

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/new/tabs/ParticipantTab.tsx` around lines
225 - 228, Remove the redundant inline guard from the button onClick handlers:
the button already sets disabled={isRegistrationClosed}, so the check
"!isRegistrationClosed && field.onChange(value)" is unnecessary; change the
onClick to just call field.onChange(value) (or the equivalent handler) for the
participant-type buttons and the policy buttons that use the same pattern,
leaving the disabled prop intact so the browser prevents clicks when
registration is closed.

137-141: initialData reference instability can silently discard in-progress edits.

If the parent component does not stabilise the initialData object reference (e.g., via useMemo), every parent re-render produces a new object reference and triggers form.reset(), discarding whatever the user has typed. Consider comparing relevant fields before resetting, or document the expectation that callers must memoize initialData.

♻️ Suggested guard
 React.useEffect(() => {
   if (initialData) {
-    form.reset(initialData);
+    form.reset(initialData, { keepDirtyValues: true });
   }
 }, [initialData, form]);

keepDirtyValues: true preserves fields the user has already changed while still syncing any server-side updates.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/new/tabs/ParticipantTab.tsx` around lines
137 - 141, The effect in ParticipantTab.tsx resets the form on any change to the
initialData reference causing in-progress edits to be discarded; change the
useEffect so it only resets when the meaningful values actually differ or when
the form is pristine — e.g., inside the React.useEffect that references
initialData and form, compare key fields from initialData with form.getValues()
(or check form.isDirty) and only call form.reset(...) when they differ, or call
form.reset(..., { keepDirtyValues: true }) to preserve user edits while syncing
server updates; update the logic around React.useEffect, initialData, and
form.reset to implement this guard.

149-196: NumberInput is defined inside the component body — move it out.

Because NumberInput is declared as a new function reference inside ParticipantTab's render, React treats it as a brand-new component type every render, causing it to unmount and remount on every update (including unrelated state changes). This drops focus and resets any internal DOM state on each re-render.

Extract NumberInput (and its NumberInputProps interface) above or outside ParticipantTab:

♻️ Proposed refactor
+interface NumberInputProps {
+  value: number;
+  onIncrement: () => void;
+  onDecrement: () => void;
+  disabled?: boolean;
+}
+
+const NumberInput = ({
+  value,
+  onIncrement,
+  onDecrement,
+  disabled = false,
+}: NumberInputProps) => (
+  // … existing JSX unchanged …
+);

 export default function ParticipantTab({ … }: ParticipantTabProps) {
-  interface NumberInputProps { … }
-  const NumberInput = ({ … }) => ( … );
   // rest of component
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/new/tabs/ParticipantTab.tsx` around lines
149 - 196, NumberInput and its NumberInputProps are currently declared inside
ParticipantTab causing remounts; move the NumberInputProps interface and the
NumberInput functional component so they are declared outside (above) the
ParticipantTab component body, keep the same prop names (value, onIncrement,
onDecrement, disabled) and JSX, and ensure any used symbols (cn, Minus, Plus)
remain in scope or imported so ParticipantTab can render <NumberInput ... />
without changing its call sites.
components/hackathons/hackathonStickyCard.tsx (1)

240-247: Consider a more semantically distinct icon for the "Hackathon Ended" state.

The Calendar icon is already used for the active Register/Join button, making it ambiguous in the disabled "Hackathon Ended" context. An icon like CheckCircle or Clock would better communicate finality to users.

🎨 Suggested icon swap
-            <Calendar className='mr-1.5 h-3.5 w-3.5' />
+            <CheckCircle className='mr-1.5 h-3.5 w-3.5' />

Add CheckCircle to the existing lucide-react import at line 10.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/hackathons/hackathonStickyCard.tsx` around lines 240 - 247,
Replace the ambiguous Calendar icon used in the disabled "Hackathon Ended"
button with a more finality-oriented icon: add CheckCircle to the existing
lucide-react import and swap the <Calendar .../> element inside the ended-state
branch of the HackathonStickyCard component (the JSX that renders the disabled
Button when status === 'ended') to <CheckCircle .../> so the ended state is
semantically distinct from the active Register/Join button.
components/organization/hackathons/rewards/RewardDistributionStatusBanner.tsx (1)

195-207: Magic number 1e7 for prize pool conversion.

The division by 1e7 (10,000,000) appears to be a Stellar stroops-to-token conversion, but it's unexplained. Extract it to a named constant for clarity and maintainability.

♻️ Suggested improvement
+const STROOPS_PER_UNIT = 1e7;
+
 // Inside the component:
-                  {(
-                    distributionStatus.snapshot.totalPrizePool / 1e7
-                  ).toLocaleString('en-US', { maximumFractionDigits: 2 })}{' '}
+                  {(
+                    distributionStatus.snapshot.totalPrizePool / STROOPS_PER_UNIT
+                  ).toLocaleString('en-US', { maximumFractionDigits: 2 })}{' '}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@components/organization/hackathons/rewards/RewardDistributionStatusBanner.tsx`
around lines 195 - 207, Extract the magic number 1e7 used to convert Stellar
stroops into tokens inside the RewardDistributionStatusBanner component into a
clearly named constant (e.g., STROOPS_PER_TOKEN or STROOPS_TO_TOKEN) and replace
the inline division (distributionStatus.snapshot.totalPrizePool / 1e7) with a
reference to that constant; update any nearby uses (like formatting for
distributionStatus.snapshot.totalPrizePool) to use the constant and add a short
comment explaining the conversion to keep the purpose and units explicit.
app/(landing)/organizations/[id]/hackathons/[hackathonId]/settings/page.tsx (2)

51-55: fetchHackathon missing from useEffect dependency array.

fetchHackathon is referenced inside the effect but not listed in the dependency array. While this works because hackathonId changes trigger a re-render, ESLint's react-hooks/exhaustive-deps rule will flag this. Wrap fetchHackathon in useCallback with [hackathonId] deps, or inline the fetch inside the effect.

♻️ Suggested fix
+ import { useCallback } from 'react';
  ...
- const fetchHackathon = async () => {
+ const fetchHackathon = useCallback(async () => {
    try {
      const res = await getHackathon(hackathonId);
      setHackathon(res.data);
    } catch {
      toast.error('Failed to load hackathon data');
    } finally {
      setIsLoading(false);
    }
- };
+ }, [hackathonId]);

  useEffect(() => {
    if (hackathonId) {
      fetchHackathon();
    }
- }, [hackathonId]);
+ }, [hackathonId, fetchHackathon]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(landing)/organizations/[id]/hackathons/[hackathonId]/settings/page.tsx
around lines 51 - 55, The effect uses fetchHackathon but doesn’t list it in its
dependency array, which will trigger react-hooks/exhaustive-deps; to fix, either
wrap the fetchHackathon function in useCallback with [hackathonId] and then
include fetchHackathon in the useEffect deps, or move the fetch logic directly
into the useEffect and keep [hackathonId] as the sole dependency—update the
useEffect dependency array accordingly and ensure fetchHackathon (or the inlined
fetch) captures hackathonId correctly.

61-140: Pervasive as any casts in mapping helpers erode type safety.

The mapping functions (getGeneralData, getTimelineData, getParticipantData, getAdvancedData) and the initialData props (lines 267, 307, etc.) heavily rely on as any to bridge the gap between Hackathon fields and the tab component prop types.

This suppresses compile-time detection of shape mismatches. Consider defining explicit intermediate types (or reusing the tab components' expected types) for each mapper's return value. At minimum, replace as any on venueType (line 70) and participantType (line 104) with proper union type assertions.

♻️ Example for venueType
-     venueType: h.venueType.toLowerCase() as any,
+     venueType: h.venueType.toLowerCase() as 'virtual' | 'physical',
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(landing)/organizations/[id]/hackathons/[hackathonId]/settings/page.tsx
around lines 61 - 140, The helpers getGeneralData, getTimelineData,
getParticipantData and getAdvancedData currently use "as any" casts that hide
type errors; replace those casts by declaring explicit return types (or reusing
the tab components' prop interfaces) for each mapper and map fields to those
types. Specifically: give getGeneralData a return type that defines venueType as
the exact union expected by the UI and convert venueType =
h.venueType.toLowerCase() to an explicit union assertion rather than "as any";
give getParticipantData a return type that defines participantType and
registrationDeadlinePolicy as their exact unions and replace participantType =
h.participantType.toLowerCase() as any with an explicit cast/validation to that
union; for getTimelineData declare phases' element type and remove the "as any"
on phases by mapping to the typed phase shape; and for getAdvancedData provide a
typed return value for discord/telegram fields. Ensure the new mapper signatures
match the initialData prop types used by the settings tabs so TypeScript will
catch shape mismatches.
hooks/use-publish-winners.ts (1)

36-48: Side effect during render: sessionStorage access outside of useEffect.

Reading/writing sessionStorage in the component body (outside useEffect or a callback) is a side effect during render. While the if (!idempotencyKeyRef.current) guard makes it effectively run only once and the logic is idempotent, React's concurrent features or Strict Mode double-invocation could cause unexpected behavior.

Consider moving this initialization into a lazy initializer or useEffect:

♻️ Suggested: use a helper for lazy ref init
+ const getIdempotencyKey = (hId: string): string => {
+   if (typeof window !== 'undefined') {
+     const storageKey = `publish-idempotency-${hId}`;
+     let key = sessionStorage.getItem(storageKey);
+     if (!key) {
+       key = uuidv4();
+       sessionStorage.setItem(storageKey, key);
+     }
+     return key;
+   }
+   return uuidv4();
+ };
+
  const idempotencyKeyRef = useRef<string | null>(null);

- if (!idempotencyKeyRef.current) {
-   if (typeof window !== 'undefined') {
-     const storageKey = `publish-idempotency-${hackathonId}`;
-     let key = sessionStorage.getItem(storageKey);
-     if (!key) {
-       key = uuidv4();
-       sessionStorage.setItem(storageKey, key);
-     }
-     idempotencyKeyRef.current = key;
-   } else {
-     idempotencyKeyRef.current = uuidv4();
-   }
- }
+ if (!idempotencyKeyRef.current) {
+   idempotencyKeyRef.current = getIdempotencyKey(hackathonId);
+ }

This at least extracts the logic, though ideally the initialization would happen inside publishWinners itself (lazy on first call) to keep the render pure.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-publish-winners.ts` around lines 36 - 48, The sessionStorage
read/write in the hook render should be moved out of the component body to avoid
render-side effects: initialize idempotencyKeyRef.current lazily (or inside a
useEffect) instead of directly accessing sessionStorage at top-level; for
example, replace the current block that references idempotencyKeyRef,
sessionStorage, storageKey, uuidv4 and hackathonId with a lazy initializer or a
useEffect that sets idempotencyKeyRef.current (or set it the first time
publishWinners runs) and only then reads/writes sessionStorage so render remains
pure.
components/organization/hackathons/rewards/WinnersGrid.tsx (1)

37-48: getTierOrder fallback logic is redundant when tiers already have ranks.

When sortedTiers.length === 3, the .find(t => t.rank === 2) with fallback || sortedTiers[1] is only needed if the three tiers don't have ranks 1, 2, 3 (e.g., ranks 1, 2, 5). Since tiersWithWinners can contain any ranks, the fallback is a reasonable safety measure.

However, note that getTierOrder is not memoized despite receiving a new array reference on every render (from the memoized tiersWithWinners). Consider wrapping it with useMemo or calling it inside the existing memo:

♻️ Optional: memoize tiersToDisplay
- const tiersToDisplay = getTierOrder(tiersWithWinners);
+ const tiersToDisplay = useMemo(
+   () => getTierOrder(tiersWithWinners),
+   [tiersWithWinners]
+ );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/rewards/WinnersGrid.tsx` around lines 37 -
48, getTierOrder performs sorting each render and isn't memoized even though
tiersWithWinners is memoized; wrap the tier-order computation with useMemo (or
move getTierOrder call inside the existing memo that computes tiersToDisplay) so
you only recompute when tiersWithWinners changes, keeping the same fallback
logic (sortedTiers.find(...) || sortedTiers[index]) inside the memoized callback
for function getTierOrder/tier ordering to avoid unnecessary re-sorts on every
render.
hooks/use-wizard-steps.ts (1)

19-22: escrow prop is accepted but never used.

After removing the milestone/wallet gating logic, escrow is no longer referenced anywhere in the hook body. Consider removing it from UseWizardStepsProps and all call sites to avoid confusion about its purpose.

♻️ Suggested cleanup
-interface UseWizardStepsProps {
-  open: boolean;
-  escrow: HackathonEscrowData | null;
-}
+interface UseWizardStepsProps {
+  open: boolean;
+}

Also remove the unused import:

-import type { HackathonEscrowData } from '@/lib/api/hackathons';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-wizard-steps.ts` around lines 19 - 22, Remove the unused escrow
prop and its type from the hook: delete escrow: HackathonEscrowData | null from
the UseWizardStepsProps interface and remove any import of HackathonEscrowData
at the top of hooks/use-wizard-steps.ts; then update all call sites that pass an
escrow argument to useWizardSteps (remove the argument) so the hook signature
and usages stay consistent (look for references to UseWizardStepsProps and any
import of HackathonEscrowData to find affected places).
components/organization/hackathons/rewards/PublishWinnersWizard.tsx (1)

87-87: canGoNext is hardcoded to true — consider removing it.

Since canGoNext is always true and no conditional logic gates navigation, it's effectively dead code. If there's no plan to re-introduce conditional gating, remove it and the corresponding prop from WizardFooter.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/rewards/PublishWinnersWizard.tsx` at line
87, The constant canGoNext in PublishWinnersWizard.tsx is hardcoded to true and
unused for gating — remove the declaration of canGoNext and any place it's
passed into WizardFooter (or other components) so you don't carry a dead prop;
then update the WizardFooter invocation in PublishWinnersWizard to omit the
canGoNext prop and, if WizardFooter's prop type includes canGoNext, remove it
from WizardFooter's props/interface and any internal references (e.g., in
WizardFooter component definition) to keep types and usage consistent.
components/organization/hackathons/rewards/RewardsPageContent.tsx (3)

126-133: Use cn() for merging conditional classes.

Line 128 concatenates badge.color via a template literal. As per coding guidelines, prefer clsx or a similar helper for conditional classes. The cn utility is already used elsewhere in the project.

Suggested fix
+import { cn } from '@/lib/utils';
 ...
 <button
   onClick={() => setIsStatusSheetOpen(true)}
-  className={`flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs font-medium transition-opacity hover:opacity-80 ${badge.color}`}
+  className={cn(
+    'flex items-center gap-2 rounded-full border px-3 py-1.5 text-xs font-medium transition-opacity hover:opacity-80',
+    badge.color
+  )}
 >

As per coding guidelines, "For conditional classes, prefer clsx or similar helper functions over ternary operators in JSX".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/rewards/RewardsPageContent.tsx` around
lines 126 - 133, The button's className currently concatenates badge.color with
a template literal; replace that concatenation with the project's cn utility to
merge classes safely. Update the className for the button in RewardsPageContent
(the element using setIsStatusSheetOpen and badge.icon/label) to call cn(...)
with the static classes string and badge.color as additional argument so
conditional/resolved classes are merged via cn instead of string interpolation.

178-190: Naming mismatch: button says "Trigger Reward Distribution" but calls onPublishClick.

onPublishClick opens the PublishWinnersWizard, which is semantically different from "triggering reward distribution." This could confuse future maintainers. If the publish wizard now serves as the distribution trigger, consider renaming the prop to something like onTriggerDistribution for clarity, or updating the button label to match the actual action.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/rewards/RewardsPageContent.tsx` around
lines 178 - 190, The button label and prop name are semantically inconsistent:
the BoundlessButton shows "Trigger Reward Distribution" / "Re-trigger Reward
Distribution" but invokes onPublishClick which opens PublishWinnersWizard;
rename the prop and any handlers to reflect the actual action (e.g., rename
onPublishClick -> onTriggerDistribution or openPublishWinners ->
triggerDistribution) and update references where PublishWinnersWizard is opened,
plus adjust prop types and tests; alternatively, if the intent is to "publish"
rather than "trigger distribution", change the button text to match
onPublishClick/PublishWinnersWizard (and keep canTrigger and isRejected checks
intact).

74-119: STATUS_BADGE is recreated with new React elements on every render.

Since the object contains JSX nodes (icons), it can't be trivially hoisted out of the component. Consider wrapping it in useMemo to avoid creating new element instances on each render. This is a minor optimization, but the component renders on every parent state change.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/rewards/RewardsPageContent.tsx` around
lines 74 - 119, STATUS_BADGE is rebuilt each render because it contains JSX icon
nodes; wrap its creation in React's useMemo so the icon elements (e.g., Info,
Clock, CheckCircle2, Loader2, XCircle, AlertTriangle) are only re-created when
the dependent status-related data changes. Replace the plain const STATUS_BADGE
= { ... } with a memoized value (useMemo(() => ({ ... }), [] or with a small
dependency array if statuses change) and keep the lookup const badge = status ?
STATUS_BADGE[status] : null so renders reuse the same icon instances.
hooks/use-hackathon-rewards.ts (1)

275-278: Brittle avatar detection heuristic.

avatar.includes('github.com/shadcn.png') is a magic string check for a "generic" avatar. If the default avatar URL ever changes, this silently breaks enrichment for all submissions that have the new default. Consider extracting this to a named constant or checking for the absence of a user-uploaded avatar instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-hackathon-rewards.ts` around lines 275 - 278, The avatar detection
is brittle because it relies on the magic string 'github.com/shadcn.png'; update
the logic in use-hackathon-rewards.ts where isGenericAvatar is computed to use a
named constant (e.g., GENERIC_AVATAR_URL) or a more robust check (e.g., detect
absence of a user-uploaded avatar by checking falsy avatar or hostname/path
patterns) instead of a hardcoded substring; update any references to
isGenericAvatar and keep the existing isGenericName logic unchanged.
lib/api/hackathons.ts (2)

1552-1560: Inconsistent response unwrapping pattern across API functions.

getRewardDistributionStatus and triggerRewardDistribution use res.data?.data ?? res.data to handle both wrapped and unwrapped responses, while every other API function in this file simply returns res.data. This suggests the backend API shape is uncertain. Consider standardizing the response handling (perhaps via a shared utility) or adding a comment explaining why these two endpoints differ.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/api/hackathons.ts` around lines 1552 - 1560, getRewardDistributionStatus
(and its companion triggerRewardDistribution) uses a different
response-unwrapping pattern (res.data?.data ?? res.data) than the rest of the
file which returns res.data; standardize behavior by either (A) changing these
functions to return res.data like others, or (B) centralizing the unwrap logic
into a shared helper (e.g., normalizeApiResponse) and using that helper in both
getRewardDistributionStatus and triggerRewardDistribution; include a brief
comment on why this endpoint may return wrapped data if you keep special
handling.

1514-1521: walletAddresses has a plural name but singular string type.

If a winner can have multiple wallet addresses, this should be string[]. If only one address is expected, consider renaming to walletAddress for clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/api/hackathons.ts` around lines 1514 - 1521, The WinnerSnapshot interface
currently defines walletAddresses as a singular string but is named plural;
update the interface (WinnerSnapshot) so the type matches intent: if a winner
may have multiple addresses change walletAddresses: string to walletAddresses:
string[]; if only one address is allowed, rename the property to walletAddress:
string and update all usages of WinnerSnapshot accordingly (search for
WinnerSnapshot and walletAddresses/walletAddress to update callers).
lib/utils/rewards-data-mapper.ts (1)

10-14: Pervasive as any casts defeat the purpose of typed parameters.

The function signature accepts JudgingSubmission, but line 10 immediately casts it to any, discarding all type information. This means the compiler can't catch field-name typos or structural mismatches in any of the subsequent property accesses (lines 11-77).

If the runtime data doesn't reliably match JudgingSubmission, consider:

  1. Defining a broader union/intersection type that captures the actual shapes you encounter, or
  2. Using a Record<string, unknown> input type with explicit narrowing/guards.

This would preserve some compile-time safety while accommodating the flexible data.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/utils/rewards-data-mapper.ts` around lines 10 - 14, The code casts the
parameter judgingSubmission to any which loses type safety; instead, update the
function signature (the parameter currently named judgingSubmission) to accept a
properly widened type (e.g., a union of the actual shapes you expect or
Record<string, unknown>) and remove the "as any" cast, then add explicit type
guards/narrowing before accessing properties used to derive participant,
submissionData, and userProfile (check participant.user.profile,
participant.submitterProfile, sub.submission, etc.) so the compiler can validate
those accesses; adjust any downstream uses (the variables participant,
submissionData, userProfile) to rely on the narrowed types or optional chaining
rather than casting away types.
hooks/use-reward-distribution-status.ts (1)

33-63: Use unknown instead of any for the caught error type.

catch (err: any) bypasses type checking. Using catch (err: unknown) and narrowing with type guards (err instanceof Error, checking for response property) is safer and aligns with TypeScript best practices.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-reward-distribution-status.ts` around lines 33 - 63, Change the
catch clause from catch (err: any) to catch (err: unknown) and add a
narrow/guard before reading properties: implement a small type guard (or inline
checks) to detect a 404 status (e.g. check typeof err === 'object' && err !==
null and then test (err as any).response?.status or (err as any).status is 404)
and keep the existing branch that calls setDistributionStatus; for the error
branch use err instanceof Error to extract err.message (or coerce safely if not
an Error). Update the catch in the function containing setDistributionStatus and
setError to use these guards so no direct property access on unknown occurs.

Comment on lines 39 to 43
const {
distributionStatus,
isLoading: isLoadingDistributionStatus,
refetch: refetchDistributionStatus,
} = useRewardDistributionStatus(organizationId, hackathonId);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Distribution status error is not destructured or displayed.

The useRewardDistributionStatus hook exposes an error field, but it's not included in the destructuring here. If the distribution status API fails (with a non-404 error), the user won't see any feedback. Consider destructuring error and either displaying it in the UI or logging it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(landing)/organizations/[id]/hackathons/[hackathonId]/rewards/page.tsx
around lines 39 - 43, Destructure the error from useRewardDistributionStatus
(e.g., add error to the existing destructuring alongside distributionStatus,
isLoading: isLoadingDistributionStatus, refetch: refetchDistributionStatus) and
handle it: if error exists and it's not a 404 show or render a user-facing
message (or log it via the page logger) so API failures surface to the user;
keep existing 404 handling unchanged.

Comment on lines 42 to 50
className='markdown-content text-gray-400'
dangerouslySetInnerHTML={{
__html: isExpanded
? announcementContent.content
: announcementContent.content.length > 500
? announcementContent.content.substring(0, 500) + '...'
: announcementContent.content.length > 300
? announcementContent.content.substring(0, 300) + '...'
: announcementContent.content,
}}
/>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find the useMarkdown hook
fd -type f -name "*markdown*" -o -name "*use-markdown*" | head -20

Repository: boundlessfi/boundless

Length of output: 235


🏁 Script executed:

#!/bin/bash
# Search for useMarkdown import/definition
rg -r "useMarkdown|use-markdown" --type=ts --type=tsx -n | head -20

Repository: boundlessfi/boundless

Length of output: 461


🏁 Script executed:

#!/bin/bash
# Check for DOMPurify or sanitization libraries in the codebase
rg -n "DOMPurify|sanitize|dompurify" --type=ts --type=tsx | head -30

Repository: boundlessfi/boundless

Length of output: 92


🏁 Script executed:

#!/bin/bash
# Get the full AnnouncementSection.tsx file to review
cat components/organization/hackathons/rewards/AnnouncementSection.tsx

Repository: boundlessfi/boundless

Length of output: 2093


🏁 Script executed:

#!/bin/bash
# Find the use-markdown hook
find . -path ./node_modules -prune -o -name "*use-markdown*" -type f -print

Repository: boundlessfi/boundless

Length of output: 88


🏁 Script executed:

#!/bin/bash
# Search for useMarkdown definition
rg "useMarkdown" -A 15 | head -50

Repository: boundlessfi/boundless

Length of output: 3903


🏁 Script executed:

#!/bin/bash
# Check for markdown-related hooks
ls -la hooks/ 2>/dev/null | grep -i mark || find . -path ./node_modules -prune -o -path "./.git" -prune -o -name "*.ts" -o -name "*.tsx" | xargs grep -l "useMarkdown" | head -10

Repository: boundlessfi/boundless

Length of output: 131


🏁 Script executed:

#!/bin/bash
# Read the full use-markdown.ts hook
cat hooks/use-markdown.ts

Repository: boundlessfi/boundless

Length of output: 17986


🏁 Script executed:

#!/bin/bash
# Verify if marked library sanitizes by default
# Check if there are any sanitization calls in the codebase for marked output
rg "DOMPurify|sanitize.*marked|marked.*sanitize" -i

Repository: boundlessfi/boundless

Length of output: 430


🏁 Script executed:

#!/bin/bash
# Check package.json for security/sanitization libraries
cat package.json | grep -i "dompurify\|sanitize\|xss"

Repository: boundlessfi/boundless

Length of output: 91


🏁 Script executed:

#!/bin/bash
# Examine the renderHtml utility
cat lib/utils/renderHtml.ts

Repository: boundlessfi/boundless

Length of output: 878


Sanitize markdown output with DOMPurify before rendering via dangerouslySetInnerHTML

The announcement string is user-controlled content that flows through useMarkdown()marked.parse()dangerouslySetInnerHTML without sanitization. The marked library does not sanitize HTML by default, allowing an attacker to inject arbitrary HTML/scripts via markdown payloads. A sanitizeHtml() utility exists in your codebase (lib/utils/renderHtml.ts) but is not being used here.

Additionally, slicing the rendered HTML string at character position 300 (lines 46-47) breaks mid-tag, producing malformed HTML and DOM corruption. Truncate the raw markdown source before parsing instead, or use CSS truncation.

🛡️ Proposed fix
+import { sanitizeHtml } from '@/lib/utils/renderHtml';

  <div
    className='markdown-content text-gray-400'
-   dangerouslySetInnerHTML={{
-     __html: isExpanded
-       ? announcementContent.content
-       : announcementContent.content.length > 300
-         ? announcementContent.content.substring(0, 300) + '...'
-         : announcementContent.content,
-   }}
+   dangerouslySetInnerHTML={sanitizeHtml(
+     isExpanded ? announcement : announcement.substring(0, 300)
+   )}
  />

Also extract the toggle handler to a named function per event handler naming guidelines:

+ const handleToggleExpand = () => setIsExpanded(!isExpanded);
  
  <button
-   onClick={() => setIsExpanded(!isExpanded)}
+   onClick={handleToggleExpand}
    className='text-primary mt-1 text-xs hover:underline'
  >
🧰 Tools
🪛 ast-grep (0.40.5)

[warning] 42-42: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/rewards/AnnouncementSection.tsx` around
lines 42 - 50, The rendered announcement HTML is not sanitized and is being
truncated after parsing which can break tags; update AnnouncementSection (the
element using dangerouslySetInnerHTML with announcementContent.content and
isExpanded) to first truncate the raw markdown source (announcementContent.raw
or announcementContent.markdown) before calling useMarkdown/marked, then pass
the parsed HTML through the existing sanitizeHtml() from lib/utils/renderHtml.ts
(or DOMPurify) and use that sanitized string in dangerouslySetInnerHTML; avoid
slicing the parsed HTML (remove the substring usage) or replace truncation with
CSS-based truncation, and extract the inline toggle handler into a named
function (e.g., handleToggleExpand) for the expand/collapse control.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
components/organization/hackathons/new/tabs/ParticipantTab.tsx (3)

443-455: ⚠️ Potential issue | 🟠 Major

teamMax NumberInput is not disabled when isRegistrationClosed is true.

teamMin received disabled={isRegistrationClosed} (line 415) but teamMax was not updated. When registration is closed, users can still increment/decrement the maximum team size.

🐛 Proposed fix
 <NumberInput
   value={field.value || 5}
+  disabled={isRegistrationClosed}
   onIncrement={() => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/new/tabs/ParticipantTab.tsx` around lines
443 - 455, The teamMax NumberInput is missing the disabled handling when
isRegistrationClosed is true; update the NumberInput for teamMax (the component
using field for teamMax) to include disabled={isRegistrationClosed} and guard
its onIncrement/onDecrement handlers so they no-op when isRegistrationClosed is
true (or early return) to prevent changing field.value; mirror how teamMin is
handled (referencing teamMin, teamMax, field, form and isRegistrationClosed in
the existing code).

351-391: ⚠️ Potential issue | 🟠 Major

Custom deadline calendar remains interactive when isRegistrationClosed is true.

The PopoverTrigger Button has no disabled prop and onSelect has no isRegistrationClosed guard. If registrationDeadlinePolicy is already 'custom' when registration closes, users can still open the calendar and pick a new date. The PR objective explicitly calls for disabling this control.

🐛 Proposed fix
 <Button
   variant='outline'
+  disabled={isRegistrationClosed}
   className={cn(
     'h-12 w-full rounded-lg border border-zinc-800 bg-zinc-900/30 p-4 text-left font-normal hover:bg-zinc-900/50',
-    !field.value && 'text-zinc-500'
+    !field.value && 'text-zinc-500',
+    isRegistrationClosed && 'cursor-not-allowed opacity-60'
   )}
 >

And guard the onSelect callback:

-  onSelect={date => {
-    if (date) {
-      field.onChange(date.toISOString());
-    }
-  }}
+  onSelect={date => {
+    if (date && !isRegistrationClosed) {
+      field.onChange(date.toISOString());
+    }
+  }}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/new/tabs/ParticipantTab.tsx` around lines
351 - 391, The custom deadline calendar remains interactive when registration is
closed; in ParticipantTab.tsx disable the PopoverTrigger Button (the Button
wrapped by PopoverTrigger) when isRegistrationClosed is true and also guard the
Calendar onSelect handler (and/or return early) so it does not call
field.onChange if isRegistrationClosed is true; additionally ensure the
Calendar's disabled predicate accounts for isRegistrationClosed (e.g., disable
all dates or prevent selection) so the UI and selection logic are both inert
when registrationDeadlinePolicy === 'custom' and isRegistrationClosed is true.

535-538: ⚠️ Potential issue | 🟡 Minor

Tab Visibility switches are not disabled when isRegistrationClosed is true.

All other interactive controls (participant type, deadline policy, submission requirement switches, number inputs) are gated on isRegistrationClosed, but the Tab Visibility Switch components are not. If this omission is intentional, a brief comment explaining why would help.

🐛 Proposed fix (if the omission is unintentional)
 <Switch
   checked={Boolean(field.value ?? true)}
   onCheckedChange={field.onChange}
+  disabled={isRegistrationClosed}
 />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/new/tabs/ParticipantTab.tsx` around lines
535 - 538, The Tab Visibility Switches (the Switch using
checked={Boolean(field.value ?? true)} and onCheckedChange={field.onChange}) are
not disabled when isRegistrationClosed is true; update the Switch to include
disabled={isRegistrationClosed} so it is non-interactive once registration is
closed (or add a brief inline comment explaining intentional omission if you
want it to remain enabled). Ensure you apply the same change to all Tab
Visibility Switch instances in ParticipantTab.tsx.
components/organization/hackathons/settings/GeneralSettingsTab.tsx (1)

50-66: ⚠️ Potential issue | 🟡 Minor

Remove unused organizationId / hackathonId props and fix the function declaration.

Two issues here:

  1. organizationId and hackathonId are declared in GeneralSettingsTabProps and destructured in the component signature, but they are never referenced in the component body. The comment on line 151 ("Pass organizationId and hackathonId to onSave") is also misleading — onSave(data) does not forward them. These are dead props that inflate the public API surface unnecessarily.

  2. export default function GeneralSettingsTab(...) is a function declaration. As per coding guidelines, const arrow functions with explicit type annotations are preferred.

♻️ Proposed fix
 interface GeneralSettingsTabProps {
-  organizationId: string;
-  hackathonId: string;
   initialData?: Partial<InfoFormData>;
   onSave?: (data: InfoFormData) => Promise<void>;
   isLoading?: boolean;
   isPublished?: boolean;
 }

-export default function GeneralSettingsTab({
-  organizationId,
-  hackathonId,
+const GeneralSettingsTab = ({
   initialData,
   onSave,
   isLoading = false,
   isPublished = false,
-}: GeneralSettingsTabProps) {
+}: GeneralSettingsTabProps): JSX.Element => {
   ...
   const onSubmit = async (data: InfoFormData) => {
     if (onSave) {
-      // Pass organizationId and hackathonId to onSave for API calls
       await onSave(data);
     }
   };
-}
+};
+export default GeneralSettingsTab;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/settings/GeneralSettingsTab.tsx` around
lines 50 - 66, The component exposes unused props organizationId and hackathonId
and is declared as a function instead of a const arrow; remove organizationId
and hackathonId from GeneralSettingsTabProps and from the component's parameter
destructuring, convert the component declaration to a const arrow with an
explicit React.FC/props type (e.g. const GeneralSettingsTab:
React.FC<GeneralSettingsTabProps> = ({ initialData, onSave, isLoading = false,
isPublished = false }) => { ... }), and keep the onSave invocation as
onSave(data) (do not attempt to forward organizationId/hackathonId); also remove
or update the misleading comment that says "Pass organizationId and hackathonId
to onSave".
🧹 Nitpick comments (30)
components/organization/hackathons/rewards/AnnouncementSection.tsx (1)

14-17: Prefer const arrow function with explicit type annotation.

Per the coding guidelines for *.tsx files, prefer const arrow functions with explicit type annotations over function declarations.

♻️ Suggested refactor
-export default function AnnouncementSection({
-  announcement,
-  onEdit,
-}: AnnouncementSectionProps) {
+const AnnouncementSection: React.FC<AnnouncementSectionProps> = ({
+  announcement,
+  onEdit,
+}) => {
   const [isExpanded, setIsExpanded] = useState(false);
   // ... rest of component
-}
+};
+
+export default AnnouncementSection;

As per coding guidelines, "Prefer const arrow functions with explicit type annotations over function declarations".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/rewards/AnnouncementSection.tsx` around
lines 14 - 17, Convert the function declaration AnnouncementSection to a const
arrow component with an explicit type annotation using AnnouncementSectionProps
(e.g., declare const AnnouncementSection: React.FC<AnnouncementSectionProps> =
({ announcement, onEdit }) => { ... }) and then export default
AnnouncementSection; keep the same prop names (announcement, onEdit) and
existing implementation inside the arrow function body.
components/organization/hackathons/settings/AdvancedSettingsTab.tsx (1)

84-88: initialData reference equality may cause unintended form resets.

If the parent component re-creates the initialData object on each render (e.g. inline literal or un-memoized transformation), this effect fires every render and calls form.reset(...), silently discarding the user's unsaved edits.

Consider a shallow-compare guard or serialize to a stable key:

Option: serialize to a stable dependency
+ const initialDataKey = JSON.stringify(initialData);
+
  useEffect(() => {
    if (initialData) {
      form.reset(initialData);
    }
- }, [initialData, form]);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [initialDataKey]);

Alternatively, ensure the parent memoizes initialData (via useMemo).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/settings/AdvancedSettingsTab.tsx` around
lines 84 - 88, The effect unconditionally calls form.reset whenever the
reference to initialData changes, which can discard user edits if the parent
recreates the object; update the useEffect to guard against spurious resets by
comparing the new initialData to a saved previous value (e.g., keep
prevInitialData in a useRef and compare via a shallow/equality check or stable
serialization like JSON.stringify) and only call form.reset(initialData) when
the comparison indicates the data actually changed; keep the symbols useEffect,
initialData, form.reset and prevInitialData (useRef) to locate and implement the
change.
components/organization/hackathons/new/tabs/RewardsTab.tsx (1)

430-435: Avoid error: any in catch — use unknown with type narrowing.

catch (error: any) silences TypeScript's type checker and allows unchecked property access on an arbitrary thrown value. If the thrown value is a string, null, or a non-Axios error, the access to error.response etc. is silent undefined chaining that happens to work, but the safety guarantee is gone.

♻️ Suggested refactor using `unknown` with explicit narrowing
-  } catch (error: any) {
-    const message = error.response?.data?.message || error.message;
-    const errorMessage = Array.isArray(message) ? message[0] : message;
-    toast.error(
-      errorMessage || 'Failed to save rewards settings. Please try again.'
-    );
+  } catch (error: unknown) {
+    const axiosLike = error as {
+      response?: { data?: { message?: string | string[] } };
+      message?: string;
+    };
+    const raw = axiosLike.response?.data?.message ?? axiosLike.message;
+    const errorMessage = Array.isArray(raw) ? raw[0] : raw;
+    toast.error(
+      errorMessage || 'Failed to save rewards settings. Please try again.'
+    );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/new/tabs/RewardsTab.tsx` around lines 430
- 435, The catch block in RewardsTab (currently written as catch (error: any))
disables TypeScript safety; change it to catch (error: unknown) and narrow the
type before accessing properties: use an Axios/type guard (e.g.,
import/isAxiosError or check typeof and presence of response) or explicit type
assertions after runtime checks to extract message (for example, if
(isAxiosError(error) && error.response?.data?.message) { … } else if (typeof
error === 'string') { … } else if (error && typeof (error as any).message ===
'string') { … }), then compute errorMessage and call toast.error(errorMessage ||
'Failed to save rewards settings. Please try again.'). Ensure you update the
catch block around the save/rewards API call in RewardsTab to use these narrowed
checks instead of any.
components/organization/hackathons/settings/TimelineSettingsTab.tsx (4)

141-141: Prefer unknown over any for the caught error.

Using unknown forces explicit narrowing and is the recommended TypeScript practice for catch clauses.

♻️ Suggested change
-    } catch (error: any) {
-      const message = error.response?.data?.message || error.message;
+    } catch (error: unknown) {
+      const err = error as Record<string, any>;
+      const message = err?.response?.data?.message || err?.message;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/settings/TimelineSettingsTab.tsx` at line
141, Change the catch parameter from "error: any" to "error: unknown" in the
try/catch inside TimelineSettingsTab (the catch block shown), and update any
usage of that caught value (e.g., logging or reading error.message) by narrowing
it first — for example check "if (error instanceof Error)" before accessing
error.message or otherwise convert safely (String(error)) so the code compiles
with the unknown type.

608-618: Consider disabling the button when the form has no changes.

Currently the button is only disabled while saving. You could also gate on form.formState.isDirty to prevent no-op API calls:

-              disabled={isSaving}
+              disabled={isSaving || !form.formState.isDirty}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/settings/TimelineSettingsTab.tsx` around
lines 608 - 618, The save button currently only uses isSaving; update the
BoundlessButton in TimelineSettingsTab so it is also disabled when the form has
no changes by combining isSaving with the form's dirty state (e.g., use
form.formState.isDirty). Locate the BoundlessButton (type='submit') and change
its disabled prop to disable when saving OR when the form is not dirty so no-op
submissions are prevented.

166-397: Extract a reusable DateTimePickerField to eliminate ~300 lines of duplication.

The date-picker + time-input block is repeated six times with only the field name, label, and placeholder varying. A small helper component would collapse each instance to a few lines and make future styling changes a single-point edit.

♻️ Sketch of extracted component
// Inline or in a shared module
const DateTimePickerField = ({
  control,
  name,
  label,
  placeholder,
  required,
  formatTimeValue,
  applyTimeToDate,
}: {
  control: Control<TimelineFormData>;
  name: keyof TimelineFormData;
  label: string;
  placeholder: string;
  required?: boolean;
  formatTimeValue: (date?: Date) => string;
  applyTimeToDate: (date: Date, time: string) => Date;
}) => (
  <FormField
    control={control}
    name={name}
    render={({ field }) => (
      <FormItem className='gap-3'>
        <FormLabel className='text-sm'>
          {label} {required && <span className='text-error-400'>*</span>}
        </FormLabel>
        <Popover>
          <PopoverTrigger asChild>
            <FormControl>
              <Button
                variant='outline'
                className={cn(
                  'bg-background-card h-12 w-full rounded-[12px] border border-gray-900 p-4 text-left font-normal',
                  !field.value && 'text-gray-600'
                )}
              >
                {field.value ? format(field.value as Date, 'PPP') : <span>{placeholder}</span>}
                <CalendarIcon className='ml-auto h-4 w-4 text-gray-400' />
              </Button>
            </FormControl>
          </PopoverTrigger>
          <PopoverContent className='bg-background-card w-auto border-gray-900 p-0 text-white' align='start'>
            <Calendar mode='single' selected={field.value as Date} onSelect={field.onChange} initialFocus />
          </PopoverContent>
        </Popover>
        <FormControl>
          <Input
            type='time'
            className='bg-background-card h-12 w-full rounded-[12px] border border-gray-900 px-4 text-sm text-white'
            value={formatTimeValue(field.value as Date)}
            onChange={e => {
              if (!field.value) return;
              field.onChange(applyTimeToDate(field.value as Date, e.target.value));
            }}
            disabled={!field.value}
          />
        </FormControl>
        <FormMessage className='text-error-400 text-xs' />
      </FormItem>
    )}
  />
);

Then usage becomes:

<DateTimePickerField
  control={form.control}
  name='startDate'
  label='Start Date'
  placeholder='Select start date'
  required
  formatTimeValue={formatTimeValue}
  applyTimeToDate={applyTimeToDate}
/>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/settings/TimelineSettingsTab.tsx` around
lines 166 - 397, Extract the repeated date-picker + time-input block into a
reusable DateTimePickerField component and replace each duplicated FormField
(e.g., startDate, submissionDeadline, judgingStart, endDate) with it;
DateTimePickerField should accept props: control (pass form.control), name
(keyof TimelineFormData), label, placeholder, required (boolean), and the helper
functions formatTimeValue and applyTimeToDate, render the same UI using
FormField/FormItem/Popover/Calendar/Input, and call field.onChange with
applyTimeToDate on time change; ensure proper typing for the name prop, forward
the FormMessage, and import/export the component or define it inline above the
TimelineSettingsTab so styling and behavior remain identical.

51-56: Coding guideline: prefer const arrow function with explicit return type.

The component uses a function declaration. Per project conventions, prefer a const arrow function with an explicit type annotation.

♻️ Suggested refactor
-export default function TimelineSettingsTab({
+const TimelineSettingsTab = ({
   organizationId,
   hackathonId,
   initialData,
   onSaveSuccess,
-}: TimelineSettingsTabProps) {
+}: TimelineSettingsTabProps): React.JSX.Element => {

And at the bottom of the file:

export default TimelineSettingsTab;

As per coding guidelines, **/*.{ts,tsx}: "Prefer const arrow functions with explicit type annotations over function declarations".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/settings/TimelineSettingsTab.tsx` around
lines 51 - 56, Replace the function declaration for TimelineSettingsTab with a
const arrow component using an explicit type annotation: change "export default
function TimelineSettingsTab({ organizationId, hackathonId, initialData,
onSaveSuccess, }: TimelineSettingsTabProps)" to "const TimelineSettingsTab:
React.FC<TimelineSettingsTabProps> = ({ organizationId, hackathonId,
initialData, onSaveSuccess }) => { ... }" and keep "export default
TimelineSettingsTab;" at the bottom; ensure React is imported for the React.FC
type if not already.
hooks/use-hackathon-rewards.ts (3)

263-269: Pervasive any casts throughout the enrichment pipeline.

Lines 264, 276–277, 286, 290–291, 349, 388 all use any to bypass type checking. This makes the enrichment logic fragile — property renames or shape changes in the API won't surface at compile time. Consider defining lightweight interfaces (e.g., RawSubmission, RawParticipant) for the API response shapes to catch mismatches early.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-hackathon-rewards.ts` around lines 263 - 269, Replace the pervasive
any casts by defining lightweight TS interfaces (e.g., RawSubmission,
RawParticipant) that reflect the API shapes, then change the parsing and
enrichment code to use those types instead of any: type rawData as RawSubmission
| { submissions: RawSubmission[] }, make submissionsArray a RawSubmission[] (use
a type guard to detect the paginated shape), and update the enrichment helpers
(the functions that consume submissionsArray and participant fields) to accept
RawSubmission/RawParticipant so property accesses are checked by the compiler;
this removes the "any" casts at variables rawData and submissionsArray and in
the downstream enrichment pipeline.

15-18: JudgingResult type is imported but not referenced in the code.

The resultsList on line 384 is not annotated with JudgingResult[]. Either apply the type or remove the unused import.

Option A: Apply the type
-             const resultsList = resultsRes.data.results || [];
+             const resultsList: JudgingResult[] = resultsRes.data.results || [];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-hackathon-rewards.ts` around lines 15 - 18, The imported type
JudgingResult is unused; either remove the import or annotate the resultsList
variable with JudgingResult[] to use it: locate resultsList in
hooks/use-hackathon-rewards.ts and change its declaration to explicitly type it
as JudgingResult[] (or delete the JudgingResult import if you prefer no explicit
typing).

226-228: Silent catch discards the failure reason — consider logging.

When getHackathon throws, the catch block falls back to default prize tiers without logging the error or surfacing it to the user. This makes debugging production issues harder and may cause the UI to silently display placeholder data (all $0 prizes) without any indication that the real data failed to load.

Suggested improvement
-    } catch {
-      setPrizeTiers(getDefaultPrizeTiers());
-    }
+    } catch (err) {
+      console.error('Failed to fetch hackathon:', err);
+      setPrizeTiers(getDefaultPrizeTiers());
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-hackathon-rewards.ts` around lines 226 - 228, The catch block
swallowing errors when calling getHackathon should log and optionally surface
the error; update the catch in the async flow that calls getHackathon to capture
the caught error (e.g., catch (err)) and call your logging mechanism
(console.error or processLogger) with a clear message and the error, still call
setPrizeTiers(getDefaultPrizeTiers()) as fallback, and consider setting an error
state or invoking a provided onError callback so the UI can indicate the failure
instead of silently showing defaults; references: getHackathon, setPrizeTiers,
getDefaultPrizeTiers.
components/organization/hackathons/new/tabs/ParticipantTab.tsx (2)

157-204: NumberInput (and NumberInputProps) should be defined outside ParticipantTab.

Defining a component inside another component creates a new function reference on every render, causing React to unmount and remount the NumberInput subtree each time ParticipantTab re-renders (e.g., on form.watch updates). This can also unexpectedly lose focus.

♻️ Proposed refactor — hoist to module scope
+interface NumberInputProps {
+  value: number;
+  onIncrement: () => void;
+  onDecrement: () => void;
+  disabled?: boolean;
+}
+
+const NumberInput = ({
+  value,
+  onIncrement,
+  onDecrement,
+  disabled = false,
+}: NumberInputProps) => (
+  // ... same JSX body
+);
+
 export default function ParticipantTab({ ... }: ParticipantTabProps) {
-  // Remove the NumberInputProps interface and NumberInput definition from here
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/new/tabs/ParticipantTab.tsx` around lines
157 - 204, The NumberInput component and NumberInputProps type are currently
declared inside ParticipantTab which recreates their functions on every render;
move the NumberInputProps interface and the NumberInput functional component out
of ParticipantTab to module scope (top-level of the file) so they are stable
across renders, then import or reference them from ParticipantTab (update any
props usage like value, onIncrement, onDecrement, disabled) to prevent
unnecessary unmounts/remounts and lost focus during form.watch updates.

148-153: Prefer unknown over any for the caught error.

error: any bypasses type-safety. Using unknown and a helper narrows the type correctly.

♻️ Proposed refactor
-  } catch (error: any) {
-    const message = error.response?.data?.message || error.message;
-    const errorMessage = Array.isArray(message) ? message[0] : message;
+  } catch (error: unknown) {
+    const err = error as { response?: { data?: { message?: string | string[] } }; message?: string };
+    const message = err.response?.data?.message ?? err.message;
+    const errorMessage = Array.isArray(message) ? message[0] : message;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/new/tabs/ParticipantTab.tsx` around lines
148 - 153, Change the catch clause in ParticipantTab.tsx to use error: unknown
instead of any and add a small type-narrowing helper to extract the message
safely before calling toast.error; specifically update the catch(error: unknown)
in the save handler (the block that computes message/errorMessage and calls
toast.error) to use a function (e.g., extractErrorMessage) that checks for
Axios-like shape (error.response?.data?.message), string, array, or Error and
returns a string fallback, then pass that returned string into toast.error; keep
the existing fallback text unchanged.
components/organization/hackathons/rewards/WinnersGrid.tsx (2)

41-52: getTierOrder is recomputed on every render — consider memoizing or inlining.

getTierOrder is called with tiersWithWinners (which is memoized), but the function itself is recreated each render. Since it's called once to produce tiersToDisplay, consider wrapping the result in useMemo:

Suggested change
-  const getTierOrder = (availableTiers: typeof prizeTiers) => {
-    ...
-  };
-
-  const tiersToDisplay = getTierOrder(tiersWithWinners);
+  const tiersToDisplay = useMemo(() => {
+    const sortedTiers = [...tiersWithWinners].sort((a, b) => a.rank - b.rank);
+    if (sortedTiers.length === 3) {
+      const secondTier = sortedTiers.find(t => t.rank === 2) || sortedTiers[1];
+      const firstTier = sortedTiers.find(t => t.rank === 1) || sortedTiers[0];
+      const thirdTier = sortedTiers.find(t => t.rank === 3) || sortedTiers[2];
+      return [secondTier, firstTier, thirdTier].filter(Boolean);
+    }
+    return sortedTiers;
+  }, [tiersWithWinners]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/rewards/WinnersGrid.tsx` around lines 41 -
52, getTierOrder is recreated each render even though it's only used once to
compute tiersToDisplay from the memoized tiersWithWinners; wrap the computation
in a useMemo so getTierOrder's work is memoized instead of redefining the
function. Replace the inline getTierOrder definition + immediate call with a
useMemo that depends on tiersWithWinners and returns the same logic (sorting and
special 3-tier reorder) so tiersToDisplay is derived via useMemo; reference
getTierOrder, tiersWithWinners, and tiersToDisplay when updating the component.

22-26: Use a const arrow function per coding guidelines.

Same pattern as the other files — prefer const arrow with explicit typing over function declaration.

As per coding guidelines, "Prefer const arrow functions with explicit type annotations over function declarations."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/rewards/WinnersGrid.tsx` around lines 22 -
26, Replace the function declaration for WinnersGrid with a const arrow function
using an explicit React/TSX type annotation: change "export default function
WinnersGrid({ prizeTiers, winners, getPrizeForRank, }: WinnersGridProps)" to a
const arrow named WinnersGrid typed with the appropriate Props
(WinnersGridProps) and export default that const; ensure all referenced symbols
(WinnersGridProps, prizeTiers, winners, getPrizeForRank) remain unchanged and
that the component signature and return type align with the project's React/TSX
typing conventions.
components/organization/hackathons/rewards/PublishWinnersWizard.tsx (3)

99-109: getPrizeForRank is not memoized and recreated on every render.

This function is passed as a prop to PreviewStepWinnersGrid, causing unnecessary re-renders of memoized children. Wrap it with useCallback since its only dependency (mappedPrizeTiers) is already memoized.

Suggested change
-  const getPrizeForRank = (rank: number) => {
+  const getPrizeForRank = useCallback((rank: number) => {
     const tier = mappedPrizeTiers.find(t => t.rank === rank);
     if (tier) {
       const amount = parseFloat(tier.prizeAmount || '0').toLocaleString('en-US');
       const currency = tier.currency || 'USDC';
       return { amount, currency, label: `${amount} ${currency}` };
     }
     return { label: 'No prize configured' };
-  };
+  }, [mappedPrizeTiers]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/rewards/PublishWinnersWizard.tsx` around
lines 99 - 109, The getPrizeForRank function is recreated on every render which
forces re-renders downstream; wrap getPrizeForRank in React.useCallback so it is
memoized and only changes when mappedPrizeTiers changes (since mappedPrizeTiers
is already memoized). Locate the getPrizeForRank declaration and replace it with
a useCallback-wrapped version that has [mappedPrizeTiers] as its dependency
array so PreviewStep → WinnersGrid receives a stable prop reference.

87-87: canGoNext is hardcoded to true — is this intentional?

This is passed to WizardFooter but never varies. If it's a placeholder for future validation logic (e.g., requiring a non-empty announcement), consider adding a TODO. Otherwise, remove it and pass true directly to simplify.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/rewards/PublishWinnersWizard.tsx` at line
87, canGoNext is hardcoded to true but never changes; either make it dynamic for
future validation or simplify by removing the variable and passing true directly
to WizardFooter. Update the PublishWinnersWizard component to either (a)
implement validation logic that sets the canGoNext state/constant (e.g., based
on announcement text length or selected winners) and use that variable where
WizardFooter is called, or (b) delete the unused canGoNext binding and pass true
inline into the WizardFooter prop, and if keeping a placeholder add a TODO
comment near canGoNext explaining intended validation. Ensure you modify the
symbol canGoNext and the WizardFooter prop usage in PublishWinnersWizard to
reflect the chosen approach.

33-42: Use a const arrow function with explicit type annotations.

The coding guidelines prefer const arrow functions over function declarations for components.

Suggested change
-export default function PublishWinnersWizard({
+const PublishWinnersWizard: React.FC<PublishWinnersWizardProps> = ({
   open,
   onOpenChange,
   ...
-}: PublishWinnersWizardProps) {
+}) => {

Then at the bottom of the file:

export default PublishWinnersWizard;

As per coding guidelines, "Prefer const arrow functions with explicit type annotations over function declarations."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/rewards/PublishWinnersWizard.tsx` around
lines 33 - 42, Convert the component declaration from a function declaration to
a const arrow function with an explicit React.FC (or appropriate) type
annotation using the existing PublishWinnersWizardProps, e.g. const
PublishWinnersWizard: React.FC<PublishWinnersWizardProps> = ({ open,
onOpenChange, submissions, prizeTiers, escrow, organizationId, hackathonId,
onSuccess }) => { ... }; then remove the current default export on the
declaration and add export default PublishWinnersWizard at the bottom of the
file so the component follows the project's const-arrow + explicit-type
guideline.
components/organization/hackathons/rewards/WinnersPreviewPage.tsx (1)

23-30: Use a const arrow function per coding guidelines.

As per coding guidelines, "Prefer const arrow functions with explicit type annotations over function declarations."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/rewards/WinnersPreviewPage.tsx` around
lines 23 - 30, The component is declared using a function declaration; change it
to a const arrow function with an explicit type annotation: replace the export
default function WinnersPreviewPage(...) { ... } declaration with a const
WinnersPreviewPage: React.FC<WinnersPreviewPageProps> = ({ submissions,
announcement, prizeTiers, onBack, onEdit, onPublish }) => { ... } and then
export default WinnersPreviewPage; ensure the prop type remains
WinnersPreviewPageProps and no other implementation details are changed.
components/organization/hackathons/rewards/WinnerCard.tsx (1)

27-34: Use a const arrow function per coding guidelines.

As per coding guidelines, "Prefer const arrow functions with explicit type annotations over function declarations."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/rewards/WinnerCard.tsx` around lines 27 -
34, Replace the current function declaration for WinnerCard with a const arrow
function and add an explicit type annotation using the WinnerCardProps (e.g.,
export default const WinnerCard: React.FC<WinnerCardProps> = (...) => { ... });
update the parameter destructuring (rank, winner, prizeAmount, currency,
prizeLabel, maxRank) to live in the arrow function signature and keep existing
implementation unchanged inside the body; ensure the export remains default and
the component name WinnerCard and prop type WinnerCardProps are used to locate
the change.
app/(landing)/organizations/[id]/hackathons/[hackathonId]/rewards/page.tsx (1)

101-107: Distribution error alert gates on !isLoading but not !isLoadingDistributionStatus.

If the main rewards data loads first while the distribution status fetch is still in-flight and then fails, the error alert could flash briefly before distribution loading settles. Consider also gating on !isLoadingDistributionStatus for a more stable UX:

-        {!isLoading && distributionError && (
+        {!isLoading && !isLoadingDistributionStatus && distributionError && (
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(landing)/organizations/[id]/hackathons/[hackathonId]/rewards/page.tsx
around lines 101 - 107, The Distribution Status error Alert currently renders
when !isLoading && distributionError; change the conditional to also require
that distribution status fetch is finished by adding
!isLoadingDistributionStatus (i.e. render only when !isLoading &&
!isLoadingDistributionStatus && distributionError) so the Alert component
(Alert/AlertTitle/AlertDescription) doesn't briefly flash while the distribution
status is still in-flight; update the JSX where isLoading, distributionError and
isLoadingDistributionStatus are referenced to reflect this combined gating.
hooks/use-reward-distribution-status.ts (1)

37-68: Consider extracting the NOT_TRIGGERED placeholder into a named constant.

The inline 20-line object literal makes fetchStatus harder to scan. A top-level const NOT_TRIGGERED_DEFAULT: RewardDistributionStatusResponse = { … } would also make it reusable if other consumers need the same sentinel.

♻️ Suggested refactor
+const NOT_TRIGGERED_DEFAULT: RewardDistributionStatusResponse = {
+  distributionId: null,
+  status: 'NOT_TRIGGERED',
+  snapshot: {
+    idempotencyKey: '',
+    winners: [],
+    totalPrizePool: 0,
+    platformFee: 0,
+    totalRequired: 0,
+    currency: 'USDC',
+    escrowAddress: '',
+    winnersChecksum: '',
+    snapshotAt: '',
+    organizerNote: null,
+  },
+  triggeredAt: '',
+  adminDecisionAt: null,
+  adminNote: null,
+  adminUserId: null,
+  rejectionReason: null,
+  updatedAt: '',
+};
+
 export const useRewardDistributionStatus = (
   ...
       if (err?.response?.status === 404 || err?.status === 404) {
-        setDistributionStatus({
-          distributionId: null,
-          status: 'NOT_TRIGGERED',
-          ...
-        });
+        setDistributionStatus(NOT_TRIGGERED_DEFAULT);
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-reward-distribution-status.ts` around lines 37 - 68, Extract the
large inline "NOT_TRIGGERED" placeholder object in the catch of fetchStatus into
a top-level constant (e.g., const NOT_TRIGGERED_DEFAULT:
RewardDistributionStatusResponse) and replace the object literal passed to
setDistributionStatus(...) with that constant; ensure the constant is
exported/typed as RewardDistributionStatusResponse and referenced where the
current object is used (look for setDistributionStatus call inside the catch in
use-reward-distribution-status.ts and fetchStatus) so other consumers can reuse
the sentinel.
components/organization/hackathons/rewards/RewardDistributionStatusBanner.tsx (1)

148-158: Prefer clsx (or cn) for conditional class composition.

Lines 150 and 158 use template literal ternaries for class assembly. Per project coding guidelines, conditional classes should use clsx or a similar helper.

♻️ Example refactor for line 150
+import { cn } from '@/lib/utils'; // or import clsx from 'clsx'

 <RefreshCw
-  className={`h-4 w-4 ${isLoading ? 'animate-spin' : ''}`}
+  className={cn('h-4 w-4', isLoading && 'animate-spin')}
 />

As per coding guidelines, "For conditional classes, prefer clsx or similar helper functions over ternary operators in JSX".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@components/organization/hackathons/rewards/RewardDistributionStatusBanner.tsx`
around lines 148 - 158, The JSX uses template literal ternaries for conditional
classes on the RefreshCw component and the surrounding div (see the className on
<RefreshCw ... isLoading> and the div using cfg.bgClass and cfg.borderClass);
replace these string template expressions with the project's class helper (e.g.,
clsx or cn) to build classes—import the helper if missing, then change
className={`h-4 w-4 ${isLoading ? 'animate-spin' : ''}`} to clsx('h-4 w-4', {
'animate-spin': isLoading }) and change className={`rounded-xl border p-5
${cfg.bgClass} ${cfg.borderClass}`} to clsx('rounded-xl border p-5',
cfg.bgClass, cfg.borderClass`) so conditional logic uses the helper
consistently.
lib/api/hackathons.ts (1)

1552-1560: Dual unwrapping pattern res.data?.data ?? res.data is fragile.

Both getRewardDistributionStatus and triggerRewardDistribution use res.data?.data ?? res.data to handle two potential response shapes. Other API functions in this file (e.g., getHackathonEscrow at line 1590, assignRanks at line 1499) return res.data directly. This inconsistency suggests the backend may wrap these specific endpoints differently, but it makes the contract ambiguous — if the backend changes to consistently wrap responses, the ?? res.data fallback could return the wrapper object instead of the payload.

Consider aligning these with the rest of the file (using res.data directly) if the API response shape is stable, or add a comment explaining why the dual unwrapping is needed.

Also applies to: 1571-1581

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/api/hackathons.ts` around lines 1552 - 1560, The dual-unwrapping pattern
in getRewardDistributionStatus (and triggerRewardDistribution) using
res.data?.data ?? res.data is inconsistent with other functions like
getHackathonEscrow and assignRanks and makes the contract ambiguous; either
simplify to return res.data directly to match the rest of the file (remove the
?.data ?? fallback) or, if the backend truly returns two different shapes, keep
the current logic but add a concise comment above getRewardDistributionStatus
and triggerRewardDistribution explaining why the extra unwrap is necessary and
documenting the exact expected shapes so future maintainers know why these
endpoints differ.
components/landing-page/Hero2.tsx (1)

309-321: Hardcoded daysUntilStart / daysUntilEnd will drift over time.

hackathon-2 uses hardcoded day counters (100 / 130) with a June 2026 event window. These are demo-only values displayed on the landing page, so it's not critical, but they'll become stale in the same way the previous review flagged for hackathon-1. Consider computing them from the dates if the card renders them visually, or leave a comment marking them as approximations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/landing-page/Hero2.tsx` around lines 309 - 321, Hardcoded
daysUntilStart/daysUntilEnd in the hackathon-2 data will become stale; compute
them from the startDate/endDate instead (or add a comment marking them as
approximations). Replace the literal 100/130 with calculated values derived from
the startDate and endDate fields (e.g., compute daysUntilStart = Math.ceil((new
Date(startDate).getTime() - Date.now())/MS_PER_DAY) and similarly for
daysUntilEnd), keep the values as numbers, and update any rendering in the Hero2
component that reads daysUntilStart/daysUntilEnd to use these computed values
(or add a clear comment next to the daysUntilStart/daysUntilEnd declarations if
you intentionally keep demo approximations).
components/organization/hackathons/settings/GeneralSettingsTab.tsx (2)

212-213: readOnly is redundant alongside disabled on the slug input.

disabled={isPublished} already prevents all interaction (focus, keyboard, click). readOnly={isPublished} adds no additional protection and is noise. Keep disabled only, or readOnly only if you want the value to remain in form submission (note: disabled fields are excluded from native form submissions, but since the value is controlled via react-hook-form, this distinction doesn't matter here).

♻️ Proposed fix
-                    readOnly={isPublished}
                     disabled={isPublished}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/settings/GeneralSettingsTab.tsx` around
lines 212 - 213, Remove the redundant readOnly prop from the slug input in
GeneralSettingsTab and keep only disabled={isPublished}; specifically, locate
the slug input element (the JSX that currently sets readOnly={isPublished} and
disabled={isPublished}) and delete the readOnly attribute so the input is
controlled solely via disabled using the existing isPublished flag.

149-153: Rename onSubmit to handleSubmit to comply with the event-handler naming convention.

As per coding guidelines, event handlers should start with the handle prefix. onSubmit is used directly as an event-handler callback via form.handleSubmit(onSubmit).

♻️ Proposed fix
-  const onSubmit = async (data: InfoFormData) => {
+  const handleSubmit = async (data: InfoFormData) => {
     if (onSave) {
       await onSave(data);
     }
   };

   return (
     ...
-      <form onSubmit={form.handleSubmit(onSubmit)} className='space-y-6'>
+      <form onSubmit={form.handleSubmit(handleSubmit)} className='space-y-6'>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/settings/GeneralSettingsTab.tsx` around
lines 149 - 153, Rename the onSubmit function to handleSubmit throughout the
component: change the declaration const onSubmit = async (data: InfoFormData) =>
{ ... } to const handleSubmit = async (data: InfoFormData) => { ... }, and
update all usages such as form.handleSubmit(onSubmit) ->
form.handleSubmit(handleSubmit) and any other references (e.g., tests or prop
callbacks) while preserving the existing logic that awaits onSave(data). Ensure
the function signature (InfoFormData parameter) and interaction with onSave
remain unchanged.
app/(landing)/organizations/[id]/hackathons/[hackathonId]/settings/page.tsx (4)

289-294: Redundant null guard — getParticipantData already handles null.

getParticipantData returns undefined when called with null (line 102), so the ternary on line 290 and the one on line 293 are unnecessary.

♻️ Proposed simplification
-                initialData={
-                  hackathon ? getParticipantData(hackathon) : undefined
-                }
-                isRegistrationClosed={
-                  hackathon ? !hackathon.registrationOpen : false
-                }
+                initialData={getParticipantData(hackathon)}
+                isRegistrationClosed={!(hackathon?.registrationOpen ?? true)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(landing)/organizations/[id]/hackathons/[hackathonId]/settings/page.tsx
around lines 289 - 294, The ternary null-guards around hackathon are redundant
because getParticipantData already returns undefined for null; simplify by
passing getParticipantData(hackathon) directly to initialData and by deriving
isRegistrationClosed as !hackathon.registrationOpen only after ensuring
hackathon exists (e.g., use a boolean expression like hackathon ?
!hackathon.registrationOpen : false) can be replaced with a safe evaluation such
as Boolean(hackathon && !hackathon.registrationOpen); update the JSX props
initialData and isRegistrationClosed to remove the unnecessary hackathon ? ... :
... around getParticipantData and to use a concise safe boolean for registration
state while keeping references to getParticipantData, initialData,
isRegistrationClosed, and hackathon.

31-31: Use a const arrow function instead of a function declaration.

export default function SettingsPage() is a function declaration. As per coding guidelines, TypeScript/TSX files should prefer const arrow functions with explicit type annotations over function declarations.

♻️ Proposed fix
-export default function SettingsPage() {
+const SettingsPage = (): JSX.Element => {
   ...
-}
+};
+export default SettingsPage;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(landing)/organizations/[id]/hackathons/[hackathonId]/settings/page.tsx
at line 31, The export uses a function declaration "SettingsPage"; change it to
a const arrow component with an explicit type annotation (e.g., const
SettingsPage: React.FC or NextPage) and export it as the default export to
follow project TypeScript/TSX conventions; update any internal references if
needed and ensure the component remains the default export.

40-55: Wrap fetchHackathon in useCallback to satisfy exhaustive-deps and avoid future stale-closure bugs.

fetchHackathon is recreated on every render and omitted from the useEffect dependency array. Failing to specify dependencies can result in effects not updating correctly when state changes — these "stale closures" are a common source of surprising bugs. While it's functionally safe today because hackathonId (which fetchHackathon closes over) is already in deps, the linter will flag it and any future closure over additional state would silently become stale.

Also consolidate the useEffect import (line 20) with the existing React import on line 3.

♻️ Proposed fix
-import React, { useState } from 'react';
+import React, { useState, useEffect, useCallback } from 'react';
 import { useParams } from 'next/navigation';
 ...
-import { useEffect } from 'react';
 ...
-  const fetchHackathon = async () => {
-    try {
-      const res = await getHackathon(hackathonId);
-      setHackathon(res.data);
-    } catch {
-      toast.error('Failed to load hackathon data');
-    } finally {
-      setIsLoading(false);
-    }
-  };
+  const fetchHackathon = useCallback(async () => {
+    try {
+      const res = await getHackathon(hackathonId);
+      setHackathon(res.data);
+    } catch {
+      toast.error('Failed to load hackathon data');
+    } finally {
+      setIsLoading(false);
+    }
+  }, [hackathonId]);

   useEffect(() => {
     if (hackathonId) {
       fetchHackathon();
     }
-  }, [hackathonId]);
+  }, [hackathonId, fetchHackathon]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(landing)/organizations/[id]/hackathons/[hackathonId]/settings/page.tsx
around lines 40 - 55, Wrap the fetchHackathon function in useCallback (e.g.,
const fetchHackathon = useCallback(async () => { ... }, [hackathonId,
getHackathon, setHackathon, setIsLoading])) and then include fetchHackathon in
the useEffect dependency array so the effect becomes useEffect(() => { if
(hackathonId) fetchHackathon(); }, [hackathonId, fetchHackathon]); ensure the
useCallback includes all values it closes over (hackathonId, getHackathon,
setHackathon, setIsLoading) to avoid stale closures; also consolidate the
separate useEffect import into the main React import at the top (import React, {
useEffect, useCallback, useState, ... } from 'react').

61-140: Replace as any casts with proper types in mapping functions.

as any at lines 70, 97, and 104 silently discards TypeScript's type checking on the mapped data, and as any on tab initialData props (lines 267, 306, 325) extends that blindspot to the child components. Mismatches between the Hackathon API shape and the form *FormData types should be resolved by aligning the types or using as unknown as TargetType with explicit intermediate checks, not by widening to any.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(landing)/organizations/[id]/hackathons/[hackathonId]/settings/page.tsx
around lines 61 - 140, The helper mappers getGeneralData, getTimelineData,
getParticipantData, and getAdvancedData are using unsafe "as any" casts (e.g.,
venueType, phases, participantType, registrationDeadlinePolicy and tab
initialData props) which bypass TypeScript checks; replace those with explicit,
correct target types (e.g., cast phases to PhaseFormData[] or map to
PhaseFormData objects, cast venueType/participantType/registrationDeadlinePolicy
to the appropriate union/type used by the forms) or use "as unknown as
TargetType" only after validating/transforming values (e.g., map/convert strings
to expected enums, ensure optional Dates are Date | undefined), and update the
initialData props passed to the child form components to use these typed return
shapes instead of any so the form data types line up with the Hackathon API
shape.

Comment on lines +184 to 187
if (isLoading) return <Loading />;

return (
<AuthGuard redirectTo='/auth?mode=signin' fallback={<Loading />}>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

isLoading early-return bypasses AuthGuard, triggering an unnecessary API call for unauthenticated users.

if (isLoading) return <Loading /> fires unconditionally before AuthGuard is ever evaluated. For unauthenticated visitors this means:

  1. fetchHackathon() runs (and fails with an auth error) via the useEffect.
  2. A brief <Loading /> flash appears.
  3. The error toast fires before the auth redirect kicks in.

Move the loading indicator inside AuthGuard, or make fetchHackathon dependent on an auth check:

♻️ Proposed fix
-  if (isLoading) return <Loading />;
-
   return (
     <AuthGuard redirectTo='/auth?mode=signin' fallback={<Loading />}>
+      {isLoading ? <Loading /> : (
       <div className='bg-background min-h-screen p-4 text-white sm:p-6 md:p-8'>
         ...
       </div>
+      )}
     </AuthGuard>
   );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/`(landing)/organizations/[id]/hackathons/[hackathonId]/settings/page.tsx
around lines 184 - 187, The top-level early return "if (isLoading) return
<Loading />" short-circuits rendering AuthGuard and lets useEffect ->
fetchHackathon run for unauthenticated users; remove that top-level return and
instead render AuthGuard unconditionally, passing <Loading /> as its fallback
(so the spinner shows inside AuthGuard), and/or gate the useEffect that calls
fetchHackathon behind the auth state (check session/isAuthenticated before
invoking fetchHackathon in the useEffect). Update references: remove the global
isLoading return, ensure AuthGuard is always rendered, and add an auth-check
around fetchHackathon in the useEffect to prevent unauthenticated API calls.

Comment on lines +55 to +63
slug: z
.string()
.min(3, 'Slug must be at least 3 characters')
.max(50, 'Slug must be less than 50 characters')
.regex(
/^[a-z0-9-]+$/,
'Slug can only contain lowercase letters, numbers, and hyphens'
)
.optional(),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

.optional() after .min(3) doesn't cover empty strings — form UX gap.

In Zod, .optional() only permits undefined; an empty string "" is still validated against .min(3) and will fail with "Slug must be at least 3 characters" instead of being treated as absent. If the form library (e.g., react-hook-form) submits an empty slug field as "" rather than undefined, users who leave the field blank will see a misleading error.

🔧 Proposed fix using `z.preprocess`
-    slug: z
-      .string()
-      .min(3, 'Slug must be at least 3 characters')
-      .max(50, 'Slug must be less than 50 characters')
-      .regex(
-        /^[a-z0-9-]+$/,
-        'Slug can only contain lowercase letters, numbers, and hyphens'
-      )
-      .optional(),
+    slug: z.preprocess(
+      (v) => (v === '' ? undefined : v),
+      z
+        .string()
+        .min(3, 'Slug must be at least 3 characters')
+        .max(50, 'Slug must be less than 50 characters')
+        .regex(
+          /^[a-z0-9]+(?:-[a-z0-9]+)*$/,
+          'Slug must start and end with a letter or number, and contain only lowercase letters, numbers, and single hyphens'
+        )
+        .optional()
+    ),

z.preprocess coerces "" to undefined before the string schema runs, so clearing the field correctly produces undefined and skips all string validations.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
slug: z
.string()
.min(3, 'Slug must be at least 3 characters')
.max(50, 'Slug must be less than 50 characters')
.regex(
/^[a-z0-9-]+$/,
'Slug can only contain lowercase letters, numbers, and hyphens'
)
.optional(),
slug: z.preprocess(
(v) => (v === '' ? undefined : v),
z
.string()
.min(3, 'Slug must be at least 3 characters')
.max(50, 'Slug must be less than 50 characters')
.regex(
/^[a-z0-9]+(?:-[a-z0-9]+)*$/,
'Slug must start and end with a letter or number, and contain only lowercase letters, numbers, and single hyphens'
)
.optional()
),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/new/tabs/schemas/infoSchema.ts` around
lines 55 - 63, The slug schema in infoSchema.ts uses z.string().min(3)...
.optional(), but .optional() doesn't treat an empty string "" as absent; coerce
"" to undefined before validation by wrapping the current string schema in
z.preprocess (e.g., preprocess value => value === "" ? undefined : value) so
that when the form submits an empty slug it becomes undefined and skips the
.min/.max/.regex checks; update the slug definition to use z.preprocess(...) and
keep the existing string().min(3)... .optional() validations.

Comment on lines +59 to +62
.regex(
/^[a-z0-9-]+$/,
'Slug can only contain lowercase letters, numbers, and hyphens'
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Regex permits leading/trailing/consecutive hyphens — tighten the pattern.

/^[a-z0-9-]+$/ accepts -bad-slug, bad-slug-, and my--slug, all of which produce broken or ugly URLs. A slug should start and end with an alphanumeric character and never have consecutive hyphens.

🔧 Proposed fix
-      .regex(
-        /^[a-z0-9-]+$/,
-        'Slug can only contain lowercase letters, numbers, and hyphens'
-      )
+      .regex(
+        /^[a-z0-9]+(?:-[a-z0-9]+)*$/,
+        'Slug must start and end with a letter or number, and contain only lowercase letters, numbers, and single hyphens'
+      )

The updated pattern:

  • Requires at least one alphanumeric segment.
  • Allows hyphens only between alphanumeric segments (no leading, trailing, or consecutive hyphens).
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.regex(
/^[a-z0-9-]+$/,
'Slug can only contain lowercase letters, numbers, and hyphens'
)
.regex(
/^[a-z0-9]+(?:-[a-z0-9]+)*$/,
'Slug must start and end with a letter or number, and contain only lowercase letters, numbers, and single hyphens'
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/new/tabs/schemas/infoSchema.ts` around
lines 59 - 62, The slug regex currently used in the .regex call on the slug
field allows leading/trailing/consecutive hyphens; replace it with a pattern
that requires the slug to start with one or more lowercase alphanumerics,
optionally followed by groups consisting of a single hyphen and one or more
lowercase alphanumerics (i.e., no leading/trailing hyphens and no consecutive
hyphens), and keep the existing error message or update it to reflect the
tightened rule; locate the .regex(...) chained on the slug schema in
infoSchema.ts and swap the pattern accordingly.

Comment on lines 91 to 92
prizeTiers.map(tier => ({
rank: (tier as any).rank || 0,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

fd "rewardsSchema" -e ts -e tsx

Repository: boundlessfi/boundless

Length of output: 133


🏁 Script executed:

rg "interface PrizeTier|type PrizeTier" -t ts -t tsx

Repository: boundlessfi/boundless

Length of output: 92


🏁 Script executed:

rg "PrizeTier" -t ts -t tsx -C 3

Repository: boundlessfi/boundless

Length of output: 92


🏁 Script executed:

cat -n components/organization/hackathons/rewards/PublishWinnersWizard.tsx | head -120

Repository: boundlessfi/boundless

Length of output: 4056


🏁 Script executed:

cat -n components/organization/hackathons/new/tabs/schemas/rewardsSchema.ts

Repository: boundlessfi/boundless

Length of output: 916


🏁 Script executed:

rg "PrizeTier" --type ts | head -30

Repository: boundlessfi/boundless

Length of output: 2696


🏁 Script executed:

cat -n types/hackathon/core.ts | grep -A 10 "interface PrizeTier"

Repository: boundlessfi/boundless

Length of output: 368


🏁 Script executed:

cat -n lib/api/hackathons.ts | grep -A 10 "interface PrizeTier"

Repository: boundlessfi/boundless

Length of output: 448


🏁 Script executed:

rg "\.rank" --type ts components/organization/hackathons/rewards/ -B 2 -A 2

Repository: boundlessfi/boundless

Length of output: 11600


🏁 Script executed:

rg "rank:" --type ts hooks/use-publish-winners.ts -B 3 -A 3

Repository: boundlessfi/boundless

Length of output: 47


Remove unsafe as any cast and add rank to PrizeTier type.

The PrizeTier type definition does not include a rank property, yet line 92 casts to as any to access it. This bypasses type safety and masks a type system mismatch. The pattern appears throughout the codebase (WinnersGrid.tsx, WinnersPreviewPage.tsx) where tier.rank is accessed directly.

Resolve this by either:

  1. Adding rank: number; to the PrizeTier interface in rewardsSchema.ts
  2. Using the array index as the deterministic rank:
Suggested fix
-      prizeTiers.map(tier => ({
-        rank: (tier as any).rank || 0,
+      prizeTiers.map((tier, index) => ({
+        rank: index + 1,
         prizeAmount: tier.prizeAmount,
         currency: tier.currency,
       })),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
prizeTiers.map(tier => ({
rank: (tier as any).rank || 0,
prizeTiers.map((tier, index) => ({
rank: index + 1,
prizeAmount: tier.prizeAmount,
currency: tier.currency,
})),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/rewards/PublishWinnersWizard.tsx` around
lines 91 - 92, The code uses an unsafe (tier as any).rank; update the PrizeTier
type in rewardsSchema.ts to include rank: number and then remove the casts and
access tier.rank directly in PublishWinnersWizard.tsx (the map creating { rank:
... }), WinnersGrid.tsx, and WinnersPreviewPage.tsx; ensure any code that
constructs PrizeTier objects (form handlers, serializers, defaults) sets a
numeric rank so the compiler and runtime remain consistent, or if you prefer
deterministic ordering use the array index when constructing PrizeTier instances
but do that consistently where tier.rank is consumed.

Comment on lines +92 to 111
try {
await api.patch(
`/organizations/${organizationId}/hackathons/${hackathonId}/advanced-settings`,
{ advancedSettings: data }
);
toast.success('Advanced settings saved successfully!');
// Update form state with new values to maintain "clean" status
form.reset(data);
if (onSaveSuccess) {
await onSaveSuccess();
}
} catch (error: any) {
const message = error.response?.data?.message || error.message;
const errorMessage = Array.isArray(message) ? message[0] : message;
toast.error(
errorMessage || 'Failed to save advanced settings. Please try again.'
);
} finally {
setIsSaving(false);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

onSaveSuccess failure produces a contradictory success + error toast pair.

If onSaveSuccess throws (e.g. a refetch fails), the catch block fires after the success toast on line 97 has already been shown. The user sees "Advanced settings saved successfully!" immediately followed by an error toast, which is confusing.

Consider isolating the callback error:

Proposed fix
      toast.success('Advanced settings saved successfully!');
      form.reset(data);
      if (onSaveSuccess) {
-       await onSaveSuccess();
+       await onSaveSuccess().catch(() => {
+         // Post-save callback failed (e.g. refetch); save itself succeeded.
+       });
      }
    } catch (error: any) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/settings/AdvancedSettingsTab.tsx` around
lines 92 - 111, The success toast is shown before calling onSaveSuccess, so if
onSaveSuccess throws you end up with both success and error toasts; change the
flow in the save handler (the try block around api.patch, form.reset, and
onSaveSuccess) so that either (a) you call await onSaveSuccess() before calling
toast.success('Advanced settings saved successfully!') or (b) wrap the await
onSaveSuccess() call in its own try/catch and only show the success toast if the
callback completes without error; keep form.reset(data) behavior and ensure
setIsSaving(false) remains in the finally block.

Comment on lines +72 to +85
useEffect(() => {
if (initialData) {
form.reset({
startDate: initialData.startDate || undefined,
submissionDeadline: initialData.submissionDeadline || undefined,
judgingStart: initialData.judgingStart || undefined,
endDate: initialData.endDate || undefined,
judgingEnd: initialData.judgingEnd || undefined,
winnersAnnouncedAt: initialData.winnersAnnouncedAt || undefined,
timezone: initialData.timezone || 'UTC',
phases: initialData.phases || [],
});
}
}, [initialData, form]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

rg -n -C5 'TimelineSettingsTab' --type=tsx --type=ts --type=jsx

Repository: boundlessfi/boundless

Length of output: 92


🏁 Script executed:

rg -n -C5 'TimelineSettingsTab' --type=typescript

Repository: boundlessfi/boundless

Length of output: 99


🏁 Script executed:

rg 'TimelineSettingsTab' -C5

Repository: boundlessfi/boundless

Length of output: 5273


🏁 Script executed:

cat -n 'app/(landing)/organizations/[id]/hackathons/[hackathonId]/settings/page.tsx'

Repository: boundlessfi/boundless

Length of output: 16026


🏁 Script executed:

sed -n '72,85p' 'components/organization/hackathons/settings/TimelineSettingsTab.tsx'

Repository: boundlessfi/boundless

Length of output: 630


initialData object reference in dependency array causes spurious form resets.

The parent component calls getTimelineData(hackathon) directly in JSX without memoization, creating a new object reference on every parent re-render. This causes the effect to fire repeatedly, calling form.reset() and wiping any in-progress user edits.

Fix by either:

  1. Wrapping initialData={getTimelineData(hackathon)} in useMemo in the parent, or
  2. Replacing the initialData dependency with a stable key (e.g., hackathon?.id or a serialized hash of the data).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/settings/TimelineSettingsTab.tsx` around
lines 72 - 85, The effect in TimelineSettingsTab.tsx uses initialData in the
dependency array which changes reference on every parent render and triggers
form.reset (the useEffect that calls form.reset with fields like startDate,
submissionDeadline, phases, timezone); change the dependency to a stable
identifier instead of the object reference — either have the parent memoize
getTimelineData(hackathon) or in this component replace initialData in the
dependency array with a stable key (e.g., hackathon?.id or a deterministic
string such as JSON.stringify(initialData) or a computed hash) so useEffect only
runs when the underlying timeline actually changes and does not wipe in-progress
edits.

Comment on lines 188 to 235
const fetchHackathon = useCallback(async () => {
try {
const response = await getHackathon(hackathonId);
if (response.success) {
const fetchedHackathon: Hackathon = response.data;
setHackathon(fetchedHackathon);

const hackathonContractId =
hackathon.contractId || hackathon.escrowAddress || null;
if (hackathonContractId) {
setContractId(hackathonContractId);
}
if (fetchedHackathon.prizeTiers) {
// Sort tiers by amount descending or use position if available
const sortedTiers = [...fetchedHackathon.prizeTiers].sort(
(a: any, b: any) => {
if (a.position && b.position) return a.position - b.position;
return (b.amount || 0) - (a.amount || 0);
}
);

const tiers: PrizeTier[] = sortedTiers.map(
(tier: any, index: number) => ({
id: tier.id || `tier-${index + 1}`,
place: tier.position
? `${getOrdinalSuffix(tier.position)} Place`
: `${getOrdinalSuffix(index + 1)} Place`,
prizeAmount: (tier.prizeAmount || tier.amount)?.toString() || '0',
currency: tier.currency || 'USDC',
passMark: tier.passMark || 0,
description: tier.description,
rank: tier.position || index + 1,
})
);
setPrizeTiers(tiers);
}

const hackathonContractId =
fetchedHackathon.contractId || fetchedHackathon.escrowAddress || null;
if (hackathonContractId) {
setContractId(hackathonContractId);
}
} catch {
setPrizeTiers(getDefaultPrizeTiers());
}
};
} catch {
setPrizeTiers(getDefaultPrizeTiers());
}
}, [hackathonId]);

useEffect(() => {
if (organizationId && hackathonId) {
fetchHackathon();
}
}, [organizationId, hackathonId]);
}, [organizationId, hackathonId, fetchHackathon]);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

isLoading doesn't account for hackathon data fetch.

isLoading is initialized to true (line 122) and only set to false inside fetchSubmissions (line 430). The fetchHackathon callback runs concurrently but never participates in isLoading. This means consumers may see isLoading === false while prize tiers and hackathon state are still being resolved, potentially rendering stale default data briefly.

Consider either folding hackathon fetch status into isLoading or exposing a separate isLoadingHackathon flag.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-hackathon-rewards.ts` around lines 188 - 235, The fetchHackathon
flow is not reflected in isLoading (initialized true and only toggled in
fetchSubmissions), causing consumers to see loading=false while
hackathon/prizeTiers are unresolved; update fetchHackathon to participate in
loading by either (A) toggling the shared isLoading around the async call
(setIsLoading(true) at start and setIsLoading(false) in a finally block) or (B)
add a separate boolean state isLoadingHackathon and set it true/false in
fetchHackathon (use try/finally) and expose it alongside isLoading so callers
can wait for prizeTiers/hackathon; reference the fetchHackathon function, the
isLoading state and the fetchSubmissions usage to keep behavior consistent.

Comment on lines +310 to +336
return {
...sub,
participant: {
...partData,
name: creator.name || partData.name,
username: creator.username || partData.username,
image: creator.image,
email: creator.email,
user: {
...partData.user,
name: creator.name,
username: creator.username,
image: creator.image,
email: creator.email,
profile: {
...partData.user?.profile,
firstName: creator.name?.split(' ')[0] || '',
lastName:
creator.name?.split(' ').slice(1).join(' ') ||
'',
username: creator.username,
avatar: creator.image,
image: creator.image,
},
},
},
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

PII exposure: creator.email is propagated into client-side participant state.

Lines 317 and 323 copy creator.email into the enriched submission data. If this email isn't needed by the rewards UI, avoid storing it in client state to reduce PII surface area. If it is needed downstream (e.g., for distribution notifications), ensure it's handled in compliance with your privacy policy.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-hackathon-rewards.ts` around lines 310 - 336, The enrichment block
is copying creator.email into client state (participant.email and
participant.user.email), exposing PII; remove those assignments so the returned
object does not propagate creator.email (omit participant.email and
participant.user.email or set them to undefined/empty) unless a secure
server-side flow explicitly requires it; update the code in the function that
builds the submission object (the return that spreads sub and sets participant:
{ ...partData, name, username, image, email, user: { ...partData.user, name,
username, image, email, profile: { ... } } }) to stop assigning creator.email to
participant and participant.user (keep other fields like name/username/image
as-is) or gate inclusion behind an explicit opt-in flag for downstream secure
handling.

Comment on lines +398 to +410
if (result) {
return {
...sub,
rank: result.rank,
score: Math.round(Number(result.averageScore || 0)),
maxScore: 100,
averageScore: Number(result.averageScore || 0),
projectName: result.projectName || sub.projectName,
submissionTitle: result.projectName || sub.submissionTitle,
};
}
return sub;
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hardcoded maxScore: 100 and submissionTitle uses projectName as source.

  1. maxScore: 100 (Line 403) is a magic number. If the scoring rubric changes, this won't adapt. Consider deriving it from hackathon settings or the judging configuration.

  2. submissionTitle (Line 406) is set to result.projectName, which is the same value used for projectName on the preceding line. This conflates two potentially distinct fields — the project name and the submission title may differ.

Suggested fix for submissionTitle
                    projectName: result.projectName || sub.projectName,
-                   submissionTitle: result.projectName || sub.submissionTitle,
+                   submissionTitle: result.submissionTitle || result.projectName || sub.submissionTitle,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-hackathon-rewards.ts` around lines 398 - 410, The mapped result
block that creates the augmented submission object (inside the map callback
referencing result and sub) currently hardcodes maxScore: 100 and sets
submissionTitle from result.projectName; change maxScore to derive from the
hackathon/judging configuration (e.g., use the hackathon settings or
judgingConfig value accessible in this hook rather than a literal 100) and set
submissionTitle to use result.submissionTitle || sub.submissionTitle so
projectName and submissionTitle remain distinct; update the code in the map
where rank, score, maxScore, averageScore, projectName, and submissionTitle are
assembled to reference the derived max score and the correct submissionTitle
field.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
components/organization/hackathons/rewards/PublishWinnersWizard.tsx (1)

87-87: ⚠️ Potential issue | 🟡 Minor

canGoNext is unconditionally true — the wizard step guard has no effect.

With canGoNext = true, a user can advance from the AnnouncementStep to PreviewStep regardless of whether they have entered any announcement content. If an announcement is genuinely optional this is fine, but then the variable adds no value. If it is required, add the validation guard.

🔒 Proposed fix — guard on announcement step
-  const canGoNext = true;
+  const canGoNext =
+    currentStep !== 'announcement' || announcement.trim().length > 0;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/rewards/PublishWinnersWizard.tsx` at line
87, canGoNext is hardcoded true so the wizard (PublishWinnersWizard) lets users
advance from AnnouncementStep to PreviewStep without validating announcement
content; update the guard so canGoNext evaluates to a boolean based on the
current step and announcement state (e.g., when the active step is
AnnouncementStep require the announcement string to be non-empty/trimmed),
otherwise allow navigation for other steps—adjust the expression in the
component where canGoNext is declared and referenced so it uses the announcement
state and currentStep instead of the literal true.
components/organization/hackathons/new/tabs/RewardsTab.tsx (1)

421-428: ⚠️ Potential issue | 🟠 Major

rank values desync from visual position after drag-and-drop.

move(oldIndex, newIndex) reorders the fields array but leaves each tier's stored rank value untouched. After a drag, onSave receives tiers whose rank fields no longer match their array positions. If the backend uses rank for prize distribution (which the PR summary explicitly states), winners receive incorrect prizes.

The same root cause applies to handleRemove (remaining ranks have gaps) and handleAdd (fields.length + 1 produces duplicate ranks when tiers were previously removed — e.g., after removing rank:2 from [1,2,3], adding yields two rank:3 entries).

The safest centralised fix is to normalise ranks in onSubmit before delegating to onSave:

🐛 Proposed fix — normalise ranks at submission time
  const onSubmit = async (data: RewardsFormData) => {
    try {
      if (onSave) {
-       await onSave(data);
+       const normalizedData = {
+         ...data,
+         prizeTiers: data.prizeTiers.map((tier, index) => ({
+           ...tier,
+           rank: index + 1,
+         })),
+       };
+       await onSave(normalizedData);
      }
    } catch (error: any) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/new/tabs/RewardsTab.tsx` around lines 421
- 428, The tier objects' rank fields get out of sync after
reordering/removal/addition; update the submission path to normalise ranks
before saving: in the onSubmit (or wherever onSave is called) iterate the
current fields array and set each tier.rank = index + 1 (or appropriate base) to
reflect their visual order, then pass the normalised array to onSave; also
ensure handleAdd and handleRemove no longer try to compute persistent ranks
(they can push/remove items and rely on onSubmit normalization) so you avoid
duplicate or gap ranks after move(oldIndex, newIndex), handleRemove, and
handleAdd.
🧹 Nitpick comments (5)
components/organization/hackathons/rewards/PublishWinnersWizard.tsx (1)

33-42: PublishWinnersWizard should be a const arrow function with an explicit type annotation.

Per the coding guideline for .tsx files, prefer const arrow functions with explicit type annotations over function declarations.

♻️ Proposed refactor
-export default function PublishWinnersWizard({
+const PublishWinnersWizard = ({
   open,
   onOpenChange,
   submissions,
   prizeTiers,
   escrow,
   organizationId,
   hackathonId,
   onSuccess,
-}: PublishWinnersWizardProps) {
+}: PublishWinnersWizardProps): React.JSX.Element => {
   ...
-}
+};
+
+export default PublishWinnersWizard;

As per coding guidelines, "Prefer const arrow functions with explicit type annotations over function declarations."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/rewards/PublishWinnersWizard.tsx` around
lines 33 - 42, Change the named function declaration PublishWinnersWizard to a
const arrow function and add an explicit React.FC (or appropriate) type
annotation using the existing PublishWinnersWizardProps type; locate the export
default PublishWinnersWizard declaration and replace it with a const
PublishWinnersWizard: React.FC<PublishWinnersWizardProps> = ({ open,
onOpenChange, submissions, prizeTiers, escrow, organizationId, hackathonId,
onSuccess }) => { ... } and keep the existing export default
PublishWinnersWizard at the end (or export inline) so the component signature
and props type are explicit.
components/organization/hackathons/rewards/RewardDistributionStatusBanner.tsx (1)

154-156: Prefer clsx over ternary in className for conditional classes.

The inline ternary ${isLoading ? 'animate-spin' : ''} inside a template literal violates the project guideline. Use clsx instead.

♻️ Proposed refactor
+import clsx from 'clsx';
 ...
             <RefreshCw
-              className={`h-4 w-4 ${isLoading ? 'animate-spin' : ''}`}
+              className={clsx('h-4 w-4', isLoading && 'animate-spin')}
             />

As per coding guidelines, "For conditional classes, prefer clsx or similar helper functions over ternary operators in JSX."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@components/organization/hackathons/rewards/RewardDistributionStatusBanner.tsx`
around lines 154 - 156, In RewardDistributionStatusBanner, replace the inline
template literal on the RefreshCw element that uses `${isLoading ?
'animate-spin' : ''}` with clsx to express the conditional class; update the
RefreshCw className to something like clsx('h-4 w-4', { 'animate-spin':
isLoading }) and add an import for clsx if missing so the conditional class
follows the project guideline.
components/organization/hackathons/new/tabs/RewardsTab.tsx (2)

435-435: Prefer unknown over any for caught errors.

catch (error: any) bypasses type checking. unknown with explicit narrowing is safer and idiomatic TypeScript.

♻️ Proposed fix
-   } catch (error: any) {
-     const message = error.response?.data?.message || error.message;
+   } catch (error: unknown) {
+     const err = error as { response?: { data?: { message?: string | string[] } }; message?: string };
+     const message = err.response?.data?.message || err.message;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/new/tabs/RewardsTab.tsx` at line 435,
Change the catch clause from catch (error: any) to catch (error: unknown) in the
try/catch block in RewardsTab.tsx, then narrow the error before using it (for
example use if (error instanceof Error) to access error.message or coerce via
String(error) for logging/notifications); update any uses of error inside the
block (e.g., logging, setError calls) to use the narrowed value so the code
type-checks safely.

410-410: Use slice(2, 11) instead of substr(2, 9)substr is a deprecated legacy feature.

String.prototype.substr is defined in ECMAScript Annex B (non-core features) and marked deprecated by MDN. Replace with slice(2, 11) for modern, portable code (equivalent: start at index 2, take 9 characters).

♻️ Proposed fix
-     id: `tier-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
+     id: `tier-${Date.now()}-${Math.random().toString(36).slice(2, 11)}`,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/new/tabs/RewardsTab.tsx` at line 410, The
id generation uses the deprecated String.prototype.substr; in the RewardsTab.tsx
where the id is created as
`tier-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, replace the use
of .substr(2, 9) with .slice(2, 11) so it takes 9 characters starting at index 2
(i.e., `Math.random().toString(36).slice(2, 11)`), keeping the rest of the id
template unchanged.
hooks/use-hackathon-rewards.ts (1)

57-70: getOrdinalSuffix is missing an explicit return type annotation.

🛠️ Proposed fix
-const getOrdinalSuffix = (i: number) => {
+const getOrdinalSuffix = (i: number): string => {

As per coding guidelines, **/*.{ts,tsx} requires "Prefer const arrow functions with explicit type annotations."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-hackathon-rewards.ts` around lines 57 - 70, The getOrdinalSuffix
arrow function lacks an explicit return type; update its signature to include a
string return annotation so it conforms to the rule "Prefer const arrow
functions with explicit type annotations." Modify the const getOrdinalSuffix
declaration (function name: getOrdinalSuffix) to include the return type (e.g.,
: string) while keeping the existing parameter type (i: number) and function
body unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/organization/hackathons/rewards/PublishWinnersWizard.tsx`:
- Around line 99-109: getPrizeForRank returns different shapes which makes
callers like PreviewStep and WinnersGrid get undefined for amount/currency when
no tier is found; update getPrizeForRank (which looks up mappedPrizeTiers) to
always return a consistent object shape (e.g., { amount: string, currency:
string, label: string }) by providing safe defaults on the not-found path (for
example amount: '0' and currency: 'USDC' and label derived from those) and/or
add an explicit return type for the function so the compiler enforces the
uniform shape.

In `@hooks/use-hackathon-rewards.ts`:
- Around line 307-308: The code sets pId = sub.projectId || subData.projectId ||
subData.id which incorrectly treats subData.id (a submission ID) as a project ID
and triggers spurious getCrowdfundingProject calls; change the assignment to
only use sub.projectId || subData.projectId and add/keep the guard if (!pId) to
skip calling getCrowdfundingProject entirely (update the pId reference in the
block that calls getCrowdfundingProject and the inner catch handling to reflect
that no fetch should occur when projectId is absent).
- Around line 199-218: The sort and mapping use non-existent fields position and
amount, so sortedTiers and mapping in the hook never respect the PrizeTier
fields; update the comparator and mapper to reference the correct fields (place
and prizeAmount) — i.e., in sortedTiers.sort use place (parse numeric rank from
place if present) and numeric prizeAmount for amount-based sorting, and in the
mapped PrizeTier object use tier.place and tier.prizeAmount (falling back to
getOrdinalSuffix(index+1) and stringified prizeAmount '0' as needed) while
preserving rank and other properties; adjust any uses of
tier.position/tier.amount to tier.place/tier.prizeAmount and parse values where
numeric comparisons are required (see sortedTiers, PrizeTier mapping, and
getOrdinalSuffix usage).

In `@lib/utils/renderHtml.ts`:
- Around line 46-48: The catch block in sanitizeHtml silently swallows errors
from DOMPurify.sanitize; update the catch to log the error with console.error so
failures are visible (include a short context message and the caught error, and
optionally the input HTML variable) before returning { __html: '' }; reference
sanitizeHtml and DOMPurify.sanitize when making the change.

---

Outside diff comments:
In `@components/organization/hackathons/new/tabs/RewardsTab.tsx`:
- Around line 421-428: The tier objects' rank fields get out of sync after
reordering/removal/addition; update the submission path to normalise ranks
before saving: in the onSubmit (or wherever onSave is called) iterate the
current fields array and set each tier.rank = index + 1 (or appropriate base) to
reflect their visual order, then pass the normalised array to onSave; also
ensure handleAdd and handleRemove no longer try to compute persistent ranks
(they can push/remove items and rely on onSubmit normalization) so you avoid
duplicate or gap ranks after move(oldIndex, newIndex), handleRemove, and
handleAdd.

In `@components/organization/hackathons/rewards/PublishWinnersWizard.tsx`:
- Line 87: canGoNext is hardcoded true so the wizard (PublishWinnersWizard) lets
users advance from AnnouncementStep to PreviewStep without validating
announcement content; update the guard so canGoNext evaluates to a boolean based
on the current step and announcement state (e.g., when the active step is
AnnouncementStep require the announcement string to be non-empty/trimmed),
otherwise allow navigation for other steps—adjust the expression in the
component where canGoNext is declared and referenced so it uses the announcement
state and currentStep instead of the literal true.

---

Duplicate comments:
In
`@components/organization/hackathons/rewards/RewardDistributionStatusBanner.tsx`:
- Line 15: Remove the unused import "format" from 'date-fns' in
RewardDistributionStatusBanner.tsx; keep only the used "formatInTimeZone" import
and verify there are no remaining references to "format" in the file (e.g., in
functions or JSX) before committing the change.

In `@components/organization/hackathons/rewards/WinnerCard.tsx`:
- Around line 129-135: The conditional in WinnerCard that uses a truthy check on
winner.averageScore incorrectly treats 0 as falsy; change the check to test for
null/undefined (e.g., use != null) so an actual 0.0 is displayed, and use a
nullish fallback for winner.score (e.g., ?? 0) for the fallback path; keep the
toFixed(1) call on averageScore when present and ensure you only call it when
averageScore is not null/undefined.

In `@hooks/use-hackathon-rewards.ts`:
- Around line 315-339: The participant object is still copying PII: remove
propagation of creator.email into participant.email and participant.user.email
in the participant merge block (the section spreading partData and constructing
participant.user and participant.user.profile); instead either omit those email
assignments entirely or explicitly set them to undefined / preserve only
partData.email if that is allowed, so no creator.email flows into client-side
state.
- Around line 191-232: The fetchHackathon function currently never toggles any
loading state, causing consumers to see isLoading === false while
hackathon/prizeTiers are unresolved; update fetchHackathon to manage loading by
either (A) wrapping its logic in try/finally and calling setIsLoading(true) at
start and setIsLoading(false) in finally (so it coordinates with
fetchSubmissions), or (B) introduce a dedicated isLoadingHackathon state with
setIsLoadingHackathon(true) at the start and setIsLoadingHackathon(false) in
finally; modify callers/effects to consume the new flag if you choose option B
and ensure you reference fetchHackathon, setPrizeTiers, setHackathon, and
setContractId when adding the loading toggles.
- Around line 401-413: The map callback in use-hackathon-rewards.ts still
hardcodes maxScore and incorrectly prefers result.projectName for
submissionTitle; update the mapping to derive maxScore from the incoming data
(e.g., use result.maxScore or a passed-in rubric max value instead of 100) and
set submissionTitle to prefer result.submissionTitle with fallback to
sub.submissionTitle (keep projectName logic unchanged), i.e., replace the
hardcoded maxScore and swap the submissionTitle source so it reads
result.submissionTitle || sub.submissionTitle while still using
result.projectName || sub.projectName for projectName.

---

Nitpick comments:
In `@components/organization/hackathons/new/tabs/RewardsTab.tsx`:
- Line 435: Change the catch clause from catch (error: any) to catch (error:
unknown) in the try/catch block in RewardsTab.tsx, then narrow the error before
using it (for example use if (error instanceof Error) to access error.message or
coerce via String(error) for logging/notifications); update any uses of error
inside the block (e.g., logging, setError calls) to use the narrowed value so
the code type-checks safely.
- Line 410: The id generation uses the deprecated String.prototype.substr; in
the RewardsTab.tsx where the id is created as
`tier-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, replace the use
of .substr(2, 9) with .slice(2, 11) so it takes 9 characters starting at index 2
(i.e., `Math.random().toString(36).slice(2, 11)`), keeping the rest of the id
template unchanged.

In `@components/organization/hackathons/rewards/PublishWinnersWizard.tsx`:
- Around line 33-42: Change the named function declaration PublishWinnersWizard
to a const arrow function and add an explicit React.FC (or appropriate) type
annotation using the existing PublishWinnersWizardProps type; locate the export
default PublishWinnersWizard declaration and replace it with a const
PublishWinnersWizard: React.FC<PublishWinnersWizardProps> = ({ open,
onOpenChange, submissions, prizeTiers, escrow, organizationId, hackathonId,
onSuccess }) => { ... } and keep the existing export default
PublishWinnersWizard at the end (or export inline) so the component signature
and props type are explicit.

In
`@components/organization/hackathons/rewards/RewardDistributionStatusBanner.tsx`:
- Around line 154-156: In RewardDistributionStatusBanner, replace the inline
template literal on the RefreshCw element that uses `${isLoading ?
'animate-spin' : ''}` with clsx to express the conditional class; update the
RefreshCw className to something like clsx('h-4 w-4', { 'animate-spin':
isLoading }) and add an import for clsx if missing so the conditional class
follows the project guideline.

In `@hooks/use-hackathon-rewards.ts`:
- Around line 57-70: The getOrdinalSuffix arrow function lacks an explicit
return type; update its signature to include a string return annotation so it
conforms to the rule "Prefer const arrow functions with explicit type
annotations." Modify the const getOrdinalSuffix declaration (function name:
getOrdinalSuffix) to include the return type (e.g., : string) while keeping the
existing parameter type (i: number) and function body unchanged.

Comment on lines +307 to +308
const pId = sub.projectId || subData.projectId || subData.id;
if (pId) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

subData.id is a submission ID, not a project ID — the fallback triggers a spurious getCrowdfundingProject call.

When neither sub.projectId nor subData.projectId is set, subData.id (the submission's own ID) is used as pId. getCrowdfundingProject called with a submission ID will fail (or return unexpected data); the inner catch silently swallows it. The guard should skip the project fetch entirely when no projectId is available.

🐛 Proposed fix
-                const pId = sub.projectId || subData.projectId || subData.id;
+                const pId = sub.projectId || subData.projectId;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-hackathon-rewards.ts` around lines 307 - 308, The code sets pId =
sub.projectId || subData.projectId || subData.id which incorrectly treats
subData.id (a submission ID) as a project ID and triggers spurious
getCrowdfundingProject calls; change the assignment to only use sub.projectId ||
subData.projectId and add/keep the guard if (!pId) to skip calling
getCrowdfundingProject entirely (update the pId reference in the block that
calls getCrowdfundingProject and the inner catch handling to reflect that no
fetch should occur when projectId is absent).

Comment on lines 46 to 48
} catch {
return { __html: dirty };
return { __html: '' };
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n lib/utils/renderHtml.ts

Repository: boundlessfi/boundless

Length of output: 1218


🏁 Script executed:

rg -n "isomorphic-dompurify|jsdom|DOMPurify" --type ts --type tsx -C2

Repository: boundlessfi/boundless

Length of output: 92


🏁 Script executed:

cat package.json | grep -A 5 -B 5 "dompurify"

Repository: boundlessfi/boundless

Length of output: 388


🏁 Script executed:

rg "isomorphic-dompurify|jsdom" -t ts -C2

Repository: boundlessfi/boundless

Length of output: 47


🏁 Script executed:

rg "console\.error|logger\." -t ts --max-count=20

Repository: boundlessfi/boundless

Length of output: 8375


🏁 Script executed:

cat package.json | grep -E "jsdom|dompurify" | head -20

Repository: boundlessfi/boundless

Length of output: 91


Add error logging to the catch block to aid debugging.

The sanitizeHtml function silently discards exceptions from DOMPurify.sanitize(), making it difficult to diagnose why content disappears. The project uses vanilla dompurify with no SSR shim (isomorphic-dompurify or jsdom), so failures are possible in Node.js environments. Throughout the codebase, error handlers consistently use console.error; this catch block should follow the same pattern.

Suggested improvement
   } catch (err) {
+    console.error('[sanitizeHtml] DOMPurify failed, returning empty string:', err);
     return { __html: '' };
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
} catch {
return { __html: dirty };
return { __html: '' };
}
} catch (err) {
console.error('[sanitizeHtml] DOMPurify failed, returning empty string:', err);
return { __html: '' };
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/utils/renderHtml.ts` around lines 46 - 48, The catch block in
sanitizeHtml silently swallows errors from DOMPurify.sanitize; update the catch
to log the error with console.error so failures are visible (include a short
context message and the caught error, and optionally the input HTML variable)
before returning { __html: '' }; reference sanitizeHtml and DOMPurify.sanitize
when making the change.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
hooks/use-hackathon-rewards.ts (1)

237-239: Silent catch in fetchHackathon drops errors without logging or notifying the user.

fetchSubmissions calls toast.error on failure (Line 438), but fetchHackathon's catch does neither, leaving consumers with null/default state and no diagnostic signal.

🔧 Proposed fix
-    } catch {
-      setPrizeTiers(getDefaultPrizeTiers());
+    } catch (err) {
+      console.error('Failed to fetch hackathon data:', err);
+      setPrizeTiers(getDefaultPrizeTiers());
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-hackathon-rewards.ts` around lines 237 - 239, The catch block in
fetchHackathon silently swallows errors and resets prize tiers without any
logging or user feedback; change the catch to accept the error (e.g., catch
(err)), log the error (console.error or your app logger) and surface a
user-facing message (call toast.error with a helpful message) before falling
back to setPrizeTiers(getDefaultPrizeTiers()); update the fetchHackathon
function to mirror the behavior in fetchSubmissions so failures are diagnosable
and users are notified.
components/organization/hackathons/rewards/WinnersGrid.tsx (1)

41-52: getTierOrder can produce duplicate tiers for non-consecutive rank sets.

When sortedTiers.length === 3 but the three winner ranks are not exactly {1, 2, 3} (e.g., ranks 1, 3, 4), find(t => t.rank === 3) returns sortedTiers[1] (rank 3) while find(t => t.rank === 2) falls back to sortedTiers[1] as well — resulting in rank 3 appearing twice and rank 4 being dropped.

The podium reorder intent (center=1st, left=2nd, right=3rd) is better expressed positionally:

♻️ Proposed refactor
  const getTierOrder = (availableTiers: typeof prizeTiers) => {
    const sortedTiers = [...availableTiers].sort((a, b) => a.rank - b.rank);

    if (sortedTiers.length === 3) {
-     const secondTier = sortedTiers.find(t => t.rank === 2) || sortedTiers[1];
-     const firstTier = sortedTiers.find(t => t.rank === 1) || sortedTiers[0];
-     const thirdTier = sortedTiers.find(t => t.rank === 3) || sortedTiers[2];
-     return [secondTier, firstTier, thirdTier].filter(Boolean);
+     // Podium order: 2nd (left), 1st (center), 3rd (right)
+     return [sortedTiers[1], sortedTiers[0], sortedTiers[2]];
    }

    return sortedTiers;
  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/rewards/WinnersGrid.tsx` around lines 41 -
52, getTierOrder can produce duplicate tiers when the three available tiers
don't have consecutive ranks; change the length===3 branch to select tiers
positionally from sortedTiers (first = sortedTiers[0], second = sortedTiers[1],
third = sortedTiers[2]) and return [second, first, third] instead of using
find(...) with fallbacks, so you preserve all three unique entries; update the
function getTierOrder and references to sortedTiers/prizeTiers accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/organization/hackathons/rewards/PublishWinnersWizard.tsx`:
- Around line 91-92: The getPrizeForRank function is missing an explicit return
type; update its signature to include the specified return type so TypeScript
enforces it (e.g., change the arrow function declaration for getPrizeForRank to
accept rank: number and return { amount: string; currency: string; label: string
}). Locate getPrizeForRank (used alongside prizeTiers.map and the PrizeTier
schema) and add the return type annotation to the function signature exactly as
requested.

In `@components/organization/hackathons/rewards/WinnersPreviewPage.tsx`:
- Around line 40-46: The computed amount can become NaN because
parseFloat(tier.prizeAmount) is called without a fallback; update the logic that
builds the returned object (the branch that sets amount, currency, label) to
parse with a safe default (e.g., use tier.prizeAmount || '0' or check isNaN and
coerce to 0) so amount is always a valid number/string and label never becomes
"NaN USDC"; adjust the code that references tier.prizeAmount, the amount
variable, and the label returned to ensure consistency with
PublishWinnersWizard.tsx's parseFloat(tier.prizeAmount || '0') approach and keep
WinnerCard rendering expectations intact.

In `@hooks/use-hackathon-rewards.ts`:
- Line 57: The arrow function getOrdinalSuffix currently lacks an explicit
return type; update its signature to include a return type annotation (e.g.,
change const getOrdinalSuffix = (i: number) => { ... } to const getOrdinalSuffix
= (i: number): string => { ... }) so the function explicitly returns a string
and matches the project's coding guideline for const arrow functions with
explicit types.

---

Duplicate comments:
In `@hooks/use-hackathon-rewards.ts`:
- Around line 315-316: The code sets pId = sub.projectId || subData.projectId ||
subData.id which wrongly uses submission id (subData.id) as a project id and
leaves a silent inner catch; change the pId computation to only use
sub.projectId or subData.projectId (remove subData.id fallback) and update the
block that calls getCrowdfundingProject(...) to handle failures instead of
swallowing them (e.g., log the error via the existing logger or propagate it) so
getCrowdfundingProject is never invoked with a submission id and errors are
visible; refer to the pId assignment, sub.projectId, subData.projectId,
subData.id, getCrowdfundingProject, and the inner catch when making the changes.
- Around line 321-347: The returned participant object is leaking PII by
assigning creator.email into participant and participant.user; remove the email
properties (delete or stop setting email: creator.email in both the participant
and participant.user blocks) in the mapping that returns {...sub, participant:
{...partData, ...}} so the client-side state no longer receives creator.email;
if the UI truly requires email, wire it behind an explicit opt-in/flag (e.g.,
isEmailAllowed) and only set the fields when that flag is true.
- Around line 191-246: Add a loading state for the hackathon fetch and toggle it
inside fetchHackathon: introduce a new state (e.g., isLoadingHackathon) or reuse
the existing isLoading by setting it true at the start of fetchHackathon and
false in a finally block; ensure fetchHackathon sets isLoadingHackathon (or
shared isLoading) around the try/catch/finally that calls getHackathon and
setHackathon/setPrizeTiers, and expose the new flag from the hook return so
consumers see the correct loading state while
prizeTiers/hackathon/resultsPublished are unresolved (reference functions/state:
fetchHackathon, fetchSubmissions, isLoading, isLoadingHackathon, setPrizeTiers,
setHackathon).
- Around line 409-421: The mapping currently hardcodes maxScore to 100 and
incorrectly builds submissionTitle from result.projectName; change maxScore to
prefer result.maxScore then fall back to sub.maxScore (and a single defined
DEFAULT_MAX_SCORE constant if neither exists) instead of the literal 100, and
change submissionTitle to use result.submissionTitle || sub.submissionTitle
(keeping projectName assignment as-is). Update the mapping in the hook (the
block that returns {...sub, rank: result.rank, score: ..., maxScore: ...,
averageScore: ..., projectName: ..., submissionTitle: ...}) to implement these
fallbacks and add/consume a DEFAULT_MAX_SCORE constant in the module or hook
initializer.

---

Nitpick comments:
In `@components/organization/hackathons/rewards/WinnersGrid.tsx`:
- Around line 41-52: getTierOrder can produce duplicate tiers when the three
available tiers don't have consecutive ranks; change the length===3 branch to
select tiers positionally from sortedTiers (first = sortedTiers[0], second =
sortedTiers[1], third = sortedTiers[2]) and return [second, first, third]
instead of using find(...) with fallbacks, so you preserve all three unique
entries; update the function getTierOrder and references to
sortedTiers/prizeTiers accordingly.

In `@hooks/use-hackathon-rewards.ts`:
- Around line 237-239: The catch block in fetchHackathon silently swallows
errors and resets prize tiers without any logging or user feedback; change the
catch to accept the error (e.g., catch (err)), log the error (console.error or
your app logger) and surface a user-facing message (call toast.error with a
helpful message) before falling back to setPrizeTiers(getDefaultPrizeTiers());
update the fetchHackathon function to mirror the behavior in fetchSubmissions so
failures are diagnosable and users are notified.

Comment on lines +91 to +92
prizeTiers.map(tier => ({
rank: tier.rank,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find and examine rewardsSchema to verify PrizeTier type
fd -t f "rewardsSchema" -e ts -e tsx

Repository: boundlessfi/boundless

Length of output: 133


🏁 Script executed:

#!/bin/bash
# Search for PrizeTier type definition and rank property
rg -n "type PrizeTier|interface PrizeTier|export.*PrizeTier" -A 10

Repository: boundlessfi/boundless

Length of output: 6528


🏁 Script executed:

#!/bin/bash
# Examine the PublishWinnersWizard.tsx file to see the full context
wc -l components/organization/hackathons/rewards/PublishWinnersWizard.tsx

Repository: boundlessfi/boundless

Length of output: 136


🏁 Script executed:

#!/bin/bash
# Check Tailwind version in project config
cat package.json | grep -i tailwind

Repository: boundlessfi/boundless

Length of output: 201


🏁 Script executed:

#!/bin/bash
# Read PublishWinnersWizard.tsx to see actual lines and context
cat -n components/organization/hackathons/rewards/PublishWinnersWizard.tsx | head -130 | tail -70

Repository: boundlessfi/boundless

Length of output: 2456


🏁 Script executed:

#!/bin/bash
# Examine the prizeTierSchema to understand the Zod definition
cat -n components/organization/hackathons/new/tabs/schemas/rewardsSchema.ts

Repository: boundlessfi/boundless

Length of output: 956


Add explicit return type annotation to getPrizeForRank function.

The tier.rank access is safe — verified that PrizeTier includes rank: number in the Zod schema. However, the arrow function at line 99 is missing an explicit return type annotation per coding guidelines for TypeScript files.

Add the return type:

const getPrizeForRank = (rank: number): { amount: string; currency: string; label: string } => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/rewards/PublishWinnersWizard.tsx` around
lines 91 - 92, The getPrizeForRank function is missing an explicit return type;
update its signature to include the specified return type so TypeScript enforces
it (e.g., change the arrow function declaration for getPrizeForRank to accept
rank: number and return { amount: string; currency: string; label: string }).
Locate getPrizeForRank (used alongside prizeTiers.map and the PrizeTier schema)
and add the return type annotation to the function signature exactly as
requested.

Comment on lines +40 to +46
return {
amount,
currency: tier.currency,
label: `${amount} ${tier.currency}`,
};
}
return rank === 1
? '10,000 USDC'
: rank === 2
? '5,000 USDC'
: '8,000 USDC';
return { amount: '0', currency: 'USDC', label: 'No prize configured' };
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Addresses previous review concerns — consistent return shape ✓

Both the found and not-found paths now return { amount, currency, label }, resolving the past comment about amount/currency being undefined on the fallback path.

One remaining inconsistency: line 39 uses parseFloat(tier.prizeAmount) without a || '0' guard, while PublishWinnersWizard.tsx line 102 uses parseFloat(tier.prizeAmount || '0'). If prizeAmount is empty or non-numeric, amount becomes 'NaN', which propagates into the returned label: 'NaN USDC' and ultimately renders as $NaN USDC in WinnerCard (since prizeAmount !== '0' is true for 'NaN').

🛡️ Proposed fix
-      const amount = parseFloat(tier.prizeAmount).toLocaleString('en-US');
+      const amount = parseFloat(tier.prizeAmount || '0').toLocaleString('en-US');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/organization/hackathons/rewards/WinnersPreviewPage.tsx` around
lines 40 - 46, The computed amount can become NaN because
parseFloat(tier.prizeAmount) is called without a fallback; update the logic that
builds the returned object (the branch that sets amount, currency, label) to
parse with a safe default (e.g., use tier.prizeAmount || '0' or check isNaN and
coerce to 0) so amount is always a valid number/string and label never becomes
"NaN USDC"; adjust the code that references tier.prizeAmount, the amount
variable, and the label returned to ensure consistency with
PublishWinnersWizard.tsx's parseFloat(tier.prizeAmount || '0') approach and keep
WinnerCard rendering expectations intact.

};
};

const getOrdinalSuffix = (i: number) => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add explicit return type annotation to getOrdinalSuffix.

-const getOrdinalSuffix = (i: number) => {
+const getOrdinalSuffix = (i: number): string => {

As per coding guidelines, "Prefer const arrow functions with explicit type annotations over function declarations."

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const getOrdinalSuffix = (i: number) => {
const getOrdinalSuffix = (i: number): string => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/use-hackathon-rewards.ts` at line 57, The arrow function
getOrdinalSuffix currently lacks an explicit return type; update its signature
to include a return type annotation (e.g., change const getOrdinalSuffix = (i:
number) => { ... } to const getOrdinalSuffix = (i: number): string => { ... })
so the function explicitly returns a string and matches the project's coding
guideline for const arrow functions with explicit types.

@Benjtalkshow Benjtalkshow merged commit cf34dda into boundlessfi:main Feb 22, 2026
4 of 8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant