@opndev/react-native-events 0.0.14 → 0.0.16
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 +12 -0
- package/README.md +2 -2
- package/lib/actions/news.js +15 -93
- package/lib/components/app-screen-top-bar.jsx +1 -1
- package/lib/components/app-screen.jsx +2 -1
- package/lib/components/hero-screen-fixed.jsx +21 -7
- package/lib/components/hero-screen-header.jsx +157 -0
- package/lib/components/hero-screen-parallax.jsx +50 -21
- package/lib/components/hero-screen.jsx +20 -6
- package/lib/components/panel.jsx +44 -10
- package/lib/components/parallax-scroll-view.js +5 -2
- package/lib/components/schedule.jsx +97 -0
- package/lib/debug.js +5 -0
- package/lib/hooks/use-json-api-data.js +262 -0
- package/lib/hooks/use-static-data.js +32 -0
- package/lib/index.js +1 -1
- package/lib/utils/route-debug.js +21 -0
- package/lib/widgets/data-list-widget.jsx +189 -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 +2 -1
- package/lib/widgets/news-widget.jsx +0 -190
package/Changes
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
Revision history for @opndev/opndev-react-native-events
|
|
2
2
|
|
|
3
|
+
0.0.16 2026-06-27 23:25:11Z
|
|
4
|
+
|
|
5
|
+
* Add widgets for dynamic and static content:
|
|
6
|
+
news now is dynamic and some other bits are static because we don't need to
|
|
7
|
+
do call to a backend to decide what scheduling or rules there are. Offline
|
|
8
|
+
mode is cool too
|
|
9
|
+
|
|
10
|
+
0.0.15 2026-06-26 22:56:26Z
|
|
11
|
+
|
|
12
|
+
* Add debugRouteState() to debug export to debug route states more easily
|
|
13
|
+
* Throw shade at panels: I mean shadows, we not dissing panels here.
|
|
14
|
+
|
|
3
15
|
0.0.14 2026-06-26 04:53:23Z
|
|
4
16
|
|
|
5
17
|
* Add Panel instead of a Tile, Tiles are more buttony thing. Panels are
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
|
|
|
5
5
|
SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
|
|
6
6
|
-->
|
|
7
7
|
|
|
8
|
-
# Welcome to @opndev/
|
|
8
|
+
# Welcome to @opndev/react-native-events
|
|
9
9
|
|
|
10
10
|
Reusable React Native / Expo components for event-style apps.
|
|
11
11
|
|
|
@@ -26,7 +26,7 @@ This package provides a small set of building blocks for:
|
|
|
26
26
|
## Installation
|
|
27
27
|
|
|
28
28
|
```bash
|
|
29
|
-
npm install @opndev/
|
|
29
|
+
npm install @opndev/react-native-events
|
|
30
30
|
```
|
|
31
31
|
|
|
32
32
|
Peer dependencies are expected to be installed by the consuming app.
|
package/lib/actions/news.js
CHANGED
|
@@ -2,99 +2,21 @@
|
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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;
|
|
5
|
+
// This used to be its own standalone cache implementation. The
|
|
6
|
+
// logic was always fully generic underneath (just a TTL cache
|
|
7
|
+
// around fetch + bearer auth) — it's been moved to
|
|
8
|
+
// @opndev/react-native-events as useJsonApiData's default fetcher,
|
|
9
|
+
// shared with RemoteDataWidget. This file now just re-exports under
|
|
10
|
+
// the original names so NewsListScreen/NewsItemScreen don't need to
|
|
11
|
+
// change, and there's only one actual cache implementation rather
|
|
12
|
+
// than two that could drift apart.
|
|
13
|
+
|
|
14
|
+
import { fetchJson, clearJsonCache } from '@opndev/react-native-events/widgets';
|
|
15
|
+
|
|
16
|
+
export function fetchNewsJson({ uri, bearerToken, force }) {
|
|
17
|
+
return fetchJson({ uri, bearerToken, force });
|
|
84
18
|
}
|
|
85
19
|
|
|
86
|
-
|
|
87
|
-
|
|
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));
|
|
20
|
+
export function clearNewsCache({ uri, bearerToken }) {
|
|
21
|
+
return clearJsonCache({ uri, bearerToken });
|
|
100
22
|
}
|
|
@@ -84,10 +84,10 @@ export default function AppScreenTopBar({
|
|
|
84
84
|
return (
|
|
85
85
|
<View
|
|
86
86
|
style={[
|
|
87
|
+
style,
|
|
87
88
|
styles.bar,
|
|
88
89
|
{ height, justifyContent, paddingHorizontal: horizontalPadding, paddingBottom: verticalPadding },
|
|
89
90
|
backgroundColor ? { backgroundColor } : null,
|
|
90
|
-
style,
|
|
91
91
|
]}
|
|
92
92
|
>
|
|
93
93
|
{applyBarStyling(logo, { height, maxWidthPixels })}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
// SPDX-FileCopyrightText: 2026 Wesley Schwengle <wesleys@opperschaap.net>
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
|
|
4
|
-
|
|
5
4
|
import { View, StyleSheet } from 'react-native';
|
|
6
5
|
import { useSafeAreaTopInset } from '../hooks/use-safe-area-top-inset';
|
|
7
6
|
import { AppScreenOffsetContext } from '../hooks/use-app-screen-offset';
|
|
@@ -17,6 +16,7 @@ const defaultStyle = StyleSheet.create({
|
|
|
17
16
|
left: 0,
|
|
18
17
|
width: '100%',
|
|
19
18
|
zIndex: 10,
|
|
19
|
+
elevation: 10,
|
|
20
20
|
},
|
|
21
21
|
});
|
|
22
22
|
|
|
@@ -67,6 +67,7 @@ export default function AppScreen({
|
|
|
67
67
|
style={[
|
|
68
68
|
defaultStyle.topBarWrap,
|
|
69
69
|
{ height: offset, paddingTop: insetTop },
|
|
70
|
+
topBar.props?.backgroundColor ? { backgroundColor: topBar.props.backgroundColor } : null,
|
|
70
71
|
]}
|
|
71
72
|
>
|
|
72
73
|
{topBar}
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import { View, ScrollView, StyleSheet } from 'react-native';
|
|
6
6
|
import {
|
|
7
|
-
renderHeaderImage,
|
|
8
7
|
HeaderOverlay,
|
|
8
|
+
HeaderContent,
|
|
9
|
+
splitCarouselChildren,
|
|
9
10
|
useHeroHeaderHeight,
|
|
10
11
|
heroContentStyle,
|
|
11
12
|
} from './hero-screen-header';
|
|
@@ -38,9 +39,12 @@ const defaultStyle = StyleSheet.create({
|
|
|
38
39
|
* gap: 16 — so spacing matches the parallax variant exactly.
|
|
39
40
|
*
|
|
40
41
|
* @param {object} props
|
|
41
|
-
* @param {React.ReactNode} props.children
|
|
42
|
-
* @param {object} [props.headerImage]
|
|
42
|
+
* @param {React.ReactNode} props.children Normal body content, and/or <CarouselScreen> elements for slides.
|
|
43
|
+
* @param {object} [props.headerImage] Used when there are no <CarouselScreen> children.
|
|
43
44
|
* @param {React.ReactNode} [props.headerOverlay]
|
|
45
|
+
* @param {(slide: { item: any, route: any }, index: number) => void} [props.onSlidePress]
|
|
46
|
+
* @param {number} [props.autoPlayInterval] Defaults to 4000. Pass 0 to disable.
|
|
47
|
+
* @param {boolean} [props.loop] Defaults to true.
|
|
44
48
|
* @param {string} [props.backgroundColor]
|
|
45
49
|
* @param {string} [props.headerBackgroundColor]
|
|
46
50
|
* @param {number} [props.headerHeight] Visible image height (excludes the safe-area inset)
|
|
@@ -55,6 +59,9 @@ export default function HeroScreenFixed({
|
|
|
55
59
|
children,
|
|
56
60
|
headerImage,
|
|
57
61
|
headerOverlay,
|
|
62
|
+
onSlidePress,
|
|
63
|
+
autoPlayInterval,
|
|
64
|
+
loop,
|
|
58
65
|
backgroundColor,
|
|
59
66
|
headerBackgroundColor,
|
|
60
67
|
headerHeight = 220,
|
|
@@ -63,7 +70,8 @@ export default function HeroScreenFixed({
|
|
|
63
70
|
headerStyle,
|
|
64
71
|
contentStyle,
|
|
65
72
|
}) {
|
|
66
|
-
const {
|
|
73
|
+
const { topOffset, totalHeaderHeight } = useHeroHeaderHeight(headerHeight);
|
|
74
|
+
const { slideElements, bodyChildren } = splitCarouselChildren(children);
|
|
67
75
|
|
|
68
76
|
return (
|
|
69
77
|
<View
|
|
@@ -84,10 +92,16 @@ export default function HeroScreenFixed({
|
|
|
84
92
|
<View
|
|
85
93
|
style={[
|
|
86
94
|
defaultStyle.imageInner,
|
|
87
|
-
{ top:
|
|
95
|
+
{ top: topOffset, height: headerHeight },
|
|
88
96
|
]}
|
|
89
97
|
>
|
|
90
|
-
|
|
98
|
+
<HeaderContent
|
|
99
|
+
headerImage={headerImage}
|
|
100
|
+
slideElements={slideElements}
|
|
101
|
+
onSlidePress={onSlidePress}
|
|
102
|
+
autoPlayInterval={autoPlayInterval}
|
|
103
|
+
loop={loop}
|
|
104
|
+
/>
|
|
91
105
|
</View>
|
|
92
106
|
</View>
|
|
93
107
|
|
|
@@ -97,7 +111,7 @@ export default function HeroScreenFixed({
|
|
|
97
111
|
>
|
|
98
112
|
<View style={[heroContentStyle, contentStyle]}>
|
|
99
113
|
<HeaderOverlay headerOverlay={headerOverlay} />
|
|
100
|
-
{
|
|
114
|
+
{bodyChildren}
|
|
101
115
|
</View>
|
|
102
116
|
</ContentComponent>
|
|
103
117
|
</View>
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
|
|
4
4
|
|
|
5
|
+
import { useState, isValidElement, Children } from 'react';
|
|
5
6
|
import { View, Image, Pressable, StyleSheet } from 'react-native';
|
|
6
7
|
import MaterialCommunityIcons from '@expo/vector-icons/MaterialCommunityIcons';
|
|
8
|
+
import Carousel from 'react-native-reanimated-carousel';
|
|
7
9
|
import { useAppScreenOffset } from '../hooks/use-app-screen-offset';
|
|
8
10
|
|
|
9
11
|
const styles = StyleSheet.create({
|
|
@@ -25,6 +27,27 @@ const styles = StyleSheet.create({
|
|
|
25
27
|
shadowOpacity: 0.3,
|
|
26
28
|
shadowRadius: 3,
|
|
27
29
|
},
|
|
30
|
+
carouselFill: StyleSheet.absoluteFillObject,
|
|
31
|
+
slideFill: { flex: 1 },
|
|
32
|
+
dotsRow: {
|
|
33
|
+
position: 'absolute',
|
|
34
|
+
bottom: 12,
|
|
35
|
+
left: 0,
|
|
36
|
+
right: 0,
|
|
37
|
+
flexDirection: 'row',
|
|
38
|
+
justifyContent: 'center',
|
|
39
|
+
gap: 6,
|
|
40
|
+
},
|
|
41
|
+
dot: {
|
|
42
|
+
width: 6,
|
|
43
|
+
height: 6,
|
|
44
|
+
borderRadius: 3,
|
|
45
|
+
backgroundColor: 'rgba(255,255,255,0.5)',
|
|
46
|
+
},
|
|
47
|
+
dotActive: {
|
|
48
|
+
backgroundColor: '#FFFFFF',
|
|
49
|
+
width: 16,
|
|
50
|
+
},
|
|
28
51
|
});
|
|
29
52
|
|
|
30
53
|
/**
|
|
@@ -66,6 +89,140 @@ export function HeaderOverlay({ headerOverlay }) {
|
|
|
66
89
|
return <View style={styles.titleWrap}>{headerOverlay}</View>;
|
|
67
90
|
}
|
|
68
91
|
|
|
92
|
+
/**
|
|
93
|
+
* CarouselScreen
|
|
94
|
+
*
|
|
95
|
+
* A marker component, not a real standalone screen — used as a
|
|
96
|
+
* child of any Hero variant to declare one carousel slide:
|
|
97
|
+
*
|
|
98
|
+
* <HeroScreenFixed onSlidePress={(slide) => router.push(slide.route)}>
|
|
99
|
+
* <CarouselScreen item={fooData} route="/foo">
|
|
100
|
+
* ...whatever JSX you want for this slide...
|
|
101
|
+
* </CarouselScreen>
|
|
102
|
+
* <CarouselScreen item={barData} route="/bar">
|
|
103
|
+
* ...
|
|
104
|
+
* </CarouselScreen>
|
|
105
|
+
* </HeroScreenFixed>
|
|
106
|
+
*
|
|
107
|
+
* Every Hero variant scans its children for these by type (via
|
|
108
|
+
* splitCarouselChildren below) and pulls out `item`/`route` plus
|
|
109
|
+
* your own nested children for each slide. Any non-CarouselScreen
|
|
110
|
+
* children are treated as normal body content, same as always.
|
|
111
|
+
*
|
|
112
|
+
* If rendered standalone it just renders its own children, so it's
|
|
113
|
+
* harmless either way.
|
|
114
|
+
*
|
|
115
|
+
* @param {object} props
|
|
116
|
+
* @param {any} [props.item]
|
|
117
|
+
* @param {any} [props.route]
|
|
118
|
+
* @param {React.ReactNode} props.children
|
|
119
|
+
*/
|
|
120
|
+
export function CarouselScreen({ children }) {
|
|
121
|
+
return children ?? null;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* splitCarouselChildren
|
|
126
|
+
*
|
|
127
|
+
* Separates a Hero variant's children into carousel slides
|
|
128
|
+
* (CarouselScreen elements) and normal body content (everything
|
|
129
|
+
* else). Shared by every Hero variant so the splitting logic is
|
|
130
|
+
* identical across all of them.
|
|
131
|
+
*
|
|
132
|
+
* @param {React.ReactNode} children
|
|
133
|
+
* @returns {{ slideElements: React.ReactElement[], bodyChildren: React.ReactNode[] }}
|
|
134
|
+
*/
|
|
135
|
+
export function splitCarouselChildren(children) {
|
|
136
|
+
const childArray = Children.toArray(children);
|
|
137
|
+
|
|
138
|
+
const slideElements = childArray.filter(
|
|
139
|
+
(child) => isValidElement(child) && child.type === CarouselScreen
|
|
140
|
+
);
|
|
141
|
+
const bodyChildren = childArray.filter(
|
|
142
|
+
(child) => !(isValidElement(child) && child.type === CarouselScreen)
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return { slideElements, bodyChildren };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* HeaderContent
|
|
150
|
+
*
|
|
151
|
+
* The single thing every Hero variant renders inside its header
|
|
152
|
+
* box — decides whether that's a static image (renderHeaderImage,
|
|
153
|
+
* unchanged default behaviour) or a swipeable/auto-advancing
|
|
154
|
+
* carousel (when CarouselScreen children were found), so a Hero
|
|
155
|
+
* variant only ever needs to swap one line to gain carousel
|
|
156
|
+
* support, with zero changes to its own scroll/positioning logic.
|
|
157
|
+
*
|
|
158
|
+
* Fills whatever box it's placed in — each variant owns its own
|
|
159
|
+
* positioning (Fixed/Overlay via their own imageInner wrapper,
|
|
160
|
+
* Parallax via ParallaxScrollView's existing wrapper) — this
|
|
161
|
+
* component doesn't position itself.
|
|
162
|
+
*
|
|
163
|
+
* @param {object} props
|
|
164
|
+
* @param {object} [props.headerImage] Used when there are no slides.
|
|
165
|
+
* @param {React.ReactElement[]} [props.slideElements] From splitCarouselChildren. When empty/absent, renders the static image instead.
|
|
166
|
+
* @param {(slide: { item: any, route: any }, index: number) => void} [props.onSlidePress]
|
|
167
|
+
* @param {number} [props.autoPlayInterval] Defaults to 4000. Pass 0 to disable.
|
|
168
|
+
* @param {boolean} [props.loop] Defaults to true.
|
|
169
|
+
*
|
|
170
|
+
* @returns {JSX.Element|null}
|
|
171
|
+
*/
|
|
172
|
+
export function HeaderContent({
|
|
173
|
+
headerImage,
|
|
174
|
+
slideElements,
|
|
175
|
+
onSlidePress,
|
|
176
|
+
autoPlayInterval = 4000,
|
|
177
|
+
loop = true,
|
|
178
|
+
}) {
|
|
179
|
+
const [activeIndex, setActiveIndex] = useState(0);
|
|
180
|
+
|
|
181
|
+
if (!slideElements || slideElements.length === 0) {
|
|
182
|
+
return renderHeaderImage(headerImage);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<View style={styles.carouselFill}>
|
|
187
|
+
<Carousel
|
|
188
|
+
style={{ width: '100%', height: '100%' }}
|
|
189
|
+
data={slideElements}
|
|
190
|
+
loop={loop}
|
|
191
|
+
autoPlay={autoPlayInterval > 0}
|
|
192
|
+
autoPlayInterval={autoPlayInterval}
|
|
193
|
+
onSnapToItem={setActiveIndex}
|
|
194
|
+
renderItem={({ item: slideElement, index }) => {
|
|
195
|
+
const { item, route } = slideElement.props;
|
|
196
|
+
const onPress = onSlidePress
|
|
197
|
+
? () => onSlidePress({ item, route }, index)
|
|
198
|
+
: undefined;
|
|
199
|
+
|
|
200
|
+
if (!onPress) {
|
|
201
|
+
return <View style={styles.slideFill}>{slideElement}</View>;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return (
|
|
205
|
+
<Pressable style={styles.slideFill} onPress={onPress}>
|
|
206
|
+
{slideElement}
|
|
207
|
+
</Pressable>
|
|
208
|
+
);
|
|
209
|
+
}}
|
|
210
|
+
/>
|
|
211
|
+
|
|
212
|
+
{slideElements.length > 1 ? (
|
|
213
|
+
<View style={styles.dotsRow}>
|
|
214
|
+
{slideElements.map((_, i) => (
|
|
215
|
+
<View
|
|
216
|
+
key={i}
|
|
217
|
+
style={[styles.dot, i === activeIndex ? styles.dotActive : null]}
|
|
218
|
+
/>
|
|
219
|
+
))}
|
|
220
|
+
</View>
|
|
221
|
+
) : null}
|
|
222
|
+
</View>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
69
226
|
/**
|
|
70
227
|
* useTopOffset
|
|
71
228
|
*
|
|
@@ -2,49 +2,78 @@
|
|
|
2
2
|
//
|
|
3
3
|
// SPDX-License-Identifier: GPL-3.0-or-later WITH LicenseRef-OPNDEV-exceptions
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
HeaderOverlay,
|
|
7
|
+
HeaderContent,
|
|
8
|
+
splitCarouselChildren,
|
|
9
|
+
} from './hero-screen-header';
|
|
6
10
|
import ParallaxScrollView from './parallax-scroll-view';
|
|
7
|
-
import { renderHeaderImage, HeaderOverlay } from './hero-screen-header';
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
12
|
+
/**
|
|
13
|
+
* HeroScreenParallax
|
|
14
|
+
*
|
|
15
|
+
* Header image scales/translates with scroll (the classic parallax
|
|
16
|
+
* effect) via ParallaxScrollView. Carousel support needed no new
|
|
17
|
+
* positioning logic here at all — ParallaxScrollView already treats
|
|
18
|
+
* its header as an opaque element it animates, so swapping in
|
|
19
|
+
* HeaderContent (image-or-carousel) instead of a plain image just
|
|
20
|
+
* works, same one-line swap as Fixed/Overlay.
|
|
21
|
+
*
|
|
22
|
+
* @param {object} props
|
|
23
|
+
* @param {React.ReactNode} props.children Normal body content, and/or <CarouselScreen> elements for slides.
|
|
24
|
+
* @param {object} [props.headerImage] Used when there are no <CarouselScreen> children.
|
|
25
|
+
* @param {React.ReactNode} [props.headerOverlay]
|
|
26
|
+
* @param {(slide: { item: any, route: any }, index: number) => void} [props.onSlidePress]
|
|
27
|
+
* @param {number} [props.autoPlayInterval] Defaults to 4000. Pass 0 to disable.
|
|
28
|
+
* @param {boolean} [props.loop] Defaults to true.
|
|
29
|
+
* @param {string} [props.backgroundColor]
|
|
30
|
+
* @param {string} [props.headerBackgroundColor]
|
|
31
|
+
* @param {number} [props.headerHeight] Visible image height (excludes the safe-area inset)
|
|
32
|
+
* @param {React.ComponentType} [props.ContentComponent]
|
|
33
|
+
* @param {object} [props.containerStyle]
|
|
34
|
+
* @param {object} [props.headerStyle]
|
|
35
|
+
* @param {object} [props.contentStyle]
|
|
36
|
+
*
|
|
37
|
+
* @returns {JSX.Element}
|
|
38
|
+
*/
|
|
39
|
+
export default function HeroScreenParallax({
|
|
20
40
|
children,
|
|
21
41
|
headerImage,
|
|
22
42
|
headerOverlay,
|
|
43
|
+
onSlidePress,
|
|
44
|
+
autoPlayInterval,
|
|
45
|
+
loop,
|
|
23
46
|
backgroundColor,
|
|
24
47
|
headerBackgroundColor,
|
|
25
48
|
headerHeight,
|
|
26
|
-
ContentComponent,
|
|
27
49
|
containerStyle,
|
|
28
50
|
headerStyle,
|
|
29
51
|
contentStyle,
|
|
52
|
+
ContentComponent,
|
|
30
53
|
}) {
|
|
54
|
+
const { slideElements, bodyChildren } = splitCarouselChildren(children);
|
|
55
|
+
|
|
31
56
|
return (
|
|
32
57
|
<ParallaxScrollView
|
|
58
|
+
headerImage={
|
|
59
|
+
<HeaderContent
|
|
60
|
+
headerImage={headerImage}
|
|
61
|
+
slideElements={slideElements}
|
|
62
|
+
onSlidePress={onSlidePress}
|
|
63
|
+
autoPlayInterval={autoPlayInterval}
|
|
64
|
+
loop={loop}
|
|
65
|
+
/>
|
|
66
|
+
}
|
|
33
67
|
backgroundColor={backgroundColor}
|
|
34
68
|
headerBackgroundColor={headerBackgroundColor}
|
|
35
69
|
headerHeight={headerHeight}
|
|
36
|
-
ContentComponent={ContentComponent}
|
|
37
70
|
containerStyle={containerStyle}
|
|
38
71
|
headerStyle={headerStyle}
|
|
39
72
|
contentStyle={contentStyle}
|
|
40
|
-
|
|
41
|
-
<View style={defaultStyle.headerImageWrap}>
|
|
42
|
-
{renderHeaderImage(headerImage)}
|
|
43
|
-
</View>
|
|
44
|
-
}
|
|
73
|
+
ContentComponent={ContentComponent}
|
|
45
74
|
>
|
|
46
75
|
<HeaderOverlay headerOverlay={headerOverlay} />
|
|
47
|
-
{
|
|
76
|
+
{bodyChildren}
|
|
48
77
|
</ParallaxScrollView>
|
|
49
78
|
);
|
|
50
79
|
}
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
import { View, ScrollView, StyleSheet } from 'react-native';
|
|
6
6
|
import {
|
|
7
|
-
renderHeaderImage,
|
|
8
7
|
HeaderOverlay,
|
|
8
|
+
HeaderContent,
|
|
9
|
+
splitCarouselChildren,
|
|
9
10
|
BackButton,
|
|
10
11
|
useHeroHeaderHeight,
|
|
11
12
|
heroContentStyle,
|
|
@@ -44,10 +45,13 @@ const defaultStyle = StyleSheet.create({
|
|
|
44
45
|
* own padded content area.
|
|
45
46
|
*
|
|
46
47
|
* @param {object} props
|
|
47
|
-
* @param {React.ReactNode} props.children
|
|
48
|
-
* @param {object} [props.headerImage]
|
|
48
|
+
* @param {React.ReactNode} props.children Normal body content, and/or <CarouselScreen> elements for slides.
|
|
49
|
+
* @param {object} [props.headerImage] Used when there are no <CarouselScreen> children.
|
|
49
50
|
* @param {React.ReactNode} [props.headerOverlay]
|
|
50
|
-
* @param {Function} [props.onBack] Renders a back button over the header
|
|
51
|
+
* @param {Function} [props.onBack] Renders a back button over the header when provided. Omit for no back button.
|
|
52
|
+
* @param {(slide: { item: any, route: any }, index: number) => void} [props.onSlidePress]
|
|
53
|
+
* @param {number} [props.autoPlayInterval] Defaults to 4000. Pass 0 to disable.
|
|
54
|
+
* @param {boolean} [props.loop] Defaults to true.
|
|
51
55
|
* @param {string} [props.backgroundColor]
|
|
52
56
|
* @param {string} [props.headerBackgroundColor]
|
|
53
57
|
* @param {number} [props.headerHeight] Visible image height (excludes the safe-area inset)
|
|
@@ -63,6 +67,9 @@ export default function HeroScreenOverlay({
|
|
|
63
67
|
headerImage,
|
|
64
68
|
headerOverlay,
|
|
65
69
|
onBack,
|
|
70
|
+
onSlidePress,
|
|
71
|
+
autoPlayInterval,
|
|
72
|
+
loop,
|
|
66
73
|
backgroundColor,
|
|
67
74
|
headerBackgroundColor,
|
|
68
75
|
headerHeight = 220,
|
|
@@ -72,6 +79,7 @@ export default function HeroScreenOverlay({
|
|
|
72
79
|
contentStyle,
|
|
73
80
|
}) {
|
|
74
81
|
const { topOffset, totalHeaderHeight } = useHeroHeaderHeight(headerHeight);
|
|
82
|
+
const { slideElements, bodyChildren } = splitCarouselChildren(children);
|
|
75
83
|
|
|
76
84
|
return (
|
|
77
85
|
<View
|
|
@@ -89,7 +97,7 @@ export default function HeroScreenOverlay({
|
|
|
89
97
|
|
|
90
98
|
<View style={[heroContentStyle, contentStyle]}>
|
|
91
99
|
<HeaderOverlay headerOverlay={headerOverlay} />
|
|
92
|
-
{
|
|
100
|
+
{bodyChildren}
|
|
93
101
|
</View>
|
|
94
102
|
</ContentComponent>
|
|
95
103
|
|
|
@@ -107,7 +115,13 @@ export default function HeroScreenOverlay({
|
|
|
107
115
|
{ top: topOffset, height: headerHeight },
|
|
108
116
|
]}
|
|
109
117
|
>
|
|
110
|
-
|
|
118
|
+
<HeaderContent
|
|
119
|
+
headerImage={headerImage}
|
|
120
|
+
slideElements={slideElements}
|
|
121
|
+
onSlidePress={onSlidePress}
|
|
122
|
+
autoPlayInterval={autoPlayInterval}
|
|
123
|
+
loop={loop}
|
|
124
|
+
/>
|
|
111
125
|
</View>
|
|
112
126
|
|
|
113
127
|
<BackButton
|