@opndev/react-native-events 0.0.10
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/Changes +64 -0
- package/LICENSE +4 -0
- package/LICENSES/GPL-3.0-or-later.txt +232 -0
- package/LICENSES/LicenseRef-OPNDEV-exceptions.txt +35 -0
- package/LICENSES/LicenseRef-Opndev-Proprietary.txt +0 -0
- package/README.md +320 -0
- package/lib/actions/news.js +100 -0
- package/lib/actions/qrcode.js +115 -0
- package/lib/components/gradient-tile.jsx +49 -0
- package/lib/components/hero-screen.jsx +109 -0
- package/lib/components/parallax-scroll-view.jsx +144 -0
- package/lib/components/qr-code-form.jsx +436 -0
- package/lib/components/tile-base.jsx +123 -0
- package/lib/components/tile.jsx +44 -0
- package/lib/components.js +15 -0
- package/lib/hero-screen-registery.js +26 -0
- package/lib/index.js +10 -0
- package/lib/notifications/fcm.js +63 -0
- package/lib/notifications.js +74 -0
- package/lib/screen-registery.js +68 -0
- package/lib/screens/food-menu-screen.jsx +139 -0
- package/lib/screens/food-vendor-screen.jsx +80 -0
- package/lib/screens/news-item-screen.jsx +154 -0
- package/lib/screens/news-list-screen.jsx +196 -0
- package/lib/screens/qr-code-screen.jsx +56 -0
- package/lib/screens.js +8 -0
- package/lib/testing/react-native.js +94 -0
- package/lib/utils/colors.js +82 -0
- package/lib/utils/format-price.js +19 -0
- package/lib/utils/header-picker.js +38 -0
- package/lib/utils/launch.js +94 -0
- package/package.json +37 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
|
|
4
|
+
|
|
5
|
+
import { StyleSheet, View, Platform } from 'react-native';
|
|
6
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
7
|
+
|
|
8
|
+
import Animated, {
|
|
9
|
+
interpolate,
|
|
10
|
+
useAnimatedRef,
|
|
11
|
+
useAnimatedStyle,
|
|
12
|
+
useScrollOffset,
|
|
13
|
+
} from 'react-native-reanimated';
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
const DEFAULT_HEADER_HEIGHT = 250;
|
|
17
|
+
|
|
18
|
+
const defaultStyle = StyleSheet.create({
|
|
19
|
+
container: {
|
|
20
|
+
flex: 1,
|
|
21
|
+
},
|
|
22
|
+
header: {
|
|
23
|
+
overflow: 'hidden',
|
|
24
|
+
zIndex: 1,
|
|
25
|
+
},
|
|
26
|
+
content: {
|
|
27
|
+
padding: 32,
|
|
28
|
+
gap: 16,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A scroll view with a parallax header image and a pluggable content wrapper.
|
|
34
|
+
*
|
|
35
|
+
* The component is intentionally theme-agnostic. Consumers are expected to pass
|
|
36
|
+
* resolved colors and, if needed, a custom content wrapper component.
|
|
37
|
+
*
|
|
38
|
+
* @param {object} props
|
|
39
|
+
* @param {React.ReactNode} props.children
|
|
40
|
+
* Content rendered below the parallax header.
|
|
41
|
+
* @param {React.ReactElement} props.headerImage
|
|
42
|
+
* Element rendered inside the header area, usually an image.
|
|
43
|
+
* @param {string} [props.backgroundColor='#fff']
|
|
44
|
+
* Background color for the scroll view container.
|
|
45
|
+
* @param {string} [props.headerBackgroundColor]
|
|
46
|
+
* Background color shown behind the header image.
|
|
47
|
+
* @param {number} [props.headerHeight=250]
|
|
48
|
+
* Height of the parallax header in device pixels.
|
|
49
|
+
* @param {React.ComponentType<{style?: any, children?: React.ReactNode}>}
|
|
50
|
+
* [props.ContentComponent=View]
|
|
51
|
+
* Wrapper used for the content area below the header.
|
|
52
|
+
*
|
|
53
|
+
* This is a container injection point, not an arbitrary render callback.
|
|
54
|
+
* The component passed here must accept:
|
|
55
|
+
*
|
|
56
|
+
* - `style`
|
|
57
|
+
* - `children`
|
|
58
|
+
*
|
|
59
|
+
* Typical examples are `View`, `SafeAreaView`, or a themed wrapper such as
|
|
60
|
+
* `ThemedView`.
|
|
61
|
+
*
|
|
62
|
+
* Avoid passing scroll containers such as `ScrollView` unless nested scrolling
|
|
63
|
+
* is explicitly intended.
|
|
64
|
+
* @param {import('react-native').StyleProp<import('react-native').ViewStyle>}
|
|
65
|
+
* [props.containerStyle]
|
|
66
|
+
* Optional style overrides for the outer scroll view container.
|
|
67
|
+
* @param {import('react-native').StyleProp<import('react-native').ViewStyle>}
|
|
68
|
+
* [props.headerStyle]
|
|
69
|
+
* Optional style overrides for the header container.
|
|
70
|
+
* @param {import('react-native').StyleProp<import('react-native').ViewStyle>}
|
|
71
|
+
* [props.contentStyle]
|
|
72
|
+
* Optional style overrides for the content wrapper.
|
|
73
|
+
* @returns {JSX.Element}
|
|
74
|
+
*/
|
|
75
|
+
|
|
76
|
+
export default function ParallaxScrollView({
|
|
77
|
+
children,
|
|
78
|
+
headerImage,
|
|
79
|
+
headerBackgroundColor,
|
|
80
|
+
backgroundColor = '#fff',
|
|
81
|
+
colorScheme = 'light',
|
|
82
|
+
headerHeight = DEFAULT_HEADER_HEIGHT,
|
|
83
|
+
containerStyle,
|
|
84
|
+
headerStyle,
|
|
85
|
+
contentStyle,
|
|
86
|
+
ContentComponent = View,
|
|
87
|
+
}) {
|
|
88
|
+
const scrollOffset = useScrollOffset(scrollRef);
|
|
89
|
+
const scrollRef = useAnimatedRef();
|
|
90
|
+
const insets = useSafeAreaInsets();
|
|
91
|
+
const effectiveHeaderHeight = headerHeight + (insets.top / 2);
|
|
92
|
+
|
|
93
|
+
const headerAnimatedStyle = useAnimatedStyle(() => {
|
|
94
|
+
return {
|
|
95
|
+
transform: [
|
|
96
|
+
{
|
|
97
|
+
translateY: interpolate(
|
|
98
|
+
scrollOffset.value,
|
|
99
|
+
[-headerHeight, 0, headerHeight],
|
|
100
|
+
[-headerHeight / 2, 0, headerHeight * 0.75]
|
|
101
|
+
),
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
scale: interpolate(
|
|
105
|
+
scrollOffset.value,
|
|
106
|
+
[-headerHeight, 0, headerHeight],
|
|
107
|
+
[2, 1, 1]
|
|
108
|
+
),
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<Animated.ScrollView
|
|
116
|
+
ref={scrollRef}
|
|
117
|
+
|
|
118
|
+
style={[
|
|
119
|
+
defaultStyle.container,
|
|
120
|
+
{ backgroundColor },
|
|
121
|
+
containerStyle,
|
|
122
|
+
]}
|
|
123
|
+
scrollEventThrottle={16}
|
|
124
|
+
>
|
|
125
|
+
<Animated.View
|
|
126
|
+
style={[
|
|
127
|
+
defaultStyle.header,
|
|
128
|
+
{
|
|
129
|
+
height: effectiveHeaderHeight,
|
|
130
|
+
backgroundColor: headerBackgroundColor,
|
|
131
|
+
},
|
|
132
|
+
headerAnimatedStyle,
|
|
133
|
+
headerStyle,
|
|
134
|
+
]}
|
|
135
|
+
>
|
|
136
|
+
{headerImage}
|
|
137
|
+
</Animated.View>
|
|
138
|
+
|
|
139
|
+
<ContentComponent style={[defaultStyle.content, contentStyle]}>
|
|
140
|
+
{children}
|
|
141
|
+
</ContentComponent>
|
|
142
|
+
</Animated.ScrollView>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
|
|
4
|
+
|
|
5
|
+
import React, { useEffect, useMemo, useState } from 'react';
|
|
6
|
+
import {
|
|
7
|
+
View,
|
|
8
|
+
TextInput,
|
|
9
|
+
StyleSheet,
|
|
10
|
+
Pressable,
|
|
11
|
+
} from 'react-native';
|
|
12
|
+
import QRCode from 'react-native-qrcode-svg';
|
|
13
|
+
|
|
14
|
+
const defaultStyle = StyleSheet.create({
|
|
15
|
+
container: {
|
|
16
|
+
padding: 16,
|
|
17
|
+
gap: 16,
|
|
18
|
+
},
|
|
19
|
+
section: {
|
|
20
|
+
gap: 12,
|
|
21
|
+
},
|
|
22
|
+
inputWrap: {
|
|
23
|
+
gap: 6,
|
|
24
|
+
},
|
|
25
|
+
input: {
|
|
26
|
+
backgroundColor: '#FFFFFF',
|
|
27
|
+
borderWidth: 1,
|
|
28
|
+
borderRadius: 8,
|
|
29
|
+
padding: 12,
|
|
30
|
+
},
|
|
31
|
+
button: {
|
|
32
|
+
padding: 14,
|
|
33
|
+
borderRadius: 8,
|
|
34
|
+
alignItems: 'center',
|
|
35
|
+
},
|
|
36
|
+
wideButton: {
|
|
37
|
+
alignSelf: 'stretch',
|
|
38
|
+
},
|
|
39
|
+
card: {
|
|
40
|
+
padding: 20,
|
|
41
|
+
borderRadius: 12,
|
|
42
|
+
alignItems: 'center',
|
|
43
|
+
gap: 16,
|
|
44
|
+
},
|
|
45
|
+
qrWrap: {
|
|
46
|
+
padding: 16,
|
|
47
|
+
borderWidth: 4,
|
|
48
|
+
borderRadius: 12,
|
|
49
|
+
backgroundColor: '#FFFFFF',
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* QRCodeForm
|
|
55
|
+
*
|
|
56
|
+
* Reusable QR form component.
|
|
57
|
+
*
|
|
58
|
+
* @param {object} props
|
|
59
|
+
* @param {Array<object>} props.fields
|
|
60
|
+
* @param {object} props.action
|
|
61
|
+
* @param {React.ComponentType} props.TextComponent
|
|
62
|
+
* @param {string} [props.backgroundColor]
|
|
63
|
+
* @param {string} [props.inputBackgroundColor]
|
|
64
|
+
* @param {string} [props.inputBorderColor]
|
|
65
|
+
* @param {string} [props.inputTextColor]
|
|
66
|
+
* @param {string} [props.placeholderTextColor]
|
|
67
|
+
* @param {string} [props.primaryButtonBackgroundColor]
|
|
68
|
+
* @param {string} [props.secondaryButtonBackgroundColor]
|
|
69
|
+
* @param {string} [props.cardBackgroundColor]
|
|
70
|
+
* @param {string} [props.linkColor]
|
|
71
|
+
* @param {string} [props.errorBackgroundColor]
|
|
72
|
+
* @param {string} [props.errorTextColor]
|
|
73
|
+
* @param {object} [props.containerStyle]
|
|
74
|
+
* @param {object} [props.labelStyle]
|
|
75
|
+
* @param {object} [props.inputStyle]
|
|
76
|
+
* @param {object} [props.buttonStyle]
|
|
77
|
+
* @param {object} [props.buttonTextStyle]
|
|
78
|
+
* @param {object} [props.secondaryButtonStyle]
|
|
79
|
+
* @param {object} [props.secondaryButtonTextStyle]
|
|
80
|
+
* @param {object} [props.cardStyle]
|
|
81
|
+
* @param {object} [props.linkStyle]
|
|
82
|
+
* @param {object} [props.errorStyle]
|
|
83
|
+
* @param {object} [props.errorTextStyle]
|
|
84
|
+
* @param {string} [props.submitLabel]
|
|
85
|
+
* @param {string} [props.loadingLabel]
|
|
86
|
+
* @param {string} [props.refreshLabel]
|
|
87
|
+
* @param {string} [props.resetLabel]
|
|
88
|
+
* @param {string} [props.errorLabel]
|
|
89
|
+
* @param {number} [props.qrSize]
|
|
90
|
+
* @param {string} [props.qrBorderColor]
|
|
91
|
+
* @param {number} [props.qrBorderWidth]
|
|
92
|
+
* @param {Function} [props.renderAboveQRCode]
|
|
93
|
+
* @param {Function} [props.renderBelowQRCode]
|
|
94
|
+
*
|
|
95
|
+
* @returns {JSX.Element}
|
|
96
|
+
*/
|
|
97
|
+
export default function QRCodeForm({
|
|
98
|
+
fields,
|
|
99
|
+
action,
|
|
100
|
+
TextComponent,
|
|
101
|
+
backgroundColor,
|
|
102
|
+
inputBackgroundColor = '#FFFFFF',
|
|
103
|
+
inputBorderColor,
|
|
104
|
+
inputTextColor,
|
|
105
|
+
placeholderTextColor,
|
|
106
|
+
primaryButtonBackgroundColor,
|
|
107
|
+
secondaryButtonBackgroundColor,
|
|
108
|
+
cardBackgroundColor,
|
|
109
|
+
linkColor,
|
|
110
|
+
errorBackgroundColor,
|
|
111
|
+
errorTextColor,
|
|
112
|
+
containerStyle,
|
|
113
|
+
labelStyle,
|
|
114
|
+
inputStyle,
|
|
115
|
+
buttonStyle,
|
|
116
|
+
buttonTextStyle,
|
|
117
|
+
secondaryButtonStyle,
|
|
118
|
+
secondaryButtonTextStyle,
|
|
119
|
+
cardStyle,
|
|
120
|
+
linkStyle,
|
|
121
|
+
errorStyle,
|
|
122
|
+
errorTextStyle,
|
|
123
|
+
submitLabel = 'Generate',
|
|
124
|
+
loadingLabel = 'Loading...',
|
|
125
|
+
refreshLabel = 'Refresh',
|
|
126
|
+
resetLabel = 'Re-enter',
|
|
127
|
+
errorLabel = 'Request failed',
|
|
128
|
+
qrSize = 220,
|
|
129
|
+
qrBorderColor = '#000000',
|
|
130
|
+
qrBorderWidth = 4,
|
|
131
|
+
renderAboveQRCode,
|
|
132
|
+
renderBelowQRCode,
|
|
133
|
+
}) {
|
|
134
|
+
const initialValues = useMemo(() => {
|
|
135
|
+
return fields.reduce((acc, field) => {
|
|
136
|
+
acc[field.key] = '';
|
|
137
|
+
return acc;
|
|
138
|
+
}, {});
|
|
139
|
+
}, [fields]);
|
|
140
|
+
|
|
141
|
+
const [values, setValues] = useState(initialValues);
|
|
142
|
+
const [loading, setLoading] = useState(false);
|
|
143
|
+
const [response, setResponse] = useState(null);
|
|
144
|
+
const [error, setError] = useState(null);
|
|
145
|
+
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
loadValues();
|
|
148
|
+
}, []);
|
|
149
|
+
|
|
150
|
+
async function loadValues() {
|
|
151
|
+
const stored = await action.loadValues();
|
|
152
|
+
|
|
153
|
+
setValues((prev) => ({
|
|
154
|
+
...prev,
|
|
155
|
+
...stored,
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function fetchQr() {
|
|
160
|
+
setLoading(true);
|
|
161
|
+
setError(null);
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const json = await action.fetchResponse(values);
|
|
165
|
+
setResponse(json);
|
|
166
|
+
}
|
|
167
|
+
catch (e) {
|
|
168
|
+
setError(e.message || errorLabel);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
setLoading(false);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function reset() {
|
|
175
|
+
setResponse(null);
|
|
176
|
+
setError(null);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const responseError = getResponseError(response);
|
|
180
|
+
const visibleError = error || responseError;
|
|
181
|
+
const hasResult = Boolean(response && !responseError);
|
|
182
|
+
const token = hasResult ? action.getToken(response) : undefined;
|
|
183
|
+
|
|
184
|
+
const renderArgs = {
|
|
185
|
+
response,
|
|
186
|
+
values,
|
|
187
|
+
fields,
|
|
188
|
+
token,
|
|
189
|
+
loading,
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* @param {object|null} value
|
|
194
|
+
* @returns {string|null}
|
|
195
|
+
*/
|
|
196
|
+
function getResponseError(value) {
|
|
197
|
+
if (!value) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (value.status === 'error') {
|
|
202
|
+
return value.error || value.message || errorLabel;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (value.error) {
|
|
206
|
+
return value.error;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return null;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* @param {object} field
|
|
214
|
+
* @returns {JSX.Element}
|
|
215
|
+
*/
|
|
216
|
+
function renderField(field) {
|
|
217
|
+
return (
|
|
218
|
+
<View key={field.key} style={defaultStyle.inputWrap}>
|
|
219
|
+
<TextComponent style={labelStyle}>
|
|
220
|
+
{field.label}
|
|
221
|
+
</TextComponent>
|
|
222
|
+
|
|
223
|
+
<TextInput
|
|
224
|
+
style={[
|
|
225
|
+
defaultStyle.input,
|
|
226
|
+
{
|
|
227
|
+
backgroundColor: inputBackgroundColor,
|
|
228
|
+
borderColor: inputBorderColor,
|
|
229
|
+
color: inputTextColor,
|
|
230
|
+
},
|
|
231
|
+
inputStyle,
|
|
232
|
+
]}
|
|
233
|
+
placeholder={field.placeholder || field.label}
|
|
234
|
+
placeholderTextColor={placeholderTextColor}
|
|
235
|
+
value={values[field.key]}
|
|
236
|
+
onChangeText={(text) => updateValue(field.key, text)}
|
|
237
|
+
autoCapitalize={
|
|
238
|
+
field.autoCapitalize ? 'characters' : 'none'
|
|
239
|
+
}
|
|
240
|
+
keyboardType={field.keyboardType || 'default'}
|
|
241
|
+
/>
|
|
242
|
+
</View>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* @param {string} key
|
|
248
|
+
* @param {string} value
|
|
249
|
+
* @returns {void}
|
|
250
|
+
*/
|
|
251
|
+
function updateValue(key, value) {
|
|
252
|
+
setValues((prev) => ({
|
|
253
|
+
...prev,
|
|
254
|
+
[key]: value,
|
|
255
|
+
}));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* @returns {JSX.Element|null}
|
|
260
|
+
*/
|
|
261
|
+
function renderForm() {
|
|
262
|
+
if (response) {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<View style={defaultStyle.section}>
|
|
268
|
+
{fields.map(renderField)}
|
|
269
|
+
{renderPrimaryButton()}
|
|
270
|
+
</View>
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* @returns {JSX.Element}
|
|
276
|
+
*/
|
|
277
|
+
function renderPrimaryButton() {
|
|
278
|
+
return (
|
|
279
|
+
<Pressable
|
|
280
|
+
style={[
|
|
281
|
+
defaultStyle.button,
|
|
282
|
+
primaryButtonBackgroundColor
|
|
283
|
+
? { backgroundColor: primaryButtonBackgroundColor }
|
|
284
|
+
: null,
|
|
285
|
+
buttonStyle,
|
|
286
|
+
]}
|
|
287
|
+
onPress={fetchQr}
|
|
288
|
+
>
|
|
289
|
+
<TextComponent style={buttonTextStyle}>
|
|
290
|
+
{loading ? loadingLabel : submitLabel}
|
|
291
|
+
</TextComponent>
|
|
292
|
+
</Pressable>
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* @returns {JSX.Element|null}
|
|
298
|
+
*/
|
|
299
|
+
function renderResult() {
|
|
300
|
+
if (!hasResult) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return (
|
|
305
|
+
<View
|
|
306
|
+
style={[
|
|
307
|
+
defaultStyle.card,
|
|
308
|
+
cardBackgroundColor
|
|
309
|
+
? { backgroundColor: cardBackgroundColor }
|
|
310
|
+
: null,
|
|
311
|
+
cardStyle,
|
|
312
|
+
]}
|
|
313
|
+
>
|
|
314
|
+
{renderAboveQRCode ? renderAboveQRCode(renderArgs) : null}
|
|
315
|
+
{renderQRCode()}
|
|
316
|
+
{renderBelowQRCode ? renderBelowQRCode(renderArgs) : null}
|
|
317
|
+
{renderRefreshButton()}
|
|
318
|
+
{renderResetLink()}
|
|
319
|
+
</View>
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* @returns {JSX.Element|null}
|
|
325
|
+
*/
|
|
326
|
+
function renderQRCode() {
|
|
327
|
+
if (!token) {
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return (
|
|
332
|
+
<View
|
|
333
|
+
style={[
|
|
334
|
+
defaultStyle.qrWrap,
|
|
335
|
+
{
|
|
336
|
+
borderColor: qrBorderColor,
|
|
337
|
+
borderWidth: qrBorderWidth,
|
|
338
|
+
},
|
|
339
|
+
]}
|
|
340
|
+
>
|
|
341
|
+
<QRCode
|
|
342
|
+
value={token}
|
|
343
|
+
size={qrSize}
|
|
344
|
+
backgroundColor="#FFFFFF"
|
|
345
|
+
color="#000000"
|
|
346
|
+
/>
|
|
347
|
+
</View>
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* @returns {JSX.Element}
|
|
353
|
+
*/
|
|
354
|
+
function renderRefreshButton() {
|
|
355
|
+
return (
|
|
356
|
+
<Pressable
|
|
357
|
+
style={[
|
|
358
|
+
defaultStyle.button,
|
|
359
|
+
defaultStyle.wideButton,
|
|
360
|
+
secondaryButtonBackgroundColor
|
|
361
|
+
? { backgroundColor: secondaryButtonBackgroundColor }
|
|
362
|
+
: null,
|
|
363
|
+
secondaryButtonStyle,
|
|
364
|
+
]}
|
|
365
|
+
onPress={fetchQr}
|
|
366
|
+
>
|
|
367
|
+
<TextComponent style={secondaryButtonTextStyle}>
|
|
368
|
+
{refreshLabel}
|
|
369
|
+
</TextComponent>
|
|
370
|
+
</Pressable>
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* @returns {JSX.Element}
|
|
376
|
+
*/
|
|
377
|
+
function renderResetLink() {
|
|
378
|
+
return (
|
|
379
|
+
<Pressable onPress={reset}>
|
|
380
|
+
<TextComponent
|
|
381
|
+
style={[
|
|
382
|
+
linkColor ? { color: linkColor } : null,
|
|
383
|
+
linkStyle,
|
|
384
|
+
]}
|
|
385
|
+
>
|
|
386
|
+
{resetLabel}
|
|
387
|
+
</TextComponent>
|
|
388
|
+
</Pressable>
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* @returns {JSX.Element|null}
|
|
394
|
+
*/
|
|
395
|
+
function renderError() {
|
|
396
|
+
if (!visibleError) {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
return (
|
|
401
|
+
<View
|
|
402
|
+
style={[
|
|
403
|
+
errorBackgroundColor
|
|
404
|
+
? { backgroundColor: errorBackgroundColor }
|
|
405
|
+
: null,
|
|
406
|
+
errorStyle,
|
|
407
|
+
]}
|
|
408
|
+
>
|
|
409
|
+
<TextComponent
|
|
410
|
+
style={[
|
|
411
|
+
errorTextColor ? { color: errorTextColor } : null,
|
|
412
|
+
errorTextStyle,
|
|
413
|
+
]}
|
|
414
|
+
>
|
|
415
|
+
{visibleError}
|
|
416
|
+
</TextComponent>
|
|
417
|
+
|
|
418
|
+
{responseError ? renderResetLink() : null}
|
|
419
|
+
</View>
|
|
420
|
+
);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
return (
|
|
424
|
+
<View
|
|
425
|
+
style={[
|
|
426
|
+
defaultStyle.container,
|
|
427
|
+
backgroundColor ? { backgroundColor } : null,
|
|
428
|
+
containerStyle,
|
|
429
|
+
]}
|
|
430
|
+
>
|
|
431
|
+
{renderForm()}
|
|
432
|
+
{renderResult()}
|
|
433
|
+
{renderError()}
|
|
434
|
+
</View>
|
|
435
|
+
);
|
|
436
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
|
|
4
|
+
|
|
5
|
+
import React from 'react';
|
|
6
|
+
import {
|
|
7
|
+
Text,
|
|
8
|
+
View,
|
|
9
|
+
Image,
|
|
10
|
+
StyleSheet,
|
|
11
|
+
Pressable,
|
|
12
|
+
} from 'react-native';
|
|
13
|
+
|
|
14
|
+
import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons';
|
|
15
|
+
|
|
16
|
+
const defaultStyle = StyleSheet.create({
|
|
17
|
+
surface: {
|
|
18
|
+
borderRadius: 16,
|
|
19
|
+
overflow: 'hidden',
|
|
20
|
+
},
|
|
21
|
+
wrapper: {
|
|
22
|
+
flex: 1,
|
|
23
|
+
padding: 16,
|
|
24
|
+
alignItems: 'center',
|
|
25
|
+
justifyContent: 'center',
|
|
26
|
+
},
|
|
27
|
+
content: {
|
|
28
|
+
alignItems: 'center',
|
|
29
|
+
gap: 8,
|
|
30
|
+
},
|
|
31
|
+
label: {
|
|
32
|
+
fontSize: 18,
|
|
33
|
+
textAlign: 'center',
|
|
34
|
+
},
|
|
35
|
+
image: {
|
|
36
|
+
alignSelf: 'center',
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Shared tile renderer used by Tile and GradientTile.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} props
|
|
44
|
+
* @param {React.ReactNode} [props.background]
|
|
45
|
+
* @param {string} [props.label]
|
|
46
|
+
* @param {string} [props.icon]
|
|
47
|
+
* @param {*} [props.image]
|
|
48
|
+
* @param {number} [props.iconSize=48]
|
|
49
|
+
* @param {string} [props.iconColor='#000']
|
|
50
|
+
* @param {function} [props.onPress]
|
|
51
|
+
* @param {number} [props.pressedOpacity=0.85]
|
|
52
|
+
* @param {object} [props.style]
|
|
53
|
+
* @param {object} [props.wrapperStyle]
|
|
54
|
+
* @param {object} [props.contentStyle]
|
|
55
|
+
* @param {object} [props.labelStyle]
|
|
56
|
+
* @param {object} [props.imageStyle]
|
|
57
|
+
*/
|
|
58
|
+
export default function TileBase({
|
|
59
|
+
background,
|
|
60
|
+
label,
|
|
61
|
+
icon,
|
|
62
|
+
image,
|
|
63
|
+
iconSize = 48,
|
|
64
|
+
iconColor = '#000',
|
|
65
|
+
onPress,
|
|
66
|
+
pressedOpacity = 0.85,
|
|
67
|
+
style,
|
|
68
|
+
wrapperStyle,
|
|
69
|
+
contentStyle,
|
|
70
|
+
labelStyle,
|
|
71
|
+
imageStyle,
|
|
72
|
+
TextComponent = Text,
|
|
73
|
+
}) {
|
|
74
|
+
const renderIcon = () => {
|
|
75
|
+
if (image) {
|
|
76
|
+
return (
|
|
77
|
+
<Image
|
|
78
|
+
source={image}
|
|
79
|
+
style={[defaultStyle.image, imageStyle]}
|
|
80
|
+
resizeMode="contain"
|
|
81
|
+
/>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (icon) {
|
|
86
|
+
return (
|
|
87
|
+
<MaterialCommunityIcons
|
|
88
|
+
name={icon}
|
|
89
|
+
size={iconSize}
|
|
90
|
+
color={iconColor}
|
|
91
|
+
/>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return null;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<Pressable
|
|
100
|
+
onPress={onPress}
|
|
101
|
+
style={({ pressed }) => [
|
|
102
|
+
defaultStyle.surface,
|
|
103
|
+
style,
|
|
104
|
+
{ opacity: pressed ? pressedOpacity : 1 },
|
|
105
|
+
]}
|
|
106
|
+
>
|
|
107
|
+
{background}
|
|
108
|
+
|
|
109
|
+
<View style={[defaultStyle.wrapper, wrapperStyle]}>
|
|
110
|
+
<View style={[defaultStyle.content, contentStyle]}>
|
|
111
|
+
{renderIcon()}
|
|
112
|
+
|
|
113
|
+
{label ? (
|
|
114
|
+
<TextComponent style={[defaultStyle.label, labelStyle]}>
|
|
115
|
+
{label}
|
|
116
|
+
</TextComponent>
|
|
117
|
+
) : null}
|
|
118
|
+
</View>
|
|
119
|
+
</View>
|
|
120
|
+
</Pressable>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|