@boneframework/native-components 1.0.2 → 1.0.5
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 +4 -0
- package/Screens.ts +4 -0
- package/components/ActivityIndicator.js +48 -0
- package/components/Animation.js +21 -0
- package/components/ApiInterceptor.js +104 -0
- package/components/Button.js +40 -0
- package/components/Card.js +56 -0
- package/components/CategoryPickerItem.js +31 -0
- package/components/DateTimePicker.js +12 -0
- package/components/Icon.js +28 -0
- package/components/Image.js +61 -0
- package/components/ImageInput.tsx +97 -0
- package/components/ImageInputList.tsx +34 -0
- package/components/ListItemDeleteAction.tsx +30 -0
- package/components/ListItemFlipswitch.tsx +60 -0
- package/components/ListItemSeparator.tsx +23 -0
- package/components/ListItemSwipable.tsx +64 -0
- package/components/OfflineNotice.tsx +36 -0
- package/components/Picker.tsx +87 -0
- package/components/PickerItem.tsx +20 -0
- package/components/PickerItemComponent.tsx +14 -0
- package/components/RoundIconButton.tsx +36 -0
- package/components/Screen.tsx +26 -0
- package/components/Text.tsx +15 -0
- package/components/TextInput.tsx +39 -0
- package/components/forms/ErrorMessage.js +20 -0
- package/components/forms/Form.js +21 -0
- package/components/forms/FormDateTimePicker.js +31 -0
- package/components/forms/FormField.js +26 -0
- package/components/forms/FormImagePicker.js +30 -0
- package/components/forms/FormPicker.js +30 -0
- package/components/forms/SubmitButton.js +14 -0
- package/components/forms/index.js +7 -0
- package/hooks/useApi.js +20 -0
- package/package.json +4 -2
- package/screens/RegisterScreen.tsx +87 -0
- package/screens/WelcomeScreen.tsx +50 -0
- package/src/Screens.ts +0 -1
- /package/{src/Bone.ts → Bone.ts} +0 -0
- /package/{src/Components.ts → Components.ts} +0 -0
- /package/{src/Contexts.ts → Contexts.ts} +0 -0
- /package/{src/Hooks.ts → Hooks.ts} +0 -0
- /package/{src/Utilities.ts → Utilities.ts} +0 -0
- /package/{src/api → api}/client.js +0 -0
- /package/{src/api → api}/expoPushTokens.js +0 -0
- /package/{src/api → api}/notifications.js +0 -0
- /package/{src/api → api}/ping.js +0 -0
- /package/{src/api → api}/users.js +0 -0
- /package/{src/components → components}/SessionProvider.tsx +0 -0
- /package/{src/contexts → contexts}/auth.js +0 -0
- /package/{src/hooks → hooks}/useAuth.js +0 -0
- /package/{src/utilities → utilities}/authStorage.js +0 -0
package/README.md
CHANGED
package/Screens.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React, {useEffect, useRef} from 'react';
|
|
2
|
+
import {View, StyleSheet} from "react-native";
|
|
3
|
+
|
|
4
|
+
import Animation from "./Animation";
|
|
5
|
+
import useStyle from "../hooks/useStyle";
|
|
6
|
+
|
|
7
|
+
function ActivityIndicator({ visible = false , type="default"}) {
|
|
8
|
+
const defaultStyles = useStyle();
|
|
9
|
+
|
|
10
|
+
const styles = StyleSheet.create({
|
|
11
|
+
overlay: {
|
|
12
|
+
flex:1,
|
|
13
|
+
justifyContent: 'center',
|
|
14
|
+
alignItems: 'center',
|
|
15
|
+
backgroundColor: defaultStyles.backgroundColor,
|
|
16
|
+
height: '100%',
|
|
17
|
+
position: 'absolute',
|
|
18
|
+
width: '100%',
|
|
19
|
+
zIndex: 1,
|
|
20
|
+
opacity: 0.8
|
|
21
|
+
},
|
|
22
|
+
default: {
|
|
23
|
+
flex: 1,
|
|
24
|
+
justifyContent: 'center',
|
|
25
|
+
alignItems: 'center'
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
if (!visible) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const style = type === 'default' ? styles.default : styles.overlay;
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<View style={style}>
|
|
37
|
+
<Animation
|
|
38
|
+
source={require('../assets/animations/loader.json')}
|
|
39
|
+
autoPlay={true}
|
|
40
|
+
loop={true}
|
|
41
|
+
style={{height: 100, width: 100, opacity: 1}}
|
|
42
|
+
speed={1.5}
|
|
43
|
+
/>
|
|
44
|
+
</View>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export default ActivityIndicator;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React, {useEffect, useRef} from 'react';
|
|
2
|
+
import LottieView from "lottie-react-native";
|
|
3
|
+
|
|
4
|
+
function Animation({source, style, onAnimationFinish, autoPlay = true, loop = true, speed = 1.5}) {
|
|
5
|
+
|
|
6
|
+
const lottieRef = useRef(null);
|
|
7
|
+
|
|
8
|
+
return(
|
|
9
|
+
<LottieView
|
|
10
|
+
source={source}
|
|
11
|
+
autoPlay={autoPlay}
|
|
12
|
+
loop={loop}
|
|
13
|
+
style={style}
|
|
14
|
+
speed={speed}
|
|
15
|
+
onAnimationFinish={onAnimationFinish}
|
|
16
|
+
ref={lottieRef}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export default Animation;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import React, {useEffect} from 'react';
|
|
2
|
+
|
|
3
|
+
import apiClient from "../api/client";
|
|
4
|
+
import authStorage from "../auth/storage";
|
|
5
|
+
import settings from "../config/settings";
|
|
6
|
+
import useAuth from '../hooks/useAuth';
|
|
7
|
+
|
|
8
|
+
// call to refresh an access token using our refresh token
|
|
9
|
+
const refreshToken = async (token) => {
|
|
10
|
+
const formData = new FormData();
|
|
11
|
+
formData.append('client_id', settings.clientId);
|
|
12
|
+
formData.append('grant_type', 'refresh_token');
|
|
13
|
+
formData.append('refresh_token', token);
|
|
14
|
+
formData.append('scope', 'basic');
|
|
15
|
+
|
|
16
|
+
const result = await apiClient.post(settings.discovery.tokenEndpoint, formData, {
|
|
17
|
+
headers: {'Content-Type': 'multipart/form-data'}
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const newToken = {
|
|
21
|
+
accessToken: result.data.access_token,
|
|
22
|
+
expiresIn: result.data.expires_in,
|
|
23
|
+
refreshToken: result.data.refresh_token,
|
|
24
|
+
tokenType: result.data.token_type,
|
|
25
|
+
};
|
|
26
|
+
authStorage.storeAuthToken(newToken);
|
|
27
|
+
|
|
28
|
+
return newToken;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function ApiInterceptor(props) {
|
|
32
|
+
const {user, logout} = useAuth();
|
|
33
|
+
let refreshing = null;
|
|
34
|
+
|
|
35
|
+
const addTransformers = () => {
|
|
36
|
+
|
|
37
|
+
const requestTransformers = apiClient.asyncRequestTransforms.length;
|
|
38
|
+
const responseTransformers = apiClient.asyncResponseTransforms.length;
|
|
39
|
+
const transformersAdded = requestTransformers + responseTransformers > 1;
|
|
40
|
+
|
|
41
|
+
if (!transformersAdded) {
|
|
42
|
+
apiClient.addAsyncRequestTransform(async request => {
|
|
43
|
+
const authToken = await authStorage.getAuthToken();
|
|
44
|
+
|
|
45
|
+
if (!authToken) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (settings.xDebugHeader === true) {
|
|
50
|
+
if (!request.params) {
|
|
51
|
+
request.params = [];
|
|
52
|
+
}
|
|
53
|
+
request.params['XDEBUG_SESSION'] = 'PHPSTORM';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
request.headers['Authorization'] = 'Bearer ' + authToken.accessToken;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// check for a 401 response (expired access token), use refresh token to fetch new access token, retry request
|
|
60
|
+
apiClient.addAsyncResponseTransform(async response => {
|
|
61
|
+
if (response.ok) {
|
|
62
|
+
return response.data;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (response.problem) {
|
|
66
|
+
const originalConfig = response.config;
|
|
67
|
+
|
|
68
|
+
//Access Token was expired, grab a fresh one using the refresh token and try again
|
|
69
|
+
if (originalConfig.url !== settings.discovery.authEndpoint && response.status === 401 && !originalConfig.retry) {
|
|
70
|
+
// settimng retry flag to allow retrying once and not loop infinitely
|
|
71
|
+
originalConfig.retry = true;
|
|
72
|
+
try {
|
|
73
|
+
const token = await authStorage.getAuthToken();
|
|
74
|
+
if (token) {
|
|
75
|
+
// first request to refresh will call the method, all the other requests will await the promise
|
|
76
|
+
// so only one call to refresh will be made in the case of multile async 401s
|
|
77
|
+
refreshing = refreshing ? refreshing : refreshToken(token.refreshToken);
|
|
78
|
+
await refreshing;
|
|
79
|
+
refreshing = null;
|
|
80
|
+
|
|
81
|
+
return apiClient.any(originalConfig);
|
|
82
|
+
} else {
|
|
83
|
+
return logout();
|
|
84
|
+
}
|
|
85
|
+
} catch (_error) {
|
|
86
|
+
// if we get here, the refresh token has also expired, log the user out.
|
|
87
|
+
return logout();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return Promise.reject(response.problem);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
useEffect( () => {
|
|
98
|
+
addTransformers();
|
|
99
|
+
}, []);
|
|
100
|
+
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export default ApiInterceptor;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {View, StyleSheet, Platform, TouchableOpacity, TouchableHighlight} from "react-native";
|
|
3
|
+
|
|
4
|
+
import Text from '../components/Text'
|
|
5
|
+
import colors from '../config/colors'
|
|
6
|
+
import defaultStyles from '../config/styles'
|
|
7
|
+
|
|
8
|
+
function Button({title, onPress, color, textColor}) {
|
|
9
|
+
return (
|
|
10
|
+
<TouchableHighlight style={[styles.roundbutton, {
|
|
11
|
+
backgroundColor: color ? colors[color]: styles.roundbutton.color,
|
|
12
|
+
}]} onPress={onPress}>
|
|
13
|
+
<Text style={[styles.text, {
|
|
14
|
+
color: textColor ? colors[textColor] : styles.text.color}]
|
|
15
|
+
}>{title}</Text>
|
|
16
|
+
</TouchableHighlight>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const styles = StyleSheet.create({
|
|
21
|
+
roundbutton: {
|
|
22
|
+
width: '100%',
|
|
23
|
+
height: 70,
|
|
24
|
+
borderRadius: 35,
|
|
25
|
+
backgroundColor: colors.primary,
|
|
26
|
+
justifyContent: 'center',
|
|
27
|
+
alignItems: 'center',
|
|
28
|
+
marginVertical: 10,
|
|
29
|
+
color: colors.black
|
|
30
|
+
},
|
|
31
|
+
text: {
|
|
32
|
+
fontFamily: defaultStyles.text.fontFamily,
|
|
33
|
+
color: colors.white,
|
|
34
|
+
fontSize: 18,
|
|
35
|
+
textTransform: 'uppercase',
|
|
36
|
+
fontWeight: 'bold'
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export default Button;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {StyleSheet, TouchableWithoutFeedback, View} from "react-native";
|
|
3
|
+
import {Image} from 'react-native-expo-image-cache';
|
|
4
|
+
|
|
5
|
+
import Text from './Text'
|
|
6
|
+
import colors from '../config/colors'
|
|
7
|
+
import useStyle from "../hooks/useStyle";
|
|
8
|
+
|
|
9
|
+
function Card({title, subtitle, imageUrl, onPress, thumbnaiilUrl}) {
|
|
10
|
+
const style = useStyle();
|
|
11
|
+
|
|
12
|
+
const styles = StyleSheet.create({
|
|
13
|
+
card: {
|
|
14
|
+
borderRadius: 15,
|
|
15
|
+
backgroundColor: style.box.backgroundColor,
|
|
16
|
+
marginBottom: 20,
|
|
17
|
+
overflow: "hidden"
|
|
18
|
+
},
|
|
19
|
+
image: {
|
|
20
|
+
width: '100%',
|
|
21
|
+
height: 200,
|
|
22
|
+
},
|
|
23
|
+
detailsContainer: {
|
|
24
|
+
padding: 20,
|
|
25
|
+
},
|
|
26
|
+
title: {
|
|
27
|
+
color: style.text.color
|
|
28
|
+
},
|
|
29
|
+
subtitle: {
|
|
30
|
+
color: style.errorText.color
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<TouchableWithoutFeedback onPress={onPress} >
|
|
36
|
+
<View style={styles.card}>
|
|
37
|
+
<Image
|
|
38
|
+
style={styles.image}
|
|
39
|
+
uri={imageUrl}
|
|
40
|
+
preview={{uri: thumbnaiilUrl}}
|
|
41
|
+
tint={'light'}
|
|
42
|
+
/>
|
|
43
|
+
<View style={styles.detailsContainer}>
|
|
44
|
+
<Text style={styles.title} numberOfLines={1}>
|
|
45
|
+
{title}
|
|
46
|
+
</Text>
|
|
47
|
+
<Text style={styles.subtitle} numberOfLines={5}>
|
|
48
|
+
{subtitle}
|
|
49
|
+
</Text>
|
|
50
|
+
</View>
|
|
51
|
+
</View>
|
|
52
|
+
</TouchableWithoutFeedback>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default Card;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {View, StyleSheet, TouchableOpacity} from "react-native";
|
|
3
|
+
|
|
4
|
+
import Text from '../components/Text'
|
|
5
|
+
import Icon from '../components/Icon'
|
|
6
|
+
import useStyle from "../hooks/useStyle";
|
|
7
|
+
|
|
8
|
+
function CategoryPickerItem({item, onPress}) {
|
|
9
|
+
const style = useStyle();
|
|
10
|
+
|
|
11
|
+
const styles = StyleSheet.create({
|
|
12
|
+
container: {
|
|
13
|
+
flex: 1,
|
|
14
|
+
paddingHorizontal: 20,
|
|
15
|
+
paddingVertical: 15,
|
|
16
|
+
alignItems: 'center',
|
|
17
|
+
justifyContent: 'center'
|
|
18
|
+
},
|
|
19
|
+
label: {
|
|
20
|
+
paddingTop: 5,
|
|
21
|
+
textAlign: 'center',
|
|
22
|
+
color: style.text.color
|
|
23
|
+
}
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
return <TouchableOpacity onPress={onPress} style={styles.container}>
|
|
27
|
+
<Icon backgroundColor={item.backgroundColor} name={item.icon} size={80}/>
|
|
28
|
+
<Text style={styles.label}>{item.label}</Text>
|
|
29
|
+
</TouchableOpacity>;
|
|
30
|
+
}
|
|
31
|
+
export default CategoryPickerItem;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {StyleSheet, View} from "react-native";
|
|
3
|
+
import RNDateTimePicker from "@react-native-community/datetimepicker";
|
|
4
|
+
|
|
5
|
+
function DateTimePicker({ ...props } ) {
|
|
6
|
+
|
|
7
|
+
return (
|
|
8
|
+
<RNDateTimePicker {...props} />
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default DateTimePicker;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {View, StyleSheet} from "react-native";
|
|
3
|
+
import {MaterialCommunityIcons} from "@expo/vector-icons";
|
|
4
|
+
|
|
5
|
+
function Icon({name, size = 40, backgroundColor, borderRadius , iconColor = 'white'}) {
|
|
6
|
+
borderRadius = borderRadius ? borderRadius : size /2;
|
|
7
|
+
|
|
8
|
+
const styles = StyleSheet.create({
|
|
9
|
+
icon: {
|
|
10
|
+
width: size,
|
|
11
|
+
height: size,
|
|
12
|
+
borderRadius: borderRadius,
|
|
13
|
+
backgroundColor: backgroundColor,
|
|
14
|
+
justifyContent: "center",
|
|
15
|
+
alignItems: "center"
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<View style={styles.icon}>
|
|
21
|
+
<MaterialCommunityIcons name={name} color={iconColor} size={size / 2} />
|
|
22
|
+
</View>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
export default Icon;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import React, {useContext, useEffect, useState} from 'react';
|
|
2
|
+
import {Image as RNImage, StyleSheet, View} from "react-native";
|
|
3
|
+
|
|
4
|
+
import storage from '../auth/storage';
|
|
5
|
+
import * as Notifications from "expo-notifications";
|
|
6
|
+
import useAuth from "../hooks/useAuth";
|
|
7
|
+
import authStorage from "../auth/storage";
|
|
8
|
+
import settings from '../config/api';
|
|
9
|
+
import AuthContext from "../auth/context";
|
|
10
|
+
|
|
11
|
+
function Image({style, uri, onPress, handleError, source}) {
|
|
12
|
+
const {user, setUser} = useContext(AuthContext);
|
|
13
|
+
|
|
14
|
+
const tryAgain = async error => {
|
|
15
|
+
if (handleError !== null) {
|
|
16
|
+
handleError();
|
|
17
|
+
}
|
|
18
|
+
setTimeout(() => {
|
|
19
|
+
|
|
20
|
+
}, 1000);
|
|
21
|
+
};
|
|
22
|
+
let imageSource;
|
|
23
|
+
let protectedUri = false;
|
|
24
|
+
|
|
25
|
+
if (typeof source === 'object' && 'uri' in source !== null && (typeof source.uri === 'string' || source.uri instanceof String)) {
|
|
26
|
+
imageSource = source;
|
|
27
|
+
|
|
28
|
+
if (source.uri.startsWith(settings.baseURL)) {
|
|
29
|
+
imageSource = { headers: {Authorization: 'Bearer ' + user.authToken.accessToken }, uri: source.uri};
|
|
30
|
+
protectedUri = true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else if (uri) {
|
|
34
|
+
imageSource = {uri: uri}
|
|
35
|
+
|
|
36
|
+
if (uri.startsWith(settings.baseURL)) {
|
|
37
|
+
imageSource = { headers: {Authorization: 'Bearer ' + user.authToken.accessToken }, uri: uri};
|
|
38
|
+
protectedUri = true;
|
|
39
|
+
}
|
|
40
|
+
} else if (source) {
|
|
41
|
+
imageSource = source;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if ((null !== user.authToken.accessToken && protectedUri == true) || protectedUri == false) {
|
|
45
|
+
return (
|
|
46
|
+
<RNImage
|
|
47
|
+
source={imageSource}
|
|
48
|
+
style={style}
|
|
49
|
+
onError={tryAgain}
|
|
50
|
+
></RNImage>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const styles = StyleSheet.create({
|
|
56
|
+
container: {}
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
export default Image;
|
|
60
|
+
|
|
61
|
+
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import React, {useEffect} from 'react';
|
|
2
|
+
import {Alert, Image, StyleSheet, TouchableWithoutFeedback, View} from "react-native";
|
|
3
|
+
|
|
4
|
+
import colors from '../config/colors'
|
|
5
|
+
import Icon from './Icon';
|
|
6
|
+
import useCamera from '../hooks/useCamera';
|
|
7
|
+
import usePhotos from '../hooks/usePhotos';
|
|
8
|
+
import useStyle from "../hooks/useStyle";
|
|
9
|
+
|
|
10
|
+
function ImageInput({imageUri, onChangeImage, onCancel = () => {}, mode = 'both'}) {
|
|
11
|
+
|
|
12
|
+
const camera = useCamera();
|
|
13
|
+
const photos = usePhotos();
|
|
14
|
+
const style = useStyle();
|
|
15
|
+
|
|
16
|
+
const styles = StyleSheet.create({
|
|
17
|
+
container: {
|
|
18
|
+
alignItems: 'center',
|
|
19
|
+
backgroundColor: style.formInput.backgroundColor,
|
|
20
|
+
color: style.formInput.color,
|
|
21
|
+
borderRadius: 15,
|
|
22
|
+
justifyContent: 'center',
|
|
23
|
+
height: 100,
|
|
24
|
+
width: 100,
|
|
25
|
+
overflow: 'hidden'
|
|
26
|
+
},
|
|
27
|
+
image: {
|
|
28
|
+
width: '100%',
|
|
29
|
+
height: '100%',
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const handlePress = async () => {
|
|
34
|
+
if (!imageUri) {
|
|
35
|
+
switch (mode) {
|
|
36
|
+
case 'camera':
|
|
37
|
+
selectImage('camera')
|
|
38
|
+
break;
|
|
39
|
+
case 'photos':
|
|
40
|
+
selectImage('photos');
|
|
41
|
+
break;
|
|
42
|
+
case 'both':
|
|
43
|
+
default:
|
|
44
|
+
Alert.alert(
|
|
45
|
+
'Please choose',
|
|
46
|
+
null,
|
|
47
|
+
[
|
|
48
|
+
{ text: 'Photos', onPress: () => selectImage('photos') },
|
|
49
|
+
{ text: 'Camera', onPress: () => selectImage('camera') },
|
|
50
|
+
{ text: 'Cancel', style: 'cancel' }
|
|
51
|
+
]
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
} else {
|
|
56
|
+
Alert.alert('Remove', 'are you sure you want to remove this image?', [
|
|
57
|
+
{ text: 'Yes', onPress: () => onChangeImage(null)},
|
|
58
|
+
{ text: 'No'},
|
|
59
|
+
]);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const selectImage = async (pickerType) => {
|
|
64
|
+
try {
|
|
65
|
+
if (pickerType === 'camera') {
|
|
66
|
+
const result = await camera.takePhoto({
|
|
67
|
+
allowsEditing: true,
|
|
68
|
+
quality: 0.5
|
|
69
|
+
});
|
|
70
|
+
result.canceled ? onCancel() : onChangeImage(result.assets[0].uri);
|
|
71
|
+
} else {
|
|
72
|
+
const result = await photos.selectImage({
|
|
73
|
+
quality: 0.5
|
|
74
|
+
});
|
|
75
|
+
result.canceled ? onCancel() : onChangeImage(result.assets[0].uri);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
} catch (error) {
|
|
79
|
+
Alert.alert('Image error', 'Error reading image');
|
|
80
|
+
console.log(error);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<TouchableWithoutFeedback onPress={handlePress}>
|
|
86
|
+
<View style={styles.container}>
|
|
87
|
+
{!imageUri ? (
|
|
88
|
+
<Icon name="camera" size={75} iconColor={colors.medium} />
|
|
89
|
+
) : (
|
|
90
|
+
<Image source={{ uri: imageUri }} style={styles.image} />
|
|
91
|
+
)}
|
|
92
|
+
</View>
|
|
93
|
+
</TouchableWithoutFeedback>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export default ImageInput;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import React, {useRef} from 'react';
|
|
2
|
+
import {FlatList, Image, ScrollView, StyleSheet, TouchableWithoutFeedback, View} from "react-native";
|
|
3
|
+
import ImageInput from "./ImageInput";
|
|
4
|
+
|
|
5
|
+
function ImageInputList({imageUris = [], onAddImage, onRemoveImage}) {
|
|
6
|
+
const scrollView = useRef();
|
|
7
|
+
|
|
8
|
+
return <View>
|
|
9
|
+
<ScrollView ref={scrollView} horizontal onContentSizeChange={ () => scrollView.current.scrollToEnd() } >
|
|
10
|
+
<View style={styles.container}>
|
|
11
|
+
{ imageUris.map( uri => (
|
|
12
|
+
<View key={uri} style={styles.image}>
|
|
13
|
+
<ImageInput imageUri={uri} onChangeImage={ () => onRemoveImage(uri)} />
|
|
14
|
+
</View>
|
|
15
|
+
)) }
|
|
16
|
+
<ImageInput onChangeImage={ uri => onAddImage(uri) } />
|
|
17
|
+
</View>
|
|
18
|
+
</ScrollView>
|
|
19
|
+
</View>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const styles = StyleSheet.create({
|
|
23
|
+
container: {
|
|
24
|
+
flexDirection: 'row'
|
|
25
|
+
},
|
|
26
|
+
image: {
|
|
27
|
+
marginRight: 5
|
|
28
|
+
},
|
|
29
|
+
list: {
|
|
30
|
+
backgroundColor: 'yellow'
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
export default ImageInputList;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {StyleSheet, TouchableWithoutFeedback, View} from "react-native";
|
|
3
|
+
import {MaterialCommunityIcons} from '@expo/vector-icons';
|
|
4
|
+
|
|
5
|
+
import colors from '../config/colors'
|
|
6
|
+
|
|
7
|
+
function ListItemDeleteAction({onPress}) {
|
|
8
|
+
return (
|
|
9
|
+
<TouchableWithoutFeedback onPress={onPress}>
|
|
10
|
+
<View style={styles.deleteBox} >
|
|
11
|
+
<MaterialCommunityIcons
|
|
12
|
+
name="trash-can"
|
|
13
|
+
size={35}
|
|
14
|
+
color={colors.white}
|
|
15
|
+
/>
|
|
16
|
+
</View>
|
|
17
|
+
</TouchableWithoutFeedback>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const styles = StyleSheet.create({
|
|
22
|
+
deleteBox: {
|
|
23
|
+
justifyContent: "center",
|
|
24
|
+
alignItems: "center",
|
|
25
|
+
backgroundColor: colors.danger,
|
|
26
|
+
width: 70
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
export default ListItemDeleteAction;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {StyleSheet, View} from "react-native";
|
|
3
|
+
import ToggleSwitch from "toggle-switch-react-native";
|
|
4
|
+
import Image from "./Image";
|
|
5
|
+
import {MaterialCommunityIcons} from "@expo/vector-icons";
|
|
6
|
+
|
|
7
|
+
import Text from '../components/Text'
|
|
8
|
+
import colors from "../config/colors";
|
|
9
|
+
import useStyle from "../hooks/useStyle";
|
|
10
|
+
|
|
11
|
+
function ListItemFlipswitch({title, subtitle, IconComponent, isOn, onColor, offColor, onToggle = () => {}}) {
|
|
12
|
+
const style = useStyle();
|
|
13
|
+
const styles = StyleSheet.create({
|
|
14
|
+
container: {
|
|
15
|
+
flexDirection: 'row',
|
|
16
|
+
alignItems: 'center',
|
|
17
|
+
padding: 15,
|
|
18
|
+
backgroundColor: style.box.backgroundColor
|
|
19
|
+
},
|
|
20
|
+
detailsContainer: {
|
|
21
|
+
flex: 1,
|
|
22
|
+
marginLeft: 10,
|
|
23
|
+
justifyContent: "center"
|
|
24
|
+
},
|
|
25
|
+
image: {
|
|
26
|
+
width: 70,
|
|
27
|
+
height: 70,
|
|
28
|
+
borderRadius: 35,
|
|
29
|
+
},
|
|
30
|
+
title: {
|
|
31
|
+
color: style.text.color,
|
|
32
|
+
fontWeight: "500",
|
|
33
|
+
},
|
|
34
|
+
subtitle: {
|
|
35
|
+
color: colors.medium
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
onColor = onColor ? onColor : style.flipswitch.onColor
|
|
40
|
+
offColor = offColor ? offColor : style.flipswitch.offColor
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<View style={styles.container}>
|
|
44
|
+
{IconComponent}
|
|
45
|
+
<View style={styles.detailsContainer}>
|
|
46
|
+
<Text style={styles.title} numberOfLines={1}>{title}</Text>
|
|
47
|
+
{subtitle && <Text numberOfLines={2} style={styles.subtitle}>{subtitle}</Text> }
|
|
48
|
+
</View>
|
|
49
|
+
<ToggleSwitch
|
|
50
|
+
isOn={isOn}
|
|
51
|
+
onColor={onColor}
|
|
52
|
+
offColor={offColor}
|
|
53
|
+
size="large"
|
|
54
|
+
onToggle={onToggle}
|
|
55
|
+
/>
|
|
56
|
+
</View>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default ListItemFlipswitch;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {StyleSheet, View} from "react-native";
|
|
3
|
+
|
|
4
|
+
import colors from '../config/colors'
|
|
5
|
+
import useStyle from "../hooks/useStyle";
|
|
6
|
+
|
|
7
|
+
function ListItemSeparator() {
|
|
8
|
+
const style = useStyle();
|
|
9
|
+
|
|
10
|
+
const styles = StyleSheet.create({
|
|
11
|
+
separator: {
|
|
12
|
+
width: '100%',
|
|
13
|
+
height: 1,
|
|
14
|
+
backgroundColor: style.backgroundColor
|
|
15
|
+
}
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<View style={styles.separator} />
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export default ListItemSeparator;
|