@grupalia/rn-ui-kit 0.15.0 → 0.16.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/lib/commonjs/assets/illustrations/camera.svg +87 -0
- package/lib/commonjs/components/CameraImageInput.js +478 -0
- package/lib/commonjs/components/CameraImageInput.js.map +1 -0
- package/lib/commonjs/components/CameraWrapperModal.js +255 -0
- package/lib/commonjs/components/CameraWrapperModal.js.map +1 -0
- package/lib/commonjs/components/FormikCameraImageInput.js +37 -0
- package/lib/commonjs/components/FormikCameraImageInput.js.map +1 -0
- package/lib/commonjs/components/ImagePickerBottomSheet.js +100 -0
- package/lib/commonjs/components/ImagePickerBottomSheet.js.map +1 -0
- package/lib/commonjs/components/PhotoPickerModal.js +98 -0
- package/lib/commonjs/components/PhotoPickerModal.js.map +1 -0
- package/lib/commonjs/components/Toasts.js +188 -0
- package/lib/commonjs/components/Toasts.js.map +1 -0
- package/lib/commonjs/components/index.js +66 -0
- package/lib/commonjs/components/index.js.map +1 -1
- package/lib/commonjs/components/svgs/Camera.js +17 -0
- package/lib/commonjs/components/svgs/Camera.js.map +1 -0
- package/lib/commonjs/hooks/index.js +41 -0
- package/lib/commonjs/hooks/index.js.map +1 -0
- package/lib/commonjs/hooks/useInternetConnectionStatus.js +182 -0
- package/lib/commonjs/hooks/useInternetConnectionStatus.js.map +1 -0
- package/lib/commonjs/types/svg.d.js +2 -0
- package/lib/commonjs/types/svg.d.js.map +1 -0
- package/lib/commonjs/utils/fileDirectoryUtils.js +19 -0
- package/lib/commonjs/utils/fileDirectoryUtils.js.map +1 -0
- package/lib/commonjs/utils/index.js +22 -0
- package/lib/commonjs/utils/index.js.map +1 -1
- package/lib/commonjs/utils/timeConstants.js +15 -0
- package/lib/commonjs/utils/timeConstants.js.map +1 -0
- package/lib/module/assets/illustrations/camera.svg +87 -0
- package/lib/module/components/CameraImageInput.js +471 -0
- package/lib/module/components/CameraImageInput.js.map +1 -0
- package/lib/module/components/CameraWrapperModal.js +250 -0
- package/lib/module/components/CameraWrapperModal.js.map +1 -0
- package/lib/module/components/FormikCameraImageInput.js +32 -0
- package/lib/module/components/FormikCameraImageInput.js.map +1 -0
- package/lib/module/components/ImagePickerBottomSheet.js +95 -0
- package/lib/module/components/ImagePickerBottomSheet.js.map +1 -0
- package/lib/module/components/PhotoPickerModal.js +93 -0
- package/lib/module/components/PhotoPickerModal.js.map +1 -0
- package/lib/module/components/Toasts.js +182 -0
- package/lib/module/components/Toasts.js.map +1 -0
- package/lib/module/components/index.js +6 -0
- package/lib/module/components/index.js.map +1 -1
- package/lib/module/components/svgs/Camera.js +12 -0
- package/lib/module/components/svgs/Camera.js.map +1 -0
- package/lib/module/hooks/index.js +6 -0
- package/lib/module/hooks/index.js.map +1 -0
- package/lib/module/hooks/useInternetConnectionStatus.js +177 -0
- package/lib/module/hooks/useInternetConnectionStatus.js.map +1 -0
- package/lib/module/types/svg.d.js +2 -0
- package/lib/module/types/svg.d.js.map +1 -0
- package/lib/module/utils/fileDirectoryUtils.js +14 -0
- package/lib/module/utils/fileDirectoryUtils.js.map +1 -0
- package/lib/module/utils/index.js +2 -0
- package/lib/module/utils/index.js.map +1 -1
- package/lib/module/utils/timeConstants.js +12 -0
- package/lib/module/utils/timeConstants.js.map +1 -0
- package/lib/typescript/commonjs/components/CameraImageInput.d.ts +41 -0
- package/lib/typescript/commonjs/components/CameraImageInput.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/CameraWrapperModal.d.ts +18 -0
- package/lib/typescript/commonjs/components/CameraWrapperModal.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/FormikCameraImageInput.d.ts +14 -0
- package/lib/typescript/commonjs/components/FormikCameraImageInput.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/ImagePickerBottomSheet.d.ts +15 -0
- package/lib/typescript/commonjs/components/ImagePickerBottomSheet.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/PhotoPickerModal.d.ts +19 -0
- package/lib/typescript/commonjs/components/PhotoPickerModal.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/Toasts.d.ts +3 -0
- package/lib/typescript/commonjs/components/Toasts.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/index.d.ts +6 -0
- package/lib/typescript/commonjs/components/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/components/svgs/Camera.d.ts +9 -0
- package/lib/typescript/commonjs/components/svgs/Camera.d.ts.map +1 -0
- package/lib/typescript/commonjs/hooks/index.d.ts +4 -0
- package/lib/typescript/commonjs/hooks/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/hooks/useInternetConnectionStatus.d.ts +20 -0
- package/lib/typescript/commonjs/hooks/useInternetConnectionStatus.d.ts.map +1 -0
- package/lib/typescript/commonjs/utils/fileDirectoryUtils.d.ts +3 -0
- package/lib/typescript/commonjs/utils/fileDirectoryUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/utils/index.d.ts +2 -0
- package/lib/typescript/commonjs/utils/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/utils/timeConstants.d.ts +10 -0
- package/lib/typescript/commonjs/utils/timeConstants.d.ts.map +1 -0
- package/lib/typescript/module/components/CameraImageInput.d.ts +41 -0
- package/lib/typescript/module/components/CameraImageInput.d.ts.map +1 -0
- package/lib/typescript/module/components/CameraWrapperModal.d.ts +18 -0
- package/lib/typescript/module/components/CameraWrapperModal.d.ts.map +1 -0
- package/lib/typescript/module/components/FormikCameraImageInput.d.ts +14 -0
- package/lib/typescript/module/components/FormikCameraImageInput.d.ts.map +1 -0
- package/lib/typescript/module/components/ImagePickerBottomSheet.d.ts +15 -0
- package/lib/typescript/module/components/ImagePickerBottomSheet.d.ts.map +1 -0
- package/lib/typescript/module/components/PhotoPickerModal.d.ts +19 -0
- package/lib/typescript/module/components/PhotoPickerModal.d.ts.map +1 -0
- package/lib/typescript/module/components/Toasts.d.ts +3 -0
- package/lib/typescript/module/components/Toasts.d.ts.map +1 -0
- package/lib/typescript/module/components/index.d.ts +6 -0
- package/lib/typescript/module/components/index.d.ts.map +1 -1
- package/lib/typescript/module/components/svgs/Camera.d.ts +9 -0
- package/lib/typescript/module/components/svgs/Camera.d.ts.map +1 -0
- package/lib/typescript/module/hooks/index.d.ts +4 -0
- package/lib/typescript/module/hooks/index.d.ts.map +1 -0
- package/lib/typescript/module/hooks/useInternetConnectionStatus.d.ts +20 -0
- package/lib/typescript/module/hooks/useInternetConnectionStatus.d.ts.map +1 -0
- package/lib/typescript/module/utils/fileDirectoryUtils.d.ts +3 -0
- package/lib/typescript/module/utils/fileDirectoryUtils.d.ts.map +1 -0
- package/lib/typescript/module/utils/index.d.ts +2 -0
- package/lib/typescript/module/utils/index.d.ts.map +1 -1
- package/lib/typescript/module/utils/timeConstants.d.ts +10 -0
- package/lib/typescript/module/utils/timeConstants.d.ts.map +1 -0
- package/package.json +27 -2
- package/src/components/CameraImageInput.tsx +610 -0
- package/src/components/CameraWrapperModal.tsx +309 -0
- package/src/components/FormikCameraImageInput.tsx +39 -0
- package/src/components/ImagePickerBottomSheet.tsx +109 -0
- package/src/components/PhotoPickerModal.tsx +116 -0
- package/src/components/Toasts.tsx +200 -0
- package/src/components/index.ts +6 -0
- package/src/components/svgs/Camera.tsx +14 -0
- package/src/hooks/index.ts +3 -0
- package/src/hooks/useBreakpoints.ts +20 -0
- package/src/hooks/useInternetConnectionStatus.ts +221 -0
- package/src/hooks/useIsAboveBreakpoint.ts +8 -0
- package/src/utils/fileDirectoryUtils.ts +12 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/timeConstants.ts +19 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { useAppState } from '@react-native-community/hooks';
|
|
2
|
+
import { useIsFocused } from '@react-navigation/native';
|
|
3
|
+
import { captureException } from '@sentry/react-native';
|
|
4
|
+
import { useMutation } from '@tanstack/react-query';
|
|
5
|
+
import clsx from 'clsx';
|
|
6
|
+
import { styled } from 'nativewind';
|
|
7
|
+
import {
|
|
8
|
+
ComponentProps, useRef, useState, useCallback,
|
|
9
|
+
} from 'react';
|
|
10
|
+
import toast from 'react-hot-toast/headless';
|
|
11
|
+
import { Linking } from 'react-native';
|
|
12
|
+
import {
|
|
13
|
+
ArrowLongLeftIcon, ArrowPathIcon, ArrowUturnLeftIcon, CheckIcon,
|
|
14
|
+
} from 'react-native-heroicons/outline';
|
|
15
|
+
import {
|
|
16
|
+
PhotoFile,
|
|
17
|
+
Camera as RNVisionCamera,
|
|
18
|
+
useCameraPermission,
|
|
19
|
+
useCameraDevice,
|
|
20
|
+
useCameraFormat,
|
|
21
|
+
FormatFilter,
|
|
22
|
+
CameraPosition,
|
|
23
|
+
} from 'react-native-vision-camera';
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
View, Modal, Pressable, Image,
|
|
27
|
+
} from '../hoc-components';
|
|
28
|
+
import BaseButton from './BaseButton';
|
|
29
|
+
import BaseIcon from './BaseIcon';
|
|
30
|
+
import BaseText from './BaseText';
|
|
31
|
+
import PressableOpacity from './PressableOpacity';
|
|
32
|
+
import { useIsAboveBreakpoint } from '../hooks/useIsAboveBreakpoint';
|
|
33
|
+
import { ensureDirExists } from '../utils/fileDirectoryUtils';
|
|
34
|
+
import CameraSvg from './svgs/Camera';
|
|
35
|
+
import Toasts from './Toasts';
|
|
36
|
+
|
|
37
|
+
interface PermissionCardProps {
|
|
38
|
+
onPermissionRequestPress: () => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type CameraWrapperModalImage = PhotoFile & { uri: string };
|
|
42
|
+
|
|
43
|
+
function PermissionCard({ onPermissionRequestPress }: PermissionCardProps) {
|
|
44
|
+
const largePhone = useIsAboveBreakpoint('xs');
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<View className={clsx(
|
|
48
|
+
'w-full flex-col items-center rounded-lg bg-white p-4',
|
|
49
|
+
largePhone ? 'mt-10 space-y-10' : 'space-y-4',
|
|
50
|
+
)}
|
|
51
|
+
>
|
|
52
|
+
<BaseText className={clsx('text-center font-semibold', largePhone && 'text-xl')}>
|
|
53
|
+
Necesitamos permiso para acceder a la camara
|
|
54
|
+
</BaseText>
|
|
55
|
+
<CameraSvg
|
|
56
|
+
width="100%"
|
|
57
|
+
height={largePhone ? 200 : 100}
|
|
58
|
+
/>
|
|
59
|
+
<BaseButton
|
|
60
|
+
className="w-full"
|
|
61
|
+
size={largePhone ? 'md' : 'xs'}
|
|
62
|
+
text="Permitir"
|
|
63
|
+
onPress={onPermissionRequestPress}
|
|
64
|
+
/>
|
|
65
|
+
</View>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface CameraPreviewProps {
|
|
70
|
+
onCapture: (photoFile: CameraWrapperModalImage) => void;
|
|
71
|
+
previewOverlay: JSX.Element | undefined;
|
|
72
|
+
filters: FormatFilter[];
|
|
73
|
+
allowDeviceSwitching: boolean;
|
|
74
|
+
isActive: boolean;
|
|
75
|
+
initialDevice: CameraPosition;
|
|
76
|
+
pathToSave?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function CameraPreview({
|
|
80
|
+
onCapture,
|
|
81
|
+
previewOverlay,
|
|
82
|
+
filters,
|
|
83
|
+
isActive,
|
|
84
|
+
allowDeviceSwitching,
|
|
85
|
+
initialDevice,
|
|
86
|
+
pathToSave,
|
|
87
|
+
}: CameraPreviewProps) {
|
|
88
|
+
const cameraRef = useRef<RNVisionCamera>(null);
|
|
89
|
+
const [selectedDevice, setSelectedDevice] = useState<CameraPosition>(initialDevice);
|
|
90
|
+
const device = useCameraDevice(selectedDevice);
|
|
91
|
+
const format = useCameraFormat(device, filters);
|
|
92
|
+
|
|
93
|
+
const { mutate } = useMutation<PhotoFile, Error, void>({
|
|
94
|
+
mutationFn: async () => {
|
|
95
|
+
if (cameraRef.current) {
|
|
96
|
+
if (pathToSave) await ensureDirExists(pathToSave);
|
|
97
|
+
|
|
98
|
+
const file = await cameraRef.current.takePhoto({ path: pathToSave?.replace('file://', '') ?? undefined });
|
|
99
|
+
|
|
100
|
+
return file;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return Promise.reject(new Error('Camera ref is not defined'));
|
|
104
|
+
},
|
|
105
|
+
networkMode: 'always',
|
|
106
|
+
onSuccess: (photoFile) => onCapture({ ...photoFile, uri: `file://${photoFile.path}` }),
|
|
107
|
+
onError: (error) => {
|
|
108
|
+
captureException(error);
|
|
109
|
+
toast.error('Ocurrió un error al tomar la foto');
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return (
|
|
114
|
+
<View className="flex h-full w-full flex-col">
|
|
115
|
+
{device ? (
|
|
116
|
+
<View className="relative h-full w-full flex-1 overflow-hidden rounded-xl border-2">
|
|
117
|
+
<RNVisionCamera
|
|
118
|
+
style={{ flex: 1, width: '100%', height: '100%' }}
|
|
119
|
+
ref={cameraRef}
|
|
120
|
+
device={device}
|
|
121
|
+
format={format}
|
|
122
|
+
isActive={isActive}
|
|
123
|
+
photo
|
|
124
|
+
/>
|
|
125
|
+
{previewOverlay}
|
|
126
|
+
{allowDeviceSwitching && (
|
|
127
|
+
<PressableOpacity
|
|
128
|
+
onPress={() => setSelectedDevice(selectedDevice === 'back' ? 'front' : 'back')}
|
|
129
|
+
className="absolute bottom-8 right-8 flex h-12 w-12 items-center justify-center rounded-full bg-secondary"
|
|
130
|
+
>
|
|
131
|
+
<BaseIcon
|
|
132
|
+
icon={ArrowPathIcon}
|
|
133
|
+
className="h-full w-full"
|
|
134
|
+
color="utility-gray-200"
|
|
135
|
+
/>
|
|
136
|
+
</PressableOpacity>
|
|
137
|
+
)}
|
|
138
|
+
</View>
|
|
139
|
+
) : (
|
|
140
|
+
<View className="flex h-full w-full items-center justify-center bg-utility-gray-200">
|
|
141
|
+
<BaseText className="text-secondary">
|
|
142
|
+
No fue posible acceder a la camara
|
|
143
|
+
</BaseText>
|
|
144
|
+
</View>
|
|
145
|
+
)}
|
|
146
|
+
<PressableOpacity
|
|
147
|
+
onPress={() => mutate()}
|
|
148
|
+
className="my-10 h-20 w-20 self-center rounded-full border-4 border-white bg-black p-1"
|
|
149
|
+
>
|
|
150
|
+
<View
|
|
151
|
+
className="h-full w-full rounded-full bg-white"
|
|
152
|
+
/>
|
|
153
|
+
</PressableOpacity>
|
|
154
|
+
</View>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
interface PhotoFilePreviewProps {
|
|
159
|
+
photoFile: CameraWrapperModalImage;
|
|
160
|
+
onConfirm: () => void;
|
|
161
|
+
onCancel: () => void;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function PhotoFilePreview({ photoFile, onConfirm, onCancel }: PhotoFilePreviewProps) {
|
|
165
|
+
return (
|
|
166
|
+
<View className="flex h-full w-full flex-col items-center pb-10">
|
|
167
|
+
<View className="w-full flex-1 overflow-hidden rounded-xl">
|
|
168
|
+
<Image
|
|
169
|
+
style={{ width: 'auto', height: '100%' }}
|
|
170
|
+
source={{ uri: photoFile.uri }}
|
|
171
|
+
resizeMode="cover"
|
|
172
|
+
/>
|
|
173
|
+
</View>
|
|
174
|
+
<View className="mt-12 flex flex-row items-center space-x-16">
|
|
175
|
+
<Pressable
|
|
176
|
+
onPress={onCancel}
|
|
177
|
+
>
|
|
178
|
+
<BaseIcon
|
|
179
|
+
icon={ArrowUturnLeftIcon}
|
|
180
|
+
size={64}
|
|
181
|
+
color="fg-error-primary"
|
|
182
|
+
/>
|
|
183
|
+
</Pressable>
|
|
184
|
+
<Pressable
|
|
185
|
+
onPress={onConfirm}
|
|
186
|
+
className="self-end"
|
|
187
|
+
>
|
|
188
|
+
<BaseIcon
|
|
189
|
+
icon={CheckIcon}
|
|
190
|
+
size={64}
|
|
191
|
+
color="fg-success-primary"
|
|
192
|
+
/>
|
|
193
|
+
</Pressable>
|
|
194
|
+
</View>
|
|
195
|
+
</View>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
interface Props extends ComponentProps<typeof Modal> {
|
|
200
|
+
onCapture: (photoFile: CameraWrapperModalImage) => void;
|
|
201
|
+
onClose: () => void;
|
|
202
|
+
filters?: FormatFilter[];
|
|
203
|
+
previewOverlay?: JSX.Element;
|
|
204
|
+
allowDeviceSwitching?: boolean;
|
|
205
|
+
initialDevice?: CameraPosition;
|
|
206
|
+
pathToSave?: string;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function CameraWrapperModal({
|
|
210
|
+
onCapture,
|
|
211
|
+
onClose,
|
|
212
|
+
filters = [{ photoResolution: { width: 720, height: 1280 } }],
|
|
213
|
+
previewOverlay = undefined,
|
|
214
|
+
allowDeviceSwitching = true,
|
|
215
|
+
initialDevice = 'back',
|
|
216
|
+
pathToSave = undefined,
|
|
217
|
+
...modalProps
|
|
218
|
+
}: Props) {
|
|
219
|
+
const [photoFile, setPhotoFile] = useState<CameraWrapperModalImage | null>(null);
|
|
220
|
+
const [permissionRequested, setPermissionRequested] = useState(false);
|
|
221
|
+
|
|
222
|
+
const isFocused = useIsFocused();
|
|
223
|
+
const appState = useAppState();
|
|
224
|
+
const isActive = isFocused && appState === 'active';
|
|
225
|
+
|
|
226
|
+
const { hasPermission, requestPermission } = useCameraPermission();
|
|
227
|
+
|
|
228
|
+
const handlePermissionRequestPress = useCallback(() => {
|
|
229
|
+
if (!permissionRequested) {
|
|
230
|
+
setPermissionRequested(true);
|
|
231
|
+
requestPermission();
|
|
232
|
+
} else {
|
|
233
|
+
Linking.openSettings();
|
|
234
|
+
}
|
|
235
|
+
}, [permissionRequested, requestPermission]);
|
|
236
|
+
|
|
237
|
+
const handleCaptureConfirm = useCallback(() => {
|
|
238
|
+
if (photoFile) {
|
|
239
|
+
onCapture({
|
|
240
|
+
...photoFile,
|
|
241
|
+
uri: `file://${photoFile.path}`,
|
|
242
|
+
});
|
|
243
|
+
setPhotoFile(null);
|
|
244
|
+
}
|
|
245
|
+
}, [onCapture, photoFile]);
|
|
246
|
+
|
|
247
|
+
const removePhotoFile = useCallback(() => {
|
|
248
|
+
setPhotoFile(null);
|
|
249
|
+
}, [setPhotoFile]);
|
|
250
|
+
|
|
251
|
+
let content;
|
|
252
|
+
|
|
253
|
+
if (!hasPermission) {
|
|
254
|
+
content = (
|
|
255
|
+
<PermissionCard
|
|
256
|
+
onPermissionRequestPress={handlePermissionRequestPress}
|
|
257
|
+
/>
|
|
258
|
+
);
|
|
259
|
+
} else if (photoFile) {
|
|
260
|
+
content = (
|
|
261
|
+
<PhotoFilePreview
|
|
262
|
+
photoFile={photoFile}
|
|
263
|
+
onCancel={removePhotoFile}
|
|
264
|
+
onConfirm={handleCaptureConfirm}
|
|
265
|
+
/>
|
|
266
|
+
);
|
|
267
|
+
} else {
|
|
268
|
+
content = (
|
|
269
|
+
<CameraPreview
|
|
270
|
+
isActive={isActive}
|
|
271
|
+
filters={filters}
|
|
272
|
+
onCapture={setPhotoFile}
|
|
273
|
+
previewOverlay={previewOverlay}
|
|
274
|
+
allowDeviceSwitching={allowDeviceSwitching}
|
|
275
|
+
initialDevice={initialDevice}
|
|
276
|
+
pathToSave={pathToSave}
|
|
277
|
+
/>
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return (
|
|
282
|
+
<Modal
|
|
283
|
+
animationType="slide"
|
|
284
|
+
statusBarTranslucent
|
|
285
|
+
{...modalProps}
|
|
286
|
+
>
|
|
287
|
+
<Toasts />
|
|
288
|
+
<View className="flex h-full w-full flex-col items-center bg-black px-4 pt-12">
|
|
289
|
+
<View className="self-start py-4">
|
|
290
|
+
<Pressable
|
|
291
|
+
hitSlop={10}
|
|
292
|
+
onPress={onClose}
|
|
293
|
+
>
|
|
294
|
+
<BaseIcon
|
|
295
|
+
icon={ArrowLongLeftIcon}
|
|
296
|
+
color="fg-brand-primary"
|
|
297
|
+
size={32}
|
|
298
|
+
/>
|
|
299
|
+
</Pressable>
|
|
300
|
+
</View>
|
|
301
|
+
<View className="mb-4 w-full flex-1">
|
|
302
|
+
{content}
|
|
303
|
+
</View>
|
|
304
|
+
</View>
|
|
305
|
+
</Modal>
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export default styled(CameraWrapperModal);
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { ErrorMessage, useField } from 'formik';
|
|
2
|
+
import { styled } from 'nativewind';
|
|
3
|
+
import React from 'react';
|
|
4
|
+
import { ViewProps } from 'react-native';
|
|
5
|
+
|
|
6
|
+
import BaseText from './BaseText';
|
|
7
|
+
import CameraImageInput, { CameraImageInputProps } from './CameraImageInput';
|
|
8
|
+
|
|
9
|
+
interface FormikCameraImageInputProps extends Omit<CameraImageInputProps, 'value' | 'onValueChange' | 'hasError'>, ViewProps {
|
|
10
|
+
name: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function FormikCameraImageInput({
|
|
14
|
+
name,
|
|
15
|
+
...props
|
|
16
|
+
}: FormikCameraImageInputProps) {
|
|
17
|
+
const [field, meta, helpers] = useField(name);
|
|
18
|
+
|
|
19
|
+
const hasError = !!(meta.error && meta.touched);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<>
|
|
23
|
+
<CameraImageInput
|
|
24
|
+
value={field.value}
|
|
25
|
+
onValueChange={helpers.setValue}
|
|
26
|
+
hasError={hasError}
|
|
27
|
+
showImages
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
{hasError && (
|
|
31
|
+
<ErrorMessage name={name}>
|
|
32
|
+
{(msg) => <BaseText className="mt-1 text-sm text-error-primary">{msg}</BaseText>}
|
|
33
|
+
</ErrorMessage>
|
|
34
|
+
)}
|
|
35
|
+
</>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export default styled(FormikCameraImageInput);
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { BottomSheetModal } from '@gorhom/bottom-sheet';
|
|
2
|
+
import {
|
|
3
|
+
ComponentProps,
|
|
4
|
+
useState, useRef, useEffect, useCallback,
|
|
5
|
+
} from 'react';
|
|
6
|
+
|
|
7
|
+
import BaseBottomSheetModal from './BaseBottomSheetModal';
|
|
8
|
+
import BaseText from './BaseText';
|
|
9
|
+
import CameraWrapperModal, { CameraWrapperModalImage } from './CameraWrapperModal';
|
|
10
|
+
import PhotoPickerModal, { PhotoPickerModalImage } from './PhotoPickerModal';
|
|
11
|
+
import PressableOpacity from './PressableOpacity';
|
|
12
|
+
import { View } from '../hoc-components';
|
|
13
|
+
|
|
14
|
+
export type ImagePickerBottomSheetImage = PhotoPickerModalImage | CameraWrapperModalImage;
|
|
15
|
+
|
|
16
|
+
interface Props extends Partial<ComponentProps<typeof BaseBottomSheetModal>> {
|
|
17
|
+
open: boolean;
|
|
18
|
+
onImagePick: (image: ImagePickerBottomSheetImage) => void;
|
|
19
|
+
onDismiss: () => void;
|
|
20
|
+
galleryAllowed?: boolean;
|
|
21
|
+
cameraWrapperProps?: Omit<ComponentProps<typeof CameraWrapperModal>, 'onCapture' | 'onClose'>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export default function ImagePickerBottomSheet({
|
|
25
|
+
open,
|
|
26
|
+
onImagePick,
|
|
27
|
+
onDismiss,
|
|
28
|
+
galleryAllowed = true,
|
|
29
|
+
cameraWrapperProps = {},
|
|
30
|
+
...props
|
|
31
|
+
}: Props) {
|
|
32
|
+
const bottomSheetModalRef = useRef<BottomSheetModal>(null);
|
|
33
|
+
const [isCameraModalOpen, setIsCameraModalOpen] = useState(false);
|
|
34
|
+
const [isPhotoPickerModalOpen, setIsPhotoPickerModalOpen] = useState(false);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (open) {
|
|
38
|
+
if (galleryAllowed) {
|
|
39
|
+
bottomSheetModalRef.current?.present();
|
|
40
|
+
} else {
|
|
41
|
+
setIsCameraModalOpen(true);
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
bottomSheetModalRef.current?.dismiss();
|
|
45
|
+
setIsCameraModalOpen(false);
|
|
46
|
+
setIsPhotoPickerModalOpen(false);
|
|
47
|
+
}
|
|
48
|
+
}, [open, galleryAllowed]);
|
|
49
|
+
|
|
50
|
+
const handleClose = useCallback(() => {
|
|
51
|
+
setIsCameraModalOpen(false);
|
|
52
|
+
setIsPhotoPickerModalOpen(false);
|
|
53
|
+
onDismiss();
|
|
54
|
+
}, [onDismiss]);
|
|
55
|
+
|
|
56
|
+
const onPhotoPick = useCallback((image: PhotoPickerModalImage) => {
|
|
57
|
+
onImagePick(image);
|
|
58
|
+
}, [onImagePick]);
|
|
59
|
+
|
|
60
|
+
const onCameraImagePick = useCallback((image: CameraWrapperModalImage) => {
|
|
61
|
+
onImagePick(({
|
|
62
|
+
uri: `file://${image.path}`,
|
|
63
|
+
width: image.width,
|
|
64
|
+
height: image.height,
|
|
65
|
+
}));
|
|
66
|
+
}, [onImagePick]);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<>
|
|
70
|
+
<BaseBottomSheetModal
|
|
71
|
+
enableDynamicSizing
|
|
72
|
+
ref={bottomSheetModalRef}
|
|
73
|
+
onDismiss={handleClose}
|
|
74
|
+
{...props}
|
|
75
|
+
>
|
|
76
|
+
<View className="flex flex-col items-center space-y-8 py-8">
|
|
77
|
+
<PressableOpacity
|
|
78
|
+
className="flex w-1/2 flex-row items-center justify-between space-x-4 p-2"
|
|
79
|
+
onPress={() => setIsCameraModalOpen(true)}
|
|
80
|
+
>
|
|
81
|
+
<BaseText className="text-xl">Tomar foto</BaseText>
|
|
82
|
+
<BaseText className="text-xl">📸</BaseText>
|
|
83
|
+
</PressableOpacity>
|
|
84
|
+
<PressableOpacity
|
|
85
|
+
className="flex w-1/2 flex-row items-center justify-between space-x-4 p-2"
|
|
86
|
+
onPress={() => setIsPhotoPickerModalOpen(true)}
|
|
87
|
+
>
|
|
88
|
+
<BaseText className="text-xl">Abrir galería</BaseText>
|
|
89
|
+
<BaseText className="text-xl">🌅</BaseText>
|
|
90
|
+
</PressableOpacity>
|
|
91
|
+
</View>
|
|
92
|
+
</BaseBottomSheetModal>
|
|
93
|
+
<PhotoPickerModal
|
|
94
|
+
visible={isPhotoPickerModalOpen}
|
|
95
|
+
onImagePick={onPhotoPick}
|
|
96
|
+
onClose={handleClose}
|
|
97
|
+
onRequestClose={handleClose}
|
|
98
|
+
/>
|
|
99
|
+
<CameraWrapperModal
|
|
100
|
+
visible={isCameraModalOpen}
|
|
101
|
+
className="h-full w-full"
|
|
102
|
+
onCapture={onCameraImagePick}
|
|
103
|
+
onClose={handleClose}
|
|
104
|
+
onRequestClose={handleClose}
|
|
105
|
+
{...cameraWrapperProps}
|
|
106
|
+
/>
|
|
107
|
+
</>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { styled } from 'nativewind';
|
|
2
|
+
import { ComponentProps, useEffect, useState } from 'react';
|
|
3
|
+
import toast from 'react-hot-toast/headless';
|
|
4
|
+
import {
|
|
5
|
+
launchImageLibrary, PhotoQuality, Asset,
|
|
6
|
+
} from 'react-native-image-picker';
|
|
7
|
+
|
|
8
|
+
import BaseButton from './BaseButton';
|
|
9
|
+
import BaseText from './BaseText';
|
|
10
|
+
import { View, Modal, Image } from '../hoc-components';
|
|
11
|
+
import PressableOpacity from './PressableOpacity';
|
|
12
|
+
|
|
13
|
+
export type PhotoPickerModalImage = Asset & { uri: string };
|
|
14
|
+
|
|
15
|
+
interface Props extends ComponentProps<typeof Modal> {
|
|
16
|
+
onImagePick: (image: PhotoPickerModalImage) => void;
|
|
17
|
+
onClose: () => void;
|
|
18
|
+
imageOptions?: {
|
|
19
|
+
maxWidth?: number;
|
|
20
|
+
maxHeight?: number;
|
|
21
|
+
quality?: PhotoQuality;
|
|
22
|
+
includeBase64?: boolean;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function MediaLibraryModal({
|
|
27
|
+
onImagePick,
|
|
28
|
+
onClose,
|
|
29
|
+
visible,
|
|
30
|
+
imageOptions = {
|
|
31
|
+
maxWidth: 1024,
|
|
32
|
+
maxHeight: 1024,
|
|
33
|
+
quality: 0.8,
|
|
34
|
+
includeBase64: false,
|
|
35
|
+
},
|
|
36
|
+
...props
|
|
37
|
+
}: Props) {
|
|
38
|
+
const [image, setImage] = useState<PhotoPickerModalImage | undefined>();
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
const handleImageSelect = async () => {
|
|
42
|
+
try {
|
|
43
|
+
const result = await launchImageLibrary({
|
|
44
|
+
mediaType: 'photo',
|
|
45
|
+
selectionLimit: 1,
|
|
46
|
+
...imageOptions,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (result.didCancel) {
|
|
50
|
+
onClose();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!result.assets?.length) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const asset = result.assets[0];
|
|
59
|
+
if (!asset.uri) {
|
|
60
|
+
toast.error('Error al seleccionar la imagen');
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
setImage({
|
|
65
|
+
uri: asset.uri,
|
|
66
|
+
type: asset.type,
|
|
67
|
+
width: asset.width,
|
|
68
|
+
height: asset.height,
|
|
69
|
+
});
|
|
70
|
+
} catch {
|
|
71
|
+
toast.error('Error al seleccionar la imagen');
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
if (visible) {
|
|
76
|
+
handleImageSelect();
|
|
77
|
+
}
|
|
78
|
+
}, [visible]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<Modal
|
|
82
|
+
visible={visible}
|
|
83
|
+
{...props}
|
|
84
|
+
>
|
|
85
|
+
<View className="flex h-full flex-col px-4 py-8">
|
|
86
|
+
<PressableOpacity
|
|
87
|
+
onPress={onClose}
|
|
88
|
+
hitSlop={6}
|
|
89
|
+
className="mb-4 self-end"
|
|
90
|
+
>
|
|
91
|
+
<BaseText className="font-medium text-gray-700">
|
|
92
|
+
Cancelar
|
|
93
|
+
</BaseText>
|
|
94
|
+
</PressableOpacity>
|
|
95
|
+
{image && (
|
|
96
|
+
<>
|
|
97
|
+
<View className="w-full flex-1 overflow-hidden rounded-xl">
|
|
98
|
+
<Image
|
|
99
|
+
className="h-full w-auto"
|
|
100
|
+
source={{ uri: image.uri }}
|
|
101
|
+
resizeMode="contain"
|
|
102
|
+
/>
|
|
103
|
+
</View>
|
|
104
|
+
<BaseButton
|
|
105
|
+
text="Confirmar"
|
|
106
|
+
onPress={() => onImagePick(image)}
|
|
107
|
+
className="mt-4 w-full self-center"
|
|
108
|
+
/>
|
|
109
|
+
</>
|
|
110
|
+
)}
|
|
111
|
+
</View>
|
|
112
|
+
</Modal>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export default styled(MediaLibraryModal);
|