From 111a2af573bad5e22143ccfba4df17d2682f09d3 Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Wed, 6 May 2026 07:13:41 +0300 Subject: [PATCH] feat(ui): language distribution bar on project cards Extract LanguageBar into a shared component used by both DashPage (compact, bar only) and ProjectPage (full, with percentage labels). Remove redundant forge source text from project cards since the forge icon already indicates it. Co-Authored-By: Claude Opus 4.6 (1M context) --- ui/src/components/LanguageBar.tsx | 35 +++++++++++++++++++++++++++++++ ui/src/pages/DashPage.tsx | 28 +++++++++++-------------- ui/src/pages/ProjectPage.tsx | 32 ++-------------------------- 3 files changed, 49 insertions(+), 46 deletions(-) create mode 100644 ui/src/components/LanguageBar.tsx diff --git a/ui/src/components/LanguageBar.tsx b/ui/src/components/LanguageBar.tsx new file mode 100644 index 0000000..af18af8 --- /dev/null +++ b/ui/src/components/LanguageBar.tsx @@ -0,0 +1,35 @@ +export function LanguageBar({ languages, colorMap, compact }: { + languages: Record; + colorMap: Record; + compact?: boolean; +}) { + const total = Object.values(languages).reduce((a, b) => a + b, 0); + if (total === 0) return null; + + const sorted = Object.entries(languages).sort(([, a], [, b]) => b - a); + + return ( +
+
+ {sorted.map(([lang, bytes]) => ( +
+ ))} +
+ {!compact && ( +
+ {sorted.slice(0, 8).map(([lang, bytes]) => ( + + + {lang} {((bytes / total) * 100).toFixed(1)}% + + ))} +
+ )} +
+ ); +} diff --git a/ui/src/pages/DashPage.tsx b/ui/src/pages/DashPage.tsx index dad7932..1629e34 100644 --- a/ui/src/pages/DashPage.tsx +++ b/ui/src/pages/DashPage.tsx @@ -5,6 +5,7 @@ import Col from 'react-bootstrap/Col'; import Row from 'react-bootstrap/Row'; import { fetchProjects, fetchRepoLanguages, type ProjectSummary } from '../api/client'; +import { LanguageBar } from '../components/LanguageBar'; import { ContributionGraph, AllTimeGraph } from '../components/ContributionGraph'; import { LanguageStreamGraph } from '../components/LanguageStreamGraph'; @@ -31,6 +32,14 @@ export function DashPage() { return map; }, [langsQ.data]); + const langColors = useMemo(() => { + const map: Record = {}; + for (const e of langsQ.data ?? []) { + if (e.color && !map[e.language]) map[e.language] = e.color; + } + return map; + }, [langsQ.data]); + const projects = projectsQ.data ?? []; const ranked = rankProjects(projects); @@ -54,7 +63,7 @@ export function DashPage() { {ranked.map((p) => ( - + ))} @@ -62,17 +71,12 @@ export function DashPage() { ); } -function ProjectCard({ project: p, langs }: { project: ProjectSummary; langs: Record | null }) { - const topLangs = langs ? topLanguages(langs, 3) : null; - +function ProjectCard({ project: p, langs, colorMap }: { project: ProjectSummary; langs: Record | null; colorMap: Record }) { return (
{p.source}{p.repo}
- - {p.source} - {topLangs && ` ยท ${topLangs}`} - + {langs && }
{p.commit_count > 0 && {p.commit_count} commits} {p.issue_count > 0 && {p.issue_count} issues} @@ -95,14 +99,6 @@ function forgeIcon(source: string): string { } } -function topLanguages(langs: Record, n: number): string { - return Object.entries(langs) - .sort(([, a], [, b]) => b - a) - .slice(0, n) - .map(([lang]) => lang.toLowerCase()) - .join(', '); -} - function formatRange(first: string | null, last: string | null): string { const fmt = (iso: string) => new Date(iso).toLocaleDateString('en-GB', { month: 'short', year: 'numeric' }).toLowerCase(); diff --git a/ui/src/pages/ProjectPage.tsx b/ui/src/pages/ProjectPage.tsx index 490e0ff..a0be294 100644 --- a/ui/src/pages/ProjectPage.tsx +++ b/ui/src/pages/ProjectPage.tsx @@ -7,6 +7,7 @@ import ReactMarkdown from 'react-markdown'; import { VerticalTimeline } from 'react-vertical-timeline-component'; import { fetchEvents, fetchReadme, fetchRepoLanguages, fetchProjects, type Source } from '../api/client'; +import { LanguageBar } from '../components/LanguageBar'; import { TimelineEntry } from '../components/TimelineEntry'; export function ProjectPage() { @@ -73,7 +74,7 @@ export function ProjectPage() {

{source}{repo}

- {langs && } + {langs && }
@@ -125,32 +126,3 @@ function forgeIcon(source: string): string { } } -function LanguageBar({ languages, colorMap }: { languages: Record; colorMap: Record }) { - const total = Object.values(languages).reduce((a, b) => a + b, 0); - if (total === 0) return null; - - const sorted = Object.entries(languages).sort(([, a], [, b]) => b - a); - - return ( -
-
- {sorted.map(([lang, bytes]) => ( -
- ))} -
-
- {sorted.slice(0, 8).map(([lang, bytes]) => ( - - - {lang} {((bytes / total) * 100).toFixed(1)}% - - ))} -
-
- ); -}