Skip to content

Comments

Judging dashboard#382

Merged
Benjtalkshow merged 105 commits intoboundlessfi:mainfrom
Benjtalkshow:judging-dashboard
Feb 16, 2026
Merged

Judging dashboard#382
Benjtalkshow merged 105 commits intoboundlessfi:mainfrom
Benjtalkshow:judging-dashboard

Conversation

@Benjtalkshow
Copy link
Collaborator

@Benjtalkshow Benjtalkshow commented Feb 15, 2026

Judging System Refinements & Competition Finalization Workflow

I have completed the refinements to the judging system and implemented the full competition finalization workflow, including winner retrieval and results publishing.


Key Changes

Advanced Leaderboard Visualization

  • Automatic Sorting
    JudgingResultsTable now automatically sorts projects by averageScore in descending order, providing an instant and accurate view of competition standings.

  • Participation Tracking
    Added a Participation column showing judgeCount / totalJudges for each submission.
    A green success badge appears once all assigned judges have completed their evaluations.

  • Improved Hierarchy
    Enhanced rank display and project naming for better scannability and visual clarity.


Enhanced Individual Score Breakdowns

  • Visual Criteria Scores
    Replaced plain text scores with color-coded progress bars (0–10) for each evaluation criterion, making score interpretation more intuitive at a glance.

  • Expert Feedback Highlighting
    The global judge comment is now prominently displayed as "Expert Feedback" at the top of each judge’s breakdown.

  • Scoring Discrepancy Detection
    Implemented a deviation-highlighting system that flags judge scores that differ by more than 2 points from the submission’s average.
    A pulse badge alerts organizers to potential Scoring Discrepancies for review.


Competition Finalization Workflow

  • Publish Results
    Added a dedicated "Publish Results" action within the Results tab.
    This triggers the publishJudgingResults API to finalize competition rankings.

  • Permission Control
    Only users who are:

    • Admins/Owners
      AND
    • Assigned as judges

    can publish results — ensuring only active judging participants can finalize the competition.

  • Winners Display
    Automatically fetches and displays finalized winners using the getJudgingWinners API.
    Winners appear in a dedicated "Final Winners" section with a trophy icon.

  • Contextual UI Improvements

    • Relocated publish action from the header to the Results tab for better workflow alignment.
    • Publish button appears only:
      • For authorized users
      • When there are results ready to be published
  • Auto-Refresh Behavior
    After publishing, the dashboard automatically refreshes:

    • Current standings
    • Finalized winners

    This ensures the UI reflects the latest competition state immediately.


Implementation Details

  • judging.ts

    • Added getJudgingWinners
    • Added publishJudgingResults
  • JudgingResultsTable.tsx

    • Added useMemo for stable and robust sorting
    • Integrated participation badges
  • IndividualScoresBreakdown.tsx

    • Implemented deviation-based discrepancy detection logic
    • Replaced text scores with visual progress bars
  • JudgingPage.tsx

    • Integrated publishing and winners logic
    • Refined Results tab layout to display both finalized winners and current standings

Outcome

This PR significantly improves:

  • Clarity of competition standings
  • Transparency in judge scoring
  • Detection of inconsistent evaluations
  • Workflow for finalizing and publishing results
  • Overall organizer experience

The judging system is now more visual, more robust, and fully supports end-to-end competition finalization.

Summary by CodeRabbit

  • New Features

    • Hackathon announcements: public announcement pages, announcements tab, and full create/edit/delete/publish workflow
    • Expanded judging tools: multi-tab judging dashboard, criteria management, judge management, results/winners, per-judge breakdowns, and inline scoring comments
    • Profile follow/unfollow and real-time social link validation
    • Mobile-friendly sidebars and improved navigation
  • Bug Fixes

    • Consolidated error handling and clearer user feedback across profiles, organizations, and hackathons
    • Ownership transfer and member-management prompts made more robust
  • UI/UX Enhancements

    • Participant type indicators (Individual/Team/Hybrid) and refreshed countdown/status displays

@vercel
Copy link

vercel bot commented Feb 15, 2026

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

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Feb 15, 2026

📝 Walkthrough

Walkthrough

Adds announcements (public detail page, admin CRUD, API), a major judging redesign (criteria, per-judge scores, judge management, results/winners), new link-validation hook, mobile-friendly sidebars, participant-type UI, scoring refactors, and multiple supporting type/API changes.

Changes

Cohort / File(s) Summary
Announcements (public + admin + API)
app/(landing)/hackathons/[slug]/announcements/[announcementId]/page.tsx, app/(landing)/organizations/[id]/hackathons/[hackathonId]/announcement/page.tsx, components/hackathons/announcements/AnnouncementsTab.tsx, lib/api/hackathons/announcements.ts, lib/api/hackathons/index.ts, lib/api/types.ts
New public announcement detail page, admin CRUD editor (create/update/delete/publish), announcements list tab UI with sorting/pinning, and API wrappers/types for announcements.
Hackathon landing & tabs
app/(landing)/hackathons/[slug]/page.tsx
Fetches published announcements, exposes Announcements tab with badge/count and renders AnnouncementsTab.
Judging redesign (pages, components, API & types)
app/(landing)/organizations/[id]/hackathons/[hackathonId]/judging/page.tsx, components/organization/hackathons/judging/..., components/organization/cards/JudgingParticipant.tsx, components/organization/cards/JudgingParticipant/IndividualScoresBreakdown.tsx, components/organization/cards/GradeSubmissionModal/*, lib/api/hackathons/judging.ts, lib/api/hackathons.ts, types/hackathon/core.ts
Large judging workflow: tabbed dashboard, parallel data loading, judge add/remove APIs, per-judge scoring and comments, results/winners endpoints, criteria list/table, individual scores breakdown, grading modal and many type/API surface changes.
Scoring form & hooks
components/organization/cards/GradeSubmissionModal/useScoreForm.ts, useSubmissionScores.ts, useScoreCalculation.ts, useJudgingCriteria.ts
Switched from title-based keys to derived criterion keys (id/name/title), added per-criterion comments and overallComment, score scale normalized to 0–10, submitJudgingScore integration, and session-aware initialization.
Organization & members management
components/organization/tabs/MembersTab.tsx, .../MemberCard.tsx, .../TeamManagementSection.tsx, components/organization/OrganizationSidebar.tsx, lib/providers/OrganizationProvider.tsx, lib/api/organization.ts
Role/permission checks and owner transfer changes, canManage gating for member controls, mobile-friendly OrganizationSidebar extraction with Sheet, and new getOrganizationMembers API.
Link validation & LinksTab
hooks/use-link-validation.ts, components/organization/tabs/LinksTab.tsx
New useLinkValidation hook (website/X/GitHub validators), debounced per-field validation state, UI indicators, and save-blocking while validating.
Sidebars & mobile UX
components/organization/hackathons/details/HackathonSidebar.tsx, components/organization/hackathons/new/NewHackathonSidebar.tsx, components/organization/OrganizationSidebar.tsx
Extracted sidebar content components and added Sheet-based mobile drawers to unify mobile/desktop sidebar UX.
Hackathon UI surfaces & submissions
components/hackathons/hackathonBanner.tsx, components/hackathons/hackathonStickyCard.tsx, components/hackathons/submissions/SubmissionForm.tsx
Exposed participantType prop to banner/sticky card (shows Hybrid/lowercase), integrated CountdownTimer, and added participation-type enforced flows in submission UI.
Profile, follow & small text changes
components/hackathons/participants/profileCard.tsx, features/projects/.../Contact.tsx, app/me/crowdfunding/.../ContactSocialSection.tsx, components/hackathons/HackathonsPage.tsx
Profile caching, follow/unfollow and follow-stats added; minor copy edits ("Boundless team"); small empty-state text tweak.
Timer & misc hooks
components/ui/timer.tsx, hooks/hackathon/use-team-posts.ts
CountdownTimer gained displayClassName prop; team-posts hook improves error typing/message extraction.
Project layout / backers
app/(landing)/projects/[slug]/page.tsx, components/project-details/project-backers/Empty.tsx, components/project-details/project-layout.tsx
hiddenTabs now always passed as empty array to ProjectLayout; backers empty-state conditionalized for submission mode; visibleTabs filtered by hiddenTabs.
Scoring UI public API changes
components/organization/cards/GradeSubmissionModal/ScoringSection.tsx
ScoringSection props updated to use criterionKey-based handlers, added per-criterion comments and overallComment — public API signatures changed.
Miscellaneous refactors & error handling
app/(landing)/organizations/layout.tsx, components/organization/hackathons/new/*, components/organization/tabs/*, hooks/*
Conditional layout padding, publish/error handling tweaks, numerous refactors across organization/hackathon tabs and hooks.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client/Browser
    participant HackathonPage as Hackathon Page
    participant HackathonAPI as Hackathon API
    participant AnnouncementAPI as Announcement API

    Client->>HackathonPage: navigate to hackathon slug
    HackathonPage->>HackathonAPI: fetch hackathon by slug
    HackathonPage->>AnnouncementAPI: listAnnouncements(hackathonId)
    HackathonAPI-->>HackathonPage: hackathon data
    AnnouncementAPI-->>HackathonPage: announcements[]
    HackathonPage->>HackathonPage: filter published, compute tab badge
    Client->>HackathonPage: click announcement link
    HackathonPage->>AnnouncementAPI: getAnnouncementDetails(announcementId)
    AnnouncementAPI-->>Client: announcement with markdown
    Client->>Client: render announcement page, convert markdown -> HTML
Loading
sequenceDiagram
    participant Judge as Judge UI
    participant JudgingPage as Judging Dashboard
    participant JudgingAPI as Judging API
    participant SubmissionAPI as Submission API
    participant GradeModal as Grade Modal

    Judge->>JudgingPage: open judging dashboard
    JudgingPage->>SubmissionAPI: getJudgingSubmissions(status=SHORTLISTED)
    JudgingPage->>JudgingAPI: getJudgingCriteria(hackathon), getHackathonJudges, getJudgingResults
    SubmissionAPI-->>JudgingPage: submissions[]
    JudgingAPI-->>JudgingPage: criteria, judges, results
    Judge->>JudgingPage: open participant -> GradeModal
    GradeModal->>JudgingAPI: getSubmissionScores(submissionId)
    JudgingAPI-->>GradeModal: IndividualJudgeScore[] (existing scores)
    Judge->>GradeModal: enter per-criterion scores & comments
    GradeModal->>JudgingAPI: submitJudgingScore({ submissionId, criteriaScores, comment })
    JudgingAPI-->>JudgingPage: success -> refresh results
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • 0xdevcollins

Poem

🐰 I hopped through tabs and API streams,

Pinned notes and judges, bright as beams.
Scores and comments, sidebars that slide,
Links checked, timers ticking—what a ride.
A carrot-coded cheer for this deploy! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.07% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'Judging dashboard' is concise but vague. It refers to the general feature area but does not clearly convey the primary changes: implementing a complete judging/competition finalization workflow with results publishing, winner retrieval, discrepancy detection, and visual enhancements. Consider a more specific title that captures the main objective, such as 'Implement judging dashboard with results publishing and discrepancy detection' or 'Add competition finalization workflow with judging results and winners.'
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Merge Conflict Detection ✅ Passed ✅ No merge conflicts detected when merging into main

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

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

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

⚠️ Outside diff range comments (19)
app/(landing)/projects/[slug]/page.tsx (1)

115-121: ⚠️ Potential issue | 🟡 Minor

Duplicate notFound() guard.

The identical if (error || !project) { notFound(); } check appears twice in a row (lines 115–117 and 119–121). Remove one of them.

Proposed fix
   if (error || !project) {
     notFound();
   }
 
-  if (error || !project) {
-    notFound();
-  }
-
   return (
components/hackathons/participants/profileCard.tsx (2)

120-124: ⚠️ Potential issue | 🟠 Major

Remove debug console.log statements before merging.

Lines 120–124 and 154–161 contain [DEBUG] console logs that leak internal state (usernames, user IDs, roles) to the browser console. These should be removed or replaced with a proper dev-only logger to avoid leaking information in production.

Also applies to: 154-161


211-211: ⚠️ Potential issue | 🟡 Minor

Non-null assertion on participant.joinedDate! risks a runtime crash.

If joinedDate is undefined or null, formatJoinDate will receive undefined, and new Date(undefined) produces an Invalid Date, rendering "undefined undefined" in the UI. Guard against this or provide a fallback.

🛡️ Proposed fix
-            Joined {formatJoinDate(participant.joinedDate!)}
+            {participant.joinedDate
+              ? `Joined ${formatJoinDate(participant.joinedDate)}`
+              : null}
components/organization/OrganizationSidebar.tsx (1)

229-236: ⚠️ Potential issue | 🟠 Major

"Host Hackathon" link navigates to /organizations/undefined/hackathons/new when derivedOrgId is absent.

Unlike the menuItems which guard against a missing derivedOrgId (falling back to '#'), the "Host Hackathon" quick action unconditionally interpolates derivedOrgId into the href. When the org ID can't be derived, this produces a broken URL.

🐛 Proposed fix
     {
       icon: Plus,
       label: 'Host Hackathon',
-      href: `/organizations/${derivedOrgId}/hackathons/new`,
+      href: derivedOrgId ? `/organizations/${derivedOrgId}/hackathons/new` : '#',
       gradient: 'from-primary/20 to-primary/5',
       disabled: false,
     },
components/organization/hackathons/details/HackathonSidebar.tsx (1)

221-265: 🛠️ Refactor suggestion | 🟠 Major

Move the hackathons prop check to the top of the memo for an early return.

Line 263 checks if (hackathons.length > 0) return hackathons only after iterating over apiHackathons and drafts, discarding all that work. Moving this to the top avoids unnecessary computation and improves clarity.

Proposed fix
 const hackathonData = useMemo<Hackathon[]>(() => {
+  if (hackathons.length > 0) return hackathons;
+
   const items: Hackathon[] = [];
   apiHackathons.forEach(hackathon => {
     // ...
   });
   drafts.forEach(draft => {
     // ...
   });
-  if (hackathons.length > 0) return hackathons;
   return items;
 }, [drafts, apiHackathons, derivedOrgId, hackathons]);

As per coding guidelines, "Use early returns to improve code clarity."

hooks/hackathon/use-team-posts.ts (1)

118-139: ⚠️ Potential issue | 🟡 Minor

Pass organizationId to getMyTeam call.

getMyTeam accepts an optional organizationId parameter (per API definition) and uses it to determine the endpoint: with organizationId it fetches /organizations/${organizationId}/hackathons/${hackathonSlugOrId}/my-team, without it fetches /hackathons/${hackathonSlugOrId}/my-team. Currently, organizationId is in the dependency array but never passed to the function, leaving the code unable to fetch organization-scoped team data. Either pass organizationId to the call and keep it in dependencies, or remove it entirely if organization context is not needed.

Proposed fix
-      const response = await getMyTeam(hackathonSlugOrId);
+      const response = await getMyTeam(hackathonSlugOrId, organizationId);
components/organization/tabs/MembersTab.tsx (2)

200-211: ⚠️ Potential issue | 🟠 Major

Permission mismatch: admins see the Remove button but the handler rejects them.

canManageTeam (line 403-405) is true for both owner and admin, so admins will see the remove-member button via MemberCard. However, handleRemoveMember (line 207-210) re-checks ownership and rejects non-owners with "Only organization owners can remove members".

Either restrict canManageTeam to owners only (for the remove action), or update handleRemoveMember to also allow admins. As-is, this is a confusing UX where the button is visible but always fails for admins.

Also applies to: 403-405


163-175: ⚠️ Potential issue | 🟡 Minor

setInviteEmails([]) and setEmailInput('') should be moved after the loop.

These calls are inside the for loop, so they execute on every iteration. While React batches state updates and the closure captures the original inviteEmails array so the loop still completes, the intent is clearly to clear inputs once after all invitations are sent.

♻️ Suggested fix
       for (const email of inviteEmails) {
         const { error } = await authClient.organization.inviteMember({
           email,
           role: 'member',
           organizationId: activeOrgId,
           resend: true,
         });
         if (error) {
           toast.error(error.message);
         }
-        setInviteEmails([]);
-        setEmailInput('');
       }
+      setInviteEmails([]);
+      setEmailInput('');
       await fetchInvitations();
components/organization/tabs/MembersTab/TeamManagementSection.tsx (1)

48-64: ⚠️ Potential issue | 🔴 Critical

Bug: orgOwner.image is always undefined — should be orgOwner.avatar.

The data transform in MembersTab.tsx (line 102) populates only the avatar field (avatar: member.user.image), but TeamManagementSection.tsx line 51 references orgOwner.image, which is never set. This causes the owner's avatar to never render and fall back to the initials.

🐛 Proposed fix
-              <AvatarImage src={orgOwner.image} alt={orgOwner.name} />
+              <AvatarImage src={orgOwner.avatar} alt={orgOwner.name} />
components/organization/hackathons/new/NewHackathonTab.tsx (1)

85-85: ⚠️ Potential issue | 🟡 Minor

Remove debug console.log statement.

'sjcdkformData' looks like an accidental debug artifact left from development. This will clutter the console in production.

🧹 Proposed fix
-      console.log('sjcdkformData', formData);
components/organization/cards/GradeSubmissionModal/useJudgingCriteria.ts (1)

31-49: ⚠️ Potential issue | 🔴 Critical

Potential infinite fetch loop when the API returns an empty array.

localCriteria is listed in the dependency array (Line 49). If getJudgingCriteria returns [], the state is set to a new empty array reference, the effect re-triggers, and the guard localCriteria.length === 0 (Line 36) is still true — causing an unbounded loop of API calls.

Consider introducing a hasFetched ref to break the cycle:

🐛 Proposed fix
+  const hasFetchedRef = useRef(false);
+
   useEffect(() => {
     if (
       open &&
       organizationId &&
       hackathonId &&
-      (!localCriteria || localCriteria.length === 0)
+      (!localCriteria || localCriteria.length === 0) &&
+      !hasFetchedRef.current
     ) {
+      hasFetchedRef.current = true;
       setIsFetchingCriteria(true);
       getJudgingCriteria(hackathonId)
         .then((criteria: JudgingCriterion[]) => {
           setLocalCriteria(Array.isArray(criteria) ? criteria : []);
         })
-        .catch(() => {})
+        .catch(() => {
+          // Reset so a retry is possible on next open
+          hasFetchedRef.current = false;
+        })
         .finally(() => {
           setIsFetchingCriteria(false);
         });
     }
-  }, [open, organizationId, hackathonId, localCriteria]);
+  }, [open, organizationId, hackathonId, localCriteria]);
lib/api/hackathons/core.ts (1)

119-124: ⚠️ Potential issue | 🟡 Minor

Use generic type parameter instead of unsafe as cast, and fix function naming.

getHackathon and GetHackathonBySlug use unsafe type casts (as GetHackathonResponse) without providing a generic type to api.get(). This reduces type safety compared to the pattern used elsewhere (e.g., line 272 with api.get<PublicHackathonsListResponse>(url)).

Provide the generic type parameter: api.get<GetHackathonResponse>(...) instead of casting the result. Also, rename GetHackathonBySlug to getHackathonBySlug to follow the function naming convention (PascalCase is for components/classes).

components/organization/tabs/TransferOwnershipTab.tsx (2)

242-246: ⚠️ Potential issue | 🟡 Minor

Remove debug info before shipping.

This "Available members" counter with loading indicator looks like development-time debugging. It leaks internal state to users and clutters the UI.

🧹 Proposed fix
-          {/* Debug info */}
-          <div className='mt-2 text-xs text-gray-400'>
-            Available members: {members.length}{' '}
-            {loadingMembers && '(Loading...)'}
-          </div>

109-109: ⚠️ Potential issue | 🟠 Major

createdAt.toISOString() may throw at runtime.

BetterAuthMember.createdAt is typed as Date, but JSON deserialization from the API typically yields a string. If createdAt is actually a string at runtime, calling .toISOString() on it will throw TypeError: member.createdAt.toISOString is not a function.

🛡️ Safer conversion
-          joinedAt: member.createdAt.toISOString(),
+          joinedAt: new Date(member.createdAt).toISOString(),
components/hackathons/HackathonsPage.tsx (1)

124-156: ⚠️ Potential issue | 🟡 Minor

Minor: Singular/plural inconsistency in empty state text.

The title was changed to singular "No available hackathon" but the description still uses plural "hackathons" (lines 134–135). Consider making them consistent — either both singular or both plural. Typically "No available hackathons" (plural) is more natural for a list page empty state.

components/organization/cards/GradeSubmissionModal/useScoreCalculation.ts (1)

16-27: ⚠️ Potential issue | 🟠 Major

totalWeight is accumulated but unused in the score calculation — incorrect result when weights don't sum to 100.

Line 24 accumulates totalWeight, but line 27 divides by the hard-coded constant 10 instead of using totalWeight. The formula totalWeightedScore / 10 only produces correct percentages when criterion weights sum to exactly 100. If they don't (which the JudgingCriteriaList component explicitly warns about), results will be wrong.

The weighted-average percentage should be:

Proposed fix
-    return totalWeight > 0 ? totalWeightedScore / 10 : 0;
+    return totalWeight > 0 ? (totalWeightedScore / totalWeight) * 10 : 0;

This normalizes scores correctly regardless of whether weights sum to 100.

components/organization/cards/GradeSubmissionModal/index.tsx (1)

30-30: ⚠️ Potential issue | 🟡 Minor

Unused participantId prop — submission.id is used instead.

The participantId prop is declared in GradeSubmissionModalProps (Line 30) and destructured (Line 41), but it's never referenced. Both useSubmissionScores (Line 66) and useScoreForm (Line 91) receive participantId: submission.id. This makes the prop dead code and could mislead callers into thinking they must provide a separate participant ID.

Either remove the prop from the interface and destructuring, or use it consistently instead of submission.id.

Proposed fix
 interface GradeSubmissionModalProps {
   open: boolean;
   onOpenChange: (open: boolean) => void;
   organizationId: string;
   hackathonId: string;
-  participantId: string;
   judgingCriteria?: JudgingCriterion[];
   submission: SubmissionData;
   onSuccess?: () => void;
 }

 export default function GradeSubmissionModal({
   open,
   onOpenChange,
   organizationId,
   hackathonId,
-  participantId,
   judgingCriteria,
   submission,
   onSuccess,
 }: GradeSubmissionModalProps) {

Also applies to: 41-41, 66-66, 91-91

app/(landing)/hackathons/[slug]/page.tsx (2)

404-405: ⚠️ Potential issue | 🟠 Major

Resources tab content rendered twice.

HackathonResources is rendered in two separate blocks: Lines 404-405 (activeTab === 'resources' && currentHackathon.resources?.length > 0) and Lines 443-445 (activeTab === 'resources' && currentHackathon?.resources?.[0]). Both conditions overlap, so the component renders twice when the resources tab is active and resources exist.

Remove one of the duplicate blocks.

Proposed fix — remove the second block
-            {activeTab === 'resources' && currentHackathon?.resources?.[0] && (
-              <HackathonResources />
-            )}

Also applies to: 443-445


299-303: ⚠️ Potential issue | 🟡 Minor

Variable params shadows outer scope.

const params on Line 301 shadows the component-level params from useParams() on Line 37. This compiles but harms readability and can lead to subtle bugs.

Proposed fix
   const handleTabChange = (tabId: string) => {
     setActiveTab(tabId);
-    const params = new URLSearchParams(searchParams.toString());
-    params.set('tab', tabId);
-    router.push(`?${params.toString()}`, { scroll: false });
+    const tabParams = new URLSearchParams(searchParams.toString());
+    tabParams.set('tab', tabId);
+    router.push(`?${tabParams.toString()}`, { scroll: false });
   };
🤖 Fix all issues with AI agents
In `@app/`(landing)/hackathons/[slug]/page.tsx:
- Line 95: The memoized hackathonTabs computed by useMemo references
announcements.length but announcements is not included in the dependency array;
update the dependency array for the useMemo that defines hackathonTabs (the
useMemo block containing hackathonTabs) to include announcements (or
announcements.length) so the memo recomputes when announcements change, ensuring
the announcements tab appears; locate the useMemo that returns hackathonTabs and
add announcements to its dependency list.
🟠 Major comments (20)
components/organization/hackathons/details/HackathonSidebar.tsx-51-51 (1)

51-51: ⚠️ Potential issue | 🟠 Major

Absolute-positioned gradient needs a relative parent.

The gradient overlay on line 164 uses absolute positioning, but the parent <nav> (line 51) lacks relative. This means the gradient will anchor to whatever ancestor happens to have positioning, likely causing layout issues — especially in the mobile Sheet where the containing hierarchy is different from desktop.

Proposed fix
-    <nav className='flex h-full flex-col overflow-y-auto px-4 py-6'>
+    <nav className='relative flex h-full flex-col overflow-y-auto px-4 py-6'>

Also applies to: 164-164

components/organization/hackathons/details/HackathonSidebar.tsx-33-40 (1)

33-40: 🛠️ Refactor suggestion | 🟠 Major

menuItems: any[] defeats type safety.

Define a MenuItem interface and use it here instead of any[]. This also makes the component self-documenting about what shape each menu item should have.

Proposed fix
+interface MenuItem {
+  icon: React.ComponentType<{ className?: string }>;
+  label: string;
+  href: string;
+  description: string;
+  disabled?: boolean;
+}
+
 interface SidebarContentProps {
   hackathonData: Hackathon[];
   currentHackathon?: Hackathon;
-  menuItems: any[];
+  menuItems: MenuItem[];
   isLoading: boolean;
   normalizedPath: string | null;
   basePath: string;
 }

As per coding guidelines, "Prefer const arrow functions with explicit type annotations over function declarations" — any[] bypasses this intent.

hooks/use-link-validation.ts-39-114 (1)

39-114: ⚠️ Potential issue | 🟠 Major

Client-side fetch to arbitrary user-supplied URLs is unreliable and has privacy implications.

validateWebsite issues a full GET request from the user's browser to any URL they type. This:

  1. Leaks the user's IP to arbitrary third-party domains on every debounced keystroke.
  2. Is blocked by CORS on most sites, so the "reachability" check silently falls back to "valid" (lines 104-113), making it a no-op for the majority of URLs.
  3. Can trigger side-effects on servers that don't treat GET as safe/idempotent.

Consider restricting this validator to format-only checks (like X handle validation) and, if reachability verification is needed, proxying through your own backend with HEAD requests.

hooks/use-link-validation.ts-226-267 (1)

226-267: ⚠️ Potential issue | 🟠 Major

GitHub API rate limit (60 req/hour unauthenticated) will be exhausted quickly.

Every debounced edit triggers a GitHub API call. With an 800ms debounce and no caching, a single user editing the field a few times across multiple orgs can easily exhaust the per-IP limit. Once rate-limited, the validator rejects the input (line 255-264) even if it's valid.

Consider:

  • Caching successful lookups (e.g., in a Map or useRef) to avoid repeat calls for the same handle.
  • Using a HEAD request or adding an auth token if available.
  • Falling back to format-only validation (like X) instead of rejecting on 403.
hooks/use-link-validation.ts-255-264 (1)

255-264: ⚠️ Potential issue | 🟠 Major

Rate-limited response should not reject a valid handle.

When GitHub returns 403 (rate limited), the validator marks the input as invalid. This falsely prevents the user from saving a perfectly valid GitHub username. A safer approach is to accept format-valid handles when the API is unavailable.

🐛 Proposed fix
          } else if (response.status === 403) {
-            // Rate limited - we can't verify, so we'll reject it to be safe
             const result = {
-              isValid: false,
+              isValid: true,
               isValidating: false,
-              error: 'Unable to verify (rate limited). Please try again later.',
+              error: null,
             };
 
             setValidationState(prev => ({ ...prev, github: result }));
             return result;
components/organization/tabs/LinksTab.tsx-176-188 (1)

176-188: ⚠️ Potential issue | 🟠 Major

Save is allowed while validation is still in-flight — potential race condition.

The pre-save guard only checks isValid === false, but if the user clicks "Save" while a validation request is still pending (isValidating === true), the check passes because isValid is null. This allows saving with unvalidated input.

🐛 Proposed fix: also block save during validation
+    // Block save while any field is still validating
+    const isAnyValidating =
+      validationStates.website.isValidating ||
+      validationStates.x.isValidating ||
+      validationStates.github.isValidating;
+    if (isAnyValidating) {
+      toast.error('Please wait for link validation to complete.');
+      return;
+    }
+
     // Check for validation errors
     if (links.website && validationStates.website.isValid === false) {
hooks/use-link-validation.ts-20-23 (1)

20-23: 🛠️ Refactor suggestion | 🟠 Major

Internal validationState is set but never returned or read — remove it.

The hook maintains validationState via setValidationState across all three validators, but this state is never exposed in the return value (lines 293-297) and never read internally. Meanwhile, the consumer (LinksTab.tsx) maintains its own parallel validationStates. All these setValidationState calls are dead writes that trigger unnecessary re-renders of every component using this hook.

Either remove the internal state entirely (since each function already returns the result) or expose it and let the consumer use it directly instead of duplicating state.

♻️ Proposed fix: remove internal state
 export function useLinkValidation(): LinkValidationHook {
-  const [validationState, setValidationState] = useState<
-    Record<string, ValidationResult>
-  >({});

Then remove every setValidationState(...) call throughout the file and drop the useState import.

components/hackathons/submissions/SubmissionForm.tsx-246-288 (1)

246-288: ⚠️ Potential issue | 🟠 Major

currentStep in the dependency array will re-advance the user whenever they navigate back to step 0.

For INDIVIDUAL and TEAM (with existing team) hackathons, if a user clicks "Back" to return to step 0, this effect re-fires, sees currentStep === 0, and immediately pushes them forward to step 1 again — making step 0 unreachable.

Consider removing currentStep from the dependency array and instead using a ref or a "has already auto-advanced" flag to ensure the auto-skip only happens once (on initial open).

Additionally, there's a duplicate effect at lines 595–599 that also sets participationType to 'TEAM' when myTeam exists. This overlaps with the logic here (lines 259–265, 268–274) and can cause subtle ordering issues. Consider consolidating the two into this single effect.

Proposed approach
+  const hasAutoAdvanced = useRef(false);
+
   // Participation type enforcement
   useEffect(() => {
     if (!open || !currentHackathon) return;
+    if (hasAutoAdvanced.current) return;
 
     const hackathonType = currentHackathon.participantType;
 
     if (hackathonType === 'INDIVIDUAL') {
       form.setValue('participationType', 'INDIVIDUAL');
-      if (!submissionId && currentStep === 0) {
+      if (!submissionId) {
         setCurrentStep(1);
         updateStepState(0, 'completed');
         updateStepState(1, 'active');
+        hasAutoAdvanced.current = true;
       }
     } else if (hackathonType === 'TEAM') {
       form.setValue('participationType', 'TEAM');
-      if (!submissionId && !!myTeam && currentStep === 0) {
+      if (!submissionId && !!myTeam) {
         setCurrentStep(1);
         updateStepState(0, 'completed');
         updateStepState(1, 'active');
+        hasAutoAdvanced.current = true;
       }
     } else if (hackathonType === 'TEAM_OR_INDIVIDUAL') {
       if (!submissionId) {
         if (myTeam) {
           form.setValue('participationType', 'TEAM');
-          if (currentStep === 0) {
-            setCurrentStep(1);
-            updateStepState(0, 'completed');
-            updateStepState(1, 'active');
-          }
+          setCurrentStep(1);
+          updateStepState(0, 'completed');
+          updateStepState(1, 'active');
+          hasAutoAdvanced.current = true;
         } else {
           form.setValue('participationType', 'INDIVIDUAL');
         }
       }
     }
   }, [
     open,
     currentHackathon,
     myTeam,
     submissionId,
     form,
-    currentStep,
     updateStepState,
   ]);

And reset the ref when the modal closes (in the existing close-reset effect):

   useEffect(() => {
     if (!open) {
       const timer = setTimeout(() => {
         // ... existing reset logic
+        hasAutoAdvanced.current = false;
       }, 300);
       return () => clearTimeout(timer);
     }
   }, [open, form]);

Then remove the duplicate effect at lines 595–599.

lib/providers/OrganizationProvider.tsx-1326-1342 (1)

1326-1342: 🛠️ Refactor suggestion | 🟠 Major

Dead fetch — result is discarded.

getFullOrganization is called but the returned data is never used (the if (data) block is empty). This adds a network request with no effect. Either wire it up to update state or remove the call.

♻️ Remove the unused fetch
          // Refresh organizations list to update current user's role in the UI
          await fetchOrganizations();
-
-          // Refresh active organization details if possible
-          if (state.activeOrgId === orgId) {
-            try {
-              const { data } =
-                await authClient.organization.getFullOrganization({
-                  query: {
-                    organizationId: orgId,
-                  },
-                });
-              if (data) {
-                // We might want to update the active organization with full details
-                // creating a merged object if needed, but for now let's rely on fetchOrganizations
-                // to update the user's role in the list which matters most for permissions
-              }
-            } catch (err) {
-              console.error(
-                'Failed to refresh organization details after transfer:',
-                err
-              );
-            }
-          }
components/organization/tabs/TransferOwnershipTab.tsx-57-57 (1)

57-57: ⚠️ Potential issue | 🟠 Major

Default isCurrentUserOwner to false instead of true.

Initializing to true means if activeOrgId is falsy (or the check hasn't run), the user briefly sees the transfer form as if they had permission. Defaulting to false is the safer posture — deny by default, then grant after verification.

🛡️ Proposed fix
-  const [isCurrentUserOwner, setIsCurrentUserOwner] = useState(true);
+  const [isCurrentUserOwner, setIsCurrentUserOwner] = useState(false);
components/organization/cards/GradeSubmissionModal/useSubmissionScores.ts-100-110 (1)

100-110: ⚠️ Potential issue | 🟠 Major

Fragile judge matching — judgeName === user.email can cause false positives.

Line 108 matches s.judgeName against user.email. If any judge's display name happens to equal another user's email address, this returns the wrong judge's scores. The matching chain should prioritize judgeId and only fall back to secondary identifiers with greater care.

Additionally, the multiple (s as any) casts suggest the IndividualJudgeScore type is missing fields. Consider extending the type instead.

♻️ Safer matching approach
          const currentUserScore = (
            response.data as IndividualJudgeScore[]
          ).find(
            s =>
              s.judgeId === user.id ||
-              s.judgeName === user.name ||
-              s.judgeName === user.email ||
-              (s as any).judgeEmail === user.email
+              (s as any).judgeEmail === user.email ||
+              (s.judgeName === user.name && user.name !== undefined)
          );
components/organization/cards/JudgingParticipant/IndividualScoresBreakdown.tsx-170-178 (1)

170-178: ⚠️ Potential issue | 🟠 Major

Criterion label likely shows an internal ID instead of the human-readable title.

c.criterionId || c.criterionTitle prefers criterionId (which is typically a UUID or database identifier) over criterionTitle. The displayed label will be an opaque ID whenever one exists. This should be reversed:

Proposed fix
-                              {c.criterionId || c.criterionTitle}
+                              {c.criterionTitle || c.criterionId}
components/organization/hackathons/judging/JudgingResultsTable.tsx-42-57 (1)

42-57: 🛠️ Refactor suggestion | 🟠 Major

Pervasive as any casts indicate a misaligned JudgingResult type — extract a helper and fix the type.

The score-extraction logic (a as any).averageScore ?? (a as any).average_score ?? ... bypasses TypeScript's safety and is duplicated verbatim at lines 120–125 for display. This suggests JudgingResult doesn't include the properties the component actually receives.

Fix the root cause by updating the JudgingResult type to include the expected score fields, then extract a helper:

Suggested refactor
+const getScore = (result: JudgingResult): number =>
+  Number(result.averageScore ?? result.average_score ?? result.score ?? 0);
+
 const sortedResults = React.useMemo(() => {
   return [...results].sort((a, b) => {
-    const scoreA = Number(
-      (a as any).averageScore ??
-        (a as any).average_score ??
-        (a as any).score ??
-        0
-    );
-    const scoreB = Number(
-      (b as any).averageScore ??
-        (b as any).average_score ??
-        (b as any).score ??
-        0
-    );
-    return scoreB - scoreA;
+    return getScore(b) - getScore(a);
   });
 }, [results]);

Then reuse getScore(result).toFixed(2) at lines 120–125.

app/(landing)/organizations/[id]/hackathons/[hackathonId]/judging/page.tsx-142-155 (1)

142-155: ⚠️ Potential issue | 🟠 Major

fetchData has stale closures over fetchJudges and fetchResults.

fetchData (Line 142) calls fetchJudges() and fetchResults() (Lines 154-155), but its useCallback dependency array (Line 251) only lists [organizationId, hackathonId]. Since fetchJudges captures activeOrgId in its closure and is itself a useCallback, changes to activeOrgId won't be reflected by the stale reference inside fetchData.

Add fetchJudges and fetchResults to fetchData's dependencies:

Proposed fix
-  }, [organizationId, hackathonId]);
+  }, [organizationId, hackathonId, fetchJudges, fetchResults]);

Also applies to: 251-251

components/organization/cards/JudgingParticipant.tsx-47-58 (1)

47-58: ⚠️ Potential issue | 🟠 Major

Each JudgingParticipant instance fetches its own session — N×API calls.

Every card independently calls authClient.getSession() on mount (Lines 52-58). If there are 50 submissions, that's 50 parallel session requests. Lift session fetching to the parent (JudgingPage) and pass currentUserId (or isAssignedJudge) as a prop instead.

lib/api/hackathons.ts-2304-2372 (1)

2304-2372: ⚠️ Potential issue | 🟠 Major

Judges API is fully duplicated between hackathons.ts and hackathons/judging.ts.

addJudge, removeJudge, getHackathonJudges, and their request/response types are defined in both lib/api/hackathons.ts (Lines 2304-2372) and lib/api/hackathons/judging.ts (Lines 412-459). Worse, the AddJudgeRequest type differs: here it has { userId } (Line 2308-2310), while in judging.ts it has { userId, email } (Lines 212-215). This type mismatch means callers importing from different modules get different contracts.

Consolidate these to a single canonical location (likely judging.ts) and re-export from hackathons.ts if needed.

lib/api/hackathons.ts-1292-1304 (1)

1292-1304: 🛠️ Refactor suggestion | 🟠 Major

getJudgingCriteria duplicated — identical implementation exists in judging.ts.

This function (Lines 1292-1304) is an exact copy of the one in lib/api/hackathons/judging.ts (Lines 365-374). Consolidate into one module.

components/organization/cards/GradeSubmissionModal/useScoreForm.ts-120-124 (1)

120-124: ⚠️ Potential issue | 🟠 Major

A legitimate score of 0 is rejected by validation.

Line 120 treats score === 0 as invalid ('Score required'). If a judge intentionally gives 0/10 for a criterion, the form won't submit. If scores genuinely must be > 0, consider changing the minimum to 0.1 or making the intent clear in the UI. Otherwise, allow 0 as a valid score.

Proposed fix (if 0 should be valid)
       const score = scores[key];
-      if (typeof score !== 'number' || score === 0) {
+      if (typeof score !== 'number') {
         errors[key] = 'Score required';
         isValid = false;
       }
components/organization/cards/GradeSubmissionModal/index.tsx-191-203 (1)

191-203: ⚠️ Potential issue | 🟠 Major

Use absolute positioning for the footer or ensure BoundlessSheet establishes a stacking context.

The footer's fixed positioning is viewport-relative and will overlay content outside the sheet. Since BoundlessSheet doesn't create a stacking context (the SheetContent lacks transform, z-index, opacity, or similar properties), the fixed element cannot be constrained to the sheet's bounds. Either reposition the footer with absolute relative to its parent scroll container, or add a stacking context property (e.g., transform: translateZ(0) or explicit z-index) to the sheet wrapper.

lib/api/hackathons/judging.ts-183-206 (1)

183-206: ⚠️ Potential issue | 🟠 Major

& any on data type makes the entire union meaningless.

Line 197's & any widens the data field to any, making the preceding union type (JudgingSubmission[] | { submissions, pagination }) effectively useless. Remove the & any to preserve type safety.

Proposed fix
 export interface GetJudgingSubmissionsResponse extends ApiResponse<any> {
   success: true;
   data:
     | JudgingSubmission[]
     | {
         submissions: any[];
         pagination: {
           page: number;
           limit: number;
           total: number;
           totalPages: number;
         };
-      }
-  ) &
-    any;
+      };
   pagination?: {
     currentPage: number;
     totalPages: number;
     totalItems: number;
     itemsPerPage: number;
     hasNext: boolean;
     hasPrev: boolean;
   };
 }
🟡 Minor comments (18)
components/hackathons/participants/profileCard.tsx-76-88 (1)

76-88: ⚠️ Potential issue | 🟡 Minor

Missing abort controller — potential state update after unmount or stale response on rapid prop changes.

If participant.username changes quickly or the component unmounts before the fetch completes, setProfileData will be called with a stale/outdated result (or on an unmounted component). Use an AbortController or a cleanup flag to discard obsolete responses.

🛡️ Proposed fix
  useEffect(() => {
+   let cancelled = false;
    const fetchProfile = async () => {
      try {
        if (participant.username) {
          const data = await getUserProfileByUsername(participant.username);
-         setProfileData(data);
+         if (!cancelled) setProfileData(data);
        }
      } catch (error) {
-       console.error('Error fetching profile data:', error);
+       if (!cancelled) console.error('Error fetching profile data:', error);
      }
    };
    fetchProfile();
+   return () => { cancelled = true; };
  }, [participant.username]);
components/organization/OrganizationSidebar.tsx-273-276 (1)

273-276: ⚠️ Potential issue | 🟡 Minor

Conflicting top value: Tailwind class top-4 is overridden by inline style={{ top: '90px' }}.

The Tailwind top-4 (16px) in className is silently overridden by the inline style top: '90px'. This is confusing and the Tailwind class is dead code. Remove the stale Tailwind class to avoid misleading future readers.

🧹 Proposed fix
       <aside
-        className='fixed top-4 left-0 hidden w-[280px] border-r border-zinc-800/50 bg-black/40 backdrop-blur-xl md:block'
+        className='fixed left-0 hidden w-[280px] border-r border-zinc-800/50 bg-black/40 backdrop-blur-xl md:block'
         style={{ height: availableHeight, top: '90px' }}
       >

As per coding guidelines, "Use TailwindCSS classes for styling; avoid plain CSS." Consider using a Tailwind arbitrary value top-[90px] instead of an inline style to keep styling consistent.

components/organization/OrganizationSidebar.tsx-32-179 (1)

32-179: ⚠️ Potential issue | 🟡 Minor

Decorative gradient at Line 178 uses absolute positioning but the parent <nav> lacks relative.

The absolute-positioned gradient div on Line 178 will position itself relative to the nearest positioned ancestor, which may not be the <nav>. Add relative to the <nav> element to ensure the gradient anchors correctly.

🐛 Proposed fix
-    <nav className='flex h-full flex-col overflow-y-auto px-4 py-6'>
+    <nav className='relative flex h-full flex-col overflow-y-auto px-4 py-6'>
components/organization/hackathons/details/HackathonSidebar.tsx-369-373 (1)

369-373: ⚠️ Potential issue | 🟡 Minor

Mobile sidebar trigger button is missing an accessible label.

Screen readers will announce this as an unlabeled button. Add an aria-label (or use the <SheetTrigger> title) so it's identifiable.

-            <button className='flex h-10 w-10 items-center justify-center rounded-lg border border-zinc-800 bg-black/60 shadow-lg backdrop-blur-xl'>
+            <button
+              aria-label='Open sidebar menu'
+              className='flex h-10 w-10 items-center justify-center rounded-lg border border-zinc-800 bg-black/60 shadow-lg backdrop-blur-xl'
+            >
components/organization/tabs/ProfileTab.tsx-435-480 (1)

435-480: ⚠️ Potential issue | 🟡 Minor

Extract duplicate error-handling logic to a shared utility function.

The error message extraction pattern appears identically in both ProfileTab.tsx (lines 435–480) and LinksTab.tsx (lines 218–236). More importantly, the order-dependent substring matching is fragile and can misclassify errors. For example, an error message like "Resource 'Unauthorized Access Log' not found" would match the 'Unauthorized' check and incorrectly report authentication failure instead of the actual resource-not-found condition.

Create a reusable extractErrorMessage(error: unknown): string utility and use it in both files. The utility should check for structured error properties (context.body.message, response.data.message) before attempting any string-based heuristics, and prefer numeric HTTP status codes when available over substring matching.

components/hackathons/submissions/SubmissionForm.tsx-606-678 (1)

606-678: ⚠️ Potential issue | 🟡 Minor

Guard against currentHackathon being null during initial render.

If currentHackathon hasn't loaded yet, the else branch (line 666) renders the informational banner. Line 671 falls through to display "Individual" even though the actual participation type is unknown. Consider either returning a loading/skeleton state when currentHackathon is nullish, or skipping the banner entirely until data is available.

Also, disabled={false} on line 620 is the default and can be removed.

Suggested guard
+          {!currentHackathon ? (
+            <div className="flex items-center justify-center py-4">
+              <Loader2 className="h-6 w-6 animate-spin text-[`#a7f950`]" />
+            </div>
+          ) : currentHackathon.participantType === 'TEAM_OR_INDIVIDUAL' ? (
-          {currentHackathon?.participantType === 'TEAM_OR_INDIVIDUAL' ? (
              <FormField
                ...
components/organization/tabs/MembersTab.tsx-355-355 (1)

355-355: ⚠️ Potential issue | 🟡 Minor

Bug: Stray {userIsOwner} expression renders a boolean as text.

Line 355 renders the raw boolean value (true or false) into the DOM as visible text. This is almost certainly a leftover from a refactor and should be removed.

🐛 Proposed fix
-                    {userIsOwner}
                     {userIsOwner && (
components/organization/tabs/MembersTab/TeamManagementSection.tsx-26-31 (1)

26-31: ⚠️ Potential issue | 🟡 Minor

Remove activeOrg from the TeamManagementSectionProps interface — it's passed by the parent but never used.

The activeOrg prop is declared in the interface (line 22) but not destructured in the function signature (lines 26–31) and appears only in commented-out debug code (line 66). Since the component doesn't consume it, remove it from the interface definition to prevent unnecessary prop passing from the parent.

♻️ Suggested fix

Update TeamManagementSectionProps to remove the unused prop:

interface TeamManagementSectionProps {
  members: Member[];
  onRoleChange: (memberId: string, newRole: string) => void;
  onRemoveMember: (memberId: string) => void;
-  activeOrg?: Organization | null;
  canManageTeam?: boolean;
}

Also update the parent component to stop passing activeOrg:

<TeamManagementSection
  members={members}
  onRoleChange={handleRoleChange}
  onRemoveMember={handleRemoveMember}
-  activeOrg={activeOrg}
  canManageTeam={...}
/>
app/(landing)/organizations/[id]/hackathons/[hackathonId]/announcement/page.tsx-310-337 (1)

310-337: ⚠️ Potential issue | 🟡 Minor

Action buttons are inaccessible via keyboard.

The publish, edit, and delete buttons use opacity-0 group-hover:opacity-100, making them invisible and unreachable for keyboard-only users. Consider adding focus-within visibility so the buttons appear when any child receives focus.

♿ Proposed fix
-                    <div className='ml-4 flex items-center gap-2 opacity-0 transition-opacity group-hover:opacity-100'>
+                    <div className='ml-4 flex items-center gap-2 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100'>
app/(landing)/organizations/[id]/hackathons/[hackathonId]/announcement/page.tsx-51-65 (1)

51-65: ⚠️ Potential issue | 🟡 Minor

fetchAnnouncements missing from useEffect deps and not memoized.

fetchAnnouncements is called inside the effect but not listed in the dependency array. If organizationId or hackathonId change, the stale closure won't pick up the new values. Wrap fetchAnnouncements in useCallback and include it in the deps.

♻️ Proposed fix
+import { useState, useEffect, useCallback } from 'react';
 ...
-  const fetchAnnouncements = async () => {
+  const fetchAnnouncements = useCallback(async () => {
     try {
       const data = await listAnnouncements(hackathonId);
       setAnnouncements(data);
     } catch (error) {
       console.error('Failed to fetch announcements:', error);
       toast.error('Failed to load announcements');
     } finally {
       setIsLoading(false);
     }
-  };
+  }, [hackathonId]);

   useEffect(() => {
     fetchAnnouncements();
-  }, [hackathonId]);
+  }, [fetchAnnouncements]);
app/(landing)/hackathons/[slug]/announcements/[announcementId]/page.tsx-28-49 (1)

28-49: ⚠️ Potential issue | 🟡 Minor

No guard for missing slug parameter before the API call.

Line 30 guards against a missing announcementId but not slug. If slug is somehow undefined, GetHackathonBySlug would be called with undefined, and hackathonData.data.name (line 38) could throw if the response structure is unexpected. Consider adding slug to the guard:

Suggested fix
-      if (!announcementId) return;
+      if (!announcementId || !slug) return;
app/(landing)/hackathons/[slug]/announcements/[announcementId]/page.tsx-92-98 (1)

92-98: ⚠️ Potential issue | 🟡 Minor

window.close() is unreliable — will silently do nothing if the tab wasn't opened via JavaScript.

Browsers restrict window.close() to tabs/windows opened by window.open(). If a user navigates to this page directly or via a regular link, the button labeled "Close Tab" will be non-functional with no feedback. The same issue applies at line 189.

Consider using router.back() (as you already do in the error state) or router.push(...) to navigate to the hackathon page instead.

Suggested fix
           <button
-            onClick={() => window.close()}
+            onClick={() => router.push(`/hackathons/${slug}`)}
             className='flex items-center gap-2 text-sm text-zinc-400 transition-colors hover:text-white'
           >
             <ArrowLeft className='h-4 w-4' />
-            Close Tab
+            Back to Hackathon
           </button>

Apply similar change at line 189.

app/(landing)/hackathons/[slug]/page.tsx-76-76 (1)

76-76: ⚠️ Potential issue | 🟡 Minor

announcementsLoading state is declared but never used.

The loading state on Line 76 is set during the fetch but not consumed in the UI (no loading indicator shown while announcements load).

app/(landing)/organizations/[id]/hackathons/[hackathonId]/judging/page.tsx-257-260 (1)

257-260: ⚠️ Potential issue | 🟡 Minor

handleSuccess double-fetches results.

handleSuccess calls both fetchData() and fetchResults(). But fetchData already calls fetchResults() internally (Line 155), so results are fetched twice on every success callback.

Proposed fix
   const handleSuccess = () => {
     fetchData();
-    fetchResults(); // Refresh results to update metrics/table
   };
lib/api/hackathons/judging.ts-49-55 (1)

49-55: ⚠️ Potential issue | 🟡 Minor

rank typed as any — use a concrete type.

rank?: any on Line 54 discards type information. Based on usage, this should be number | null or number | undefined.

Proposed fix
 export interface JudgingResult {
   submissionId: string;
   projectName: string;
   averageScore: number;
   judgeCount: number;
-  rank?: any;
+  rank?: number | null;
 }
components/organization/cards/JudgingParticipant.tsx-186-190 (1)

186-190: ⚠️ Potential issue | 🟡 Minor

Potential "Invalid Date" rendered if both date fields are missing.

new Date(submissionData.submissionDate || participant.registeredAt) will produce Invalid Date when both values are falsy, since new Date(undefined) is invalid. Add a fallback or guard.

Proposed fix
-            <span>
-              Submitted{' '}
-              {new Date(
-                submissionData.submissionDate || participant.registeredAt
-              ).toLocaleDateString()}
-            </span>
+            <span>
+              Submitted{' '}
+              {submissionData.submissionDate || participant.registeredAt
+                ? new Date(
+                    submissionData.submissionDate || participant.registeredAt
+                  ).toLocaleDateString()
+                : 'N/A'}
+            </span>
lib/api/hackathons.ts-1742-1745 (1)

1742-1745: ⚠️ Potential issue | 🟡 Minor

Duplicate JSDoc block for getSubmissionDetails.

Lines 1742-1745 contain a second /** Returns full submission... */ JSDoc right after the first one that starts on Line 1739. Remove the duplicate.

Proposed fix
-/**
- * Returns full submission with votes and comments
- */
 export const getSubmissionDetails = async (
app/(landing)/organizations/[id]/hackathons/[hackathonId]/judging/page.tsx-312-312 (1)

312-312: ⚠️ Potential issue | 🟡 Minor

Debug console.log left in production code.

Line 312: console.log('Judging Results Response:', res); — remove before merging.

Proposed fix
       const res = await getJudgingResults(organizationId, hackathonId);
-      console.log('Judging Results Response:', res);
       if (res.success) {

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (1)
app/(landing)/hackathons/[slug]/page.tsx (1)

444-446: ⚠️ Potential issue | 🟡 Minor

Duplicate resources tab rendering — <HackathonResources /> is rendered twice.

Lines 405-406 already render the resources tab content when activeTab === 'resources'. This block at lines 444-446 duplicates it with a slightly different guard, causing two instances of <HackathonResources /> to mount simultaneously.

Remove this duplicate block:

Proposed fix
-            {activeTab === 'resources' && currentHackathon?.resources?.[0] && (
-              <HackathonResources />
-            )}
🧹 Nitpick comments (1)
app/(landing)/hackathons/[slug]/page.tsx (1)

76-76: announcementsLoading is unused dead code.

The state variable is set on lines 82 and 89 but never read anywhere in the component. Remove it or wire it to a loading indicator.

Proposed fix
  const [announcements, setAnnouncements] = useState<HackathonAnnouncement[]>(
    []
  );
-  const [announcementsLoading, setAnnouncementsLoading] = useState(false);

  useEffect(() => {
    async function fetchAnnouncements() {
      if (!hackathonId) return;
      try {
-        setAnnouncementsLoading(true);
        const data = await listAnnouncements(hackathonId);
        // Only show published announcements for public view
        setAnnouncements(data.filter(a => !a.isDraft));
      } catch (error) {
        console.error('Failed to fetch announcements:', error);
-      } finally {
-        setAnnouncementsLoading(false);
      }
    }
    fetchAnnouncements();
  }, [hackathonId]);

@Benjtalkshow Benjtalkshow merged commit 1ced6dd into boundlessfi:main Feb 16, 2026
4 of 8 checks passed
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 19

Caution

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

⚠️ Outside diff range comments (2)
components/organization/cards/GradeSubmissionModal/useScoreForm.ts (1)

130-183: ⚠️ Potential issue | 🟠 Major

Silent failure when response.success is falsy.

If submitJudgingScore returns { success: false, message: "..." }, the code reaches line 154, the if (response.success) check fails, and the function exits without any user feedback. The user sees the loading spinner disappear with no toast.

Proposed fix
       if (response.success) {
         setShowSuccess(true);
         toast.success(
           existingScore
             ? 'Grade updated successfully'
             : 'Grade submitted successfully',
           {
             duration: 2000,
           }
         );
 
         if (onSuccess) {
           onSuccess();
         }
 
         setTimeout(() => {
           setShowSuccess(false);
           onClose();
         }, 2000);
+      } else {
+        toast.error(response.message || 'Failed to submit grade');
       }
lib/api/hackathons/judging.ts (1)

301-317: ⚠️ Potential issue | 🔴 Critical

Include organizationId in the request URL to match the pattern of all other judging functions in this file.

getJudgingSubmissions accepts organizationId but the endpoint URL is /hackathons/${hackathonId}/submissions, unlike all other functions in the same file which use /organizations/${organizationId}/hackathons/${hackathonId}/.... Since callers intentionally pass organizationId and every related function (submitGrade, getSubmissionScores, getJudgingResults, etc.) includes the organization scope in the URL, the endpoint should be /organizations/${organizationId}/hackathons/${hackathonId}/submissions?... to maintain consistency and ensure proper organization-level access control.

🤖 Fix all issues with AI agents
In `@app/`(landing)/organizations/[id]/hackathons/[hackathonId]/judging/page.tsx:
- Around line 289-292: handleSuccess is redundantly calling fetchResults() even
though fetchData() already invokes fetchResults() (see fetchData and
fetchResults). Remove the explicit fetchResults() call from handleSuccess so it
only calls fetchData(); ensure handleSuccess remains the post-submit callback
that triggers fetchData() which in turn refreshes results/metrics.
- Around line 148-172: In the useCallback fetchResults function remove the
leftover debug console.log('Judging Results Response:', res) so response data is
not leaked to the browser console; locate the log inside fetchResults
(referencing getJudgingResults, res, and setJudgingResults) and delete that
console.log call (or replace it with a conditional debug-only logger gated by
NODE_ENV !== 'production' if you need retained debug output).
- Around line 516-521: The UI is rendering internal judge.userId which may
expose PII/internal IDs; update the rendering to show a user-facing attribute
(e.g., judge.email or judge.role) instead of judge.userId, or remove the second
<p> entirely if no friendly field exists. Locate the JSX that displays
{judge.name} and {judge.userId} and replace {judge.userId} with the appropriate
display field (judge.email or judge.role) and add a safe fallback (e.g.,
fallback text like "No contact info") to avoid rendering raw IDs.
- Around line 174-283: fetchData currently calls fetchJudges() and
fetchResults() without awaiting them, so setIsLoading(false) fires before those
requests finish; change to await their execution (preferably in parallel) so
loading/error handling covers them — e.g. replace the fire-and-forget calls
inside fetchData with awaiting a combined promise (Promise.all or
Promise.allSettled) of fetchJudges() and fetchResults(), and handle their
failures (propagate or log/throw) so the catch block's toast is triggered; keep
names fetchData, fetchJudges, fetchResults, setIsLoading and ensure finally only
runs after those awaited calls complete.

In `@components/hackathons/submissions/SubmissionForm.tsx`:
- Around line 247-283: The effect that enforces participation type can later
override a user’s manual choice because hasAutoAdvanced.current is only set when
the effect itself auto-advances; modify the manual advancement logic (e.g., in
the handleNext handler) so that when the user successfully advances past step 0
(the same transition where setCurrentStep(1) and updateStepState(0/1) are
called), you also set hasAutoAdvanced.current = true; this prevents the
useEffect (which reads currentHackathon, myTeam, submissionId, and calls
form.setValue / setCurrentStep) from re-running later and clobbering the user’s
manually chosen participationType.
- Around line 597-669: The banner is rendered incorrectly when currentHackathon
is undefined because the existing conditional (currentHackathon?.participantType
=== 'TEAM_OR_INDIVIDUAL') falls through to the else branch; update the render
logic in the SubmissionForm component to first check for the presence/loading
state of currentHackathon (e.g., if (!currentHackathon) return a loading
placeholder or null) or change the conditional to explicitly handle undefined
(e.g., currentHackathon ? currentHackathon.participantType ===
'TEAM_OR_INDIVIDUAL' : loading), so the "Team/Individual only" banner is only
shown when currentHackathon.participantType is known.

In `@components/organization/cards/GradeSubmissionModal/useScoreForm.ts`:
- Around line 49-51: getCriterionKey currently uses criterion.id ||
criterion.name || criterion.title which can produce duplicate keys when id/name
are missing and titles repeat; update getCriterionKey to accept the criterion's
array index (e.g., getCriterionKey(criterion, index)) and return a disambiguated
key like "<id|name|title>-<index>" so keys are unique, and add a warning/log via
the existing logger when criterion.id is missing to surface potentially
non-unique identifiers; update all call sites that invoke getCriterionKey (e.g.,
where scores are built) to pass the index.
- Around line 139-146: In useScoreForm's scoreData mapping, don't duplicate the
fallback chain for criterion IDs; replace the inline `criterion.id ||
criterion.name || criterion.title` with a call to getCriterionKey(criterion) so
criterionId uses the same key derivation as the local `key` variable; update the
mapping in the scoreData construction (inside the criteria.map) to assign
criterionId = getCriterionKey(criterion) to keep key derivation consistent.

In `@components/organization/cards/GradeSubmissionModal/useSubmissionScores.ts`:
- Around line 78-82: The effect's early-return guard only helps when
loadedParticipantIdRef.current is set in the "happy path"; update the code so
loadedParticipantIdRef.current = participantId is assigned in every terminal
branch of the async flow (no auth session from authClient.getSession, no
matching score from getSubmissionScores, and API/network error) as well as the
existing happy path where a score is found, so the ref prevents repeated
refetches when criteria or other deps change.
- Around line 41-43: getCriterionKey can return undefined which becomes the
string "undefined" as an object key; update it to always return a stable string
key by using a deterministic fallback (e.g., return `${criterion.id ??
criterion.name ?? (criterion as any).title ?? `missing-criterion-${index}`}`)
and change callsites to pass the criterion index if needed; also extend the
JudgingCriterion type to include an optional title field (remove the repeated
"as any" casts at getCriterionKey and the uses around lines where casts occur)
so the function can use criterion.title without casting and avoid creating
“undefined” keys in the Records.

In `@components/organization/cards/JudgingParticipant.tsx`:
- Around line 64-66: The code casts submission to any (const sub = submission as
any) which removes type safety for JudgingSubmission; replace this with a proper
typed narrowing or union instead: define/extend the JudgingSubmission type (or
create a union type) to cover the response shapes and use type guards or
optional chaining to safely access fields, then change references to use the
typed variable (avoid sub:any) and update uses of participant and submissionData
to derive from the typed object (participant, submissionData) so accesses like
sub.averageScore, sub.id, sub.type are type-checked.
- Around line 176-181: The render currently calls new
Date(submissionData.submissionDate ||
participant.registeredAt).toLocaleDateString() which yields "Invalid Date" if
both are undefined; in the JudgingParticipant component guard this by checking
for a valid date source before constructing Date and rendering — e.g., compute
const dateSrc = submissionData.submissionDate ?? participant.registeredAt and if
(!dateSrc) render a fallback string like "N/A" (or empty) instead of calling new
Date, otherwise call new Date(dateSrc).toLocaleDateString(); update the span
that currently contains the Date construction to use this guarded value.
- Around line 148-265: The breakdown panel is being rendered as a child of the
responsive flex container that switches to md:flex-row causing it to appear as a
narrow column; move the showBreakdown block (the JSX that renders
IndividualScoresBreakdown when showBreakdown is true) out of the outer flex
container (the div with classes 'bg-background/8 ... md:flex-row
md:items-center') so it becomes a sibling below that container, or alternatively
wrap the image + content areas in a new inner container (e.g., a div around the
image and the content area) and keep the breakdown as a full-width sibling under
that inner container so IndividualScoresBreakdown spans the full width on
desktop.

In
`@components/organization/cards/JudgingParticipant/IndividualScoresBreakdown.tsx`:
- Around line 29-49: The component's fetchScores flow swallows network errors
and still renders "No individual scores available yet"; modify the
useEffect/fetchScores logic to track an error state (e.g., add setError / error
state alongside setIsLoading and setScores), clear error at the start of
fetchScores, and in the catch setError with a user-friendly message; then update
the render path so the error state displays a distinct error UI (with a retry
action that calls fetchScores again) before the empty-scores check. Ensure you
reference the existing fetchScores, getSubmissionScores, setIsLoading, setScores
and the participantId/organizationId/hackathonId inputs when wiring retry and
clearing error.

In `@components/organization/tabs/LinksTab.tsx`:
- Around line 99-163: The three near-identical useEffect blocks (validating
debouncedWebsite, debouncedX, debouncedGithub using
validateWebsite/validateXHandle/validateGithubHandle and updating
setValidationStates) are duplicated and prone to stale async results; extract a
helper/custom hook (e.g., useFieldValidation(debouncedValue, validatorFn,
fieldKey)) that encapsulates the shared logic: set isValidating true, call the
validator, and in the effect cleanup set an isCancelled flag (or use
AbortController) so the async result is ignored if cancelled; only call
setValidationStates(fieldKey, result) when not cancelled, and ensure
empty/cleared state is handled when debouncedValue is falsy.

In `@components/organization/tabs/TransferOwnershipTab.tsx`:
- Around line 185-225: The permission-denied early return is after the
members-loading check, causing non-owners to briefly see the members loading
overlay; move the ownership check (using loadingOwnerCheck and
isCurrentUserOwner) above the loadingMembers check so you return the
permission-denied UI immediately when ownership has been determined false, or
alternatively gate fetchMembers behind the ownership check so loadingMembers
never becomes true for non-owners (update references in TransferOwnershipTab to
loadingOwnerCheck, isCurrentUserOwner, loadingMembers and any fetchMembers
call).

In `@lib/api/hackathons/judging.ts`:
- Around line 50-56: The JudgingResult interface currently types rank as any;
change it to a proper union reflecting observed usage: rank can be a number or
an object with a numeric position (e.g., { position: number }), so update the
JudgingResult type (the rank field) to rank?: number | { position: number } and
adjust any code that treats result.rank to use the union (e.g.,
result.rank?.position and typeof result.rank === 'number' checks in
JudgingResultsTable will then be correct).
- Around line 388-406: The non-array API response in getJudgingResults is being
cast unsafely to GetJudgingResultsResponse, which can leave data undefined at
runtime; update getJudgingResults to validate the response shape and return a
safe default (e.g., { success: false, message: res.data?.message || 'Error',
data: [] } or { success: true, data: [], message: 'Results retrieved
successfully' } when appropriate) instead of using (res.data || {}) as
GetJudgingResultsResponse; apply the same defensive fix to getSubmissionScores,
getHackathonJudges, and getJudgingWinners by checking whether res.data is the
expected object with a .data field (or an array where applicable) and supplying
an explicit data fallback so callers never receive undefined.

In `@lib/providers/OrganizationProvider.tsx`:
- Around line 1290-1294: The call to listMembers with a hardcoded query {
organizationId: orgId, limit: 100 } (used around the demotion step in
OrganizationProvider.tsx) can miss the current user for orgs >100 members;
replace this by either paginating through listMembers until you find the current
user or, preferably, call a dedicated membership lookup (e.g., getMembership or
listMemberById) if available to fetch the current user's membership directly;
update the logic that checks for the current user's role (the demotion branch
that relies on the listMembers result) to use the found membership from
pagination or the direct lookup and handle the case where membership is not
found by retrying/paging or throwing a clear error.
🧹 Nitpick comments (23)
lib/providers/OrganizationProvider.tsx (5)

1308-1315: Use logger.error instead of console.error for consistency.

The entire file uses the structured logger instance for logging (e.g., logger.error({ eventType: '...', error })), but this catch block falls back to console.error. This breaks structured logging consistency and may bypass log-level filtering configured on line 14.

♻️ Proposed fix
         } catch (error) {
-          console.error(
-            'Failed to demote previous owner during transfer:',
-            error
-          );
+          logger.error({
+            eventType: 'org.transfer_ownership.demote_error',
+            message: 'Failed to demote previous owner during transfer',
+            error,
+          });
           // Continue execution - user might end up with two owners but better than failing completely
           // They can fix it manually now via the Members tab
         }

1316-1321: Dispatched org state may be stale after demotion step.

response (line 1275) captures the org state after promoting the new owner but before demoting the current user (lines 1301-1305). When dispatched as UPDATE_ORGANIZATION on line 1318, the local state will reflect pre-demotion member roles. While fetchOrganizations() on line 1321 refreshes the organization list, the active org detail (state.activeOrg) will still carry the stale snapshot.

Consider re-fetching the active org after a successful transfer to ensure full consistency:

♻️ Proposed fix — refresh active org after transfer
          dispatch({ type: 'UPDATE_ORGANIZATION', payload: updatedOrg });

          // Refresh organizations list to update current user's role in the UI
          await fetchOrganizations();
+
+         // Also refresh the active org detail so member roles are up to date
+         if (state.activeOrgId === orgId) {
+           await fetchActiveOrganization(orgId);
+         }

This would require adding fetchActiveOrganization to the dependency array on line 1341.


1341-1341: state.activeOrgId is unused in the callback body — unnecessary dependency.

transferOwnership receives orgId as a parameter and never references state.activeOrgId. Including it in the dependency array causes the callback identity to change whenever the active org switches, which can trigger unnecessary re-renders in consumers without any functional benefit. dispatch is also stable (guaranteed by React's useReducer) and doesn't need to be listed, though it's harmless.

♻️ Proposed fix
-    [dispatch, fetchOrganizations, state.activeOrgId]
+    [fetchOrganizations]

If you adopt the stale-data fix above (using fetchActiveOrganization and state.activeOrgId inside the body), then keep them in the array.


1269-1342: Dual-owner window acknowledged — consider a toast for partial-failure visibility.

The approach of promoting first, then demoting, is pragmatic. However, when the demotion silently fails (lines 1308-1315), the user has no indication that the org now has two owners. A brief warning toast in the catch block would surface this to the user so they can fix it immediately.

♻️ Proposed enhancement
         } catch (error) {
           logger.error({
             eventType: 'org.transfer_ownership.demote_error',
             message: 'Failed to demote previous owner during transfer',
             error,
           });
+          toast.warning(
+            'Ownership transferred, but your role was not automatically updated. Please adjust roles manually in the Members tab.'
+          );
           // Continue execution - user might end up with two owners but better than failing completely
           // They can fix it manually now via the Members tab
         }

327-360: Large block of commented-out code — consider removing.

This ~35-line commented-out useEffect for auto-refresh references variables that no longer exist (refreshTimeoutRef, fetchActiveOrgTimeoutRef, autoRefresh, refreshInterval). Dead code like this adds noise and maintenance burden. If the feature is planned for later, tracking it in an issue would be cleaner.

components/organization/tabs/LinksTab.tsx (4)

229-270: Fragile error classification via string matching; unsafe as any cast.

The chain of errorObj.message.includes('401') / '403' / '404' etc. is brittle — it will silently mis-classify any message that happens to contain those substrings (e.g., a URL containing "404"). Also, error as any (line 230) defeats TypeScript's type safety.

Consider:

  • Checking HTTP status codes directly (if the error carries a status or statusCode property) instead of searching inside freeform strings.
  • Defining a typed error interface (or a type guard) instead of as any.

282-377: Extract a reusable ValidationIndicator component to reduce JSX duplication.

The loader/check/error icon block and the error-message <p> are repeated verbatim for website, X, and GitHub. A small helper component (or render function) accepting validationState would eliminate ~60 lines of near-identical markup and make future styling changes a single edit.

Sketch
const ValidationIndicator = ({ state }: { state: { isValid: boolean | null; isValidating: boolean; error: string | null } }) => (
  <>
    {state.isValidating && (
      <Loader2 className='absolute top-1/2 right-3 h-5 w-5 -translate-y-1/2 animate-spin text-gray-400' />
    )}
    {!state.isValidating && state.isValid === true && (
      <CheckCircle2 className='absolute top-1/2 right-3 h-5 w-5 -translate-y-1/2 text-green-500' />
    )}
    {!state.isValidating && state.isValid === false && (
      <XCircle className='absolute top-1/2 right-3 h-5 w-5 -translate-y-1/2 text-red-500' />
    )}
  </>
);

26-29: Use a const arrow function with explicit type annotation for the component.

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

Diff
-export default function LinksTab({
-  initialLinks = { website: '', x: '', github: '', others: '' },
-  onSave,
-}: LinksTabProps) {
+const LinksTab: React.FC<LinksTabProps> = ({
+  initialLinks = { website: '', x: '', github: '', others: '' },
+  onSave,
+}) => {

(Add export default LinksTab; at the bottom of the file.)

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


165-168: Rename updateLink to use the handle prefix.

This is an event handler invoked from onChange; the coding guidelines require the handle prefix.

Diff
-  const updateLink = (field: keyof OrganizationLinks, value: string) => {
+  const handleLinkChange = (field: keyof OrganizationLinks, value: string) => {

Update all call sites (onChange={e => handleLinkChange(…)}).

As per coding guidelines, **/*.{jsx,tsx}: "Event handlers should start with 'handle' prefix (e.g., handleClick, handleSubmit)."

components/organization/tabs/TransferOwnershipTab.tsx (2)

121-125: Members are fetched even when the user is not the owner.

fetchMembers fires as soon as activeOrgId is set, regardless of whether the ownership check has completed or the user is actually the owner. This is a wasted API call for non-owners. Consider gating the fetch:

Proposed fix
   useEffect(() => {
-    if (activeOrgId) {
+    if (activeOrgId && !loadingOwnerCheck && isCurrentUserOwner) {
       fetchMembers();
     }
-  }, [activeOrgId, fetchMembers]);
+  }, [activeOrgId, fetchMembers, loadingOwnerCheck, isCurrentUserOwner]);

80-82: authClient.getSession() is called on every member fetch.

This adds a redundant async call each time fetchMembers runs (including after a successful transfer at Line 160). Consider hoisting the session/user ID into a ref or obtaining it once from a context/provider that likely already has it.

components/organization/cards/GradeSubmissionModal/useSubmissionScores.ts (2)

180-180: criteria array reference in the dependency list may cause spurious effect runs.

If the parent component doesn't memoize the criteria array, a new reference is created every render, re-triggering this effect. Consider stabilizing the dependency — e.g., derive a primitive key from criteria:

Example
+ const criteriaKey = JSON.stringify(criteria.map(c => c.id ?? c.name));
+
  useEffect(() => {
    // ...
- }, [open, organizationId, hackathonId, participantId, criteria]);
+ }, [open, organizationId, hackathonId, participantId, criteriaKey]);

Keep criteria accessible via a ref or the outer closure so the effect body still reads the current value.


130-153: Criterion-score matching is overly broad and relies on multiple as any casts.

The 10-way cross-match (lines 131–141) plus the fallback key (lines 143–147) try every permutation of id, name, and the untyped title field. This makes it very difficult to reason about which branch actually matches at runtime, and a single API contract change could silently break the mapping.

Consider aligning the JudgingCriterion and CriterionScore types to share a single canonical identifier, then matching on that alone. If backward compatibility requires multiple fields, add title to the type definition rather than casting through any.

components/hackathons/submissions/SubmissionForm.tsx (1)

565-576: teamId ?? undefined is a no-op.

On line 563, teamId is already typed string | undefined, so ?? undefined has no effect. Harmless but redundant.

app/(landing)/organizations/[id]/hackathons/[hackathonId]/judging/page.tsx (3)

126-145: getSession() is called on every fetchJudges invocation, which is already triggered multiple times.

fetchJudges is called from fetchData, handleAddJudge, handleRemoveJudge, and tab switches — each time making an extra round-trip for the session. Consider fetching the session once on mount and storing it, or lifting it into a context/provider that already exists (authClient).


45-46: Pervasive any usage for orgMembers and currentJudges.

Both state variables are typed any[], and downstream usages also cast to any. This undermines type safety across the component. Consider defining minimal interfaces (e.g., OrgMember, Judge) to catch shape mismatches at compile time.


34-34: Component uses function declaration instead of const arrow function.

Per coding guidelines for *.tsx files: "Prefer const arrow functions with explicit type annotations over function declarations."

Proposed fix
-export default function JudgingPage() {
+const JudgingPage = () => {
   // ... component body ...
-}
+};
+
+export default JudgingPage;

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

lib/api/hackathons/judging.ts (2)

184-205: Response types hardcode success: true, making them unable to represent error states.

GetJudgingSubmissionsResponse, AddJudgeResponse, RemoveJudgeResponse all declare success: true as a literal type. This means TypeScript will reject assigning a { success: false, ... } response to these types, forcing unsafe casts elsewhere. Consider using boolean or removing the literal override since ApiResponse already defines it.


347-358: Duplicated array-wrapping logic across four API functions.

getSubmissionScores, getJudgingResults, getHackathonJudges, and getJudgingWinners all repeat the same pattern: check Array.isArray(res.data), wrap in { success, data, message }, else cast. Extract a helper to DRY this up:

Proposed helper
const wrapArrayResponse = <T>(
  data: T[] | ApiResponse<T[]> | null | undefined,
  fallbackMessage: string
): ApiResponse<T[]> => {
  if (Array.isArray(data)) {
    return { success: true, data, message: fallbackMessage };
  }
  const response = data as ApiResponse<T[]>;
  return {
    success: response?.success ?? false,
    data: response?.data ?? [],
    message: response?.message ?? '',
  };
};

Also applies to: 392-406, 444-458, 463-481

components/organization/cards/JudgingParticipant/IndividualScoresBreakdown.tsx (2)

51-56: Rename toggleExpand to handleToggleExpand.

Per coding guidelines, event handlers should start with the handle prefix.

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


169-194: Using array index as key for criteriaScores items.

idx is used as the React key. If criterionId is available (and it should be based on the CriterionScore interface), prefer it for stable identity:

-  {score.criteriaScores.map((c, idx) => (
-    <div key={idx} className='space-y-1.5'>
+  {score.criteriaScores.map((c) => (
+    <div key={c.criterionId} className='space-y-1.5'>
components/organization/hackathons/judging/JudgingResultsTable.tsx (2)

82-153: Each expand/collapse re-fetches scores since IndividualScoresBreakdown re-mounts.

When a row is collapsed, the IndividualScoresBreakdown component unmounts (line 138 conditional rendering). Re-expanding it mounts a new instance that re-fetches from the API. For a table where users may toggle rows frequently, consider either:

  • Keeping expanded components mounted but hidden (display: none), or
  • Lifting the scores cache to this component level.

This is a nice-to-have — the current approach is simpler and correct.


40-45: Rename toggleRow to handleToggleRow.

Per coding guidelines, event handlers should start with the handle prefix.

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

Comment on lines +148 to +172
const fetchResults = useCallback(async () => {
if (!organizationId || !hackathonId) return;

setIsFetchingResults(true);
try {
const res = await getJudgingResults(organizationId, hackathonId);
console.log('Judging Results Response:', res);
if (res.success) {
setJudgingResults(res.data || []);
} else {
setJudgingResults([]);
toast.error(res.message || 'Failed to load judging results');
}
} catch (error: any) {
console.error('Error fetching results:', error);
setJudgingResults([]);
toast.error(
error.response?.data?.message ||
error.message ||
'Failed to load judging results'
);
} finally {
setIsFetchingResults(false);
}
}, [organizationId, hackathonId]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove console.log left on line 154.

Debug logging (console.log('Judging Results Response:', res)) should not ship to production. It can leak response data in browser consoles.

-      console.log('Judging Results Response:', res);
📝 Committable suggestion

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

Suggested change
const fetchResults = useCallback(async () => {
if (!organizationId || !hackathonId) return;
setIsFetchingResults(true);
try {
const res = await getJudgingResults(organizationId, hackathonId);
console.log('Judging Results Response:', res);
if (res.success) {
setJudgingResults(res.data || []);
} else {
setJudgingResults([]);
toast.error(res.message || 'Failed to load judging results');
}
} catch (error: any) {
console.error('Error fetching results:', error);
setJudgingResults([]);
toast.error(
error.response?.data?.message ||
error.message ||
'Failed to load judging results'
);
} finally {
setIsFetchingResults(false);
}
}, [organizationId, hackathonId]);
const fetchResults = useCallback(async () => {
if (!organizationId || !hackathonId) return;
setIsFetchingResults(true);
try {
const res = await getJudgingResults(organizationId, hackathonId);
if (res.success) {
setJudgingResults(res.data || []);
} else {
setJudgingResults([]);
toast.error(res.message || 'Failed to load judging results');
}
} catch (error: any) {
console.error('Error fetching results:', error);
setJudgingResults([]);
toast.error(
error.response?.data?.message ||
error.message ||
'Failed to load judging results'
);
} finally {
setIsFetchingResults(false);
}
}, [organizationId, hackathonId]);
🤖 Prompt for AI Agents
In `@app/`(landing)/organizations/[id]/hackathons/[hackathonId]/judging/page.tsx
around lines 148 - 172, In the useCallback fetchResults function remove the
leftover debug console.log('Judging Results Response:', res) so response data is
not leaked to the browser console; locate the log inside fetchResults
(referencing getJudgingResults, res, and setJudgingResults) and delete that
console.log call (or replace it with a conditional debug-only logger gated by
NODE_ENV !== 'production' if you need retained debug output).

Comment on lines +174 to +283
const fetchData = useCallback(async () => {
if (!organizationId || !hackathonId) return;

setIsLoading(true);
try {
// Fetch submissions, criteria, and judges/members
const [submissionsRes, criteriaRes] = await Promise.all([
getJudgingSubmissions(organizationId, hackathonId, 1, 50),
getJudgingCriteria(hackathonId),
]);

// Trigger judges and results fetch in parallel but handle separately
fetchJudges();
fetchResults();

let enrichedSubmissions: JudgingSubmission[] = [];

if (submissionsRes.success) {
// Standard submissions endpoint returns { data: { submissions: [], pagination: {} } }
const submissionData =
(submissionsRes.data as any)?.submissions ||
submissionsRes.data ||
[];
const basicSubmissions = Array.isArray(submissionData)
? submissionData
: [];

// 2. Fetch full details for each submission to get user info
// We do this by fetching the project details, as submission endpoints lack user data
const detailsPromises = basicSubmissions.map(async (sub: any) => {
try {
// Check if we already have sufficient user data
if (
sub.participant?.user?.profile?.firstName ||
sub.participant?.name
)
return sub;

// Try fetch project details if we have projectId
if (sub.projectId) {
const project = await getCrowdfundingProject(sub.projectId);
if (project && project.project && project.project.creator) {
const creator = project.project.creator;
return {
...sub,
participant: {
...sub.participant,
// Use creator info for participant
name: creator.name,
username: creator.username,
image: creator.image,
email: creator.email,
user: {
...sub.participant?.user,
name: creator.name,
username: creator.username,
image: creator.image,
email: creator.email,
profile: {
...sub.participant?.user?.profile,
firstName: creator.name?.split(' ')[0] || '',
lastName:
creator.name?.split(' ').slice(1).join(' ') || '',
username: creator.username,
avatar: creator.image,
},
},
},
};
}
}

// Fallback to submission details check if project fail or no projectId
const detailsRes = await getSubmissionDetails(sub.id);
if (detailsRes.success && detailsRes.data) {
const details = detailsRes.data as any;
return {
...sub,
participant: {
...sub.participant,
...details.participant,
user: details.participant?.user || sub.participant?.user,
},
};
}
return sub;
} catch (err) {
console.error(
`Failed to fetch details for submission ${sub.id}`,
err
);
return sub;
}
});

enrichedSubmissions = await Promise.all(detailsPromises);
setSubmissions(enrichedSubmissions);
} else {
setSubmissions([]);
}

// Handle criteria response safely
setCriteria(Array.isArray(criteriaRes) ? criteriaRes : []);
} catch (error) {
console.error('Judging data fetch error:', error);
toast.error('Failed to load judging data');
} finally {
setIsLoading(false);
}
}, [organizationId, hackathonId, fetchJudges, fetchResults]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fire-and-forget async calls inside fetchData cause timing issues with isLoading.

fetchJudges() and fetchResults() (lines 186–187) are called without await, so isLoading is set to false in the finally block before judges and results have actually loaded. This means the "Overview" tab spinner disappears while judge/role data is still in-flight, and errors from these calls bypass the catch block's toast.

Consider awaiting them (or at least acknowledging the race in a comment):

Proposed fix — await parallel fetches
-      // Trigger judges and results fetch in parallel but handle separately
-      fetchJudges();
-      fetchResults();
+      // Fetch judges and results in parallel
+      await Promise.all([fetchJudges(), fetchResults()]);
📝 Committable suggestion

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

Suggested change
const fetchData = useCallback(async () => {
if (!organizationId || !hackathonId) return;
setIsLoading(true);
try {
// Fetch submissions, criteria, and judges/members
const [submissionsRes, criteriaRes] = await Promise.all([
getJudgingSubmissions(organizationId, hackathonId, 1, 50),
getJudgingCriteria(hackathonId),
]);
// Trigger judges and results fetch in parallel but handle separately
fetchJudges();
fetchResults();
let enrichedSubmissions: JudgingSubmission[] = [];
if (submissionsRes.success) {
// Standard submissions endpoint returns { data: { submissions: [], pagination: {} } }
const submissionData =
(submissionsRes.data as any)?.submissions ||
submissionsRes.data ||
[];
const basicSubmissions = Array.isArray(submissionData)
? submissionData
: [];
// 2. Fetch full details for each submission to get user info
// We do this by fetching the project details, as submission endpoints lack user data
const detailsPromises = basicSubmissions.map(async (sub: any) => {
try {
// Check if we already have sufficient user data
if (
sub.participant?.user?.profile?.firstName ||
sub.participant?.name
)
return sub;
// Try fetch project details if we have projectId
if (sub.projectId) {
const project = await getCrowdfundingProject(sub.projectId);
if (project && project.project && project.project.creator) {
const creator = project.project.creator;
return {
...sub,
participant: {
...sub.participant,
// Use creator info for participant
name: creator.name,
username: creator.username,
image: creator.image,
email: creator.email,
user: {
...sub.participant?.user,
name: creator.name,
username: creator.username,
image: creator.image,
email: creator.email,
profile: {
...sub.participant?.user?.profile,
firstName: creator.name?.split(' ')[0] || '',
lastName:
creator.name?.split(' ').slice(1).join(' ') || '',
username: creator.username,
avatar: creator.image,
},
},
},
};
}
}
// Fallback to submission details check if project fail or no projectId
const detailsRes = await getSubmissionDetails(sub.id);
if (detailsRes.success && detailsRes.data) {
const details = detailsRes.data as any;
return {
...sub,
participant: {
...sub.participant,
...details.participant,
user: details.participant?.user || sub.participant?.user,
},
};
}
return sub;
} catch (err) {
console.error(
`Failed to fetch details for submission ${sub.id}`,
err
);
return sub;
}
});
enrichedSubmissions = await Promise.all(detailsPromises);
setSubmissions(enrichedSubmissions);
} else {
setSubmissions([]);
}
// Handle criteria response safely
setCriteria(Array.isArray(criteriaRes) ? criteriaRes : []);
} catch (error) {
console.error('Judging data fetch error:', error);
toast.error('Failed to load judging data');
} finally {
setIsLoading(false);
}
}, [organizationId, hackathonId, fetchJudges, fetchResults]);
const fetchData = useCallback(async () => {
if (!organizationId || !hackathonId) return;
setIsLoading(true);
try {
// Fetch submissions, criteria, and judges/members
const [submissionsRes, criteriaRes] = await Promise.all([
getJudgingSubmissions(organizationId, hackathonId, 1, 50),
getJudgingCriteria(hackathonId),
]);
// Fetch judges and results in parallel
await Promise.all([fetchJudges(), fetchResults()]);
let enrichedSubmissions: JudgingSubmission[] = [];
if (submissionsRes.success) {
// Standard submissions endpoint returns { data: { submissions: [], pagination: {} } }
const submissionData =
(submissionsRes.data as any)?.submissions ||
submissionsRes.data ||
[];
const basicSubmissions = Array.isArray(submissionData)
? submissionData
: [];
// 2. Fetch full details for each submission to get user info
// We do this by fetching the project details, as submission endpoints lack user data
const detailsPromises = basicSubmissions.map(async (sub: any) => {
try {
// Check if we already have sufficient user data
if (
sub.participant?.user?.profile?.firstName ||
sub.participant?.name
)
return sub;
// Try fetch project details if we have projectId
if (sub.projectId) {
const project = await getCrowdfundingProject(sub.projectId);
if (project && project.project && project.project.creator) {
const creator = project.project.creator;
return {
...sub,
participant: {
...sub.participant,
// Use creator info for participant
name: creator.name,
username: creator.username,
image: creator.image,
email: creator.email,
user: {
...sub.participant?.user,
name: creator.name,
username: creator.username,
image: creator.image,
email: creator.email,
profile: {
...sub.participant?.user?.profile,
firstName: creator.name?.split(' ')[0] || '',
lastName:
creator.name?.split(' ').slice(1).join(' ') || '',
username: creator.username,
avatar: creator.image,
},
},
},
};
}
}
// Fallback to submission details check if project fail or no projectId
const detailsRes = await getSubmissionDetails(sub.id);
if (detailsRes.success && detailsRes.data) {
const details = detailsRes.data as any;
return {
...sub,
participant: {
...sub.participant,
...details.participant,
user: details.participant?.user || sub.participant?.user,
},
};
}
return sub;
} catch (err) {
console.error(
`Failed to fetch details for submission ${sub.id}`,
err
);
return sub;
}
});
enrichedSubmissions = await Promise.all(detailsPromises);
setSubmissions(enrichedSubmissions);
} else {
setSubmissions([]);
}
// Handle criteria response safely
setCriteria(Array.isArray(criteriaRes) ? criteriaRes : []);
} catch (error) {
console.error('Judging data fetch error:', error);
toast.error('Failed to load judging data');
} finally {
setIsLoading(false);
}
}, [organizationId, hackathonId, fetchJudges, fetchResults]);
🤖 Prompt for AI Agents
In `@app/`(landing)/organizations/[id]/hackathons/[hackathonId]/judging/page.tsx
around lines 174 - 283, fetchData currently calls fetchJudges() and
fetchResults() without awaiting them, so setIsLoading(false) fires before those
requests finish; change to await their execution (preferably in parallel) so
loading/error handling covers them — e.g. replace the fire-and-forget calls
inside fetchData with awaiting a combined promise (Promise.all or
Promise.allSettled) of fetchJudges() and fetchResults(), and handle their
failures (propagate or log/throw) so the catch block's toast is triggered; keep
names fetchData, fetchJudges, fetchResults, setIsLoading and ensure finally only
runs after those awaited calls complete.

Comment on lines 289 to +292
const handleSuccess = () => {
fetchSubmissions(page);
fetchData();
fetchResults(); // Refresh results to update metrics/table
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

handleSuccess double-triggers fetchResults.

fetchData() already calls fetchResults() internally (line 187), so the explicit fetchResults() on line 291 causes a redundant API call every time a grade is submitted.

Proposed fix
   const handleSuccess = () => {
     fetchData();
-    fetchResults(); // Refresh results to update metrics/table
   };
🤖 Prompt for AI Agents
In `@app/`(landing)/organizations/[id]/hackathons/[hackathonId]/judging/page.tsx
around lines 289 - 292, handleSuccess is redundantly calling fetchResults() even
though fetchData() already invokes fetchResults() (see fetchData and
fetchResults). Remove the explicit fetchResults() call from handleSuccess so it
only calls fetchData(); ensure handleSuccess remains the post-submit callback
that triggers fetchData() which in turn refreshes results/metrics.

Comment on lines +516 to +521
<p className='text-sm font-medium'>
{judge.name}
</p>
<p className='text-xs text-gray-500'>
{judge.userId}
</p>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Displaying raw judge.userId to the user.

Line 520 renders the internal userId under each judge name. This is likely not meaningful to end users and could be a minor PII/internal-data exposure concern. Consider showing the judge's email or role instead.

                              <p className='text-xs text-gray-500'>
-                                {judge.userId}
+                                {judge.email || judge.role || ''}
                              </p>
🤖 Prompt for AI Agents
In `@app/`(landing)/organizations/[id]/hackathons/[hackathonId]/judging/page.tsx
around lines 516 - 521, The UI is rendering internal judge.userId which may
expose PII/internal IDs; update the rendering to show a user-facing attribute
(e.g., judge.email or judge.role) instead of judge.userId, or remove the second
<p> entirely if no friendly field exists. Locate the JSX that displays
{judge.name} and {judge.userId} and replace {judge.userId} with the appropriate
display field (judge.email or judge.role) and add a safe fallback (e.g.,
fallback text like "No contact info") to avoid rendering raw IDs.

Comment on lines +247 to +283
// Participation type enforcement
useEffect(() => {
if (!open || !currentHackathon) return;
if (hasAutoAdvanced.current) return;

const hackathonType = currentHackathon.participantType;

if (hackathonType === 'INDIVIDUAL') {
form.setValue('participationType', 'INDIVIDUAL');
if (!submissionId) {
setCurrentStep(1);
updateStepState(0, 'completed');
updateStepState(1, 'active');
hasAutoAdvanced.current = true;
}
} else if (hackathonType === 'TEAM') {
form.setValue('participationType', 'TEAM');
if (!submissionId && !!myTeam) {
setCurrentStep(1);
updateStepState(0, 'completed');
updateStepState(1, 'active');
hasAutoAdvanced.current = true;
}
} else if (hackathonType === 'TEAM_OR_INDIVIDUAL') {
if (!submissionId) {
if (myTeam) {
form.setValue('participationType', 'TEAM');
setCurrentStep(1);
updateStepState(0, 'completed');
updateStepState(1, 'active');
hasAutoAdvanced.current = true;
} else {
form.setValue('participationType', 'INDIVIDUAL');
}
}
}
}, [open, currentHackathon, myTeam, submissionId, form, updateStepState]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Late myTeam load can override manual user progress.

If hackathonType is TEAM_OR_INDIVIDUAL (or TEAM) and myTeam is initially undefined, hasAutoAdvanced stays false. If the user manually selects INDIVIDUAL and navigates forward, a later async resolution of myTeam will re-trigger this effect, overwrite participationType to TEAM, and call setCurrentStep(1) — yanking the user back and discarding their choice.

Consider setting hasAutoAdvanced.current = true when the user manually advances past step 0 (e.g., in handleNext), so this effect can no longer fire.

Proposed fix

In handleNext, after successfully advancing from step 0:

   if (isValid && currentStep < steps.length - 1) {
+    if (currentStep === 0) {
+      hasAutoAdvanced.current = true;
+    }
     updateStepState(currentStep, 'completed');
     updateStepState(currentStep + 1, 'active');
     setCurrentStep(currentStep + 1);
   }
🤖 Prompt for AI Agents
In `@components/hackathons/submissions/SubmissionForm.tsx` around lines 247 - 283,
The effect that enforces participation type can later override a user’s manual
choice because hasAutoAdvanced.current is only set when the effect itself
auto-advances; modify the manual advancement logic (e.g., in the handleNext
handler) so that when the user successfully advances past step 0 (the same
transition where setCurrentStep(1) and updateStepState(0/1) are called), you
also set hasAutoAdvanced.current = true; this prevents the useEffect (which
reads currentHackathon, myTeam, submissionId, and calls form.setValue /
setCurrentStep) from re-running later and clobbering the user’s manually chosen
participationType.

Comment on lines +99 to +163
// Validate website URL
useEffect(() => {
if (!debouncedWebsite) {
setValidationStates(prev => ({
...prev,
website: { isValid: null, isValidating: false, error: null },
}));
return;
}

const validate = async () => {
setValidationStates(prev => ({
...prev,
website: { ...prev.website, isValidating: true },
}));
const result = await validateWebsite(debouncedWebsite);
setValidationStates(prev => ({ ...prev, website: result }));
};

validate();
}, [debouncedWebsite, validateWebsite]);

// Validate X handle
useEffect(() => {
if (!debouncedX) {
setValidationStates(prev => ({
...prev,
x: { isValid: null, isValidating: false, error: null },
}));
return;
}

const validate = async () => {
setValidationStates(prev => ({
...prev,
x: { ...prev.x, isValidating: true },
}));
const result = await validateXHandle(debouncedX);
setValidationStates(prev => ({ ...prev, x: result }));
};

validate();
}, [debouncedX, validateXHandle]);

// Validate GitHub handle
useEffect(() => {
if (!debouncedGithub) {
setValidationStates(prev => ({
...prev,
github: { isValid: null, isValidating: false, error: null },
}));
return;
}

const validate = async () => {
setValidationStates(prev => ({
...prev,
github: { ...prev.github, isValidating: true },
}));
const result = await validateGithubHandle(debouncedGithub);
setValidationStates(prev => ({ ...prev, github: result }));
};

validate();
}, [debouncedGithub, validateGithubHandle]);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Stale async results & heavy duplication across three identical validation effects.

Two concerns:

  1. Race condition on rapid input: If two debounced values fire in quick succession, there's no cancellation of the previous validate() call. The earlier promise can resolve after the later one, overwriting the correct result with a stale one. Use an isCancelled flag (or AbortController) in each effect's cleanup to discard outdated results.

  2. DRY violation: All three effects (website, X, GitHub) share the exact same structure. Extract a single helper or a custom hook (e.g., useFieldValidation(debouncedValue, validatorFn, fieldKey)) that encapsulates the debounce-triggered validation + cancellation logic.

Example: cancellation guard + extracted helper
-  // Validate website URL
-  useEffect(() => {
-    if (!debouncedWebsite) {
-      setValidationStates(prev => ({
-        ...prev,
-        website: { isValid: null, isValidating: false, error: null },
-      }));
-      return;
-    }
-
-    const validate = async () => {
-      setValidationStates(prev => ({
-        ...prev,
-        website: { ...prev.website, isValidating: true },
-      }));
-      const result = await validateWebsite(debouncedWebsite);
-      setValidationStates(prev => ({ ...prev, website: result }));
-    };
-
-    validate();
-  }, [debouncedWebsite, validateWebsite]);
+  // Reusable validation runner with stale-result cancellation
+  const useFieldValidation = (
+    debouncedValue: string,
+    validatorFn: (v: string) => Promise<ValidationResult>,
+    fieldKey: 'website' | 'x' | 'github'
+  ) => {
+    useEffect(() => {
+      let cancelled = false;
+      if (!debouncedValue) {
+        setValidationStates(prev => ({
+          ...prev,
+          [fieldKey]: { isValid: null, isValidating: false, error: null },
+        }));
+        return;
+      }
+      setValidationStates(prev => ({
+        ...prev,
+        [fieldKey]: { ...prev[fieldKey], isValidating: true },
+      }));
+      validatorFn(debouncedValue).then(result => {
+        if (!cancelled) {
+          setValidationStates(prev => ({ ...prev, [fieldKey]: result }));
+        }
+      });
+      return () => { cancelled = true; };
+    }, [debouncedValue, validatorFn, fieldKey]);
+  };
+
+  useFieldValidation(debouncedWebsite, validateWebsite, 'website');
+  useFieldValidation(debouncedX, validateXHandle, 'x');
+  useFieldValidation(debouncedGithub, validateGithubHandle, 'github');

(The helper above is inline for brevity — ideally move it to its own hook file.)

🤖 Prompt for AI Agents
In `@components/organization/tabs/LinksTab.tsx` around lines 99 - 163, The three
near-identical useEffect blocks (validating debouncedWebsite, debouncedX,
debouncedGithub using validateWebsite/validateXHandle/validateGithubHandle and
updating setValidationStates) are duplicated and prone to stale async results;
extract a helper/custom hook (e.g., useFieldValidation(debouncedValue,
validatorFn, fieldKey)) that encapsulates the shared logic: set isValidating
true, call the validator, and in the effect cleanup set an isCancelled flag (or
use AbortController) so the async result is ignored if cancelled; only call
setValidationStates(fieldKey, result) when not cancelled, and ensure
empty/cleared state is handled when debouncedValue is falsy.

Comment on lines +185 to 225
if (loadingMembers) {
return (
<div className='relative h-[calc(100vh-300px)] space-y-8'>
{loadingui && (
<div className='absolute top-0 right-0 bottom-0 left-0 z-10 flex items-center justify-center bg-black/50 backdrop-blur-sm'>
<LoadingSpinner size='lg' className='z-20 text-white' />
<div className='absolute inset-0 z-10 flex items-center justify-center rounded-[12px] bg-black/50 backdrop-blur-sm'>
<LoadingSpinner size='lg' className='z-20 text-white' />
</div>
</div>
);
}
// Show permission denied message if not owner
if (!loadingMembers && !loadingOwnerCheck && !isCurrentUserOwner) {
return (
<div className='bg-background-card rounded-[12px] border border-gray-900 p-6'>
<div className='flex flex-col items-center justify-center space-y-4 py-8'>
<div className='rounded-full bg-red-500/10 p-3'>
<svg
className='h-8 w-8 text-red-500'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z'
/>
</svg>
</div>
)}
<div className='text-center'>
<h3 className='text-lg font-medium text-white'>
Permission Denied
</h3>
<p className='mt-2 text-sm text-gray-400'>
Only the organization owner can transfer ownership.
</p>
</div>
</div>
</div>
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Early-return ordering lets non-owners see the members loading overlay.

The loadingMembers check (Line 185) precedes the permission-denied check (Line 195). If both loadingOwnerCheck is false and isCurrentUserOwner is false, but loadingMembers is still true (due to the parallel fetch), the user briefly sees a loading overlay instead of the permission-denied screen. Move the permission check above the members-loading check:

Proposed fix
+  // Show permission denied message if not owner
+  if (!loadingOwnerCheck && !isCurrentUserOwner) {
+    return (
+      <div className='bg-background-card rounded-[12px] border border-gray-900 p-6'>
+        ...permission denied UI...
+      </div>
+    );
+  }
+
   if (loadingMembers) {
     return (
       ...loading overlay...
     );
   }
-  // Show permission denied message if not owner
-  if (!loadingMembers && !loadingOwnerCheck && !isCurrentUserOwner) {
-    ...
-  }

This becomes a non-issue if you also gate fetchMembers behind the ownership check (previous comment), but the ordering is still worth correcting defensively.

📝 Committable suggestion

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

Suggested change
if (loadingMembers) {
return (
<div className='relative h-[calc(100vh-300px)] space-y-8'>
{loadingui && (
<div className='absolute top-0 right-0 bottom-0 left-0 z-10 flex items-center justify-center bg-black/50 backdrop-blur-sm'>
<LoadingSpinner size='lg' className='z-20 text-white' />
<div className='absolute inset-0 z-10 flex items-center justify-center rounded-[12px] bg-black/50 backdrop-blur-sm'>
<LoadingSpinner size='lg' className='z-20 text-white' />
</div>
</div>
);
}
// Show permission denied message if not owner
if (!loadingMembers && !loadingOwnerCheck && !isCurrentUserOwner) {
return (
<div className='bg-background-card rounded-[12px] border border-gray-900 p-6'>
<div className='flex flex-col items-center justify-center space-y-4 py-8'>
<div className='rounded-full bg-red-500/10 p-3'>
<svg
className='h-8 w-8 text-red-500'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z'
/>
</svg>
</div>
)}
<div className='text-center'>
<h3 className='text-lg font-medium text-white'>
Permission Denied
</h3>
<p className='mt-2 text-sm text-gray-400'>
Only the organization owner can transfer ownership.
</p>
</div>
</div>
</div>
);
}
// Show permission denied message if not owner
if (!loadingOwnerCheck && !isCurrentUserOwner) {
return (
<div className='bg-background-card rounded-[12px] border border-gray-900 p-6'>
<div className='flex flex-col items-center justify-center space-y-4 py-8'>
<div className='rounded-full bg-red-500/10 p-3'>
<svg
className='h-8 w-8 text-red-500'
fill='none'
viewBox='0 0 24 24'
stroke='currentColor'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth={2}
d='M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z'
/>
</svg>
</div>
<div className='text-center'>
<h3 className='text-lg font-medium text-white'>
Permission Denied
</h3>
<p className='mt-2 text-sm text-gray-400'>
Only the organization owner can transfer ownership.
</p>
</div>
</div>
</div>
);
}
if (loadingMembers) {
return (
<div className='relative h-[calc(100vh-300px)] space-y-8'>
<div className='absolute inset-0 z-10 flex items-center justify-center rounded-[12px] bg-black/50 backdrop-blur-sm'>
<LoadingSpinner size='lg' className='z-20 text-white' />
</div>
</div>
);
}
🤖 Prompt for AI Agents
In `@components/organization/tabs/TransferOwnershipTab.tsx` around lines 185 -
225, The permission-denied early return is after the members-loading check,
causing non-owners to briefly see the members loading overlay; move the
ownership check (using loadingOwnerCheck and isCurrentUserOwner) above the
loadingMembers check so you return the permission-denied UI immediately when
ownership has been determined false, or alternatively gate fetchMembers behind
the ownership check so loadingMembers never becomes true for non-owners (update
references in TransferOwnershipTab to loadingOwnerCheck, isCurrentUserOwner,
loadingMembers and any fetchMembers call).

Comment on lines +50 to +56
export interface JudgingResult {
submissionId: string;
projectName: string;
averageScore: number;
judgeCount: number;
rank?: any;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

rank typed as any — define its actual shape.

rank?: any discards type information. Based on usage in JudgingResultsTable (which accesses result.rank?.position and checks typeof result.rank === 'number'), it should be a union type:

-  rank?: any;
+  rank?: number | { position: number };
📝 Committable suggestion

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

Suggested change
export interface JudgingResult {
submissionId: string;
projectName: string;
averageScore: number;
judgeCount: number;
rank?: any;
}
export interface JudgingResult {
submissionId: string;
projectName: string;
averageScore: number;
judgeCount: number;
rank?: number | { position: number };
}
🤖 Prompt for AI Agents
In `@lib/api/hackathons/judging.ts` around lines 50 - 56, The JudgingResult
interface currently types rank as any; change it to a proper union reflecting
observed usage: rank can be a number or an object with a numeric position (e.g.,
{ position: number }), so update the JudgingResult type (the rank field) to
rank?: number | { position: number } and adjust any code that treats result.rank
to use the union (e.g., result.rank?.position and typeof result.rank ===
'number' checks in JudgingResultsTable will then be correct).

Comment on lines +388 to +406
export const getJudgingResults = async (
organizationId: string,
hackathonId: string
): Promise<GetJudgingResultsResponse> => {
const res = await api.get<JudgingResult[] | ApiResponse<JudgingResult[]>>(
`/organizations/${organizationId}/hackathons/${hackathonId}/judging/results`
);

// Handle raw array response format
if (Array.isArray(res.data)) {
return {
success: true,
data: res.data,
message: 'Results retrieved successfully',
} as GetJudgingResultsResponse;
}

return (res.data || {}) as GetJudgingResultsResponse;
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Unsafe fallback (res.data || {}) as GetJudgingResultsResponse when API returns a non-array error.

If the API returns { success: false, message: "..." }, the Array.isArray check is false, and the code falls through to line 405. The cast succeeds at compile time, but at runtime the object may lack data, causing downstream code (e.g., setJudgingResults(res.data || [])) to silently produce undefined. The same pattern repeats in getSubmissionScores (line 358), getHackathonJudges (line 457), and getJudgingWinners (line 480).

Consider validating the response shape or at minimum defaulting data:

-  return (res.data || {}) as GetJudgingResultsResponse;
+  const response = res.data as ApiResponse<JudgingResult[]>;
+  return {
+    success: response?.success ?? false,
+    data: response?.data ?? [],
+    message: response?.message ?? '',
+  } as GetJudgingResultsResponse;
🤖 Prompt for AI Agents
In `@lib/api/hackathons/judging.ts` around lines 388 - 406, The non-array API
response in getJudgingResults is being cast unsafely to
GetJudgingResultsResponse, which can leave data undefined at runtime; update
getJudgingResults to validate the response shape and return a safe default
(e.g., { success: false, message: res.data?.message || 'Error', data: [] } or {
success: true, data: [], message: 'Results retrieved successfully' } when
appropriate) instead of using (res.data || {}) as GetJudgingResultsResponse;
apply the same defensive fix to getSubmissionScores, getHackathonJudges, and
getJudgingWinners by checking whether res.data is the expected object with a
.data field (or an array where applicable) and supplying an explicit data
fallback so callers never receive undefined.

Comment on lines +1290 to +1294
query: {
organizationId: orgId,
limit: 100, // Should be enough to find the owner
},
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Magic limit: 100 may silently miss the current user in large orgs.

If an organization has more than 100 members, listMembers may not return the current user in the first page, causing the demotion step to silently skip. Consider paginating or using a dedicated endpoint to look up the current user's membership directly, if one is available.

🤖 Prompt for AI Agents
In `@lib/providers/OrganizationProvider.tsx` around lines 1290 - 1294, The call to
listMembers with a hardcoded query { organizationId: orgId, limit: 100 } (used
around the demotion step in OrganizationProvider.tsx) can miss the current user
for orgs >100 members; replace this by either paginating through listMembers
until you find the current user or, preferably, call a dedicated membership
lookup (e.g., getMembership or listMemberById) if available to fetch the current
user's membership directly; update the logic that checks for the current user's
role (the demotion branch that relies on the listMembers result) to use the
found membership from pagination or the direct lookup and handle the case where
membership is not found by retrying/paging or throwing a clear error.

@coderabbitai coderabbitai bot mentioned this pull request Feb 16, 2026
Benjtalkshow added a commit that referenced this pull request Feb 19, 2026
* Judging dashboard (#382)

* fix: modify api.ts

* fix: remove google auth buttom

* fix: fixes responsive fixes on organization

* fix: minor fixes

* fix: modify create organization

* fix: modify create organization

* fix: fix organization permission

* fix: merge into main

* feat: hackathon overview page

* feat: hackathon overview page

* feat: implement participant overview

* feat: implement participant overview

* feat: implement resources tab

* feat: implement the submission tab

* feat: implement comment tab

* fix: implement provider for hackathon

* fix: implement provider for hackathon

* fix: minor fixes

* fix: hackathon banner

* fix: hackathon banner

* fix: fix organization page

* fix: fix organization page

* fix: use transform

* fix: add tagline

* fix: add tagline

* fix: minor fixes

* fix: minor fixes

* fix: fix timeline and prizes

* fix: correct timeline events

* fix: implement registration deadline policy

* fix: implement registration deadline policy

* feat: implement leave hackathon

* feat: implement leave hackathon

* fix: delete hackathon

* fix: implement invite participants

* fix: implement participant profile viewing

* feat: fetch participants team

* fix: redesign hackathon banner

* fix: fix hackthon card

* fix: fix search bar in blog page

* fix: fix search bar in blog page

* fix: fix search bar in blog page

* fix: fix error in fetching team posts

* feat: implement create team, get my team

* feat: implement create team, get my team

* feat: implement hackathon project submission flow

* feat: implement voting for submission

* fix: team formation updates

* fix: implement team invitation

* feat: hackathon submissions bulk actions, ranking and ui refinements

* fix: implement empty state for hackathons page

* feat: Implement  submission visibility and explore submissions

* feat: Implement winners tab for hackathon winners display

* feat: implement hackathon analytics

* fix: fix coderabbit corrections

* fix: fix coderabbit corrections

* fix: fix coderabbit corrections

* fix: fix organization settings data persistence, hackathondrafts and population

* feat: fix user profile

* feat: Implemented the participant-facing and hackathon organizers announcement features

* feat: Implement judging dashboard and detailed breakdowns

* feat: Implement judging dashboard and detailed breakdowns

* fix: fix coderabbit corrections

* Refactor/updated the timeline tab (#383)

* feat: enhance hackathon timeline management and publishing flow

- Updated HackathonPublishedModal to utilize new publish response structure.
- Refactored TimelineSection to display additional timeline details including judging start/end and winner announcement dates.
- Introduced DateTimeInput component for better date and time selection.
- Added FieldLabel component for improved form labeling with tooltips.
- Created timelineConstants for standardized tooltips and timezone options.
- Modified timelineSchema to include new fields for judging start, end, and winner announcement dates with validation.
- Enhanced TimelineSettingsTab to support new timeline fields and improved user experience with time inputs.
- Updated use-hackathon-publish hook to handle new publish response format and manage state accordingly.
- Adjusted use-hackathons hook to ensure proper handling of publish responses.
- Updated API interfaces and utility functions to accommodate new timeline structure.
- Improved validation logic for hackathon steps to ensure all necessary dates are set correctly.
- Enhanced timeline calculation logic to reflect new judging and announcement phases.

* chore: update markdown-it to version 14.1.1 and remove framer-motion dependency

* refactor: clean up ReviewTab and HackathonPublishedModal components, improve DateTimeInput logic, and update timeline constants

* refactor: update timeline calculation logic to ensure phases are only created with non-zero duration and add endDate to HackathonTimeline interface

* Judging dashboard (#384)

* fix: modify api.ts

* fix: remove google auth buttom

* fix: fixes responsive fixes on organization

* fix: minor fixes

* fix: modify create organization

* fix: modify create organization

* fix: fix organization permission

* fix: merge into main

* feat: hackathon overview page

* feat: hackathon overview page

* feat: implement participant overview

* feat: implement participant overview

* feat: implement resources tab

* feat: implement the submission tab

* feat: implement comment tab

* fix: implement provider for hackathon

* fix: implement provider for hackathon

* fix: minor fixes

* fix: hackathon banner

* fix: hackathon banner

* fix: fix organization page

* fix: fix organization page

* fix: use transform

* fix: add tagline

* fix: add tagline

* fix: minor fixes

* fix: minor fixes

* fix: fix timeline and prizes

* fix: correct timeline events

* fix: implement registration deadline policy

* fix: implement registration deadline policy

* feat: implement leave hackathon

* feat: implement leave hackathon

* fix: delete hackathon

* fix: implement invite participants

* fix: implement participant profile viewing

* feat: fetch participants team

* fix: redesign hackathon banner

* fix: fix hackthon card

* fix: fix search bar in blog page

* fix: fix search bar in blog page

* fix: fix search bar in blog page

* fix: fix error in fetching team posts

* feat: implement create team, get my team

* feat: implement create team, get my team

* feat: implement hackathon project submission flow

* feat: implement voting for submission

* fix: team formation updates

* fix: implement team invitation

* feat: hackathon submissions bulk actions, ranking and ui refinements

* fix: implement empty state for hackathons page

* feat: Implement  submission visibility and explore submissions

* feat: Implement winners tab for hackathon winners display

* feat: implement hackathon analytics

* fix: fix coderabbit corrections

* fix: fix coderabbit corrections

* fix: fix coderabbit corrections

* fix: fix organization settings data persistence, hackathondrafts and population

* feat: fix user profile

* feat: Implemented the participant-facing and hackathon organizers announcement features

* feat: Implement judging dashboard and detailed breakdowns

* feat: Implement judging dashboard and detailed breakdowns

* fix: fix coderabbit corrections

* fix: submission management ux refinements

* fix: submission management ux refinements

* fix: submission management ux refinements

* fix: submission management ux refinements

* fix: submission management ux refinements

* feat: implement organizer override functionality for judging scores a… (#385)

* feat: implement organizer override functionality for judging scores and enhance judging UI

* fix: update response handling in score submission and enhance submissions list styling

* fix: update eslint dependencies to latest versions for improved linting

* fix: update ajv and json-schema-traverse dependencies in package-lock.json

* fix: update

* fix: update security audit level to high in pre-push checks

* fix: update eslint dependencies and ensure security audit fails on error

---------

Co-authored-by: Collins Ikechukwu <collinschristroa@gmail.com>
0xdevcollins added a commit that referenced this pull request Feb 21, 2026
* Judging dashboard (#382)

* fix: modify api.ts

* fix: remove google auth buttom

* fix: fixes responsive fixes on organization

* fix: minor fixes

* fix: modify create organization

* fix: modify create organization

* fix: fix organization permission

* fix: merge into main

* feat: hackathon overview page

* feat: hackathon overview page

* feat: implement participant overview

* feat: implement participant overview

* feat: implement resources tab

* feat: implement the submission tab

* feat: implement comment tab

* fix: implement provider for hackathon

* fix: implement provider for hackathon

* fix: minor fixes

* fix: hackathon banner

* fix: hackathon banner

* fix: fix organization page

* fix: fix organization page

* fix: use transform

* fix: add tagline

* fix: add tagline

* fix: minor fixes

* fix: minor fixes

* fix: fix timeline and prizes

* fix: correct timeline events

* fix: implement registration deadline policy

* fix: implement registration deadline policy

* feat: implement leave hackathon

* feat: implement leave hackathon

* fix: delete hackathon

* fix: implement invite participants

* fix: implement participant profile viewing

* feat: fetch participants team

* fix: redesign hackathon banner

* fix: fix hackthon card

* fix: fix search bar in blog page

* fix: fix search bar in blog page

* fix: fix search bar in blog page

* fix: fix error in fetching team posts

* feat: implement create team, get my team

* feat: implement create team, get my team

* feat: implement hackathon project submission flow

* feat: implement voting for submission

* fix: team formation updates

* fix: implement team invitation

* feat: hackathon submissions bulk actions, ranking and ui refinements

* fix: implement empty state for hackathons page

* feat: Implement  submission visibility and explore submissions

* feat: Implement winners tab for hackathon winners display

* feat: implement hackathon analytics

* fix: fix coderabbit corrections

* fix: fix coderabbit corrections

* fix: fix coderabbit corrections

* fix: fix organization settings data persistence, hackathondrafts and population

* feat: fix user profile

* feat: Implemented the participant-facing and hackathon organizers announcement features

* feat: Implement judging dashboard and detailed breakdowns

* feat: Implement judging dashboard and detailed breakdowns

* fix: fix coderabbit corrections

* Refactor/updated the timeline tab (#383)

* feat: enhance hackathon timeline management and publishing flow

- Updated HackathonPublishedModal to utilize new publish response structure.
- Refactored TimelineSection to display additional timeline details including judging start/end and winner announcement dates.
- Introduced DateTimeInput component for better date and time selection.
- Added FieldLabel component for improved form labeling with tooltips.
- Created timelineConstants for standardized tooltips and timezone options.
- Modified timelineSchema to include new fields for judging start, end, and winner announcement dates with validation.
- Enhanced TimelineSettingsTab to support new timeline fields and improved user experience with time inputs.
- Updated use-hackathon-publish hook to handle new publish response format and manage state accordingly.
- Adjusted use-hackathons hook to ensure proper handling of publish responses.
- Updated API interfaces and utility functions to accommodate new timeline structure.
- Improved validation logic for hackathon steps to ensure all necessary dates are set correctly.
- Enhanced timeline calculation logic to reflect new judging and announcement phases.

* chore: update markdown-it to version 14.1.1 and remove framer-motion dependency

* refactor: clean up ReviewTab and HackathonPublishedModal components, improve DateTimeInput logic, and update timeline constants

* refactor: update timeline calculation logic to ensure phases are only created with non-zero duration and add endDate to HackathonTimeline interface

* Judging dashboard (#384)

* fix: modify api.ts

* fix: remove google auth buttom

* fix: fixes responsive fixes on organization

* fix: minor fixes

* fix: modify create organization

* fix: modify create organization

* fix: fix organization permission

* fix: merge into main

* feat: hackathon overview page

* feat: hackathon overview page

* feat: implement participant overview

* feat: implement participant overview

* feat: implement resources tab

* feat: implement the submission tab

* feat: implement comment tab

* fix: implement provider for hackathon

* fix: implement provider for hackathon

* fix: minor fixes

* fix: hackathon banner

* fix: hackathon banner

* fix: fix organization page

* fix: fix organization page

* fix: use transform

* fix: add tagline

* fix: add tagline

* fix: minor fixes

* fix: minor fixes

* fix: fix timeline and prizes

* fix: correct timeline events

* fix: implement registration deadline policy

* fix: implement registration deadline policy

* feat: implement leave hackathon

* feat: implement leave hackathon

* fix: delete hackathon

* fix: implement invite participants

* fix: implement participant profile viewing

* feat: fetch participants team

* fix: redesign hackathon banner

* fix: fix hackthon card

* fix: fix search bar in blog page

* fix: fix search bar in blog page

* fix: fix search bar in blog page

* fix: fix error in fetching team posts

* feat: implement create team, get my team

* feat: implement create team, get my team

* feat: implement hackathon project submission flow

* feat: implement voting for submission

* fix: team formation updates

* fix: implement team invitation

* feat: hackathon submissions bulk actions, ranking and ui refinements

* fix: implement empty state for hackathons page

* feat: Implement  submission visibility and explore submissions

* feat: Implement winners tab for hackathon winners display

* feat: implement hackathon analytics

* fix: fix coderabbit corrections

* fix: fix coderabbit corrections

* fix: fix coderabbit corrections

* fix: fix organization settings data persistence, hackathondrafts and population

* feat: fix user profile

* feat: Implemented the participant-facing and hackathon organizers announcement features

* feat: Implement judging dashboard and detailed breakdowns

* feat: Implement judging dashboard and detailed breakdowns

* fix: fix coderabbit corrections

* fix: submission management ux refinements

* fix: submission management ux refinements

* fix: submission management ux refinements

* fix: submission management ux refinements

* fix: submission management ux refinements

* feat: implement organizer override functionality for judging scores a… (#385)

* feat: implement organizer override functionality for judging scores and enhance judging UI

* fix: update response handling in score submission and enhance submissions list styling

* fix: update eslint dependencies to latest versions for improved linting

* fix: update ajv and json-schema-traverse dependencies in package-lock.json

* fix: update

* fix: update security audit level to high in pre-push checks

* fix: update eslint dependencies and ensure security audit fails on error

* refactor: Dynamically determine Stellar network and centralize wallet utility functions, including explorer URLs and API key handling.

---------

Co-authored-by: Nnaji Benjamin <60315147+Benjtalkshow@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant