@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
@@ -14,30 +14,46 @@ import {
14
14
  type ViewStyle,
15
15
  type AccessibilityProps,
16
16
  type ListRenderItem,
17
- type ListRenderItemInfo,
18
17
  } from 'react-native';
18
+
19
19
  import SkeletonPlaceholder from 'react-native-skeleton-placeholder';
20
20
  import FastImage from 'react-native-fast-image';
21
21
  import { moderateScale, verticalScale } from 'react-native-size-matters';
22
+ import { RFValue } from 'react-native-responsive-fontsize';
22
23
 
23
- import { usePaginatedSection } from '../../../../hooks/usePaginatedSection';
24
24
  import type {
25
+ IAdItem,
25
26
  IGetSectionData,
26
27
  ISectionContent,
27
28
  MoreFetchData,
28
29
  } from '../../../../types';
30
+
29
31
  import type { ITheme, ThemeOverride } from '../../../../theme/themes';
30
32
  import { useInternalTheme } from '../../../../theme/hook/useInternalTheme';
33
+
31
34
  import NavigateToMore from '../components/NavigateToMore';
32
35
  import CardPoster from '../components/CardPoster';
33
- import { RFValue } from 'react-native-responsive-fontsize';
34
- import type { IContentData } from '@zezosoft/zezo-ott-api-client';
36
+ import { AdsPoster } from '../components';
37
+ import type { IContentData, IServeAd } from '@zezosoft/zezo-ott-api-client';
38
+ import { useCards } from '../../../../hooks';
35
39
 
36
- // Defaults
40
+ // Constants
37
41
  const DEFAULT_WIDTH = moderateScale(172);
38
42
  const DEFAULT_BORDER_RADIUS = moderateScale(5);
39
43
  const DEFAULT_SKELETON_COUNT = 3;
44
+ const END_REACHED_THRESHOLD = 0.5;
45
+ const ITEM_ASPECT_RATIO = 0.67;
46
+ const SKELETON_TITLE_WIDTH = moderateScale(100);
47
+ const SKELETON_TITLE_HEIGHT = verticalScale(14);
48
+ const SKELETON_TITLE_BORDER_RADIUS = moderateScale(4);
49
+ const SKELETON_ITEM_MARGIN = moderateScale(12);
50
+ const ITEM_MARGIN_LEFT = moderateScale(10);
51
+ const ITEM_MARGIN_RIGHT = moderateScale(12);
52
+ const ITEM_ACTIVE_OPACITY = 0.8;
53
+ const DEFAULT_BACKGROUND_COLOR = '#2c2c2c';
54
+ const DEFAULT_IMAGE_BACKGROUND_COLOR = '#222';
40
55
 
56
+ // Types
41
57
  type MovieCardTwoProps = {
42
58
  title: string;
43
59
  section_id: string;
@@ -57,17 +73,27 @@ type MovieCardTwoProps = {
57
73
  itemWidth?: number;
58
74
  borderRadius?: number;
59
75
  skeletonCount?: number;
76
+ paginationSkeletonCount?: number;
60
77
  containerStyle?: StyleProp<ViewStyle>;
61
78
  titleStyle?: StyleProp<TextStyle>;
62
79
  itemStyle?: StyleProp<ViewStyle>;
63
80
  isLoading?: boolean;
64
81
  theme?: ThemeOverride;
82
+ onDisplayAds?: (ad: IServeAd) => void;
83
+ screenDimensions?: { width: number; height: number };
84
+ viewportOffsets?: {
85
+ top: number;
86
+ bottom: number;
87
+ left: number;
88
+ right: number;
89
+ };
65
90
  } & AccessibilityProps;
66
91
 
92
+ // Main Component
67
93
  const MovieCardTwo: React.FC<MovieCardTwoProps> = ({
68
94
  title,
69
95
  section_id,
70
- data: initialData,
96
+ data: externalData,
71
97
  moreFetchData,
72
98
  onPressMore,
73
99
  onPressItem,
@@ -76,6 +102,7 @@ const MovieCardTwo: React.FC<MovieCardTwoProps> = ({
76
102
  itemWidth = DEFAULT_WIDTH,
77
103
  borderRadius = DEFAULT_BORDER_RADIUS,
78
104
  skeletonCount = DEFAULT_SKELETON_COUNT,
105
+ paginationSkeletonCount,
79
106
  containerStyle,
80
107
  titleStyle,
81
108
  itemStyle,
@@ -83,34 +110,46 @@ const MovieCardTwo: React.FC<MovieCardTwoProps> = ({
83
110
  theme,
84
111
  accessibilityLabel,
85
112
  accessibilityHint,
113
+ onDisplayAds,
114
+ screenDimensions,
115
+ viewportOffsets,
86
116
  }) => {
87
- const flatListRef = useRef<FlatList<IContentData>>(null);
88
-
117
+ // Refs
118
+ const flatListRef = useRef<FlatList<IContentData | IAdItem>>(null);
89
119
  const onEndReachedCalledDuringMomentum = useRef(false);
120
+
121
+ // Theme
90
122
  const { theme: appliedTheme } = useInternalTheme(theme);
91
123
 
124
+ // Styles
92
125
  const styles = useMemo(
93
126
  () => getStyles(appliedTheme, itemWidth, borderRadius),
94
127
  [appliedTheme, itemWidth, borderRadius]
95
128
  );
96
129
 
130
+ // Data Management
97
131
  const {
98
- data,
99
- pagination: paginated,
100
- loading: isPaginating,
101
- loadMoreData,
102
- } = usePaginatedSection(
103
- section_id,
104
- moreFetchData,
105
- initialData?.data || [],
106
- isLoading
107
- );
132
+ listData,
133
+ pagination,
134
+ loadMore,
135
+ isEmpty,
136
+ isPaging: isPaginating,
137
+ } = useCards({
138
+ sectionId: section_id,
139
+ data: externalData,
140
+ fetchMore: moreFetchData,
141
+ loading: isLoading,
142
+ initialSkeleton: skeletonCount,
143
+ pagingSkeleton: paginationSkeletonCount,
144
+ adsRender: false,
145
+ });
146
+
147
+ // Event Handlers
108
148
  const handleItemPress = useCallback(
109
- (item: IContentData) => {
110
- onPressItem?.(item);
111
- },
149
+ (item: IContentData) => onPressItem?.(item),
112
150
  [onPressItem]
113
151
  );
152
+
114
153
  const handlePressMore = useCallback(() => {
115
154
  onPressMore?.({
116
155
  section_id,
@@ -118,39 +157,83 @@ const MovieCardTwo: React.FC<MovieCardTwoProps> = ({
118
157
  type,
119
158
  });
120
159
  }, [onPressMore, section_id, title, type]);
160
+
121
161
  const handleEndReached = useCallback(() => {
122
162
  if (
123
163
  !onEndReachedCalledDuringMomentum.current &&
124
- paginated?.hasNextPage &&
125
- paginated?.nextPage
164
+ pagination?.hasNextPage &&
165
+ pagination?.nextPage
126
166
  ) {
127
167
  onEndReachedCalledDuringMomentum.current = true;
128
- loadMoreData(paginated.nextPage);
168
+ loadMore(pagination.nextPage);
129
169
  }
130
- }, [loadMoreData, paginated?.hasNextPage, paginated?.nextPage]);
170
+ }, [pagination, loadMore]);
171
+
172
+ const handleMomentumScrollBegin = useCallback(() => {
173
+ onEndReachedCalledDuringMomentum.current = false;
174
+ }, []);
175
+
176
+ // Render Functions
177
+
131
178
  const renderItem = useCallback(
132
- ({ item, index }: ListRenderItemInfo<IContentData>) => {
179
+ ({ item, index }: { item: IContentData | IAdItem; index: number }) => {
180
+ // Handle ads
181
+ if ('type' in item && item.type === 'ads') {
182
+ const adItem = item as IAdItem;
183
+ return (
184
+ <View
185
+ key={`ad-${adItem.tracking?.impression || index}`}
186
+ style={[styles.item, index === 0 && styles.firstAdItem, itemStyle]}
187
+ >
188
+ <AdsPoster
189
+ ad={adItem}
190
+ theme={appliedTheme}
191
+ isLoading={isLoading}
192
+ containerStyle={{
193
+ width: itemWidth,
194
+ height: itemWidth / ITEM_ASPECT_RATIO,
195
+ }}
196
+ imageStyle={styles.image}
197
+ borderRadius={borderRadius}
198
+ onDisplayAds={onDisplayAds}
199
+ screenDimensions={screenDimensions}
200
+ viewportOffsets={viewportOffsets}
201
+ />
202
+ </View>
203
+ );
204
+ }
205
+
206
+ // Handle content items
207
+ const contentItem = item as IContentData;
208
+ const isSkeleton =
209
+ contentItem._id?.startsWith?.('pagination-skeleton') ?? false;
210
+
133
211
  return (
134
212
  <TouchableOpacity
135
- key={index}
136
- style={[styles.item, index === 0 && styles.firstItem, itemStyle]}
137
- onPress={() => handleItemPress(item)}
138
- activeOpacity={0.85}
139
- testID={`movie-card-two-item-${index}`}
140
- accessibilityLabel={`Card Two: ${item.name || 'item'} ${index + 1}`}
141
- accessibilityHint="Press to view movie details"
213
+ key={contentItem._id}
214
+ style={[
215
+ styles.item,
216
+ index === 0 && styles.firstContentItem,
217
+ itemStyle,
218
+ ]}
219
+ activeOpacity={ITEM_ACTIVE_OPACITY}
220
+ onPress={() => {
221
+ if (!isSkeleton && !isLoading) {
222
+ handleItemPress(contentItem);
223
+ }
224
+ }}
142
225
  >
143
226
  {renderItemImage ? (
144
227
  renderItemImage({ item, index })
145
228
  ) : (
146
229
  <CardPoster
147
- content_offering_type={item.content_offering_type}
148
- posterUri={item.poster}
230
+ posterUri={contentItem.poster}
231
+ content_offering_type={contentItem.content_offering_type}
149
232
  theme={appliedTheme}
150
233
  borderRadius={borderRadius}
151
234
  posterWrapperStyle={styles.image}
152
235
  resizeMode={FastImage.resizeMode.cover}
153
- isLoading={isLoading}
236
+ isLoading={isSkeleton || isLoading}
154
237
  />
155
238
  )}
156
239
  </TouchableOpacity>
@@ -158,16 +241,22 @@ const MovieCardTwo: React.FC<MovieCardTwoProps> = ({
158
241
  },
159
242
  [
160
243
  styles.item,
161
- styles.firstItem,
244
+ styles.firstContentItem,
162
245
  styles.image,
246
+ styles.firstAdItem,
163
247
  itemStyle,
164
248
  renderItemImage,
165
249
  appliedTheme,
166
250
  borderRadius,
167
251
  isLoading,
252
+ itemWidth,
253
+ onDisplayAds,
254
+ screenDimensions,
255
+ viewportOffsets,
168
256
  handleItemPress,
169
257
  ]
170
258
  );
259
+
171
260
  const renderSkeletonItem: ListRenderItem<null> = useCallback(
172
261
  ({ index }) => (
173
262
  <SkeletonPlaceholder
@@ -177,24 +266,47 @@ const MovieCardTwo: React.FC<MovieCardTwoProps> = ({
177
266
  >
178
267
  <SkeletonPlaceholder.Item
179
268
  width={itemWidth}
180
- height={itemWidth / 0.67}
269
+ height={itemWidth / ITEM_ASPECT_RATIO}
181
270
  borderRadius={borderRadius}
182
- marginRight={moderateScale(12)}
183
- marginLeft={index === 0 ? moderateScale(12) : 0}
271
+ marginRight={SKELETON_ITEM_MARGIN}
272
+ marginLeft={index === 0 ? SKELETON_ITEM_MARGIN : 0}
184
273
  />
185
274
  </SkeletonPlaceholder>
186
275
  ),
187
276
  [appliedTheme, itemWidth, borderRadius]
188
277
  );
189
- if (!initialData) {
190
- return null;
191
- }
192
- const listData = data ?? [];
193
278
 
194
- if (!isLoading && listData.length === 0) {
195
- return null;
196
- }
279
+ const keyExtractor = useCallback(
280
+ (item: IContentData | IAdItem, index: number) => {
281
+ return '_id' in item ? item._id : `ad-${index}`;
282
+ },
283
+ []
284
+ );
285
+
286
+ const renderPaginationFooter = useCallback(() => {
287
+ if (!isPaginating) return null;
197
288
 
289
+ return (
290
+ <SkeletonPlaceholder
291
+ highlightColor={appliedTheme.colors.skeletonHighlightColor}
292
+ backgroundColor={appliedTheme.colors.skeletonBaseColor}
293
+ borderRadius={borderRadius}
294
+ >
295
+ <SkeletonPlaceholder.Item
296
+ width={itemWidth}
297
+ height={itemWidth / ITEM_ASPECT_RATIO}
298
+ borderRadius={borderRadius}
299
+ marginRight={SKELETON_ITEM_MARGIN}
300
+ />
301
+ </SkeletonPlaceholder>
302
+ );
303
+ }, [isPaginating, appliedTheme, borderRadius, itemWidth]);
304
+
305
+ // Early Returns
306
+ if (!externalData) return null;
307
+ if (!isLoading && listData.length === 0 && !isEmpty) return null;
308
+
309
+ // Loading State
198
310
  if (isLoading) {
199
311
  return (
200
312
  <View style={[styles.root, containerStyle]}>
@@ -203,13 +315,14 @@ const MovieCardTwo: React.FC<MovieCardTwoProps> = ({
203
315
  backgroundColor={appliedTheme.colors.skeletonBaseColor}
204
316
  >
205
317
  <SkeletonPlaceholder.Item
206
- width={moderateScale(100)}
207
- height={verticalScale(14)}
208
- borderRadius={moderateScale(4)}
209
- marginLeft={moderateScale(12)}
318
+ width={SKELETON_TITLE_WIDTH}
319
+ height={SKELETON_TITLE_HEIGHT}
320
+ borderRadius={SKELETON_TITLE_BORDER_RADIUS}
321
+ marginLeft={SKELETON_ITEM_MARGIN}
210
322
  marginBottom={verticalScale(6)}
211
323
  />
212
324
  </SkeletonPlaceholder>
325
+
213
326
  <FlatList
214
327
  data={Array.from({ length: skeletonCount }, () => null)}
215
328
  horizontal
@@ -222,6 +335,7 @@ const MovieCardTwo: React.FC<MovieCardTwoProps> = ({
222
335
  );
223
336
  }
224
337
 
338
+ // Main Render
225
339
  return (
226
340
  <View
227
341
  style={[styles.root, containerStyle]}
@@ -240,61 +354,50 @@ const MovieCardTwo: React.FC<MovieCardTwoProps> = ({
240
354
  ]}
241
355
  showAllProps={{ iconColor: appliedTheme.colors.textPrimary, theme }}
242
356
  />
357
+
243
358
  <FlatList
244
359
  ref={flatListRef}
245
360
  data={listData}
246
361
  horizontal
247
362
  showsHorizontalScrollIndicator={false}
248
- keyExtractor={(item, index) => `${item._id ?? item.name}-${index}`}
363
+ keyExtractor={keyExtractor}
249
364
  renderItem={renderItem}
250
365
  contentContainerStyle={styles.listContent}
251
366
  onEndReached={handleEndReached}
252
- onEndReachedThreshold={0.5}
253
- onMomentumScrollBegin={() => {
254
- onEndReachedCalledDuringMomentum.current = false;
255
- }}
256
- ListFooterComponent={
257
- isPaginating ? (
258
- <SkeletonPlaceholder
259
- highlightColor={appliedTheme.colors.skeletonHighlightColor}
260
- backgroundColor={appliedTheme.colors.skeletonBaseColor}
261
- borderRadius={borderRadius}
262
- >
263
- <SkeletonPlaceholder.Item
264
- width={itemWidth}
265
- height={itemWidth / 0.67}
266
- borderRadius={borderRadius}
267
- marginRight={moderateScale(12)}
268
- />
269
- </SkeletonPlaceholder>
270
- ) : null
271
- }
367
+ onEndReachedThreshold={END_REACHED_THRESHOLD}
368
+ onMomentumScrollBegin={handleMomentumScrollBegin}
369
+ ListFooterComponent={renderPaginationFooter()}
272
370
  />
273
371
  </View>
274
372
  );
275
373
  };
276
374
 
375
+ // Styles
277
376
  const getStyles = (theme: ITheme, width: number, radius: number) =>
278
377
  StyleSheet.create({
279
378
  root: {
280
379
  marginVertical: verticalScale(6),
281
380
  },
282
381
  item: {
283
- width: width,
284
- height: width / 0.67,
285
- marginRight: moderateScale(12),
382
+ width,
383
+ height: width / ITEM_ASPECT_RATIO,
384
+ marginRight: ITEM_MARGIN_RIGHT,
286
385
  borderRadius: radius,
287
386
  overflow: 'hidden',
288
- backgroundColor: theme.colors.background ?? '#2c2c2c',
387
+ backgroundColor: theme.colors.background ?? DEFAULT_BACKGROUND_COLOR,
388
+ },
389
+ firstContentItem: {
390
+ marginLeft: ITEM_MARGIN_LEFT,
289
391
  },
290
- firstItem: {
291
- marginLeft: moderateScale(10),
392
+ firstAdItem: {
393
+ marginLeft: ITEM_MARGIN_LEFT,
292
394
  },
293
395
  image: {
294
396
  width: '100%',
295
397
  height: '100%',
296
398
  borderRadius: radius,
297
- backgroundColor: theme.colors.background ?? '#222',
399
+ backgroundColor:
400
+ theme.colors.background ?? DEFAULT_IMAGE_BACKGROUND_COLOR,
298
401
  },
299
402
  navigateToMoreContainer: {
300
403
  paddingHorizontal: moderateScale(10),
@@ -0,0 +1,202 @@
1
+ import { memo, useMemo } from 'react';
2
+ import {
3
+ TouchableOpacity,
4
+ View,
5
+ StyleSheet,
6
+ type StyleProp,
7
+ type ViewStyle,
8
+ } from 'react-native';
9
+ import SkeletonPlaceholder from 'react-native-skeleton-placeholder';
10
+ import { scale } from 'react-native-size-matters';
11
+ import type { IServeAd } from '@zezosoft/zezo-ott-api-client';
12
+ import CardPoster from './CardPoster';
13
+ import { Text } from '../../../Text';
14
+ import type { ITheme } from '../../../../theme';
15
+ import { useAdTracking } from '../../../../hooks';
16
+
17
+ type Props = {
18
+ ad: IServeAd;
19
+ theme: ITheme;
20
+ isLoading?: boolean;
21
+ containerStyle?: StyleProp<ViewStyle>;
22
+ imageStyle?: StyleProp<ViewStyle>;
23
+ onPress?: (item: IServeAd) => void;
24
+ borderRadius?: number;
25
+ onDisplayAds?: (ad: IServeAd) => void;
26
+ screenDimensions?: { width: number; height: number };
27
+ viewportOffsets?: {
28
+ top: number;
29
+ bottom: number;
30
+ left: number;
31
+ right: number;
32
+ };
33
+ };
34
+
35
+ // Constants
36
+ const DEFAULT_BORDER_RADIUS = scale(6);
37
+ const DEFAULT_AD_COLOR = '#CA091E';
38
+ const TOUCHABLE_OPACITY = 0.85;
39
+
40
+ // Badge styling constants
41
+ const BADGE_POSITION = scale(4);
42
+ const BADGE_MIN_WIDTH = scale(28);
43
+ const BADGE_HEIGHT = scale(19);
44
+ const BADGE_PADDING_HORIZONTAL = scale(8);
45
+ const BADGE_BORDER_RADIUS = scale(12);
46
+ const BADGE_FONT_SIZE = scale(9);
47
+ const BADGE_LETTER_SPACING = 1.2;
48
+ const BADGE_LINE_HEIGHT = scale(12);
49
+
50
+ // Badge background with semi-transparent white for better visibility
51
+ const BADGE_BACKGROUND = 'rgba(255, 255, 255, 0.95)';
52
+ const BADGE_BORDER_WIDTH = scale(0.5);
53
+ const BADGE_BORDER_COLOR = 'rgba(0, 0, 0, 0.08)';
54
+
55
+ // Enhanced shadow for better depth and visibility
56
+ const BADGE_SHADOW = {
57
+ shadowColor: '#000',
58
+ shadowOffset: { width: 0, height: scale(2) },
59
+ shadowOpacity: 0.25,
60
+ shadowRadius: scale(4),
61
+ elevation: 5,
62
+ };
63
+
64
+ export const AdsPoster = memo(
65
+ ({
66
+ ad,
67
+ theme,
68
+ isLoading = false,
69
+ containerStyle,
70
+ imageStyle,
71
+ borderRadius = DEFAULT_BORDER_RADIUS,
72
+ onPress,
73
+ onDisplayAds,
74
+ screenDimensions: propScreenDimensions,
75
+ viewportOffsets: propViewportOffsets,
76
+ }: Props) => {
77
+ const { colors } = theme;
78
+
79
+ const { viewRef, handleLayout } = useAdTracking({
80
+ ad,
81
+ onDisplayAds,
82
+ isLoading,
83
+ screenDimensions: propScreenDimensions,
84
+ viewportOffsets: propViewportOffsets,
85
+ });
86
+
87
+ // Memoize badge styles with dynamic positioning
88
+ const badgeStyle = useMemo(
89
+ () => [
90
+ styles.badge,
91
+ {
92
+ top: BADGE_POSITION,
93
+ left: BADGE_POSITION,
94
+ minWidth: BADGE_MIN_WIDTH,
95
+ height: BADGE_HEIGHT,
96
+ paddingHorizontal: BADGE_PADDING_HORIZONTAL,
97
+ borderRadius: BADGE_BORDER_RADIUS,
98
+ },
99
+ isLoading && styles.badgeSkeleton,
100
+ ],
101
+ [isLoading]
102
+ );
103
+
104
+ // Memoize badge text style with better typography
105
+ const badgeTextStyle = useMemo(
106
+ () => [
107
+ styles.badgeText,
108
+ {
109
+ color: colors.premiumIcon || DEFAULT_AD_COLOR,
110
+ lineHeight: BADGE_LINE_HEIGHT,
111
+ },
112
+ ],
113
+ [colors.premiumIcon]
114
+ );
115
+
116
+ // Memoize skeleton item style matching badge dimensions
117
+ const skeletonItemStyle = useMemo(
118
+ () => ({
119
+ width: BADGE_MIN_WIDTH,
120
+ height: BADGE_HEIGHT,
121
+ borderRadius: BADGE_BORDER_RADIUS,
122
+ }),
123
+ []
124
+ );
125
+
126
+ // Early return if no media URL
127
+ if (!ad?.mediaUrl) {
128
+ return null;
129
+ }
130
+
131
+ return (
132
+ <TouchableOpacity
133
+ ref={viewRef}
134
+ activeOpacity={TOUCHABLE_OPACITY}
135
+ disabled={isLoading}
136
+ onPress={() => onPress?.(ad)}
137
+ style={[styles.wrapper, containerStyle]}
138
+ accessibilityRole="button"
139
+ accessibilityLabel="Advertisement"
140
+ onLayout={handleLayout}
141
+ >
142
+ <View style={[styles.posterWrapper, { borderRadius }]}>
143
+ <CardPoster
144
+ posterUri={ad.mediaUrl}
145
+ theme={theme}
146
+ isLoading={isLoading}
147
+ posterWrapperStyle={imageStyle}
148
+ borderRadius={borderRadius}
149
+ />
150
+
151
+ {/* AD Badge */}
152
+ <View style={badgeStyle}>
153
+ {isLoading ? (
154
+ <SkeletonPlaceholder
155
+ backgroundColor={colors.skeletonBaseColor}
156
+ highlightColor={colors.skeletonHighlightColor}
157
+ >
158
+ <SkeletonPlaceholder.Item style={skeletonItemStyle} />
159
+ </SkeletonPlaceholder>
160
+ ) : (
161
+ <Text type="caption" weight="700" style={badgeTextStyle}>
162
+ AD
163
+ </Text>
164
+ )}
165
+ </View>
166
+ </View>
167
+ </TouchableOpacity>
168
+ );
169
+ }
170
+ );
171
+
172
+ AdsPoster.displayName = 'AdsPoster';
173
+
174
+ const styles = StyleSheet.create({
175
+ wrapper: {
176
+ position: 'relative',
177
+ },
178
+ posterWrapper: {
179
+ position: 'relative',
180
+ },
181
+ badge: {
182
+ position: 'absolute',
183
+ alignItems: 'center',
184
+ justifyContent: 'center',
185
+ zIndex: 10,
186
+ backgroundColor: BADGE_BACKGROUND,
187
+ borderWidth: BADGE_BORDER_WIDTH,
188
+ borderColor: BADGE_BORDER_COLOR,
189
+ ...BADGE_SHADOW,
190
+ },
191
+ badgeSkeleton: {
192
+ backgroundColor: 'transparent',
193
+ borderWidth: 0,
194
+ shadowOpacity: 0,
195
+ elevation: 0,
196
+ },
197
+ badgeText: {
198
+ fontSize: BADGE_FONT_SIZE,
199
+ letterSpacing: BADGE_LETTER_SPACING,
200
+ fontWeight: '700',
201
+ },
202
+ });