@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,44 @@
|
|
|
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 { View, StyleSheet } from 'react-native';
|
|
7
|
+
|
|
8
|
+
import TileBase from './tile-base';
|
|
9
|
+
|
|
10
|
+
const defaultStyle = StyleSheet.create({
|
|
11
|
+
background: {
|
|
12
|
+
...StyleSheet.absoluteFillObject,
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Plain tile with a solid background color.
|
|
18
|
+
*
|
|
19
|
+
* @param {object} props
|
|
20
|
+
* @param {object} TextComponent
|
|
21
|
+
* @param {string} [props.backgroundColor]
|
|
22
|
+
*/
|
|
23
|
+
export default function Tile({
|
|
24
|
+
backgroundColor,
|
|
25
|
+
TextComponent,
|
|
26
|
+
...props
|
|
27
|
+
}) {
|
|
28
|
+
const background = backgroundColor ? (
|
|
29
|
+
<View
|
|
30
|
+
style={[
|
|
31
|
+
defaultStyle.background,
|
|
32
|
+
{ backgroundColor },
|
|
33
|
+
]}
|
|
34
|
+
/>
|
|
35
|
+
) : null;
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<TileBase
|
|
39
|
+
{...props}
|
|
40
|
+
background={background}
|
|
41
|
+
TextComponent={TextComponent}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
export { default as ParallaxScrollView }
|
|
6
|
+
from './components/parallax-scroll-view.jsx';
|
|
7
|
+
|
|
8
|
+
export { default as HeroScreen } from './components/hero-screen.jsx';
|
|
9
|
+
export { default as TileBase } from './components/tile-base.jsx';
|
|
10
|
+
export { default as Tile } from './components/tile.jsx';
|
|
11
|
+
export { default as GradientTile } from './components/gradient-tile.jsx';
|
|
12
|
+
export { default as QRCodeForm } from './components/qr-code-form.jsx';
|
|
13
|
+
|
|
14
|
+
export { openUrl, openApp, openExternal } from './utils/launch.js';
|
|
15
|
+
|
|
@@ -0,0 +1,26 @@
|
|
|
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
|
+
|
|
7
|
+
import HeroScreen from './components/hero-screen';
|
|
8
|
+
import Tile from './components/tile';
|
|
9
|
+
|
|
10
|
+
export function createScreens({
|
|
11
|
+
TextComponent,
|
|
12
|
+
CategoryTextComponent,
|
|
13
|
+
} = {}) {
|
|
14
|
+
return {
|
|
15
|
+
HeroScreen: (props) => (
|
|
16
|
+
<HeroScreen {...props} />
|
|
17
|
+
),
|
|
18
|
+
|
|
19
|
+
Tile: (props) => (
|
|
20
|
+
<Tile
|
|
21
|
+
{...props}
|
|
22
|
+
TextComponent={TextComponent}
|
|
23
|
+
/>
|
|
24
|
+
),
|
|
25
|
+
};
|
|
26
|
+
}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
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
|
+
const VERSION = "0.0.10";
|
|
6
|
+
|
|
7
|
+
// TODO: @opndev/util?
|
|
8
|
+
export { formatPrice } from './utils/format-price.js';
|
|
9
|
+
export { mixHexColors, contrastColor } from './utils/colors.js';
|
|
10
|
+
export { pickDailyImage, pickRandomImage } from './utils/header-picker.js';
|
|
@@ -0,0 +1,63 @@
|
|
|
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 messaging from '@react-native-firebase/messaging';
|
|
6
|
+
import { Platform, Linking } from 'react-native';
|
|
7
|
+
|
|
8
|
+
export async function getNotificationPermissions() {
|
|
9
|
+
const status = await messaging().requestPermission();
|
|
10
|
+
|
|
11
|
+
return status === messaging.AuthorizationStatus.AUTHORIZED ||
|
|
12
|
+
status === messaging.AuthorizationStatus.PROVISIONAL;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function hasNotificationPermissions() {
|
|
16
|
+
const status = await messaging().hasPermission();
|
|
17
|
+
|
|
18
|
+
return status === messaging.AuthorizationStatus.AUTHORIZED ||
|
|
19
|
+
status === messaging.AuthorizationStatus.PROVISIONAL;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function hasConfiguredNotificationPermission() {
|
|
23
|
+
const status = await messaging().hasPermission();
|
|
24
|
+
|
|
25
|
+
return status !== messaging.AuthorizationStatus.NOT_DETERMINED;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Return the Firebase/FCM push token.
|
|
30
|
+
*
|
|
31
|
+
* @returns {Promise<string|null>}
|
|
32
|
+
*/
|
|
33
|
+
export async function getPushToken() {
|
|
34
|
+
if (!(await hasNotificationPermissions())) return null;
|
|
35
|
+
|
|
36
|
+
return await messaging().getToken();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Register this device for push notifications.
|
|
41
|
+
*
|
|
42
|
+
* @param {string} token Firebase/FCM token.
|
|
43
|
+
* @returns {Promise<void>}
|
|
44
|
+
*/
|
|
45
|
+
export async function registerPushToken(endpoint, token, data = {}) {
|
|
46
|
+
if (!(await hasNotificationPermissions())) return;
|
|
47
|
+
|
|
48
|
+
await fetch(endpoint, {
|
|
49
|
+
method: 'POST',
|
|
50
|
+
headers: {
|
|
51
|
+
'Content-Type': 'application/json',
|
|
52
|
+
},
|
|
53
|
+
body: JSON.stringify({
|
|
54
|
+
token,
|
|
55
|
+
platform: Platform.OS,
|
|
56
|
+
...data,
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function changeNotificationSettings() {
|
|
62
|
+
return await Linking.openSettings();
|
|
63
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
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 * as Notifications from 'expo-notifications';
|
|
6
|
+
import { Platform, Linking } from 'react-native';
|
|
7
|
+
import Constants from 'expo-constants';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Verify users have push notifications enabled
|
|
11
|
+
*
|
|
12
|
+
* @returns {Promise<string|null>}
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export async function getNotificationPermissions() {
|
|
16
|
+
const perms = await Notifications.requestPermissionsAsync();
|
|
17
|
+
return perms.granted;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function hasNotificationPermissions() {
|
|
21
|
+
const perms = await Notifications.getPermissionsAsync();
|
|
22
|
+
return perms.granted;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function hasConfiguredNotificationPermission() {
|
|
26
|
+
const perms = await Notifications.getPermissionsAsync();
|
|
27
|
+
return perms.status !== 'undetermined';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Return the Expo push token.
|
|
32
|
+
* Callers must verify via getNotificationPermissions():
|
|
33
|
+
*
|
|
34
|
+
* @returns {Promise<string|null>}
|
|
35
|
+
*/
|
|
36
|
+
export async function getPushToken(id = null) {
|
|
37
|
+
|
|
38
|
+
if (!(await hasNotificationPermissions())) return;
|
|
39
|
+
|
|
40
|
+
const projectId = id ?? Constants.expoConfig?.extra?.eas?.projectId ??
|
|
41
|
+
Constants.easConfig?.projectId;
|
|
42
|
+
|
|
43
|
+
if (projectId === null) return;
|
|
44
|
+
|
|
45
|
+
const token = await Notifications.getExpoPushTokenAsync({projectId});
|
|
46
|
+
return token.data;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Register this device for push notifications.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} token Expo push token.
|
|
53
|
+
* @returns {Promise<void>}
|
|
54
|
+
*/
|
|
55
|
+
export async function registerPushToken(endpoint, token, data = {}) {
|
|
56
|
+
|
|
57
|
+
if (!(await hasNotificationPermissions())) return;
|
|
58
|
+
|
|
59
|
+
await fetch(endpoint, {
|
|
60
|
+
method: 'POST',
|
|
61
|
+
headers: {
|
|
62
|
+
'Content-Type': 'application/json',
|
|
63
|
+
},
|
|
64
|
+
body: JSON.stringify({
|
|
65
|
+
token,
|
|
66
|
+
platform: Platform.OS,
|
|
67
|
+
...data
|
|
68
|
+
}),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function changeNotificationSettings() {
|
|
73
|
+
return await Linking.openSettings();
|
|
74
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
|
|
7
|
+
import HeroScreen from './components/hero-screen';
|
|
8
|
+
import Tile from './components/tile';
|
|
9
|
+
|
|
10
|
+
import FoodMenuScreen from './screens/food-menu-screen';
|
|
11
|
+
import FoodVendorScreen from './screens/food-vendor-screen';
|
|
12
|
+
import NewsItemScreen from './screens/news-item-screen';
|
|
13
|
+
import NewsListScreen from './screens/news-list-screen';
|
|
14
|
+
import QRCodeScreen from './screens/qr-code-screen';
|
|
15
|
+
|
|
16
|
+
export function createScreens({
|
|
17
|
+
TextComponent,
|
|
18
|
+
CategoryTextComponent,
|
|
19
|
+
} = {}) {
|
|
20
|
+
return {
|
|
21
|
+
HeroScreen: (props) => (
|
|
22
|
+
<HeroScreen {...props} />
|
|
23
|
+
),
|
|
24
|
+
|
|
25
|
+
FoodMenuScreen: (props) => (
|
|
26
|
+
<FoodMenuScreen
|
|
27
|
+
{...props}
|
|
28
|
+
TextComponent={TextComponent}
|
|
29
|
+
CategoryTextComponent={CategoryTextComponent}
|
|
30
|
+
/>
|
|
31
|
+
),
|
|
32
|
+
|
|
33
|
+
FoodVendorScreen: (props) => (
|
|
34
|
+
<FoodVendorScreen
|
|
35
|
+
{...props}
|
|
36
|
+
TextComponent={TextComponent}
|
|
37
|
+
/>
|
|
38
|
+
),
|
|
39
|
+
|
|
40
|
+
QRCodeScreen: (props) => (
|
|
41
|
+
<QRCodeScreen
|
|
42
|
+
{...props}
|
|
43
|
+
TextComponent={TextComponent}
|
|
44
|
+
/>
|
|
45
|
+
),
|
|
46
|
+
|
|
47
|
+
NewsListScreen: (props) => (
|
|
48
|
+
<NewsListScreen
|
|
49
|
+
{...props}
|
|
50
|
+
TextComponent={TextComponent}
|
|
51
|
+
/>
|
|
52
|
+
),
|
|
53
|
+
|
|
54
|
+
NewsItemScreen: (props) => (
|
|
55
|
+
<NewsItemScreen
|
|
56
|
+
{...props}
|
|
57
|
+
TextComponent={TextComponent}
|
|
58
|
+
/>
|
|
59
|
+
),
|
|
60
|
+
|
|
61
|
+
Tile: (props) => (
|
|
62
|
+
<Tile
|
|
63
|
+
{...props}
|
|
64
|
+
TextComponent={TextComponent}
|
|
65
|
+
/>
|
|
66
|
+
),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
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
|
+
StyleSheet,
|
|
8
|
+
Text,
|
|
9
|
+
View,
|
|
10
|
+
} from 'react-native';
|
|
11
|
+
|
|
12
|
+
import { formatPrice } from '../utils/format-price';
|
|
13
|
+
|
|
14
|
+
const defaultStyle = StyleSheet.create({
|
|
15
|
+
container: {
|
|
16
|
+
padding: 16,
|
|
17
|
+
},
|
|
18
|
+
categoryTile: {
|
|
19
|
+
borderRadius: 16,
|
|
20
|
+
padding: 16,
|
|
21
|
+
marginBottom: 24,
|
|
22
|
+
},
|
|
23
|
+
categoryLabel: {
|
|
24
|
+
fontSize: 20,
|
|
25
|
+
marginBottom: 12,
|
|
26
|
+
textAlign: 'center',
|
|
27
|
+
},
|
|
28
|
+
itemRow: {
|
|
29
|
+
flexDirection: 'row',
|
|
30
|
+
justifyContent: 'space-between',
|
|
31
|
+
paddingVertical: 12,
|
|
32
|
+
paddingHorizontal: 16,
|
|
33
|
+
borderRadius: 10,
|
|
34
|
+
marginBottom: 8,
|
|
35
|
+
},
|
|
36
|
+
item: {
|
|
37
|
+
flex: 1,
|
|
38
|
+
fontSize: 16,
|
|
39
|
+
marginRight: 8,
|
|
40
|
+
},
|
|
41
|
+
price: {
|
|
42
|
+
flexShrink: 0,
|
|
43
|
+
fontSize: 16,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const defaultColors = {
|
|
48
|
+
categoryTileBackgroundColor: '#F2F2F2',
|
|
49
|
+
categoryLabelColor: '#000000',
|
|
50
|
+
itemRowBackgroundColor: '#DDDDDD',
|
|
51
|
+
itemColor: '#000000',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* FoodMenuScreen
|
|
56
|
+
*
|
|
57
|
+
* Renders a vendor menu.
|
|
58
|
+
*
|
|
59
|
+
* @param {object} props
|
|
60
|
+
* @param {object} props.menu
|
|
61
|
+
* @param {object} [props.colors]
|
|
62
|
+
* @param {React.ComponentType} [props.TextComponent]
|
|
63
|
+
* @param {React.ComponentType} [props.CategoryTextComponent]
|
|
64
|
+
*
|
|
65
|
+
* @returns {JSX.Element}
|
|
66
|
+
*/
|
|
67
|
+
export default function FoodMenuScreen({
|
|
68
|
+
menu,
|
|
69
|
+
colors = {},
|
|
70
|
+
TextComponent = Text,
|
|
71
|
+
CategoryTextComponent = Text,
|
|
72
|
+
currencySymbol = '$',
|
|
73
|
+
}) {
|
|
74
|
+
const mergedColors = {
|
|
75
|
+
...defaultColors,
|
|
76
|
+
...colors,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
const categories = menu || {};
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<View style={defaultStyle.container}>
|
|
83
|
+
{Object.entries(categories).map(([category, items]) => (
|
|
84
|
+
<View
|
|
85
|
+
key={category}
|
|
86
|
+
style={[
|
|
87
|
+
defaultStyle.categoryTile,
|
|
88
|
+
{
|
|
89
|
+
backgroundColor:
|
|
90
|
+
mergedColors.categoryTileBackgroundColor,
|
|
91
|
+
},
|
|
92
|
+
]}
|
|
93
|
+
>
|
|
94
|
+
{category && category !== '_' ? (
|
|
95
|
+
<CategoryTextComponent
|
|
96
|
+
style={[
|
|
97
|
+
defaultStyle.categoryLabel,
|
|
98
|
+
{ color: mergedColors.categoryLabelColor },
|
|
99
|
+
]}
|
|
100
|
+
>
|
|
101
|
+
{category}
|
|
102
|
+
</CategoryTextComponent>
|
|
103
|
+
) : null}
|
|
104
|
+
|
|
105
|
+
{Object.entries(items).map(([itemName, price]) => (
|
|
106
|
+
<View
|
|
107
|
+
key={itemName}
|
|
108
|
+
style={[
|
|
109
|
+
defaultStyle.itemRow,
|
|
110
|
+
{
|
|
111
|
+
backgroundColor:
|
|
112
|
+
mergedColors.itemRowBackgroundColor,
|
|
113
|
+
},
|
|
114
|
+
]}
|
|
115
|
+
>
|
|
116
|
+
<TextComponent
|
|
117
|
+
style={[
|
|
118
|
+
defaultStyle.item,
|
|
119
|
+
{ color: mergedColors.itemColor },
|
|
120
|
+
]}
|
|
121
|
+
>
|
|
122
|
+
{itemName}
|
|
123
|
+
</TextComponent>
|
|
124
|
+
|
|
125
|
+
<TextComponent
|
|
126
|
+
style={[
|
|
127
|
+
defaultStyle.price,
|
|
128
|
+
{ color: mergedColors.itemColor },
|
|
129
|
+
]}
|
|
130
|
+
>
|
|
131
|
+
{currencySymbol} {formatPrice(price)}
|
|
132
|
+
</TextComponent>
|
|
133
|
+
</View>
|
|
134
|
+
))}
|
|
135
|
+
</View>
|
|
136
|
+
))}
|
|
137
|
+
</View>
|
|
138
|
+
);
|
|
139
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
View,
|
|
8
|
+
StyleSheet,
|
|
9
|
+
Dimensions,
|
|
10
|
+
Text,
|
|
11
|
+
} from 'react-native';
|
|
12
|
+
|
|
13
|
+
import Tile from '../components/tile';
|
|
14
|
+
|
|
15
|
+
const { width } = Dimensions.get('window');
|
|
16
|
+
const GAP = 16;
|
|
17
|
+
const TILE_WIDTH = (width - GAP * 3) / 2;
|
|
18
|
+
|
|
19
|
+
const defaultStyle = StyleSheet.create({
|
|
20
|
+
grid: {
|
|
21
|
+
padding: GAP / 2,
|
|
22
|
+
flexDirection: 'row',
|
|
23
|
+
flexWrap: 'wrap',
|
|
24
|
+
justifyContent: 'center',
|
|
25
|
+
alignItems: 'flex-start',
|
|
26
|
+
width: '100%',
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* FoodVendorScreen
|
|
32
|
+
*
|
|
33
|
+
* Renders a grid of vendor tiles.
|
|
34
|
+
*
|
|
35
|
+
* @param {object} props
|
|
36
|
+
* @param {object} props.vendors
|
|
37
|
+
* @param {function} props.onSelectVendor
|
|
38
|
+
* @param {string} [props.tileBackgroundColor]
|
|
39
|
+
* @param {string} [props.tileForegroundColor]
|
|
40
|
+
* @param {object} [props.contentStyle]
|
|
41
|
+
* @param {React.ComponentType} [props.TextComponent]
|
|
42
|
+
*
|
|
43
|
+
* @returns {JSX.Element}
|
|
44
|
+
*/
|
|
45
|
+
export default function FoodVendorScreen({
|
|
46
|
+
vendors,
|
|
47
|
+
onSelectVendor,
|
|
48
|
+
tileBackgroundColor,
|
|
49
|
+
tileForegroundColor,
|
|
50
|
+
contentStyle,
|
|
51
|
+
TextComponent = Text,
|
|
52
|
+
}) {
|
|
53
|
+
return (
|
|
54
|
+
<View style={[defaultStyle.grid, contentStyle]}>
|
|
55
|
+
{Object.keys(vendors).map((name) => {
|
|
56
|
+
const vendor = vendors[name];
|
|
57
|
+
const metadata = vendor.metadata || {};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Tile
|
|
61
|
+
key={name}
|
|
62
|
+
label={name}
|
|
63
|
+
//image={metadata.img}
|
|
64
|
+
icon={metadata.ico}
|
|
65
|
+
backgroundColor={tileBackgroundColor}
|
|
66
|
+
labelStyle={{ color: tileForegroundColor }}
|
|
67
|
+
iconColor={tileForegroundColor}
|
|
68
|
+
style={{
|
|
69
|
+
width: TILE_WIDTH,
|
|
70
|
+
height: TILE_WIDTH / 2,
|
|
71
|
+
margin: GAP / 2,
|
|
72
|
+
}}
|
|
73
|
+
onPress={() => onSelectVendor(name)}
|
|
74
|
+
TextComponent={TextComponent}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
})}
|
|
78
|
+
</View>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
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, {
|
|
6
|
+
useEffect,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import {
|
|
10
|
+
ActivityIndicator,
|
|
11
|
+
StyleSheet,
|
|
12
|
+
Text,
|
|
13
|
+
View,
|
|
14
|
+
} from 'react-native';
|
|
15
|
+
import Markdown from 'react-native-markdown-display';
|
|
16
|
+
|
|
17
|
+
import HeroScreen from '../components/hero-screen';
|
|
18
|
+
import { fetchNewsJson } from '../actions/news';
|
|
19
|
+
|
|
20
|
+
const defaultStyle = StyleSheet.create({
|
|
21
|
+
content: {
|
|
22
|
+
padding: 16,
|
|
23
|
+
gap: 16,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* NewsItemScreen
|
|
29
|
+
*
|
|
30
|
+
* Fetches and renders a single news item using HeroScreen.
|
|
31
|
+
*
|
|
32
|
+
* @param {object} props
|
|
33
|
+
* @param {string} props.uri
|
|
34
|
+
* @param {string} [props.bearerToken]
|
|
35
|
+
* @param {React.ComponentType} [props.TextComponent]
|
|
36
|
+
* @param {string} [props.backgroundColor]
|
|
37
|
+
* @param {string} [props.headerBackgroundColor]
|
|
38
|
+
* @param {number} [props.headerHeight]
|
|
39
|
+
* @param {object} [props.containerStyle]
|
|
40
|
+
* @param {object} [props.headerStyle]
|
|
41
|
+
* @param {object} [props.contentStyle]
|
|
42
|
+
* @param {object} [props.summaryStyle]
|
|
43
|
+
* @param {object} [props.titleStyle]
|
|
44
|
+
* @param {object} [props.markdownStyle]
|
|
45
|
+
* @param {string} [props.loadingLabel]
|
|
46
|
+
* @param {string} [props.errorLabel]
|
|
47
|
+
*
|
|
48
|
+
* @returns {JSX.Element}
|
|
49
|
+
*/
|
|
50
|
+
export default function NewsItemScreen({
|
|
51
|
+
uri,
|
|
52
|
+
bearerToken,
|
|
53
|
+
TextComponent = Text,
|
|
54
|
+
MarkdownComponent = Markdown,
|
|
55
|
+
defaultHeaderImage,
|
|
56
|
+
|
|
57
|
+
backgroundColor,
|
|
58
|
+
headerBackgroundColor,
|
|
59
|
+
headerHeight,
|
|
60
|
+
|
|
61
|
+
containerStyle,
|
|
62
|
+
headerStyle,
|
|
63
|
+
contentStyle,
|
|
64
|
+
summaryStyle,
|
|
65
|
+
titleStyle,
|
|
66
|
+
markdownStyle,
|
|
67
|
+
|
|
68
|
+
loadingLabel = 'Loading...',
|
|
69
|
+
errorLabel = 'Unable to load news item.',
|
|
70
|
+
}) {
|
|
71
|
+
const [item, setItem] = useState(null);
|
|
72
|
+
const [loading, setLoading] = useState(true);
|
|
73
|
+
const [error, setError] = useState(null);
|
|
74
|
+
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
load();
|
|
77
|
+
}, [uri, bearerToken]);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Loads a single news item from the configured URI.
|
|
81
|
+
*
|
|
82
|
+
* @returns {Promise<void>}
|
|
83
|
+
*/
|
|
84
|
+
async function load() {
|
|
85
|
+
setLoading(true);
|
|
86
|
+
setError(null);
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const data = await fetchNewsJson({
|
|
90
|
+
uri,
|
|
91
|
+
bearerToken,
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
setItem(data);
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
setError(e.message || errorLabel);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
setLoading(false);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!item) return;
|
|
104
|
+
|
|
105
|
+
const headerImage = defaultHeaderImage;
|
|
106
|
+
|
|
107
|
+
// const headerImage = item?.image
|
|
108
|
+
// ? {
|
|
109
|
+
// source: { uri: item.image },
|
|
110
|
+
// style: defaultHeaderImage?.style,
|
|
111
|
+
// fit: defaultHeaderImage?.fit,
|
|
112
|
+
// }
|
|
113
|
+
// : defaultHeaderImage;
|
|
114
|
+
|
|
115
|
+
return (
|
|
116
|
+
<HeroScreen
|
|
117
|
+
TextComponent={TextComponent}
|
|
118
|
+
titleStyle={titleStyle}
|
|
119
|
+
headerImage={headerImage}
|
|
120
|
+
headerOverlay={
|
|
121
|
+
<TextComponent type="title">
|
|
122
|
+
{item.title}
|
|
123
|
+
</TextComponent>
|
|
124
|
+
}
|
|
125
|
+
backgroundColor={backgroundColor}
|
|
126
|
+
headerBackgroundColor={headerBackgroundColor}
|
|
127
|
+
headerHeight={headerHeight}
|
|
128
|
+
containerStyle={containerStyle}
|
|
129
|
+
headerStyle={headerStyle}
|
|
130
|
+
contentStyle={contentStyle}
|
|
131
|
+
>
|
|
132
|
+
<View style={defaultStyle.content}>
|
|
133
|
+
{loading ? (
|
|
134
|
+
<View>
|
|
135
|
+
<ActivityIndicator />
|
|
136
|
+
<TextComponent>{loadingLabel}</TextComponent>
|
|
137
|
+
</View>
|
|
138
|
+
) : null}
|
|
139
|
+
|
|
140
|
+
{!loading && error ? (
|
|
141
|
+
<TextComponent>
|
|
142
|
+
{error}
|
|
143
|
+
</TextComponent>
|
|
144
|
+
) : null}
|
|
145
|
+
|
|
146
|
+
{!loading && !error ? (
|
|
147
|
+
<MarkdownComponent style={markdownStyle}>
|
|
148
|
+
{item?.content || ''}
|
|
149
|
+
</MarkdownComponent>
|
|
150
|
+
) : null}
|
|
151
|
+
</View>
|
|
152
|
+
</HeroScreen>
|
|
153
|
+
);
|
|
154
|
+
}
|