Skip to content

Commit df858b3

Browse files
committed
chore: convert chart modal to modal route
1 parent 123f321 commit df858b3

2 files changed

Lines changed: 132 additions & 31 deletions

File tree

app/components/PackageWeeklyDownloadStats.vue

Lines changed: 4 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { ref, computed, onMounted, watch } from 'vue'
2+
import { computed, onMounted, watch } from 'vue'
33
import { VueUiSparkline } from 'vue-data-ui/vue-ui-sparkline'
44
import { useCssVariables } from '../composables/useColors'
55
import { OKLCH_NEUTRAL_FALLBACK, lightenOklch } from '../utils/colors'
@@ -8,8 +8,6 @@ const { packageName } = defineProps<{
88
packageName: string
99
}>()
1010
11-
const showModal = shallowRef(false)
12-
1311
const { data: packument } = usePackage(() => packageName)
1412
const createdIso = computed(() => packument.value?.time?.created ?? null)
1513
@@ -193,15 +191,14 @@ const config = computed(() => {
193191
<div class="space-y-8">
194192
<CollapsibleSection id="downloads" :title="$t('package.downloads.title')">
195193
<template #actions>
196-
<button
197-
type="button"
198-
@click="showModal = true"
194+
<NuxtLink
195+
:to="{ name: 'analytics', params: { package: packageName.split('/') } }"
199196
class="link-subtle font-mono text-sm inline-flex items-center gap-1.5 ms-auto shrink-0 self-center focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 rounded"
200197
:title="$t('package.downloads.analyze')"
201198
>
202199
<span class="i-carbon:data-analytics w-4 h-4" aria-hidden="true" />
203200
<span class="sr-only">{{ $t('package.downloads.analyze') }}</span>
204-
</button>
201+
</NuxtLink>
205202
</template>
206203

207204
<div class="w-full overflow-hidden">
@@ -241,30 +238,6 @@ const config = computed(() => {
241238
</div>
242239
</CollapsibleSection>
243240
</div>
244-
245-
<ChartModal v-model:open="showModal">
246-
<template #title>{{ $t('package.downloads.modal_title') }}</template>
247-
248-
<PackageDownloadAnalytics
249-
:weeklyDownloads="weeklyDownloads"
250-
:inModal="true"
251-
:packageName="packageName"
252-
:createdIso="createdIso"
253-
/>
254-
255-
<template #after="{ close }">
256-
<div class="sm:hidden flex justify-center">
257-
<button
258-
type="button"
259-
@click="close"
260-
class="w-12 h-12 bg-bg-elevated border border-border rounded-full shadow-lg flex items-center justify-center text-fg-muted hover:text-fg transition-colors"
261-
:aria-label="$t('common.close')"
262-
>
263-
<span class="w-5 h-5 i-carbon:close" aria-hidden="true" />
264-
</button>
265-
</div>
266-
</template>
267-
</ChartModal>
268241
</template>
269242

270243
<style>
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
<script setup lang="ts">
2+
definePageMeta({
3+
name: 'analytics',
4+
})
5+
6+
const route = useRoute('analytics')
7+
const router = useRouter()
8+
9+
// Extract package name from route params (handles scoped packages like @org/pkg)
10+
const packageName = computed(() => {
11+
const params = route.params.package || []
12+
return params.join('/')
13+
})
14+
15+
// Fetch package data to get creation date and check if package exists
16+
const { data: packument, status: packageStatus } = usePackage(packageName)
17+
const createdIso = computed(() => packument.value?.time?.created ?? null)
18+
19+
// Determine if package exists (has data after loading)
20+
const packageExists = computed(() => packageStatus.value === 'success' && !!packument.value)
21+
22+
// Close destination: package page if exists, home otherwise
23+
const closeDestination = computed(() => (packageExists.value ? `/${packageName.value}` : '/'))
24+
25+
// Fetch weekly downloads for initial display
26+
const { fetchPackageDownloadEvolution } = useCharts()
27+
const weeklyDownloads = shallowRef<WeeklyDownloadPoint[]>([])
28+
29+
async function loadWeeklyDownloads() {
30+
if (!import.meta.client) return
31+
if (!packageName.value) return
32+
33+
try {
34+
const result = await fetchPackageDownloadEvolution(
35+
() => packageName.value,
36+
() => createdIso.value,
37+
() => ({ granularity: 'week' as const, weeks: 52 }),
38+
)
39+
weeklyDownloads.value = (result as WeeklyDownloadPoint[]) ?? []
40+
} catch {
41+
weeklyDownloads.value = []
42+
}
43+
}
44+
45+
onMounted(() => {
46+
loadWeeklyDownloads()
47+
})
48+
49+
watch(packageName, () => loadWeeklyDownloads())
50+
51+
function handleKeydown(event: KeyboardEvent) {
52+
if (event.key === 'Escape') {
53+
router.push(closeDestination.value)
54+
}
55+
}
56+
57+
// SEO
58+
useSeoMeta({
59+
title: () => (packageName.value ? `${packageName.value} Downloads - npmx` : 'Downloads - npmx'),
60+
description: () => `Download statistics and trends for ${packageName.value}`,
61+
})
62+
</script>
63+
64+
<template>
65+
<Teleport to="body">
66+
<div
67+
class="fixed inset-0 z-50 flex items-center justify-center p-0 sm:p-4"
68+
@keydown="handleKeydown"
69+
>
70+
<!-- Backdrop - clicking navigates back -->
71+
<NuxtLink
72+
:to="closeDestination"
73+
class="absolute inset-0 bg-black/60 cursor-default"
74+
:aria-label="$t('common.close_modal')"
75+
/>
76+
77+
<div
78+
class="relative w-full h-full sm:h-auto bg-bg sm:border sm:border-border sm:rounded-lg shadow-xl sm:max-h-[90vh] overflow-y-auto overscroll-contain sm:max-w-3xl"
79+
role="dialog"
80+
aria-modal="true"
81+
aria-labelledby="analytics-modal-title"
82+
>
83+
<div class="p-4 sm:p-6">
84+
<div class="flex items-center justify-between mb-4 sm:mb-6">
85+
<h1 id="analytics-modal-title" class="font-mono text-lg font-medium">
86+
{{ $t('package.downloads.modal_title') }}
87+
</h1>
88+
<NuxtLink
89+
:to="closeDestination"
90+
class="text-fg-subtle hover:text-fg transition-colors duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-fg/50 rounded"
91+
:aria-label="$t('common.close')"
92+
>
93+
<span class="i-carbon-close block w-5 h-5" aria-hidden="true" />
94+
</NuxtLink>
95+
</div>
96+
<div class="font-mono text-sm">
97+
<ClientOnly>
98+
<PackageDownloadAnalytics
99+
:weeklyDownloads="weeklyDownloads"
100+
:inModal="true"
101+
:packageName="packageName"
102+
:createdIso="createdIso"
103+
/>
104+
<template #fallback>
105+
<div class="min-h-[260px] flex items-center justify-center">
106+
<span
107+
class="i-carbon:circle-dash w-6 h-6 motion-safe:animate-spin text-fg-subtle"
108+
/>
109+
</div>
110+
</template>
111+
</ClientOnly>
112+
</div>
113+
</div>
114+
115+
<!-- Mobile close button -->
116+
<div class="sm:hidden flex justify-center pb-4">
117+
<NuxtLink
118+
:to="closeDestination"
119+
class="w-12 h-12 bg-bg-elevated border border-border rounded-full shadow-lg flex items-center justify-center text-fg-muted hover:text-fg transition-colors"
120+
:aria-label="$t('common.close')"
121+
>
122+
<span class="w-5 h-5 i-carbon:close" aria-hidden="true" />
123+
</NuxtLink>
124+
</div>
125+
</div>
126+
</div>
127+
</Teleport>
128+
</template>

0 commit comments

Comments
 (0)