-
Notifications
You must be signed in to change notification settings - Fork 7
Upgrade staging #184
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Upgrade staging #184
Changes from 10 commits
6f7509a
a70baa4
cd04520
6d30be2
b02e5c6
e03f9bb
bf21834
26c2b29
bce477e
76c37da
ccc02ec
d38e1cc
f68e1cc
7b58d30
f797c5a
bfb2e6c
f8e6c2c
a52e094
37a29a3
2123450
83c1045
765eb74
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| import { ChevronUp } from "lucide-react"; | ||
| import { createColumnHelper } from "@tanstack/react-table"; | ||
| import { LeaderboardEntry } from "../../lib/api"; | ||
| import { UserLink } from "../FeedItem"; | ||
|
|
||
| export interface ExtendedLeaderboardEntry extends LeaderboardEntry { | ||
| originalRank: number; | ||
| } | ||
|
|
||
| export function createLeaderboardColumns( | ||
| expandedRows: number[], | ||
| toggleRow: (index: number) => void, | ||
| ) { | ||
| const columnHelper = createColumnHelper<ExtendedLeaderboardEntry>(); | ||
|
|
||
| return [ | ||
| columnHelper.accessor("originalRank", { | ||
| header: "Rank", | ||
| cell: (info) => { | ||
| const rank = info.getValue(); | ||
| return ( | ||
| <div className="flex items-center w-[35px] h-[32px]"> | ||
| {rank === 1 && ( | ||
| <img | ||
| src="/icons/star-gold.svg" | ||
| className="h-4 w-4 mr-1" | ||
| alt="Gold star - 1st place" | ||
| /> | ||
| )} | ||
| {rank === 2 && ( | ||
| <img | ||
| src="/icons/star-silver.svg" | ||
| className="h-4 w-4 mr-1" | ||
| alt="Silver star - 2nd place" | ||
| /> | ||
| )} | ||
| {rank === 3 && ( | ||
| <img | ||
| src="/icons/star-bronze.svg" | ||
| className="h-4 w-4 mr-1" | ||
| alt="Bronze star - 3rd place" | ||
| /> | ||
| )} | ||
| <div className="flex w-full text-right justify-end"> | ||
| <span className="text-[#111111] font-medium">{rank}</span> | ||
| </div> | ||
| </div> | ||
| ); | ||
| }, | ||
| }), | ||
| columnHelper.accessor("curatorUsername", { | ||
| header: "Username", | ||
| cell: (info) => ( | ||
| <div className="flex items-center gap-2 h-[32px]"> | ||
| <UserLink username={info.getValue()} /> | ||
| </div> | ||
| ), | ||
| }), | ||
| columnHelper.accessor( | ||
| (row) => { | ||
| return row.submissionCount > 0 | ||
| ? Math.round((row.approvalCount / row.submissionCount) * 100) | ||
| : 0; | ||
| }, | ||
| { | ||
| id: "approvalRate", | ||
| header: "Approval Rate", | ||
| cell: (info) => ( | ||
| <div className="flex items-center h-[32px]">{info.getValue()}%</div> | ||
| ), | ||
| }, | ||
| ), | ||
| columnHelper.accessor("submissionCount", { | ||
| header: "Submissions", | ||
| cell: (info) => ( | ||
| <div className="flex items-center text-[#111111] font-medium h-[32px]"> | ||
| {info.getValue()} | ||
| </div> | ||
| ), | ||
| }), | ||
| columnHelper.accessor("feedSubmissions", { | ||
| header: "Top Feeds", | ||
| cell: (info) => { | ||
| const feedSubmissions = info.getValue(); | ||
| const rowIndex = info.row.index; | ||
|
|
||
| return ( | ||
| <div className="flex flex-col min-h-[32px] justify-center"> | ||
| <div className="flex items-center gap-2"> | ||
| {feedSubmissions && feedSubmissions.length > 0 && ( | ||
| <div className="flex items-center justify-between gap-1 border border-neutral-400 px-2 py-1 rounded-md w-[150px]"> | ||
| <span className="text-sm">#{feedSubmissions[0].feedId}</span> | ||
| <span className="text-sm"> | ||
| {feedSubmissions[0].count}/{feedSubmissions[0].totalInFeed} | ||
| </span> | ||
| </div> | ||
| )} | ||
|
|
||
| {feedSubmissions && feedSubmissions.length > 1 && ( | ||
| <button | ||
| onClick={() => toggleRow(rowIndex)} | ||
| className="w-8 h-8 flex items-center justify-center border border-neutral-400 rounded-md transition-colors" | ||
| > | ||
| {expandedRows.includes(rowIndex) ? ( | ||
| <ChevronUp className="h-4 w-4" /> | ||
| ) : ( | ||
| <span className="text-xs"> | ||
| +{feedSubmissions.length - 1} | ||
| </span> | ||
| )} | ||
| </button> | ||
| )} | ||
| </div> | ||
|
|
||
| {feedSubmissions && expandedRows.includes(rowIndex) && ( | ||
| <div className="flex flex-col gap-2 mt-2 pl-0"> | ||
| {feedSubmissions.slice(1).map((feed, feedIndex) => ( | ||
| <div key={feedIndex} className="flex items-center"> | ||
| <div className="flex items-center gap-1 border border-neutral-400 px-2 py-1 rounded-md justify-between w-[150px]"> | ||
| <span className="text-sm">#{feed.feedId}</span> | ||
| <span className="text-sm"> | ||
| {feed.count}/{feed.totalInFeed} | ||
| </span> | ||
| </div> | ||
| </div> | ||
| ))} | ||
| </div> | ||
| )} | ||
| </div> | ||
| ); | ||
| }, | ||
| }), | ||
| ]; | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,144 @@ | ||||||
| import { Link } from "@tanstack/react-router"; | ||||||
| import { ChevronDown, Search } from "lucide-react"; | ||||||
|
|
||||||
| interface Feed { | ||||||
| label: string; | ||||||
| value: string; | ||||||
| } | ||||||
|
|
||||||
| interface TimeOption { | ||||||
| label: string; | ||||||
| value: string; | ||||||
| } | ||||||
|
|
||||||
| interface LeaderboardFiltersProps { | ||||||
| searchQuery: string | null; | ||||||
| onSearchChange: (e: React.ChangeEvent<HTMLInputElement>) => void; | ||||||
| feeds: Feed[]; | ||||||
| timeOptions: TimeOption[]; | ||||||
| search: { | ||||||
| feed: string; | ||||||
| timeframe: string; | ||||||
| }; | ||||||
| showFeedDropdown: boolean; | ||||||
| showTimeDropdown: boolean; | ||||||
| onFeedDropdownToggle: () => void; | ||||||
| onTimeDropdownToggle: () => void; | ||||||
| onFeedDropdownClose: () => void; | ||||||
| onTimeDropdownClose: () => void; | ||||||
| feedDropdownRef: React.RefObject<HTMLDivElement>; | ||||||
| timeDropdownRef: React.RefObject<HTMLDivElement>; | ||||||
| } | ||||||
|
|
||||||
| export function LeaderboardFilters({ | ||||||
| searchQuery, | ||||||
| onSearchChange, | ||||||
| feeds, | ||||||
| timeOptions, | ||||||
| search, | ||||||
| showFeedDropdown, | ||||||
| showTimeDropdown, | ||||||
| onFeedDropdownToggle, | ||||||
| onTimeDropdownToggle, | ||||||
| onFeedDropdownClose, | ||||||
| onTimeDropdownClose, | ||||||
| feedDropdownRef, | ||||||
| timeDropdownRef, | ||||||
| }: LeaderboardFiltersProps) { | ||||||
| return ( | ||||||
| <div className="flex flex-col md:flex-row max-w-[400px] md:max-w-screen-xl md:w-full mx-auto justify-between items-center mb-6 gap-4 px-4 py-8"> | ||||||
| <div className="relative w-full md:w-auto"> | ||||||
| <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[#a3a3a3] h-4 w-4" /> | ||||||
| <input | ||||||
| type="text" | ||||||
| placeholder="Search by curator or feed" | ||||||
| value={searchQuery || ""} | ||||||
| onChange={onSearchChange} | ||||||
| className="pl-10 pr-4 py-2 border border-neutral-300 rounded-md w-full md:w-[300px] focus:outline-none focus:ring-2 focus:ring-[#60a5fa] focus:border-transparent" | ||||||
| /> | ||||||
| </div> | ||||||
| <div className="flex gap-3 w-full md:w-auto"> | ||||||
| <div className="relative w-full md:w-auto" ref={feedDropdownRef}> | ||||||
| <button | ||||||
| onClick={onFeedDropdownToggle} | ||||||
| className="flex items-center justify-between gap-2 px-4 py-2 border border-neutral-300 rounded-md bg-white w-full md:w-[180px]" | ||||||
| aria-expanded={showFeedDropdown} | ||||||
| aria-haspopup="listbox" | ||||||
| aria-controls="feed-dropdown" | ||||||
| > | ||||||
| <span className="text-[#111111] text-sm"> | ||||||
| {feeds.find((feed) => feed.value === search.feed)?.label} | ||||||
| </span> | ||||||
| <ChevronDown className="h-4 w-4 text-[#64748b]" /> | ||||||
| </button> | ||||||
| {showFeedDropdown && ( | ||||||
| <div | ||||||
| id="feed-dropdown" | ||||||
| role="listbox" | ||||||
| className="absolute top-full flex flex-col left-0 mt-1 w-full bg-white border border-neutral-200 rounded-md shadow-lg z-20" | ||||||
| > | ||||||
| {feeds.map((feed, index) => ( | ||||||
| <Link | ||||||
| key={index} | ||||||
| to="/leaderboard" | ||||||
| search={{ feed: feed.value, timeframe: search.timeframe }} | ||||||
| role="option" | ||||||
| aria-selected={search.feed === feed.value} | ||||||
| onClick={onFeedDropdownClose} | ||||||
| className={`w-full px-4 py-2 text-left hover:bg-neutral-100 text-sm ${ | ||||||
| search.feed === feed.value ? "bg-neutral-100" : "" | ||||||
| }`} | ||||||
| > | ||||||
| {feed.label} | ||||||
| </Link> | ||||||
| ))} | ||||||
| </div> | ||||||
| )} | ||||||
| </div> | ||||||
| <div className="relative w-full md:w-auto" ref={timeDropdownRef}> | ||||||
| <button | ||||||
| onClick={onTimeDropdownToggle} | ||||||
| className="flex items-center justify-between gap-2 px-4 py-2 border border-neutral-300 rounded-md bg-white w-full md:w-[160px]" | ||||||
| aria-expanded={showTimeDropdown} | ||||||
| aria-haspopup="listbox" | ||||||
| aria-controls="time-dropdown" | ||||||
| > | ||||||
| <span className="text-[#111111] text-sm"> | ||||||
| { | ||||||
| timeOptions.find((option) => option.value === search.timeframe) | ||||||
| ?.label | ||||||
| } | ||||||
| </span> | ||||||
| <ChevronDown className="h-4 w-4 text-[#64748b]" /> | ||||||
| </button> | ||||||
| {showTimeDropdown && ( | ||||||
| <div | ||||||
| id="time-dropdown" | ||||||
| role="listbox" | ||||||
| className="absolute top-full flex flex-col left-0 mt-1 w-full bg-white border border-neutral-200 rounded-md shadow-lg z-20" | ||||||
| > | ||||||
| {timeOptions.map((time) => ( | ||||||
| <Link | ||||||
| key={time.value} | ||||||
| to="/leaderboard" | ||||||
| search={{ | ||||||
| feed: search.feed.toLowerCase(), | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid unnecessary toLowerCase() on feed value The - feed: search.feed.toLowerCase(),
+ feed: search.feed,📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| timeframe: time.value, | ||||||
| }} | ||||||
| role="option" | ||||||
| aria-selected={search.timeframe === time.label} | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix aria-selected comparison logic The comparison should use - aria-selected={search.timeframe === time.label}
+ aria-selected={search.timeframe === time.value}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| onClick={onTimeDropdownClose} | ||||||
| className={`w-full px-4 py-2 text-left hover:bg-neutral-100 text-sm ${ | ||||||
| search.timeframe === time.label ? "bg-neutral-100" : "" | ||||||
| }`} | ||||||
| > | ||||||
| {time.label} | ||||||
| </Link> | ||||||
| ))} | ||||||
| </div> | ||||||
| )} | ||||||
| </div> | ||||||
| </div> | ||||||
| </div> | ||||||
| ); | ||||||
| } | ||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| import { TableCell, TableRow } from "../ui/table"; | ||
|
|
||
| function SkeletonRow() { | ||
| return ( | ||
| <TableRow className="border-b border-[#e5e5e5]"> | ||
| {/* Rank column */} | ||
| <TableCell className="py-2 px-2 align-middle"> | ||
| <div className="flex items-center w-[35px] h-[32px]"> | ||
| <div className="w-4 h-4 bg-gray-200 rounded animate-pulse mr-1" /> | ||
| <div className="w-6 h-4 bg-gray-200 rounded animate-pulse" /> | ||
| </div> | ||
| </TableCell> | ||
|
|
||
| {/* Username column */} | ||
| <TableCell className="py-2 px-2 align-middle"> | ||
| <div className="flex items-center gap-2 h-[32px]"> | ||
| <div className="w-24 h-4 bg-gray-200 rounded animate-pulse" /> | ||
| </div> | ||
| </TableCell> | ||
|
|
||
| {/* Approval Rate column */} | ||
| <TableCell className="py-2 px-2 align-middle"> | ||
| <div className="flex items-center h-[32px]"> | ||
| <div className="w-12 h-4 bg-gray-200 rounded animate-pulse" /> | ||
| </div> | ||
| </TableCell> | ||
|
|
||
| {/* Submissions column */} | ||
| <TableCell className="py-2 px-2 align-middle"> | ||
| <div className="flex items-center h-[32px]"> | ||
| <div className="w-8 h-4 bg-gray-200 rounded animate-pulse" /> | ||
| </div> | ||
| </TableCell> | ||
|
|
||
| {/* Top Feeds column */} | ||
| <TableCell className="py-2 px-2 align-middle"> | ||
| <div className="flex flex-col min-h-[40px] justify-center"> | ||
| <div className="flex items-center gap-2"> | ||
| <div className="w-[150px] h-8 bg-gray-200 rounded animate-pulse" /> | ||
| <div className="w-8 h-8 bg-gray-200 rounded animate-pulse" /> | ||
| </div> | ||
| </div> | ||
| </TableCell> | ||
| </TableRow> | ||
| ); | ||
| } | ||
|
|
||
| interface LeaderboardSkeletonProps { | ||
| rows?: number; | ||
| } | ||
|
|
||
| export function LeaderboardSkeleton({ rows = 8 }: LeaderboardSkeletonProps) { | ||
| return ( | ||
| <> | ||
| {Array.from({ length: rows }).map((_, index) => ( | ||
| <SkeletonRow key={index} /> | ||
| ))} | ||
| </> | ||
| ); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Add accessibility attributes to the expand button
The expand button should include proper ARIA attributes for better screen reader support.
<button onClick={() => toggleRow(rowIndex)} className="w-8 h-8 flex items-center justify-center border border-neutral-400 rounded-md transition-colors" + aria-expanded={expandedRows.includes(rowIndex)} + aria-label={expandedRows.includes(rowIndex) ? "Collapse additional feeds" : `Show ${feedSubmissions.length - 1} more feeds`} >📝 Committable suggestion
🤖 Prompt for AI Agents