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.
This commit is contained in:
Dorian
2026-02-14 16:11:30 +00:00
parent e48e5f5b4d
commit f40b4a1268
5 changed files with 63 additions and 32 deletions

View File

@@ -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,

View File

@@ -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);
}
}

View File

@@ -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 {

View File

@@ -95,25 +95,27 @@ const { getStats } = useContentDiscovery()
/** Backend zap stats (BTCPay zaps) so film cards show total + who zapped. */
const backendZapStats = ref<Record<string, { zapCount: number; zapAmountSats: number; recentZapperPubkeys: string[] }>>({})
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)
})
</script>

View File

@@ -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',