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
82 changes: 82 additions & 0 deletions src/components/leaderboard/EligibilityToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React from 'react';
import { Box } from '@mui/material';
import { alpha } from '@mui/material/styles';
import { FONTS } from './types';

export type EligibilityFilter = 'all' | 'eligible' | 'ineligible';

export const ELIGIBILITY_OPTIONS: Array<{
value: EligibilityFilter;
label: string;
}> = [
{ value: 'all', label: 'All' },
{ value: 'eligible', label: 'Eligible' },
{ value: 'ineligible', label: 'Ineligible' },
];

interface EligibilityToggleProps {
value: EligibilityFilter;
onChange: (next: EligibilityFilter) => void;
/** Tighter pills for the dual watchlist bar. */
compact?: boolean;
}

export const EligibilityToggle: React.FC<EligibilityToggleProps> = ({
value,
onChange,
compact = false,
}) => (
<Box
sx={(theme) => ({
display: 'inline-flex',
gap: compact ? 0.35 : 0.5,
p: compact ? 0.35 : 0.5,
borderRadius: 1.75,
backgroundColor: theme.palette.surface.light,
flexShrink: 0,
})}
>
{ELIGIBILITY_OPTIONS.map((option) => {
const isActive = value === option.value;
return (
<Box
key={option.value}
component="button"
type="button"
aria-pressed={isActive}
onClick={() => onChange(option.value)}
sx={(theme) => ({
px: compact ? 1 : 1.5,
height: compact ? 22 : 24,
display: 'flex',
alignItems: 'center',
border: 0,
borderRadius: 1.25,
backgroundColor: isActive
? alpha(theme.palette.text.primary, 0.15)
: 'transparent',
color: isActive
? theme.palette.text.primary
: theme.palette.text.tertiary,
cursor: 'pointer',
fontFamily: FONTS.mono,
fontSize: compact ? '0.65rem' : '0.72rem',
fontWeight: isActive ? 600 : 500,
lineHeight: 1,
transition: 'all 0.2s ease',
'&:hover': {
backgroundColor: alpha(theme.palette.text.primary, 0.1),
color: theme.palette.text.primary,
},
'&:focus-visible': {
outline: `1px solid ${theme.palette.border.medium}`,
outlineOffset: 1,
},
})}
>
{option.label}
</Box>
);
})}
</Box>
);
77 changes: 1 addition & 76 deletions src/components/leaderboard/TopMinersTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
type LeaderboardVariant,
FONTS,
} from './types';
import { EligibilityToggle, type EligibilityFilter } from './EligibilityToggle';

type ViewMode = 'cards' | 'list';

Expand Down Expand Up @@ -111,8 +112,6 @@ const getAllowedSortOptions = (variant: LeaderboardVariant): SortOption[] => {
return ['totalScore', 'usdPerDay', 'totalPRs', 'credibility', 'watch'];
};

type EligibilityFilter = 'all' | 'eligible' | 'ineligible';

type TopMinersUrlFilters = {
view: ViewMode;
search: string;
Expand Down Expand Up @@ -972,80 +971,6 @@ const WatchlistEligibilityBar: React.FC<WatchlistEligibilityBarProps> = ({
);
};

interface EligibilityToggleProps {
value: EligibilityFilter;
onChange: (next: EligibilityFilter) => void;
/** Tighter pills for the dual watchlist bar. */
compact?: boolean;
}

const ELIGIBILITY_OPTIONS: Array<{ value: EligibilityFilter; label: string }> =
[
{ value: 'all', label: 'All' },
{ value: 'eligible', label: 'Eligible' },
{ value: 'ineligible', label: 'Ineligible' },
];

const EligibilityToggle: React.FC<EligibilityToggleProps> = ({
value,
onChange,
compact = false,
}) => (
<Box
sx={(theme) => ({
display: 'inline-flex',
gap: compact ? 0.35 : 0.5,
p: compact ? 0.35 : 0.5,
borderRadius: 1.75,
backgroundColor: theme.palette.surface.light,
flexShrink: 0,
})}
>
{ELIGIBILITY_OPTIONS.map((option) => {
const isActive = value === option.value;
return (
<Box
key={option.value}
component="button"
type="button"
aria-pressed={isActive}
onClick={() => onChange(option.value)}
sx={(theme) => ({
px: compact ? 1 : 1.5,
height: compact ? 22 : 24,
display: 'flex',
alignItems: 'center',
border: 0,
borderRadius: 1.25,
backgroundColor: isActive
? alpha(theme.palette.text.primary, 0.15)
: 'transparent',
color: isActive
? theme.palette.text.primary
: theme.palette.text.tertiary,
cursor: 'pointer',
fontFamily: FONTS.mono,
fontSize: compact ? '0.65rem' : '0.72rem',
fontWeight: isActive ? 600 : 500,
lineHeight: 1,
transition: 'all 0.2s ease',
'&:hover': {
backgroundColor: alpha(theme.palette.text.primary, 0.1),
color: theme.palette.text.primary,
},
'&:focus-visible': {
outline: `1px solid ${theme.palette.border.medium}`,
outlineOffset: 1,
},
})}
>
{option.label}
</Box>
);
})}
</Box>
);

/* ─── ToolbarSidebarPanel: all controls rendered inline for sidebar ── */

const sidebarLabelSx = {
Expand Down
136 changes: 101 additions & 35 deletions src/components/miners/MinerRepoStandings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
Tooltip,
Typography,
} from '@mui/material';
import type { Theme } from '@mui/material/styles';
import {
GridView as GridViewIcon,
TableRows as TableRowsIcon,
Expand All @@ -21,12 +22,33 @@ import { STATUS_COLORS, tooltipSlotProps } from '../../theme';
import { credibilityColor } from '../../utils/format';
import { getRepositoryOwnerAvatarSrc } from '../../utils/avatar';
import { DataTable, type DataTableColumn } from '../common';
import {
EligibilityToggle,
type EligibilityFilter,
} from '../leaderboard/EligibilityToggle';
import EmptyStateMessage from './EmptyStateMessage';
import MinerRepoStandingCard, { repoMinersHref } from './MinerRepoStandingCard';

type ViewMode = 'prs' | 'issues';
type StandingsView = 'cards' | 'table';

const repoTrackEligible = (
repo: MinerRepositoryEvaluation,
isIssueMode: boolean,
): boolean => (isIssueMode ? repo.isIssueEligible : repo.isEligible);

const filterReposByEligibility = (
repos: MinerRepositoryEvaluation[],
filter: EligibilityFilter,
isIssueMode: boolean,
): MinerRepositoryEvaluation[] => {
if (filter === 'all') return repos;
if (filter === 'eligible') {
return repos.filter((r) => repoTrackEligible(r, isIssueMode));
}
return repos.filter((r) => !repoTrackEligible(r, isIssueMode));
};

interface MinerRepoStandingsProps {
githubId: string;
viewMode?: ViewMode;
Expand Down Expand Up @@ -128,9 +150,11 @@ const MinerRepoStandings: React.FC<MinerRepoStandingsProps> = ({
}) => {
const { data: minerStats, isLoading } = useMinerStats(githubId);
const [view, setView] = useState<StandingsView>('cards');
const [eligibilityFilter, setEligibilityFilter] =
useState<EligibilityFilter>('all');
const isIssueMode = viewMode === 'issues';

const rows = useMemo(() => {
const allRows = useMemo(() => {
// Pre-migration placeholder rows carry an empty repo name — drop them.
const repos = (minerStats?.repositories ?? []).filter(
(r) => r.repositoryFullName.trim().length > 0,
Expand All @@ -144,8 +168,13 @@ const MinerRepoStandings: React.FC<MinerRepoStandingsProps> = ({
);
}, [minerStats, isIssueMode]);

const eligibleCount = rows.filter((r) =>
isIssueMode ? r.isIssueEligible : r.isEligible,
const rows = useMemo(
() => filterReposByEligibility(allRows, eligibilityFilter, isIssueMode),
[allRows, eligibilityFilter, isIssueMode],
);

const eligibleCount = allRows.filter((r) =>
repoTrackEligible(r, isIssueMode),
).length;

const prColumns: DataTableColumn<MinerRepositoryEvaluation>[] = [
Expand Down Expand Up @@ -238,6 +267,36 @@ const MinerRepoStandings: React.FC<MinerRepoStandingsProps> = ({
if (next) setView(next);
};

const toggleGroupSx = {
'& .MuiToggleButton-root': {
color: 'text.tertiary',
borderColor: 'border.light',
height: 32,
minHeight: 32,
px: 1.25,
py: 0,
lineHeight: 1,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
textTransform: 'none',
'&.Mui-selected': {
color: 'text.primary',
backgroundColor: (theme: Theme) =>
alpha(theme.palette.text.primary, 0.15),
'&:hover': {
backgroundColor: (theme: Theme) =>
alpha(theme.palette.text.primary, 0.1),
},
},
'&:hover': {
backgroundColor: (theme: Theme) =>
alpha(theme.palette.text.primary, 0.1),
color: 'text.primary',
},
},
};

return (
<Card
sx={{
Expand Down Expand Up @@ -273,10 +332,14 @@ const MinerRepoStandings: React.FC<MinerRepoStandingsProps> = ({
fontSize: '0.75rem',
}}
>
({rows.length})
({rows.length}
{eligibilityFilter !== 'all' && allRows.length !== rows.length
? ` of ${allRows.length}`
: ''}
)
</Typography>
</Box>
{rows.length > 0 && (
{allRows.length > 0 && (
<Typography sx={{ fontSize: '0.75rem', color: 'text.secondary' }}>
{isIssueMode ? 'Issue-discovery eligible' : 'PR eligible'} in{' '}
<Box
Expand All @@ -289,53 +352,56 @@ const MinerRepoStandings: React.FC<MinerRepoStandingsProps> = ({
fontWeight: 600,
}}
>
{eligibleCount}/{rows.length}
{eligibleCount}/{allRows.length}
</Box>{' '}
{rows.length === 1 ? 'repository' : 'repositories'} · sorted by
{allRows.length === 1 ? 'repository' : 'repositories'} · sorted by
score
</Typography>
)}
</Box>

{rows.length > 0 && (
<ToggleButtonGroup
value={view}
exclusive
onChange={handleViewChange}
size="small"
aria-label="Standings view"
{allRows.length > 0 && (
<Box
sx={{
'& .MuiToggleButton-root': {
color: 'text.secondary',
borderColor: 'border.light',
px: 1.25,
py: 0.5,
'&.Mui-selected': {
color: 'primary.main',
backgroundColor: (t) => alpha(t.palette.primary.main, 0.12),
'&:hover': {
backgroundColor: (t) => alpha(t.palette.primary.main, 0.18),
},
},
},
display: 'flex',
alignItems: 'center',
gap: 1.5,
flexWrap: 'wrap',
}}
>
<ToggleButton value="cards" aria-label="Card grid">
<GridViewIcon sx={{ fontSize: '1.05rem' }} />
</ToggleButton>
<ToggleButton value="table" aria-label="Table">
<TableRowsIcon sx={{ fontSize: '1.05rem' }} />
</ToggleButton>
</ToggleButtonGroup>
<EligibilityToggle
value={eligibilityFilter}
onChange={setEligibilityFilter}
/>
<ToggleButtonGroup
value={view}
exclusive
onChange={handleViewChange}
size="small"
aria-label="Standings view"
sx={toggleGroupSx}
>
<ToggleButton value="cards" aria-label="Card grid">
<GridViewIcon sx={{ fontSize: '1.05rem' }} />
</ToggleButton>
<ToggleButton value="table" aria-label="Table">
<TableRowsIcon sx={{ fontSize: '1.05rem' }} />
</ToggleButton>
</ToggleButtonGroup>
</Box>
)}
</Box>

{isLoading ? (
<Box sx={{ display: 'flex', justifyContent: 'center', py: 8 }}>
<CircularProgress size={36} sx={{ color: 'primary.main' }} />
</Box>
) : rows.length === 0 ? (
) : allRows.length === 0 ? (
<EmptyStateMessage message="No per-repository evaluations for this miner yet." />
) : rows.length === 0 ? (
<EmptyStateMessage
message={`No ${eligibilityFilter === 'eligible' ? 'eligible' : 'ineligible'} repositories for this miner.`}
/>
) : view === 'cards' ? (
<Box
sx={{
Expand Down