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 { AwardIssuersModule } from './award-issuers/award-issuers.module';
import { FestivalsModule } from './festivals/festivals.module'; import { FestivalsModule } from './festivals/festivals.module';
import { GenresModule } from './genres/genres.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 { APP_FILTER, APP_INTERCEPTOR } from '@nestjs/core';
import { RentsModule } from './rents/rents.module'; import { RentsModule } from './rents/rents.module';
import { EventsModule } from './events/events.module'; import { EventsModule } from './events/events.module';
@@ -102,7 +103,7 @@ import { ZapsModule } from './zaps/zaps.module';
AppService, AppService,
{ {
provide: APP_INTERCEPTOR, provide: APP_INTERCEPTOR,
useClass: CacheInterceptor, useClass: HttpCacheInterceptor,
}, },
{ {
provide: APP_FILTER, 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) { function handleZapped(_amount: number) {
// In-app zaps go through BTCPay; refetch backend stats so modal updates. // 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 { function getZapperName(pubkey: string): string {

View File

@@ -95,10 +95,8 @@ const { getStats } = useContentDiscovery()
/** Backend zap stats (BTCPay zaps) so film cards show total + who zapped. */ /** Backend zap stats (BTCPay zaps) so film cards show total + who zapped. */
const backendZapStats = ref<Record<string, { zapCount: number; zapAmountSats: number; recentZapperPubkeys: string[] }>>({}) const backendZapStats = ref<Record<string, { zapCount: number; zapAmountSats: number; recentZapperPubkeys: string[] }>>({})
watch( function fetchZapStats() {
() => props.contents, const ids = props.contents?.map((c) => c.id).filter(Boolean) ?? []
(contents) => {
const ids = contents?.map((c) => c.id).filter(Boolean) ?? []
if (ids.length === 0) { if (ids.length === 0) {
backendZapStats.value = {} backendZapStats.value = {}
return return
@@ -111,9 +109,13 @@ watch(
.catch(() => { .catch(() => {
backendZapStats.value = {} backendZapStats.value = {}
}) })
}, }
{ immediate: true },
) watch(() => props.contents, fetchZapStats, { immediate: true })
function onZapCompleted() {
fetchZapStats()
}
function getReactionCount(contentId: string): number { function getReactionCount(contentId: string): number {
return getStats(contentId).plusCount ?? 0 return getStats(contentId).plusCount ?? 0
@@ -156,12 +158,14 @@ onMounted(() => {
sliderRef.value.addEventListener('scroll', handleScroll) sliderRef.value.addEventListener('scroll', handleScroll)
handleScroll() handleScroll()
} }
window.addEventListener('indeehub:zap-completed', onZapCompleted)
}) })
onUnmounted(() => { onUnmounted(() => {
if (sliderRef.value) { if (sliderRef.value) {
sliderRef.value.removeEventListener('scroll', handleScroll) sliderRef.value.removeEventListener('scroll', handleScroll)
} }
window.removeEventListener('indeehub:zap-completed', onZapCompleted)
}) })
</script> </script>

View File

@@ -243,23 +243,28 @@ const rentalPct = computed(() =>
totalRevenue.value > 0 ? Math.round(((watchAnalytics.value?.rentalRevenueSats || 0) / totalRevenue.value) * 100) : 50 totalRevenue.value > 0 ? Math.round(((watchAnalytics.value?.rentalRevenueSats || 0) / totalRevenue.value) * 100) : 50
) )
function formatNumber(n: number): string { function formatNumber(n: number | undefined | null): string {
return n.toLocaleString() const val = n ?? 0
return typeof val === 'number' && !Number.isNaN(val) ? val.toLocaleString() : '0'
} }
function formatSats(n: number): string { function formatSats(n: number | undefined | null): string {
return n.toLocaleString() const val = n ?? 0
return typeof val === 'number' && !Number.isNaN(val) ? val.toLocaleString() : '0'
} }
function formatDuration(seconds: number): string { function formatDuration(seconds: number | undefined | null): string {
if (!seconds) return '0m' const s = seconds ?? 0
const m = Math.floor(seconds / 60) if (!s || typeof s !== 'number' || Number.isNaN(s)) return '0m'
const s = Math.round(seconds % 60) const m = Math.floor(s / 60)
return m > 0 ? `${m}m ${s}s` : `${s}s` const sec = Math.round(s % 60)
return m > 0 ? `${m}m ${sec}s` : `${sec}s`
} }
function formatDate(dateString: string): string { function formatDate(dateString: string | undefined | null): string {
return new Date(dateString).toLocaleDateString('en-US', { if (dateString == null || dateString === '') return '—'
const date = new Date(dateString)
return Number.isNaN(date.getTime()) ? '—' : date.toLocaleDateString('en-US', {
year: 'numeric', year: 'numeric',
month: 'short', month: 'short',
day: 'numeric', day: 'numeric',