Skip to content

Commit e0cf0d7

Browse files
committed
feat(web): implement package details page
1 parent fb9e0af commit e0cf0d7

10 files changed

Lines changed: 757 additions & 90 deletions

File tree

web/clef/package-lock.json

Lines changed: 35 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/clef/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"react-dom": "^19.1.0",
3333
"react-hook-form": "^7.60.0",
3434
"react-router": "^7.7.0",
35+
"semver": "^7.7.2",
3536
"tailwind-merge": "^3.3.1",
3637
"zod": "^4.0.5",
3738
"zustand": "^5.0.6"
@@ -42,6 +43,7 @@
4243
"@types/node": "^24.0.15",
4344
"@types/react": "^19.1.8",
4445
"@types/react-dom": "^19.1.6",
46+
"@types/semver": "^7.7.0",
4547
"@vitejs/plugin-react": "^4.7.0",
4648
"tailwindcss": "^4.1.11",
4749
"tw-animate-css": "^1.3.5",

web/clef/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { BrowserRouter, Navigate, Route, Routes } from "react-router";
33
import { composeProviders } from "./lib/compose-providers";
44
import { Dashboard } from "./pages/dashboard/dasboard";
55
import { Layout } from "./pages/layout/layout";
6+
import { Package } from "./pages/package/package";
67
import { Packages } from "./pages/packages/packages";
78

89
const queryClient = new QueryClient({
@@ -30,6 +31,7 @@ function AppRoutes() {
3031
<Route index element={<Navigate to={"/dashboard"} />} />
3132
<Route path="/dashboard" element={<Dashboard />} />
3233
<Route path="/packages" element={<Packages />} />
34+
<Route path="/packages/*" element={<Package />} />
3335
</Route>
3436
</Routes>
3537
);

web/clef/src/hooks/use-package.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { useQuery, useQueryClient } from "@tanstack/react-query";
2+
import { useEffect } from "react";
3+
import { api } from "@/api/client";
4+
import { usePackageStore } from "@/stores/package";
5+
import type { PackageResponse } from "@/types/packages";
6+
7+
// Query keys
8+
export const packageKeys = {
9+
all: ["package"] as const,
10+
detail: (name: string) => [...packageKeys.all, "detail", name] as const,
11+
};
12+
13+
// API function
14+
const fetchPackage = async (name: string): Promise<PackageResponse> => {
15+
return await api.get<PackageResponse>(`/api/v1/packages/${encodeURIComponent(name)}`);
16+
};
17+
18+
// Custom hooks
19+
export const usePackage = (name: string) => {
20+
const { setData, setLoading, setError, setPackageName } = usePackageStore();
21+
const queryClient = useQueryClient();
22+
23+
// Set package name in store
24+
useEffect(() => {
25+
setPackageName(name);
26+
}, [name, setPackageName]);
27+
28+
const query = useQuery({
29+
queryKey: packageKeys.detail(name),
30+
queryFn: () => fetchPackage(name),
31+
staleTime: 5 * 60 * 1000, // 5 minutes
32+
gcTime: 10 * 60 * 1000, // 10 minutes
33+
retry: 3,
34+
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),
35+
enabled: !!name, // Only run query if name is provided
36+
});
37+
38+
// Sync query state with store
39+
useEffect(() => {
40+
setLoading(query.isLoading);
41+
42+
if (query.error) {
43+
setError(query.error.message);
44+
} else if (query.data) {
45+
setData(query.data);
46+
setError(null);
47+
}
48+
}, [query.isLoading, query.error, query.data, setData, setLoading, setError]);
49+
50+
// Refresh function
51+
const refresh = () => {
52+
return queryClient.invalidateQueries({
53+
queryKey: packageKeys.detail(name),
54+
});
55+
};
56+
57+
return {
58+
...query,
59+
data: query.data,
60+
refresh,
61+
};
62+
};

web/clef/src/pages/dashboard/dasboard.tsx

Lines changed: 24 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,6 @@
11
import { Database, HardDrive, Package, TrendingUp } from "lucide-react";
22
import { Button } from "@/components/ui/button";
3-
import {
4-
Card,
5-
CardContent,
6-
CardDescription,
7-
CardHeader,
8-
CardTitle,
9-
} from "@/components/ui/card";
3+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
104
import { Skeleton } from "@/components/ui/skeleton";
115
import { useAnalytics } from "@/hooks/analytics";
126
import { formatBytes, formatNumber, roundNumber } from "@/lib/utils";
@@ -18,9 +12,7 @@ export function Dashboard() {
1812
return (
1913
<div className="flex min-h-[400px] flex-col items-center justify-center space-y-4">
2014
<div className="text-center">
21-
<h3 className="font-semibold text-destructive text-lg">
22-
Error loading analytics
23-
</h3>
15+
<h3 className="font-semibold text-destructive text-lg">Error loading analytics</h3>
2416
<p className="mt-2 text-muted-foreground text-sm">{String(error)}</p>
2517
</div>
2618
<Button onClick={() => window.location.reload()} variant="outline">
@@ -36,9 +28,7 @@ export function Dashboard() {
3628
<div className="flex items-center justify-between">
3729
<div>
3830
<h1 className="font-bold text-3xl tracking-tight">Dashboard</h1>
39-
<p className="text-muted-foreground">
40-
Package registry analytics overview
41-
</p>
31+
<p className="text-muted-foreground">Package registry analytics overview</p>
4232
</div>
4333
</div>
4434

@@ -61,66 +51,44 @@ export function Dashboard() {
6151
<>
6252
<Card>
6353
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
64-
<CardTitle className="font-medium text-sm">
65-
Total Packages
66-
</CardTitle>
54+
<CardTitle className="font-medium text-sm">Total Packages</CardTitle>
6755
<Package className="h-4 w-4 text-muted-foreground" />
6856
</CardHeader>
6957
<CardContent>
70-
<div className="font-bold text-2xl">
71-
{formatNumber(data.total_packages)}
72-
</div>
73-
<p className="text-muted-foreground text-xs">
74-
Packages in registry
75-
</p>
58+
<div className="font-bold text-2xl">{formatNumber(data.total_packages)}</div>
59+
<p className="text-muted-foreground text-xs">Packages in registry</p>
7660
</CardContent>
7761
</Card>
7862

7963
<Card>
8064
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
81-
<CardTitle className="font-medium text-sm">
82-
Total Size
83-
</CardTitle>
65+
<CardTitle className="font-medium text-sm">Total Size</CardTitle>
8466
<HardDrive className="h-4 w-4 text-muted-foreground" />
8567
</CardHeader>
8668
<CardContent>
87-
<div className="font-bold text-2xl">
88-
{data.total_size_mb.toFixed(1)} MB
89-
</div>
90-
<p className="text-muted-foreground text-xs">
91-
Cached packages size
92-
</p>
69+
<div className="font-bold text-2xl">{data.total_size_mb.toFixed(1)} MB</div>
70+
<p className="text-muted-foreground text-xs">Cached packages size</p>
9371
</CardContent>
9472
</Card>
9573

9674
<Card>
9775
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
98-
<CardTitle className="font-medium text-sm">
99-
Cache Hit Rate
100-
</CardTitle>
76+
<CardTitle className="font-medium text-sm">Cache Hit Rate</CardTitle>
10177
<TrendingUp className="h-4 w-4 text-muted-foreground" />
10278
</CardHeader>
10379
<CardContent>
104-
<div className="font-bold text-2xl">
105-
{roundNumber(data.cache_hit_rate, 2)}%
106-
</div>
107-
<p className="text-muted-foreground text-xs">
108-
Cache efficiency
109-
</p>
80+
<div className="font-bold text-2xl">{roundNumber(data.cache_hit_rate, 2)}%</div>
81+
<p className="text-muted-foreground text-xs">Cache efficiency</p>
11082
</CardContent>
11183
</Card>
11284

11385
<Card>
11486
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
115-
<CardTitle className="font-medium text-sm">
116-
Metadata Cache
117-
</CardTitle>
87+
<CardTitle className="font-medium text-sm">Metadata Cache</CardTitle>
11888
<Database className="h-4 w-4 text-muted-foreground" />
11989
</CardHeader>
12090
<CardContent>
121-
<div className="font-bold text-2xl">
122-
{data.metadata_cache_size_mb.toFixed(1)} MB
123-
</div>
91+
<div className="font-bold text-2xl">{data.metadata_cache_size_mb.toFixed(1)} MB</div>
12492
<p className="text-muted-foreground text-xs">
12593
{formatNumber(data.metadata_cache_entries)} metadata files
12694
</p>
@@ -152,43 +120,32 @@ export function Dashboard() {
152120
</div>
153121
))}
154122
</div>
155-
) : data?.most_popular_packages &&
156-
data.most_popular_packages.length > 0 ? (
123+
) : data?.most_popular_packages && data.most_popular_packages.length > 0 ? (
157124
<div className="space-y-3">
158125
{data.most_popular_packages.map((pkg, index) => (
159-
<div
160-
key={pkg.name}
161-
className="flex items-center justify-between"
162-
>
126+
<div key={pkg.name} className="flex items-center justify-between">
163127
<div className="flex items-center space-x-3">
164128
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary/10">
165129
<span className="font-medium text-xs">{index + 1}</span>
166130
</div>
167131
<div className="min-w-0 flex-1 space-y-1">
168-
<p className="truncate font-medium text-sm leading-none">
169-
{pkg.name}
170-
</p>
132+
<p className="truncate font-medium text-sm leading-none">{pkg.name}</p>
171133
<p className="text-muted-foreground text-xs">
172134
{pkg.unique_versions} version
173-
{pkg.unique_versions !== 1 ? "s" : ""}{" "}
174-
{formatBytes(pkg.total_size_bytes)}
135+
{pkg.unique_versions !== 1 ? "s" : ""}{formatBytes(pkg.total_size_bytes)}
175136
</p>
176137
</div>
177138
</div>
178139
<div className="text-right">
179-
<p className="font-medium text-sm">
180-
{pkg.total_downloads}
181-
</p>
140+
<p className="font-medium text-sm">{pkg.total_downloads}</p>
182141
<p className="text-muted-foreground text-xs">downloads</p>
183142
</div>
184143
</div>
185144
))}
186145
</div>
187146
) : (
188147
<div className="flex h-32 items-center justify-center">
189-
<p className="text-muted-foreground text-sm">
190-
No popular packages data
191-
</p>
148+
<p className="text-muted-foreground text-sm">No popular packages data</p>
192149
</div>
193150
)}
194151
</CardContent>
@@ -215,24 +172,16 @@ export function Dashboard() {
215172
) : data?.recent_packages && data.recent_packages.length > 0 ? (
216173
<div className="space-y-3">
217174
{data.recent_packages.slice(0, 5).map((recentPackage) => (
218-
<div
219-
key={recentPackage.package.id}
220-
className="flex items-start space-x-3"
221-
>
175+
<div key={recentPackage.package.id} className="flex items-start space-x-3">
222176
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-primary/10">
223177
<Package className="h-4 w-4" />
224178
</div>
225179
<div className="min-w-0 flex-1 space-y-1">
226-
<p className="truncate font-medium text-sm leading-none">
227-
{recentPackage.package.name}
228-
</p>
180+
<p className="truncate font-medium text-sm leading-none">{recentPackage.package.name}</p>
229181
<p className="text-muted-foreground text-xs">
230182
{recentPackage.package.description
231183
? recentPackage.package.description.length > 80
232-
? `${recentPackage.package.description.substring(
233-
0,
234-
80
235-
)}...`
184+
? `${recentPackage.package.description.substring(0, 80)}...`
236185
: recentPackage.package.description
237186
: "No description"}
238187
</p>
@@ -242,9 +191,7 @@ export function Dashboard() {
242191
</div>
243192
) : (
244193
<div className="flex h-32 items-center justify-center">
245-
<p className="text-muted-foreground text-sm">
246-
No recent packages
247-
</p>
194+
<p className="text-muted-foreground text-sm">No recent packages</p>
248195
</div>
249196
)}
250197
</CardContent>

web/clef/src/pages/layout/layout.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,15 @@ function generateBreadcrumbs(pathname: string) {
3232
const config = breadcrumbConfig[pathname];
3333

3434
if (!config) {
35+
// Handle dynamic routes like /packages/:name
36+
if (pathname.startsWith("/packages/") && pathname !== "/packages") {
37+
const packageName = pathname.split("/packages/")[1];
38+
return {
39+
section: { title: "Packages", href: "/packages" },
40+
page: packageName,
41+
};
42+
}
43+
3544
// Fallback for unknown routes
3645
return {
3746
section: { title: "Dashboard", href: "/dashboard" },

0 commit comments

Comments
 (0)