From f40b4a12685acba493e441eb8b6f79ba6cb53989 Mon Sep 17 00:00:00 2001 From: Dorian Date: Sat, 14 Feb 2026 16:11:30 +0000 Subject: [PATCH] refactor: update caching and zap handling in components - Replaced CacheInterceptor with HttpCacheInterceptor in app.module.ts for improved caching strategy. - Enhanced ContentDetailModal to dispatch a custom event upon zap completion, improving inter-component communication. - Refactored ContentRow to streamline zap stats fetching and added a listener for zap completion events, ensuring real-time updates. - Updated Analytics.vue to improve number formatting functions, handling undefined and null values more robustly. These changes enhance the application's performance and user experience by optimizing caching and ensuring accurate, real-time data updates across components. --- backend/src/app.module.ts | 5 ++- .../interceptors/http-cache.interceptor.ts | 18 +++++++++ src/components/ContentDetailModal.vue | 5 ++- src/components/ContentRow.vue | 40 ++++++++++--------- src/views/backstage/Analytics.vue | 27 ++++++++----- 5 files changed, 63 insertions(+), 32 deletions(-) create mode 100644 backend/src/common/interceptors/http-cache.interceptor.ts diff --git a/backend/src/app.module.ts b/backend/src/app.module.ts index 9c50eee..9cf93fd 100644 --- a/backend/src/app.module.ts +++ b/backend/src/app.module.ts @@ -17,7 +17,8 @@ import { SubscriptionsModule } from './subscriptions/subscriptions.module'; import { AwardIssuersModule } from './award-issuers/award-issuers.module'; import { FestivalsModule } from './festivals/festivals.module'; import { GenresModule } from './genres/genres.module'; -import { CacheInterceptor, CacheModule } from '@nestjs/cache-manager'; +import { CacheModule } from '@nestjs/cache-manager'; +import { HttpCacheInterceptor } from './common/interceptors/http-cache.interceptor'; import { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core'; import { RentsModule } from './rents/rents.module'; import { EventsModule } from './events/events.module'; @@ -102,7 +103,7 @@ import { ZapsModule } from './zaps/zaps.module'; AppService, { provide: APP_INTERCEPTOR, - useClass: CacheInterceptor, + useClass: HttpCacheInterceptor, }, { provide: APP_FILTER, diff --git a/backend/src/common/interceptors/http-cache.interceptor.ts b/backend/src/common/interceptors/http-cache.interceptor.ts new file mode 100644 index 0000000..8e4ed53 --- /dev/null +++ b/backend/src/common/interceptors/http-cache.interceptor.ts @@ -0,0 +1,18 @@ +import { CacheInterceptor } from '@nestjs/cache-manager'; +import { ExecutionContext, Injectable } from '@nestjs/common'; + +/** + * Global cache interceptor that skips caching for paths that must stay fresh + * (e.g. /zaps/stats so zap counts update after a user zaps). + */ +@Injectable() +export class HttpCacheInterceptor extends CacheInterceptor { + trackBy(context: ExecutionContext): string | undefined { + const request = context.switchToHttp().getRequest(); + const path = request.url?.split('?')[0] ?? ''; + if (path.startsWith('/zaps') || path.startsWith('/api/zaps')) { + return undefined; + } + return super.trackBy(context); + } +} diff --git a/src/components/ContentDetailModal.vue b/src/components/ContentDetailModal.vue index a9382d9..93fc98e 100644 --- a/src/components/ContentDetailModal.vue +++ b/src/components/ContentDetailModal.vue @@ -543,7 +543,10 @@ function handleZap() { function handleZapped(_amount: number) { // In-app zaps go through BTCPay; refetch backend stats so modal updates. - if (props.content?.id) fetchBackendZapStats(props.content.id) + if (props.content?.id) { + fetchBackendZapStats(props.content.id) + window.dispatchEvent(new CustomEvent('indeehub:zap-completed', { detail: { contentId: props.content.id } })) + } } function getZapperName(pubkey: string): string { diff --git a/src/components/ContentRow.vue b/src/components/ContentRow.vue index 0b18a5c..4e9bf04 100644 --- a/src/components/ContentRow.vue +++ b/src/components/ContentRow.vue @@ -95,25 +95,27 @@ const { getStats } = useContentDiscovery() /** Backend zap stats (BTCPay zaps) so film cards show total + who zapped. */ const backendZapStats = ref>({}) -watch( - () => props.contents, - (contents) => { - const ids = contents?.map((c) => c.id).filter(Boolean) ?? [] - if (ids.length === 0) { +function fetchZapStats() { + const ids = props.contents?.map((c) => c.id).filter(Boolean) ?? [] + if (ids.length === 0) { + backendZapStats.value = {} + return + } + indeehubApiService + .getZapStats(ids) + .then((data) => { + backendZapStats.value = data + }) + .catch(() => { backendZapStats.value = {} - return - } - indeehubApiService - .getZapStats(ids) - .then((data) => { - backendZapStats.value = data - }) - .catch(() => { - backendZapStats.value = {} - }) - }, - { immediate: true }, -) + }) +} + +watch(() => props.contents, fetchZapStats, { immediate: true }) + +function onZapCompleted() { + fetchZapStats() +} function getReactionCount(contentId: string): number { return getStats(contentId).plusCount ?? 0 @@ -156,12 +158,14 @@ onMounted(() => { sliderRef.value.addEventListener('scroll', handleScroll) handleScroll() } + window.addEventListener('indeehub:zap-completed', onZapCompleted) }) onUnmounted(() => { if (sliderRef.value) { sliderRef.value.removeEventListener('scroll', handleScroll) } + window.removeEventListener('indeehub:zap-completed', onZapCompleted) }) diff --git a/src/views/backstage/Analytics.vue b/src/views/backstage/Analytics.vue index 4c1f9f8..2cacd8c 100644 --- a/src/views/backstage/Analytics.vue +++ b/src/views/backstage/Analytics.vue @@ -243,23 +243,28 @@ const rentalPct = computed(() => totalRevenue.value > 0 ? Math.round(((watchAnalytics.value?.rentalRevenueSats || 0) / totalRevenue.value) * 100) : 50 ) -function formatNumber(n: number): string { - return n.toLocaleString() +function formatNumber(n: number | undefined | null): string { + const val = n ?? 0 + return typeof val === 'number' && !Number.isNaN(val) ? val.toLocaleString() : '0' } -function formatSats(n: number): string { - return n.toLocaleString() +function formatSats(n: number | undefined | null): string { + const val = n ?? 0 + return typeof val === 'number' && !Number.isNaN(val) ? val.toLocaleString() : '0' } -function formatDuration(seconds: number): string { - if (!seconds) return '0m' - const m = Math.floor(seconds / 60) - const s = Math.round(seconds % 60) - return m > 0 ? `${m}m ${s}s` : `${s}s` +function formatDuration(seconds: number | undefined | null): string { + const s = seconds ?? 0 + if (!s || typeof s !== 'number' || Number.isNaN(s)) return '0m' + const m = Math.floor(s / 60) + const sec = Math.round(s % 60) + return m > 0 ? `${m}m ${sec}s` : `${sec}s` } -function formatDate(dateString: string): string { - return new Date(dateString).toLocaleDateString('en-US', { +function formatDate(dateString: string | undefined | null): string { + if (dateString == null || dateString === '') return '—' + const date = new Date(dateString) + return Number.isNaN(date.getTime()) ? '—' : date.toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric',