Skip to content

Commit 3e46cc8

Browse files
committed
chore: add client only and restore facets from url
1 parent f5c7eed commit 3e46cc8

File tree

3 files changed

+181
-116
lines changed

3 files changed

+181
-116
lines changed

app/components/Compare/FacetRow.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,9 +120,9 @@ function isCellLoading(index: number): boolean {
120120
/>
121121
</template>
122122

123-
<!-- No data -->
123+
<!-- No data (Skeleton) -->
124124
<template v-else-if="!value">
125-
<span class="text-fg-subtle text-sm">-</span>
125+
<SkeletonInline class="w-16 h-4" />
126126
</template>
127127

128128
<!-- Value display -->

app/composables/usePackageComparison.ts

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -75,11 +75,15 @@ export function usePackageComparison(packageNames: MaybeRefOrGetter<string[]>) {
7575
const bytesFormatter = useBytesFormatter()
7676
const packages = computed(() => toValue(packageNames))
7777

78+
const ready = shallowRef(false)
79+
7880
// Cache of fetched data by package name (source of truth)
7981
const cache = shallowRef(new Map<string, PackageComparisonData>())
8082

8183
// Derived array in current package order
82-
const packagesData = computed(() => packages.value.map(name => cache.value.get(name) ?? null))
84+
const packagesData = computed(
85+
() => ready.value && packages.value.map(name => cache.value.get(name) ?? null),
86+
)
8387

8488
const status = shallowRef<'idle' | 'pending' | 'success' | 'error'>('idle')
8589
const error = shallowRef<Error | null>(null)
@@ -249,18 +253,24 @@ export function usePackageComparison(packageNames: MaybeRefOrGetter<string[]>) {
249253

250254
// Watch for package changes and refetch (client-side only)
251255
if (import.meta.client) {
252-
watch(
253-
packages,
254-
newPackages => {
255-
fetchPackages(newPackages)
256-
},
257-
{ immediate: true },
258-
)
256+
useNuxtApp().hook('app:suspense:resolve', () => {
257+
ready.value = true
258+
watch(
259+
packages,
260+
newPackages => {
261+
fetchPackages(newPackages)
262+
},
263+
{ immediate: true },
264+
)
265+
})
259266
}
260267

261268
// Compute values for each facet
262269
function getFacetValues(facet: ComparisonFacet): (FacetValue | null)[] {
263-
if (!packagesData.value || packagesData.value.length === 0) return []
270+
// If not ready or no data, return array of nulls to render skeletons
271+
if (!ready.value || !packagesData.value || packagesData.value.length === 0) {
272+
return Array.from({ length: packages.value.length }, () => null)
273+
}
264274

265275
return packagesData.value.map(pkg => {
266276
if (!pkg) return null
@@ -277,7 +287,7 @@ export function usePackageComparison(packageNames: MaybeRefOrGetter<string[]>) {
277287

278288
// Check if a facet depends on slow-loading data
279289
function isFacetLoading(facet: ComparisonFacet): boolean {
280-
if (!installSizeLoading.value) return false
290+
if (!ready.value || !installSizeLoading.value) return false
281291
// These facets depend on install-size API
282292
return facet === 'installSize' || facet === 'totalDependencies'
283293
}

app/pages/compare.vue

Lines changed: 159 additions & 104 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,11 @@ const gridColumns = computed(() =>
4747
.map((pkg, i) => ({ pkg, originalIndex: i }))
4848
.filter(({ pkg }) => pkg !== NO_DEPENDENCY_ID)
4949
.map(({ pkg, originalIndex }) => {
50-
const data = packagesData.value?.[originalIndex]
50+
// packagesData can be false (not ready) or array with nulls (loading)
51+
// Ensure we handle the boolean case safely for TS
52+
const list = packagesData.value
53+
const data = Array.isArray(list) ? list[originalIndex] : null
54+
5155
return {
5256
name: data?.package.name || pkg,
5357
version: data?.package.version,
@@ -80,10 +84,12 @@ const gridHeaders = computed(() =>
8084
)
8185
8286
useSeoMeta({
83-
title: () =>
84-
packages.value.length > 0
85-
? $t('compare.packages.meta_title', { packages: packages.value.join(' vs ') })
86-
: $t('compare.packages.meta_title_empty'),
87+
title: () => {
88+
if (packages.value.length === 0) return $t('compare.packages.meta_title_empty')
89+
const title = $t('compare.packages.meta_title', { packages: packages.value.join(' vs ') })
90+
// Avoid long titles (SEO/HTML validation)
91+
return title.length > 60 ? $t('compare.packages.meta_title_empty') : title
92+
},
8793
ogTitle: () =>
8894
packages.value.length > 0
8995
? $t('compare.packages.meta_title', { packages: packages.value.join(' vs ') })
@@ -135,31 +141,38 @@ useSeoMeta({
135141
<h2 id="packages-heading" class="text-xs text-fg-subtle uppercase tracking-wider mb-3">
136142
{{ $t('compare.packages.section_packages') }}
137143
</h2>
138-
<ComparePackageSelector v-model="packages" :max="4" />
144+
<ClientOnly>
145+
<ComparePackageSelector v-model="packages" :max="4" />
146+
<template #fallback>
147+
<SkeletonBlock class="min-h-[42px]" />
148+
</template>
149+
</ClientOnly>
139150

140-
<!-- "No dep" replacement suggestions (native, simple) -->
141-
<div v-if="noDepSuggestions.length > 0" class="mt-3 space-y-2">
142-
<CompareReplacementSuggestion
143-
v-for="suggestion in noDepSuggestions"
144-
:key="suggestion.forPackage"
145-
:package-name="suggestion.forPackage"
146-
:replacement="suggestion.replacement"
147-
variant="nodep"
148-
:show-action="canAddNoDep"
149-
@add-no-dep="addNoDep"
150-
/>
151-
</div>
151+
<ClientOnly>
152+
<!-- "No dep" replacement suggestions (native, simple) -->
153+
<div v-if="noDepSuggestions.length > 0" class="mt-3 space-y-2">
154+
<CompareReplacementSuggestion
155+
v-for="suggestion in noDepSuggestions"
156+
:key="suggestion.forPackage"
157+
:package-name="suggestion.forPackage"
158+
:replacement="suggestion.replacement"
159+
variant="nodep"
160+
:show-action="canAddNoDep"
161+
@add-no-dep="addNoDep"
162+
/>
163+
</div>
152164

153-
<!-- Informational replacement suggestions (documented) -->
154-
<div v-if="infoSuggestions.length > 0" class="mt-3 space-y-2">
155-
<CompareReplacementSuggestion
156-
v-for="suggestion in infoSuggestions"
157-
:key="suggestion.forPackage"
158-
:package-name="suggestion.forPackage"
159-
:replacement="suggestion.replacement"
160-
variant="info"
161-
/>
162-
</div>
165+
<!-- Informational replacement suggestions (documented) -->
166+
<div v-if="infoSuggestions.length > 0" class="mt-3 space-y-2">
167+
<CompareReplacementSuggestion
168+
v-for="suggestion in infoSuggestions"
169+
:key="suggestion.forPackage"
170+
:package-name="suggestion.forPackage"
171+
:replacement="suggestion.replacement"
172+
variant="info"
173+
/>
174+
</div>
175+
</ClientOnly>
163176
</section>
164177

165178
<!-- Facet selector -->
@@ -168,47 +181,86 @@ useSeoMeta({
168181
<h2 id="facets-heading" class="text-xs text-fg-subtle uppercase tracking-wider">
169182
{{ $t('compare.packages.section_facets') }}
170183
</h2>
171-
<ButtonBase
172-
size="small"
173-
:aria-pressed="isAllSelected"
174-
:disabled="isAllSelected"
175-
:aria-label="$t('compare.facets.select_all')"
176-
@click="selectAll"
177-
>
178-
{{ $t('compare.facets.all') }}
179-
</ButtonBase>
180-
<span class="text-3xs text-fg-muted/40" aria-hidden="true">/</span>
181-
<ButtonBase
182-
size="small"
183-
:aria-pressed="isNoneSelected"
184-
:disabled="isNoneSelected"
185-
:aria-label="$t('compare.facets.deselect_all')"
186-
@click="deselectAll"
187-
>
188-
{{ $t('compare.facets.none') }}
189-
</ButtonBase>
184+
<ClientOnly>
185+
<div class="contents">
186+
<ButtonBase
187+
size="small"
188+
:aria-pressed="isAllSelected"
189+
:disabled="isAllSelected"
190+
:aria-label="$t('compare.facets.select_all')"
191+
@click="selectAll"
192+
>
193+
{{ $t('compare.facets.all') }}
194+
</ButtonBase>
195+
<span class="text-3xs text-fg-muted/40" aria-hidden="true">/</span>
196+
<ButtonBase
197+
size="small"
198+
:aria-pressed="isNoneSelected"
199+
:disabled="isNoneSelected"
200+
:aria-label="$t('compare.facets.deselect_all')"
201+
@click="deselectAll"
202+
>
203+
{{ $t('compare.facets.none') }}
204+
</ButtonBase>
205+
</div>
206+
<template #fallback>
207+
<div class="flex gap-2 items-center">
208+
<SkeletonBlock class="w-8 h-6" />
209+
<span class="text-3xs text-fg-muted/40" aria-hidden="true">/</span>
210+
<SkeletonBlock class="w-10 h-6" />
211+
</div>
212+
</template>
213+
</ClientOnly>
190214
</div>
191-
<CompareFacetSelector />
215+
<ClientOnly>
216+
<CompareFacetSelector />
217+
<template #fallback>
218+
<div class="space-y-3">
219+
<div v-for="i in 4" :key="i">
220+
<div class="flex items-center gap-2 mb-2">
221+
<SkeletonBlock class="w-20 h-3" />
222+
<SkeletonBlock class="w-8 h-6" />
223+
<span class="text-2xs text-fg-muted/40">/</span>
224+
<SkeletonBlock class="w-10 h-6" />
225+
</div>
226+
<div class="flex items-center gap-1.5 flex-wrap">
227+
<SkeletonBlock v-for="j in 3" :key="j" class="w-24 h-6" />
228+
</div>
229+
</div>
230+
</div>
231+
</template>
232+
</ClientOnly>
192233
</section>
193234

194-
<!-- Comparison grid -->
195-
<section v-if="canCompare" class="mt-10" aria-labelledby="comparison-heading">
196-
<h2 id="comparison-heading" class="text-xs text-fg-subtle uppercase tracking-wider mb-4">
197-
{{ $t('compare.packages.section_comparison') }}
198-
</h2>
235+
<ClientOnly>
236+
<!-- Comparison grid -->
237+
<section v-if="canCompare" class="mt-10" aria-labelledby="comparison-heading">
238+
<h2 id="comparison-heading" class="text-xs text-fg-subtle uppercase tracking-wider mb-4">
239+
{{ $t('compare.packages.section_comparison') }}
240+
</h2>
199241

200-
<div
201-
v-if="status === 'pending' && (!packagesData || packagesData.every(p => p === null))"
202-
class="flex items-center justify-center py-12"
203-
>
204-
<LoadingSpinner :text="$t('compare.packages.loading')" />
205-
</div>
242+
<!-- Show grid if we have packages, even if loading (skeletons) -->
243+
<div v-if="packages.length > 0">
244+
<!-- Desktop: Grid layout -->
245+
<div class="hidden md:block overflow-x-auto">
246+
<CompareComparisonGrid :columns="gridColumns" :show-no-dependency="showNoDependency">
247+
<CompareFacetRow
248+
v-for="facet in selectedFacets"
249+
:key="facet.id"
250+
:label="facet.label"
251+
:description="facet.description"
252+
:values="getFacetValues(facet.id)"
253+
:facet-loading="isFacetLoading(facet.id)"
254+
:column-loading="columnLoading"
255+
:bar="facet.id !== 'lastUpdated'"
256+
:headers="gridHeaders"
257+
/>
258+
</CompareComparisonGrid>
259+
</div>
206260

207-
<div v-else-if="packagesData && packagesData.some(p => p !== null)">
208-
<!-- Desktop: Grid layout -->
209-
<div class="hidden md:block overflow-x-auto">
210-
<CompareComparisonGrid :columns="gridColumns" :show-no-dependency="showNoDependency">
211-
<CompareFacetRow
261+
<!-- Mobile: Card-based layout -->
262+
<div class="md:hidden space-y-3">
263+
<CompareFacetCard
212264
v-for="facet in selectedFacets"
213265
:key="facet.id"
214266
:label="facet.label"
@@ -219,52 +271,55 @@ useSeoMeta({
219271
:bar="facet.id !== 'lastUpdated'"
220272
:headers="gridHeaders"
221273
/>
222-
</CompareComparisonGrid>
274+
</div>
275+
276+
<h2
277+
id="comparison-heading"
278+
class="text-xs text-fg-subtle uppercase tracking-wider mb-4 mt-10"
279+
>
280+
{{ $t('compare.facets.trends.title') }}
281+
</h2>
282+
283+
<CompareLineChart :packages="packages.filter(p => p !== NO_DEPENDENCY_ID)" />
223284
</div>
224285

225-
<!-- Mobile: Card-based layout -->
226-
<div class="md:hidden space-y-3">
227-
<CompareFacetCard
228-
v-for="facet in selectedFacets"
229-
:key="facet.id"
230-
:label="facet.label"
231-
:description="facet.description"
232-
:values="getFacetValues(facet.id)"
233-
:facet-loading="isFacetLoading(facet.id)"
234-
:column-loading="columnLoading"
235-
:bar="facet.id !== 'lastUpdated'"
236-
:headers="gridHeaders"
237-
/>
286+
<div v-else-if="status === 'error'" class="text-center py-12" role="alert">
287+
<p class="text-fg-muted">{{ $t('compare.packages.error') }}</p>
238288
</div>
289+
</section>
239290

240-
<h2
241-
id="comparison-heading"
242-
class="text-xs text-fg-subtle uppercase tracking-wider mb-4 mt-10"
243-
>
244-
{{ $t('compare.facets.trends.title') }}
291+
<!-- Empty state -->
292+
<section
293+
v-else
294+
class="text-center px-1.5 py-16 border border-dashed border-border-hover rounded-lg"
295+
>
296+
<div class="i-carbon:compare w-12 h-12 text-fg-subtle mx-auto mb-4" aria-hidden="true" />
297+
<h2 class="font-mono text-lg text-fg-muted mb-2">
298+
{{ $t('compare.packages.empty_title') }}
245299
</h2>
300+
<p class="text-sm text-fg-subtle max-w-md mx-auto">
301+
{{ $t('compare.packages.empty_description') }}
302+
</p>
303+
</section>
246304

247-
<CompareLineChart :packages="packages.filter(p => p !== NO_DEPENDENCY_ID)" />
248-
</div>
249-
250-
<div v-else class="text-center py-12" role="alert">
251-
<p class="text-fg-muted">{{ $t('compare.packages.error') }}</p>
252-
</div>
253-
</section>
254-
255-
<!-- Empty state -->
256-
<section
257-
v-else
258-
class="text-center px-1.5 py-16 border border-dashed border-border-hover rounded-lg"
259-
>
260-
<div class="i-carbon:compare w-12 h-12 text-fg-subtle mx-auto mb-4" aria-hidden="true" />
261-
<h2 class="font-mono text-lg text-fg-muted mb-2">
262-
{{ $t('compare.packages.empty_title') }}
263-
</h2>
264-
<p class="text-sm text-fg-subtle max-w-md mx-auto">
265-
{{ $t('compare.packages.empty_description') }}
266-
</p>
267-
</section>
305+
<template #fallback>
306+
<!-- Generic loading state for the whole grid section if needed, or rely on inner skeletons -->
307+
<div class="mt-10 space-y-4">
308+
<SkeletonBlock class="w-32 h-4 mb-4" />
309+
<div class="border border-border rounded-lg p-4 space-y-4">
310+
<div class="flex gap-4">
311+
<SkeletonBlock class="w-1/4 h-8" />
312+
<SkeletonBlock class="w-1/4 h-8" />
313+
<SkeletonBlock class="w-1/4 h-8" />
314+
<SkeletonBlock class="w-1/4 h-8" />
315+
</div>
316+
<div v-for="i in 5" :key="i" class="flex gap-4">
317+
<SkeletonBlock class="w-full h-12" />
318+
</div>
319+
</div>
320+
</div>
321+
</template>
322+
</ClientOnly>
268323
</div>
269324
</main>
270325
</template>

0 commit comments

Comments
 (0)