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.
@@ -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
- // import { ScrollView } from "react-native-gesture-handler";
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
- newRoom[guest.key] = guest.defaultValue;
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? : StyleProp<ViewStyle>;
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 handleGuestChange = useCallback(
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 === roomId) {
177
- const currentVal = room[guestKey];
178
- let newValue = currentVal;
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
- newValue = Math.min(maxValue, currentVal + 1);
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
- newValue = Math.max(minValue, currentVal - 1);
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 room;
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
- [triggerSelectionChange, guestConfig]
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 style={[{maxHeight: 300}, roomListContainerStyle]} showsVerticalScrollIndicator={false}>
210
- {rooms.map((room, index) => (
211
- <View key={room.id} style={[styles.roomContainer, roomContainerStyle]}>
212
- <View style={[styles.roomHeader, roomHeaderStyle]}>
213
- <Text style={[styles.roomTitle, roomTitleStyle]}>
214
- {`${roomTitlePrefix} ${index + 1}`}
215
- </Text>
216
- {( rooms.length > minRooms) && (
217
- <TouchableOpacity
218
- style={removeRoomButtonStyle}
219
- onPress={() => removeRoom(room.id)}
220
- >
221
- <Text style={[styles.removeRoomText, removeRoomTextStyle]}>
222
- {removeRoomText}
223
- </Text>
224
- </TouchableOpacity>
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
- {guest.description}
246
- </Text>
247
- </View>
248
- <NumericStepper
249
- value={room[guest.key]}
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
- </View>
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: "flex-start",
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;