Conversation
… mod merge into main
… mod merge into main
… mod merge into main
… organization-fixes merge into main
…into organization-fixes
… hackathon-details fix: merge into main
… hackathon-details Merge into main
… hackathon-analytics merge into main
… hackathon-analytics merge into main
…ouncement features
… judging-dashboard Merge into main
… judging-dashboard Merge into main
… judging-dashboard Merge into main
… judging-dashboard Merge into main
… reward-settings Merge into main
|
@Benjtalkshow is attempting to deploy a commit to the Threadflow Team on Vercel. A member of the Team first needs to authorize it. |
📝 WalkthroughWalkthroughThis 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
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 | 🟡 MinorStale 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
teamMaxNumberInputis missingdisabled={isRegistrationClosed}.
teamMinat line 407 correctly receivesdisabled={isRegistrationClosed}, butteamMaxdoes 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 | 🟡 MinorCustom deadline calendar can still be changed when registration is closed.
The registration-policy buttons are correctly disabled when
isRegistrationClosedistrue, but thePopoverTrigger/Buttonthat opens the calendar date picker has no correspondingdisabledstate. 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
organizationIdis declared but never used in the request URL.Every other scoped function in this file (
addJudge,removeJudge,submitGrade) correctly includesorganizationIdin an/organizations/${organizationId}/hackathons/...pattern. This function calls/hackathons/${hackathonId}/submissionsdirectly, silently droppingorganizationId.Call sites in
hooks/use-hackathon-rewards.tsandapp/(landing)/organizations/[id]/hackathons/[hackathonId]/judging/page.tsxexplicitly passorganizationId, expecting scope enforcement.Either:
- The endpoint genuinely requires no org scoping → remove the
organizationIdparameter 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.tsandlib/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 | 🟠 MajorConsolidate duplicate function —
getJudgingSubmissionsexported identically from bothlib/api/hackathons.tsandlib/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.tsimports from@/lib/api/hackathonswhileapp/(landing)/organizations/[id]/hackathons/[hackathonId]/judging/page.tsximports from@/lib/api/hackathons/judging. Updating one without the other silently diverges behavior for different callers.Keep the implementation in
lib/api/hackathons/judging.tsand either re-export it fromlib/api/hackathons.tsor 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 | 🟡 MinorRemove
@types/uuidfrom devDependencies — it is redundant.
uuid@7+bundles its own TypeScript declarations, making@types/uuidunnecessary. Since the project usesuuid@^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'), anduuid@13is 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 | 🟠 MajorUnsafe
as anycast bypasses type checking for non-existentrankproperty.
PrizeTierdoes not have arankproperty (it hasplace,prizeAmount,description,currency,passMark,id). The cast(tier as any).rank || 0defaults every tier to rank0, causinggetPrizeForRank()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 + 1gives 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: Preferunknownoveranyfor the caught error.Using
catch (error: any)disables all type-checking on the error object. Typing it asunknownand narrowing (or using a type guard / utility likeisAxiosError) 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:onSubmithandler naming.Per coding guidelines, event handlers should start with
handle(e.g.,handleSubmit). WhileonSubmitis conventional in react-hook-form examples, renaming tohandleSubmit(and adjusting the form binding toform.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: Preferconstarrow 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: preferhandleprefix.
onSubmitreads like a callback prop name. Rename to e.g.handleSaveto follow the project convention and avoid confusion withform.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 theuseEffectreset (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
DateTimePickerFieldcomponent 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: Preferunknownoveranyin catch blocks.Using
error: anybypasses type safety. Useunknownand 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:onEditprop name andfunctiondeclaration style
onEditprop: Per coding guidelines, event handlers should start withhandle. The prop should beonEdit→handleEdit(and updated at the call-site).Component declaration: The component uses
export default functioninstead of aconstarrow function with an explicit type annotation, which is the preferred style for.tsxfiles.♻️ 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 anonymousonClickto a namedhandleToggleExpandhandlerThe inline arrow on
onClickis an anonymous function with no named handler, violating the coding guideline that event handlers must start with thehandleprefix.♻️ 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:statusvariable at line 74 is shadowed inside the.map()callback at line 293.At line 74,
statusis'upcoming' | 'ongoing' | 'ended'(hackathon lifecycle). Inside the.map()at line 293, a newconst statusis declared with type'Approved' | 'Rejected' | 'Pending'. Both scopes work correctly, but the name collision is confusing and will be flagged byeslint/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 inSubmissionTabContent.
SubmissionTab(line 400) already holdscurrentHackathonand could passisDeadlinePassed(or the raw date fields) as a prop toSubmissionTabContent, eliminating the extra context read and keepingSubmissionTabContent'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
SubmissionTabContententirely.🤖 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
ifbody 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 withhandle;onSubmitshould behandleSubmit(orhandleFormSubmitto avoid shadowingform.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
onSubmitprop 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 insideonClickis 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:initialDatareference instability can silently discard in-progress edits.If the parent component does not stabilise the
initialDataobject reference (e.g., viauseMemo), every parent re-render produces a new object reference and triggersform.reset(), discarding whatever the user has typed. Consider comparing relevant fields before resetting, or document the expectation that callers must memoizeinitialData.♻️ Suggested guard
React.useEffect(() => { if (initialData) { - form.reset(initialData); + form.reset(initialData, { keepDirtyValues: true }); } }, [initialData, form]);
keepDirtyValues: truepreserves 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:NumberInputis defined inside the component body — move it out.Because
NumberInputis declared as a new function reference insideParticipantTab'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 itsNumberInputPropsinterface) above or outsideParticipantTab:♻️ 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
Calendaricon is already used for the active Register/Join button, making it ambiguous in the disabled "Hackathon Ended" context. An icon likeCheckCircleorClockwould 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
CheckCircleto 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 number1e7for 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:fetchHackathonmissing fromuseEffectdependency array.
fetchHackathonis referenced inside the effect but not listed in the dependency array. While this works becausehackathonIdchanges trigger a re-render, ESLint'sreact-hooks/exhaustive-depsrule will flag this. WrapfetchHackathoninuseCallbackwith[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: Pervasiveas anycasts in mapping helpers erode type safety.The mapping functions (
getGeneralData,getTimelineData,getParticipantData,getAdvancedData) and theinitialDataprops (lines 267, 307, etc.) heavily rely onas anyto bridge the gap betweenHackathonfields 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 anyonvenueType(line 70) andparticipantType(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:sessionStorageaccess outside ofuseEffect.Reading/writing
sessionStoragein the component body (outsideuseEffector a callback) is a side effect during render. While theif (!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
publishWinnersitself (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:getTierOrderfallback 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). SincetiersWithWinnerscan contain any ranks, the fallback is a reasonable safety measure.However, note that
getTierOrderis not memoized despite receiving a new array reference on every render (from the memoizedtiersWithWinners). Consider wrapping it withuseMemoor 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:escrowprop is accepted but never used.After removing the milestone/wallet gating logic,
escrowis no longer referenced anywhere in the hook body. Consider removing it fromUseWizardStepsPropsand 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:canGoNextis hardcoded totrue— consider removing it.Since
canGoNextis alwaystrueand 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 fromWizardFooter.🤖 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: Usecn()for merging conditional classes.Line 128 concatenates
badge.colorvia a template literal. As per coding guidelines, preferclsxor a similar helper for conditional classes. Thecnutility 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 callsonPublishClick.
onPublishClickopens thePublishWinnersWizard, 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 likeonTriggerDistributionfor 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_BADGEis 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
useMemoto 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.
getRewardDistributionStatusandtriggerRewardDistributionuseres.data?.data ?? res.datato handle both wrapped and unwrapped responses, while every other API function in this file simply returnsres.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:walletAddresseshas a plural name but singularstringtype.If a winner can have multiple wallet addresses, this should be
string[]. If only one address is expected, consider renaming towalletAddressfor 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: Pervasiveas anycasts defeat the purpose of typed parameters.The function signature accepts
JudgingSubmission, but line 10 immediately casts it toany, 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:
- Defining a broader union/intersection type that captures the actual shapes you encounter, or
- 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: Useunknowninstead ofanyfor the caught error type.
catch (err: any)bypasses type checking. Usingcatch (err: unknown)and narrowing with type guards (err instanceof Error, checking forresponseproperty) 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.
| const { | ||
| distributionStatus, | ||
| isLoading: isLoadingDistributionStatus, | ||
| refetch: refetchDistributionStatus, | ||
| } = useRewardDistributionStatus(organizationId, hackathonId); |
There was a problem hiding this comment.
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.
app/(landing)/organizations/[id]/hackathons/[hackathonId]/settings/page.tsx
Show resolved
Hide resolved
| 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, | ||
| }} | ||
| /> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find the useMarkdown hook
fd -type f -name "*markdown*" -o -name "*use-markdown*" | head -20Repository: 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 -20Repository: 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 -30Repository: boundlessfi/boundless
Length of output: 92
🏁 Script executed:
#!/bin/bash
# Get the full AnnouncementSection.tsx file to review
cat components/organization/hackathons/rewards/AnnouncementSection.tsxRepository: 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 -printRepository: boundlessfi/boundless
Length of output: 88
🏁 Script executed:
#!/bin/bash
# Search for useMarkdown definition
rg "useMarkdown" -A 15 | head -50Repository: 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 -10Repository: boundlessfi/boundless
Length of output: 131
🏁 Script executed:
#!/bin/bash
# Read the full use-markdown.ts hook
cat hooks/use-markdown.tsRepository: 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" -iRepository: 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.tsRepository: 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.
There was a problem hiding this comment.
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
teamMaxNumberInput is not disabled whenisRegistrationClosedistrue.
teamMinreceiveddisabled={isRegistrationClosed}(line 415) butteamMaxwas 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 | 🟠 MajorCustom deadline calendar remains interactive when
isRegistrationClosedistrue.The
PopoverTriggerButton has nodisabledprop andonSelecthas noisRegistrationClosedguard. IfregistrationDeadlinePolicyis 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
onSelectcallback:- 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 | 🟡 MinorTab Visibility switches are not disabled when
isRegistrationClosedistrue.All other interactive controls (participant type, deadline policy, submission requirement switches, number inputs) are gated on
isRegistrationClosed, but the Tab VisibilitySwitchcomponents 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 | 🟡 MinorRemove unused
organizationId/hackathonIdprops and fix the function declaration.Two issues here:
organizationIdandhackathonIdare declared inGeneralSettingsTabPropsand 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.
export default function GeneralSettingsTab(...)is a function declaration. As per coding guidelines,constarrow 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: Preferconstarrow function with explicit type annotation.Per the coding guidelines for
*.tsxfiles, preferconstarrow functions with explicit type annotations overfunctiondeclarations.♻️ 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:initialDatareference equality may cause unintended form resets.If the parent component re-creates the
initialDataobject on each render (e.g. inline literal or un-memoized transformation), this effect fires every render and callsform.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(viauseMemo).🤖 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: Avoiderror: anyin catch — useunknownwith 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 toerror.responseetc. is silentundefinedchaining 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: Preferunknownoveranyfor the caught error.Using
unknownforces 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.isDirtyto 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 reusableDateTimePickerFieldto 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: preferconstarrow function with explicit return type.The component uses a
functiondeclaration. Per project conventions, prefer aconstarrow 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: Pervasiveanycasts throughout the enrichment pipeline.Lines 264, 276–277, 286, 290–291, 349, 388 all use
anyto 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:JudgingResulttype is imported but not referenced in the code.The
resultsListon line 384 is not annotated withJudgingResult[]. 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
getHackathonthrows, 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(andNumberInputProps) should be defined outsideParticipantTab.Defining a component inside another component creates a new function reference on every render, causing React to unmount and remount the
NumberInputsubtree each timeParticipantTabre-renders (e.g., onform.watchupdates). 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: Preferunknownoveranyfor the caught error.
error: anybypasses type-safety. Usingunknownand 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:getTierOrderis recomputed on every render — consider memoizing or inlining.
getTierOrderis called withtiersWithWinners(which is memoized), but the function itself is recreated each render. Since it's called once to producetiersToDisplay, consider wrapping the result inuseMemo: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 aconstarrow function per coding guidelines.Same pattern as the other files — prefer
constarrow with explicit typing overfunctiondeclaration.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:getPrizeForRankis not memoized and recreated on every render.This function is passed as a prop to
PreviewStep→WinnersGrid, causing unnecessary re-renders of memoized children. Wrap it withuseCallbacksince 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:canGoNextis hardcoded totrue— is this intentional?This is passed to
WizardFooterbut 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 passtruedirectly 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 aconstarrow function with explicit type annotations.The coding guidelines prefer
constarrow 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 aconstarrow 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 aconstarrow 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!isLoadingbut 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
!isLoadingDistributionStatusfor 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
fetchStatusharder to scan. A top-levelconst 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: Preferclsx(orcn) for conditional class composition.Lines 150 and 158 use template literal ternaries for class assembly. Per project coding guidelines, conditional classes should use
clsxor 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 patternres.data?.data ?? res.datais fragile.Both
getRewardDistributionStatusandtriggerRewardDistributionuseres.data?.data ?? res.datato handle two potential response shapes. Other API functions in this file (e.g.,getHackathonEscrowat line 1590,assignRanksat line 1499) returnres.datadirectly. 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.datafallback could return the wrapper object instead of the payload.Consider aligning these with the rest of the file (using
res.datadirectly) 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: HardcodeddaysUntilStart/daysUntilEndwill drift over time.
hackathon-2uses 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 forhackathon-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:readOnlyis redundant alongsidedisabledon the slug input.
disabled={isPublished}already prevents all interaction (focus, keyboard, click).readOnly={isPublished}adds no additional protection and is noise. Keepdisabledonly, orreadOnlyonly if you want the value to remain in form submission (note:disabledfields are excluded from native form submissions, but since the value is controlled viareact-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: RenameonSubmittohandleSubmitto comply with the event-handler naming convention.As per coding guidelines, event handlers should start with the
handleprefix.onSubmitis used directly as an event-handler callback viaform.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 —getParticipantDataalready handlesnull.
getParticipantDatareturnsundefinedwhen called withnull(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 aconstarrow function instead of a function declaration.
export default function SettingsPage()is a function declaration. As per coding guidelines, TypeScript/TSX files should preferconstarrow 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: WrapfetchHackathoninuseCallbackto satisfy exhaustive-deps and avoid future stale-closure bugs.
fetchHackathonis recreated on every render and omitted from theuseEffectdependency 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 becausehackathonId(whichfetchHackathoncloses over) is already in deps, the linter will flag it and any future closure over additional state would silently become stale.Also consolidate the
useEffectimport (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: Replaceas anycasts with proper types in mapping functions.
as anyat lines 70, 97, and 104 silently discards TypeScript's type checking on the mapped data, andas anyon tabinitialDataprops (lines 267, 306, 325) extends that blindspot to the child components. Mismatches between theHackathonAPI shape and the form*FormDatatypes should be resolved by aligning the types or usingas unknown as TargetTypewith explicit intermediate checks, not by widening toany.🤖 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.
| if (isLoading) return <Loading />; | ||
|
|
||
| return ( | ||
| <AuthGuard redirectTo='/auth?mode=signin' fallback={<Loading />}> |
There was a problem hiding this comment.
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:
fetchHackathon()runs (and fails with an auth error) via theuseEffect.- A brief
<Loading />flash appears. - 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.
| 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(), |
There was a problem hiding this comment.
.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.
| 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.
| .regex( | ||
| /^[a-z0-9-]+$/, | ||
| 'Slug can only contain lowercase letters, numbers, and hyphens' | ||
| ) |
There was a problem hiding this comment.
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.
| .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.
| prizeTiers.map(tier => ({ | ||
| rank: (tier as any).rank || 0, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd "rewardsSchema" -e ts -e tsxRepository: boundlessfi/boundless
Length of output: 133
🏁 Script executed:
rg "interface PrizeTier|type PrizeTier" -t ts -t tsxRepository: boundlessfi/boundless
Length of output: 92
🏁 Script executed:
rg "PrizeTier" -t ts -t tsx -C 3Repository: boundlessfi/boundless
Length of output: 92
🏁 Script executed:
cat -n components/organization/hackathons/rewards/PublishWinnersWizard.tsx | head -120Repository: boundlessfi/boundless
Length of output: 4056
🏁 Script executed:
cat -n components/organization/hackathons/new/tabs/schemas/rewardsSchema.tsRepository: boundlessfi/boundless
Length of output: 916
🏁 Script executed:
rg "PrizeTier" --type ts | head -30Repository: 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 2Repository: boundlessfi/boundless
Length of output: 11600
🏁 Script executed:
rg "rank:" --type ts hooks/use-publish-winners.ts -B 3 -A 3Repository: 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:
- Adding
rank: number;to thePrizeTierinterface inrewardsSchema.ts - 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.
| 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.
| 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); | ||
| } |
There was a problem hiding this comment.
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.
| 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]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n -C5 'TimelineSettingsTab' --type=tsx --type=ts --type=jsxRepository: boundlessfi/boundless
Length of output: 92
🏁 Script executed:
rg -n -C5 'TimelineSettingsTab' --type=typescriptRepository: boundlessfi/boundless
Length of output: 99
🏁 Script executed:
rg 'TimelineSettingsTab' -C5Repository: 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:
- Wrapping
initialData={getTimelineData(hackathon)}inuseMemoin the parent, or - Replacing the
initialDatadependency with a stable key (e.g.,hackathon?.idor 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.
| 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]); |
There was a problem hiding this comment.
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.
| 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, | ||
| }, | ||
| }, | ||
| }, | ||
| }; |
There was a problem hiding this comment.
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.
| 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; | ||
| }); |
There was a problem hiding this comment.
Hardcoded maxScore: 100 and submissionTitle uses projectName as source.
-
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. -
submissionTitle(Line 406) is set toresult.projectName, which is the same value used forprojectNameon 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.
There was a problem hiding this comment.
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
canGoNextis unconditionallytrue— the wizard step guard has no effect.With
canGoNext = true, a user can advance from theAnnouncementSteptoPreviewStepregardless 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
rankvalues desync from visual position after drag-and-drop.
move(oldIndex, newIndex)reorders thefieldsarray but leaves each tier's storedrankvalue untouched. After a drag,onSavereceives tiers whoserankfields no longer match their array positions. If the backend usesrankfor prize distribution (which the PR summary explicitly states), winners receive incorrect prizes.The same root cause applies to
handleRemove(remaining ranks have gaps) andhandleAdd(fields.length + 1produces 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
onSubmitbefore delegating toonSave:🐛 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:PublishWinnersWizardshould be aconstarrow function with an explicit type annotation.Per the coding guideline for
.tsxfiles, preferconstarrow functions with explicit type annotations overfunctiondeclarations.♻️ 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: Preferclsxover ternary inclassNamefor conditional classes.The inline ternary
${isLoading ? 'animate-spin' : ''}inside a template literal violates the project guideline. Useclsxinstead.♻️ 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: Preferunknownoveranyfor caught errors.
catch (error: any)bypasses type checking.unknownwith 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: Useslice(2, 11)instead ofsubstr(2, 9)—substris a deprecated legacy feature.
String.prototype.substris defined in ECMAScript Annex B (non-core features) and marked deprecated by MDN. Replace withslice(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:getOrdinalSuffixis 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.
| const pId = sub.projectId || subData.projectId || subData.id; | ||
| if (pId) { |
There was a problem hiding this comment.
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).
| } catch { | ||
| return { __html: dirty }; | ||
| return { __html: '' }; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n lib/utils/renderHtml.tsRepository: boundlessfi/boundless
Length of output: 1218
🏁 Script executed:
rg -n "isomorphic-dompurify|jsdom|DOMPurify" --type ts --type tsx -C2Repository: 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 -C2Repository: boundlessfi/boundless
Length of output: 47
🏁 Script executed:
rg "console\.error|logger\." -t ts --max-count=20Repository: boundlessfi/boundless
Length of output: 8375
🏁 Script executed:
cat package.json | grep -E "jsdom|dompurify" | head -20Repository: 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.
| } 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.
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (2)
hooks/use-hackathon-rewards.ts (1)
237-239: SilentcatchinfetchHackathondrops errors without logging or notifying the user.
fetchSubmissionscallstoast.erroron failure (Line 438), butfetchHackathon'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:getTierOrdercan produce duplicate tiers for non-consecutive rank sets.When
sortedTiers.length === 3but the three winner ranks are not exactly{1, 2, 3}(e.g., ranks1, 3, 4),find(t => t.rank === 3)returnssortedTiers[1](rank 3) whilefind(t => t.rank === 2)falls back tosortedTiers[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.
| prizeTiers.map(tier => ({ | ||
| rank: tier.rank, |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Find and examine rewardsSchema to verify PrizeTier type
fd -t f "rewardsSchema" -e ts -e tsxRepository: 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 10Repository: 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.tsxRepository: boundlessfi/boundless
Length of output: 136
🏁 Script executed:
#!/bin/bash
# Check Tailwind version in project config
cat package.json | grep -i tailwindRepository: 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 -70Repository: 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.tsRepository: 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.
| 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' }; |
There was a problem hiding this comment.
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) => { |
There was a problem hiding this comment.
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.
| 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.
Summary by CodeRabbit
New Features
Improvements
Refinements