@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
package/README.md
ADDED
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesley@opndev.io>
|
|
3
|
+
SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
|
|
4
|
+
|
|
5
|
+
SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
|
|
6
|
+
-->
|
|
7
|
+
|
|
8
|
+
# Welcome to @opndev/opndev-react-native-events
|
|
9
|
+
|
|
10
|
+
Reusable React Native / Expo components for event-style apps.
|
|
11
|
+
|
|
12
|
+
Focus:
|
|
13
|
+
- fast to build
|
|
14
|
+
- minimal dependencies
|
|
15
|
+
- reusable across projects
|
|
16
|
+
- no app-specific assumptions
|
|
17
|
+
|
|
18
|
+
This package provides a small set of building blocks for:
|
|
19
|
+
|
|
20
|
+
- hero / parallax screens
|
|
21
|
+
- vendor grids
|
|
22
|
+
- menu-style content
|
|
23
|
+
- tile-based navigation
|
|
24
|
+
- external link / app launching
|
|
25
|
+
|
|
26
|
+
## Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
npm install @opndev/opndev-react-native-events
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Peer dependencies are expected to be installed by the consuming app.
|
|
33
|
+
|
|
34
|
+
## Components
|
|
35
|
+
|
|
36
|
+
### HeroScreen
|
|
37
|
+
|
|
38
|
+
Base layout for screens with:
|
|
39
|
+
- parallax header image
|
|
40
|
+
- optional overlay (title, back button, etc)
|
|
41
|
+
- scrollable content
|
|
42
|
+
|
|
43
|
+
```jsx
|
|
44
|
+
<HeroScreen
|
|
45
|
+
headerImage={<Image ... />}
|
|
46
|
+
headerOverlay={<Text>Title</Text>}
|
|
47
|
+
>
|
|
48
|
+
{children}
|
|
49
|
+
</HeroScreen>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### ParallaxScrollView
|
|
53
|
+
|
|
54
|
+
Low-level scroll + animation component used by `HeroScreen`.
|
|
55
|
+
|
|
56
|
+
Theme-agnostic. Consumers inject:
|
|
57
|
+
- colors
|
|
58
|
+
- content wrapper
|
|
59
|
+
- styling overrides
|
|
60
|
+
|
|
61
|
+
### FoodVendorScreen
|
|
62
|
+
|
|
63
|
+
Grid of vendor tiles.
|
|
64
|
+
|
|
65
|
+
```jsx
|
|
66
|
+
<FoodVendorScreen
|
|
67
|
+
title="Food vendors"
|
|
68
|
+
headerImage={<Image ... />}
|
|
69
|
+
vendors={[
|
|
70
|
+
{ key: 'bar', label: 'Bar' },
|
|
71
|
+
]}
|
|
72
|
+
onSelectVendor={(vendor) => {
|
|
73
|
+
// navigation handled by app
|
|
74
|
+
}}
|
|
75
|
+
/>
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### FoodMenuScreen
|
|
79
|
+
|
|
80
|
+
Menu-style screen with categories and items.
|
|
81
|
+
|
|
82
|
+
```jsx
|
|
83
|
+
<FoodMenuScreen
|
|
84
|
+
title="Bar"
|
|
85
|
+
headerImage={<Image ... />}
|
|
86
|
+
menu={vendor.menu}
|
|
87
|
+
onBack={() => router.back()}
|
|
88
|
+
/>
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Tile / GradientTile
|
|
92
|
+
|
|
93
|
+
Reusable tile components.
|
|
94
|
+
|
|
95
|
+
```jsx
|
|
96
|
+
<Tile
|
|
97
|
+
label="Info"
|
|
98
|
+
onPress={...}
|
|
99
|
+
/>
|
|
100
|
+
|
|
101
|
+
<GradientTile
|
|
102
|
+
label="Bar"
|
|
103
|
+
colors={['#F4D645', '#9FDEED']}
|
|
104
|
+
onPress={...}
|
|
105
|
+
/>
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## QR Code Screen
|
|
109
|
+
|
|
110
|
+
A generic, configurable QR code form + result screen.
|
|
111
|
+
|
|
112
|
+
### Basic Usage
|
|
113
|
+
|
|
114
|
+
```jsx
|
|
115
|
+
<QrCodeScreen
|
|
116
|
+
TextComponent={Text}
|
|
117
|
+
endpoint={endpoint}
|
|
118
|
+
fields={fields}
|
|
119
|
+
title="Check-in QR"
|
|
120
|
+
|
|
121
|
+
renderAboveQRCode={renderAboveQRCode}
|
|
122
|
+
renderBelowQRCode={renderBelowQRCode}
|
|
123
|
+
|
|
124
|
+
successButtons={['refresh', 'reset', 'back']}
|
|
125
|
+
onBack={() => navigation.goBack()}
|
|
126
|
+
/>
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Data Contract
|
|
130
|
+
|
|
131
|
+
```json
|
|
132
|
+
{
|
|
133
|
+
"token": "...",
|
|
134
|
+
"status": "pending",
|
|
135
|
+
"error": null
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
- HTTP always returns 200
|
|
140
|
+
- logical errors come via `error`
|
|
141
|
+
- component treats `error` as failure
|
|
142
|
+
|
|
143
|
+
### Render Hooks
|
|
144
|
+
|
|
145
|
+
#### renderAboveQRCode
|
|
146
|
+
|
|
147
|
+
```jsx
|
|
148
|
+
const renderAboveQRCode = ({ response }) => {
|
|
149
|
+
if (response.status !== 'pending') {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<>
|
|
155
|
+
<Text>Awaiting Check-in</Text>
|
|
156
|
+
<Text>Present this QR code at the entry desk</Text>
|
|
157
|
+
</>
|
|
158
|
+
);
|
|
159
|
+
};
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
#### renderBelowQRCode
|
|
163
|
+
|
|
164
|
+
```jsx
|
|
165
|
+
const renderBelowQRCode = ({ fields, values }) => {
|
|
166
|
+
return (
|
|
167
|
+
<View>
|
|
168
|
+
{fields.map((field) => (
|
|
169
|
+
<View key={field.key}>
|
|
170
|
+
<Text>{field.label}:</Text>
|
|
171
|
+
<Text>{values[field.key]}</Text>
|
|
172
|
+
</View>
|
|
173
|
+
))}
|
|
174
|
+
</View>
|
|
175
|
+
);
|
|
176
|
+
};
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
### Multi-step Status Example
|
|
180
|
+
|
|
181
|
+
```jsx
|
|
182
|
+
const steps = [
|
|
183
|
+
{ key: 'pending', label: 'Pending' },
|
|
184
|
+
{ key: 'checked_in', label: 'Checked-in' },
|
|
185
|
+
{ key: 'goodie_bag_received', label: 'Goodie-bag-received' },
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
const renderAboveQRCode = ({ response }) => {
|
|
189
|
+
const currentIndex = steps.findIndex(
|
|
190
|
+
(step) => step.key === response.status
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
return (
|
|
194
|
+
<>
|
|
195
|
+
<Text>Event Status</Text>
|
|
196
|
+
<View>
|
|
197
|
+
{steps.map((step, idx) => {
|
|
198
|
+
const isCurrent = idx === currentIndex;
|
|
199
|
+
const isDone = idx < currentIndex;
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<Text key={step.key}>
|
|
203
|
+
{step.label}
|
|
204
|
+
{isCurrent ? ' (current)' : ''}
|
|
205
|
+
{isDone ? ' (done)' : ''}
|
|
206
|
+
</Text>
|
|
207
|
+
);
|
|
208
|
+
})}
|
|
209
|
+
</View>
|
|
210
|
+
</>
|
|
211
|
+
);
|
|
212
|
+
};
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Success Buttons
|
|
216
|
+
|
|
217
|
+
```jsx
|
|
218
|
+
successButtons={['refresh', 'reset', 'back']}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Supported:
|
|
222
|
+
- refresh
|
|
223
|
+
- reset
|
|
224
|
+
- back
|
|
225
|
+
|
|
226
|
+
Default:
|
|
227
|
+
```jsx
|
|
228
|
+
['refresh', 'reset']
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Responsibilities
|
|
232
|
+
|
|
233
|
+
Component:
|
|
234
|
+
- form state
|
|
235
|
+
- persistence
|
|
236
|
+
- API calls
|
|
237
|
+
- error handling
|
|
238
|
+
- QR rendering
|
|
239
|
+
- success actions
|
|
240
|
+
|
|
241
|
+
Caller:
|
|
242
|
+
- status UI
|
|
243
|
+
- layout above/below QR
|
|
244
|
+
- mapping status to UI
|
|
245
|
+
|
|
246
|
+
## Utils
|
|
247
|
+
|
|
248
|
+
### openExternal
|
|
249
|
+
|
|
250
|
+
Helper for opening:
|
|
251
|
+
- URLs
|
|
252
|
+
- apps (deep links)
|
|
253
|
+
- Play Store / App Store fallback
|
|
254
|
+
|
|
255
|
+
```js
|
|
256
|
+
import { openExternal } from '@opndev/opndev-react-native-events';
|
|
257
|
+
|
|
258
|
+
openExternal({
|
|
259
|
+
url: 'instagram://user?username=...',
|
|
260
|
+
packageName: 'com.instagram.android',
|
|
261
|
+
appStoreUrl: 'https://apps.apple.com/app/instagram',
|
|
262
|
+
});
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
Internal navigation is intentionally **not handled** by this package.
|
|
266
|
+
|
|
267
|
+
## Development
|
|
268
|
+
|
|
269
|
+
Try to keep functions small and try not to depend on any outside functions. We
|
|
270
|
+
aim to keep the dependency graph as small as possible. The use of `esm` is
|
|
271
|
+
welcomed and writing tests is encouraged.
|
|
272
|
+
|
|
273
|
+
### jsdoc
|
|
274
|
+
|
|
275
|
+
You can build the documentation by running `npm run jsdoc`.
|
|
276
|
+
|
|
277
|
+
### eslint
|
|
278
|
+
|
|
279
|
+
The ES Linting profile is flexible and does not try to enforce much. There is
|
|
280
|
+
more than one way to do it ([TIMTOWTDI](https://en.wikipedia.org/wiki/There%27s_more_than_one_way_to_do_it)).
|
|
281
|
+
|
|
282
|
+
Try to stay constistent, but forcing a programming style upon others is bad.
|
|
283
|
+
|
|
284
|
+
## Code of Conduct
|
|
285
|
+
|
|
286
|
+
Be human.
|
|
287
|
+
|
|
288
|
+
## Semver
|
|
289
|
+
|
|
290
|
+
This project does not adhere to semver and one should not rely on the version
|
|
291
|
+
x.y.z notation to infer stability or reliability. Read the Changes file to see
|
|
292
|
+
any updates a version may bring. The fact that this module sits currently at
|
|
293
|
+
0.x.z ranges does not indicate alpha or beta or even unstable associations. It
|
|
294
|
+
is just a number and we started at 0.0.1.
|
|
295
|
+
|
|
296
|
+
In general the following hard guarantee will be given: We will not break your
|
|
297
|
+
code. In case we do happen to cause breakage: we will fix it accordingly.
|
|
298
|
+
|
|
299
|
+
In case we foresee breaking changes we'll add deprecation warnings. Giving you
|
|
300
|
+
time to fix things before a breaking change will be introduced. When a change
|
|
301
|
+
will be introduced is communicated in the Changes file. Security fixes may
|
|
302
|
+
cause breakage at any given time without notice.
|
|
303
|
+
|
|
304
|
+
This package is released by `@opndev/rzilla`, changes to `package.json` will be
|
|
305
|
+
overridden. In addition to a little bit of promotion, this also means that
|
|
306
|
+
version numbers are autoincremented at release time and bumped in all relevant
|
|
307
|
+
files: Versioning for humans, not machines.
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
### License
|
|
311
|
+
|
|
312
|
+
Please be aware that this package is GPL-3.0-or-later with exceptions. Please
|
|
313
|
+
refer to the LICENSES directory for more on this.
|
|
314
|
+
|
|
315
|
+
### Code contributions
|
|
316
|
+
|
|
317
|
+
This project does not accept pull, merge or patches from others.
|
|
318
|
+
|
|
319
|
+
Due to the license and how copyright law works, this module will not accept
|
|
320
|
+
code written by people who are not employed by opndev.io.
|
|
@@ -0,0 +1,100 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* News API cache time-to-live.
|
|
7
|
+
*/
|
|
8
|
+
const TTL_MS = 15 * 60 * 1000;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* In-memory cache for news API responses.
|
|
12
|
+
*/
|
|
13
|
+
const cache = new Map();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Builds a cache key for a given URI and bearer token.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} uri
|
|
19
|
+
* @param {string} [bearerToken]
|
|
20
|
+
*
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
function getCacheKey(uri, bearerToken) {
|
|
24
|
+
return `${uri}::${bearerToken || ''}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Returns true when a cache entry is still fresh.
|
|
29
|
+
*
|
|
30
|
+
* @param {object} entry
|
|
31
|
+
*
|
|
32
|
+
* @returns {boolean}
|
|
33
|
+
*/
|
|
34
|
+
function isFresh(entry) {
|
|
35
|
+
if (!entry) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return (Date.now() - entry.fetchedAt) < TTL_MS;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Fetches JSON from the news API with optional bearer token support.
|
|
44
|
+
*
|
|
45
|
+
* Cached responses are reused for 15 minutes unless `force` is set.
|
|
46
|
+
*
|
|
47
|
+
* @param {object} args
|
|
48
|
+
* @param {string} args.uri
|
|
49
|
+
* @param {string} [args.bearerToken]
|
|
50
|
+
* @param {boolean} [args.force]
|
|
51
|
+
*
|
|
52
|
+
* @returns {Promise<any>}
|
|
53
|
+
*/
|
|
54
|
+
export async function fetchNewsJson({
|
|
55
|
+
uri,
|
|
56
|
+
bearerToken,
|
|
57
|
+
force = false,
|
|
58
|
+
}) {
|
|
59
|
+
const key = getCacheKey(uri, bearerToken);
|
|
60
|
+
const entry = cache.get(key);
|
|
61
|
+
|
|
62
|
+
if (!force && isFresh(entry)) {
|
|
63
|
+
return entry.data;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const headers = {};
|
|
67
|
+
if (bearerToken) {
|
|
68
|
+
headers.Authorization = `Bearer ${bearerToken}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const res = await fetch(uri, {
|
|
72
|
+
method: 'GET',
|
|
73
|
+
headers,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const data = await res.json();
|
|
77
|
+
|
|
78
|
+
cache.set(key, {
|
|
79
|
+
data,
|
|
80
|
+
fetchedAt: Date.now(),
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
return data;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Clears the cached response for a specific URI.
|
|
88
|
+
*
|
|
89
|
+
* @param {object} args
|
|
90
|
+
* @param {string} args.uri
|
|
91
|
+
* @param {string} [args.bearerToken]
|
|
92
|
+
*
|
|
93
|
+
* @returns {void}
|
|
94
|
+
*/
|
|
95
|
+
export function clearNewsCache({
|
|
96
|
+
uri,
|
|
97
|
+
bearerToken,
|
|
98
|
+
}) {
|
|
99
|
+
cache.delete(getCacheKey(uri, bearerToken));
|
|
100
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* QRCodeAction
|
|
7
|
+
*
|
|
8
|
+
* Small helper for QR form persistence + request handling.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
12
|
+
|
|
13
|
+
export default class QRCodeAction {
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {object} props
|
|
17
|
+
* @param {{url: string, method?: string, headers?: object}} props.endpoint
|
|
18
|
+
* @param {string} [props.storageKey]
|
|
19
|
+
* @param {(values: object) => any} [props.buildPayload]
|
|
20
|
+
* @param {(response: any) => string} [props.extractToken]
|
|
21
|
+
*/
|
|
22
|
+
constructor({
|
|
23
|
+
endpoint,
|
|
24
|
+
storageKey = 'qr-form',
|
|
25
|
+
buildPayload,
|
|
26
|
+
extractToken,
|
|
27
|
+
}) {
|
|
28
|
+
this.endpoint = endpoint;
|
|
29
|
+
this.storageKey = storageKey;
|
|
30
|
+
this.buildPayload = buildPayload;
|
|
31
|
+
this.extractToken = extractToken;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @returns {Promise<object>}
|
|
36
|
+
*/
|
|
37
|
+
async loadValues() {
|
|
38
|
+
const stored = await AsyncStorage.getItem(this.storageKey);
|
|
39
|
+
|
|
40
|
+
if (!stored) {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return JSON.parse(stored);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {object} values
|
|
49
|
+
* @returns {Promise<void>}
|
|
50
|
+
*/
|
|
51
|
+
async persistValues(values) {
|
|
52
|
+
await AsyncStorage.setItem(
|
|
53
|
+
this.storageKey,
|
|
54
|
+
JSON.stringify(values)
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* @param {object} values
|
|
60
|
+
* @returns {Promise<any>}
|
|
61
|
+
*/
|
|
62
|
+
async fetchResponse(values) {
|
|
63
|
+
await this.persistValues(values);
|
|
64
|
+
|
|
65
|
+
const payload = this.buildPayload
|
|
66
|
+
? this.buildPayload(values)
|
|
67
|
+
: values;
|
|
68
|
+
|
|
69
|
+
console.log(this.endpoint);
|
|
70
|
+
console.log(payload);
|
|
71
|
+
|
|
72
|
+
const res = await fetch(this.endpoint.url, {
|
|
73
|
+
method: this.endpoint.method || 'POST',
|
|
74
|
+
headers: {
|
|
75
|
+
'Content-Type': 'application/json',
|
|
76
|
+
...(this.endpoint.headers || {}),
|
|
77
|
+
},
|
|
78
|
+
body: JSON.stringify(payload),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
return await res.json();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
getResponseError(response) {
|
|
85
|
+
if (!response) {
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (response.status === 'error') {
|
|
90
|
+
return response.error || response.message || 'Something went wrong.';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (response.error) {
|
|
94
|
+
return response.error;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {any} response
|
|
102
|
+
* @returns {string|undefined}
|
|
103
|
+
*/
|
|
104
|
+
getToken(response) {
|
|
105
|
+
if (!response) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (this.extractToken) {
|
|
110
|
+
return this.extractToken(response);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return response.token;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
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 { StyleSheet } from 'react-native';
|
|
7
|
+
import { LinearGradient } from 'expo-linear-gradient';
|
|
8
|
+
|
|
9
|
+
import TileBase from './tile-base';
|
|
10
|
+
|
|
11
|
+
const defaultStyle = StyleSheet.create({
|
|
12
|
+
background: {
|
|
13
|
+
...StyleSheet.absoluteFillObject,
|
|
14
|
+
},
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const DEFAULT_START = { x: 0, y: 0 };
|
|
18
|
+
const DEFAULT_END = { x: 1, y: 1 };
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Tile with a linear gradient background.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} props
|
|
24
|
+
* @param {string[]} props.colors
|
|
25
|
+
* @param {object} [props.start]
|
|
26
|
+
* @param {object} [props.end]
|
|
27
|
+
*/
|
|
28
|
+
export default function GradientTile({
|
|
29
|
+
colors,
|
|
30
|
+
start = DEFAULT_START,
|
|
31
|
+
end = DEFAULT_END,
|
|
32
|
+
...props
|
|
33
|
+
}) {
|
|
34
|
+
const background = (
|
|
35
|
+
<LinearGradient
|
|
36
|
+
colors={colors}
|
|
37
|
+
start={start}
|
|
38
|
+
end={end}
|
|
39
|
+
style={defaultStyle.background}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<TileBase
|
|
45
|
+
{...props}
|
|
46
|
+
background={background}
|
|
47
|
+
/>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
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 { View, StyleSheet, Image } from 'react-native';
|
|
6
|
+
import ParallaxScrollView from './parallax-scroll-view';
|
|
7
|
+
|
|
8
|
+
const defaultStyle = StyleSheet.create({
|
|
9
|
+
headerImageWrap: {
|
|
10
|
+
width: '100%',
|
|
11
|
+
height: 220,
|
|
12
|
+
bottom: 0,
|
|
13
|
+
left: 0,
|
|
14
|
+
position: 'absolute',
|
|
15
|
+
},
|
|
16
|
+
headerImage: {
|
|
17
|
+
width: '100%',
|
|
18
|
+
height: '100%',
|
|
19
|
+
},
|
|
20
|
+
titleWrap: {
|
|
21
|
+
paddingHorizontal: 8,
|
|
22
|
+
paddingTop: 8,
|
|
23
|
+
paddingBottom: 8,
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* HeroScreen
|
|
29
|
+
*
|
|
30
|
+
* Thin wrapper around ParallaxScrollView that:
|
|
31
|
+
* - renders a full-bleed header image
|
|
32
|
+
* - renders optional title/header content below the image
|
|
33
|
+
* - renders children below that
|
|
34
|
+
*
|
|
35
|
+
* @param {object} props
|
|
36
|
+
* @param {React.ReactNode} props.children
|
|
37
|
+
* @param {object} [props.headerImage]
|
|
38
|
+
* Header image config:
|
|
39
|
+
* - source: React Native image source
|
|
40
|
+
* - style: optional image style
|
|
41
|
+
* - fit: optional resize mode, defaults to 'cover'
|
|
42
|
+
* @param {React.ReactNode} [props.headerOverlay]
|
|
43
|
+
* Optional content rendered below the header image.
|
|
44
|
+
* @param {string} [props.backgroundColor]
|
|
45
|
+
* @param {string} [props.headerBackgroundColor]
|
|
46
|
+
* @param {number} [props.headerHeight]
|
|
47
|
+
* @param {React.ComponentType<{style?: any, children?: React.ReactNode}>}
|
|
48
|
+
* [props.ContentComponent]
|
|
49
|
+
* @param {object} [props.containerStyle]
|
|
50
|
+
* @param {object} [props.headerStyle]
|
|
51
|
+
* @param {object} [props.contentStyle]
|
|
52
|
+
*
|
|
53
|
+
* @returns {JSX.Element}
|
|
54
|
+
*/
|
|
55
|
+
export default function HeroScreen({
|
|
56
|
+
children,
|
|
57
|
+
headerImage,
|
|
58
|
+
headerOverlay,
|
|
59
|
+
|
|
60
|
+
backgroundColor,
|
|
61
|
+
headerBackgroundColor,
|
|
62
|
+
headerHeight,
|
|
63
|
+
|
|
64
|
+
ContentComponent,
|
|
65
|
+
containerStyle,
|
|
66
|
+
headerStyle,
|
|
67
|
+
contentStyle,
|
|
68
|
+
}) {
|
|
69
|
+
let hi;
|
|
70
|
+
|
|
71
|
+
if (headerImage) {
|
|
72
|
+
const fit = headerImage.fit || 'cover';
|
|
73
|
+
const style = headerImage.style || defaultStyle.headerImage;
|
|
74
|
+
|
|
75
|
+
hi = (
|
|
76
|
+
<Image
|
|
77
|
+
source={headerImage.source}
|
|
78
|
+
style={style}
|
|
79
|
+
resizeMode={fit}
|
|
80
|
+
/>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<ParallaxScrollView
|
|
86
|
+
backgroundColor={backgroundColor}
|
|
87
|
+
headerBackgroundColor={headerBackgroundColor}
|
|
88
|
+
headerHeight={headerHeight}
|
|
89
|
+
ContentComponent={ContentComponent}
|
|
90
|
+
containerStyle={containerStyle}
|
|
91
|
+
headerStyle={headerStyle}
|
|
92
|
+
contentStyle={contentStyle}
|
|
93
|
+
headerImage={
|
|
94
|
+
<View style={defaultStyle.headerImageWrap}>
|
|
95
|
+
{hi}
|
|
96
|
+
</View>
|
|
97
|
+
}
|
|
98
|
+
>
|
|
99
|
+
{headerOverlay ? (
|
|
100
|
+
<View style={defaultStyle.titleWrap}>
|
|
101
|
+
{headerOverlay}
|
|
102
|
+
</View>
|
|
103
|
+
) : null}
|
|
104
|
+
|
|
105
|
+
{children}
|
|
106
|
+
</ParallaxScrollView>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|