Skip to content

Commit 072c8a3

Browse files
sykp241095claude
andauthored
refactor: unified VI, shared components, React 19, performance (#2706)
* refactor: unified VI, removed compose layer, consolidated shared components - Unified sidebar: CollapsibleSidebar across repo/org/user/collection/compare pages - Unified title hierarchy: H1(28px) H2(22px) H3(18px) H4(14px) across all pages - Unified ShowSQL: all placed in title row right side - Unified table styles: RankTable pattern (No./Name/Value) for repo/org - Unified chart grid lines: #2a2a2c dashed splitLine on all axes - Unified dataZoom: slim 16px slider, no data shadow - Unified sidebar nav: left border selected state, no background highlight - Unified spacing: section pt-8/pb-8, title pb-4, chart title mb-3 - Unified right padding: 10% on all analysis/collection layouts - Unified OG image constants: shared lib/og-image.ts - Unified colors: removed yellow (#f7df83) from collection pages - Unified breadcrumb: Home icon style on collection pages with Edit link - Removed compose layer: deleted charts/compose/org/ (17 chart wrappers) - Removed SectionTemplate: org/user sections use ScrollspySectionWrapper - Removed duplicate code: utils/format.ts, charts-utils/ui/, Analyze/options/ - Removed duplicate API route: /gh/repos/[owner]/[repo] - Removed unused components: MainSideGridTemplate, SplitTemplate, SubChart, ChartLinks - Shared chart visualizations: map-chart.ts, word-cloud.tsx, time-heatmap.tsx - Added StickyRepoHeader for repo analysis - Added ScrollspySectionWrapper anchors for sidebar scroll tracking - Added RadarChart/PieChart/TreemapChart to ECharts registry - Fixed hydration mismatch in ShareButtons - Fixed tainted canvas export error - Replaced native select with tabs in developer activities - Removed Organization/Developer type labels - Removed breadcrumbs from analysis pages - Removed "Last active at" from org header Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * refactor: UI abstractions, performance, React 19 upgrade, engineering cleanup ## Shared UI Components - TabBar: replaces 3 inline tab implementations - SectionHeading: replaces 25 inline heading elements - MetricTable: replaces duplicate overview tables in repo/user pages - LoadingSkeleton: consolidates 3 identical definitions - useChartContainer: shared ResizeObserver hook for RepoChart/OrgChart - createTimeSeriesOption: reduces 7 inline ECharts option builders - PageSkeleton: shared loading.tsx skeletons ## Visual Consistency - Removed ALL yellow accent colors (#ffe895/#f7df83/#fbe593) — primary is now white - Unified border-radius: rounded-xl/2xl/3xl → rounded-md/sm across all components - Unified hover colors: 31 yellow hover variants → hover:text-white - Unified description text: 4 non-standard styles → text-[16px] text-[#7c7c7c] - Unified dropdown/popover bg: #2f2f3a → #212122 - Unified input/select focus: yellow ring → subtle gray border - Unified kbd styling: removed border, simplified - Cleaned search dialog width (CSS class with !important) - Removed card backgrounds from Repository Statistics section ## Performance - Context values memoized (AnalyzeContext, AnalyzeChartContext, CollapsibleSidebar) - Removed 6 remaining key={} props forcing chart remounts - Sidebar animation: removed transition-[width] (layout thrash) - SQLDialog lazy-loaded via dynamic import (~300 lines deferred) - fetchCollections wrapped in React.cache for server dedup - useTransition for tab switches and URL param changes - TabBar onPrefetch hook for hover-based data prefetching - staleTime 5min on RepoChart/OrgChart queries - content-visibility: auto on ScrollspySectionWrapper - will-change-transform on sticky elements - requestAnimationFrame throttling on scroll handlers - passive: true on all scroll listeners - Font display: swap for Geist font - Fixed layout shift: StickyRepoHeader → fixed positioning - transition-all → specific properties (6 files) ## Next.js Best Practices - React 18 → 19, @types/react 18 → 19 - error.tsx on all 14 routes - loading.tsx with proper skeletons on all routes - generateStaticParams for popular repo/org pages - Removed contradictory force-dynamic + revalidate on trending/languages - next/image optimization enabled (removed unoptimized: true) - router.push → Link component (3 places) ## Engineering Quality - Removed 9 console.log/warn from client code - Cleaned 18 TODO/FIXME comments - VisualizerModule type replaces 14 `any` in chart system - Deduplicated chart color palettes - Unified percentage formatting to number2percent - CSS variables configured for design tokens - Fixed duplicate splitLine properties in 7 chart files - Removed unused @ts-expect-error directives Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7756bc6 commit 072c8a3

295 files changed

Lines changed: 4084 additions & 8638 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.claude/settings.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"permissions": {
3+
"allow": [
4+
"Edit"
5+
]
6+
}
7+
}

apps/web/app/analyze-user/[login]/_charts/ChartWrapper.tsx

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import dynamic from 'next/dynamic';
44
import React, { useMemo } from 'react';
55
import type { EChartsOption } from 'echarts';
6+
import { ShowSQLInline } from '@/components/Analyze/ShowSQL';
67

78
const EChartsWrapper = dynamic(() => import('@/components/Analyze/EChartsWrapper'), { ssr: false });
89

@@ -12,36 +13,78 @@ interface PersonalChartProps {
1213
height?: number;
1314
loading?: boolean;
1415
noData?: boolean;
16+
/** SQL string returned from the API, used for the SHOW SQL button */
17+
sql?: string;
18+
/** Query name for the SHOW SQL explain tab */
19+
queryName?: string;
20+
/** Query params for the SHOW SQL explain tab */
21+
queryParams?: Record<string, any>;
1522
}
1623

17-
export default function PersonalChart ({ title, option, height = 400, loading, noData }: PersonalChartProps) {
18-
const mergedOption = useMemo<EChartsOption>(() => ({
19-
backgroundColor: 'transparent',
20-
title: { text: title, left: 'center', textStyle: { color: '#dadada', fontSize: 14, fontWeight: 'bold' } },
21-
legend: { type: 'scroll', orient: 'horizontal', top: 32, textStyle: { color: '#aaa' } },
22-
grid: { top: 64, left: 8, right: 8, bottom: 48, containLabel: true },
23-
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
24-
dataZoom: [{ type: 'slider', showDataShadow: false }],
25-
...(noData
24+
export default function PersonalChart ({ title, option, height = 400, loading, noData, sql, queryName, queryParams }: PersonalChartProps) {
25+
const mergedOption = useMemo<EChartsOption>(() => {
26+
const axisLine = { lineStyle: { color: '#2a2a2c' } };
27+
const splitLine = { show: true, lineStyle: { color: '#2a2a2c', type: 'dashed' as const } };
28+
const baseOption = noData
2629
? {
2730
graphic: [{ type: 'text', left: 'center', top: 'middle', style: { fontSize: 16, fontWeight: 'bold' as const, text: 'No relevant data yet', fill: '#7c7c7c' } }],
28-
xAxis: { type: 'time' },
29-
yAxis: { type: 'value' },
31+
xAxis: { type: 'time' as const, splitLine, axisLine },
32+
yAxis: { type: 'value' as const, splitLine, axisLine },
3033
series: [],
3134
}
32-
: option),
33-
}), [title, option, noData]);
35+
: option;
36+
37+
// Inject grid lines into existing axes
38+
const injectSplitLine = (axis: any) => {
39+
if (!axis) return axis;
40+
if (Array.isArray(axis)) return axis.map((a: any) => ({ ...a, splitLine: { ...splitLine, ...a?.splitLine }, axisLine: { ...axisLine, ...a?.axisLine } }));
41+
return { ...axis, splitLine: { ...splitLine, ...axis?.splitLine }, axisLine: { ...axisLine, ...axis?.axisLine } };
42+
};
43+
44+
return {
45+
backgroundColor: 'transparent',
46+
legend: { type: 'scroll', orient: 'horizontal', top: 8, textStyle: { color: '#aaa' } },
47+
grid: { top: 40, left: 8, right: 8, bottom: 48, containLabel: true },
48+
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
49+
dataZoom: [{
50+
type: 'slider',
51+
showDataShadow: false,
52+
height: 16,
53+
borderColor: 'transparent',
54+
backgroundColor: '#1a1a1b',
55+
fillerColor: 'rgba(255,255,255,0.05)',
56+
handleSize: 16,
57+
handleStyle: { color: '#555', borderColor: '#555', borderWidth: 1 },
58+
moveHandleSize: 4,
59+
textStyle: { color: '#888', fontSize: 10 },
60+
dataBackground: { lineStyle: { color: 'transparent' }, areaStyle: { color: 'transparent' } },
61+
selectedDataBackground: { lineStyle: { color: 'transparent' }, areaStyle: { color: 'transparent' } },
62+
}],
63+
...baseOption,
64+
xAxis: injectSplitLine((baseOption as any)?.xAxis),
65+
yAxis: injectSplitLine((baseOption as any)?.yAxis),
66+
};
67+
}, [option, noData]);
3468

3569
if (loading) {
3670
return (
37-
<div className="mb-4 flex items-center justify-center rounded-2xl overflow-hidden" style={{ height, background: 'rgb(36, 35, 49)', boxShadow: '0px 4px 4px 0px rgba(36, 39, 56, 0.25)' }}>
38-
<div className="text-[#fbe593] text-sm animate-pulse">Loading...</div>
71+
<div className="mb-6">
72+
<div className="flex items-center justify-between gap-4 mb-3">
73+
<h4 className="text-[14px] font-medium text-[#e9eaee]">{title}</h4>
74+
</div>
75+
<div className="flex items-center justify-center" style={{ height }}>
76+
<div className="text-[#7c7c7c] text-sm animate-pulse">Loading...</div>
77+
</div>
3978
</div>
4079
);
4180
}
4281

4382
return (
44-
<div className="mb-4 rounded-2xl overflow-hidden" style={{ background: 'rgb(36, 35, 49)', boxShadow: '0px 4px 4px 0px rgba(36, 39, 56, 0.25)' }}>
83+
<div className="mb-6">
84+
<div className="flex items-center justify-between gap-4 mb-3">
85+
<h4 className="text-[14px] font-medium text-[#e9eaee]">{title}</h4>
86+
{sql && <ShowSQLInline sql={sql} queryName={queryName} queryParams={queryParams} />}
87+
</div>
4588
<EChartsWrapper
4689
option={mergedOption}
4790
notMerge
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import type { EChartsOption } from 'echarts';
2+
3+
export function createTimeSeriesOption (series: any[]): EChartsOption {
4+
return {
5+
xAxis: { type: 'time', min: '2011-01-01' },
6+
yAxis: { type: 'value' },
7+
series,
8+
};
9+
}

apps/web/app/analyze-user/[login]/_hooks/usePersonal.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export function usePersonalData<K extends keyof RequestMap> (key: K, userId: num
4545

4646
return {
4747
data: data?.data as RequestMap[K][] | undefined,
48+
sql: data?.sql as string | undefined,
49+
queryName: data?.query as string | undefined,
4850
loading: isLoading,
4951
error,
5052
};
@@ -101,6 +103,8 @@ export function usePersonalContributionActivities (userId: number | undefined, t
101103

102104
return {
103105
data: data?.data as ContributionActivity[] | undefined,
106+
sql: data?.sql as string | undefined,
107+
queryName: data?.query as string | undefined,
104108
loading: isLoading,
105109
error,
106110
};

apps/web/app/analyze-user/[login]/_sections/activities.tsx

Lines changed: 15 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client';
22

3-
import SectionTemplate from '@/components/Analyze/Section';
3+
import { ScrollspySectionWrapper } from '@/components/Scrollspy/SectionWrapper';
4+
import { SectionHeading } from '@/components/ui/SectionHeading';
45
import { AnalyzeOwnerContext } from '@/components/Context/Analyze/AnalyzeOwner';
56
import * as React from 'react';
67
import { useCallback, useMemo, useState } from 'react';
@@ -15,24 +16,25 @@ import {
1516
usePersonalContributionActivities,
1617
useRange,
1718
} from '../_hooks/usePersonal';
19+
import { TabBar } from '@/components/ui/TabBar';
1820

1921
export default function ActivitiesSection () {
2022
const { id: userId } = React.useContext(AnalyzeOwnerContext);
2123

2224
return (
23-
<SectionTemplate id="activities" title="Contribution Activities" level={2} className="pt-8"
24-
description="All personal activities happened on all public repositories in GitHub since 2011. You can check each specific activity type by type with a timeline."
25-
>
25+
<ScrollspySectionWrapper anchor="activities" className="pt-8 pb-8">
26+
<SectionHeading>Contribution Activities</SectionHeading>
27+
<p className="text-sm text-[#8c8c8c] mb-4">All personal activities happened on all public repositories in GitHub since 2011. You can check each specific activity type by type with a timeline.</p>
2628
<ActivityChart userId={userId} />
27-
</SectionTemplate>
29+
</ScrollspySectionWrapper>
2830
);
2931
}
3032

3133
function ActivityChart ({ userId }: { userId: number }) {
3234
const [type, setType] = useState<ContributionActivityType>('all');
3335
const [period, setPeriod] = useState<ContributionActivityRange>('last_28_days');
3436

35-
const { data, loading } = usePersonalContributionActivities(userId, type, period);
37+
const { data, sql, queryName, loading } = usePersonalContributionActivities(userId, type, period);
3638
const repoNames = useDimension(data ?? [], 'repo_name');
3739

3840
const [min, max] = useRange(period);
@@ -50,8 +52,8 @@ function ActivityChart ({ userId }: { userId: number }) {
5052
const chartData = useMemo(() => (data ?? []).map(r => [r.event_period, r.repo_name, r.cnt]), [data]);
5153

5254
const option = useMemo(() => ({
53-
legend: { type: 'scroll' as const, orient: 'horizontal' as const, top: 32, textStyle: { color: '#aaa' } },
54-
grid: { top: 64, left: 8, right: 8, bottom: 8, containLabel: true },
55+
legend: { type: 'scroll' as const, orient: 'horizontal' as const, top: 8, textStyle: { color: '#aaa' } },
56+
grid: { top: 40, left: 8, right: 8, bottom: 8, containLabel: true },
5557
tooltip: { trigger: 'item' as const },
5658
dataZoom: undefined as any,
5759
xAxis: { type: 'time' as const, min, max },
@@ -65,25 +67,16 @@ function ActivityChart ({ userId }: { userId: number }) {
6567
}],
6668
}), [chartData, repoNames, min, max, tooltipFormatter]);
6769

70+
const queryParams = useMemo(() => ({ userId }), [userId]);
6871
const chartHeight = 240 + 30 * repoNames.length;
6972

7073
return (
7174
<div>
72-
<div className="flex flex-wrap gap-3 mb-4">
73-
<label className="flex flex-col gap-1 text-xs text-[#8c8c8c]">
74-
Contribution type
75-
<select value={type} onChange={e => setType(e.target.value as ContributionActivityType)} className="rounded border border-[#363638] bg-[#2e2e2f] px-2 py-1 text-sm text-[#e0e0e0] outline-none focus:border-[#fbe593]">
76-
{contributionActivityTypes.map(({ key, label }) => <option key={key} value={key}>{label}</option>)}
77-
</select>
78-
</label>
79-
<label className="flex flex-col gap-1 text-xs text-[#8c8c8c]">
80-
Period
81-
<select value={period} onChange={e => setPeriod(e.target.value as ContributionActivityRange)} className="rounded border border-[#363638] bg-[#2e2e2f] px-2 py-1 text-sm text-[#e0e0e0] outline-none focus:border-[#fbe593]">
82-
{contributionActivityRanges.map(({ key, label }) => <option key={key} value={key}>{label}</option>)}
83-
</select>
84-
</label>
75+
<div className="flex flex-wrap gap-6 mb-4">
76+
<TabBar items={contributionActivityTypes} value={type} onChange={(key) => setType(key as ContributionActivityType)} />
77+
<TabBar items={contributionActivityRanges} value={period} onChange={(key) => setPeriod(key as ContributionActivityRange)} />
8578
</div>
86-
<PersonalChart title={title} option={option} height={chartHeight} loading={loading} noData={!loading && (!data || data.length === 0)} />
79+
<PersonalChart title={title} option={option} height={chartHeight} loading={loading} noData={!loading && (!data || data.length === 0)} sql={sql} queryName={queryName} queryParams={queryParams} />
8780
</div>
8881
);
8982
}

apps/web/app/analyze-user/[login]/_sections/behaviour.tsx

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client';
22

3-
import SectionTemplate from '@/components/Analyze/Section';
3+
import { ScrollspySectionWrapper } from '@/components/Scrollspy/SectionWrapper';
4+
import { SectionHeading } from '@/components/ui/SectionHeading';
45
import { AnalyzeOwnerContext } from '@/components/Context/Analyze/AnalyzeOwner';
56
import * as React from 'react';
67
import { useMemo, useState } from 'react';
@@ -22,17 +23,17 @@ export default function BehaviourSection () {
2223
const { id: userId } = React.useContext(AnalyzeOwnerContext);
2324

2425
return (
25-
<SectionTemplate id="behaviour" title="Behaviour" level={2} className="pt-8"
26-
description="You can see the total contributions in different repositories since 2011, as well as check the status of different contribution categories type by type."
27-
>
26+
<ScrollspySectionWrapper anchor="behaviour" className="pt-8 pb-8">
27+
<SectionHeading>Behaviour</SectionHeading>
28+
<p className="text-sm text-[#8c8c8c] mb-4">You can see the total contributions in different repositories since 2011, as well as check the status of different contribution categories type by type.</p>
2829
<AllContributions userId={userId} />
2930
<ContributionTime userId={userId} />
30-
</SectionTemplate>
31+
</ScrollspySectionWrapper>
3132
);
3233
}
3334

3435
function AllContributions ({ userId }: { userId: number }) {
35-
const { data, loading } = usePersonalData('personal-contributions-for-repos', userId);
36+
const { data, sql, queryName, loading } = usePersonalData('personal-contributions-for-repos', userId);
3637

3738
const { repos, seriesList } = useMemo(() => {
3839
if (!data || data.length === 0) return { repos: [], seriesList: [] };
@@ -79,7 +80,9 @@ function AllContributions ({ userId }: { userId: number }) {
7980
series: seriesList,
8081
};
8182

82-
return <PersonalChart title="Type of total contributions" option={option} loading={loading} noData={repos.length === 0} />;
83+
const queryParams = useMemo(() => ({ userId }), [userId]);
84+
85+
return <PersonalChart title="Type of total contributions" option={option} loading={loading} noData={repos.length === 0} sql={sql} queryName={queryName} queryParams={queryParams} />;
8386
}
8487

8588
function ContributionTime ({ userId }: { userId: number }) {
@@ -93,7 +96,7 @@ function ContributionTime ({ userId }: { userId: number }) {
9396

9497
return (
9598
<div className="mb-4">
96-
<h3 className="text-sm font-medium text-[#dadada] mb-3">{title}</h3>
99+
<h3 className="text-[18px] font-semibold text-[#e9eaee] mb-3">{title}</h3>
97100
<div className="flex flex-wrap gap-3 mb-4">
98101
<Select label="Period" value={period} onChange={setPeriod} options={periods.map(p => ({ value: p, label: toCamel(p) }))} />
99102
<Select label="Contribution Type" value={type} onChange={setType} options={eventTypes.map(e => ({ value: e, label: toCamel(e) }))} />
@@ -110,7 +113,7 @@ function Select ({ label, value, onChange, options }: { label: string; value: st
110113
return (
111114
<label className="flex flex-col gap-1 text-xs text-[#8c8c8c]">
112115
{label}
113-
<select value={value} onChange={e => onChange(e.target.value)} className="rounded border border-[#363638] bg-[#2e2e2f] px-2 py-1 text-sm text-[#e0e0e0] outline-none focus:border-[#fbe593]">
116+
<select value={value} onChange={e => onChange(e.target.value)} className="rounded border border-[#363638] bg-[#2e2e2f] px-2 py-1 text-sm text-[#e0e0e0] outline-none focus:border-white">
114117
{options.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
115118
</select>
116119
</label>
Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,34 @@
11
'use client';
22

3-
import SectionTemplate from '@/components/Analyze/Section';
3+
import { ScrollspySectionWrapper } from '@/components/Scrollspy/SectionWrapper';
4+
import { SectionHeading } from '@/components/ui/SectionHeading';
45
import { AnalyzeOwnerContext } from '@/components/Context/Analyze/AnalyzeOwner';
56
import * as React from 'react';
67
import { useMemo } from 'react';
78
import PersonalChart from '../_charts/ChartWrapper';
89
import { orange, primary } from '../_charts/colors';
10+
import { createTimeSeriesOption } from '../_charts/createChartOption';
911
import { usePersonalData } from '../_hooks/usePersonal';
1012

1113
export default function CodeReviewSection () {
1214
const { id: userId } = React.useContext(AnalyzeOwnerContext);
1315

1416
return (
15-
<SectionTemplate id="code-review" title="Code Review" level={2} className="pt-8"
16-
description="The history about the number of code review times and comments in pull requests since 2011."
17-
>
17+
<ScrollspySectionWrapper anchor="code-review" className="pt-8 pb-8">
18+
<SectionHeading>Code Review</SectionHeading>
1819
<CodeReviewHistory userId={userId} />
19-
</SectionTemplate>
20+
</ScrollspySectionWrapper>
2021
);
2122
}
2223

2324
function CodeReviewHistory ({ userId }: { userId: number }) {
24-
const { data, loading } = usePersonalData('personal-pull-request-reviews-history', userId);
25-
const option = useMemo(() => ({
26-
xAxis: { type: 'time' as const, min: '2011-01-01' },
27-
yAxis: { type: 'value' as const },
28-
series: [
29-
{ type: 'bar' as const, data: (data ?? []).map(r => [r.event_month, r.reviews]), name: 'review', color: orange, barMaxWidth: 10 },
30-
{ type: 'bar' as const, data: (data ?? []).map(r => [r.event_month, r.review_comments]), name: 'review comments', color: primary, barMaxWidth: 10 },
31-
],
32-
}), [data]);
25+
const { data, sql, queryName, loading } = usePersonalData('personal-pull-request-reviews-history', userId);
26+
const option = useMemo(() => createTimeSeriesOption([
27+
{ type: 'bar' as const, data: (data ?? []).map(r => [r.event_month, r.reviews]), name: 'review', color: orange, barMaxWidth: 10 },
28+
{ type: 'bar' as const, data: (data ?? []).map(r => [r.event_month, r.review_comments]), name: 'review comments', color: primary, barMaxWidth: 10 },
29+
]), [data]);
3330

34-
return <PersonalChart title="Code Review History" option={option} loading={loading} noData={!loading && (!data || data.length === 0)} />;
31+
const queryParams = useMemo(() => ({ userId }), [userId]);
32+
33+
return <PersonalChart title="Code Review History" option={option} loading={loading} noData={!loading && (!data || data.length === 0)} sql={sql} queryName={queryName} queryParams={queryParams} />;
3534
}

0 commit comments

Comments
 (0)