@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,198 @@
1
+ import React from "react";
2
+ import { Text, TouchableOpacity, View, StyleSheet, Image } from "react-native";
3
+ import { Icon } from "react-native-elements";
4
+ import _ from "lodash";
5
+ import { Helper, Colours } from "../core.config";
6
+ import { values } from "../values.config";
7
+
8
+ import FieldRenderer from "./FieldRenderer";
9
+
10
+ const FeatureListItem = ({
11
+ listing,
12
+ onItemPress,
13
+ colourBrandingMain,
14
+ style,
15
+ featureDefinition,
16
+ }) => {
17
+ const getImageSource = (imageField) => {
18
+ if (!imageField) return null;
19
+
20
+ if (typeof imageField === "string") {
21
+ return { uri: imageField };
22
+ }
23
+
24
+ if (typeof imageField === "object" && imageField.uri) {
25
+ return imageField;
26
+ }
27
+
28
+ if (typeof imageField === "object" && imageField.url) {
29
+ return { uri: imageField.url };
30
+ }
31
+
32
+ return null;
33
+ };
34
+
35
+ const onPress = () => {
36
+ if (onItemPress) {
37
+ onItemPress(listing);
38
+ }
39
+ };
40
+
41
+ // Get data from feature builder fields
42
+ const title =
43
+ listing.fields?.[values.mandatoryFields.title] ||
44
+ values.labels.defaultTitle;
45
+ const description = listing.fields?.description || "";
46
+ const imageField = listing.fields?.[values.mandatoryFields.featureImage];
47
+ const imageSource = getImageSource(imageField);
48
+
49
+ // Get field definitions for type-aware rendering
50
+ const fieldDefinitions = featureDefinition?.fields || [];
51
+
52
+ // Use featureDefinitionId to determine the feature type, fallback to featureName
53
+ const getFeatureType = () => {
54
+ if (listing.featureDefinitionId) {
55
+ // Convert kebab-case to title case for display
56
+ return listing.featureDefinitionId
57
+ .split("-")
58
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
59
+ .join(" ");
60
+ }
61
+ return values.featureName || "Feature";
62
+ };
63
+
64
+ const featureType = getFeatureType();
65
+
66
+ // Render field content based on field definitions
67
+ const renderFieldContent = () => {
68
+ // Get non-mandatory fields to render
69
+ const nonMandatoryFields = fieldDefinitions.filter(
70
+ (fieldDef) =>
71
+ fieldDef.id !== values.mandatoryFields.title &&
72
+ fieldDef.id !== values.mandatoryFields.featureImage,
73
+ );
74
+
75
+ if (nonMandatoryFields.length === 0) {
76
+ return null;
77
+ }
78
+
79
+ return nonMandatoryFields.map((fieldDef) => {
80
+ const fieldValue = listing.fields[fieldDef.id];
81
+ if (!fieldValue) return null;
82
+
83
+ return (
84
+ <View key={fieldDef.id} style={styles.fieldContainer}>
85
+ <FieldRenderer
86
+ fieldId={fieldDef.id}
87
+ fieldValue={fieldValue}
88
+ fieldDefinition={fieldDef}
89
+ color={colourBrandingMain}
90
+ />
91
+ </View>
92
+ );
93
+ });
94
+ };
95
+
96
+ return (
97
+ <TouchableOpacity onPress={onPress}>
98
+ <View style={[styles.container, style]}>
99
+ <View style={styles.innerContainer}>
100
+ <View style={styles.textContainer}>
101
+ <Text style={styles.titleText}>{title}</Text>
102
+ {!_.isEmpty(description) && (
103
+ <Text numberOfLines={3} style={styles.descriptionText}>
104
+ {description}
105
+ </Text>
106
+ )}
107
+ {renderFieldContent()}
108
+ </View>
109
+ </View>
110
+ <View style={styles.imageContainer}>
111
+ {imageSource ? (
112
+ <Image
113
+ source={imageSource}
114
+ style={styles.image}
115
+ resizeMode="cover"
116
+ />
117
+ ) : (
118
+ <View style={[styles.image, styles.placeholderImage]}></View>
119
+ )}
120
+ </View>
121
+ </View>
122
+ </TouchableOpacity>
123
+ );
124
+ };
125
+
126
+ const styles = StyleSheet.create({
127
+ container: {
128
+ marginVertical: 8,
129
+ paddingHorizontal: 16,
130
+ },
131
+ innerContainer: {
132
+ height: values.dimensions.cardHeight,
133
+ marginLeft: 50,
134
+ ...Helper.getShadowStyle(),
135
+ shadowOpacity: 0.25,
136
+ shadowRadius: 12,
137
+ elevation: 10,
138
+ },
139
+ textContainer: {
140
+ flex: 1,
141
+ marginLeft: 60,
142
+ marginVertical: 15,
143
+ paddingRight: 15,
144
+ justifyContent: "space-between",
145
+ },
146
+ titleText: {
147
+ fontFamily: "sf-semibold",
148
+ fontSize: 16,
149
+ color: Colours.TEXT_DARK,
150
+ },
151
+ descriptionText: {
152
+ fontFamily: "sf-regular",
153
+ fontSize: 12,
154
+ color: Colours.TEXT_LIGHT,
155
+ minHeight: 30,
156
+ },
157
+ typeContainer: {
158
+ flexDirection: "row",
159
+ alignItems: "center",
160
+ },
161
+ typeIcon: {
162
+ fontSize: 16,
163
+ color: Colours.TEXT_DARK,
164
+ marginRight: 6,
165
+ },
166
+ typeText: {
167
+ fontFamily: "sf-semibold",
168
+ fontSize: 12,
169
+ color: "#007AFF", // TODO: Add colour main here
170
+ },
171
+ imageContainer: {
172
+ position: "absolute",
173
+ top: 0,
174
+ left: 16,
175
+ bottom: 0,
176
+ justifyContent: "center",
177
+ elevation: 8,
178
+ },
179
+ image: {
180
+ width: values.dimensions.imageSize,
181
+ height: values.dimensions.imageSize,
182
+ resizeMode: "cover",
183
+ borderRadius: values.dimensions.imageBorderRadius,
184
+ },
185
+ placeholderImage: {
186
+ backgroundColor: "#808080",
187
+ justifyContent: "center",
188
+ alignItems: "center",
189
+ },
190
+ placeholderText: {
191
+ fontSize: 24,
192
+ },
193
+ fieldContainer: {
194
+ marginTop: 8,
195
+ },
196
+ });
197
+
198
+ export default FeatureListItem;
@@ -0,0 +1,160 @@
1
+ import React, { Component } from "react";
2
+ import { View, Text, StyleSheet } from "react-native";
3
+ import { connect } from "react-redux";
4
+ import { Components, Colours } from "../core.config";
5
+ import { values } from "../values.config";
6
+ import { loadTargetFeature } from "../actions/featureBuilderActions";
7
+
8
+ // Helper function to title case a string
9
+ const toTitleCase = (str) => {
10
+ if (!str) return str;
11
+ return str
12
+ .toLowerCase()
13
+ .split(" ")
14
+ .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
15
+ .join(" ");
16
+ };
17
+
18
+ // Layouts
19
+ import RoundImageList from "./layouts/RoundImageList";
20
+ import CondensedList from "./layouts/CondensedList";
21
+ import SquareImageList from "./layouts/SquareImageList";
22
+ import FeatureImageList from "./layouts/FeatureImageList";
23
+
24
+ class FeatureListScreen extends Component {
25
+ componentDidMount() {
26
+ // Trigger the fetch sequence
27
+ this.props.loadTargetFeature();
28
+ }
29
+
30
+ getLayoutComponent = (layoutType) => {
31
+ switch (layoutType) {
32
+ case values.layoutTypes.round:
33
+ return RoundImageList;
34
+ case values.layoutTypes.square:
35
+ return SquareImageList;
36
+ case values.layoutTypes.feature:
37
+ return FeatureImageList;
38
+ default:
39
+ return CondensedList;
40
+ }
41
+ };
42
+
43
+ render() {
44
+ const { definition, listings, loading, error, navigation, mainColor } =
45
+ this.props;
46
+
47
+ // 1. Error
48
+ if (error) {
49
+ return (
50
+ <View style={styles.container}>
51
+ <Components.Header
52
+ text="Error"
53
+ leftIcon="angle-left"
54
+ onPressLeft={() => navigation.goBack()}
55
+ />
56
+ <View style={styles.center}>
57
+ <Text style={styles.errorText}>{error}</Text>
58
+ <Components.InlineButton onPress={this.props.loadTargetFeature}>
59
+ Retry
60
+ </Components.InlineButton>
61
+ </View>
62
+ </View>
63
+ );
64
+ }
65
+
66
+ // 2. Loading state with header
67
+ if (loading && !definition) {
68
+ const title = toTitleCase(values.featureName);
69
+ return (
70
+ <View style={styles.container}>
71
+ <Components.Header
72
+ text={title}
73
+ leftIcon="angle-left"
74
+ onPressLeft={() => navigation.goBack()}
75
+ />
76
+ <Components.LoadingIndicator visible={true} />
77
+ </View>
78
+ );
79
+ }
80
+
81
+ // 3. Render List
82
+ if (definition) {
83
+ // Since we flattened the data, definition.layout is now available directly
84
+ const layoutType = definition.layout?.type || "condensed";
85
+ const LayoutComponent = this.getLayoutComponent(layoutType);
86
+ const title = toTitleCase(definition.title || values.featureName);
87
+
88
+ return (
89
+ <View style={styles.container}>
90
+ <Components.Header
91
+ text={title}
92
+ leftIcon="angle-left"
93
+ onPressLeft={() => navigation.goBack()}
94
+ />
95
+
96
+ <LayoutComponent
97
+ listings={listings}
98
+ featureDefinition={definition}
99
+ colourBrandingMain={mainColor}
100
+ title={title}
101
+ onItemPress={(item) => {
102
+ navigation.navigate(values.screens.featureDetail, {
103
+ listing: item,
104
+ featureDefinition: definition,
105
+ featureTitle: title,
106
+ });
107
+ }}
108
+ refreshing={loading}
109
+ onRefresh={this.props.loadTargetFeature}
110
+ />
111
+ </View>
112
+ );
113
+ }
114
+
115
+ // 4. Default empty state - should not reach here if logic is correct
116
+ return (
117
+ <View style={styles.container}>
118
+ <Components.Header
119
+ text={toTitleCase(values.featureName)}
120
+ leftIcon="angle-left"
121
+ onPressLeft={() => navigation.goBack()}
122
+ />
123
+ <View style={styles.center}>
124
+ <Components.EmptyStateWidget
125
+ title="Feature not available"
126
+ height={120}
127
+ />
128
+ </View>
129
+ </View>
130
+ );
131
+ }
132
+ }
133
+
134
+ const styles = StyleSheet.create({
135
+ container: { flex: 1, backgroundColor: "#fff" },
136
+ center: {
137
+ flex: 1,
138
+ justifyContent: "center",
139
+ alignItems: "center",
140
+ padding: 20,
141
+ },
142
+ errorText: {
143
+ marginBottom: 10,
144
+ fontSize: 16,
145
+ color: "#666",
146
+ textAlign: "center",
147
+ },
148
+ });
149
+
150
+ const mapStateToProps = (state) => ({
151
+ definition: state[values.reducerKey]?.feature,
152
+ listings: state[values.reducerKey]?.listings,
153
+ loading: state[values.reducerKey]?.loading,
154
+ error: state[values.reducerKey]?.error,
155
+ mainColor: Colours.getMainBrandingColourFromState(state),
156
+ });
157
+
158
+ export default connect(mapStateToProps, { loadTargetFeature })(
159
+ FeatureListScreen,
160
+ );
@@ -0,0 +1,272 @@
1
+ import React from "react";
2
+ import {
3
+ Text,
4
+ View,
5
+ StyleSheet,
6
+ TouchableOpacity,
7
+ Linking,
8
+ } from "react-native";
9
+ import { Icon } from "react-native-elements";
10
+ import { Colours, Components, Helper } from "../core.config";
11
+ import { Attachment, InlineButton } from "./common";
12
+ import {
13
+ FIELD_TYPES,
14
+ isCTAField,
15
+ isImageField,
16
+ isLinkField,
17
+ } from "../js/FieldTypes";
18
+ import { SPACING } from "../js/spacing";
19
+
20
+ /**
21
+ * Smart Field Renderer Component
22
+ * Renders different field types based on field definition and value
23
+ */
24
+ const FieldRenderer = ({
25
+ fieldId,
26
+ fieldValue,
27
+ fieldDefinition,
28
+ onFieldPress,
29
+ color,
30
+ style,
31
+ }) => {
32
+ const fieldType = fieldDefinition?.type;
33
+ const fieldLabel = fieldDefinition?.values?.label || fieldId;
34
+
35
+ const handleLinkPress = (url) => {
36
+ if (url) {
37
+ Linking.openURL(url).catch((err) => {
38
+ // Silently handle URL open errors
39
+ });
40
+ }
41
+ };
42
+
43
+ // If no field type, try to infer from value structure
44
+ if (!fieldType) {
45
+ if (isCTAField(fieldValue)) {
46
+ return (
47
+ <InlineButton
48
+ large
49
+ color={color || Colours.BRANDING_MAIN}
50
+ style={style}
51
+ onPress={() => fieldValue?.url && handleLinkPress(fieldValue.url)}
52
+ >
53
+ {fieldValue?.label || fieldLabel}
54
+ </InlineButton>
55
+ );
56
+ }
57
+
58
+ if (isImageField(fieldValue)) {
59
+ // Handle image fields - typically rendered elsewhere in layouts
60
+ return null;
61
+ }
62
+
63
+ if (isLinkField(fieldValue)) {
64
+ return (
65
+ <TouchableOpacity
66
+ style={[styles.linkContainer, style]}
67
+ onPress={() => handleLinkPress(fieldValue)}
68
+ >
69
+ <Icon
70
+ type="font-awesome"
71
+ name="link"
72
+ size={16}
73
+ color={color || Colours.BRANDING_MAIN}
74
+ style={styles.linkIcon}
75
+ />
76
+ <Text
77
+ style={[styles.linkText, { color: color || Colours.BRANDING_MAIN }]}
78
+ numberOfLines={2}
79
+ >
80
+ {fieldValue}
81
+ </Text>
82
+ </TouchableOpacity>
83
+ );
84
+ }
85
+
86
+ // Default to text field
87
+ return (
88
+ <>
89
+ <Text style={[styles.text, style]} numberOfLines={3}>
90
+ {fieldValue}
91
+ </Text>
92
+ </>
93
+ );
94
+ }
95
+
96
+ // Type-based rendering
97
+ switch (fieldType) {
98
+ case FIELD_TYPES.CTA:
99
+ return (
100
+ <InlineButton
101
+ large
102
+ color={color || Colours.BRANDING_MAIN}
103
+ style={style}
104
+ onPress={() => fieldValue?.url && handleLinkPress(fieldValue.url)}
105
+ >
106
+ {fieldValue?.label || fieldLabel}
107
+ </InlineButton>
108
+ );
109
+
110
+ case FIELD_TYPES.TITLE:
111
+ case FIELD_TYPES.TEXT:
112
+ return (
113
+ <Text style={[styles.text, styles.titleText, style]} numberOfLines={2}>
114
+ {fieldValue}
115
+ </Text>
116
+ );
117
+
118
+ case FIELD_TYPES.DESCRIPTION:
119
+ return (
120
+ <Text style={[styles.text, styles.descriptionText, style]}>
121
+ {fieldValue}
122
+ </Text>
123
+ );
124
+
125
+ case FIELD_TYPES.EMAIL:
126
+ return (
127
+ <TouchableOpacity
128
+ style={[styles.linkContainer, style]}
129
+ onPress={() => handleLinkPress(`mailto:${fieldValue}`)}
130
+ >
131
+ <Icon
132
+ type="font-awesome"
133
+ name="envelope"
134
+ size={16}
135
+ color={color || Colours.BRANDING_MAIN}
136
+ style={styles.linkIcon}
137
+ />
138
+ <Text
139
+ style={[styles.linkText, { color: color || Colours.BRANDING_MAIN }]}
140
+ numberOfLines={1}
141
+ >
142
+ {fieldValue}
143
+ </Text>
144
+ </TouchableOpacity>
145
+ );
146
+
147
+ case FIELD_TYPES.NUMBER:
148
+ return (
149
+ <Text style={[styles.text, styles.numberText, style]}>
150
+ {fieldValue}
151
+ </Text>
152
+ );
153
+
154
+ case FIELD_TYPES.DATE:
155
+ return (
156
+ <View style={[styles.dateContainer, style]}>
157
+ <Icon
158
+ type="font-awesome"
159
+ name="calendar"
160
+ size={16}
161
+ color={Colours.TEXT_LIGHT}
162
+ style={styles.dateIcon}
163
+ />
164
+ <Text style={[styles.text, styles.dateText]}>{fieldValue}</Text>
165
+ </View>
166
+ );
167
+
168
+ case FIELD_TYPES.IMAGE:
169
+ case FIELD_TYPES.FEATURE_IMAGE:
170
+ // Images are typically rendered by the layout components
171
+ return null;
172
+
173
+ case FIELD_TYPES.FILE:
174
+ // Handle both single file and multiple files (array)
175
+ const files = Array.isArray(fieldValue) ? fieldValue : [fieldValue];
176
+ return (
177
+ <View style={styles.filesContainer}>
178
+ {files.map((file, index) => {
179
+ // Skip null/undefined files
180
+ if (!file) return null;
181
+
182
+ return (
183
+ <Attachment
184
+ key={index}
185
+ onPress={() => onFieldPress && onFieldPress(fieldId, file)}
186
+ title={file?.name || file?.url?.split("/").pop() || "Document"}
187
+ />
188
+ );
189
+ })}
190
+ </View>
191
+ );
192
+
193
+ default:
194
+ return (
195
+ <Text style={[styles.text, style]} numberOfLines={3}>
196
+ {fieldValue}
197
+ </Text>
198
+ );
199
+ }
200
+ };
201
+
202
+ const styles = StyleSheet.create({
203
+ text: {
204
+ fontFamily: "sf-regular",
205
+ fontSize: 16,
206
+ color: Colours.TEXT_DARK,
207
+ },
208
+ titleText: {
209
+ fontFamily: "sf-semibold",
210
+ fontSize: 16,
211
+ fontWeight: "600",
212
+ },
213
+ descriptionText: {
214
+ fontFamily: "sf-regular",
215
+ fontSize: 14,
216
+ lineHeight: 20,
217
+ marginVertical: 12,
218
+ },
219
+ numberText: {
220
+ fontFamily: "sf-medium",
221
+ fontSize: 16,
222
+ fontWeight: "500",
223
+ },
224
+ linkContainer: {
225
+ flexDirection: "row",
226
+ alignItems: "center",
227
+ paddingVertical: SPACING.XS,
228
+ },
229
+ linkIcon: {
230
+ marginRight: SPACING.SM,
231
+ },
232
+ linkText: {
233
+ fontFamily: "sf-medium",
234
+ fontSize: 16,
235
+ textDecorationLine: "underline",
236
+ },
237
+ dateContainer: {
238
+ flexDirection: "row",
239
+ alignItems: "center",
240
+ paddingVertical: SPACING.XS,
241
+ },
242
+ dateIcon: {
243
+ marginRight: SPACING.SM,
244
+ },
245
+ dateText: {
246
+ fontFamily: "sf-regular",
247
+ fontSize: 14,
248
+ color: Colours.TEXT_LIGHT,
249
+ },
250
+ fileContainer: {
251
+ flexDirection: "row",
252
+ alignItems: "center",
253
+ paddingVertical: SPACING.XS,
254
+ backgroundColor: Colours.BACKGROUND_LIGHT,
255
+ padding: SPACING.SM,
256
+ borderRadius: SPACING.XS,
257
+ },
258
+ fileIcon: {
259
+ marginRight: SPACING.SM,
260
+ },
261
+ fileText: {
262
+ fontFamily: "sf-medium",
263
+ fontSize: 14,
264
+ flex: 1,
265
+ },
266
+ // File container for multiple attachments
267
+ filesContainer: {
268
+ marginVertical: SPACING.XS,
269
+ },
270
+ });
271
+
272
+ export default FieldRenderer;
@@ -0,0 +1,79 @@
1
+ import React from "react";
2
+ import { View, Text, TouchableOpacity, StyleSheet } from "react-native";
3
+ import { Icon } from "react-native-elements";
4
+ import { Colours } from "../core.config";
5
+ import { SPACING } from "../js/spacing";
6
+ import { TEXT_DARK } from "../core.config";
7
+
8
+ const FileDownload = ({ file, onPress, color, style }) => {
9
+ const fileName =
10
+ file?.name ||
11
+ (file?.url && file.url.split("?")[0].split("#")[0].split("/").pop()) ||
12
+ "Document";
13
+ const downloadColor = color || Colours.BRANDING_MAIN;
14
+
15
+ return (
16
+ <TouchableOpacity
17
+ style={[styles.container, style]}
18
+ onPress={() => onPress && onPress(file)}
19
+ >
20
+ <View style={styles.extensionIcon}>
21
+ <Icon
22
+ name="paperclip"
23
+ type="font-awesome"
24
+ size={16}
25
+ color={downloadColor}
26
+ />
27
+ </View>
28
+
29
+ <View style={styles.textContainer}>
30
+ <Text style={[styles.fileName, { color: TEXT_DARK }]} numberOfLines={1}>
31
+ {fileName}
32
+ </Text>
33
+ <Icon
34
+ name="download"
35
+ type="font-awesome"
36
+ size={16}
37
+ color={downloadColor}
38
+ />
39
+ </View>
40
+ </TouchableOpacity>
41
+ );
42
+ };
43
+
44
+ const styles = StyleSheet.create({
45
+ container: {
46
+ flexDirection: "row",
47
+ alignItems: "center",
48
+ paddingVertical: SPACING.SM,
49
+ paddingRight: SPACING.MD,
50
+ backgroundColor: "#ffffff",
51
+ borderRadius: SPACING.XS,
52
+ marginVertical: SPACING.XS / 2,
53
+ },
54
+ extensionIcon: {
55
+ width: 32,
56
+ height: 32,
57
+ borderRadius: SPACING.XS / 2,
58
+ justifyContent: "center",
59
+ alignItems: "center",
60
+ backgroundColor: "#f5f5f5",
61
+ },
62
+ textContainer: {
63
+ flex: 1,
64
+ flexDirection: "row",
65
+ justifyContent: "space-between",
66
+ alignItems: "center",
67
+ },
68
+ fileName: {
69
+ fontFamily: "sf-medium",
70
+ fontSize: 16,
71
+ fontWeight: "600",
72
+ lineHeight: 20,
73
+ flex: 1,
74
+ marginRight: SPACING.SM,
75
+ marginLeft: SPACING.SM,
76
+ },
77
+ });
78
+
79
+ export default FileDownload;