Skip to content

Commit 54e08d0

Browse files
feat: add permalink to downloads modal (#1299)
Co-authored-by: Willow (GHOST) <ghostdevbusiness@gmail.com>
1 parent 3ff5d58 commit 54e08d0

File tree

5 files changed

+171
-73
lines changed

5 files changed

+171
-73
lines changed

app/components/Package/TrendsChart.vue

Lines changed: 49 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,44 @@ import { useCssVariables } from '~/composables/useColors'
66
import { OKLCH_NEUTRAL_FALLBACK, transparentizeOklch } from '~/utils/colors'
77
import { getFrameworkColor, isListedFramework } from '~/utils/frameworks'
88
import { drawNpmxLogoAndTaglineWatermark } from '~/composables/useChartWatermark'
9+
import type {
10+
ChartTimeGranularity,
11+
DailyDataPoint,
12+
DateRangeFields,
13+
EvolutionData,
14+
EvolutionOptions,
15+
MonthlyDataPoint,
16+
WeeklyDataPoint,
17+
YearlyDataPoint,
18+
} from '~/types/chart'
19+
20+
const props = withDefaults(
21+
defineProps<{
22+
// For single package downloads history
23+
weeklyDownloads?: WeeklyDataPoint[]
24+
inModal?: boolean
925
10-
const props = defineProps<{
11-
// For single package downloads history
12-
weeklyDownloads?: WeeklyDataPoint[]
13-
inModal?: boolean
14-
15-
/**
16-
* Backward compatible single package mode.
17-
* Used when `weeklyDownloads` is provided.
18-
*/
19-
packageName?: string
26+
/**
27+
* Backward compatible single package mode.
28+
* Used when `weeklyDownloads` is provided.
29+
*/
30+
packageName?: string
2031
21-
/**
22-
* Multi-package mode.
23-
* Used when `weeklyDownloads` is not provided.
24-
*/
25-
packageNames?: string[]
26-
createdIso?: string | null
32+
/**
33+
* Multi-package mode.
34+
* Used when `weeklyDownloads` is not provided.
35+
*/
36+
packageNames?: string[]
37+
createdIso?: string | null
2738
28-
/** When true, shows facet selector (e.g. Downloads / Likes). */
29-
showFacetSelector?: boolean
30-
}>()
39+
/** When true, shows facet selector (e.g. Downloads / Likes). */
40+
showFacetSelector?: boolean
41+
permalink?: boolean
42+
}>(),
43+
{
44+
permalink: false,
45+
},
46+
)
3147
3248
const { locale } = useI18n()
3349
const { accentColors, selectedAccentColor } = useAccentColor()
@@ -110,14 +126,7 @@ const watermarkColors = computed(() => ({
110126
const mobileBreakpointWidth = 640
111127
const isMobile = computed(() => width.value > 0 && width.value < mobileBreakpointWidth)
112128
113-
type ChartTimeGranularity = 'daily' | 'weekly' | 'monthly' | 'yearly'
114129
const DEFAULT_GRANULARITY: ChartTimeGranularity = 'weekly'
115-
type EvolutionData = DailyDataPoint[] | WeeklyDataPoint[] | MonthlyDataPoint[] | YearlyDataPoint[]
116-
117-
type DateRangeFields = {
118-
startDate?: string
119-
endDate?: string
120-
}
121130
122131
function isRecord(value: unknown): value is Record<string, unknown> {
123132
return typeof value === 'object' && value !== null
@@ -322,7 +331,10 @@ const effectivePackageNames = computed<string[]>(() => {
322331
return single ? [single] : []
323332
})
324333
325-
const selectedGranularity = shallowRef<ChartTimeGranularity>(DEFAULT_GRANULARITY)
334+
const selectedGranularity = usePermalink<ChartTimeGranularity>('granularity', DEFAULT_GRANULARITY, {
335+
permanent: props.permalink,
336+
})
337+
326338
const displayedGranularity = shallowRef<ChartTimeGranularity>(DEFAULT_GRANULARITY)
327339
328340
const isEndDateOnPeriodEnd = computed(() => {
@@ -352,8 +364,13 @@ const shouldRenderEstimationOverlay = computed(
352364
() => !pending.value && isEstimationGranularity.value,
353365
)
354366
355-
const startDate = shallowRef<string>('') // YYYY-MM-DD
356-
const endDate = shallowRef<string>('') // YYYY-MM-DD
367+
const startDate = usePermalink<string>('start', '', {
368+
permanent: props.permalink,
369+
})
370+
const endDate = usePermalink<string>('end', '', {
371+
permanent: props.permalink,
372+
})
373+
357374
const hasUserEditedDates = shallowRef(false)
358375
359376
/**
@@ -578,7 +595,9 @@ const METRICS = computed<MetricDef[]>(() => [
578595
},
579596
])
580597
581-
const selectedMetric = shallowRef<MetricId>(DEFAULT_METRIC_ID)
598+
const selectedMetric = usePermalink<MetricId>('facet', DEFAULT_METRIC_ID, {
599+
permanent: props.permalink,
600+
})
582601
583602
// Per-metric state keyed by metric id
584603
const metricStates = reactive<

app/components/Package/WeeklyDownloadStats.vue

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,36 @@
11
<script setup lang="ts">
22
import { VueUiSparkline } from 'vue-data-ui/vue-ui-sparkline'
33
import { useCssVariables } from '~/composables/useColors'
4+
import type { WeeklyDataPoint } from '~/types/chart'
45
import { OKLCH_NEUTRAL_FALLBACK, lightenOklch } from '~/utils/colors'
56
67
const props = defineProps<{
78
packageName: string
89
createdIso: string | null
910
}>()
1011
12+
const router = useRouter()
13+
const route = useRoute()
14+
1115
const chartModal = useModal('chart-modal')
1216
const hasChartModalTransitioned = shallowRef(false)
13-
const isChartModalOpen = shallowRef(false)
17+
18+
const isChartModalOpen = shallowRef<boolean>(false)
1419
1520
function handleModalClose() {
1621
isChartModalOpen.value = false
1722
hasChartModalTransitioned.value = false
23+
24+
router.replace({
25+
query: {
26+
...route.query,
27+
modal: undefined,
28+
granularity: undefined,
29+
end: undefined,
30+
start: undefined,
31+
facet: undefined,
32+
},
33+
})
1834
}
1935
2036
function handleModalTransitioned() {
@@ -95,6 +111,14 @@ async function openChartModal() {
95111
96112
isChartModalOpen.value = true
97113
hasChartModalTransitioned.value = false
114+
115+
await router.replace({
116+
query: {
117+
...route.query,
118+
modal: 'chart',
119+
},
120+
})
121+
98122
// ensure the component renders before opening the dialog
99123
await nextTick()
100124
await nextTick()
@@ -119,8 +143,16 @@ async function loadWeeklyDownloads() {
119143
}
120144
}
121145
122-
onMounted(() => {
123-
loadWeeklyDownloads()
146+
onMounted(async () => {
147+
await loadWeeklyDownloads()
148+
149+
if (route.query.modal === 'chart') {
150+
isChartModalOpen.value = true
151+
}
152+
153+
if (isChartModalOpen.value && hasWeeklyDownloads.value) {
154+
openChartModal()
155+
}
124156
})
125157
126158
watch(
@@ -284,6 +316,7 @@ const config = computed(() => {
284316
:inModal="true"
285317
:packageName="props.packageName"
286318
:createdIso="createdIso"
319+
permalink
287320
show-facet-selector
288321
/>
289322
</Transition>

app/composables/useCharts.ts

Lines changed: 8 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,19 @@
11
import type { MaybeRefOrGetter } from 'vue'
22
import { toValue } from 'vue'
3+
import type {
4+
DailyDataPoint,
5+
DailyRawPoint,
6+
EvolutionOptions,
7+
MonthlyDataPoint,
8+
WeeklyDataPoint,
9+
YearlyDataPoint,
10+
} from '~/types/chart'
311
import { fetchNpmDownloadsRange } from '~/utils/npm/api'
412

513
export type PackumentLikeForTime = {
614
time?: Record<string, string>
715
}
816

9-
export type DailyDataPoint = { value: number; day: string; timestamp: number }
10-
export type WeeklyDataPoint = {
11-
value: number
12-
weekKey: string
13-
weekStart: string
14-
weekEnd: string
15-
timestampStart: number
16-
timestampEnd: number
17-
}
18-
export type MonthlyDataPoint = { value: number; month: string; timestamp: number }
19-
export type YearlyDataPoint = { value: number; year: string; timestamp: number }
20-
21-
type EvolutionOptionsBase = {
22-
startDate?: string
23-
endDate?: string
24-
}
25-
26-
export type EvolutionOptionsDay = EvolutionOptionsBase & {
27-
granularity: 'day'
28-
}
29-
export type EvolutionOptionsWeek = EvolutionOptionsBase & {
30-
granularity: 'week'
31-
weeks?: number
32-
}
33-
export type EvolutionOptionsMonth = EvolutionOptionsBase & {
34-
granularity: 'month'
35-
months?: number
36-
}
37-
export type EvolutionOptionsYear = EvolutionOptionsBase & {
38-
granularity: 'year'
39-
}
40-
41-
export type EvolutionOptions =
42-
| EvolutionOptionsDay
43-
| EvolutionOptionsWeek
44-
| EvolutionOptionsMonth
45-
| EvolutionOptionsYear
46-
47-
type DailyRawPoint = { day: string; value: number }
48-
4917
function toIsoDateString(date: Date): string {
5018
return date.toISOString().slice(0, 10)
5119
}

app/composables/usePermalink.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Creates a computed property that uses route query parameters by default,
3+
* with an option to use local state instead.
4+
*/
5+
export function usePermalink<T extends string = string>(
6+
queryKey: string,
7+
defaultValue: T = '' as T,
8+
options: { permanent?: boolean } = {},
9+
): WritableComputedRef<T> {
10+
const { permanent = true } = options
11+
const localValue = shallowRef<T>(defaultValue)
12+
const routeValue = useRouteQuery<T>(queryKey, defaultValue)
13+
14+
const permalinkValue = computed({
15+
get: () => (permanent ? routeValue.value : localValue.value),
16+
set: (value: T) => {
17+
if (permanent) {
18+
routeValue.value = value
19+
} else {
20+
localValue.value = value
21+
}
22+
},
23+
})
24+
25+
return permalinkValue
26+
}

app/types/chart.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
export type ChartTimeGranularity = 'daily' | 'weekly' | 'monthly' | 'yearly'
2+
3+
export type DateRangeFields = {
4+
startDate?: string
5+
endDate?: string
6+
}
7+
8+
export type DailyDataPoint = { value: number; day: string; timestamp: number }
9+
export type WeeklyDataPoint = {
10+
value: number
11+
weekKey: string
12+
weekStart: string
13+
weekEnd: string
14+
timestampStart: number
15+
timestampEnd: number
16+
}
17+
export type MonthlyDataPoint = { value: number; month: string; timestamp: number }
18+
export type YearlyDataPoint = { value: number; year: string; timestamp: number }
19+
20+
export type EvolutionData =
21+
| DailyDataPoint[]
22+
| WeeklyDataPoint[]
23+
| MonthlyDataPoint[]
24+
| YearlyDataPoint[]
25+
26+
type EvolutionOptionsBase = {
27+
startDate?: string
28+
endDate?: string
29+
}
30+
31+
export type EvolutionOptionsDay = EvolutionOptionsBase & {
32+
granularity: 'day'
33+
}
34+
export type EvolutionOptionsWeek = EvolutionOptionsBase & {
35+
granularity: 'week'
36+
weeks?: number
37+
}
38+
export type EvolutionOptionsMonth = EvolutionOptionsBase & {
39+
granularity: 'month'
40+
months?: number
41+
}
42+
export type EvolutionOptionsYear = EvolutionOptionsBase & {
43+
granularity: 'year'
44+
}
45+
46+
export type EvolutionOptions =
47+
| EvolutionOptionsDay
48+
| EvolutionOptionsWeek
49+
| EvolutionOptionsMonth
50+
| EvolutionOptionsYear
51+
52+
export type DailyRawPoint = { day: string; value: number }

0 commit comments

Comments
 (0)