related-ui-components 2.7.6 → 2.7.8
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/lib/module/app.js +16 -31
- package/lib/module/app.js.map +1 -1
- package/lib/module/components/CarouselCardStack/CarouselCardStack.js +107 -110
- package/lib/module/components/CarouselCardStack/CarouselCardStack.js.map +1 -1
- package/lib/module/components/TravelBooking/HotelSummary.js +168 -37
- package/lib/module/components/TravelBooking/HotelSummary.js.map +1 -1
- package/lib/typescript/src/app.d.ts.map +1 -1
- package/lib/typescript/src/components/CarouselCardStack/CarouselCardStack.d.ts +1 -1
- package/lib/typescript/src/components/CarouselCardStack/CarouselCardStack.d.ts.map +1 -1
- package/lib/typescript/src/components/TravelBooking/HotelSummary.d.ts +14 -0
- package/lib/typescript/src/components/TravelBooking/HotelSummary.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/app.tsx +17 -38
- package/src/components/CarouselCardStack/CarouselCardStack.tsx +157 -173
- package/src/components/TravelBooking/HotelSummary.tsx +243 -76
|
@@ -1,20 +1,19 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
1
|
+
import { Image } from "expo-image";
|
|
2
|
+
import React, { useEffect, useMemo } from "react";
|
|
3
|
+
import { Dimensions, StyleSheet, Text, View } from "react-native";
|
|
3
4
|
import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
|
4
5
|
import Animated, {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
runOnJS,
|
|
12
|
-
Extrapolation,
|
|
6
|
+
Extrapolation,
|
|
7
|
+
interpolate,
|
|
8
|
+
useAnimatedStyle,
|
|
9
|
+
useDerivedValue,
|
|
10
|
+
useSharedValue,
|
|
11
|
+
withSpring,
|
|
13
12
|
} from "react-native-reanimated";
|
|
14
13
|
import { useTheme } from "../../theme";
|
|
15
14
|
|
|
15
|
+
// --- Constants remain the same ---
|
|
16
16
|
const { width: SCREEN_WIDTH } = Dimensions.get("window");
|
|
17
|
-
|
|
18
17
|
const CARD_WIDTH_FACTOR = 0.5;
|
|
19
18
|
const CARD_ASPECT_RATIO = 1.35;
|
|
20
19
|
const SIDE_CARD_SCALE_FACTOR = 0.85;
|
|
@@ -25,31 +24,29 @@ const SIDE_CARD_TRANSLATE_X_FACTOR = Math.min(0.32, MAX_X_FACTOR);
|
|
|
25
24
|
const SIDE_CARD_TRANSLATE_X = CARD_WIDTH * SIDE_CARD_TRANSLATE_X_FACTOR;
|
|
26
25
|
const ACTIVE_CARD_SCALE = 1.0;
|
|
27
26
|
const SPRING_CONFIG = { damping: 18, stiffness: 120, mass: 0.6 };
|
|
28
|
-
const FADE_SPRING_CONFIG = { damping: 35, stiffness: 50, mass: 1.2 };
|
|
29
27
|
const SIDE_CARD_ROTATION_DEGREES = 7;
|
|
28
|
+
const VISIBILITY_WINDOW = 3; // Render active card + 2 on each side
|
|
30
29
|
|
|
30
|
+
// --- Interfaces remain the same ---
|
|
31
31
|
interface CarouselItemOriginal {
|
|
32
32
|
id: string;
|
|
33
33
|
image: string;
|
|
34
34
|
title?: string;
|
|
35
35
|
}
|
|
36
|
-
|
|
37
36
|
interface CarouselItemVirtual extends CarouselItemOriginal {
|
|
38
37
|
uniqueId: string;
|
|
39
38
|
}
|
|
40
|
-
|
|
41
39
|
interface CarouselCardStackProps {
|
|
42
40
|
data: CarouselItemOriginal[];
|
|
43
41
|
cardHeight?: number;
|
|
44
42
|
cardWidth?: number;
|
|
45
|
-
backgroundColor
|
|
43
|
+
backgroundColor?: string;
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
const createVirtualData = (
|
|
49
47
|
originalData: CarouselItemOriginal[]
|
|
50
48
|
): CarouselItemVirtual[] => {
|
|
51
49
|
if (!originalData || originalData.length === 0) return [];
|
|
52
|
-
|
|
53
50
|
const prefixItems = (
|
|
54
51
|
items: CarouselItemOriginal[],
|
|
55
52
|
segmentPrefix: string
|
|
@@ -58,221 +55,209 @@ const createVirtualData = (
|
|
|
58
55
|
...item,
|
|
59
56
|
uniqueId: `${segmentPrefix}-${item.id}-${idx}`,
|
|
60
57
|
}));
|
|
61
|
-
|
|
62
58
|
const prevSegment = prefixItems(originalData, "prev");
|
|
63
59
|
const currSegment = prefixItems(originalData, "curr");
|
|
64
60
|
const nextSegment = prefixItems(originalData, "next");
|
|
65
|
-
|
|
66
61
|
return [...prevSegment, ...currSegment, ...nextSegment];
|
|
67
62
|
};
|
|
68
63
|
|
|
69
|
-
const
|
|
64
|
+
const CarouselCard = React.memo(
|
|
65
|
+
({
|
|
66
|
+
item,
|
|
67
|
+
index,
|
|
68
|
+
activeIndex,
|
|
69
|
+
gestureTranslateX,
|
|
70
|
+
cardWidth,
|
|
71
|
+
cardHeight,
|
|
72
|
+
virtualDataLength,
|
|
73
|
+
}: {
|
|
74
|
+
item: CarouselItemVirtual;
|
|
75
|
+
index: number;
|
|
76
|
+
activeIndex: Animated.SharedValue<number>;
|
|
77
|
+
gestureTranslateX: Animated.SharedValue<number>;
|
|
78
|
+
cardWidth: number;
|
|
79
|
+
cardHeight: number;
|
|
80
|
+
virtualDataLength: number;
|
|
81
|
+
}) => {
|
|
82
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
83
|
+
const currentCardDragOffset = gestureTranslateX.value / cardWidth;
|
|
84
|
+
const displayOffset =
|
|
85
|
+
index - (activeIndex.value - currentCardDragOffset);
|
|
86
|
+
|
|
87
|
+
// --- THE CORE CHANGE IS HERE ---
|
|
88
|
+
// Check if the card is within our visibility window on the UI thread.
|
|
89
|
+
const isVisible = Math.abs(displayOffset) < VISIBILITY_WINDOW;
|
|
90
|
+
|
|
91
|
+
// If not visible, we don't need to calculate transforms.
|
|
92
|
+
// `display: 'none'` is highly performant as it removes the item from the layout.
|
|
93
|
+
if (!isVisible) {
|
|
94
|
+
return { display: "none" };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Calculations now only run for visible cards
|
|
98
|
+
const scale = interpolate(
|
|
99
|
+
displayOffset,
|
|
100
|
+
[-1, 0, 1],
|
|
101
|
+
[SIDE_CARD_SCALE_FACTOR, ACTIVE_CARD_SCALE, SIDE_CARD_SCALE_FACTOR],
|
|
102
|
+
Extrapolation.CLAMP
|
|
103
|
+
);
|
|
104
|
+
const translateX = interpolate(
|
|
105
|
+
displayOffset,
|
|
106
|
+
[-1, 0, 1],
|
|
107
|
+
[-SIDE_CARD_TRANSLATE_X, 0, SIDE_CARD_TRANSLATE_X],
|
|
108
|
+
Extrapolation.CLAMP
|
|
109
|
+
);
|
|
110
|
+
const rotation = interpolate(
|
|
111
|
+
displayOffset,
|
|
112
|
+
[-1, 0, 1],
|
|
113
|
+
[-SIDE_CARD_ROTATION_DEGREES, 0, SIDE_CARD_ROTATION_DEGREES],
|
|
114
|
+
Extrapolation.CLAMP
|
|
115
|
+
);
|
|
116
|
+
const opacity = interpolate(
|
|
117
|
+
Math.abs(displayOffset),
|
|
118
|
+
[0, 1, 2],
|
|
119
|
+
[1, 1, 0], // Fade out the outermost cards
|
|
120
|
+
Extrapolation.CLAMP
|
|
121
|
+
);
|
|
122
|
+
const snappedDisplayOffset = index - Math.round(activeIndex.value);
|
|
123
|
+
const zIndex = virtualDataLength - Math.abs(snappedDisplayOffset);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
display: "flex", // Ensure it's visible
|
|
127
|
+
transform: [{ translateX }, { scale }, { rotateZ: `${rotation}deg` }],
|
|
128
|
+
opacity,
|
|
129
|
+
zIndex,
|
|
130
|
+
};
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return (
|
|
134
|
+
<Animated.View
|
|
135
|
+
style={[
|
|
136
|
+
styles.card,
|
|
137
|
+
{ width: cardWidth, height: cardHeight },
|
|
138
|
+
animatedStyle,
|
|
139
|
+
]}
|
|
140
|
+
>
|
|
141
|
+
<Image
|
|
142
|
+
source={{ uri: item.image }}
|
|
143
|
+
style={styles.cardImage}
|
|
144
|
+
cachePolicy="memory-disk"
|
|
145
|
+
transition={250}
|
|
146
|
+
/>
|
|
147
|
+
{item.title && (
|
|
148
|
+
<View style={styles.titleContainer}>
|
|
149
|
+
<Text style={styles.cardTitle}>{item.title}</Text>
|
|
150
|
+
</View>
|
|
151
|
+
)}
|
|
152
|
+
</Animated.View>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
CarouselCard.displayName = "CarouselCard"
|
|
158
|
+
|
|
159
|
+
export const CarouselCardStack: React.FC<CarouselCardStackProps> = ({
|
|
70
160
|
data: originalData,
|
|
71
161
|
cardHeight = CARD_HEIGHT,
|
|
72
162
|
cardWidth = CARD_WIDTH,
|
|
73
|
-
backgroundColor = "transparent"
|
|
163
|
+
backgroundColor = "transparent",
|
|
74
164
|
}) => {
|
|
75
165
|
const { theme } = useTheme();
|
|
76
|
-
|
|
77
166
|
const N_original = originalData.length;
|
|
78
167
|
|
|
79
|
-
const virtualData = useMemo(
|
|
80
|
-
() => createVirtualData(originalData),
|
|
81
|
-
[originalData]
|
|
82
|
-
);
|
|
168
|
+
const virtualData = useMemo(() => createVirtualData(originalData), [originalData]);
|
|
83
169
|
|
|
84
170
|
const activeIndex = useSharedValue(N_original > 0 ? N_original : 0);
|
|
85
171
|
const gestureTranslateX = useSharedValue(0);
|
|
86
|
-
const contextX = useSharedValue(0);
|
|
87
172
|
|
|
88
173
|
useEffect(() => {
|
|
89
174
|
activeIndex.value = N_original > 0 ? N_original : 0;
|
|
90
175
|
gestureTranslateX.value = 0;
|
|
91
176
|
}, [N_original, activeIndex, gestureTranslateX]);
|
|
92
177
|
|
|
93
|
-
// Worklet to handle the loop reset
|
|
94
178
|
const handleLoopReset = () => {
|
|
95
179
|
"worklet";
|
|
96
180
|
if (N_original === 0) return;
|
|
97
|
-
|
|
98
181
|
const currentValue = Math.round(activeIndex.value);
|
|
99
|
-
|
|
100
|
-
// If in the "next" segment (indices >= 2*N_original)
|
|
101
182
|
if (currentValue >= N_original * 2) {
|
|
102
183
|
activeIndex.value = currentValue - N_original;
|
|
103
|
-
}
|
|
104
|
-
// If in the "prev" segment (indices < N_original)
|
|
105
|
-
else if (currentValue < N_original) {
|
|
184
|
+
} else if (currentValue < N_original) {
|
|
106
185
|
activeIndex.value = currentValue + N_original;
|
|
107
186
|
}
|
|
108
187
|
};
|
|
109
|
-
|
|
110
|
-
useDerivedValue(() => {
|
|
111
|
-
const currentValue = Math.round(activeIndex.value);
|
|
112
188
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
activeIndex.value = currentValue - N_original;
|
|
117
|
-
} else if (currentValue < N_original) {
|
|
118
|
-
activeIndex.value = currentValue + N_original;
|
|
189
|
+
useDerivedValue(() => {
|
|
190
|
+
if (gestureTranslateX.value === 0) {
|
|
191
|
+
handleLoopReset();
|
|
119
192
|
}
|
|
120
|
-
}
|
|
121
|
-
});
|
|
193
|
+
});
|
|
122
194
|
|
|
123
195
|
const panGesture = Gesture.Pan()
|
|
124
196
|
.activeOffsetX([-10, 10])
|
|
125
|
-
.onBegin(() => {
|
|
126
|
-
contextX.value = gestureTranslateX.value;
|
|
127
|
-
})
|
|
128
197
|
.onUpdate((event) => {
|
|
129
198
|
gestureTranslateX.value = event.translationX;
|
|
130
199
|
})
|
|
131
200
|
.onEnd((event) => {
|
|
132
201
|
if (virtualData.length === 0) return;
|
|
133
|
-
|
|
134
202
|
const threshold = cardWidth / 3;
|
|
135
|
-
let newTargetVirtualIndex = activeIndex.value;
|
|
136
|
-
|
|
203
|
+
let newTargetVirtualIndex = Math.round(activeIndex.value);
|
|
137
204
|
if (event.translationX < -threshold) {
|
|
138
|
-
newTargetVirtualIndex
|
|
205
|
+
newTargetVirtualIndex++;
|
|
139
206
|
} else if (event.translationX > threshold) {
|
|
140
|
-
newTargetVirtualIndex
|
|
207
|
+
newTargetVirtualIndex--;
|
|
141
208
|
}
|
|
142
|
-
|
|
143
|
-
// Clamp to virtual data bounds
|
|
144
|
-
newTargetVirtualIndex = Math.max(
|
|
145
|
-
0,
|
|
146
|
-
Math.min(newTargetVirtualIndex, virtualData.length - 1)
|
|
147
|
-
);
|
|
148
|
-
|
|
149
|
-
activeIndex.value = withSpring(
|
|
150
|
-
newTargetVirtualIndex,
|
|
151
|
-
SPRING_CONFIG,
|
|
152
|
-
(finished) => {
|
|
153
|
-
"worklet";
|
|
154
|
-
if (finished) {
|
|
155
|
-
handleLoopReset();
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
);
|
|
209
|
+
activeIndex.value = withSpring(newTargetVirtualIndex, SPRING_CONFIG);
|
|
159
210
|
gestureTranslateX.value = withSpring(0, SPRING_CONFIG);
|
|
160
|
-
contextX.value = 0;
|
|
161
211
|
});
|
|
162
212
|
|
|
163
213
|
const activeDotIndex = useDerivedValue(() => {
|
|
164
214
|
if (N_original === 0) return 0;
|
|
165
|
-
|
|
166
|
-
return ((currentVal % N_original) + N_original) % N_original;
|
|
215
|
+
return (Math.round(activeIndex.value) % N_original + N_original) % N_original;
|
|
167
216
|
});
|
|
168
217
|
|
|
169
|
-
if (virtualData.length === 0) {
|
|
170
|
-
return (
|
|
171
|
-
<View style={[styles.container, { height: cardHeight + 60 }]}>
|
|
172
|
-
<Text style={styles.emptyText}>No items to display</Text>
|
|
173
|
-
</View>
|
|
174
|
-
);
|
|
175
|
-
}
|
|
218
|
+
if (virtualData.length === 0) { /* ... */ }
|
|
176
219
|
|
|
177
220
|
return (
|
|
178
|
-
<View
|
|
221
|
+
<View
|
|
222
|
+
style={[
|
|
223
|
+
styles.container,
|
|
224
|
+
{ height: cardHeight + 60, backgroundColor: backgroundColor },
|
|
225
|
+
]}
|
|
226
|
+
>
|
|
179
227
|
<GestureDetector gesture={panGesture}>
|
|
180
228
|
<Animated.View style={[styles.cardContainer, { height: cardHeight }]}>
|
|
181
|
-
{
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
],
|
|
195
|
-
Extrapolation.CLAMP
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
const translateX = interpolate(
|
|
199
|
-
displayOffset,
|
|
200
|
-
[-1, 0, 1],
|
|
201
|
-
[-SIDE_CARD_TRANSLATE_X, 0, SIDE_CARD_TRANSLATE_X],
|
|
202
|
-
Extrapolation.CLAMP
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
const rotation = interpolate(
|
|
206
|
-
displayOffset,
|
|
207
|
-
[-1, 0, 1],
|
|
208
|
-
[
|
|
209
|
-
-SIDE_CARD_ROTATION_DEGREES,
|
|
210
|
-
0,
|
|
211
|
-
SIDE_CARD_ROTATION_DEGREES,
|
|
212
|
-
],
|
|
213
|
-
Extrapolation.CLAMP,
|
|
214
|
-
);
|
|
215
|
-
|
|
216
|
-
const opacity = interpolate(
|
|
217
|
-
Math.abs(displayOffset),
|
|
218
|
-
[0, 1, 2, 2.75],
|
|
219
|
-
[1, 1, 0.6, 0],
|
|
220
|
-
Extrapolation.CLAMP
|
|
221
|
-
);
|
|
222
|
-
|
|
223
|
-
const snappedDisplayOffset =
|
|
224
|
-
index - Math.round(activeIndex.value);
|
|
225
|
-
const zIndex =
|
|
226
|
-
virtualData.length - Math.abs(snappedDisplayOffset);
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
return {
|
|
230
|
-
transform: [{ translateX }, { scale }, { rotateZ: `${rotation}deg` }],
|
|
231
|
-
opacity,
|
|
232
|
-
zIndex,
|
|
233
|
-
};
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
return (
|
|
237
|
-
<Animated.View
|
|
238
|
-
key={item.uniqueId}
|
|
239
|
-
style={[
|
|
240
|
-
styles.card,
|
|
241
|
-
{ width: cardWidth, height: cardHeight },
|
|
242
|
-
animatedStyle,
|
|
243
|
-
]}
|
|
244
|
-
>
|
|
245
|
-
<Image source={{ uri: item.image }} style={styles.cardImage} />
|
|
246
|
-
{item.title && (
|
|
247
|
-
<View style={styles.titleContainer}>
|
|
248
|
-
<Text style={styles.cardTitle}>{item.title}</Text>
|
|
249
|
-
</View>
|
|
250
|
-
)}
|
|
251
|
-
</Animated.View>
|
|
252
|
-
);
|
|
253
|
-
})}
|
|
229
|
+
{/* We map over all data, but the logic inside CarouselCard handles visibility */}
|
|
230
|
+
{virtualData.map((item, index) => (
|
|
231
|
+
<CarouselCard
|
|
232
|
+
key={item.uniqueId}
|
|
233
|
+
item={item}
|
|
234
|
+
index={index}
|
|
235
|
+
activeIndex={activeIndex}
|
|
236
|
+
gestureTranslateX={gestureTranslateX}
|
|
237
|
+
cardWidth={cardWidth}
|
|
238
|
+
cardHeight={cardHeight}
|
|
239
|
+
virtualDataLength={virtualData.length}
|
|
240
|
+
/>
|
|
241
|
+
))}
|
|
254
242
|
</Animated.View>
|
|
255
243
|
</GestureDetector>
|
|
256
244
|
|
|
245
|
+
{/* Pagination dots remain the same */}
|
|
257
246
|
{N_original > 0 && (
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
return <Animated.View key={`dot-${i}`} style={dotStyle} />;
|
|
273
|
-
})}
|
|
274
|
-
</View>
|
|
275
|
-
)}
|
|
247
|
+
<View style={styles.paginationContainer}>
|
|
248
|
+
{originalData.map((_, i) => {
|
|
249
|
+
const isActiveDot = useDerivedValue(() => activeDotIndex.value === i);
|
|
250
|
+
const dotStyle = useAnimatedStyle(() => ({
|
|
251
|
+
width: withSpring(isActiveDot.value ? 24 : 8, SPRING_CONFIG),
|
|
252
|
+
height: 8,
|
|
253
|
+
borderRadius: 4,
|
|
254
|
+
backgroundColor: isActiveDot.value ? theme.primary : "#FFFFFF",
|
|
255
|
+
marginHorizontal: 4,
|
|
256
|
+
}));
|
|
257
|
+
return <Animated.View key={`dot-${i}`} style={dotStyle} />;
|
|
258
|
+
})}
|
|
259
|
+
</View>
|
|
260
|
+
)}
|
|
276
261
|
</View>
|
|
277
262
|
);
|
|
278
263
|
};
|
|
@@ -296,7 +281,6 @@ const styles = StyleSheet.create({
|
|
|
296
281
|
cardImage: {
|
|
297
282
|
width: "100%",
|
|
298
283
|
height: "100%",
|
|
299
|
-
resizeMode: "cover",
|
|
300
284
|
},
|
|
301
285
|
titleContainer: {
|
|
302
286
|
position: "absolute",
|
|
@@ -325,4 +309,4 @@ const styles = StyleSheet.create({
|
|
|
325
309
|
},
|
|
326
310
|
});
|
|
327
311
|
|
|
328
|
-
export default CarouselCardStack;
|
|
312
|
+
export default CarouselCardStack;
|