@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.
Files changed (136) hide show
  1. package/lib/module/components/Auth/QrLogin/QrLogin.js +304 -138
  2. package/lib/module/components/Auth/QrLogin/QrLogin.js.map +1 -1
  3. package/lib/module/components/Auth/QrLogin/components/QrViewArea.js +193 -141
  4. package/lib/module/components/Auth/QrLogin/components/QrViewArea.js.map +1 -1
  5. package/lib/module/components/Content/Card/Category/Category.js +83 -11
  6. package/lib/module/components/Content/Card/Category/Category.js.map +1 -1
  7. package/lib/module/components/Content/Card/NowWatching/NowWatching.js +237 -108
  8. package/lib/module/components/Content/Card/NowWatching/NowWatching.js.map +1 -1
  9. package/lib/module/components/Content/Card/Sliders/Styles/One.js +185 -126
  10. package/lib/module/components/Content/Card/Sliders/Styles/One.js.map +1 -1
  11. package/lib/module/components/Content/Card/Sliders/Styles/Two.js +139 -92
  12. package/lib/module/components/Content/Card/Sliders/Styles/Two.js.map +1 -1
  13. package/lib/module/components/Content/Card/Styles/Five.js +131 -48
  14. package/lib/module/components/Content/Card/Styles/Five.js.map +1 -1
  15. package/lib/module/components/Content/Card/Styles/Four.js +126 -59
  16. package/lib/module/components/Content/Card/Styles/Four.js.map +1 -1
  17. package/lib/module/components/Content/Card/Styles/One.js +125 -50
  18. package/lib/module/components/Content/Card/Styles/One.js.map +1 -1
  19. package/lib/module/components/Content/Card/Styles/RotateInOut.js +138 -53
  20. package/lib/module/components/Content/Card/Styles/RotateInOut.js.map +1 -1
  21. package/lib/module/components/Content/Card/Styles/Six.js +207 -115
  22. package/lib/module/components/Content/Card/Styles/Six.js.map +1 -1
  23. package/lib/module/components/Content/Card/Styles/Three.js +134 -79
  24. package/lib/module/components/Content/Card/Styles/Three.js.map +1 -1
  25. package/lib/module/components/Content/Card/Styles/TopTen.js +186 -171
  26. package/lib/module/components/Content/Card/Styles/TopTen.js.map +1 -1
  27. package/lib/module/components/Content/Card/Styles/Two.js +144 -64
  28. package/lib/module/components/Content/Card/Styles/Two.js.map +1 -1
  29. package/lib/module/components/Content/Card/components/AdsPoster.js +162 -0
  30. package/lib/module/components/Content/Card/components/AdsPoster.js.map +1 -0
  31. package/lib/module/components/Content/Card/components/CardPoster.js +120 -136
  32. package/lib/module/components/Content/Card/components/CardPoster.js.map +1 -1
  33. package/lib/module/components/Content/Card/components/index.js +4 -0
  34. package/lib/module/components/Content/Card/components/index.js.map +1 -0
  35. package/lib/module/components/Content/Content.js +67 -27
  36. package/lib/module/components/Content/Content.js.map +1 -1
  37. package/lib/module/components/Content/Sections.js +32 -11
  38. package/lib/module/components/Content/Sections.js.map +1 -1
  39. package/lib/module/constants/dummySections.js +44 -4
  40. package/lib/module/constants/dummySections.js.map +1 -1
  41. package/lib/module/hooks/Images/index.js +5 -0
  42. package/lib/module/hooks/Images/index.js.map +1 -0
  43. package/lib/module/hooks/Images/useImageLoader.js +168 -0
  44. package/lib/module/hooks/Images/useImageLoader.js.map +1 -0
  45. package/lib/module/hooks/Images/useImageValidation.js +36 -0
  46. package/lib/module/hooks/Images/useImageValidation.js.map +1 -0
  47. package/lib/module/hooks/index.js +3 -0
  48. package/lib/module/hooks/index.js.map +1 -1
  49. package/lib/module/hooks/useAdTracking.js +270 -0
  50. package/lib/module/hooks/useAdTracking.js.map +1 -0
  51. package/lib/module/hooks/useCards.js +164 -0
  52. package/lib/module/hooks/useCards.js.map +1 -0
  53. package/lib/module/hooks/usePaginatedSection.js +11 -6
  54. package/lib/module/hooks/usePaginatedSection.js.map +1 -1
  55. package/lib/typescript/src/components/Auth/QrLogin/QrLogin.d.ts +2 -0
  56. package/lib/typescript/src/components/Auth/QrLogin/QrLogin.d.ts.map +1 -1
  57. package/lib/typescript/src/components/Auth/QrLogin/components/QrViewArea.d.ts.map +1 -1
  58. package/lib/typescript/src/components/Content/Card/Category/Category.d.ts.map +1 -1
  59. package/lib/typescript/src/components/Content/Card/NowWatching/NowWatching.d.ts.map +1 -1
  60. package/lib/typescript/src/components/Content/Card/Sliders/Styles/One.d.ts.map +1 -1
  61. package/lib/typescript/src/components/Content/Card/Sliders/Styles/Two.d.ts.map +1 -1
  62. package/lib/typescript/src/components/Content/Card/Styles/Five.d.ts +13 -1
  63. package/lib/typescript/src/components/Content/Card/Styles/Five.d.ts.map +1 -1
  64. package/lib/typescript/src/components/Content/Card/Styles/Four.d.ts +13 -1
  65. package/lib/typescript/src/components/Content/Card/Styles/Four.d.ts.map +1 -1
  66. package/lib/typescript/src/components/Content/Card/Styles/One.d.ts +15 -3
  67. package/lib/typescript/src/components/Content/Card/Styles/One.d.ts.map +1 -1
  68. package/lib/typescript/src/components/Content/Card/Styles/RotateInOut.d.ts +13 -1
  69. package/lib/typescript/src/components/Content/Card/Styles/RotateInOut.d.ts.map +1 -1
  70. package/lib/typescript/src/components/Content/Card/Styles/Six.d.ts +1 -0
  71. package/lib/typescript/src/components/Content/Card/Styles/Six.d.ts.map +1 -1
  72. package/lib/typescript/src/components/Content/Card/Styles/Three.d.ts +13 -5
  73. package/lib/typescript/src/components/Content/Card/Styles/Three.d.ts.map +1 -1
  74. package/lib/typescript/src/components/Content/Card/Styles/TopTen.d.ts +1 -0
  75. package/lib/typescript/src/components/Content/Card/Styles/TopTen.d.ts.map +1 -1
  76. package/lib/typescript/src/components/Content/Card/Styles/Two.d.ts +13 -1
  77. package/lib/typescript/src/components/Content/Card/Styles/Two.d.ts.map +1 -1
  78. package/lib/typescript/src/components/Content/Card/components/AdsPoster.d.ts +26 -0
  79. package/lib/typescript/src/components/Content/Card/components/AdsPoster.d.ts.map +1 -0
  80. package/lib/typescript/src/components/Content/Card/components/CardPoster.d.ts +3 -1
  81. package/lib/typescript/src/components/Content/Card/components/CardPoster.d.ts.map +1 -1
  82. package/lib/typescript/src/components/Content/Card/components/index.d.ts +2 -0
  83. package/lib/typescript/src/components/Content/Card/components/index.d.ts.map +1 -0
  84. package/lib/typescript/src/components/Content/Card/index.d.ts +76 -6
  85. package/lib/typescript/src/components/Content/Card/index.d.ts.map +1 -1
  86. package/lib/typescript/src/components/Content/Content.d.ts +4 -3
  87. package/lib/typescript/src/components/Content/Content.d.ts.map +1 -1
  88. package/lib/typescript/src/components/Content/Sections.d.ts +20 -6
  89. package/lib/typescript/src/components/Content/Sections.d.ts.map +1 -1
  90. package/lib/typescript/src/constants/dummySections.d.ts +5 -0
  91. package/lib/typescript/src/constants/dummySections.d.ts.map +1 -1
  92. package/lib/typescript/src/hooks/Images/index.d.ts +3 -0
  93. package/lib/typescript/src/hooks/Images/index.d.ts.map +1 -0
  94. package/lib/typescript/src/hooks/Images/useImageLoader.d.ts +36 -0
  95. package/lib/typescript/src/hooks/Images/useImageLoader.d.ts.map +1 -0
  96. package/lib/typescript/src/hooks/Images/useImageValidation.d.ts +17 -0
  97. package/lib/typescript/src/hooks/Images/useImageValidation.d.ts.map +1 -0
  98. package/lib/typescript/src/hooks/index.d.ts +3 -0
  99. package/lib/typescript/src/hooks/index.d.ts.map +1 -1
  100. package/lib/typescript/src/hooks/useAdTracking.d.ts +39 -0
  101. package/lib/typescript/src/hooks/useAdTracking.d.ts.map +1 -0
  102. package/lib/typescript/src/hooks/useCards.d.ts +36 -0
  103. package/lib/typescript/src/hooks/useCards.d.ts.map +1 -0
  104. package/lib/typescript/src/hooks/usePaginatedSection.d.ts +12 -2
  105. package/lib/typescript/src/hooks/usePaginatedSection.d.ts.map +1 -1
  106. package/lib/typescript/src/types/sections/index.d.ts +7 -4
  107. package/lib/typescript/src/types/sections/index.d.ts.map +1 -1
  108. package/package.json +6 -3
  109. package/src/components/Auth/QrLogin/QrLogin.tsx +382 -122
  110. package/src/components/Auth/QrLogin/components/QrViewArea.tsx +291 -197
  111. package/src/components/Content/Card/Category/Category.tsx +95 -8
  112. package/src/components/Content/Card/NowWatching/NowWatching.tsx +281 -136
  113. package/src/components/Content/Card/Sliders/Styles/One.tsx +244 -148
  114. package/src/components/Content/Card/Sliders/Styles/Two.tsx +171 -102
  115. package/src/components/Content/Card/Styles/Five.tsx +161 -62
  116. package/src/components/Content/Card/Styles/Four.tsx +164 -85
  117. package/src/components/Content/Card/Styles/One.tsx +161 -71
  118. package/src/components/Content/Card/Styles/RotateInOut.tsx +157 -60
  119. package/src/components/Content/Card/Styles/Six.tsx +242 -142
  120. package/src/components/Content/Card/Styles/Three.tsx +166 -133
  121. package/src/components/Content/Card/Styles/TopTen.tsx +230 -191
  122. package/src/components/Content/Card/Styles/Two.tsx +182 -79
  123. package/src/components/Content/Card/components/AdsPoster.tsx +202 -0
  124. package/src/components/Content/Card/components/CardPoster.tsx +134 -154
  125. package/src/components/Content/Card/components/index.ts +1 -0
  126. package/src/components/Content/Content.tsx +83 -45
  127. package/src/components/Content/Sections.tsx +51 -10
  128. package/src/constants/dummySections.ts +48 -1
  129. package/src/hooks/Images/index.ts +2 -0
  130. package/src/hooks/Images/useImageLoader.ts +206 -0
  131. package/src/hooks/Images/useImageValidation.ts +36 -0
  132. package/src/hooks/index.ts +3 -0
  133. package/src/hooks/useAdTracking.ts +349 -0
  134. package/src/hooks/useCards.ts +228 -0
  135. package/src/hooks/usePaginatedSection.ts +26 -7
  136. 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 { MoreFetchData } from '../types';
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 [loading, setLoading] = useState(false);
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 || loading || !pagination.hasNextPage || !fetchFn) return;
47
+ if (!page || isPaginating || !pagination.hasNextPage || !fetchFn) return;
34
48
 
35
- setLoading(true);
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
- setLoading(false);
88
+ setIsPaginating(false);
70
89
  }
71
90
  },
72
- [loading, pagination.hasNextPage, fetchFn, sectionId]
91
+ [isPaginating, pagination.hasNextPage, fetchFn, sectionId]
73
92
  );
74
93
 
75
94
  return {
76
95
  data,
77
96
  pagination,
78
- loading,
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 Fri 04 Jul 2025 at 10:58 AM
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
  };