related-ui-components 2.7.6 → 2.7.7
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 +99 -81
- 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.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 +130 -120
- package/src/components/TravelBooking/HotelSummary.tsx +243 -76
|
@@ -13,7 +13,13 @@ import {
|
|
|
13
13
|
} from "react-native";
|
|
14
14
|
import { NumericStepper, NumericStepperProps } from "../NumericStepper";
|
|
15
15
|
import { BottomSheetScrollView } from "@gorhom/bottom-sheet";
|
|
16
|
-
|
|
16
|
+
import RNPickerSelect from "react-native-picker-select";
|
|
17
|
+
import { Ionicons } from "@expo/vector-icons";
|
|
18
|
+
|
|
19
|
+
export interface IndividualGuest {
|
|
20
|
+
id: string;
|
|
21
|
+
age: number;
|
|
22
|
+
}
|
|
17
23
|
|
|
18
24
|
export interface RoomState {
|
|
19
25
|
id: string;
|
|
@@ -27,6 +33,8 @@ export interface GuestConfigType {
|
|
|
27
33
|
defaultValue: number;
|
|
28
34
|
minValue: number;
|
|
29
35
|
maxValue?: number;
|
|
36
|
+
ageRange?: { min: number; max: number };
|
|
37
|
+
defaultAge?: number;
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
const DEFAULT_GUEST_CONFIG: GuestConfigType[] = [
|
|
@@ -45,6 +53,8 @@ const DEFAULT_GUEST_CONFIG: GuestConfigType[] = [
|
|
|
45
53
|
defaultValue: 0,
|
|
46
54
|
minValue: 0,
|
|
47
55
|
maxValue: 4,
|
|
56
|
+
ageRange: { min: 0, max: 17 },
|
|
57
|
+
defaultAge: 8,
|
|
48
58
|
},
|
|
49
59
|
];
|
|
50
60
|
|
|
@@ -53,7 +63,17 @@ const createNewRoom = (guestConfig: GuestConfigType[]): RoomState => {
|
|
|
53
63
|
id: Date.now().toString() + Math.random().toString(36).substring(2, 9),
|
|
54
64
|
};
|
|
55
65
|
guestConfig.forEach((guest) => {
|
|
56
|
-
|
|
66
|
+
if (guest.ageRange) {
|
|
67
|
+
newRoom[guest.key] = Array.from(
|
|
68
|
+
{ length: guest.defaultValue },
|
|
69
|
+
(_, i) => ({
|
|
70
|
+
id: `${guest.key}_${i}_${Date.now()}`,
|
|
71
|
+
age: guest.defaultAge ?? guest.ageRange?.min ?? 0,
|
|
72
|
+
})
|
|
73
|
+
);
|
|
74
|
+
} else {
|
|
75
|
+
newRoom[guest.key] = guest.defaultValue;
|
|
76
|
+
}
|
|
57
77
|
});
|
|
58
78
|
return newRoom;
|
|
59
79
|
};
|
|
@@ -67,9 +87,10 @@ export interface HotelSummaryProps {
|
|
|
67
87
|
roomTitlePrefix?: string;
|
|
68
88
|
removeRoomText?: string;
|
|
69
89
|
addRoomText?: string;
|
|
90
|
+
formatChildAgeLabel?: (guestType: string, index: number) => string;
|
|
70
91
|
containerStyle?: StyleProp<ViewStyle>;
|
|
71
92
|
roomContainerStyle?: StyleProp<ViewStyle>;
|
|
72
|
-
roomListContainerStyle
|
|
93
|
+
roomListContainerStyle?: StyleProp<ViewStyle>;
|
|
73
94
|
roomHeaderStyle?: StyleProp<ViewStyle>;
|
|
74
95
|
roomTitleStyle?: StyleProp<TextStyle>;
|
|
75
96
|
removeRoomButtonStyle?: StyleProp<ViewStyle>;
|
|
@@ -78,12 +99,15 @@ export interface HotelSummaryProps {
|
|
|
78
99
|
guestLabelContainerStyle?: StyleProp<ViewStyle>;
|
|
79
100
|
guestNameStyle?: StyleProp<TextStyle>;
|
|
80
101
|
guestDescriptionStyle?: StyleProp<TextStyle>;
|
|
102
|
+
agePickerRowStyle?: StyleProp<ViewStyle>;
|
|
103
|
+
agePickerLabelStyle?: StyleProp<TextStyle>;
|
|
81
104
|
addRoomButtonStyle?: StyleProp<ViewStyle>;
|
|
82
105
|
addRoomButtonTextStyle?: StyleProp<TextStyle>;
|
|
83
106
|
stepperContainerStyle?: NumericStepperProps["style"];
|
|
84
107
|
stepperButtonStyle?: NumericStepperProps["buttonStyle"];
|
|
85
108
|
stepperButtonTextStyle?: NumericStepperProps["buttonTextStyle"];
|
|
86
109
|
stepperValueTextStyle?: NumericStepperProps["valueTextStyle"];
|
|
110
|
+
overrideTheme?: ThemeType
|
|
87
111
|
}
|
|
88
112
|
|
|
89
113
|
const HotelSummary: React.FC<HotelSummaryProps> = ({
|
|
@@ -95,6 +119,7 @@ const HotelSummary: React.FC<HotelSummaryProps> = ({
|
|
|
95
119
|
roomTitlePrefix = "Room",
|
|
96
120
|
removeRoomText = "Remove",
|
|
97
121
|
addRoomText = "+ Add Room",
|
|
122
|
+
formatChildAgeLabel = (guestType, index) => `${guestType} ${index} Age`,
|
|
98
123
|
containerStyle,
|
|
99
124
|
roomContainerStyle,
|
|
100
125
|
roomHeaderStyle,
|
|
@@ -111,9 +136,13 @@ const HotelSummary: React.FC<HotelSummaryProps> = ({
|
|
|
111
136
|
stepperButtonStyle,
|
|
112
137
|
stepperButtonTextStyle,
|
|
113
138
|
stepperValueTextStyle,
|
|
114
|
-
roomListContainerStyle
|
|
139
|
+
roomListContainerStyle,
|
|
140
|
+
agePickerRowStyle,
|
|
141
|
+
agePickerLabelStyle,
|
|
142
|
+
overrideTheme
|
|
115
143
|
}) => {
|
|
116
|
-
const { theme } = useTheme();
|
|
144
|
+
const { theme: defaultTheme } = useTheme();
|
|
145
|
+
let theme = overrideTheme || defaultTheme;
|
|
117
146
|
const styles = useMemo(() => themedStyles(theme, I18nManager.isRTL), [theme]);
|
|
118
147
|
|
|
119
148
|
const guestConfig = useMemo(() => guestConfigProp, [guestConfigProp]);
|
|
@@ -158,7 +187,7 @@ const HotelSummary: React.FC<HotelSummaryProps> = ({
|
|
|
158
187
|
[triggerSelectionChange, minRooms]
|
|
159
188
|
);
|
|
160
189
|
|
|
161
|
-
const
|
|
190
|
+
const handleGuestCountChange = useCallback(
|
|
162
191
|
(
|
|
163
192
|
roomId: string,
|
|
164
193
|
guestKey: string,
|
|
@@ -168,27 +197,45 @@ const HotelSummary: React.FC<HotelSummaryProps> = ({
|
|
|
168
197
|
const guestInfo = guestConfig.find((g) => g.key === guestKey);
|
|
169
198
|
if (!guestInfo) return prevRooms;
|
|
170
199
|
|
|
171
|
-
const minValue = guestInfo.minValue;
|
|
172
|
-
const maxValue = guestInfo.maxValue ?? 99;
|
|
173
200
|
let valueChanged = false;
|
|
174
201
|
|
|
175
202
|
const updatedRooms = prevRooms.map((room) => {
|
|
176
|
-
if (room.id
|
|
177
|
-
|
|
178
|
-
|
|
203
|
+
if (room.id !== roomId) return room;
|
|
204
|
+
|
|
205
|
+
const currentVal = room[guestKey];
|
|
206
|
+
let newRoomState = { ...room };
|
|
179
207
|
|
|
208
|
+
if (guestInfo.ageRange) {
|
|
209
|
+
// Handle array of guests
|
|
210
|
+
const currentArray = (currentVal || []) as IndividualGuest[];
|
|
180
211
|
if (action === "increment") {
|
|
181
|
-
|
|
212
|
+
if (currentArray.length < (guestInfo.maxValue ?? 99)) {
|
|
213
|
+
const newGuest: IndividualGuest = {
|
|
214
|
+
id: `${guestKey}_${Date.now()}`,
|
|
215
|
+
age: guestInfo.defaultAge ?? guestInfo.ageRange.min,
|
|
216
|
+
};
|
|
217
|
+
newRoomState[guestKey] = [...currentArray, newGuest];
|
|
218
|
+
valueChanged = true;
|
|
219
|
+
}
|
|
182
220
|
} else if (action === "decrement") {
|
|
183
|
-
|
|
221
|
+
if (currentArray.length > guestInfo.minValue) {
|
|
222
|
+
newRoomState[guestKey] = currentArray.slice(0, -1);
|
|
223
|
+
valueChanged = true;
|
|
224
|
+
}
|
|
184
225
|
}
|
|
185
|
-
|
|
226
|
+
} else {
|
|
227
|
+
// Handle numeric count
|
|
228
|
+
const min = guestInfo.minValue;
|
|
229
|
+
const max = guestInfo.maxValue ?? 99;
|
|
230
|
+
let newValue = currentVal;
|
|
231
|
+
if (action === "increment") newValue = Math.min(max, currentVal + 1);
|
|
232
|
+
else newValue = Math.max(min, currentVal - 1);
|
|
186
233
|
if (newValue !== currentVal) {
|
|
234
|
+
newRoomState[guestKey] = newValue;
|
|
187
235
|
valueChanged = true;
|
|
188
|
-
return { ...room, [guestKey]: newValue };
|
|
189
236
|
}
|
|
190
237
|
}
|
|
191
|
-
return
|
|
238
|
+
return newRoomState;
|
|
192
239
|
});
|
|
193
240
|
|
|
194
241
|
if (valueChanged) {
|
|
@@ -197,8 +244,39 @@ const HotelSummary: React.FC<HotelSummaryProps> = ({
|
|
|
197
244
|
return updatedRooms;
|
|
198
245
|
});
|
|
199
246
|
},
|
|
200
|
-
[
|
|
247
|
+
[guestConfig, triggerSelectionChange]
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const handleIndividualAgeChange = useCallback(
|
|
251
|
+
(roomId: string, guestKey: string, guestId: string, newAge: number) => {
|
|
252
|
+
if (newAge === null || newAge === undefined) return;
|
|
253
|
+
|
|
254
|
+
setRooms((prevRooms) => {
|
|
255
|
+
const updatedRooms = prevRooms.map((room) => {
|
|
256
|
+
if (room.id === roomId) {
|
|
257
|
+
const guestArray = (room[guestKey] as IndividualGuest[]).map(
|
|
258
|
+
(guest) => (guest.id === guestId ? { ...guest, age: newAge } : guest)
|
|
259
|
+
);
|
|
260
|
+
return { ...room, [guestKey]: guestArray };
|
|
261
|
+
}
|
|
262
|
+
return room;
|
|
263
|
+
});
|
|
264
|
+
triggerSelectionChange(updatedRooms);
|
|
265
|
+
return updatedRooms;
|
|
266
|
+
});
|
|
267
|
+
},
|
|
268
|
+
[triggerSelectionChange]
|
|
201
269
|
);
|
|
270
|
+
|
|
271
|
+
const getAgePickerItems = (config: GuestConfigType | undefined) => {
|
|
272
|
+
if (!config?.ageRange) return [];
|
|
273
|
+
const { min, max } = config.ageRange;
|
|
274
|
+
return Array.from({ length: max - min + 1 }, (_, i) => ({
|
|
275
|
+
label: `${min + i}`,
|
|
276
|
+
value: min + i,
|
|
277
|
+
}));
|
|
278
|
+
};
|
|
279
|
+
|
|
202
280
|
|
|
203
281
|
useEffect(() => {
|
|
204
282
|
onSelectionChange?.(rooms);
|
|
@@ -206,64 +284,121 @@ const HotelSummary: React.FC<HotelSummaryProps> = ({
|
|
|
206
284
|
|
|
207
285
|
return (
|
|
208
286
|
<View style={[styles.container, containerStyle]}>
|
|
209
|
-
<BottomSheetScrollView
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
{
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
</View>
|
|
227
|
-
|
|
228
|
-
{guestConfig.map((guest) => (
|
|
229
|
-
<View key={guest.key} style={[styles.guestRow, guestRowStyle]}>
|
|
230
|
-
<View
|
|
231
|
-
style={[
|
|
232
|
-
styles.guestLabelContainer,
|
|
233
|
-
guestLabelContainerStyle,
|
|
234
|
-
]}
|
|
235
|
-
>
|
|
236
|
-
<Text style={[styles.guestName, guestNameStyle]}>
|
|
237
|
-
{guest.name}
|
|
238
|
-
</Text>
|
|
239
|
-
<Text
|
|
240
|
-
style={[
|
|
241
|
-
styles.guestDescription,
|
|
242
|
-
guestDescriptionStyle,
|
|
243
|
-
]}
|
|
287
|
+
<BottomSheetScrollView
|
|
288
|
+
style={[{ maxHeight: 300 }, roomListContainerStyle]}
|
|
289
|
+
showsVerticalScrollIndicator={false}
|
|
290
|
+
>
|
|
291
|
+
{rooms.map((room, index) => (
|
|
292
|
+
<View
|
|
293
|
+
key={room.id}
|
|
294
|
+
style={[styles.roomContainer, roomContainerStyle]}
|
|
295
|
+
>
|
|
296
|
+
<View style={[styles.roomHeader, roomHeaderStyle]}>
|
|
297
|
+
<Text style={[styles.roomTitle, roomTitleStyle]}>
|
|
298
|
+
{`${roomTitlePrefix} ${index + 1}`}
|
|
299
|
+
</Text>
|
|
300
|
+
{rooms.length > minRooms && (
|
|
301
|
+
<TouchableOpacity
|
|
302
|
+
style={removeRoomButtonStyle}
|
|
303
|
+
onPress={() => removeRoom(room.id)}
|
|
244
304
|
>
|
|
245
|
-
{
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
minValue={guest.minValue}
|
|
251
|
-
maxValue={guest.maxValue}
|
|
252
|
-
onIncrement={() =>
|
|
253
|
-
handleGuestChange(room.id, guest.key, "increment")
|
|
254
|
-
}
|
|
255
|
-
onDecrement={() =>
|
|
256
|
-
handleGuestChange(room.id, guest.key, "decrement")
|
|
257
|
-
}
|
|
258
|
-
style={stepperContainerStyle}
|
|
259
|
-
buttonStyle={stepperButtonStyle}
|
|
260
|
-
buttonTextStyle={stepperButtonTextStyle}
|
|
261
|
-
valueTextStyle={stepperValueTextStyle}
|
|
262
|
-
/>
|
|
305
|
+
<Text style={[styles.removeRoomText, removeRoomTextStyle]}>
|
|
306
|
+
{removeRoomText}
|
|
307
|
+
</Text>
|
|
308
|
+
</TouchableOpacity>
|
|
309
|
+
)}
|
|
263
310
|
</View>
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
311
|
+
|
|
312
|
+
{guestConfig.map((guest) => (
|
|
313
|
+
<View key={guest.key}>
|
|
314
|
+
<View style={[styles.guestRow, guestRowStyle]}>
|
|
315
|
+
<View
|
|
316
|
+
style={[
|
|
317
|
+
styles.guestLabelContainer,
|
|
318
|
+
guestLabelContainerStyle,
|
|
319
|
+
]}
|
|
320
|
+
>
|
|
321
|
+
<Text style={[styles.guestName, guestNameStyle]}>
|
|
322
|
+
{guest.name}
|
|
323
|
+
</Text>
|
|
324
|
+
<Text
|
|
325
|
+
style={[
|
|
326
|
+
styles.guestDescription,
|
|
327
|
+
guestDescriptionStyle,
|
|
328
|
+
]}
|
|
329
|
+
>
|
|
330
|
+
{guest.description}
|
|
331
|
+
</Text>
|
|
332
|
+
</View>
|
|
333
|
+
<NumericStepper
|
|
334
|
+
value={
|
|
335
|
+
guest.ageRange
|
|
336
|
+
? room[guest.key]?.length || 0
|
|
337
|
+
: room[guest.key]
|
|
338
|
+
}
|
|
339
|
+
minValue={guest.minValue}
|
|
340
|
+
maxValue={guest.maxValue}
|
|
341
|
+
onIncrement={() =>
|
|
342
|
+
handleGuestCountChange(room.id, guest.key, "increment")
|
|
343
|
+
}
|
|
344
|
+
onDecrement={() =>
|
|
345
|
+
handleGuestCountChange(room.id, guest.key, "decrement")
|
|
346
|
+
}
|
|
347
|
+
style={stepperContainerStyle}
|
|
348
|
+
buttonStyle={stepperButtonStyle}
|
|
349
|
+
buttonTextStyle={stepperButtonTextStyle}
|
|
350
|
+
valueTextStyle={stepperValueTextStyle}
|
|
351
|
+
/>
|
|
352
|
+
</View>
|
|
353
|
+
|
|
354
|
+
{guest.ageRange &&
|
|
355
|
+
room[guest.key]?.map(
|
|
356
|
+
(individualGuest: IndividualGuest, guestIndex: number) => (
|
|
357
|
+
<View
|
|
358
|
+
key={individualGuest.id}
|
|
359
|
+
style={[styles.agePickerRow, agePickerRowStyle]}
|
|
360
|
+
>
|
|
361
|
+
<Text
|
|
362
|
+
style={[styles.agePickerLabel, agePickerLabelStyle]}
|
|
363
|
+
>
|
|
364
|
+
{formatChildAgeLabel(guest.name, guestIndex + 1)}
|
|
365
|
+
</Text>
|
|
366
|
+
<View style={styles.pickerContainer}>
|
|
367
|
+
<RNPickerSelect
|
|
368
|
+
value={individualGuest.age}
|
|
369
|
+
onValueChange={(value) =>
|
|
370
|
+
handleIndividualAgeChange(
|
|
371
|
+
room.id,
|
|
372
|
+
guest.key,
|
|
373
|
+
individualGuest.id,
|
|
374
|
+
value
|
|
375
|
+
)
|
|
376
|
+
}
|
|
377
|
+
items={getAgePickerItems(guest)}
|
|
378
|
+
placeholder={{}}
|
|
379
|
+
style={{
|
|
380
|
+
inputIOS: styles.pickerInput,
|
|
381
|
+
inputAndroid: styles.pickerInput,
|
|
382
|
+
iconContainer: styles.pickerIconContainer,
|
|
383
|
+
}}
|
|
384
|
+
textInputProps={{ pointerEvents: "none" }}
|
|
385
|
+
useNativeAndroidPickerStyle={false}
|
|
386
|
+
Icon={() => (
|
|
387
|
+
<Ionicons
|
|
388
|
+
name="chevron-down"
|
|
389
|
+
size={20}
|
|
390
|
+
color={theme.helper}
|
|
391
|
+
/>
|
|
392
|
+
)}
|
|
393
|
+
/>
|
|
394
|
+
</View>
|
|
395
|
+
</View>
|
|
396
|
+
)
|
|
397
|
+
)}
|
|
398
|
+
</View>
|
|
399
|
+
))}
|
|
400
|
+
</View>
|
|
401
|
+
))}
|
|
267
402
|
</BottomSheetScrollView>
|
|
268
403
|
|
|
269
404
|
{rooms.length < maxRooms && (
|
|
@@ -311,14 +446,14 @@ const themedStyles = (theme: ThemeType, isRTL: boolean) =>
|
|
|
311
446
|
fontWeight: "500",
|
|
312
447
|
},
|
|
313
448
|
guestRow: {
|
|
314
|
-
flexDirection:"row",
|
|
449
|
+
flexDirection: "row",
|
|
315
450
|
justifyContent: "space-between",
|
|
316
451
|
alignItems: "center",
|
|
317
452
|
paddingVertical: 12,
|
|
318
453
|
},
|
|
319
454
|
guestLabelContainer: {
|
|
320
455
|
flexDirection: "column",
|
|
321
|
-
alignItems:
|
|
456
|
+
alignItems: "flex-start",
|
|
322
457
|
flexShrink: 1,
|
|
323
458
|
// marginRight: isRTL ? 0 : 8,
|
|
324
459
|
// marginLeft: isRTL ? 8 : 0,
|
|
@@ -342,6 +477,38 @@ const themedStyles = (theme: ThemeType, isRTL: boolean) =>
|
|
|
342
477
|
fontWeight: "bold",
|
|
343
478
|
color: theme.primary,
|
|
344
479
|
},
|
|
480
|
+
agePickerRow: {
|
|
481
|
+
flexDirection: "row",
|
|
482
|
+
alignItems: "center",
|
|
483
|
+
justifyContent: "space-between",
|
|
484
|
+
paddingVertical: 8,
|
|
485
|
+
paddingHorizontal: 16,
|
|
486
|
+
backgroundColor: theme.background,
|
|
487
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
488
|
+
borderColor: theme.divider,
|
|
489
|
+
},
|
|
490
|
+
agePickerLabel: {
|
|
491
|
+
fontSize: 14,
|
|
492
|
+
color: theme.onBackground,
|
|
493
|
+
},
|
|
494
|
+
pickerContainer: {
|
|
495
|
+
width: 120,
|
|
496
|
+
height: 40,
|
|
497
|
+
borderWidth: 1,
|
|
498
|
+
borderColor: theme.border,
|
|
499
|
+
borderRadius: 8,
|
|
500
|
+
justifyContent: "center",
|
|
501
|
+
},
|
|
502
|
+
pickerInput: {
|
|
503
|
+
fontSize: 14,
|
|
504
|
+
paddingHorizontal: 10,
|
|
505
|
+
paddingVertical: 8,
|
|
506
|
+
color: theme.onSurface,
|
|
507
|
+
},
|
|
508
|
+
pickerIconContainer: {
|
|
509
|
+
top: 10,
|
|
510
|
+
right: 10,
|
|
511
|
+
},
|
|
345
512
|
});
|
|
346
513
|
|
|
347
|
-
export default HotelSummary;
|
|
514
|
+
export default HotelSummary;
|