diff --git a/app/components/module/ModuleItem.vue b/app/components/module/ModuleItem.vue index d2535b59e..618fffa86 100644 --- a/app/components/module/ModuleItem.vue +++ b/app/components/module/ModuleItem.vue @@ -12,26 +12,27 @@ const props = withDefaults( showBadge?: boolean isAdded: boolean showAddButton?: boolean + sortKey?: string }>(), { showBadge: true, - showAddButton: true + showAddButton: true, + sortKey: 'downloads' } ) const { copy } = useClipboard() -const { selectedSort } = useModules() const { track } = useAnalytics() const publishedAgo = useTimeAgo(() => props.module.stats.publishedAt) const createdAgo = useTimeAgo(() => props.module.stats.createdAt) const relativeDate = computed(() => - selectedSort.value.key === 'publishedAt' ? publishedAgo.value : createdAgo.value + props.sortKey === 'publishedAt' ? publishedAgo.value : createdAgo.value ) const staticModuleDate = computed(() => - selectedSort.value.key === 'publishedAt' + props.sortKey === 'publishedAt' ? formatDateByLocale('en', props.module.stats.publishedAt) : formatDateByLocale('en', props.module.stats.createdAt) ) @@ -181,7 +182,7 @@ const items = computed(() => [ - + [ - + { key: 'modules-health', server: false, lazy: true, - default: () => ({}) + dedupe: 'defer', + default: () => ({}), + getCachedData(key, nuxtApp) { + return nuxtApp.payload.data[key] ?? nuxtApp.static.data[key] + } }) return { health } diff --git a/app/composables/useModules.ts b/app/composables/useModules.ts index c9efd16d9..e462ee81b 100644 --- a/app/composables/useModules.ts +++ b/app/composables/useModules.ts @@ -55,13 +55,7 @@ export const useModules = () => { }) const stats = computed(() => data.value.stats) - - // Merge via computed, not mutation: useFetch `data` is a shallowRef (Nuxt 4 - // default), so writing module.health in place would not trigger a re-render. - const { health } = useModuleHealth() - const modules = computed(() => - (data.value.modules || []).map(m => ({ ...m, health: health.value[m.name] ?? m.health ?? null })) - ) + const modules = computed(() => data.value.modules || []) const module = useState('module', () => ({} as Module)) diff --git a/app/pages/modules/index.vue b/app/pages/modules/index.vue index c31fcaabf..3a5b6a415 100644 --- a/app/pages/modules/index.vue +++ b/app/pages/modules/index.vue @@ -13,8 +13,18 @@ const el = useTemplateRef('el') const { replaceRoute } = useFilters('modules') const { fetchList, filteredModules, q, categories, modules, stats, selectedSort, selectedOrder, selectedCategory, sorts } = useModules() +const { health } = useModuleHealth() const { track } = useAnalytics() +// Merge via computed, not mutation: useFetch `data` is a shallowRef (Nuxt 4 +// default), so writing module.health in place would not trigger a re-render. +const filteredModulesWithHealth = computed(() => + filteredModules.value.map(m => ({ + ...m, + health: health.value[m.name] ?? m.health ?? null + })) +) + const cacheControl = useResponseHeader('Cache-Control') const cdnCacheControl = useResponseHeader('CDN-Cache-Control') @@ -56,35 +66,39 @@ defineShortcuts({ }) const ITEMS_PER_PAGE = 9 +const INITIAL_VISIBLE = ITEMS_PER_PAGE * 2 const SCROLL_THRESHOLD = 450 -const displayedModules = ref([]) +const visibleCount = ref(INITIAL_VISIBLE) const isLoading = ref(false) +let loadMoreTimer: ReturnType | null = null + +const displayedModules = computed(() => + filteredModulesWithHealth.value.slice(0, visibleCount.value) +) const { y: scrollY } = useWindowScroll() const { copy } = useClipboard() +const clearLoadMoreTimer = () => { + if (loadMoreTimer === null) return + clearTimeout(loadMoreTimer) + loadMoreTimer = null +} + const loadMoreModules = () => { if (isLoading.value) return - - const currentLength = displayedModules.value.length - if (currentLength >= filteredModules.value.length) return + if (visibleCount.value >= filteredModules.value.length) return isLoading.value = true + clearLoadMoreTimer() - setTimeout(() => { - const nextItems = filteredModules.value.slice( - currentLength, - currentLength + ITEMS_PER_PAGE - ) - displayedModules.value.push(...nextItems) + loadMoreTimer = setTimeout(() => { + loadMoreTimer = null + visibleCount.value += ITEMS_PER_PAGE isLoading.value = false }, 300) } -const initializeModules = () => { - displayedModules.value = filteredModules.value.slice(0, ITEMS_PER_PAGE * 2) -} - const debouncedLoadMore = useDebounceFn(loadMoreModules, 50) watch(scrollY, (y) => { @@ -93,11 +107,19 @@ watch(scrollY, (y) => { } }) -watch(filteredModules, () => { - isLoading.value = false - displayedModules.value = [] - initializeModules() -}) +watch( + () => [ + q.value, + selectedCategory.value?.key, + selectedSort.value?.key, + selectedOrder.value?.key + ], + () => { + clearLoadMoreTimer() + isLoading.value = false + visibleCount.value = INITIAL_VISIBLE + } +) const copyAllInstallCommands = () => { const moduleNames = modulesToAdd.value.map(module => module.name).join(' ') @@ -137,8 +159,6 @@ Steps: const clearAllModules = () => { modulesToAdd.value = [] } - -initializeModules()