diff --git a/src/pages/WatchlistPage.tsx b/src/pages/WatchlistPage.tsx index 89bc2bf9..5dabe340 100644 --- a/src/pages/WatchlistPage.tsx +++ b/src/pages/WatchlistPage.tsx @@ -2305,6 +2305,17 @@ const BOUNTY_STATUS_FILTERS: readonly BountyStatusFilter[] = [ 'history', ]; +const WATCHLIST_BOUNTY_SORT_OPTIONS: Array<{ + value: BountySortKey; + label: string; +}> = [ + { value: 'date', label: 'Date' }, + { value: 'repo', label: 'Repository' }, + { value: 'bounty', label: 'Bounty' }, + { value: 'status', label: 'Status' }, + { value: 'issue', label: 'Issue' }, +]; + const bountyKey = (issue: IssueBounty) => String(issue.id); const getBountyHref = (issue: IssueBounty) => @@ -2538,9 +2549,11 @@ const BountiesList: React.FC<{ itemKeys: string[] }> = ({ itemKeys }) => { const [page, setPage] = useState(0); const observerTarget = useRef(null); const [isLoadingMore, setIsLoadingMore] = useState(false); + const [showChart, setShowChart] = useState(false); const [sortField, setSortField] = useState('id'); const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('desc'); + const [showChart, setShowChart] = useState(false); const bountyVisibleSortKeys = useMemo( () => bountyVisibleSortKeysForFilter(statusFilter), @@ -2615,6 +2628,107 @@ const BountiesList: React.FC<{ itemKeys: string[] }> = ({ itemKeys }) => { Math.ceil(filtered.length / ROWS_PER_PAGE), ); + const chartOption = useMemo(() => { + const white = UI_COLORS.white; + const borderSubtle = alpha(white, 0.08); + const textColor = alpha(white, 0.85); + const gridColor = borderSubtle; + + const repoTotals = new Map(); + sorted.forEach((issue) => { + const amount = parseFloat(issue.targetBounty || issue.bountyAmount || '0'); + repoTotals.set( + issue.repositoryFullName, + (repoTotals.get(issue.repositoryFullName) || 0) + amount, + ); + }); + + const chartData = [...repoTotals.entries()] + .sort((a, b) => b[1] - a[1]) + .slice(0, 20); + + return { + backgroundColor: 'transparent', + title: { + text: 'Bounty Pool by Repository', + subtext: `${sorted.length} watched bounties`, + left: 'center', + top: 20, + textStyle: { + color: white, + fontFamily: 'JetBrains Mono', + fontSize: 16, + fontWeight: 600, + }, + subtextStyle: { + color: alpha(white, TEXT_OPACITY.tertiary), + fontFamily: 'JetBrains Mono', + fontSize: 12, + }, + }, + tooltip: { + trigger: 'axis', + axisPointer: { type: 'shadow' }, + backgroundColor: alpha(UI_COLORS.surfaceTooltip, 0.95), + borderColor: alpha(white, 0.15), + borderWidth: 1, + textStyle: { + color: white, + fontFamily: 'JetBrains Mono', + }, + formatter: (params: any) => { + const p = params[0]; + return `${p.name}: ${p.value.toFixed(4)} ل`; + }, + }, + grid: { + left: '3%', + right: '3%', + bottom: '15%', + top: '20%', + containLabel: true, + }, + xAxis: { + type: 'category', + data: chartData.map(([repo]) => repo.split('/')[1] || repo), + axisLabel: { + color: textColor, + fontFamily: 'JetBrains Mono', + rotate: 45, + interval: 0, + }, + axisLine: { lineStyle: { color: gridColor } }, + }, + yAxis: { + type: 'value', + name: 'Bounty (ل)', + nameTextStyle: { color: textColor, fontFamily: 'JetBrains Mono' }, + axisLabel: { color: textColor, fontFamily: 'JetBrains Mono' }, + splitLine: { lineStyle: { color: gridColor, type: 'dashed' } }, + }, + series: [ + { + data: chartData.map(([, v]) => v), + type: 'bar', + itemStyle: { + color: { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [ + { offset: 0, color: CHART_COLORS.open }, + { offset: 1, color: STATUS_COLORS.open }, + ], + }, + borderRadius: [4, 4, 0, 0], + }, + }, + ], + }; + }, [sorted]); + useEffect(() => { setPage((p) => Math.min(p, totalBountyPages - 1)); }, [totalBountyPages]); @@ -2774,11 +2888,57 @@ const BountiesList: React.FC<{ itemKeys: string[] }> = ({ itemKeys }) => { }} /> } + extraContent={ + + Chart + setShowChart((v) => !v)} + size="small" + sx={{ + color: showChart ? 'text.primary' : 'text.tertiary', + border: '1px solid', + borderColor: 'border.light', + borderRadius: 2, + padding: '6px', + '&:hover': { + backgroundColor: 'surface.light', + borderColor: 'border.medium', + }, + }} + > + {showChart ? ( + + ) : ( + + )} + + + } hasActiveFilter={statusFilter !== 'all'} /> )} + + alpha(t.palette.background.default, 0.5), + }} + > + {showChart && sorted.length > 0 && ( + + )} + + + {viewMode === 'list' ? ( columns={bountyColumns}