@plusscommunities/pluss-feature-builder-app-d 1.0.1-beta.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 (118) hide show
  1. package/dist/module/actions/featureBuilderActions.js +106 -0
  2. package/dist/module/actions/featureBuilderActions.js.map +1 -0
  3. package/dist/module/actions/featureBuilderStringsActions.js +106 -0
  4. package/dist/module/actions/featureBuilderStringsActions.js.map +1 -0
  5. package/dist/module/actions/index.js +12 -0
  6. package/dist/module/actions/index.js.map +1 -0
  7. package/dist/module/actions/types.js +7 -0
  8. package/dist/module/actions/types.js.map +1 -0
  9. package/dist/module/components/FeatureDetailScreen.js +725 -0
  10. package/dist/module/components/FeatureDetailScreen.js.map +1 -0
  11. package/dist/module/components/FeatureListItem.js +174 -0
  12. package/dist/module/components/FeatureListItem.js.map +1 -0
  13. package/dist/module/components/FeatureListScreen.js +159 -0
  14. package/dist/module/components/FeatureListScreen.js.map +1 -0
  15. package/dist/module/components/FieldRenderer.js +218 -0
  16. package/dist/module/components/FieldRenderer.js.map +1 -0
  17. package/dist/module/components/FileDownload.js +74 -0
  18. package/dist/module/components/FileDownload.js.map +1 -0
  19. package/dist/module/components/WidgetGrid.js +158 -0
  20. package/dist/module/components/WidgetGrid.js.map +1 -0
  21. package/dist/module/components/WidgetLarge.js +274 -0
  22. package/dist/module/components/WidgetLarge.js.map +1 -0
  23. package/dist/module/components/WidgetSmall.js +315 -0
  24. package/dist/module/components/WidgetSmall.js.map +1 -0
  25. package/dist/module/components/common/index.js +25 -0
  26. package/dist/module/components/common/index.js.map +1 -0
  27. package/dist/module/components/layouts/CondensedList.js +195 -0
  28. package/dist/module/components/layouts/CondensedList.js.map +1 -0
  29. package/dist/module/components/layouts/FeatureImageList.js +172 -0
  30. package/dist/module/components/layouts/FeatureImageList.js.map +1 -0
  31. package/dist/module/components/layouts/RoundImageList.js +198 -0
  32. package/dist/module/components/layouts/RoundImageList.js.map +1 -0
  33. package/dist/module/components/layouts/SquareImageList.js +185 -0
  34. package/dist/module/components/layouts/SquareImageList.js.map +1 -0
  35. package/dist/module/config/index.js +10 -0
  36. package/dist/module/config/index.js.map +1 -0
  37. package/dist/module/core.config.js +17 -0
  38. package/dist/module/core.config.js.map +1 -0
  39. package/dist/module/feature.config.js +113 -0
  40. package/dist/module/feature.config.js.map +1 -0
  41. package/dist/module/index.js +24 -0
  42. package/dist/module/index.js.map +1 -0
  43. package/dist/module/js/Colors.js +25 -0
  44. package/dist/module/js/Colors.js.map +1 -0
  45. package/dist/module/js/FieldTypes.js +123 -0
  46. package/dist/module/js/FieldTypes.js.map +1 -0
  47. package/dist/module/js/NavigationService.js +10 -0
  48. package/dist/module/js/NavigationService.js.map +1 -0
  49. package/dist/module/js/Styles.js +3 -0
  50. package/dist/module/js/Styles.js.map +1 -0
  51. package/dist/module/js/helpers.js +29 -0
  52. package/dist/module/js/helpers.js.map +1 -0
  53. package/dist/module/js/index.js +24 -0
  54. package/dist/module/js/index.js.map +1 -0
  55. package/dist/module/js/spacing.js +29 -0
  56. package/dist/module/js/spacing.js.map +1 -0
  57. package/dist/module/js/types.js +254 -0
  58. package/dist/module/js/types.js.map +1 -0
  59. package/dist/module/reducers/featureBuilderReducer.js +75 -0
  60. package/dist/module/reducers/featureBuilderReducer.js.map +1 -0
  61. package/dist/module/utils/featureSelectors.js +9 -0
  62. package/dist/module/utils/featureSelectors.js.map +1 -0
  63. package/dist/module/values.config.a.js +96 -0
  64. package/dist/module/values.config.a.js.map +1 -0
  65. package/dist/module/values.config.b.js +96 -0
  66. package/dist/module/values.config.b.js.map +1 -0
  67. package/dist/module/values.config.c.js +96 -0
  68. package/dist/module/values.config.c.js.map +1 -0
  69. package/dist/module/values.config.d.js +96 -0
  70. package/dist/module/values.config.d.js.map +1 -0
  71. package/dist/module/values.config.js +96 -0
  72. package/dist/module/values.config.js.map +1 -0
  73. package/dist/module/webapi/featureBuilderAPI.js +59 -0
  74. package/dist/module/webapi/featureBuilderAPI.js.map +1 -0
  75. package/dist/module/webapi/helper.js +4 -0
  76. package/dist/module/webapi/helper.js.map +1 -0
  77. package/dist/module/webapi/index.js +8 -0
  78. package/dist/module/webapi/index.js.map +1 -0
  79. package/package.json +62 -0
  80. package/src/actions/featureBuilderActions.js +112 -0
  81. package/src/actions/featureBuilderStringsActions.js +114 -0
  82. package/src/actions/index.js +12 -0
  83. package/src/actions/types.js +7 -0
  84. package/src/components/FeatureDetailScreen.js +817 -0
  85. package/src/components/FeatureListItem.js +198 -0
  86. package/src/components/FeatureListScreen.js +160 -0
  87. package/src/components/FieldRenderer.js +272 -0
  88. package/src/components/FileDownload.js +79 -0
  89. package/src/components/WidgetGrid.js +181 -0
  90. package/src/components/WidgetLarge.js +305 -0
  91. package/src/components/WidgetSmall.js +344 -0
  92. package/src/components/common/index.js +25 -0
  93. package/src/components/layouts/CondensedList.js +230 -0
  94. package/src/components/layouts/FeatureImageList.js +193 -0
  95. package/src/components/layouts/RoundImageList.js +219 -0
  96. package/src/components/layouts/SquareImageList.js +205 -0
  97. package/src/config/index.js +10 -0
  98. package/src/core.config.js +29 -0
  99. package/src/feature.config.js +127 -0
  100. package/src/index.js +27 -0
  101. package/src/js/Colors.js +30 -0
  102. package/src/js/FieldTypes.js +131 -0
  103. package/src/js/NavigationService.js +12 -0
  104. package/src/js/Styles.js +3 -0
  105. package/src/js/helpers.js +30 -0
  106. package/src/js/index.js +24 -0
  107. package/src/js/spacing.js +30 -0
  108. package/src/js/types.js +253 -0
  109. package/src/reducers/featureBuilderReducer.js +64 -0
  110. package/src/utils/featureSelectors.js +8 -0
  111. package/src/values.config.a.js +104 -0
  112. package/src/values.config.b.js +104 -0
  113. package/src/values.config.c.js +104 -0
  114. package/src/values.config.d.js +104 -0
  115. package/src/values.config.js +104 -0
  116. package/src/webapi/featureBuilderAPI.js +65 -0
  117. package/src/webapi/helper.js +4 -0
  118. package/src/webapi/index.js +8 -0
@@ -0,0 +1,817 @@
1
+ import React, { Component } from "react";
2
+ import { SPACING } from "../js/spacing";
3
+ import {
4
+ Platform,
5
+ View,
6
+ ScrollView,
7
+ Animated,
8
+ Dimensions,
9
+ StatusBar,
10
+ Text,
11
+ TouchableOpacity,
12
+ Linking,
13
+ StyleSheet,
14
+ KeyboardAvoidingView,
15
+ Image,
16
+ } from "react-native";
17
+ import { Icon } from "react-native-elements";
18
+ import { connect } from "react-redux";
19
+ import _ from "lodash";
20
+ import { LinearGradient } from "expo-linear-gradient";
21
+ import { Services } from "../feature.config";
22
+ import {
23
+ Components,
24
+ Colours,
25
+ COLOUR_TRANSPARENT,
26
+ TEXT_DARK,
27
+ } from "../core.config";
28
+ import { TEXT_MID, LINEGREY } from "../js/Colors";
29
+ import FileDownload from "./FileDownload";
30
+ import { values } from "../values.config";
31
+ import { selectFeatureDefinition } from "../reducers/featureBuilderReducer";
32
+
33
+ import Constants from "expo-constants";
34
+ import { InlineButton } from "./common";
35
+ import { FIELD_TYPES } from "../js/FieldTypes";
36
+
37
+ const SCREEN_HEIGHT = Dimensions.get("window").height;
38
+ const CARD_IMAGE_HEIGHT = SCREEN_HEIGHT * 0.3;
39
+ const SCREEN_WIDTH = Dimensions.get("window").width;
40
+
41
+ // StatusBarHeight function copied to avoid import from outside extension
42
+ const StatusBarHeight = (size) => {
43
+ return getStatusBarHeight() + size;
44
+ };
45
+
46
+ const getStatusBarHeight = () => {
47
+ return Platform.OS === "ios"
48
+ ? Constants.statusBarHeight
49
+ : StatusBar.currentHeight;
50
+ };
51
+
52
+ class FeatureDetailScreen extends Component {
53
+ constructor(props) {
54
+ super(props);
55
+ this.state = {
56
+ isHomeTab: false,
57
+ showImagePopup: false,
58
+ selectedImageIndex: 0,
59
+ allImages: [],
60
+ };
61
+ }
62
+
63
+ componentDidMount() {
64
+ this.setState({
65
+ isHomeTab: !Services.navigation.getParentRoute(),
66
+ allImages: this.collectAllImages(),
67
+ });
68
+ }
69
+
70
+ componentDidUpdate(prevProps) {
71
+ // Recalculate allImages when listing or featureDefinition changes
72
+ if (
73
+ prevProps.listing !== this.props.listing ||
74
+ prevProps.featureDefinition !== this.props.featureDefinition
75
+ ) {
76
+ this.setState({ allImages: this.collectAllImages() });
77
+ }
78
+ }
79
+
80
+ componentWillUnmount() {
81
+ if (Platform.OS === "ios") StatusBar.setBarStyle("dark-content");
82
+ }
83
+
84
+ onPressBack = () => {
85
+ Services.navigation.goBack();
86
+ };
87
+
88
+ /**
89
+ * Collect all images from listing fields
90
+ */
91
+ collectAllImages = () => {
92
+ const { listing, featureDefinition } = this.props;
93
+ if (!listing || !listing.fields || !featureDefinition) {
94
+ return [];
95
+ }
96
+
97
+ const allImages = [];
98
+ const fieldDefinitions = featureDefinition.fields || [];
99
+
100
+ // Add primary feature image first
101
+ const primaryImage = listing.fields[values.mandatoryFields.featureImage];
102
+ if (primaryImage) {
103
+ const imageObj = this.normalizeImageField(primaryImage);
104
+ if (imageObj) {
105
+ allImages.push({
106
+ ...imageObj,
107
+ fieldId: values.mandatoryFields.featureImage,
108
+ fieldLabel: values.labels.featureImage,
109
+ });
110
+ }
111
+ }
112
+
113
+ // Add additional image fields
114
+ fieldDefinitions.forEach((fieldDef) => {
115
+ if (
116
+ (fieldDef.type === FIELD_TYPES.IMAGE ||
117
+ fieldDef.type === FIELD_TYPES.GALLERY) &&
118
+ fieldDef.id !== values.mandatoryFields.featureImage
119
+ ) {
120
+ const fieldValue = listing.fields[fieldDef.id];
121
+
122
+ if (fieldValue) {
123
+ if (fieldDef.type === FIELD_TYPES.GALLERY) {
124
+ // Handle Gallery (Array of images)
125
+ if (Array.isArray(fieldValue)) {
126
+ fieldValue.forEach((item) => {
127
+ const imageObj = this.normalizeImageField(item);
128
+ if (imageObj) {
129
+ allImages.push({
130
+ ...imageObj,
131
+ fieldId: fieldDef.id,
132
+ fieldLabel: fieldDef.label || fieldDef.id,
133
+ });
134
+ }
135
+ });
136
+ }
137
+ } else {
138
+ // Handle Single Image
139
+ const imageObj = this.normalizeImageField(fieldValue);
140
+ if (imageObj) {
141
+ allImages.push({
142
+ ...imageObj,
143
+ fieldId: fieldDef.id,
144
+ fieldLabel: fieldDef.label || fieldDef.id,
145
+ });
146
+ }
147
+ }
148
+ }
149
+ }
150
+ });
151
+
152
+ return allImages;
153
+ };
154
+
155
+ /**
156
+ * Normalize image field to object format
157
+ */
158
+ normalizeImageField = (imageField) => {
159
+ if (!imageField) return null;
160
+
161
+ if (typeof imageField === "string") {
162
+ return { uri: imageField, url: imageField };
163
+ }
164
+
165
+ if (typeof imageField === "object") {
166
+ if (imageField.uri) {
167
+ return { uri: imageField.uri, url: imageField.uri, ...imageField };
168
+ }
169
+ if (imageField.url) {
170
+ return { uri: imageField.url, url: imageField.url, ...imageField };
171
+ }
172
+ }
173
+
174
+ return null;
175
+ };
176
+
177
+ /**
178
+ * Open image popup with all images
179
+ */
180
+ openImagePopup = (imageIndex = 0) => {
181
+ const { allImages } = this.state;
182
+ if (allImages.length > 0) {
183
+ this.setState({
184
+ showImagePopup: true,
185
+ selectedImageIndex: imageIndex,
186
+ });
187
+ }
188
+ };
189
+
190
+ /**
191
+ * Close image popup
192
+ */
193
+ closeImagePopup = () => {
194
+ this.setState({
195
+ showImagePopup: false,
196
+ selectedImageIndex: 0,
197
+ });
198
+ };
199
+
200
+ /**
201
+ * Process fields: filter out mandatory fields and sort by order
202
+ */
203
+ processFields() {
204
+ const { listing, featureDefinition } = this.props;
205
+ if (!listing || !listing.fields || !featureDefinition) {
206
+ return [];
207
+ }
208
+
209
+ const fieldDefinitions = featureDefinition.fields || [];
210
+ const excludedFieldIds = new Set([
211
+ values.mandatoryFields.title,
212
+ values.mandatoryFields.featureImage,
213
+ ]);
214
+
215
+ // Filter and map fields with their definitions
216
+ const filteredFields = Object.keys(listing.fields)
217
+ .filter((fieldId) => {
218
+ if (!fieldId || excludedFieldIds.has(fieldId)) return false;
219
+ if (listing.fields[fieldId] == null) return false;
220
+
221
+ // Only include fields that have definitions
222
+ const hasDefinition = fieldDefinitions.some((f) => f.id === fieldId);
223
+ if (!hasDefinition) {
224
+ return false;
225
+ }
226
+ return true;
227
+ })
228
+ .map((fieldId) => {
229
+ const fieldDefinition = fieldDefinitions.find(
230
+ (fieldDef) => fieldDef.id === fieldId,
231
+ );
232
+ return {
233
+ fieldId,
234
+ fieldValue: listing.fields[fieldId],
235
+ fieldDefinition,
236
+ fieldType: fieldDefinition?.type || FIELD_TYPES.TEXT,
237
+ };
238
+ });
239
+
240
+ // Sort by order property if available
241
+ return _.sortBy(
242
+ filteredFields,
243
+ (field) => field.fieldDefinition?.order || 999,
244
+ );
245
+ }
246
+
247
+ /**
248
+ * Handle link opening for various field types
249
+ */
250
+ handleLinkPress = (url) => {
251
+ if (url) {
252
+ Linking.openURL(url).catch((err) => {
253
+ // Silently handle URL open errors
254
+ });
255
+ }
256
+ };
257
+
258
+ handleFilePress = (file) => {
259
+ if (file?.url) {
260
+ Linking.openURL(file.url).catch((err) => {
261
+ // Silently handle URL open errors
262
+ });
263
+ }
264
+ };
265
+
266
+ /**
267
+ * Render individual field based on its type
268
+ */
269
+ renderField(fieldDef, fieldValue) {
270
+ const { colourBrandingMain } = this.props;
271
+
272
+ // Defensive: ensure fieldDef is always an object
273
+ const safeFieldDef = fieldDef || {};
274
+ const type = safeFieldDef.type || FIELD_TYPES.TEXT;
275
+ const label = safeFieldDef.label || safeFieldDef.id || "Unknown Field";
276
+
277
+ switch (type) {
278
+ case FIELD_TYPES.CTA: {
279
+ const btnLabel = fieldValue?.label || label;
280
+ const btnUrl = fieldValue?.url;
281
+ return (
282
+ <View style={styles.ctaContainer}>
283
+ <InlineButton
284
+ large
285
+ color={colourBrandingMain}
286
+ onPress={() => this.handleLinkPress(btnUrl)}
287
+ >
288
+ {btnLabel}
289
+ </InlineButton>
290
+ </View>
291
+ );
292
+ }
293
+
294
+ case FIELD_TYPES.EMAIL:
295
+ return (
296
+ <TouchableOpacity
297
+ style={styles.linkContainer}
298
+ onPress={() => this.handleLinkPress(`mailto:${fieldValue}`)}
299
+ >
300
+ <Icon
301
+ type="font-awesome"
302
+ name="envelope"
303
+ size={16}
304
+ color={colourBrandingMain}
305
+ style={styles.fieldIcon}
306
+ />
307
+ <Text style={[styles.linkText, { color: colourBrandingMain }]}>
308
+ {fieldValue}
309
+ </Text>
310
+ </TouchableOpacity>
311
+ );
312
+
313
+ case FIELD_TYPES.DATE:
314
+ return (
315
+ <View style={styles.dateContainer}>
316
+ <Icon
317
+ type="font-awesome"
318
+ name="calendar"
319
+ size={16}
320
+ color={TEXT_DARK}
321
+ style={styles.fieldIcon}
322
+ />
323
+ <Text style={styles.bodyText}>{fieldValue}</Text>
324
+ </View>
325
+ );
326
+
327
+ case FIELD_TYPES.FILE: {
328
+ // Handle both single file and multiple files (array)
329
+ const files = Array.isArray(fieldValue) ? fieldValue : [fieldValue];
330
+ return (
331
+ <View style={styles.filesContainer}>
332
+ <Text style={styles.sectionTitle}>{values.labels.attachments}</Text>
333
+ {files.map((file, index) => {
334
+ // Skip null/undefined files
335
+ if (!file) return null;
336
+
337
+ return (
338
+ <FileDownload
339
+ key={index}
340
+ file={file}
341
+ onPress={this.handleFilePress}
342
+ color={this.props.colourBrandingMain}
343
+ />
344
+ );
345
+ })}
346
+ </View>
347
+ );
348
+ }
349
+
350
+ case FIELD_TYPES.DESCRIPTION:
351
+ return <Text style={styles.descriptionText}>{fieldValue}</Text>;
352
+
353
+ case FIELD_TYPES.IMAGE: {
354
+ // Render secondary images inline with click handler
355
+ const imgUri =
356
+ fieldValue?.url ||
357
+ fieldValue?.uri ||
358
+ (typeof fieldValue === "string" ? fieldValue : null);
359
+ if (!imgUri) return null;
360
+
361
+ // Find this image's index for popup
362
+ const { allImages } = this.state;
363
+ const imageIndex = allImages.findIndex(
364
+ (img) => img.uri === imgUri || img.url === imgUri,
365
+ );
366
+
367
+ const caption = fieldValue?.caption;
368
+
369
+ return (
370
+ <TouchableOpacity
371
+ onPress={() => this.openImagePopup(imageIndex)}
372
+ activeOpacity={0.8}
373
+ >
374
+ <Image
375
+ source={{ uri: imgUri }}
376
+ style={styles.inlineImage}
377
+ resizeMode="cover"
378
+ />
379
+ {caption && <Text style={styles.imageCaption}>{caption}</Text>}
380
+ </TouchableOpacity>
381
+ );
382
+ }
383
+
384
+ case FIELD_TYPES.GALLERY: {
385
+ // Handle Gallery Fields (arrays of images)
386
+ if (!Array.isArray(fieldValue)) return null;
387
+
388
+ return (
389
+ <View style={{ marginVertical: 16 }}>
390
+ <View
391
+ style={{
392
+ flexDirection: "row",
393
+ flexWrap: "wrap",
394
+ marginHorizontal: -4,
395
+ }}
396
+ >
397
+ {fieldValue.map((imgItem, index) => {
398
+ const imgUri =
399
+ imgItem?.url ||
400
+ imgItem?.uri ||
401
+ (typeof imgItem === "string" ? imgItem : null);
402
+ if (!imgUri) return null;
403
+
404
+ // Find global index for the popup
405
+ const { allImages } = this.state;
406
+ const globalIndex = allImages.findIndex(
407
+ (img) => img.uri === imgUri || img.url === imgUri,
408
+ );
409
+
410
+ return (
411
+ <TouchableOpacity
412
+ key={index}
413
+ onPress={() =>
414
+ this.openImagePopup(globalIndex !== -1 ? globalIndex : 0)
415
+ }
416
+ activeOpacity={0.8}
417
+ style={{ width: "50%", padding: 4 }} // 2 column grid
418
+ >
419
+ <Image
420
+ source={{ uri: imgUri }}
421
+ style={[
422
+ styles.inlineImage,
423
+ { height: 150, marginVertical: 0 },
424
+ ]}
425
+ resizeMode="cover"
426
+ />
427
+ </TouchableOpacity>
428
+ );
429
+ })}
430
+ </View>
431
+ </View>
432
+ );
433
+ }
434
+
435
+ case FIELD_TYPES.TITLE:
436
+ return <Text style={styles.titleText}>{fieldValue}</Text>;
437
+
438
+ case FIELD_TYPES.NUMBER:
439
+ return <Text style={styles.numberText}>{fieldValue}</Text>;
440
+
441
+ default:
442
+ // Default text fallback
443
+ return (
444
+ <Text style={styles.bodyText}>
445
+ {typeof fieldValue === "object"
446
+ ? JSON.stringify(fieldValue)
447
+ : fieldValue}
448
+ </Text>
449
+ );
450
+ }
451
+ }
452
+
453
+ /**
454
+ * Render all fields with proper spacing and structure
455
+ */
456
+ renderFields() {
457
+ const processedFields = this.processFields();
458
+
459
+ return processedFields.map((field, _index) => {
460
+ return (
461
+ <View key={field.fieldId} style={styles.fieldContainer}>
462
+ {field.fieldType !== FIELD_TYPES.CTA &&
463
+ field.fieldType !== FIELD_TYPES.IMAGE &&
464
+ field.fieldDefinition?.label && (
465
+ <Text style={styles.fieldLabel}>
466
+ {field.fieldDefinition?.label}
467
+ </Text>
468
+ )}
469
+ {this.renderField(field.fieldDefinition, field.fieldValue)}
470
+ </View>
471
+ );
472
+ });
473
+ }
474
+
475
+ getImageSource() {
476
+ const { listing } = this.props;
477
+ if (!listing || !listing.fields) {
478
+ return null;
479
+ }
480
+
481
+ const imageField = listing.fields[values.mandatoryFields.featureImage];
482
+ if (!imageField) {
483
+ return null;
484
+ }
485
+
486
+ // Parse the image field value
487
+ if (typeof imageField === "string") {
488
+ return imageField;
489
+ }
490
+
491
+ return imageField?.uri || imageField?.url || null;
492
+ }
493
+
494
+ getTitle() {
495
+ const { listing } = this.props;
496
+ if (!listing || !listing.fields) {
497
+ return values.labels.defaultTitle;
498
+ }
499
+
500
+ return (
501
+ listing.fields[values.mandatoryFields.title] || values.labels.defaultTitle
502
+ );
503
+ }
504
+
505
+ render() {
506
+ const { listing } = this.props;
507
+ const imageSource = this.getImageSource();
508
+ const title = this.getTitle();
509
+
510
+ if (!listing || !listing.fields) {
511
+ return (
512
+ <View style={styles.centerContainer}>
513
+ <Text style={styles.errorText}>Item not found</Text>
514
+ </View>
515
+ );
516
+ }
517
+
518
+ return (
519
+ <KeyboardAvoidingView
520
+ behavior={Platform.OS === "ios" && "padding"}
521
+ style={{
522
+ position: "relative",
523
+ flex: 1,
524
+ backgroundColor: Colours.BACKGROUND_WHITE || "#fff",
525
+ }}
526
+ >
527
+ <View style={styles.backButton}>
528
+ <View style={styles.headerContent}>
529
+ <Components.BackButton
530
+ onPress={this.onPressBack}
531
+ style={{
532
+ marginLeft: 16,
533
+ }}
534
+ />
535
+ </View>
536
+ </View>
537
+
538
+ <ScrollView
539
+ style={{ backgroundColor: Colours.BACKGROUND_WHITE || "#fff" }}
540
+ keyboardShouldPersistTaps="always"
541
+ ref={(ref) => {
542
+ this.scrollView = ref;
543
+ }}
544
+ >
545
+ <View style={[styles.mainHeader]}>
546
+ {imageSource ? (
547
+ <TouchableOpacity
548
+ onPress={() => this.openImagePopup(0)}
549
+ activeOpacity={0.9}
550
+ >
551
+ <Components.AutoOffsetImage
552
+ uri={imageSource}
553
+ height={CARD_IMAGE_HEIGHT}
554
+ width={SCREEN_WIDTH}
555
+ />
556
+ </TouchableOpacity>
557
+ ) : (
558
+ <View
559
+ style={[
560
+ styles.grayPlaceholder,
561
+ { height: CARD_IMAGE_HEIGHT, width: SCREEN_WIDTH },
562
+ ]}
563
+ />
564
+ )}
565
+ <LinearGradient
566
+ style={styles.gradientOverlay}
567
+ colors={["rgba(0,0,0,0.7)", COLOUR_TRANSPARENT]}
568
+ start={{ x: 0, y: 0 }}
569
+ end={{ x: 0, y: 1 }}
570
+ locations={[0, 0.5]}
571
+ />
572
+ </View>
573
+ <View style={{ paddingHorizontal: 16 }}>
574
+ <View style={styles.titleContainer}>
575
+ <Text style={styles.eventTitle}>{title}</Text>
576
+ </View>
577
+ </View>
578
+ <View style={{ minHeight: SCREEN_HEIGHT - CARD_IMAGE_HEIGHT - 56 }}>
579
+ {this.renderFields()}
580
+ </View>
581
+ </ScrollView>
582
+
583
+ {/* Image Popup */}
584
+ {this.state.showImagePopup && (
585
+ <Components.ImagePopup
586
+ visible={this.state.showImagePopup}
587
+ images={this.state.allImages}
588
+ index={this.state.selectedImageIndex}
589
+ onClose={this.closeImagePopup}
590
+ />
591
+ )}
592
+ </KeyboardAvoidingView>
593
+ );
594
+ }
595
+ }
596
+
597
+ const styles = StyleSheet.create({
598
+ backButton: {
599
+ position: "absolute",
600
+ left: 0,
601
+ top: 0,
602
+ zIndex: 10,
603
+ elevation: 10,
604
+ height: StatusBarHeight(70),
605
+ },
606
+ headerContent: {
607
+ alignSelf: "stretch",
608
+ flexDirection: "row",
609
+ flex: 1,
610
+ paddingTop: StatusBarHeight(12),
611
+ },
612
+ image: {
613
+ backgroundColor: Colours.BACKGROUND_WHITE || "#fff",
614
+ position: "relative",
615
+ width: undefined,
616
+ height: undefined,
617
+ flex: 1,
618
+ },
619
+ mainHeader: {
620
+ position: "relative",
621
+ height: CARD_IMAGE_HEIGHT,
622
+ width: SCREEN_WIDTH,
623
+ },
624
+
625
+ gradientOverlay: {
626
+ flex: 1,
627
+ },
628
+
629
+ // Field container and spacing
630
+ fieldContainer: {
631
+ paddingHorizontal: 16,
632
+ marginVertical: 12,
633
+ },
634
+
635
+ // Field labels/captions
636
+ fieldLabel: {
637
+ fontFamily: "sf-regular",
638
+ fontSize: 11,
639
+ color: TEXT_MID,
640
+ marginBottom: 6,
641
+ textTransform: "uppercase",
642
+ letterSpacing: 1.2,
643
+ alignSelf: "center",
644
+ textAlign: "center",
645
+ paddingHorizontal: 8,
646
+ paddingVertical: 4,
647
+ backgroundColor: "rgba(0,0,0,0.03)",
648
+ borderRadius: 12,
649
+ maxWidth: "80%",
650
+ },
651
+
652
+ // Field type styles
653
+ bodyText: {
654
+ fontFamily: "sf-regular",
655
+ fontSize: 16,
656
+ color: TEXT_DARK,
657
+ lineHeight: 22,
658
+ },
659
+ titleText: {
660
+ fontFamily: "sf-semibold",
661
+ fontSize: 16,
662
+ fontWeight: "600",
663
+ color: TEXT_DARK,
664
+ },
665
+ descriptionText: {
666
+ fontFamily: "sf-regular",
667
+ fontSize: 16,
668
+ color: TEXT_DARK,
669
+ lineHeight: 24,
670
+ },
671
+ numberText: {
672
+ fontFamily: "sf-medium",
673
+ fontSize: 16,
674
+ fontWeight: "500",
675
+ color: TEXT_DARK,
676
+ },
677
+
678
+ // Link styles
679
+ linkContainer: {
680
+ flexDirection: "row",
681
+ alignItems: "center",
682
+ paddingVertical: 4,
683
+ },
684
+ linkText: {
685
+ fontFamily: "sf-medium",
686
+ fontSize: 16,
687
+ textDecorationLine: "underline",
688
+ },
689
+
690
+ // Date styles
691
+ dateContainer: {
692
+ flexDirection: "row",
693
+ alignItems: "center",
694
+ paddingVertical: 4,
695
+ },
696
+
697
+ // Image styles
698
+ inlineImage: {
699
+ width: "100%",
700
+ height: 200,
701
+ borderRadius: 8,
702
+ marginVertical: 8,
703
+ },
704
+ imageCaption: {
705
+ fontFamily: "sf-italic",
706
+ fontSize: 14,
707
+ fontStyle: "italic",
708
+ color: TEXT_MID,
709
+ marginTop: 4,
710
+ textAlign: "center",
711
+ },
712
+
713
+ // CTA styles
714
+ ctaContainer: {
715
+ alignItems: "center",
716
+ justifyContent: "center",
717
+ paddingVertical: 8,
718
+ },
719
+
720
+ // Section title styles
721
+ sectionTitle: {
722
+ fontFamily: "sf-bold",
723
+ fontSize: 14,
724
+ fontWeight: "700",
725
+ color: TEXT_DARK,
726
+ marginBottom: SPACING.SM,
727
+ },
728
+
729
+ // Icon styles
730
+ fieldIcon: {
731
+ marginRight: 8,
732
+ },
733
+
734
+ // Legacy styles (kept for compatibility)
735
+ section: {
736
+ paddingVertical: 16,
737
+ },
738
+ divider: {
739
+ height: 1,
740
+ backgroundColor: LINEGREY,
741
+ marginVertical: 8,
742
+ marginHorizontal: 16,
743
+ },
744
+ facilityDetailTitle: {
745
+ fontFamily: "sf-bold",
746
+ color: TEXT_DARK,
747
+ fontSize: 14,
748
+ },
749
+ facilityDetailContent: {
750
+ fontFamily: "sf-regular",
751
+ color: TEXT_DARK,
752
+ fontSize: 16,
753
+ lineHeight: 22,
754
+ },
755
+ facilityDetailsContainer: {
756
+ flex: 1,
757
+ },
758
+ facilityDetailsTextContainer: {
759
+ flex: 1,
760
+ justifyContent: "center",
761
+ },
762
+ facilityDetailContainer: {
763
+ marginTop: 16,
764
+ flexDirection: "row",
765
+ },
766
+ facilityDetailTitleContainer: {
767
+ width: 100,
768
+ marginRight: 10,
769
+ },
770
+ titleContainer: {
771
+ paddingVertical: 16,
772
+ paddingTop: 20,
773
+ },
774
+ eventTitle: {
775
+ fontFamily: "sf-bold",
776
+ fontSize: 24,
777
+ lineHeight: 32,
778
+ color: TEXT_DARK,
779
+ backgroundColor: "transparent",
780
+ flex: 1,
781
+ },
782
+ textLink: {
783
+ fontFamily: "sf-medium",
784
+ fontSize: 16,
785
+ lineHeight: 22,
786
+ },
787
+ grayPlaceholder: {
788
+ backgroundColor: LINEGREY,
789
+ },
790
+ centerContainer: {
791
+ flex: 1,
792
+ justifyContent: "center",
793
+ alignItems: "center",
794
+ paddingHorizontal: 20,
795
+ },
796
+ errorText: {
797
+ fontSize: 16,
798
+ color: TEXT_MID,
799
+ textAlign: "center",
800
+ },
801
+ filesContainer: {
802
+ marginVertical: 8,
803
+ },
804
+ });
805
+
806
+ const mapStateToProps = (state, ownProps) => {
807
+ return {
808
+ colourBrandingMain: Colours.getMainBrandingColourFromState(state),
809
+ colourBrandingHeader: Colours.getMainBrandingColourFromState(state),
810
+ featureDefinition: selectFeatureDefinition(state),
811
+ user: state.user,
812
+ };
813
+ };
814
+
815
+ export default connect(mapStateToProps, null, null, { forwardRef: true })(
816
+ FeatureDetailScreen,
817
+ );