Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions node_modules
76 changes: 49 additions & 27 deletions src/components/dashboard/LeaderboardMiniSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,32 @@ import { ToggleGroup, ToggleGroupItem } from "../ui/toggle-group";
import { Button } from "../ui/button";

type LeaderboardType = "streak" | "progress";
type StreakEntry = { rank: number; displayName: string; streak: number; language: string };
type ProgressEntry = { rank: number; displayName: string; score: number; topLanguage: string };
Copy link

Choose a reason for hiding this comment

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

Duplicated type definitions across two leaderboard files

Low Severity

StreakEntry and ProgressEntry types are defined identically in both LeaderboardMiniSection.tsx and leaderboard.tsx. If the API shape changes, one file could be updated while the other is missed, leading to a type mismatch. These types represent the same leaderboard API response and could be shared from a single location.

Additional Locations (1)

Fix in Cursor Fix in Web


function MiniLeaderboardRow({ rank, displayName, medal, isCurrentUser, value }: {
rank: number;
displayName: string;
medal: string | null;
isCurrentUser: boolean;
value: string;
}) {
return (
<div
className={`flex items-center justify-between p-2 rounded-lg ${
isCurrentUser ? "bg-primary/10 border border-primary/20" : "bg-muted/30"
}`}
>
<div className="flex items-center gap-2">
<span className="text-sm font-medium w-6">
{medal || `#${rank}`}
</span>
<span className="text-sm font-medium">{displayName}</span>
</div>
<div className="text-sm text-muted-foreground">{value}</div>
</div>
);
}

export function LeaderboardMiniSection() {
const [selectedType, setSelectedType] = useState<LeaderboardType>("streak");
Expand Down Expand Up @@ -35,7 +61,7 @@ export function LeaderboardMiniSection() {
);

const hasDisplayName = userInfo?.displayName !== null;
const leaderboardData = selectedType === "streak" ? streakData : progressData;
const isStreak = selectedType === "streak";

// Medal icons for top 3
const getMedalIcon = (rank: number) => {
Expand Down Expand Up @@ -82,32 +108,28 @@ export function LeaderboardMiniSection() {
</div>
) : (
<div className="space-y-3">
{leaderboardData.map((user: typeof leaderboardData[0]) => {
const isCurrentUser = hasDisplayName && userRank && user.rank === userRank.rank;
const medal = getMedalIcon(user.rank);

return (
<div
key={`${user.displayName}-${user.rank}`}
className={`flex items-center justify-between p-2 rounded-lg ${
isCurrentUser ? "bg-primary/10 border border-primary/20" : "bg-muted/30"
}`}
>
<div className="flex items-center gap-2">
<span className="text-sm font-medium w-6">
{medal || `#${user.rank}`}
</span>
<span className="text-sm font-medium">{user.displayName}</span>
</div>
<div className="text-sm text-muted-foreground">
{selectedType === "streak"
? `${(user as any).streak || 0} days`
: `${(user as any).score || 0} pts`
}
</div>
</div>
);
})}
{isStreak
? streakData.map((user: StreakEntry) => (
<MiniLeaderboardRow
key={`${user.displayName}-${user.rank}`}
rank={user.rank}
displayName={user.displayName}
medal={getMedalIcon(user.rank)}
isCurrentUser={!!(hasDisplayName && userRank && user.rank === userRank.rank)}
value={`${user.streak || 0} days`}
/>
))
: progressData.map((user: ProgressEntry) => (
<MiniLeaderboardRow
key={`${user.displayName}-${user.rank}`}
rank={user.rank}
displayName={user.displayName}
medal={getMedalIcon(user.rank)}
isCurrentUser={!!(hasDisplayName && userRank && user.rank === userRank.rank)}
value={`${user.score || 0} pts`}
/>
))
}

{/* Show current user's rank if not in top 5 but in top 50 */}
{hasDisplayName && userRank && userRank.rank > 5 && userRank.rank <= 50 && (
Expand Down
128 changes: 72 additions & 56 deletions src/routes/_authed/leaderboard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,45 @@ export const Route = createFileRoute("/_authed/leaderboard")({
type LeaderboardType = "streak" | "progress";
type TimePeriod = "weekly" | "monthly" | "all-time";

type StreakEntry = { rank: number; displayName: string; streak: number; language: string };
type ProgressEntry = { rank: number; displayName: string; score: number; topLanguage: string };

function LeaderboardRow({ rank, displayName, medal, isUser, language, value }: {
rank: number;
displayName: string;
medal: string | null;
isUser: boolean;
language: string;
value: string;
}) {
return (
<div
className={`flex items-center gap-4 p-4 rounded-lg transition-colors ${
isUser
? "bg-primary/10 border border-primary/20"
: "hover:bg-muted/50"
}`}
>
<div className="w-8 text-center font-mono text-sm text-muted-foreground">
{rank}
</div>
<div className="w-6 text-center">
{medal && <span className="text-lg">{medal}</span>}
</div>
<div className="flex items-center gap-3 flex-1">
<span className="text-lg">{language}</span>
<span className="font-medium">
{displayName}
{isUser && <span className="ml-2 text-primary">⭐</span>}
</span>
</div>
<div className="text-right">
<div className="font-mono font-medium">{value}</div>
</div>
</div>
);
}

function LeaderboardPage() {
const [selectedType, setSelectedType] = useState<LeaderboardType>("streak");
const [selectedPeriod, setSelectedPeriod] = useState<TimePeriod>("all-time");
Expand Down Expand Up @@ -48,8 +87,9 @@ function LeaderboardPage() {
})
);

const leaderboardData = selectedType === "streak" ? streakData : progressData;
const isLoading = selectedType === "streak" ? streakLoading : progressLoading;
const isStreak = selectedType === "streak";
const leaderboardData = isStreak ? streakData : progressData;
const isLoading = isStreak ? streakLoading : progressLoading;

// Medal icons for top 3
const getMedalIcon = (rank: number) => {
Expand Down Expand Up @@ -140,60 +180,36 @@ function LeaderboardPage() {
</div>
) : (
<div className="space-y-2">
{leaderboardData.map((user: typeof leaderboardData[0], index: number) => {
const rank = offset + index + 1;
const medal = getMedalIcon(rank);
const isUser = isCurrentUser(rank);

return (
<div
key={`${user.displayName}-${rank}`}
className={`flex items-center gap-4 p-4 rounded-lg transition-colors ${
isUser
? "bg-primary/10 border border-primary/20"
: "hover:bg-muted/50"
}`}
>
{/* Rank */}
<div className="w-8 text-center font-mono text-sm text-muted-foreground">
{rank}
</div>

{/* Medal for top 3 */}
<div className="w-6 text-center">
{medal && <span className="text-lg">{medal}</span>}
</div>

{/* User info */}
<div className="flex items-center gap-3 flex-1">
{/* Language flag */}
<span className="text-lg">
{getLanguageFlagString(
selectedType === "streak"
? (user as any).language
: (user as any).topLanguage
)}
</span>

{/* Display name with star for current user */}
<span className="font-medium">
{user.displayName}
{isUser && <span className="ml-2 text-primary">⭐</span>}
</span>
</div>

{/* Score */}
<div className="text-right">
<div className="font-mono font-medium">
{selectedType === "streak"
? `${(user as any).streak} days`
: `${Math.round((user as any).score)} pts`
}
</div>
</div>
</div>
);
})}
{isStreak
? streakData.map((user: StreakEntry, index: number) => {
const rank = offset + index + 1;
return (
<LeaderboardRow
key={`${user.displayName}-${rank}`}
rank={rank}
displayName={user.displayName}
medal={getMedalIcon(rank)}
isUser={isCurrentUser(rank)}
language={getLanguageFlagString(user.language)}
value={`${user.streak} days`}
/>
);
})
: progressData.map((user: ProgressEntry, index: number) => {
const rank = offset + index + 1;
return (
<LeaderboardRow
key={`${user.displayName}-${rank}`}
rank={rank}
displayName={user.displayName}
medal={getMedalIcon(rank)}
isUser={isCurrentUser(rank)}
language={getLanguageFlagString(user.topLanguage)}
value={`${Math.round(user.score)} pts`}
/>
);
})
}
</div>
)}

Expand Down