@zezosoft/zezo-ott-react-native-ui-kit 1.1.2 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/module/components/Auth/QrLogin/QrLogin.js +304 -138
- package/lib/module/components/Auth/QrLogin/QrLogin.js.map +1 -1
- package/lib/module/components/Auth/QrLogin/components/QrViewArea.js +193 -141
- package/lib/module/components/Auth/QrLogin/components/QrViewArea.js.map +1 -1
- package/lib/module/components/Content/Card/Category/Category.js +83 -11
- package/lib/module/components/Content/Card/Category/Category.js.map +1 -1
- package/lib/module/components/Content/Card/NowWatching/NowWatching.js +237 -108
- package/lib/module/components/Content/Card/NowWatching/NowWatching.js.map +1 -1
- package/lib/module/components/Content/Card/Sliders/Styles/One.js +185 -126
- package/lib/module/components/Content/Card/Sliders/Styles/One.js.map +1 -1
- package/lib/module/components/Content/Card/Sliders/Styles/Two.js +139 -92
- package/lib/module/components/Content/Card/Sliders/Styles/Two.js.map +1 -1
- package/lib/module/components/Content/Card/Styles/Five.js +131 -48
- package/lib/module/components/Content/Card/Styles/Five.js.map +1 -1
- package/lib/module/components/Content/Card/Styles/Four.js +126 -59
- package/lib/module/components/Content/Card/Styles/Four.js.map +1 -1
- package/lib/module/components/Content/Card/Styles/One.js +125 -50
- package/lib/module/components/Content/Card/Styles/One.js.map +1 -1
- package/lib/module/components/Content/Card/Styles/RotateInOut.js +138 -53
- package/lib/module/components/Content/Card/Styles/RotateInOut.js.map +1 -1
- package/lib/module/components/Content/Card/Styles/Six.js +207 -115
- package/lib/module/components/Content/Card/Styles/Six.js.map +1 -1
- package/lib/module/components/Content/Card/Styles/Three.js +134 -79
- package/lib/module/components/Content/Card/Styles/Three.js.map +1 -1
- package/lib/module/components/Content/Card/Styles/TopTen.js +186 -171
- package/lib/module/components/Content/Card/Styles/TopTen.js.map +1 -1
- package/lib/module/components/Content/Card/Styles/Two.js +144 -64
- package/lib/module/components/Content/Card/Styles/Two.js.map +1 -1
- package/lib/module/components/Content/Card/components/AdsPoster.js +162 -0
- package/lib/module/components/Content/Card/components/AdsPoster.js.map +1 -0
- package/lib/module/components/Content/Card/components/CardPoster.js +120 -136
- package/lib/module/components/Content/Card/components/CardPoster.js.map +1 -1
- package/lib/module/components/Content/Card/components/index.js +4 -0
- package/lib/module/components/Content/Card/components/index.js.map +1 -0
- package/lib/module/components/Content/Content.js +67 -27
- package/lib/module/components/Content/Content.js.map +1 -1
- package/lib/module/components/Content/Sections.js +32 -11
- package/lib/module/components/Content/Sections.js.map +1 -1
- package/lib/module/constants/dummySections.js +44 -4
- package/lib/module/constants/dummySections.js.map +1 -1
- package/lib/module/hooks/Images/index.js +5 -0
- package/lib/module/hooks/Images/index.js.map +1 -0
- package/lib/module/hooks/Images/useImageLoader.js +168 -0
- package/lib/module/hooks/Images/useImageLoader.js.map +1 -0
- package/lib/module/hooks/Images/useImageValidation.js +36 -0
- package/lib/module/hooks/Images/useImageValidation.js.map +1 -0
- package/lib/module/hooks/index.js +3 -0
- package/lib/module/hooks/index.js.map +1 -1
- package/lib/module/hooks/useAdTracking.js +270 -0
- package/lib/module/hooks/useAdTracking.js.map +1 -0
- package/lib/module/hooks/useCards.js +164 -0
- package/lib/module/hooks/useCards.js.map +1 -0
- package/lib/module/hooks/usePaginatedSection.js +11 -6
- package/lib/module/hooks/usePaginatedSection.js.map +1 -1
- package/lib/typescript/src/components/Auth/QrLogin/QrLogin.d.ts +2 -0
- package/lib/typescript/src/components/Auth/QrLogin/QrLogin.d.ts.map +1 -1
- package/lib/typescript/src/components/Auth/QrLogin/components/QrViewArea.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/Category/Category.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/NowWatching/NowWatching.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/Sliders/Styles/One.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/Sliders/Styles/Two.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/Styles/Five.d.ts +13 -1
- package/lib/typescript/src/components/Content/Card/Styles/Five.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/Styles/Four.d.ts +13 -1
- package/lib/typescript/src/components/Content/Card/Styles/Four.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/Styles/One.d.ts +15 -3
- package/lib/typescript/src/components/Content/Card/Styles/One.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/Styles/RotateInOut.d.ts +13 -1
- package/lib/typescript/src/components/Content/Card/Styles/RotateInOut.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/Styles/Six.d.ts +1 -0
- package/lib/typescript/src/components/Content/Card/Styles/Six.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/Styles/Three.d.ts +13 -5
- package/lib/typescript/src/components/Content/Card/Styles/Three.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/Styles/TopTen.d.ts +1 -0
- package/lib/typescript/src/components/Content/Card/Styles/TopTen.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/Styles/Two.d.ts +13 -1
- package/lib/typescript/src/components/Content/Card/Styles/Two.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/components/AdsPoster.d.ts +26 -0
- package/lib/typescript/src/components/Content/Card/components/AdsPoster.d.ts.map +1 -0
- package/lib/typescript/src/components/Content/Card/components/CardPoster.d.ts +3 -1
- package/lib/typescript/src/components/Content/Card/components/CardPoster.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Card/components/index.d.ts +2 -0
- package/lib/typescript/src/components/Content/Card/components/index.d.ts.map +1 -0
- package/lib/typescript/src/components/Content/Card/index.d.ts +76 -6
- package/lib/typescript/src/components/Content/Card/index.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Content.d.ts +4 -3
- package/lib/typescript/src/components/Content/Content.d.ts.map +1 -1
- package/lib/typescript/src/components/Content/Sections.d.ts +20 -6
- package/lib/typescript/src/components/Content/Sections.d.ts.map +1 -1
- package/lib/typescript/src/constants/dummySections.d.ts +5 -0
- package/lib/typescript/src/constants/dummySections.d.ts.map +1 -1
- package/lib/typescript/src/hooks/Images/index.d.ts +3 -0
- package/lib/typescript/src/hooks/Images/index.d.ts.map +1 -0
- package/lib/typescript/src/hooks/Images/useImageLoader.d.ts +36 -0
- package/lib/typescript/src/hooks/Images/useImageLoader.d.ts.map +1 -0
- package/lib/typescript/src/hooks/Images/useImageValidation.d.ts +17 -0
- package/lib/typescript/src/hooks/Images/useImageValidation.d.ts.map +1 -0
- package/lib/typescript/src/hooks/index.d.ts +3 -0
- package/lib/typescript/src/hooks/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/useAdTracking.d.ts +39 -0
- package/lib/typescript/src/hooks/useAdTracking.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useCards.d.ts +36 -0
- package/lib/typescript/src/hooks/useCards.d.ts.map +1 -0
- package/lib/typescript/src/hooks/usePaginatedSection.d.ts +12 -2
- package/lib/typescript/src/hooks/usePaginatedSection.d.ts.map +1 -1
- package/lib/typescript/src/types/sections/index.d.ts +7 -4
- package/lib/typescript/src/types/sections/index.d.ts.map +1 -1
- package/package.json +6 -3
- package/src/components/Auth/QrLogin/QrLogin.tsx +382 -122
- package/src/components/Auth/QrLogin/components/QrViewArea.tsx +291 -197
- package/src/components/Content/Card/Category/Category.tsx +95 -8
- package/src/components/Content/Card/NowWatching/NowWatching.tsx +281 -136
- package/src/components/Content/Card/Sliders/Styles/One.tsx +244 -148
- package/src/components/Content/Card/Sliders/Styles/Two.tsx +171 -102
- package/src/components/Content/Card/Styles/Five.tsx +161 -62
- package/src/components/Content/Card/Styles/Four.tsx +164 -85
- package/src/components/Content/Card/Styles/One.tsx +161 -71
- package/src/components/Content/Card/Styles/RotateInOut.tsx +157 -60
- package/src/components/Content/Card/Styles/Six.tsx +242 -142
- package/src/components/Content/Card/Styles/Three.tsx +166 -133
- package/src/components/Content/Card/Styles/TopTen.tsx +230 -191
- package/src/components/Content/Card/Styles/Two.tsx +182 -79
- package/src/components/Content/Card/components/AdsPoster.tsx +202 -0
- package/src/components/Content/Card/components/CardPoster.tsx +134 -154
- package/src/components/Content/Card/components/index.ts +1 -0
- package/src/components/Content/Content.tsx +83 -45
- package/src/components/Content/Sections.tsx +51 -10
- package/src/constants/dummySections.ts +48 -1
- package/src/hooks/Images/index.ts +2 -0
- package/src/hooks/Images/useImageLoader.ts +206 -0
- package/src/hooks/Images/useImageValidation.ts +36 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useAdTracking.ts +349 -0
- package/src/hooks/useCards.ts +228 -0
- package/src/hooks/usePaginatedSection.ts +26 -7
- package/src/types/sections/index.ts +7 -4
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import { useRef, useMemo, useCallback, useEffect } from 'react';
|
|
2
|
+
import { type View, Dimensions } from 'react-native';
|
|
3
|
+
import type { IServeAd } from '@zezosoft/zezo-ott-api-client';
|
|
4
|
+
|
|
5
|
+
const CHECK_INTERVAL = 500;
|
|
6
|
+
const INITIAL_CHECK_DELAY = 100;
|
|
7
|
+
|
|
8
|
+
export interface ViewportOffsets {
|
|
9
|
+
top: number;
|
|
10
|
+
bottom: number;
|
|
11
|
+
left: number;
|
|
12
|
+
right: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ScreenDimensions {
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface UseAdTrackingOptions {
|
|
21
|
+
ad: IServeAd;
|
|
22
|
+
onDisplayAds?: (ad: IServeAd) => void;
|
|
23
|
+
isLoading?: boolean;
|
|
24
|
+
screenDimensions?: ScreenDimensions;
|
|
25
|
+
viewportOffsets?: ViewportOffsets;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface UseAdTrackingReturn {
|
|
29
|
+
viewRef: React.RefObject<View | null>;
|
|
30
|
+
layoutRef: React.MutableRefObject<{
|
|
31
|
+
x: number;
|
|
32
|
+
y: number;
|
|
33
|
+
width: number;
|
|
34
|
+
height: number;
|
|
35
|
+
} | null>;
|
|
36
|
+
hasDisplayed: React.MutableRefObject<boolean>;
|
|
37
|
+
adUniqueId: string;
|
|
38
|
+
handleLayout: () => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Global set to track displayed ads - prevents duplicate tracking
|
|
42
|
+
const globalDisplayedAds = new Set<string>();
|
|
43
|
+
// Lock to prevent race conditions when checking and adding ads
|
|
44
|
+
const trackingLock = new Map<string, boolean>();
|
|
45
|
+
|
|
46
|
+
export const extractToken = (trackingUrl?: string): string => {
|
|
47
|
+
if (!trackingUrl) return '';
|
|
48
|
+
try {
|
|
49
|
+
const url = new URL(trackingUrl);
|
|
50
|
+
return url.searchParams.get('token') || '';
|
|
51
|
+
} catch {
|
|
52
|
+
const match = trackingUrl.match(/[?&]token=([^&]+)/);
|
|
53
|
+
return match?.[1] || '';
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Creates a unique ID for an ad to prevent duplicate tracking
|
|
59
|
+
* Priority: _id > token > mediaUrl > fallback
|
|
60
|
+
*/
|
|
61
|
+
export const createAdUniqueId = (ad: IServeAd): string => {
|
|
62
|
+
// First try to use _id if available (most reliable)
|
|
63
|
+
if ('_id' in ad && ad._id) {
|
|
64
|
+
return `ad-${String(ad._id)}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Then try token from tracking URL
|
|
68
|
+
const token = extractToken(ad.tracking?.impression);
|
|
69
|
+
if (token) {
|
|
70
|
+
return `ad-token-${token}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Then use mediaUrl
|
|
74
|
+
const mediaUrl = ad.mediaUrl || '';
|
|
75
|
+
if (mediaUrl) {
|
|
76
|
+
return `ad-media-${mediaUrl}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Last resort: use a combination of available fields
|
|
80
|
+
const clickUrl = ad.tracking?.click || '';
|
|
81
|
+
if (clickUrl) {
|
|
82
|
+
return `ad-click-${clickUrl}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Fallback (should rarely happen)
|
|
86
|
+
return `ad-fallback-${Date.now()}-${Math.random()}`;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const useAdTracking = (
|
|
90
|
+
options: UseAdTrackingOptions
|
|
91
|
+
): UseAdTrackingReturn => {
|
|
92
|
+
const {
|
|
93
|
+
ad,
|
|
94
|
+
onDisplayAds,
|
|
95
|
+
isLoading = false,
|
|
96
|
+
screenDimensions: propScreenDimensions,
|
|
97
|
+
viewportOffsets: propViewportOffsets,
|
|
98
|
+
} = options;
|
|
99
|
+
|
|
100
|
+
const viewRef = useRef<View>(null);
|
|
101
|
+
const layoutRef = useRef<{
|
|
102
|
+
x: number;
|
|
103
|
+
y: number;
|
|
104
|
+
width: number;
|
|
105
|
+
height: number;
|
|
106
|
+
} | null>(null);
|
|
107
|
+
const hasDisplayedRef = useRef<boolean>(false);
|
|
108
|
+
|
|
109
|
+
const onDisplayAdsRef = useRef(onDisplayAds);
|
|
110
|
+
const adRef = useRef(ad);
|
|
111
|
+
const isLoadingRef = useRef(isLoading);
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
onDisplayAdsRef.current = onDisplayAds;
|
|
115
|
+
adRef.current = ad;
|
|
116
|
+
isLoadingRef.current = isLoading;
|
|
117
|
+
}, [onDisplayAds, ad, isLoading]);
|
|
118
|
+
|
|
119
|
+
const adUniqueId = useMemo(() => createAdUniqueId(ad), [ad]);
|
|
120
|
+
|
|
121
|
+
const screenDimensions = useMemo(
|
|
122
|
+
() => propScreenDimensions || Dimensions.get('window'),
|
|
123
|
+
[propScreenDimensions]
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
const viewportOffsets = useMemo(
|
|
127
|
+
() => ({
|
|
128
|
+
top: propViewportOffsets?.top ?? 50,
|
|
129
|
+
bottom: propViewportOffsets?.bottom ?? 100,
|
|
130
|
+
left: propViewportOffsets?.left ?? 20,
|
|
131
|
+
right: propViewportOffsets?.right ?? 20,
|
|
132
|
+
}),
|
|
133
|
+
[
|
|
134
|
+
propViewportOffsets?.top,
|
|
135
|
+
propViewportOffsets?.bottom,
|
|
136
|
+
propViewportOffsets?.left,
|
|
137
|
+
propViewportOffsets?.right,
|
|
138
|
+
]
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
const viewportBounds = useMemo(() => {
|
|
142
|
+
const { width: screenWidth, height: screenHeight } = screenDimensions;
|
|
143
|
+
return {
|
|
144
|
+
left: viewportOffsets.left,
|
|
145
|
+
right: screenWidth - viewportOffsets.right,
|
|
146
|
+
top: viewportOffsets.top,
|
|
147
|
+
bottom: screenHeight - viewportOffsets.bottom,
|
|
148
|
+
};
|
|
149
|
+
}, [screenDimensions, viewportOffsets]);
|
|
150
|
+
|
|
151
|
+
const checkVisibility = useCallback((): boolean => {
|
|
152
|
+
const layout = layoutRef.current;
|
|
153
|
+
if (!layout) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const { x, y, width, height } = layout;
|
|
158
|
+
|
|
159
|
+
const isVisible =
|
|
160
|
+
x + width > viewportBounds.left &&
|
|
161
|
+
x < viewportBounds.right &&
|
|
162
|
+
y + height > viewportBounds.top &&
|
|
163
|
+
y < viewportBounds.bottom;
|
|
164
|
+
|
|
165
|
+
return isVisible;
|
|
166
|
+
}, [viewportBounds]);
|
|
167
|
+
|
|
168
|
+
const measureInWindow = useCallback((): void => {
|
|
169
|
+
const view = viewRef.current;
|
|
170
|
+
if (!view) return;
|
|
171
|
+
|
|
172
|
+
view.measureInWindow((x, y, width, height) => {
|
|
173
|
+
layoutRef.current = { x, y, width, height };
|
|
174
|
+
});
|
|
175
|
+
}, []);
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Atomically checks and tracks ad display to prevent duplicate tracking
|
|
179
|
+
* Uses a lock mechanism to ensure only one instance can track the same ad
|
|
180
|
+
*/
|
|
181
|
+
const trackAdDisplay = useCallback((): boolean => {
|
|
182
|
+
const currentOnDisplayAds = onDisplayAdsRef.current;
|
|
183
|
+
const currentIsLoading = isLoadingRef.current;
|
|
184
|
+
|
|
185
|
+
if (!currentOnDisplayAds || currentIsLoading) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Early return if already displayed by this instance
|
|
190
|
+
if (hasDisplayedRef.current) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Atomic check-and-set: if ad is already being tracked or displayed, return false
|
|
195
|
+
if (globalDisplayedAds.has(adUniqueId)) {
|
|
196
|
+
// Mark this instance as displayed to prevent further checks
|
|
197
|
+
hasDisplayedRef.current = true;
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Set lock to prevent other instances from tracking the same ad
|
|
202
|
+
if (trackingLock.get(adUniqueId) === true) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Acquire lock
|
|
207
|
+
trackingLock.set(adUniqueId, true);
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
// Double-check after acquiring lock (another instance might have added it)
|
|
211
|
+
if (globalDisplayedAds.has(adUniqueId)) {
|
|
212
|
+
hasDisplayedRef.current = true;
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Mark as displayed in both local and global state
|
|
217
|
+
hasDisplayedRef.current = true;
|
|
218
|
+
globalDisplayedAds.add(adUniqueId);
|
|
219
|
+
|
|
220
|
+
// Call the tracking callback
|
|
221
|
+
currentOnDisplayAds(adRef.current);
|
|
222
|
+
|
|
223
|
+
return true;
|
|
224
|
+
} finally {
|
|
225
|
+
// Release lock after a short delay to ensure callback completes
|
|
226
|
+
setTimeout(() => {
|
|
227
|
+
trackingLock.delete(adUniqueId);
|
|
228
|
+
}, 100);
|
|
229
|
+
}
|
|
230
|
+
}, [adUniqueId]);
|
|
231
|
+
|
|
232
|
+
const checkAndTrackVisibility = useCallback((): void => {
|
|
233
|
+
// Early exit if already displayed
|
|
234
|
+
if (hasDisplayedRef.current) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Early exit if ad is already tracked globally or locked
|
|
239
|
+
if (
|
|
240
|
+
globalDisplayedAds.has(adUniqueId) ||
|
|
241
|
+
trackingLock.get(adUniqueId) === true
|
|
242
|
+
) {
|
|
243
|
+
// Mark this instance as displayed to prevent further checks
|
|
244
|
+
hasDisplayedRef.current = true;
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Check visibility before tracking
|
|
249
|
+
if (!checkVisibility()) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Attempt to track (will handle duplicate prevention internally)
|
|
254
|
+
trackAdDisplay();
|
|
255
|
+
}, [adUniqueId, checkVisibility, trackAdDisplay]);
|
|
256
|
+
|
|
257
|
+
const handleLayout = useCallback(() => {
|
|
258
|
+
const view = viewRef.current;
|
|
259
|
+
if (!view) return;
|
|
260
|
+
|
|
261
|
+
view.measureInWindow((x, y, width, height) => {
|
|
262
|
+
layoutRef.current = { x, y, width, height };
|
|
263
|
+
requestAnimationFrame(() => {
|
|
264
|
+
checkAndTrackVisibility();
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
}, [checkAndTrackVisibility]);
|
|
268
|
+
|
|
269
|
+
useEffect(() => {
|
|
270
|
+
if (isLoadingRef.current) {
|
|
271
|
+
return undefined;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
let timeoutId: NodeJS.Timeout | null = null;
|
|
275
|
+
let rafId: number | null = null;
|
|
276
|
+
|
|
277
|
+
rafId = requestAnimationFrame(() => {
|
|
278
|
+
timeoutId = setTimeout(() => {
|
|
279
|
+
measureInWindow();
|
|
280
|
+
requestAnimationFrame(() => {
|
|
281
|
+
checkAndTrackVisibility();
|
|
282
|
+
});
|
|
283
|
+
}, INITIAL_CHECK_DELAY);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
return () => {
|
|
287
|
+
if (rafId !== null) {
|
|
288
|
+
cancelAnimationFrame(rafId);
|
|
289
|
+
}
|
|
290
|
+
if (timeoutId !== null) {
|
|
291
|
+
clearTimeout(timeoutId);
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}, [isLoading, checkAndTrackVisibility, measureInWindow]);
|
|
295
|
+
|
|
296
|
+
useEffect(() => {
|
|
297
|
+
if (
|
|
298
|
+
!onDisplayAdsRef.current ||
|
|
299
|
+
hasDisplayedRef.current ||
|
|
300
|
+
isLoadingRef.current
|
|
301
|
+
) {
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let rafId: number | null = null;
|
|
306
|
+
let lastCheckTime = 0;
|
|
307
|
+
|
|
308
|
+
const checkWithThrottle = () => {
|
|
309
|
+
const now = Date.now();
|
|
310
|
+
|
|
311
|
+
if (now - lastCheckTime < CHECK_INTERVAL) {
|
|
312
|
+
rafId = requestAnimationFrame(checkWithThrottle);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
lastCheckTime = now;
|
|
317
|
+
|
|
318
|
+
if (hasDisplayedRef.current || !viewRef.current) {
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
measureInWindow();
|
|
323
|
+
requestAnimationFrame(() => {
|
|
324
|
+
if (!hasDisplayedRef.current) {
|
|
325
|
+
checkAndTrackVisibility();
|
|
326
|
+
}
|
|
327
|
+
if (!hasDisplayedRef.current) {
|
|
328
|
+
rafId = requestAnimationFrame(checkWithThrottle);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
};
|
|
332
|
+
|
|
333
|
+
rafId = requestAnimationFrame(checkWithThrottle);
|
|
334
|
+
|
|
335
|
+
return () => {
|
|
336
|
+
if (rafId !== null) {
|
|
337
|
+
cancelAnimationFrame(rafId);
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
}, [checkAndTrackVisibility, measureInWindow]);
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
viewRef,
|
|
344
|
+
layoutRef,
|
|
345
|
+
hasDisplayed: hasDisplayedRef,
|
|
346
|
+
adUniqueId,
|
|
347
|
+
handleLayout,
|
|
348
|
+
};
|
|
349
|
+
};
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Global Card Hook (No duplicates + single data key)
|
|
3
|
+
*
|
|
4
|
+
* Provides a unified interface for managing card data with:
|
|
5
|
+
* - Automatic deduplication
|
|
6
|
+
* - Skeleton loading states
|
|
7
|
+
* - Pagination support
|
|
8
|
+
* - Optimized performance
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useMemo, useCallback } from 'react';
|
|
12
|
+
import type { IContentData } from '@zezosoft/zezo-ott-api-client';
|
|
13
|
+
import { usePaginatedSection } from './usePaginatedSection';
|
|
14
|
+
import type { IAdItem, MoreFetchData } from '../types';
|
|
15
|
+
import { dummyContentData } from '../constants/dummySections';
|
|
16
|
+
|
|
17
|
+
export type CardSource =
|
|
18
|
+
| { data: (IContentData | IAdItem)[] }
|
|
19
|
+
| (IContentData | IAdItem)[]
|
|
20
|
+
| null;
|
|
21
|
+
|
|
22
|
+
export type UseCardsOptions = {
|
|
23
|
+
sectionId: string;
|
|
24
|
+
data?: CardSource;
|
|
25
|
+
fetchMore?: MoreFetchData<IContentData>;
|
|
26
|
+
loading?: boolean;
|
|
27
|
+
initialSkeleton?: number;
|
|
28
|
+
pagingSkeleton?: number;
|
|
29
|
+
adsRender?: boolean; // If false, ads will be filtered out from listData
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type UseCardsReturn = {
|
|
33
|
+
listData: (IContentData | IAdItem)[]; // ✔ Mixed array of content and ads
|
|
34
|
+
pagination: {
|
|
35
|
+
hasNextPage: boolean;
|
|
36
|
+
nextPage: number | null;
|
|
37
|
+
} | null;
|
|
38
|
+
isPaging: boolean;
|
|
39
|
+
hasMore: boolean;
|
|
40
|
+
loadMore: (page: number | null) => Promise<void>;
|
|
41
|
+
isEmpty: boolean;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// -----------------------------------------------------------
|
|
45
|
+
// Constants
|
|
46
|
+
// -----------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
const DEFAULT_INITIAL_SKELETON = 5;
|
|
49
|
+
const DEFAULT_PAGING_SKELETON = 3;
|
|
50
|
+
|
|
51
|
+
// -----------------------------------------------------------
|
|
52
|
+
// Helpers
|
|
53
|
+
// -----------------------------------------------------------
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Normalizes different data source formats into a single array
|
|
57
|
+
*/
|
|
58
|
+
const normalize = (src: CardSource): (IContentData | IAdItem)[] => {
|
|
59
|
+
if (!src) return [];
|
|
60
|
+
if (Array.isArray(src)) return src;
|
|
61
|
+
return src.data ?? [];
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Removes duplicate items by _id using Map for O(n) performance
|
|
66
|
+
* Preserves order of first occurrence
|
|
67
|
+
* Handles both IContentData and IAdItem types
|
|
68
|
+
*/
|
|
69
|
+
const uniqueById = (
|
|
70
|
+
list: (IContentData | IAdItem)[]
|
|
71
|
+
): (IContentData | IAdItem)[] => {
|
|
72
|
+
if (list.length === 0) return [];
|
|
73
|
+
|
|
74
|
+
const seen = new Map<string, IContentData | IAdItem>();
|
|
75
|
+
const result: (IContentData | IAdItem)[] = [];
|
|
76
|
+
|
|
77
|
+
for (const item of list) {
|
|
78
|
+
// Handle ads - check for type property and use appropriate ID field
|
|
79
|
+
if ('type' in item && item.type === 'ads') {
|
|
80
|
+
const adItem = item as IAdItem;
|
|
81
|
+
// Ads might have _id (from IServeAd) or use mediaUrl as identifier
|
|
82
|
+
let id: string;
|
|
83
|
+
if ('_id' in adItem && adItem._id) {
|
|
84
|
+
id = String(adItem._id);
|
|
85
|
+
} else if ('mediaUrl' in adItem && adItem.mediaUrl) {
|
|
86
|
+
id = `ad-${adItem.mediaUrl}`;
|
|
87
|
+
} else {
|
|
88
|
+
id = `ad-${Math.random()}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!seen.has(id)) {
|
|
92
|
+
seen.set(id, adItem);
|
|
93
|
+
result.push(adItem);
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Handle content items
|
|
99
|
+
const contentItem = item as IContentData;
|
|
100
|
+
if (!contentItem?._id) continue; // Skip items without valid IDs
|
|
101
|
+
|
|
102
|
+
const id = String(contentItem._id);
|
|
103
|
+
if (!seen.has(id)) {
|
|
104
|
+
seen.set(id, contentItem);
|
|
105
|
+
result.push(contentItem);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return result;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Creates skeleton items for loading states
|
|
114
|
+
* Memoized by count to avoid unnecessary recreations
|
|
115
|
+
* Returns IContentData[] for skeleton loading (ads don't need skeletons)
|
|
116
|
+
*/
|
|
117
|
+
const createSkeletonItems = (count: number, prefix: string): IContentData[] => {
|
|
118
|
+
if (count <= 0) return [];
|
|
119
|
+
return Array.from({ length: count }, (_, i) =>
|
|
120
|
+
dummyContentData(`${prefix}-${i}`)
|
|
121
|
+
);
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// -----------------------------------------------------------
|
|
125
|
+
// Hook
|
|
126
|
+
// -----------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
export function useCards({
|
|
129
|
+
sectionId,
|
|
130
|
+
data,
|
|
131
|
+
fetchMore,
|
|
132
|
+
loading = false,
|
|
133
|
+
initialSkeleton = DEFAULT_INITIAL_SKELETON,
|
|
134
|
+
pagingSkeleton = DEFAULT_PAGING_SKELETON,
|
|
135
|
+
adsRender = false,
|
|
136
|
+
}: UseCardsOptions): UseCardsReturn {
|
|
137
|
+
// Normalize input data
|
|
138
|
+
const baseItems = useMemo(() => normalize(data ?? { data: [] }), [data]);
|
|
139
|
+
|
|
140
|
+
// Use pagination hook for data management
|
|
141
|
+
const {
|
|
142
|
+
data: list,
|
|
143
|
+
pagination,
|
|
144
|
+
loadMoreData,
|
|
145
|
+
isPaginating,
|
|
146
|
+
hasMore,
|
|
147
|
+
} = usePaginatedSection<IContentData | IAdItem>(
|
|
148
|
+
sectionId,
|
|
149
|
+
fetchMore,
|
|
150
|
+
baseItems,
|
|
151
|
+
loading
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
// Apply deduplication to the list (handles both content and ads)
|
|
155
|
+
const uniqueList = useMemo(
|
|
156
|
+
() => uniqueById(list as (IContentData | IAdItem)[]),
|
|
157
|
+
[list]
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Memoize skeleton items - only recreate when count changes
|
|
161
|
+
const initialSkeletonItems = useMemo(
|
|
162
|
+
() => createSkeletonItems(initialSkeleton, 'sk'),
|
|
163
|
+
[initialSkeleton]
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const pagingSkeletonItems = useMemo(
|
|
167
|
+
() => createSkeletonItems(pagingSkeleton, 'pagination-skeleton'),
|
|
168
|
+
[pagingSkeleton]
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// Filter ads if adsRender is false
|
|
172
|
+
const filteredList = useMemo(() => {
|
|
173
|
+
if (adsRender) return uniqueList;
|
|
174
|
+
return uniqueList.filter(
|
|
175
|
+
(item): item is IContentData => item.type !== 'ads'
|
|
176
|
+
);
|
|
177
|
+
}, [uniqueList, adsRender]);
|
|
178
|
+
|
|
179
|
+
// Final data array for UI rendering
|
|
180
|
+
// Handles loading states and pagination skeletons
|
|
181
|
+
// Returns mixed array of IContentData | IAdItem (or only IContentData if adsRender is false)
|
|
182
|
+
const items = useMemo(() => {
|
|
183
|
+
// Show initial skeleton during initial load
|
|
184
|
+
if (loading) return initialSkeletonItems as (IContentData | IAdItem)[];
|
|
185
|
+
|
|
186
|
+
// Show data with pagination skeleton when loading more
|
|
187
|
+
if (isPaginating && filteredList.length > 0) {
|
|
188
|
+
return [
|
|
189
|
+
...filteredList,
|
|
190
|
+
...(pagingSkeletonItems as (IContentData | IAdItem)[]),
|
|
191
|
+
];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Return filtered list (with or without ads based on adsRender)
|
|
195
|
+
return filteredList;
|
|
196
|
+
}, [
|
|
197
|
+
loading,
|
|
198
|
+
isPaginating,
|
|
199
|
+
filteredList,
|
|
200
|
+
initialSkeletonItems,
|
|
201
|
+
pagingSkeletonItems,
|
|
202
|
+
]);
|
|
203
|
+
|
|
204
|
+
// Determine if section is empty (only when not loading and no data)
|
|
205
|
+
const isEmpty = useMemo(
|
|
206
|
+
() => !loading && filteredList.length === 0,
|
|
207
|
+
[loading, filteredList.length]
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
// Memoize loadMore callback to prevent unnecessary re-renders
|
|
211
|
+
const handleLoadMore = useCallback(
|
|
212
|
+
async (page: number | null) => {
|
|
213
|
+
if (page && hasMore && !isPaginating) {
|
|
214
|
+
await loadMoreData(page);
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
[loadMoreData, hasMore, isPaginating]
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
listData: items,
|
|
222
|
+
pagination,
|
|
223
|
+
isPaging: isPaginating,
|
|
224
|
+
hasMore,
|
|
225
|
+
loadMore: handleLoadMore,
|
|
226
|
+
isEmpty,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
@@ -3,7 +3,21 @@
|
|
|
3
3
|
* @lastModified Wed 25 Jun 2025 at 12:00 PM
|
|
4
4
|
*/
|
|
5
5
|
import React, { useCallback, useState } from 'react';
|
|
6
|
-
import type {
|
|
6
|
+
import type { ISectionMeta } from '../types';
|
|
7
|
+
|
|
8
|
+
export type MoreFetchData<T> = ({
|
|
9
|
+
sectionId,
|
|
10
|
+
meta,
|
|
11
|
+
}: {
|
|
12
|
+
sectionId: string;
|
|
13
|
+
meta: {
|
|
14
|
+
hasNextPage: boolean;
|
|
15
|
+
nextPage: number;
|
|
16
|
+
};
|
|
17
|
+
}) => Promise<{
|
|
18
|
+
data: T[];
|
|
19
|
+
meta: ISectionMeta;
|
|
20
|
+
} | null>;
|
|
7
21
|
|
|
8
22
|
export function usePaginatedSection<T>(
|
|
9
23
|
sectionId: string,
|
|
@@ -19,7 +33,7 @@ export function usePaginatedSection<T>(
|
|
|
19
33
|
hasNextPage: true,
|
|
20
34
|
nextPage: 2,
|
|
21
35
|
});
|
|
22
|
-
const [
|
|
36
|
+
const [isPaginating, setIsPaginating] = useState(false);
|
|
23
37
|
|
|
24
38
|
// Sync initial data on mount or loading complete
|
|
25
39
|
React.useEffect(() => {
|
|
@@ -30,9 +44,9 @@ export function usePaginatedSection<T>(
|
|
|
30
44
|
|
|
31
45
|
const loadMoreData = useCallback(
|
|
32
46
|
async (page: number | null) => {
|
|
33
|
-
if (!page ||
|
|
47
|
+
if (!page || isPaginating || !pagination.hasNextPage || !fetchFn) return;
|
|
34
48
|
|
|
35
|
-
|
|
49
|
+
setIsPaginating(true);
|
|
36
50
|
try {
|
|
37
51
|
const res = await fetchFn({
|
|
38
52
|
sectionId,
|
|
@@ -44,6 +58,11 @@ export function usePaginatedSection<T>(
|
|
|
44
58
|
|
|
45
59
|
if (!res || !res.data || !res.meta?.pagination) {
|
|
46
60
|
console.warn(`⚠️ Invalid response for section: ${sectionId}`, res);
|
|
61
|
+
// Stop further pagination attempts for this section
|
|
62
|
+
setPagination({
|
|
63
|
+
hasNextPage: false,
|
|
64
|
+
nextPage: null,
|
|
65
|
+
});
|
|
47
66
|
return;
|
|
48
67
|
}
|
|
49
68
|
|
|
@@ -66,16 +85,16 @@ export function usePaginatedSection<T>(
|
|
|
66
85
|
} catch (err) {
|
|
67
86
|
console.error(`❌ Error loading section [${sectionId}]:`, err);
|
|
68
87
|
} finally {
|
|
69
|
-
|
|
88
|
+
setIsPaginating(false);
|
|
70
89
|
}
|
|
71
90
|
},
|
|
72
|
-
[
|
|
91
|
+
[isPaginating, pagination.hasNextPage, fetchFn, sectionId]
|
|
73
92
|
);
|
|
74
93
|
|
|
75
94
|
return {
|
|
76
95
|
data,
|
|
77
96
|
pagination,
|
|
78
|
-
|
|
97
|
+
isPaginating,
|
|
79
98
|
hasMore: pagination.hasNextPage,
|
|
80
99
|
loadMoreData,
|
|
81
100
|
};
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @author Naresh Dhamu
|
|
3
|
-
* @lastModified
|
|
3
|
+
* @lastModified Wed 24 Dec 2025 at 12:09 PM
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import type { IContentData } from '@zezosoft/zezo-ott-api-client';
|
|
7
|
-
|
|
6
|
+
import type { IContentData, IServeAd } from '@zezosoft/zezo-ott-api-client';
|
|
7
|
+
export type IAdItem = IServeAd & {
|
|
8
|
+
type: 'ads';
|
|
9
|
+
};
|
|
8
10
|
export type ISectionType =
|
|
9
11
|
| 'normal'
|
|
10
12
|
| 'card_style_2'
|
|
@@ -15,6 +17,7 @@ export type ISectionType =
|
|
|
15
17
|
| 'card_rotate_in_out'
|
|
16
18
|
| 'slider'
|
|
17
19
|
| 'slider_style_2'
|
|
20
|
+
| 'slider_style_3'
|
|
18
21
|
| 'card_series_featured_style_1'
|
|
19
22
|
| 'continue_watching'
|
|
20
23
|
| 'new_releases'
|
|
@@ -50,7 +53,7 @@ export interface IGetSectionData {
|
|
|
50
53
|
};
|
|
51
54
|
genre?: string;
|
|
52
55
|
content: {
|
|
53
|
-
data: IContentData[];
|
|
56
|
+
data: (IContentData | IAdItem)[];
|
|
54
57
|
meta: {
|
|
55
58
|
pagination: ISectionPagination;
|
|
56
59
|
};
|