Skip to content

Commit 920d5f1

Browse files
authored
Merge pull request #30 from VectorInstitute/add_hover_desc_sort_col
Add sorting to each column, desc on hover
2 parents f309430 + eb207ad commit 920d5f1

File tree

1 file changed

+149
-33
lines changed

1 file changed

+149
-33
lines changed

catalog/app/analytics/page.tsx

Lines changed: 149 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import {
1212
Award,
1313
ExternalLink,
1414
BarChart3,
15+
ArrowUpDown,
16+
ArrowUp,
17+
ArrowDown,
1518
} from "lucide-react";
1619
import Image from "next/image";
1720
import { getAssetPath } from "@/lib/utils";
@@ -54,12 +57,24 @@ interface RepoMetrics {
5457
unique_visitors: number;
5558
unique_cloners: number;
5659
language: string | null;
60+
description?: string;
5761
}
5862

63+
interface RepositoryInfo {
64+
repo_id: string;
65+
description: string;
66+
}
67+
68+
type SortColumn = "name" | "language" | "stars" | "forks" | "unique_visitors" | "unique_cloners";
69+
type SortDirection = "asc" | "desc";
70+
5971
export default function AnalyticsPage() {
6072
// Load data dynamically to ensure fresh data during development
6173
const [historicalData, setHistoricalData] = useState<HistoricalData | null>(null);
6274
const [isLoading, setIsLoading] = useState(true);
75+
const [repoDescriptions, setRepoDescriptions] = useState<Record<string, string>>({});
76+
const [sortColumn, setSortColumn] = useState<SortColumn>("unique_cloners");
77+
const [sortDirection, setSortDirection] = useState<SortDirection>("desc");
6378

6479
useEffect(() => {
6580
const loadData = async () => {
@@ -68,12 +83,29 @@ export default function AnalyticsPage() {
6883
process.env.NEXT_PUBLIC_BASE_PATH === "true"
6984
? "/implementation-catalog"
7085
: "";
71-
const response = await fetch(
86+
87+
// Load historical metrics data
88+
const metricsResponse = await fetch(
7289
`${basePath}/data/github_metrics_history.json`
7390
);
74-
if (!response.ok) throw new Error("Failed to fetch metrics data");
75-
const data = await response.json();
76-
setHistoricalData(data);
91+
if (!metricsResponse.ok) throw new Error("Failed to fetch metrics data");
92+
const metricsData = await metricsResponse.json();
93+
setHistoricalData(metricsData);
94+
95+
// Load repository descriptions
96+
try {
97+
const reposResponse = await fetch(`${basePath}/data/repositories.json`);
98+
if (reposResponse.ok) {
99+
const reposData = await reposResponse.json();
100+
const descriptions: Record<string, string> = {};
101+
reposData.repositories?.forEach((repo: RepositoryInfo) => {
102+
descriptions[repo.repo_id] = repo.description;
103+
});
104+
setRepoDescriptions(descriptions);
105+
}
106+
} catch (error) {
107+
console.warn("No repository descriptions found:", error);
108+
}
77109
} catch (error) {
78110
console.warn("No historical metrics data found:", error);
79111
setHistoricalData(null);
@@ -101,10 +133,11 @@ export default function AnalyticsPage() {
101133
unique_visitors: latest.unique_visitors_14d || 0,
102134
unique_cloners: latest.unique_cloners_14d || 0,
103135
language: latest.language,
136+
description: repoDescriptions[repo_id],
104137
} as RepoMetrics;
105138
})
106139
.filter((r): r is RepoMetrics => r !== null);
107-
}, [historicalData]);
140+
}, [historicalData, repoDescriptions]);
108141

109142
// Calculate aggregate metrics
110143
const aggregateMetrics = useMemo(() => {
@@ -145,6 +178,54 @@ export default function AnalyticsPage() {
145178
};
146179
}, [allRepoMetrics]);
147180

181+
// Sort repository metrics
182+
const sortedRepoMetrics = useMemo(() => {
183+
const sorted = [...allRepoMetrics].sort((a, b) => {
184+
let aValue: string | number | null = a[sortColumn];
185+
let bValue: string | number | null = b[sortColumn];
186+
187+
// Handle null/undefined values
188+
if (aValue === null || aValue === undefined) aValue = "";
189+
if (bValue === null || bValue === undefined) bValue = "";
190+
191+
// For strings, use locale compare
192+
if (typeof aValue === "string" && typeof bValue === "string") {
193+
return sortDirection === "asc"
194+
? aValue.localeCompare(bValue)
195+
: bValue.localeCompare(aValue);
196+
}
197+
198+
// For numbers
199+
return sortDirection === "asc"
200+
? (aValue as number) - (bValue as number)
201+
: (bValue as number) - (aValue as number);
202+
});
203+
204+
return sorted;
205+
}, [allRepoMetrics, sortColumn, sortDirection]);
206+
207+
// Handle column header click
208+
const handleSort = (column: SortColumn) => {
209+
if (sortColumn === column) {
210+
setSortDirection(sortDirection === "asc" ? "desc" : "asc");
211+
} else {
212+
setSortColumn(column);
213+
setSortDirection("desc");
214+
}
215+
};
216+
217+
// Get sort icon for a column
218+
const getSortIcon = (column: SortColumn) => {
219+
if (sortColumn !== column) {
220+
return <ArrowUpDown className="w-3 h-3 opacity-50" />;
221+
}
222+
return sortDirection === "asc" ? (
223+
<ArrowUp className="w-3 h-3" />
224+
) : (
225+
<ArrowDown className="w-3 h-3" />
226+
);
227+
};
228+
148229
return (
149230
<div className="min-h-screen bg-gradient-to-br from-gray-50 via-white to-gray-100 dark:from-gray-900 dark:via-gray-900 dark:to-gray-800">
150231
{/* Header */}
@@ -306,60 +387,95 @@ export default function AnalyticsPage() {
306387
<table className="w-full">
307388
<thead className="bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
308389
<tr>
309-
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
310-
Repository
390+
<th
391+
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
392+
onClick={() => handleSort("name")}
393+
>
394+
<div className="flex items-center gap-2">
395+
Repository
396+
{getSortIcon("name")}
397+
</div>
311398
</th>
312-
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
313-
Language
399+
<th
400+
className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
401+
onClick={() => handleSort("language")}
402+
>
403+
<div className="flex items-center gap-2">
404+
Language
405+
{getSortIcon("language")}
406+
</div>
314407
</th>
315-
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
408+
<th
409+
className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
410+
onClick={() => handleSort("stars")}
411+
>
316412
<div className="flex items-center justify-end gap-1">
317413
<Star className="w-3 h-3" />
318414
Stars
415+
{getSortIcon("stars")}
319416
</div>
320417
</th>
321-
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
418+
<th
419+
className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
420+
onClick={() => handleSort("forks")}
421+
>
322422
<div className="flex items-center justify-end gap-1">
323423
<GitFork className="w-3 h-3" />
324424
Forks
425+
{getSortIcon("forks")}
325426
</div>
326427
</th>
327-
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
428+
<th
429+
className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
430+
onClick={() => handleSort("unique_visitors")}
431+
>
328432
<div className="flex items-center justify-end gap-1">
329433
<Eye className="w-3 h-3" />
330434
Visitors (14d)
435+
{getSortIcon("unique_visitors")}
331436
</div>
332437
</th>
333-
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
438+
<th
439+
className="px-6 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors"
440+
onClick={() => handleSort("unique_cloners")}
441+
>
334442
<div className="flex items-center justify-end gap-1">
335443
<Download className="w-3 h-3" />
336444
Cloners (14d)
445+
{getSortIcon("unique_cloners")}
337446
</div>
338447
</th>
339448
</tr>
340449
</thead>
341450
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
342-
{[...allRepoMetrics]
343-
.sort((a, b) => b.unique_cloners - a.unique_cloners)
344-
.map((repo, index) => (
345-
<motion.tr
346-
key={repo.repo_id}
347-
initial={{ opacity: 0, y: 10 }}
348-
animate={{ opacity: 1, y: 0 }}
349-
transition={{ duration: 0.3, delay: index * 0.02 }}
350-
className="hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors"
451+
{sortedRepoMetrics.map((repo, index) => (
452+
<motion.tr
453+
key={repo.repo_id}
454+
initial={{ opacity: 0, y: 10 }}
455+
animate={{ opacity: 1, y: 0 }}
456+
transition={{ duration: 0.3, delay: index * 0.02 }}
457+
className="hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors group"
458+
>
459+
<td
460+
className="px-6 py-4 whitespace-nowrap relative"
461+
title={repo.description}
351462
>
352-
<td className="px-6 py-4 whitespace-nowrap">
353-
<a
354-
href={`https://github.com/${repo.repo_id}`}
355-
target="_blank"
356-
rel="noopener noreferrer"
357-
className="flex items-center gap-2 text-sm font-medium text-vector-magenta hover:text-vector-cobalt dark:text-vector-magenta dark:hover:text-vector-cobalt"
358-
>
359-
{repo.name}
360-
<ExternalLink className="w-3 h-3" />
361-
</a>
362-
</td>
463+
<a
464+
href={`https://github.com/${repo.repo_id}`}
465+
target="_blank"
466+
rel="noopener noreferrer"
467+
className="flex items-center gap-2 text-sm font-medium text-vector-magenta hover:text-vector-cobalt dark:text-vector-magenta dark:hover:text-vector-cobalt"
468+
>
469+
{repo.name}
470+
<ExternalLink className="w-3 h-3" />
471+
</a>
472+
{repo.description && (
473+
<div className="hidden group-hover:block absolute left-0 top-full mt-2 z-50 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 text-sm rounded-lg py-3 px-4 w-96 max-w-[calc(100vw-2rem)] shadow-xl border-2 border-gray-200 dark:border-gray-600 leading-relaxed whitespace-normal break-words">
474+
{repo.description}
475+
<div className="absolute -top-2 left-8 w-4 h-4 bg-white dark:bg-gray-800 border-l-2 border-t-2 border-gray-200 dark:border-gray-600 transform rotate-45"></div>
476+
</div>
477+
)}
478+
</td>
363479
<td className="px-6 py-4 whitespace-nowrap">
364480
{repo.language ? (
365481
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200">

0 commit comments

Comments
 (0)