Skip to content

Commit 20451fe

Browse files
committed
feat: add basic version page with mock data
1 parent 3712560 commit 20451fe

3 files changed

Lines changed: 331 additions & 11 deletions

File tree

app/components/Package/Versions.vue

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,14 @@ function versionRoute(version: string): RouteLocationRaw {
106106
return packageRoute(props.packageName, version)
107107
}
108108
109+
// Route to the full versions history page
110+
const versionsPageRoute = computed((): RouteLocationRaw => {
111+
const [org, name = ''] = props.packageName.startsWith('@')
112+
? props.packageName.split('/')
113+
: ['', props.packageName]
114+
return { name: 'package-versions', params: { org, name } }
115+
})
116+
109117
// Version to tags lookup (supports multiple tags per version)
110118
const versionToTags = computed(() => buildVersionToTagsMap(props.distTags))
111119
@@ -532,15 +540,26 @@ function majorGroupContainsCurrent(group: (typeof otherMajorGroups.value)[0]): b
532540
id="versions"
533541
>
534542
<template #actions>
535-
<ButtonBase
536-
variant="secondary"
537-
class="text-fg-subtle hover:text-fg transition-colors min-w-6 min-h-6 -m-1 p-1 rounded"
538-
:title="$t('package.downloads.community_distribution')"
539-
classicon="i-lucide:file-stack"
540-
@click="openDistributionModal"
541-
>
542-
<span class="sr-only">{{ $t('package.downloads.community_distribution') }}</span>
543-
</ButtonBase>
543+
<div class="flex items-center gap-3">
544+
<LinkBase
545+
:to="versionsPageRoute"
546+
variant="button-secondary"
547+
class="text-fg-subtle hover:text-fg transition-colors min-w-6 min-h-6 p-1 rounded"
548+
title="View all versions"
549+
classicon="i-lucide:history"
550+
>
551+
<span class="sr-only">View all versions</span>
552+
</LinkBase>
553+
<ButtonBase
554+
variant="secondary"
555+
class="text-fg-subtle hover:text-fg transition-colors min-w-6 min-h-6 -m-1 p-1 rounded"
556+
:title="$t('package.downloads.community_distribution')"
557+
classicon="i-lucide:file-stack"
558+
@click="openDistributionModal"
559+
>
560+
<span class="sr-only">{{ $t('package.downloads.community_distribution') }}</span>
561+
</ButtonBase>
562+
</div>
544563
</template>
545564
<div class="space-y-0.5 min-w-0">
546565
<!-- Semver range filter -->

app/pages/package/[[org]]/[name].vue

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ const { diff: sizeDiff } = useInstallSizeDiff(packageName, resolvedVersion, pkg,
272272
// → Preserve the server-rendered DOM, don't flash to skeleton.
273273
const nuxtApp = useNuxtApp()
274274
const route = useRoute()
275+
const isVersionsRoute = computed(() => route.name === 'package-versions')
275276
const hasEmptyPayload =
276277
import.meta.client &&
277278
nuxtApp.payload.serverRendered &&
@@ -736,7 +737,8 @@ const showSkeleton = shallowRef(false)
736737
</script>
737738

738739
<template>
739-
<DevOnly>
740+
<NuxtPage v-if="isVersionsRoute" />
741+
<DevOnly v-else>
740742
<ButtonBase
741743
class="fixed bottom-4 inset-is-4 z-50 shadow-lg rounded-full! px-3! py-2!"
742744
classicon="i-simple-icons:skeleton"
@@ -748,7 +750,7 @@ const showSkeleton = shallowRef(false)
748750
<span class="text-xs">Skeleton</span>
749751
</ButtonBase>
750752
</DevOnly>
751-
<main class="container flex-1 w-full py-8">
753+
<main v-if="!isVersionsRoute" class="container flex-1 w-full py-8">
752754
<!-- Scenario 1: SPA fallback — show skeleton (no real content to preserve) -->
753755
<!-- Scenario 2: SSR with missing payload — preserve server DOM, skip skeleton -->
754756
<PackageSkeleton
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
<script setup lang="ts">
2+
import { compare } from 'semver'
3+
import { buildVersionToTagsMap, buildTaggedVersionRows } from '~/utils/versions'
4+
5+
definePageMeta({
6+
name: 'package-versions',
7+
})
8+
9+
const route = useRoute()
10+
const router = useRouter()
11+
12+
const packageName = computed(() => {
13+
const { org, name } = route.params as { org?: string; name: string }
14+
return org ? `${org}/${name}` : (name as string)
15+
})
16+
17+
// ─── Mock data ────────────────────────────────────────────────────────────────
18+
// TODO: Replace with real data from usePackage() / fetchAllPackageVersions()
19+
20+
interface VersionEntry {
21+
version: string
22+
time: string
23+
deprecated?: string
24+
hasProvenance: boolean
25+
}
26+
27+
const mockDistTags: Record<string, string> = {
28+
'latest': '7.26.3',
29+
'next': '8.0.0-alpha.16',
30+
'babel-7': '7.26.3',
31+
}
32+
33+
const mockVersions: VersionEntry[] = [
34+
{ version: '8.0.0-alpha.16', time: '2024-12-15T10:00:00Z', hasProvenance: true },
35+
{ version: '8.0.0-alpha.15', time: '2024-11-20T10:00:00Z', hasProvenance: true },
36+
{ version: '8.0.0-alpha.14', time: '2024-10-30T10:00:00Z', hasProvenance: true },
37+
{ version: '8.0.0-alpha.13', time: '2024-10-01T10:00:00Z', hasProvenance: false },
38+
{ version: '7.26.3', time: '2024-12-10T10:00:00Z', hasProvenance: true },
39+
{ version: '7.26.2', time: '2024-11-28T10:00:00Z', hasProvenance: true },
40+
{ version: '7.26.1', time: '2024-11-14T10:00:00Z', hasProvenance: true },
41+
{ version: '7.26.0', time: '2024-10-25T10:00:00Z', hasProvenance: true },
42+
{ version: '7.25.9', time: '2024-10-15T10:00:00Z', hasProvenance: true },
43+
{ version: '7.25.8', time: '2024-09-20T10:00:00Z', hasProvenance: true },
44+
{ version: '7.25.7', time: '2024-08-15T10:00:00Z', hasProvenance: false },
45+
{ version: '7.25.6', time: '2024-07-20T10:00:00Z', hasProvenance: false },
46+
{ version: '7.25.5', time: '2024-06-10T10:00:00Z', hasProvenance: false },
47+
{ version: '7.25.4', time: '2024-05-20T10:00:00Z', hasProvenance: false },
48+
{ version: '7.25.3', time: '2024-04-15T10:00:00Z', hasProvenance: false },
49+
{ version: '7.25.2', time: '2024-03-20T10:00:00Z', hasProvenance: false },
50+
{ version: '7.25.1', time: '2024-03-05T10:00:00Z', hasProvenance: false },
51+
{ version: '7.25.0', time: '2024-02-20T10:00:00Z', hasProvenance: false },
52+
{ version: '7.24.7', time: '2024-03-01T10:00:00Z', hasProvenance: false },
53+
{ version: '7.24.6', time: '2024-02-10T10:00:00Z', hasProvenance: false },
54+
{ version: '7.24.5', time: '2024-01-25T10:00:00Z', hasProvenance: false },
55+
{ version: '7.24.4', time: '2024-01-10T10:00:00Z', hasProvenance: false },
56+
{ version: '7.24.0', time: '2023-12-20T10:00:00Z', hasProvenance: false },
57+
{ version: '7.23.6', time: '2023-12-15T10:00:00Z', hasProvenance: false },
58+
{ version: '7.23.5', time: '2023-11-10T10:00:00Z', hasProvenance: false },
59+
{ version: '7.23.0', time: '2023-09-05T10:00:00Z', hasProvenance: false },
60+
{ version: '7.22.5', time: '2023-07-08T10:00:00Z', hasProvenance: false },
61+
{ version: '7.22.0', time: '2023-05-16T10:00:00Z', hasProvenance: false },
62+
{ version: '7.0.0', time: '2018-08-27T10:00:00Z', hasProvenance: false },
63+
{
64+
version: '6.26.0',
65+
time: '2018-03-14T10:00:00Z',
66+
hasProvenance: false,
67+
deprecated: 'Use @babel/types 7.x or later',
68+
},
69+
{
70+
version: '6.25.0',
71+
time: '2018-01-10T10:00:00Z',
72+
hasProvenance: false,
73+
deprecated: 'Use @babel/types 7.x or later',
74+
},
75+
{
76+
version: '6.24.1',
77+
time: '2017-09-12T10:00:00Z',
78+
hasProvenance: false,
79+
deprecated: 'Use @babel/types 7.x or later',
80+
},
81+
]
82+
83+
// ─── Derived data ─────────────────────────────────────────────────────────────
84+
85+
const versionToTagsMap = computed(() => buildVersionToTagsMap(mockDistTags))
86+
87+
const sortedVersions = computed(() =>
88+
[...mockVersions]
89+
.sort((a, b) => compare(b.version, a.version))
90+
.map(v => ({ ...v, tags: versionToTagsMap.value.get(v.version) })),
91+
)
92+
93+
const tagRows = computed(() => buildTaggedVersionRows(mockDistTags))
94+
95+
function getVersionTime(version: string): string | undefined {
96+
return mockVersions.find(v => v.version === version)?.time
97+
}
98+
99+
// ─── Jump to version ──────────────────────────────────────────────────────────
100+
101+
const jumpVersion = ref('')
102+
const jumpError = ref('')
103+
104+
function navigateToVersion() {
105+
const v = jumpVersion.value.trim()
106+
if (!v) return
107+
if (!mockVersions.some(mv => mv.version === v)) {
108+
jumpError.value = `Version "${v}" not found`
109+
return
110+
}
111+
jumpError.value = ''
112+
router.push(packageRoute(packageName.value, v))
113+
}
114+
</script>
115+
116+
<template>
117+
<main class="container flex-1 w-full py-8">
118+
<div class="max-w-3xl mx-auto">
119+
<!-- Back link -->
120+
<div class="mb-6">
121+
<LinkBase :to="packageRoute(packageName)" classicon="i-lucide:arrow-left" class="text-sm">
122+
<span class="font-mono" dir="ltr">{{ packageName }}</span>
123+
</LinkBase>
124+
</div>
125+
126+
<!-- Page heading -->
127+
<h1 class="font-mono text-2xl sm:text-3xl font-medium mb-8" dir="ltr">
128+
{{ packageName }}
129+
<span class="text-fg-muted font-normal text-lg sm:text-2xl ms-2">· Version History</span>
130+
</h1>
131+
132+
<!-- ── Current Tags ─────────────────────────────────────────────────── -->
133+
<section class="mb-10">
134+
<h2 class="text-xs text-fg-subtle uppercase tracking-wider mb-3 ps-1">Current Tags</h2>
135+
<div class="rounded-lg border border-border overflow-hidden">
136+
<table class="w-full">
137+
<thead>
138+
<tr class="border-b border-border bg-bg-subtle">
139+
<th
140+
class="text-start px-4 py-2.5 text-xs text-fg-subtle font-medium uppercase tracking-wider w-1/3"
141+
>
142+
Tag
143+
</th>
144+
<th
145+
class="text-start px-4 py-2.5 text-xs text-fg-subtle font-medium uppercase tracking-wider w-1/3"
146+
>
147+
Version
148+
</th>
149+
<th
150+
class="text-start px-4 py-2.5 text-xs text-fg-subtle font-medium uppercase tracking-wider hidden sm:table-cell"
151+
>
152+
Published
153+
</th>
154+
</tr>
155+
</thead>
156+
<tbody>
157+
<tr
158+
v-for="row in tagRows"
159+
:key="row.id"
160+
class="border-b border-border last:border-0 hover:bg-bg-subtle transition-colors"
161+
>
162+
<td class="px-4 py-3">
163+
<div class="flex items-center gap-1.5 flex-wrap">
164+
<span
165+
v-for="tag in row.tags"
166+
:key="tag"
167+
class="text-4xs font-semibold uppercase tracking-wide"
168+
:class="tag === 'latest' ? 'text-accent' : 'text-fg-subtle'"
169+
>
170+
{{ tag }}
171+
</span>
172+
</div>
173+
</td>
174+
<td class="px-4 py-3">
175+
<LinkBase
176+
:to="packageRoute(packageName, row.version)"
177+
class="font-mono text-sm"
178+
dir="ltr"
179+
>
180+
{{ row.version }}
181+
</LinkBase>
182+
</td>
183+
<td class="px-4 py-3 hidden sm:table-cell">
184+
<DateTime
185+
v-if="getVersionTime(row.version)"
186+
:datetime="getVersionTime(row.version)!"
187+
class="text-xs text-fg-subtle"
188+
year="numeric"
189+
month="short"
190+
day="numeric"
191+
/>
192+
<span v-else class="text-xs text-fg-subtle">—</span>
193+
</td>
194+
</tr>
195+
</tbody>
196+
</table>
197+
</div>
198+
</section>
199+
200+
<!-- ── Version History ──────────────────────────────────────────────── -->
201+
<section>
202+
<!-- Header + jump-to-version -->
203+
<div class="flex flex-wrap items-center justify-between gap-3 mb-4 ps-1">
204+
<h2 class="text-xs text-fg-subtle uppercase tracking-wider">
205+
Version History
206+
<span class="text-fg-subtle ms-1 normal-case font-normal tracking-normal">
207+
({{ sortedVersions.length }})
208+
</span>
209+
</h2>
210+
<div class="flex items-center gap-2">
211+
<InputBase
212+
v-model="jumpVersion"
213+
type="text"
214+
placeholder="e.g. 7.26.0"
215+
:aria-label="'Jump to version'"
216+
size="small"
217+
class="w-36 sm:w-44 font-mono"
218+
@keydown.enter="navigateToVersion"
219+
/>
220+
<ButtonBase
221+
variant="secondary"
222+
size="small"
223+
classicon="i-lucide:arrow-right"
224+
:disabled="!jumpVersion.trim()"
225+
@click="navigateToVersion"
226+
>
227+
Go
228+
</ButtonBase>
229+
</div>
230+
</div>
231+
<p v-if="jumpError" role="alert" class="text-red-500 dark:text-red-400 text-xs mb-3 ps-1">
232+
{{ jumpError }}
233+
</p>
234+
235+
<!-- Version list -->
236+
<div class="rounded-lg border border-border overflow-hidden">
237+
<div
238+
v-for="v in sortedVersions"
239+
:key="v.version"
240+
class="flex items-center gap-3 px-4 py-2.5 border-b border-border last:border-0 hover:bg-bg-subtle transition-colors group relative"
241+
>
242+
<!-- Version + badges -->
243+
<div class="flex-1 min-w-0 flex items-center gap-2 flex-wrap">
244+
<LinkBase
245+
:to="packageRoute(packageName, v.version)"
246+
class="font-mono text-sm after:absolute after:inset-0 after:content-['']"
247+
:class="v.deprecated ? 'text-red-700 dark:text-red-400' : ''"
248+
:classicon="v.deprecated ? 'i-lucide:octagon-alert' : undefined"
249+
dir="ltr"
250+
>
251+
{{ v.version }}
252+
</LinkBase>
253+
254+
<!-- Dist-tags -->
255+
<div v-if="v.tags?.length" class="flex items-center gap-1 flex-wrap relative z-10">
256+
<span
257+
v-for="tag in v.tags"
258+
:key="tag"
259+
class="text-4xs font-semibold uppercase tracking-wide"
260+
:class="tag === 'latest' ? 'text-accent' : 'text-fg-subtle'"
261+
>
262+
{{ tag }}
263+
</span>
264+
</div>
265+
266+
<!-- Deprecated label -->
267+
<span
268+
v-if="v.deprecated"
269+
class="text-3xs font-medium text-red-700 dark:text-red-400 bg-red-100 dark:bg-red-900/30 px-1.5 py-0.5 rounded relative z-10"
270+
:title="v.deprecated"
271+
>
272+
deprecated
273+
</span>
274+
</div>
275+
276+
<!-- Right side: date + provenance -->
277+
<div class="flex items-center gap-3 shrink-0 relative z-10">
278+
<DateTime
279+
v-if="v.time"
280+
:datetime="v.time"
281+
class="text-xs text-fg-subtle hidden sm:block"
282+
year="numeric"
283+
month="short"
284+
day="numeric"
285+
/>
286+
<ProvenanceBadge
287+
v-if="v.hasProvenance"
288+
:package-name="packageName"
289+
:version="v.version"
290+
compact
291+
:linked="false"
292+
/>
293+
</div>
294+
</div>
295+
</div>
296+
</section>
297+
</div>
298+
</main>
299+
</template>

0 commit comments

Comments
 (0)