@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.
- package/Changes +13 -0
- package/README.md +2 -2
- package/lib/actions/news.js +15 -93
- package/lib/components/hero-screen-fixed.jsx +27 -23
- package/lib/components/hero-screen-header.jsx +4 -0
- package/lib/components/panel-gradient.jsx +150 -0
- package/lib/components/panel.jsx +21 -4
- package/lib/components/schedule.jsx +97 -0
- package/lib/components.js +1 -0
- package/lib/hooks/use-json-api-data.js +262 -0
- package/lib/hooks/use-panel-contrast.js +20 -0
- package/lib/hooks/use-static-data.js +32 -0
- package/lib/index.js +1 -1
- package/lib/utils/colors.js +94 -11
- package/lib/widgets/data-list-widget.jsx +202 -0
- package/lib/widgets/remote-data-widget.jsx +88 -0
- package/lib/widgets/static-data-widget.jsx +50 -0
- package/lib/widgets.js +5 -1
- package/package.json +1 -1
- package/lib/widgets/news-widget.jsx +0 -190
|
@@ -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
|
|
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
|
@@ -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
|
-
|