@opndev/react-native-events 0.0.15 → 0.0.17

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,88 @@
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 {
6
+ forwardRef,
7
+ useImperativeHandle,
8
+ } from 'react';
9
+
10
+ import { useJsonApiData } from '../hooks/use-json-api-data';
11
+ import DataListWidget from './data-list-widget';
12
+
13
+ /**
14
+ * RemoteDataWidget
15
+ *
16
+ * Fetches from a remote JSON endpoint via useJsonApiData, then hands
17
+ * the result to DataListWidget for rendering. This component owns no
18
+ * rendering of its own — it's the "where the data comes from" half
19
+ * of the pair, mirrored by StaticDataWidget for data you already
20
+ * have. `mapItem` is the contract: whatever shape your API actually
21
+ * returns, map it to `{ id, title, description? }` and neither this
22
+ * component nor DataListWidget needs to know anything else about it.
23
+ *
24
+ * Includes a small refresh button next to the title, rate-limited
25
+ * via useJsonApiData's manual-refresh cooldown (regularMs/
26
+ * manualLockMs/errorRefreshMs — see useJsonApiData for details).
27
+ * Set showRefreshButton={false} to omit it entirely.
28
+ *
29
+ * App-specific presets (e.g. a NewsWidget that always points at
30
+ * your news endpoint with your styling) are expected to be built on
31
+ * top of this in your own app code.
32
+ *
33
+ * @param {object} props
34
+ * @param {string} props.uri
35
+ * @param {string} [props.bearerToken]
36
+ * @param {string} [props.jpath] Dot-path to the array within the response. Omit if the response IS the array.
37
+ * @param {(rawItem: any) => { id: any, title: string, description?: string }} [props.mapItem] Defaults to identity.
38
+ * @param {Function} [props.fetcher] Override for custom auth/caching. See useJsonApiData.
39
+ * @param {number} [props.refreshIntervalMs] Defaults to 15 minutes. Pass 0 to disable polling.
40
+ * @param {number} [props.regularMs] Data freshness window. Defaults to 15 minutes.
41
+ * @param {number} [props.manualLockMs] Manual refresh button cooldown after success. Defaults to 15 minutes.
42
+ * @param {number} [props.errorRefreshMs] Manual refresh button cooldown after a failed attempt. Defaults to 5 minutes.
43
+ * ...plus all of DataListWidget's display props (title, dividerStyle, showRefreshButton, TextComponent, onPressItem, styles, etc.)
44
+ *
45
+ * @returns {JSX.Element}
46
+ */
47
+ const RemoteDataWidget = forwardRef(function RemoteDataWidget(
48
+ {
49
+ uri,
50
+ bearerToken,
51
+ jpath,
52
+ mapItem,
53
+ fetcher,
54
+ refreshIntervalMs = 15 * 60 * 1000,
55
+ regularMs,
56
+ manualLockMs,
57
+ errorRefreshMs,
58
+ ...displayProps
59
+ },
60
+ ref
61
+ ) {
62
+ const { items, loading, error, refresh, manualRefresh, canManualRefresh } = useJsonApiData({
63
+ uri,
64
+ bearerToken,
65
+ jpath,
66
+ mapItem,
67
+ fetcher,
68
+ refreshIntervalMs,
69
+ regularMs,
70
+ manualLockMs,
71
+ errorRefreshMs,
72
+ });
73
+
74
+ useImperativeHandle(ref, () => ({ refresh }), [refresh]);
75
+
76
+ return (
77
+ <DataListWidget
78
+ items={items}
79
+ loading={loading}
80
+ error={error}
81
+ manualRefresh={manualRefresh}
82
+ canManualRefresh={canManualRefresh}
83
+ {...displayProps}
84
+ />
85
+ );
86
+ });
87
+
88
+ export default RemoteDataWidget;
@@ -0,0 +1,50 @@
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 { useStaticData } from '../hooks/use-static-data';
6
+ import DataListWidget from './data-list-widget';
7
+
8
+ /**
9
+ * StaticDataWidget
10
+ *
11
+ * Renders data you already have via useStaticData, then hands the
12
+ * result to DataListWidget — the exact same rendering RemoteDataWidget
13
+ * uses. The two components are identical except for where `items`
14
+ * comes from: RemoteDataWidget digs through an API response (jpath,
15
+ * polling, caching); this one just reshapes whatever `data` you pass
16
+ * in. `mapItem` is the same contract either way — map your raw items
17
+ * to `{ id, title, description? }`.
18
+ *
19
+ * showRefreshButton AND showBullet both default to false here
20
+ * (unlike RemoteDataWidget, where both default to true) — there's
21
+ * nothing to refresh against static data, and events generally read
22
+ * cleaner flush-left than bulleted. Pass either explicitly if you
23
+ * want them anyway.
24
+ *
25
+ * @param {object} props
26
+ * @param {any[]} props.data
27
+ * @param {(rawItem: any) => { id: any, title: string, description?: string }} [props.mapItem] Defaults to identity.
28
+ * ...plus all of DataListWidget's display props (title, dividerStyle, TextComponent, onPressItem, styles, etc.)
29
+ *
30
+ * @returns {JSX.Element}
31
+ */
32
+ export default function StaticDataWidget({ data, mapItem, ...displayProps }) {
33
+ const { items, loading, error, manualRefresh, canManualRefresh } = useStaticData({
34
+ data,
35
+ mapItem,
36
+ });
37
+
38
+ return (
39
+ <DataListWidget
40
+ items={items}
41
+ loading={loading}
42
+ error={error}
43
+ manualRefresh={manualRefresh}
44
+ canManualRefresh={canManualRefresh}
45
+ showRefreshButton={false}
46
+ showBullet={false}
47
+ {...displayProps}
48
+ />
49
+ );
50
+ }
package/lib/widgets.js CHANGED
@@ -2,4 +2,8 @@
2
2
  //
3
3
  // SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
4
4
 
5
- export { default as NewsWidget } from './widgets/news-widget.jsx';
5
+ export { default as RemoteDataWidget } from './widgets/remote-data-widget';
6
+ export { default as StaticDataWidget } from './widgets/static-data-widget';
7
+ export { default as DataListWidget } from './widgets/data-list-widget';
8
+ export { useJsonApiData, fetchJson, clearJsonCache } from './hooks/use-json-api-data';
9
+ export { useStaticData } from './hooks/use-static-data';
package/package.json CHANGED
@@ -35,5 +35,5 @@
35
35
  },
36
36
  "sideEffects": false,
37
37
  "type": "module",
38
- "version": "0.0.15"
38
+ "version": "0.0.17"
39
39
  }
@@ -1,190 +0,0 @@
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
- forwardRef,
7
- useEffect,
8
- useImperativeHandle,
9
- useRef,
10
- useState,
11
- } from 'react';
12
- import {
13
- ActivityIndicator,
14
- Pressable,
15
- StyleSheet,
16
- Text,
17
- View,
18
- } from 'react-native';
19
-
20
- import { fetchNewsJson } from '../actions/news';
21
-
22
- const defaultStyle = StyleSheet.create({
23
- container: {
24
- gap: 8,
25
- },
26
- title: {
27
- fontWeight: '700',
28
- },
29
- divider: {
30
- height: 1,
31
- backgroundColor: 'rgba(0,0,0,0.15)',
32
- marginTop: 4,
33
- marginBottom: 4,
34
- },
35
- item: {
36
- flexDirection: 'row',
37
- gap: 8,
38
- },
39
- bullet: {
40
- opacity: 0.6,
41
- },
42
- itemText: {
43
- flex: 1,
44
- },
45
- });
46
-
47
- /**
48
- * NewsWidget
49
- *
50
- * Compact, embeddable "latest N news items" panel — meant to sit
51
- * inside another page's body (e.g. as one of HeroScreen's children
52
- * sections), not to own the screen itself. Unlike NewsListScreen,
53
- * it has no ScrollView and no pull-to-refresh, since both break
54
- * once nested inside a parent page that's already scrollable.
55
- *
56
- * Refreshes itself automatically every `refreshIntervalMs` (default
57
- * 15 minutes). For anything else that should trigger a refetch —
58
- * most notably a push notification once that infra exists — attach
59
- * a ref and call `ref.current.refresh()` from wherever that handler
60
- * ends up living:
61
- *
62
- * const newsRef = useRef(null);
63
- * <NewsWidget ref={newsRef} uri={...} />
64
- * // later, e.g. inside a notification handler:
65
- * newsRef.current?.refresh();
66
- *
67
- * @param {object} props
68
- * @param {string} props.uri
69
- * @param {string} [props.bearerToken]
70
- * @param {number} [props.limit] Max items shown. Defaults to 4.
71
- * @param {string} [props.title] Defaults to 'News'.
72
- * @param {boolean} [props.showDivider] Thin line under the title, like 'News' / '-----'. Defaults to true. Ignored if title is falsy.
73
- * @param {object} [props.dividerStyle]
74
- * @param {React.ComponentType} [props.TextComponent] Defaults to Text.
75
- * @param {Function} [props.onPressItem]
76
- * @param {number} [props.refreshIntervalMs] Defaults to 15 minutes.
77
- * @param {string} [props.backgroundColor]
78
- * @param {object} [props.containerStyle]
79
- * @param {object} [props.titleStyle]
80
- * @param {object} [props.itemStyle]
81
- * @param {object} [props.itemTextStyle]
82
- * @param {object} [props.errorTextStyle]
83
- * @param {string} [props.emptyLabel]
84
- * @param {string} [props.errorLabel]
85
- *
86
- * @returns {JSX.Element}
87
- */
88
- const NewsWidget = forwardRef(function NewsWidget(
89
- {
90
- uri,
91
- bearerToken,
92
- limit = 4,
93
- title = 'News',
94
- showDivider = true,
95
- dividerStyle,
96
- TextComponent = Text,
97
- onPressItem,
98
- refreshIntervalMs = 15 * 60 * 1000,
99
-
100
- backgroundColor,
101
- containerStyle,
102
- titleStyle,
103
- itemStyle,
104
- itemTextStyle,
105
- errorTextStyle,
106
-
107
- emptyLabel = 'No news items.',
108
- errorLabel = 'Unable to load news.',
109
- },
110
- ref
111
- ) {
112
- const [items, setItems] = useState([]);
113
- const [loading, setLoading] = useState(true);
114
- const [error, setError] = useState(null);
115
- const intervalRef = useRef(null);
116
-
117
- async function load() {
118
- setError(null);
119
-
120
- try {
121
- const data = await fetchNewsJson({ uri, bearerToken, force: true });
122
- setItems((data.items || []).slice(0, limit));
123
- }
124
- catch (e) {
125
- setError(e.message || errorLabel);
126
- }
127
-
128
- setLoading(false);
129
- }
130
-
131
- useImperativeHandle(ref, () => ({
132
- refresh: load,
133
- }), [uri, bearerToken, limit]);
134
-
135
- useEffect(() => {
136
- setLoading(true);
137
- load();
138
-
139
- intervalRef.current = setInterval(load, refreshIntervalMs);
140
-
141
- return () => clearInterval(intervalRef.current);
142
- // eslint-disable-next-line react-hooks/exhaustive-deps
143
- }, [uri, bearerToken, limit, refreshIntervalMs]);
144
-
145
- return (
146
- <View
147
- style={[
148
- defaultStyle.container,
149
- backgroundColor ? { backgroundColor } : null,
150
- containerStyle,
151
- ]}
152
- >
153
- {title ? (
154
- <TextComponent style={[defaultStyle.title, titleStyle]}>
155
- {title}
156
- </TextComponent>
157
- ) : null}
158
-
159
- {title && showDivider ? (
160
- <View style={[defaultStyle.divider, dividerStyle]} />
161
- ) : null}
162
-
163
- {loading ? <ActivityIndicator /> : null}
164
-
165
- {!loading && error ? (
166
- <TextComponent style={errorTextStyle}>{error}</TextComponent>
167
- ) : null}
168
-
169
- {!loading && !error && !items.length ? (
170
- <TextComponent style={errorTextStyle}>{emptyLabel}</TextComponent>
171
- ) : null}
172
-
173
- {!loading && !error && items.map((item) => (
174
- <Pressable
175
- key={item.id}
176
- style={[defaultStyle.item, itemStyle]}
177
- onPress={() => onPressItem?.(item)}
178
- >
179
- <TextComponent style={defaultStyle.bullet}>{'\u2022'}</TextComponent>
180
- <TextComponent style={[defaultStyle.itemText, itemTextStyle]}>
181
- {item.title}
182
- </TextComponent>
183
- </Pressable>
184
- ))}
185
- </View>
186
- );
187
- });
188
-
189
- export default NewsWidget;
190
-