@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.
- package/dist/module/actions/featureBuilderActions.js +106 -0
- package/dist/module/actions/featureBuilderActions.js.map +1 -0
- package/dist/module/actions/featureBuilderStringsActions.js +106 -0
- package/dist/module/actions/featureBuilderStringsActions.js.map +1 -0
- package/dist/module/actions/index.js +12 -0
- package/dist/module/actions/index.js.map +1 -0
- package/dist/module/actions/types.js +7 -0
- package/dist/module/actions/types.js.map +1 -0
- package/dist/module/components/FeatureDetailScreen.js +725 -0
- package/dist/module/components/FeatureDetailScreen.js.map +1 -0
- package/dist/module/components/FeatureListItem.js +174 -0
- package/dist/module/components/FeatureListItem.js.map +1 -0
- package/dist/module/components/FeatureListScreen.js +159 -0
- package/dist/module/components/FeatureListScreen.js.map +1 -0
- package/dist/module/components/FieldRenderer.js +218 -0
- package/dist/module/components/FieldRenderer.js.map +1 -0
- package/dist/module/components/FileDownload.js +74 -0
- package/dist/module/components/FileDownload.js.map +1 -0
- package/dist/module/components/WidgetGrid.js +158 -0
- package/dist/module/components/WidgetGrid.js.map +1 -0
- package/dist/module/components/WidgetLarge.js +274 -0
- package/dist/module/components/WidgetLarge.js.map +1 -0
- package/dist/module/components/WidgetSmall.js +315 -0
- package/dist/module/components/WidgetSmall.js.map +1 -0
- package/dist/module/components/common/index.js +25 -0
- package/dist/module/components/common/index.js.map +1 -0
- package/dist/module/components/layouts/CondensedList.js +195 -0
- package/dist/module/components/layouts/CondensedList.js.map +1 -0
- package/dist/module/components/layouts/FeatureImageList.js +172 -0
- package/dist/module/components/layouts/FeatureImageList.js.map +1 -0
- package/dist/module/components/layouts/RoundImageList.js +198 -0
- package/dist/module/components/layouts/RoundImageList.js.map +1 -0
- package/dist/module/components/layouts/SquareImageList.js +185 -0
- package/dist/module/components/layouts/SquareImageList.js.map +1 -0
- package/dist/module/config/index.js +10 -0
- package/dist/module/config/index.js.map +1 -0
- package/dist/module/core.config.js +17 -0
- package/dist/module/core.config.js.map +1 -0
- package/dist/module/feature.config.js +113 -0
- package/dist/module/feature.config.js.map +1 -0
- package/dist/module/index.js +24 -0
- package/dist/module/index.js.map +1 -0
- package/dist/module/js/Colors.js +25 -0
- package/dist/module/js/Colors.js.map +1 -0
- package/dist/module/js/FieldTypes.js +123 -0
- package/dist/module/js/FieldTypes.js.map +1 -0
- package/dist/module/js/NavigationService.js +10 -0
- package/dist/module/js/NavigationService.js.map +1 -0
- package/dist/module/js/Styles.js +3 -0
- package/dist/module/js/Styles.js.map +1 -0
- package/dist/module/js/helpers.js +29 -0
- package/dist/module/js/helpers.js.map +1 -0
- package/dist/module/js/index.js +24 -0
- package/dist/module/js/index.js.map +1 -0
- package/dist/module/js/spacing.js +29 -0
- package/dist/module/js/spacing.js.map +1 -0
- package/dist/module/js/types.js +254 -0
- package/dist/module/js/types.js.map +1 -0
- package/dist/module/reducers/featureBuilderReducer.js +75 -0
- package/dist/module/reducers/featureBuilderReducer.js.map +1 -0
- package/dist/module/utils/featureSelectors.js +9 -0
- package/dist/module/utils/featureSelectors.js.map +1 -0
- package/dist/module/values.config.a.js +96 -0
- package/dist/module/values.config.a.js.map +1 -0
- package/dist/module/values.config.b.js +96 -0
- package/dist/module/values.config.b.js.map +1 -0
- package/dist/module/values.config.c.js +96 -0
- package/dist/module/values.config.c.js.map +1 -0
- package/dist/module/values.config.d.js +96 -0
- package/dist/module/values.config.d.js.map +1 -0
- package/dist/module/values.config.js +96 -0
- package/dist/module/values.config.js.map +1 -0
- package/dist/module/webapi/featureBuilderAPI.js +59 -0
- package/dist/module/webapi/featureBuilderAPI.js.map +1 -0
- package/dist/module/webapi/helper.js +4 -0
- package/dist/module/webapi/helper.js.map +1 -0
- package/dist/module/webapi/index.js +8 -0
- package/dist/module/webapi/index.js.map +1 -0
- package/package.json +62 -0
- package/src/actions/featureBuilderActions.js +112 -0
- package/src/actions/featureBuilderStringsActions.js +114 -0
- package/src/actions/index.js +12 -0
- package/src/actions/types.js +7 -0
- package/src/components/FeatureDetailScreen.js +817 -0
- package/src/components/FeatureListItem.js +198 -0
- package/src/components/FeatureListScreen.js +160 -0
- package/src/components/FieldRenderer.js +272 -0
- package/src/components/FileDownload.js +79 -0
- package/src/components/WidgetGrid.js +181 -0
- package/src/components/WidgetLarge.js +305 -0
- package/src/components/WidgetSmall.js +344 -0
- package/src/components/common/index.js +25 -0
- package/src/components/layouts/CondensedList.js +230 -0
- package/src/components/layouts/FeatureImageList.js +193 -0
- package/src/components/layouts/RoundImageList.js +219 -0
- package/src/components/layouts/SquareImageList.js +205 -0
- package/src/config/index.js +10 -0
- package/src/core.config.js +29 -0
- package/src/feature.config.js +127 -0
- package/src/index.js +27 -0
- package/src/js/Colors.js +30 -0
- package/src/js/FieldTypes.js +131 -0
- package/src/js/NavigationService.js +12 -0
- package/src/js/Styles.js +3 -0
- package/src/js/helpers.js +30 -0
- package/src/js/index.js +24 -0
- package/src/js/spacing.js +30 -0
- package/src/js/types.js +253 -0
- package/src/reducers/featureBuilderReducer.js +64 -0
- package/src/utils/featureSelectors.js +8 -0
- package/src/values.config.a.js +104 -0
- package/src/values.config.b.js +104 -0
- package/src/values.config.c.js +104 -0
- package/src/values.config.d.js +104 -0
- package/src/values.config.js +104 -0
- package/src/webapi/featureBuilderAPI.js +65 -0
- package/src/webapi/helper.js +4 -0
- 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;
|