Skip to content

Winners tab refactor#438

Merged
Benjtalkshow merged 21 commits into
boundlessfi:mainfrom
Michaelkingsdev:winners-tab-refactor
Mar 4, 2026
Merged

Winners tab refactor#438
Benjtalkshow merged 21 commits into
boundlessfi:mainfrom
Michaelkingsdev:winners-tab-refactor

Conversation

@Michaelkingsdev
Copy link
Copy Markdown
Contributor

@Michaelkingsdev Michaelkingsdev commented Mar 4, 2026

Closes #399

Winners Tab Refactor — Premium Organizer Design Integration

Overview

This PR overhauls the participant-facing Winners Tab to match the premium Podium design already established in the organizer's PublishWinnersWizard.

The previous basic card grid has been replaced with a high-fidelity visual hierarchy that mirrors the administrative view.


Key Changes

UI & Design Replication

  • Podium Layout

    • Implemented a 1st–2nd–3rd podium effect (2nd → 1st → 3rd)
    • Elevated scaling effect for the 1st place winner
  • Premium Components

    • Integrated canonical Ribbon component for rank display
    • Added high-quality Avatar circles with initials fallback
  • Project Preview

    • Each winner card now includes a styled project preview tile
  • USDC Branding

    • Inline prize amounts now include a trophy icon for consistent financial branding

Deep Linking & Navigation

  • Project Links

    • Project names and thumbnails link to:
      /projects/${projectId}?type=submission
  • Participant Links

    • Participant names and avatars link to:
      /profile/${username}
  • Graceful Fallbacks

    • Safe rendering for missing metadata (null IDs or usernames)
    • Prevents broken links and rendering errors

Technical Details

  • Component Updated

    • components/hackathons/winners/WinnersTab.tsx
  • Data Flow

    • Strictly uses useHackathonData() hook
    • No redundant API calls introduced
  • Type Safety

    • Fully typed using HackathonWinner interface
    • Removed all any types
  • Animation

    • Added entrance animations via motion/react for premium UX polish

Video Proof

Summary by CodeRabbit

  • New Features

    • Dedicated podium section for top‑3 winners with reordered podium presentation and rank ribbons
    • Participant avatars, tooltips, project badges, and optional project links on winner cards
    • Separate grid for ranks 4+ and a badge showing total winners on the Winners tab
  • Style

    • Responsive two‑section layout, refined card visuals, prize header for top entries, and updated entry animations for smoother reveal

@vercel
Copy link
Copy Markdown

vercel Bot commented Mar 4, 2026

@Michaelkingsdev 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
Copy Markdown

coderabbitai Bot commented Mar 4, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Refactors WinnersTab into a podium-aware layout: sorts winners by rank, extracts/reorders top three into a podium ([2,1,3]) for dedicated rendering, renders ranks 4+ in a separate responsive grid, and updates WinnerCard for podium styling, avatars, ribbons, tooltips, prize/trophy header, and deep links.

Changes

Cohort / File(s) Summary
Winners Tab (main change)
components/hackathons/winners/WinnersTab.tsx
Component now destructures only winners; sorts by rank, extracts podiumToDisplay (reordered [2,1,3]) and otherWinners, and renders a responsive podium section plus a grid for ranks 4+.
Winner Card & UI additions
components/hackathons/winners/WinnersTab.tsx
Refactored WinnerCard props to { winner, isPodium?: boolean }; added Ribbon overlay (dynamic colors/text), Avatar/AvatarImage/AvatarFallback, Tooltip, Badge, prize/trophy header, participant avatars/team info, project/profile deep links, and updated Framer Motion whileInView animation.
Imports / utilities
components/hackathons/winners/WinnersTab.tsx
Added imports for Avatar, Badge, Tooltip, Ribbon, cn, getRibbonColors, getRibbonText, and other UI/util helpers to support new presentation and interactions.
HackathonPageClient minor
app/(landing)/hackathons/[slug]/HackathonPageClient.tsx
Adds badge: winners.length to the Winners tab configuration (UI-only change).

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant WinnersTab
  participant WinnerCard
  participant Router

  User->>WinnersTab: page loads with winners[]
  WinnersTab->>WinnersTab: sort winners, split podium & others
  WinnersTab->>WinnerCard: render podium cards (isPodium=true)
  WinnersTab->>WinnerCard: render other winner cards (isPodium=false)
  User->>WinnerCard: click project area
  WinnerCard->>Router: navigate to /projects/{projectId}?type=submission
  User->>WinnerCard: click avatar/name
  WinnerCard->>Router: navigate to /profile/{username}
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • 0xdevcollins

Poem

🐇 I hopped to sort the winners three,
Ribbons bright and podium glee,
Avatars shine, links swift and true,
Prizes gleam in token blue,
I thump my paw — the view's anew!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes one minor out-of-scope change: adding a badge property to the Winners tab in HackathonPageClient.tsx that displays winners.length, which is not mentioned in issue #399 objectives. Remove the badge property addition from HackathonPageClient.tsx unless it is a separate tracked requirement, or clarify its necessity as part of the refactor scope.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Winners tab refactor' is concise and directly reflects the main change of reworking the WinnersTab component structure and visual design.
Linked Issues check ✅ Passed The PR implements all core requirements from #399: podium layout with podium-aware responsive design, Ribbon component for rank display, Avatar components with fallbacks, project/participant deep links with correct query parameters, graceful fallback rendering for missing data, and type-safe data mapping with no additional API calls.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

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

✨ Finishing Touches
🧪 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
Copy Markdown

@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: 2

🧹 Nitpick comments (4)
components/hackathons/winners/WinnersTab.tsx (4)

23-28: Consider removing unused hackathonSlug from destructuring or interface.

The hackathonSlug prop is defined in WinnersTabProps (line 25) but is intentionally not used in the component. While the call site still passes it, this creates a disconnect. Consider either:

  1. Removing hackathonSlug from WinnersTabProps if it's no longer needed, or
  2. Adding a comment explaining why it's retained (e.g., for future use or API consistency).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/hackathons/winners/WinnersTab.tsx` around lines 23 - 28, The
WinnersTabProps currently declares hackathonSlug but WinnersTab destructures
only winners, leaving hackathonSlug unused; either remove hackathonSlug from the
WinnersTabProps interface (and any call-sites that only pass it for this
component) or keep it and add an inline comment in the WinnersTabProps
definition explaining it’s intentionally retained for API consistency/future
use; update the interface named WinnersTabProps and/or the component signature
for WinnersTab to reflect the chosen approach so the prop list and usage are
consistent.

183-186: Potential undefined alt attribute value.

Line 183 uses alt={p.username} directly, but line 185 uses p.username?.charAt(0) suggesting username might be undefined. Per the HackathonWinner type, username is a required string, so this should be safe. However, the defensive coding on line 185 creates inconsistency.

Consider aligning both usages for consistency:

💡 Suggested consistency fix
-              <AvatarImage src={p.avatar} alt={p.username} />
+              <AvatarImage src={p.avatar} alt={p.username || 'Participant'} />
               <AvatarFallback className='bg-gray-800 text-lg text-white uppercase'>
-                {p.username?.charAt(0) || '?'}
+                {p.username.charAt(0)}
               </AvatarFallback>

Or keep the defensive approach on both if there's concern about runtime data:

-              <AvatarImage src={p.avatar} alt={p.username} />
+              <AvatarImage src={p.avatar} alt={p.username ?? 'Participant'} />
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/hackathons/winners/WinnersTab.tsx` around lines 183 - 186, The
Avatar usage is inconsistent: AvatarImage sets alt={p.username} while
AvatarFallback defensively uses p.username?.charAt(0); align them by choosing
one approach—either rely on the HackathonWinner required string and use
p.username in both places, or defensively handle possible undefined values in
both places (e.g., alt={p.username ?? 'Unknown'} and AvatarFallback using
p.username?.charAt(0) || '?'). Update the AvatarImage and AvatarFallback usages
(references: AvatarImage, AvatarFallback, p.username, HackathonWinner)
accordingly so they follow the same defensive or non-defensive pattern.

208-210: Minor: Avoid redundant function calls.

getRibbonColors(winner.rank) is called twice to extract primaryColor and secondaryColor. Consider destructuring once for cleaner code.

♻️ Suggested optimization
+        const { primaryColor, secondaryColor } = getRibbonColors(winner.rank);
         <Ribbon
-          primaryColor={getRibbonColors(winner.rank).primaryColor}
-          secondaryColor={getRibbonColors(winner.rank).secondaryColor}
+          primaryColor={primaryColor}
+          secondaryColor={secondaryColor}
           className='w-48'
         />

Note: This requires moving the destructuring to the component body before the return statement.

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

In `@components/hackathons/winners/WinnersTab.tsx` around lines 208 - 210, Call
getRibbonColors(winner.rank) once inside the WinnersTab component body,
destructure its result into primaryColor and secondaryColor (e.g., const {
primaryColor, secondaryColor } = getRibbonColors(winner.rank)), then pass those
variables to the <Ribbon primaryColor={primaryColor}
secondaryColor={secondaryColor} /> props to avoid duplicate calls and improve
readability.

117-119: Consider using submissionId as fallback for project navigation.

Per the HackathonWinner type, submissionId is required while projectId is optional. Currently, when projectId is missing, the project content becomes non-clickable. If the submission page can be accessed via submissionId, consider using it as a fallback to improve user experience.

That said, if the design intentionally requires non-clickable fallbacks when projectId is absent (as stated in PR objectives), this is fine as-is.

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

In `@components/hackathons/winners/WinnersTab.tsx` around lines 117 - 119, The
project link currently uses only winner.projectId so rows become non-clickable
when projectId is missing; update the projectUrl logic (the projectUrl variable)
to fallback to winner.submissionId when projectId is falsy (e.g. projectUrl =
winner.projectId ? `/projects/${winner.projectId}?type=submission` :
winner.submissionId ? `/submissions/${winner.submissionId}` : null) so the UI
remains navigable; ensure you reference HackathonWinner fields winner.projectId
and winner.submissionId and preserve null when neither exists.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/hackathons/winners/WinnersTab.tsx`:
- Around line 190-202: The wrapper elements in the participant avatar mapping
use inconsistent keys which can break React reconciliation: change the div
branch to use the same key pattern as the Link branch (e.g.,
`${p.username}-${i}`) so both branches use an identical key; locate the
conditional that returns profileUrl ? <Link ...> : <div ...> around
AvatarElement and update the div's key to match the Link's key (still
referencing p.username and i).
- Around line 230-233: The external Link rendering in WinnersTab (the JSX branch
using projectUrl and rendering {ProjectContent} inside the Next.js Link) lacks
rel attributes for target="_blank"; update the Link element that wraps
ProjectContent to include rel="noopener noreferrer" whenever target="_blank" is
present (i.e., when projectUrl is truthy) so the rendered anchor includes
rel="noopener noreferrer" to prevent tabnabbing.

---

Nitpick comments:
In `@components/hackathons/winners/WinnersTab.tsx`:
- Around line 23-28: The WinnersTabProps currently declares hackathonSlug but
WinnersTab destructures only winners, leaving hackathonSlug unused; either
remove hackathonSlug from the WinnersTabProps interface (and any call-sites that
only pass it for this component) or keep it and add an inline comment in the
WinnersTabProps definition explaining it’s intentionally retained for API
consistency/future use; update the interface named WinnersTabProps and/or the
component signature for WinnersTab to reflect the chosen approach so the prop
list and usage are consistent.
- Around line 183-186: The Avatar usage is inconsistent: AvatarImage sets
alt={p.username} while AvatarFallback defensively uses p.username?.charAt(0);
align them by choosing one approach—either rely on the HackathonWinner required
string and use p.username in both places, or defensively handle possible
undefined values in both places (e.g., alt={p.username ?? 'Unknown'} and
AvatarFallback using p.username?.charAt(0) || '?'). Update the AvatarImage and
AvatarFallback usages (references: AvatarImage, AvatarFallback, p.username,
HackathonWinner) accordingly so they follow the same defensive or non-defensive
pattern.
- Around line 208-210: Call getRibbonColors(winner.rank) once inside the
WinnersTab component body, destructure its result into primaryColor and
secondaryColor (e.g., const { primaryColor, secondaryColor } =
getRibbonColors(winner.rank)), then pass those variables to the <Ribbon
primaryColor={primaryColor} secondaryColor={secondaryColor} /> props to avoid
duplicate calls and improve readability.
- Around line 117-119: The project link currently uses only winner.projectId so
rows become non-clickable when projectId is missing; update the projectUrl logic
(the projectUrl variable) to fallback to winner.submissionId when projectId is
falsy (e.g. projectUrl = winner.projectId ?
`/projects/${winner.projectId}?type=submission` : winner.submissionId ?
`/submissions/${winner.submissionId}` : null) so the UI remains navigable;
ensure you reference HackathonWinner fields winner.projectId and
winner.submissionId and preserve null when neither exists.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2b11f17a-d82d-4d24-b527-2ca0a34a9e70

📥 Commits

Reviewing files that changed from the base of the PR and between 609b63c and a85fe37.

📒 Files selected for processing (1)
  • components/hackathons/winners/WinnersTab.tsx

Comment thread components/hackathons/winners/WinnersTab.tsx
Comment thread components/hackathons/winners/WinnersTab.tsx
Copy link
Copy Markdown

@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: 2

🧹 Nitpick comments (1)
components/hackathons/winners/WinnersTab.tsx (1)

70-77: Avoid nested ternary for class selection in JSX

This class composition is hard to scan and maintain. Consider moving podium layout selection to a small map/helper and keep JSX className flat.

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

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

In `@components/hackathons/winners/WinnersTab.tsx` around lines 70 - 77, The
nested ternary inside the WinnersTab JSX for computing className is hard to
read; replace it by extracting the logic into a small helper (e.g.,
getPodiumClasses or a PODIUM_CLASS_MAP) that takes podiumToDisplay.length and
returns the appropriate class string, then call
cn(getPodiumClasses(podiumToDisplay.length), 'grid gap-6 md:gap-8') (or use
clsx) so the JSX className is flat and readable; update references to
podiumToDisplay in WinnersTab to use that helper.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/hackathons/winners/WinnersTab.tsx`:
- Around line 117-121: The current projectUrl ternary creates a clickable
/submissions/... fallback when winner.projectId is missing; change it so
projectUrl is only set when winner.projectId exists (i.e.,
`/projects/${winner.projectId}?type=submission`) and otherwise set projectUrl to
null to render a non-clickable fallback; update the declaration of projectUrl in
WinnersTab (the variable named projectUrl that references winner.projectId and
winner.submissionId) accordingly and ensure any rendering logic uses projectUrl
=== null to show the non-clickable UI.
- Around line 223-229: The display name in WinnersTab.tsx is rendered as plain
text; change the single-participant branch so that when winner.teamName is falsy
and winner.participants.length === 1 and winner.participants[0].username exists,
the username is wrapped with a profile link to `/profile/${username}` (using
your project's Link component/anchor pattern) while keeping the existing
styling; locate the block rendering winner.teamName / participant fallback (the
JSX around the h3 in WinnersTab.tsx) and replace the plain text username with a
linked element that preserves classes and accessibility.

---

Nitpick comments:
In `@components/hackathons/winners/WinnersTab.tsx`:
- Around line 70-77: The nested ternary inside the WinnersTab JSX for computing
className is hard to read; replace it by extracting the logic into a small
helper (e.g., getPodiumClasses or a PODIUM_CLASS_MAP) that takes
podiumToDisplay.length and returns the appropriate class string, then call
cn(getPodiumClasses(podiumToDisplay.length), 'grid gap-6 md:gap-8') (or use
clsx) so the JSX className is flat and readable; update references to
podiumToDisplay in WinnersTab to use that helper.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 732a25af-a39d-4633-80d4-c306fcb301e4

📥 Commits

Reviewing files that changed from the base of the PR and between a85fe37 and 90dafe0.

📒 Files selected for processing (1)
  • components/hackathons/winners/WinnersTab.tsx

Comment thread components/hackathons/winners/WinnersTab.tsx Outdated
Comment thread components/hackathons/winners/WinnersTab.tsx
Copy link
Copy Markdown

@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

🧹 Nitpick comments (1)
components/hackathons/winners/WinnersTab.tsx (1)

182-185: Prefer cn conditional object instead of ternary in JSX class composition.

Line 184 uses a ternary for class selection; this should use cn/clsx-style conditional composition per repo convention.

♻️ Suggested refactor
               <Avatar
                 className={cn(
                   'border-background h-16 w-16 border-2 shadow-xl',
-                  profileUrl ? 'transition-transform hover:scale-105' : ''
+                  {
+                    'transition-transform hover:scale-105': Boolean(profileUrl),
+                  }
                 )}
               >

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

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

In `@components/hackathons/winners/WinnersTab.tsx` around lines 182 - 185, Replace
the ternary used inside the cn() call for the avatar classes with a conditional
object form: locate the className prop in WinnersTab (where cn is invoked with
'border-background h-16 w-16 border-2 shadow-xl' and profileUrl), and change the
ternary expression using profileUrl ? 'transition-transform hover:scale-105' :
'' to an object-style conditional entry (e.g., { 'transition-transform
hover:scale-105': profileUrl }) so the clsx/cn convention is followed; keep the
existing static classes and the cn import/usage unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@components/hackathons/winners/WinnersTab.tsx`:
- Around line 52-60: getPodiumOrder currently uses find to pick a single winner
per rank (first/second/third) which drops co-winners; change it to collect all
winners per rank using winners.filter(w => w.rank === 1), .filter(... === 2),
.filter(... === 3), then return an array that concatenates seconds, firsts,
thirds (e.g., [...seconds, ...firsts, ...thirds]) so ties are preserved; also
update the code that builds the remaining/grid list (the logic near line ~63
that excludes podium entries) to exclude all entries with rank 1–3 (use w.rank
=== 1 || w.rank === 2 || w.rank === 3) instead of removing only the single found
winners so tied top ranks are not silently dropped.

---

Nitpick comments:
In `@components/hackathons/winners/WinnersTab.tsx`:
- Around line 182-185: Replace the ternary used inside the cn() call for the
avatar classes with a conditional object form: locate the className prop in
WinnersTab (where cn is invoked with 'border-background h-16 w-16 border-2
shadow-xl' and profileUrl), and change the ternary expression using profileUrl ?
'transition-transform hover:scale-105' : '' to an object-style conditional entry
(e.g., { 'transition-transform hover:scale-105': profileUrl }) so the clsx/cn
convention is followed; keep the existing static classes and the cn import/usage
unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 02eb31c4-67f9-4b79-84cc-80c395bbe440

📥 Commits

Reviewing files that changed from the base of the PR and between 90dafe0 and 29c0e64.

📒 Files selected for processing (1)
  • components/hackathons/winners/WinnersTab.tsx

Comment on lines +52 to +60
const getPodiumOrder = (winners: HackathonWinner[]) => {
if (winners.length < 2) return winners;
const first = winners.find(w => w.rank === 1);
const second = winners.find(w => w.rank === 2);
const third = winners.find(w => w.rank === 3);

return [second, first, third].filter(
(w): w is HackathonWinner => w !== undefined
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Podium extraction can silently drop tied winners in top ranks.

On Line 54–56, find keeps only one winner per rank. If there are co-winners (e.g., two rank-1 entries), extra entries are omitted from both podium and ranks 4+ grid.

💡 Suggested fix
-  const getPodiumOrder = (winners: HackathonWinner[]) => {
-    if (winners.length < 2) return winners;
-    const first = winners.find(w => w.rank === 1);
-    const second = winners.find(w => w.rank === 2);
-    const third = winners.find(w => w.rank === 3);
-
-    return [second, first, third].filter(
-      (w): w is HackathonWinner => w !== undefined
-    );
-  };
-
-  const podiumToDisplay = getPodiumOrder(podiumWinners);
+  const podiumRankOrder = [2, 1, 3] as const;
+  const podiumToDisplay = podiumRankOrder.flatMap(rank =>
+    podiumWinners.filter(w => w.rank === rank)
+  );

Also applies to: 63-63

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

In `@components/hackathons/winners/WinnersTab.tsx` around lines 52 - 60,
getPodiumOrder currently uses find to pick a single winner per rank
(first/second/third) which drops co-winners; change it to collect all winners
per rank using winners.filter(w => w.rank === 1), .filter(... === 2),
.filter(... === 3), then return an array that concatenates seconds, firsts,
thirds (e.g., [...seconds, ...firsts, ...thirds]) so ties are preserved; also
update the code that builds the remaining/grid list (the logic near line ~63
that excludes podium entries) to exclude all entries with rank 1–3 (use w.rank
=== 1 || w.rank === 2 || w.rank === 3) instead of removing only the single found
winners so tied top ranks are not silently dropped.

Copy link
Copy Markdown
Collaborator

@Benjtalkshow Benjtalkshow left a comment

Choose a reason for hiding this comment

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

LGTM!!
I made some fixes to correct the project redirect url.

@Benjtalkshow Benjtalkshow merged commit 06e038b into boundlessfi:main Mar 4, 2026
5 of 7 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Mar 8, 2026
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.

Refactor Winners Tab to Match Organizer Design

2 participants