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.
- package/README.md +179 -0
- package/assets/images/ic_camera.svg +3 -0
- package/assets/images/ic_close.svg +8 -0
- package/assets/images/ic_folder.svg +3 -0
- package/assets/images/placeholder.png +0 -0
- package/expo-env.d.ts +7 -0
- package/mri-manifest.json +10 -0
- package/package.json +28 -0
- package/src/button/ThemedButton.tsx +120 -0
- package/src/feedback/ActivityLoader.tsx +84 -0
- package/src/feedback/CustomAlert.tsx +143 -0
- package/src/feedback/DeleteImageConfirmationDialog.tsx +58 -0
- package/src/feedback/ProgressBar.tsx +58 -0
- package/src/image/ImagePickerBottomSheet.tsx +61 -0
- package/src/image/ImagePickerView.tsx +103 -0
- package/src/image/MultipleImagePreview.tsx +424 -0
- package/src/image/StackedImage.tsx +155 -0
- package/src/index.ts +68 -0
- package/src/input/CustomDropdown.tsx +142 -0
- package/src/input/CustomInput.tsx +101 -0
- package/src/input/FormField.tsx +358 -0
- package/src/input/KeyboardScrollView.tsx +131 -0
- package/src/input/SearchViewInput.tsx +183 -0
- package/src/layout/BottomSheetDialog.tsx +208 -0
- package/src/layout/BottomTwoButtonLayoutComponent.tsx +153 -0
- package/src/layout/CardView.tsx +101 -0
- package/src/layout/PropertyHeaderComponent.tsx +110 -0
- package/src/list/SearchableList.tsx +273 -0
- package/src/models/PropertyImage.ts +20 -0
- package/src/typography/Label.tsx +225 -0
- package/src/utils/BaseStyle.ts +46 -0
- package/src/utils/Strings.ts +1 -0
- package/src/utils/TextConstants.ts +24 -0
- package/src/utils/Utils.ts +11 -0
- package/src/webbaseview/WebBaseView.tsx +26 -0
- 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;
|