ota-components-module 1.3.0

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 (36) hide show
  1. package/README.md +179 -0
  2. package/assets/images/ic_camera.svg +3 -0
  3. package/assets/images/ic_close.svg +8 -0
  4. package/assets/images/ic_folder.svg +3 -0
  5. package/assets/images/placeholder.png +0 -0
  6. package/expo-env.d.ts +7 -0
  7. package/mri-manifest.json +10 -0
  8. package/package.json +28 -0
  9. package/src/button/ThemedButton.tsx +120 -0
  10. package/src/feedback/ActivityLoader.tsx +84 -0
  11. package/src/feedback/CustomAlert.tsx +143 -0
  12. package/src/feedback/DeleteImageConfirmationDialog.tsx +58 -0
  13. package/src/feedback/ProgressBar.tsx +58 -0
  14. package/src/image/ImagePickerBottomSheet.tsx +61 -0
  15. package/src/image/ImagePickerView.tsx +103 -0
  16. package/src/image/MultipleImagePreview.tsx +424 -0
  17. package/src/image/StackedImage.tsx +155 -0
  18. package/src/index.ts +68 -0
  19. package/src/input/CustomDropdown.tsx +142 -0
  20. package/src/input/CustomInput.tsx +101 -0
  21. package/src/input/FormField.tsx +358 -0
  22. package/src/input/KeyboardScrollView.tsx +131 -0
  23. package/src/input/SearchViewInput.tsx +183 -0
  24. package/src/layout/BottomSheetDialog.tsx +208 -0
  25. package/src/layout/BottomTwoButtonLayoutComponent.tsx +153 -0
  26. package/src/layout/CardView.tsx +101 -0
  27. package/src/layout/PropertyHeaderComponent.tsx +110 -0
  28. package/src/list/SearchableList.tsx +273 -0
  29. package/src/models/PropertyImage.ts +20 -0
  30. package/src/typography/Label.tsx +225 -0
  31. package/src/utils/BaseStyle.ts +46 -0
  32. package/src/utils/Strings.ts +1 -0
  33. package/src/utils/TextConstants.ts +24 -0
  34. package/src/utils/Utils.ts +11 -0
  35. package/src/webbaseview/WebBaseView.tsx +26 -0
  36. package/tsconfig.json +9 -0
@@ -0,0 +1,58 @@
1
+ import React from 'react';
2
+ import { View, StyleSheet } from 'react-native';
3
+ import { Colors } from '../utils/BaseStyle';
4
+
5
+ interface ProgressBarProps {
6
+ progress: number; // Value between 0 and 1
7
+ maxValue?: number; // Maximum value (default: 1)
8
+ minValue?: number; // Minimum value (default: 0)
9
+ height?: number; // Height of the progress bar
10
+ backgroundColor?: string; // Background color of the progress bar
11
+ progressColor?: string; // Color of the progress indicator
12
+ borderRadius?: number; // Border radius of the progress bar
13
+ }
14
+
15
+ /**
16
+ * A progress bar component that visualizes progress as a horizontal bar
17
+ */
18
+ const ProgressBar: React.FC<ProgressBarProps> = ({
19
+ progress,
20
+ maxValue = 1,
21
+ minValue = 0,
22
+ height = 6,
23
+ backgroundColor = Colors.lightGrayColor,
24
+ progressColor = '#6CB7D8',
25
+ borderRadius = 3,
26
+ }) => {
27
+
28
+ // Calculate the width percentage based on progress, minValue, and maxValue
29
+ const normalizedProgress = Math.max(0, Math.min(1, (progress - minValue) / (maxValue - minValue)));
30
+ const width = `${normalizedProgress * 100}%`;
31
+
32
+ return (
33
+ <View style={[styles.container, { height, backgroundColor, borderRadius }]}>
34
+ <View
35
+ style={[
36
+ styles.progressIndicator,
37
+ {
38
+ width,
39
+ backgroundColor: progressColor,
40
+ borderRadius,
41
+ },
42
+ ]}
43
+ />
44
+ </View>
45
+ );
46
+ };
47
+
48
+ const styles = StyleSheet.create({
49
+ container: {
50
+ width: '100%',
51
+ overflow: 'hidden',
52
+ },
53
+ progressIndicator: {
54
+ height: '100%',
55
+ },
56
+ });
57
+
58
+ export default ProgressBar;
@@ -0,0 +1,61 @@
1
+ import React, { useRef } from 'react';
2
+ import ImagePickerView from './ImagePickerView';
3
+ import BottomSheetDialog from '../layout/BottomSheetDialog';
4
+
5
+ interface ImagePickerBottomSheetProps {
6
+ visible: boolean;
7
+ onClose: () => void;
8
+ onTakePhoto: () => void;
9
+ onUploadFile: () => void;
10
+ title?: string;
11
+ }
12
+
13
+ const ImagePickerBottomSheet: React.FC<ImagePickerBottomSheetProps> = ({
14
+ visible,
15
+ onClose,
16
+ onTakePhoto,
17
+ onUploadFile,
18
+ title,
19
+ }) => {
20
+
21
+ const ref: any = useRef(null);
22
+
23
+ const TakePhoto = () => {
24
+ closeSheet();
25
+
26
+ setTimeout(() => {
27
+ onTakePhoto();
28
+ }, 1000);
29
+ }
30
+
31
+ const UploadFile = () => {
32
+ closeSheet();
33
+
34
+ setTimeout(() => {
35
+ onUploadFile();
36
+ }, 1000);
37
+ }
38
+
39
+ const closeSheet = () => {
40
+ if(ref.current){
41
+ ref.current.closeBottomSheet()
42
+ }
43
+ }
44
+ return (
45
+ <BottomSheetDialog
46
+ visible={visible}
47
+ onClose={onClose}
48
+ height={200} // Adjust height as needed
49
+ ref={ref}
50
+ >
51
+ <ImagePickerView
52
+ title={title}
53
+ onTakePhoto={TakePhoto}
54
+ onUploadFile={UploadFile}
55
+ onClose={closeSheet}
56
+ />
57
+ </BottomSheetDialog>
58
+ );
59
+ };
60
+
61
+ export default ImagePickerBottomSheet;
@@ -0,0 +1,103 @@
1
+ import React, { useRef } from 'react';
2
+ import {
3
+ View,
4
+ Text,
5
+ StyleSheet,
6
+ TouchableOpacity,
7
+ ViewStyle,
8
+ } from 'react-native';
9
+ import CameraIcon from '../../assets/images/ic_camera.svg';
10
+ import FolderIcon from '../../assets/images/ic_folder.svg';
11
+ import CloseIcon from '../../assets/images/ic_close.svg';
12
+ import Label from '../typography/Label';
13
+ import { TextSize, TextWeight } from '../utils/TextConstants';
14
+ import { Colors } from '../utils/BaseStyle';
15
+
16
+ interface ImagePickerViewProps {
17
+ onTakePhoto: () => void;
18
+ onUploadFile: () => void;
19
+ onClose?: () => void;
20
+ containerStyle?: ViewStyle;
21
+ title?: string;
22
+ }
23
+
24
+ const ImagePickerView: React.FC<ImagePickerViewProps> = ({
25
+ onTakePhoto,
26
+ onUploadFile,
27
+ onClose,
28
+ containerStyle,
29
+ title = 'Add an Image',
30
+ }) => {
31
+
32
+ return (
33
+ <View style={[styles.container, containerStyle]}>
34
+ <View style={styles.header}>
35
+ <Label
36
+ text={title}
37
+ size={TextSize.LARGE}
38
+ weight={TextWeight.BOLD}
39
+ textColorType="primary"
40
+ />
41
+ {onClose && (
42
+ <TouchableOpacity onPress={onClose} style={styles.closeButton}>
43
+ <CloseIcon width={24} height={24} />
44
+ </TouchableOpacity>
45
+ )}
46
+ </View>
47
+
48
+ <TouchableOpacity
49
+ style={styles.option}
50
+ onPress={onTakePhoto}
51
+ activeOpacity={0.7}
52
+ >
53
+ <CameraIcon width={24} height={24} style={styles.icon} />
54
+ <Label text="Take a photo" size={TextSize.NORMAL} weight={TextWeight.NORMAL} textColorType="primary" />
55
+ </TouchableOpacity>
56
+
57
+ <View style={styles.divider} />
58
+
59
+ <TouchableOpacity
60
+ style={styles.option}
61
+ onPress={onUploadFile}
62
+ activeOpacity={0.7}
63
+ >
64
+ <FolderIcon width={24} height={24} style={styles.icon} />
65
+ <Label text="Upload a file" size={TextSize.NORMAL} weight={TextWeight.NORMAL} textColorType="primary" />
66
+ </TouchableOpacity>
67
+ </View>
68
+ );
69
+ };
70
+
71
+ const styles = StyleSheet.create({
72
+ container: {
73
+ backgroundColor: Colors.whiteColor,
74
+ borderRadius: 12,
75
+ overflow: 'hidden',
76
+ },
77
+ header: {
78
+ flexDirection: 'row',
79
+ justifyContent: 'space-between',
80
+ alignItems: 'center',
81
+ paddingStart: 16,
82
+ paddingBottom: 16
83
+ },
84
+ closeButton: {
85
+ padding: 4,
86
+ },
87
+ option: {
88
+ flexDirection: 'row',
89
+ alignItems: 'center',
90
+ paddingVertical: 16,
91
+ paddingHorizontal: 16,
92
+ },
93
+ icon: {
94
+ marginRight: 16,
95
+ },
96
+ divider: {
97
+ height: 1,
98
+ backgroundColor: Colors.lightGrayColor,
99
+ marginHorizontal: 16,
100
+ },
101
+ });
102
+
103
+ export default ImagePickerView;
@@ -0,0 +1,424 @@
1
+ import { Ionicons } from '@expo/vector-icons';
2
+ import React, { useState, useRef, useEffect, useMemo } from 'react';
3
+ import {
4
+ View,
5
+ TouchableOpacity,
6
+ StyleSheet,
7
+ Modal,
8
+ Dimensions,
9
+ FlatList,
10
+ SafeAreaView,
11
+ Platform,
12
+ Text,
13
+ } from 'react-native';
14
+ import { Image } from 'expo-image';
15
+ import Checkbox from 'expo-checkbox';
16
+ import { Colors } from '../utils/BaseStyle';
17
+ import { useSafeAreaInsets } from "react-native-safe-area-context";
18
+ import { PropertyImage } from '../models/PropertyImage';
19
+ import BottomSheetDialog, { BottomSheetDialogRef } from '../layout/BottomSheetDialog';
20
+ import DeleteImageConfirmationDialog from '../feedback/DeleteImageConfirmationDialog';
21
+ import { delete_image_confirmation_msg } from '../utils/Strings';
22
+
23
+ // Get screen dimensions
24
+ const { width: SCREEN_WIDTH, height: SCREEN_HEIGHT } = Dimensions.get('window');
25
+
26
+ // Check if we're running on web
27
+ const isWeb = Platform.OS === 'web';
28
+
29
+ type ImageSource = {
30
+ uri: PropertyImage;
31
+ isLocal?: boolean;
32
+ };
33
+
34
+ type MultipleImagePreviewProps = {
35
+ /**
36
+ * Array of image URIs to display. Can be local file paths or server URLs.
37
+ */
38
+ images: PropertyImage[];
39
+ /**
40
+ * Initial image index to display
41
+ */
42
+ initialIndex?: number;
43
+ /**
44
+ * Callback when the preview is closed
45
+ */
46
+ onClose?: () => void;
47
+ /**
48
+ * Callback when an image is deleted
49
+ */
50
+ onDelete?: (index: number) => void;
51
+ /**
52
+ * Callback when an image is set as default
53
+ */
54
+ OnMarkDefault?: (index: number, value: boolean) => void;
55
+ /**
56
+ * Whether to show the delete button
57
+ */
58
+ showDeleteButton?: boolean;
59
+ };
60
+
61
+ /**
62
+ * MultipleImagePreview component for displaying images
63
+ */
64
+ const MultipleImagePreview: React.FC<MultipleImagePreviewProps> = ({
65
+ images,
66
+ initialIndex = 0,
67
+ onClose,
68
+ onDelete,
69
+ OnMarkDefault,
70
+ showDeleteButton = true,
71
+ }) => {
72
+ const [activeIndex, setActiveIndex] = useState(initialIndex);
73
+ const [visible, setVisible] = useState(true);
74
+ const flatListRef = useRef<FlatList>(null);
75
+ const bottomSheetRef = useRef<BottomSheetDialogRef>(null);
76
+ const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
77
+
78
+ const insets = useSafeAreaInsets();
79
+
80
+ // Calculate dimensions based on platform
81
+ const dimensions = useMemo(() => {
82
+ // For web, we need to ensure each item takes exactly one screen width
83
+ return {
84
+ itemWidth: SCREEN_WIDTH,
85
+ imageWidth: isWeb ? SCREEN_WIDTH - 40 : SCREEN_WIDTH - 40,
86
+ imageHeight: isWeb ? SCREEN_HEIGHT * 0.6 : SCREEN_HEIGHT * 0.4
87
+ };
88
+ }, []);
89
+
90
+ const closeSheet = () => {
91
+ if (bottomSheetRef.current) {
92
+ bottomSheetRef.current.closeBottomSheet();
93
+ }
94
+ };
95
+
96
+ // Close the bottom sheet
97
+ const closeBottomSheet = () => {
98
+ closeSheet();
99
+
100
+ setTimeout(() => {
101
+ setShowDeleteConfirmation(false);
102
+ }, 300);
103
+ };
104
+
105
+ // Create styles with the calculated dimensions
106
+ const styles = previewStyles(insets.top, dimensions);
107
+
108
+ // Process images to handle both local and remote URLs
109
+ const processedImages = useMemo(() => {
110
+ return images.map((image) => {
111
+ // Check if the image is a local file path or a server URL
112
+ const isLocalImage = image.imageBlobUrl.startsWith('data:') || image.imageBlobUrl.startsWith('/');
113
+ return { uri: image, isLocal: isLocalImage };
114
+ });
115
+ }, [images]);
116
+
117
+ // Scroll to initial index on mount
118
+ useEffect(() => {
119
+ if (flatListRef.current) {
120
+ setActiveIndex(initialIndex);
121
+ flatListRef.current.scrollToIndex({
122
+ index: initialIndex,
123
+ animated: false,
124
+ });
125
+ }
126
+ }, [initialIndex]);
127
+
128
+ const handleClose = () => {
129
+ setVisible(false);
130
+ if (onClose) {
131
+ onClose();
132
+ }
133
+ };
134
+
135
+ const handleDelete = () => {
136
+ if (onDelete) {
137
+ onDelete(activeIndex);
138
+ }
139
+ };
140
+
141
+ // Handle scroll end to update active index
142
+ const handleScrollEnd = (event: any) => {
143
+ const contentOffsetX = event.nativeEvent.contentOffset.x;
144
+ const newIndex = Math.round(contentOffsetX / dimensions.itemWidth);
145
+ if (newIndex !== activeIndex && newIndex >= 0 && newIndex < images.length) {
146
+ setActiveIndex(newIndex);
147
+ }
148
+ };
149
+
150
+ // Function to handle manual navigation between images
151
+ const goToImage = (index: number) => {
152
+ if (flatListRef.current && index >= 0 && index < images.length) {
153
+ flatListRef.current.scrollToIndex({
154
+ index,
155
+ animated: true,
156
+ });
157
+ setActiveIndex(index);
158
+ }
159
+ };
160
+
161
+ // Render each image item
162
+ const renderItem = ({ item }: { item: ImageSource }) => {
163
+ return (
164
+ <View style={styles.itemContainer}>
165
+ {OnMarkDefault && (
166
+ <View style={styles.imageDefaultTextSection}>
167
+ <Checkbox style={{borderColor: 'white', width: 20, height: 20}} value={item.uri.isDefault} onValueChange={(value: boolean) => {
168
+ if(OnMarkDefault) {
169
+ OnMarkDefault(activeIndex, value);
170
+ }
171
+ }} />
172
+ <Text style={styles.imageDefaultText}>Mark as Default</Text>
173
+ </View>
174
+ )}
175
+
176
+ <Image
177
+ key={item.uri.imageBlobUrl}
178
+ source={item.uri.imageBlobUrl}
179
+ style={styles.image}
180
+ contentFit="cover"
181
+ placeholderContentFit='cover'
182
+ transition={500}
183
+ cachePolicy="disk"
184
+ placeholder={require("../../assets/images/placeholder.png")}
185
+ />
186
+
187
+ {/* Delete button overlay on the image */}
188
+ {showDeleteButton && onDelete && item.uri.isDefault !== true && (
189
+ <TouchableOpacity
190
+ style={styles.imageDeleteButton}
191
+ onPress={() => {
192
+ if(item.isLocal) {
193
+ handleDelete();
194
+ } else {
195
+ setShowDeleteConfirmation(true)
196
+ }
197
+ }}
198
+ activeOpacity={0.7}
199
+ >
200
+ <Ionicons name="trash-outline" size={20} color="red" />
201
+ </TouchableOpacity>
202
+ )}
203
+ </View>
204
+ );
205
+ };
206
+
207
+ // Render header with close button
208
+ const renderHeader = () => (
209
+ <View style={styles.headerContainer}>
210
+ <View style={{ flex: 1 }} />
211
+ <TouchableOpacity
212
+ style={styles.closeButton}
213
+ onPress={handleClose}
214
+ activeOpacity={0.7}
215
+ >
216
+ <Ionicons name="close-outline" size={30} color={Colors.whiteColor} />
217
+ </TouchableOpacity>
218
+ </View>
219
+ );
220
+
221
+ // Render footer with pagination dots
222
+ const renderFooter = () => (
223
+ <View style={styles.footerContainer}>
224
+ <View style={styles.dotsContainer}>
225
+ {images.map((_, index) => (
226
+ <TouchableOpacity
227
+ key={index}
228
+ style={[
229
+ styles.dot,
230
+ index === activeIndex && styles.activeDot
231
+ ]}
232
+ onPress={() => goToImage(index)}
233
+ activeOpacity={0.7}
234
+ />
235
+ ))}
236
+ </View>
237
+ </View>
238
+ );
239
+
240
+ // If not visible, don't render anything
241
+ if (!visible) {
242
+ return null;
243
+ }
244
+
245
+ return (
246
+ <Modal
247
+ visible={visible}
248
+ transparent={true}
249
+ animationType="slide"
250
+ onRequestClose={handleClose}
251
+ >
252
+ <SafeAreaView style={styles.container}>
253
+ <View style={styles.galleryContainer}>
254
+ {/* Image gallery */}
255
+ <FlatList
256
+ ref={flatListRef}
257
+ data={processedImages}
258
+ renderItem={renderItem}
259
+ horizontal
260
+ pagingEnabled
261
+ showsHorizontalScrollIndicator={false}
262
+ initialNumToRender={processedImages.length}
263
+ maxToRenderPerBatch={3}
264
+ windowSize={5}
265
+ keyExtractor={(_, index) => `image-${index}`}
266
+ onMomentumScrollEnd={handleScrollEnd}
267
+ style={styles.flatList}
268
+ contentContainerStyle={styles.flatListContent}
269
+ snapToInterval={dimensions.itemWidth}
270
+ snapToAlignment="center"
271
+ decelerationRate="fast"
272
+ getItemLayout={(_, index) => ({
273
+ length: dimensions.itemWidth,
274
+ offset: dimensions.itemWidth * index,
275
+ index,
276
+ })}
277
+ removeClippedSubviews={!isWeb} // Disable on web for better performance
278
+ scrollEventThrottle={16}
279
+ />
280
+ </View>
281
+
282
+ {/* Header overlay */}
283
+ {renderHeader()}
284
+ {renderFooter()}
285
+
286
+ <BottomSheetDialog
287
+ ref={bottomSheetRef}
288
+ visible={showDeleteConfirmation}
289
+ onClose={closeBottomSheet}
290
+ height={200}
291
+ >
292
+ <DeleteImageConfirmationDialog
293
+ message={delete_image_confirmation_msg}
294
+ onConfirm={async () => {
295
+ closeBottomSheet();
296
+ handleDelete();
297
+ }}
298
+ onCancel={closeBottomSheet}
299
+ />
300
+ </BottomSheetDialog>
301
+ </SafeAreaView>
302
+ </Modal>
303
+ );
304
+ };
305
+
306
+ const previewStyles = (top: number, dimensions?: { itemWidth: number, imageWidth: number, imageHeight: number }) => StyleSheet.create({
307
+ container: {
308
+ flex: 1,
309
+ backgroundColor: '#000000CF',
310
+ justifyContent: 'center',
311
+ },
312
+ galleryContainer: {
313
+ justifyContent: 'center',
314
+ alignItems: 'center',
315
+ },
316
+ flatList: {
317
+ width: SCREEN_WIDTH,
318
+ ...(isWeb && {
319
+ scrollSnapType: 'x mandatory', // Web-specific for better snapping
320
+ WebkitOverflowScrolling: 'touch', // Better scrolling on iOS Safari
321
+ }),
322
+ },
323
+ flatListContent: {
324
+ alignItems: 'center',
325
+ ...(isWeb && {
326
+ display: 'flex',
327
+ flexDirection: 'row',
328
+ }),
329
+ },
330
+ itemContainer: {
331
+ width: dimensions?.itemWidth || SCREEN_WIDTH,
332
+ justifyContent: 'center',
333
+ alignItems: 'center',
334
+ paddingHorizontal: 20,
335
+ },
336
+ image: {
337
+ width: dimensions?.imageWidth || SCREEN_WIDTH - 40,
338
+ height: dimensions?.imageHeight || SCREEN_HEIGHT * 0.4,
339
+ resizeMode: 'cover',
340
+ borderRadius: 20,
341
+ borderColor: Colors.whiteColor,
342
+ borderWidth: 1,
343
+ shadowColor: Colors.shadowColor,
344
+ shadowOffset: { width: 0, height: 2 },
345
+ shadowOpacity: 0.1,
346
+ shadowRadius: 4,
347
+ elevation: Platform.OS === 'android' ? 4 : 0,
348
+ },
349
+ headerContainer: {
350
+ position: 'absolute',
351
+ top: top,
352
+ right: 0,
353
+ left: 0,
354
+ height: 60,
355
+ paddingHorizontal: 15,
356
+ flexDirection: 'row',
357
+ justifyContent: 'flex-end', // Align to right side
358
+ alignItems: 'center',
359
+ zIndex: 100,
360
+ backgroundColor: "transparent",
361
+ },
362
+ closeButton: {
363
+ justifyContent: 'center',
364
+ alignItems: 'center',
365
+ },
366
+ imageDefaultTextSection: {
367
+ flexDirection: 'row',
368
+ position: 'absolute',
369
+ top: 5,
370
+ left: 20,
371
+ borderRadius: 10,
372
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
373
+ justifyContent: 'center',
374
+ alignItems: 'center',
375
+ zIndex: 10,
376
+ margin: 8,
377
+ paddingVertical: 12,
378
+ paddingHorizontal: 16
379
+ },
380
+ imageDefaultText: {
381
+ color: 'white',
382
+ alignSelf: 'center',
383
+ textAlign: 'center',
384
+ marginStart: 12
385
+ },
386
+ imageDeleteButton: {
387
+ position: 'absolute',
388
+ top: 15,
389
+ right: 30,
390
+ width: 40,
391
+ height: 40,
392
+ borderRadius: 20,
393
+ backgroundColor: 'rgba(0, 0, 0, 0.5)',
394
+ justifyContent: 'center',
395
+ alignItems: 'center',
396
+ zIndex: 10,
397
+ },
398
+ footerContainer: {
399
+ height: 60,
400
+ justifyContent: 'center',
401
+ alignItems: 'center',
402
+ zIndex: 100,
403
+ },
404
+ dotsContainer: {
405
+ flexDirection: 'row',
406
+ justifyContent: 'center',
407
+ alignItems: 'center',
408
+ },
409
+ dot: {
410
+ width: 8,
411
+ height: 8,
412
+ borderRadius: 4,
413
+ backgroundColor: '#FFFFFF',
414
+ marginHorizontal: 4,
415
+ },
416
+ activeDot: {
417
+ backgroundColor: '#007AC6',
418
+ width: 10,
419
+ height: 10,
420
+ borderRadius: 5,
421
+ },
422
+ });
423
+
424
+ export default MultipleImagePreview;