@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.
@@ -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
+ }