diff --git a/src/components/DayPRsPanel.tsx b/src/components/DayPRsPanel.tsx index e74f4ad1..ae7d686d 100644 --- a/src/components/DayPRsPanel.tsx +++ b/src/components/DayPRsPanel.tsx @@ -4,6 +4,7 @@ import { format, parseISO } from 'date-fns'; import type { CommitLog } from '../api'; import { LinkBox } from './common/linkBehavior'; import { STATUS_COLORS, TEXT_OPACITY, scrollbarSx } from '../theme'; +import { minerPrPath } from '../utils'; interface DayPRsPanelProps { date: string; @@ -97,7 +98,10 @@ const DayPRsPanel: React.FC = ({ date, prs, username }) => { {dayPRs.map((pr, idx) => { const dotColor = prStateColor(pr); const stateLabel = prStateLabel(pr); - const detailsHref = `/miners/pr?repo=${encodeURIComponent(pr.repository)}&number=${pr.pullRequestNumber}`; + const detailsHref = minerPrPath( + pr.repository, + pr.pullRequestNumber, + ); const timeLabel = pr.mergedAt ? format(new Date(pr.mergedAt), 'h:mm a') : ''; diff --git a/src/components/issues/IssueSubmissionsTable.tsx b/src/components/issues/IssueSubmissionsTable.tsx index 1f713aea..000330fb 100644 --- a/src/components/issues/IssueSubmissionsTable.tsx +++ b/src/components/issues/IssueSubmissionsTable.tsx @@ -15,7 +15,7 @@ import EmojiEventsIcon from '@mui/icons-material/EmojiEvents'; import { IssueSubmission } from '../../api/models/Issues'; import { STATUS_COLORS } from '../../theme'; import { formatDate } from '../../utils/format'; -import { getGithubAvatarSrc } from '../../utils'; +import { getGithubAvatarSrc, minerPrPath } from '../../utils'; import { LinkBox } from '../common/linkBehavior'; import { DataTable, @@ -223,7 +223,10 @@ const IssueSubmissionsTable: React.FC = ({ {winnerSubmission && ( @@ -471,7 +474,7 @@ const IssueSubmissionsTable: React.FC = ({ } onRowClick={(submission) => navigate( - `/miners/pr?repo=${encodeURIComponent(submission.repositoryFullName)}&number=${submission.number}`, + minerPrPath(submission.repositoryFullName, submission.number), { state: linkState }, ) } diff --git a/src/components/layout/GlobalSearchBar.tsx b/src/components/layout/GlobalSearchBar.tsx index 6f1c3526..f2fb3def 100644 --- a/src/components/layout/GlobalSearchBar.tsx +++ b/src/components/layout/GlobalSearchBar.tsx @@ -24,6 +24,7 @@ import CloseIcon from '@mui/icons-material/Close'; import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; import { useSearchResults } from '../../pages/search/searchData'; import { useLinkBehavior, linkResetSx } from '../common/linkBehavior'; +import { minerPrPath, minerRepositoryPath } from '../../utils'; const QUICK_RESULT_LIMIT = 3; const DROPDOWN_CLOSE_DELAY_MS = 150; @@ -316,7 +317,7 @@ const GlobalSearchBar: React.FC = () => { }); }); repositoryResults.forEach((repo) => { - const href = `/miners/repository?name=${encodeURIComponent(repo.fullName)}`; + const href = minerRepositoryPath(repo.fullName); items.push({ key: `repo-${repo.fullName}`, kind: 'repo', @@ -327,7 +328,7 @@ const GlobalSearchBar: React.FC = () => { }); }); prResults.forEach((pr) => { - const href = `/miners/pr?repo=${encodeURIComponent(pr.repository)}&number=${pr.pullRequestNumber}`; + const href = minerPrPath(pr.repository, pr.pullRequestNumber); items.push({ key: `pr-${pr.repository}-${pr.pullRequestNumber}`, kind: 'pr', diff --git a/src/components/miners/MinerOpenDiscoveryIssuesByRepo.tsx b/src/components/miners/MinerOpenDiscoveryIssuesByRepo.tsx index 7cd3a403..222aa65a 100644 --- a/src/components/miners/MinerOpenDiscoveryIssuesByRepo.tsx +++ b/src/components/miners/MinerOpenDiscoveryIssuesByRepo.tsx @@ -19,6 +19,7 @@ import { getRepositoryOwnerAvatarSrc, getScoringWindowStartIso, isOutsideScoringWindow, + minerPrPath, paginateItems, } from '../../utils'; import { @@ -325,7 +326,7 @@ const MinerOpenDiscoveryIssuesByRepo: React.FC< } const repoForPr = issue.solving_pr?.repo_full_name ?? issue.repo_full_name; - const prHref = `/miners/pr?repo=${encodeURIComponent(repoForPr)}&number=${prNumber}`; + const prHref = minerPrPath(repoForPr, prNumber); return ( = ({ expanded, }) => { const prLinkProps = useLinkBehavior( - `/miners/pr?repo=${encodeURIComponent(pr.repository)}&number=${pr.pullRequestNumber}`, + minerPrPath(pr.repository, pr.pullRequestNumber), ); const isMerged = !!pr.mergedAt; diff --git a/src/components/miners/MinerRepoStandingCard.tsx b/src/components/miners/MinerRepoStandingCard.tsx index 7938c18a..dad7e6ff 100644 --- a/src/components/miners/MinerRepoStandingCard.tsx +++ b/src/components/miners/MinerRepoStandingCard.tsx @@ -4,6 +4,7 @@ import type { MinerRepositoryEvaluation } from '../../api/models/Dashboard'; import { tooltipSlotProps } from '../../theme'; import { credibilityColor } from '../../utils/format'; import { getRepositoryOwnerAvatarSrc } from '../../utils/avatar'; +import { minerRepositoryPath } from '../../utils'; import { linkResetSx, useLinkBehavior } from '../common/linkBehavior'; type ViewMode = 'prs' | 'issues'; @@ -19,7 +20,7 @@ const INACTIVE_OPACITY = 0.42; /** Build the link to a repository's Miners tab. */ const repoMinersHref = (repositoryFullName: string): string => - `/miners/repository?name=${encodeURIComponent(repositoryFullName)}&tab=miners`; + minerRepositoryPath(repositoryFullName, { tab: 'miners' }); /* ── Eligibility marker — dot + word, echoes leaderboard MinerCard ─── */ const EligibilityLabel: React.FC<{ eligible: boolean }> = ({ eligible }) => ( diff --git a/src/components/prs/PRDetailsCard.tsx b/src/components/prs/PRDetailsCard.tsx index 0beab742..30676fe3 100644 --- a/src/components/prs/PRDetailsCard.tsx +++ b/src/components/prs/PRDetailsCard.tsx @@ -18,7 +18,11 @@ import theme, { TEXT_OPACITY, tooltipSlotProps, } from '../../theme'; -import { getRepositoryOwnerAvatarSrc, parseNumber } from '../../utils'; +import { + getRepositoryOwnerAvatarSrc, + minerRepositoryPath, + parseNumber, +} from '../../utils'; import { buildMultiplierGrid } from '../../utils/multiplierDefs'; import PRTimeDecayChart from './PRTimeDecayChart'; @@ -38,7 +42,7 @@ const PRDetailsCard: React.FC = ({ usePullRequestDetails(repository, pullRequestNumber); const repoLinkProps = useLinkBehavior( - `/miners/repository?name=${encodeURIComponent(repository)}`, + minerRepositoryPath(repository), { state: { backLabel: `Back to PR #${pullRequestNumber}` } }, ); if (isDetailsLoading) { diff --git a/src/components/prs/PRHeader.tsx b/src/components/prs/PRHeader.tsx index e43ee516..be89f124 100644 --- a/src/components/prs/PRHeader.tsx +++ b/src/components/prs/PRHeader.tsx @@ -8,6 +8,7 @@ import { formatDate, formatUsdEstimate, getRepositoryOwnerAvatarSrc, + minerRepositoryPath, } from '../../utils'; import { type PullRequestDetails } from '../../api/models/Dashboard'; import { STATUS_COLORS } from '../../theme'; @@ -25,7 +26,7 @@ const PRHeader: React.FC = ({ }) => { const [owner] = repository.split('/'); const repoLinkProps = useLinkBehavior( - `/miners/repository?name=${encodeURIComponent(repository)}`, + minerRepositoryPath(repository), { state: { backLabel: `Back to PR #${pullRequestNumber}` } }, ); const authorLinkProps = useLinkBehavior( diff --git a/src/components/repositories/RepositoryPRsTable.tsx b/src/components/repositories/RepositoryPRsTable.tsx index 8e763ad4..51bf4fbb 100644 --- a/src/components/repositories/RepositoryPRsTable.tsx +++ b/src/components/repositories/RepositoryPRsTable.tsx @@ -29,7 +29,12 @@ import { useWatchlist, } from '../../hooks/useWatchlist'; import theme, { TEXT_OPACITY, scrollbarSx } from '../../theme'; -import { filterPrs, getPrStatusCounts, type PrStatusFilter } from '../../utils'; +import { + filterPrs, + getPrStatusCounts, + minerPrPath, + type PrStatusFilter, +} from '../../utils'; import { getRepositoryOwnerAvatarSrc } from '../../utils/avatar'; import { formatDate } from '../../utils/format'; import FilterButton from '../FilterButton'; @@ -188,10 +193,9 @@ const RepositoryPRsTable: React.FC = ({ const handleRowClick = useCallback( (pr: CommitLog) => { - navigate( - `/miners/pr?repo=${encodeURIComponent(pr.repository)}&number=${pr.pullRequestNumber}`, - { state: { backLabel: `Back to ${repositoryFullName}` } }, - ); + navigate(minerPrPath(pr.repository, pr.pullRequestNumber), { + state: { backLabel: `Back to ${repositoryFullName}` }, + }); }, [navigate, repositoryFullName], ); diff --git a/src/pages/HomePage.tsx b/src/pages/HomePage.tsx index 165ecf0d..8c80f2e1 100644 --- a/src/pages/HomePage.tsx +++ b/src/pages/HomePage.tsx @@ -23,6 +23,7 @@ import { getGithubAvatarSrc, getGithubUserAvatarSrcById, getPrStatusLabel, + minerPrPath, parseNumber, } from '../utils'; import useDashboardData from './dashboard/useDashboardData'; @@ -266,9 +267,7 @@ const buildActivityRows = (prs: CommitLog[]): LandingActivityRow[] => title: pr.pullRequestTitle || `PR #${pr.pullRequestNumber}`, repository: pr.repository, author: pr.author || shortIdentity(pr.hotkey || 'unknown'), - href: `/miners/pr?repo=${encodeURIComponent(pr.repository)}&number=${ - pr.pullRequestNumber - }`, + href: minerPrPath(pr.repository, pr.pullRequestNumber), dateLabel: timestamp.dateLabel, timeLabel: timestamp.timeLabel, tone: @@ -1723,6 +1722,8 @@ const TopActiveReposPanel: React.FC<{ return ( ({ diff --git a/src/pages/RepositoriesPage.tsx b/src/pages/RepositoriesPage.tsx index 195b8f3c..d9a9c570 100644 --- a/src/pages/RepositoriesPage.tsx +++ b/src/pages/RepositoriesPage.tsx @@ -12,6 +12,7 @@ import { type CommitLog } from '../api/models/Dashboard'; import { getRepositoryOwnerAvatarSrc } from '../utils/avatar'; import { buildRepoDiscoveryRollupFromMiners } from '../utils/ExplorerUtils'; import { isMergedPr } from '../utils/prStatus'; +import { minerPrPath, minerRepositoryPath } from '../utils'; const FONTS = { mono: '"JetBrains Mono", monospace' } as const; @@ -129,10 +130,8 @@ const cardSx = (theme: Theme) => ({ // ── Page ──────────────────────────────────────────────────────────────────── const REPO_LINK_STATE = { backLabel: 'Back to Repositories' } as const; -const getRepoHref = (name: string) => - `/miners/repository?name=${encodeURIComponent(name)}`; -const getPrHref = (name: string, number: number) => - `/miners/pr?repo=${encodeURIComponent(name)}&number=${number}`; +const getRepoHref = (name: string) => minerRepositoryPath(name); +const getPrHref = (name: string, number: number) => minerPrPath(name, number); const RepositoriesPage: React.FC = () => { const registerRepoLink = useLinkBehavior( diff --git a/src/pages/WatchlistPage.tsx b/src/pages/WatchlistPage.tsx index fe91a4c7..59de2d80 100644 --- a/src/pages/WatchlistPage.tsx +++ b/src/pages/WatchlistPage.tsx @@ -108,6 +108,7 @@ import { filterPrs, type PrStatusFilter } from '../utils/prTable'; import { getIssueStatusMeta } from '../utils/issueStatus'; import { formatDate, formatTokenAmount, formatWeight } from '../utils/format'; import { getRepositoryOwnerAvatarSrc } from '../utils/avatar'; +import { minerPrPath, minerRepositoryPath } from '../utils'; import theme, { CHART_COLORS, LABEL_COLORS, @@ -1149,8 +1150,7 @@ const repoHeaderStack = ( ); -const getRepoHref = (repo: Repository) => - `/miners/repository?name=${encodeURIComponent(repo.fullName)}`; +const getRepoHref = (repo: Repository) => minerRepositoryPath(repo.fullName); const repoColumns: DataTableColumn[] = [ { @@ -3038,7 +3038,7 @@ const PRsViewModeToggle: React.FC<{ }; const getPrHref = (pr: CommitLog) => - `/miners/pr?repo=${encodeURIComponent(pr.repository)}&number=${pr.pullRequestNumber}`; + minerPrPath(pr.repository, pr.pullRequestNumber); const PRCard: React.FC<{ pr: CommitLog; diff --git a/src/pages/dashboard/views/DashboardFeaturedWork.tsx b/src/pages/dashboard/views/DashboardFeaturedWork.tsx index 1ddd69a4..b1f98be3 100644 --- a/src/pages/dashboard/views/DashboardFeaturedWork.tsx +++ b/src/pages/dashboard/views/DashboardFeaturedWork.tsx @@ -10,6 +10,7 @@ import { sectionContainerSx, sectionTitleSx, } from './featuredWorkStyles'; +import { minerPrPath, minerRepositoryPath } from '../../../utils'; interface DashboardFeaturedWorkProps { items: FeaturedWorkRepo[]; @@ -24,12 +25,6 @@ const FEATURED_WORK_TITLE = 'Featured Work'; const MAX_COLUMNS_SM = 2; const MAX_COLUMNS_LG = 3; -const buildPrDetailUrl = (repo: string, prNumber: number): string => - `/miners/pr?repo=${encodeURIComponent(repo)}&number=${encodeURIComponent(String(prNumber))}`; - -const buildRepoDetailUrl = (repo: string): string => - `/miners/repository?name=${encodeURIComponent(repo)}`; - const computeGridColumns = ( itemCount: number, ): { xs: string; sm: string; lg: string } => ({ @@ -48,14 +43,14 @@ const DashboardFeaturedWorkSection: React.FC = ({ const navigateToRepo: NavigateToRepoFn = useCallback( (repoName: string): void => { - navigate(buildRepoDetailUrl(repoName)); + navigate(minerRepositoryPath(repoName)); }, [navigate], ); const navigateToPr: NavigateToPrFn = useCallback( (repoName: string, pr: FeaturedWorkPr): void => { - navigate(buildPrDetailUrl(repoName, pr.prNumber)); + navigate(minerPrPath(repoName, pr.prNumber)); }, [navigate], ); diff --git a/src/pages/dashboard/views/LiveCommitLog.tsx b/src/pages/dashboard/views/LiveCommitLog.tsx index 7129fd18..d69dfed7 100644 --- a/src/pages/dashboard/views/LiveCommitLog.tsx +++ b/src/pages/dashboard/views/LiveCommitLog.tsx @@ -159,6 +159,9 @@ const CommitLogItem: React.FC<{ const content = ( { `/miners/details?githubId=${encodeURIComponent(miner.githubId)}`; const getRepositoryHref = (repo: { fullName: string }) => - `/miners/repository?name=${encodeURIComponent(repo.fullName)}`; + minerRepositoryPath(repo.fullName); const getPrHref = (pr: { repository: string; pullRequestNumber: number }) => - `/miners/pr?repo=${encodeURIComponent(pr.repository)}&number=${pr.pullRequestNumber}`; + minerPrPath(pr.repository, pr.pullRequestNumber); const getIssueHref = (issue: { id: number }) => `/bounties/details?id=${issue.id}`; diff --git a/src/utils/index.ts b/src/utils/index.ts index f8746a31..68908637 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -7,3 +7,4 @@ export * from './multiplierDefs'; export * from './watchlistSort'; export * from './avatar'; export * from './githubRepoUrl'; +export * from './paths'; diff --git a/src/utils/paths.ts b/src/utils/paths.ts new file mode 100644 index 00000000..0aa3b50e --- /dev/null +++ b/src/utils/paths.ts @@ -0,0 +1,13 @@ +export const minerPrPath = ( + repository: string, + pullRequestNumber: number | string, +): string => + `/miners/pr?repo=${encodeURIComponent(repository)}&number=${pullRequestNumber}`; + +export const minerRepositoryPath = ( + repositoryFullName: string, + options?: { tab?: string }, +): string => { + const base = `/miners/repository?name=${encodeURIComponent(repositoryFullName)}`; + return options?.tab ? `${base}&tab=${options.tab}` : base; +};