@autobusal/routes-order 1.8.0 → 1.9.3

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/Found/Found.tsx CHANGED
@@ -1,10 +1,18 @@
1
+ import { useMemo, useState } from 'react';
2
+ import { useSearchParams } from 'react-router-dom';
1
3
  import { TFunction } from 'i18next';
2
4
  import Dates from './Dates/Dates';
3
5
  import Booking from './Booking/Booking';
4
6
  import Header from './Header/Header';
5
7
  import Orders from './Orders/Orders';
8
+ import Sort from './Sort/Sort';
9
+ import Picks from './Picks/Picks';
10
+ import Filters from './Filters/Filters';
11
+ import Summary from './Summary/Summary';
12
+ import { Results, Sidebar, Listing } from '../styles';
6
13
  import { RoutesSearchForm } from '@autobusal/routes-search/types';
7
14
  import { FoundData } from '@autobusal/providers/types/routes';
15
+ import { Refinement, BucketKey, fromParams, toParams, filterItems, buildFacets, priceRange, currencyOf, isRefined } from './refine';
8
16
 
9
17
  interface Props {
10
18
  type: 'departure' | '_return'
@@ -17,23 +25,115 @@ interface Props {
17
25
  onSave: (data: FoundData) => void
18
26
  }
19
27
 
20
- const Found = ({ type, loading, data, step1, preferredStop, t, onSearch, onSave }: Props): JSX.Element => (
21
- <>
22
- <Dates type={ type } step1={ step1 } t={ t } onSearch={ onSearch } />
28
+ // every query parameter this component owns, so writing a new refinement
29
+ // clears the previous one without touching anything else on the URL
30
+ const OWNED = [ 'sort', 'operators', 'times', 'direct', 'from_stops', 'to_stops', 'features', 'max_price' ];
23
31
 
24
- { (data !== undefined && data.length > 0) && <Booking data={ data } step1={ step1 } t={ t } /> }
32
+ const Found = ({ type, loading, data, step1, preferredStop, t, onSearch, onSave }: Props): JSX.Element => {
33
+ const [ searchParams, setSearchParams ] = useSearchParams();
34
+ const [ open, setOpen ] = useState<boolean>(false);
25
35
 
26
- <Header t={ t } />
36
+ /**
37
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
38
+ *
39
+ * The refinement lives in the URL, not in component state, so a filtered
40
+ * result set survives a reload and can be sent to somebody else - which is
41
+ * how people actually share a trip they found.
42
+ */
43
+ const refinement = useMemo(() => fromParams(searchParams), [ searchParams ]);
27
44
 
28
- <Orders
29
- loading={ loading }
30
- data={ data }
31
- type={ type }
32
- preferredStop={ Number(preferredStop) }
33
- t={ t }
34
- onSave={ onSave }
35
- />
36
- </>
37
- );
45
+ const items = useMemo(() => data ?? [], [ data ]);
38
46
 
39
- export default Found;
47
+ const bucketLabel = (key: BucketKey): string => t(`routes_order.step2.filters.buckets.${ key }`);
48
+
49
+ const facets = useMemo(() => buildFacets(items, refinement, bucketLabel), [ items, refinement, t ]);
50
+
51
+ const refined = useMemo(() => filterItems(items, refinement), [ items, refinement ]);
52
+
53
+ const range = useMemo(() => priceRange(refined.length > 0 ? refined : items), [ refined, items ]);
54
+
55
+ // the slider's bounds must come from the WHOLE result set, not the
56
+ // filtered one - bounds that shrink as you drag make it impossible to
57
+ // widen the range again
58
+ const bounds = useMemo(() => priceRange(items), [ items ]);
59
+
60
+ const currency = useMemo(() => currencyOf(items), [ items ]);
61
+
62
+ const onRefine = (next: Refinement): void => {
63
+ const params = new URLSearchParams(searchParams);
64
+
65
+ OWNED.forEach(name => params.delete(name));
66
+
67
+ Object.entries(toParams(next)).forEach(([ name, value ]) => params.set(name, value));
68
+
69
+ // `replace` so a back press returns to the search form rather than
70
+ // walking back through every checkbox the user just ticked
71
+ setSearchParams(params, { replace: true });
72
+ };
73
+
74
+ const passengers = Number(step1.adults ?? 0) + Number(step1.children ?? 0) + Number(step1.babies ?? 0);
75
+
76
+ // controls are pointless while there is nothing (or only one thing) to
77
+ // refine, and they'd only add noise above an empty state
78
+ const showControls = !loading && items.length > 1;
79
+
80
+ return (
81
+ <>
82
+ <Dates type={ type } step1={ step1 } t={ t } onSearch={ onSearch } />
83
+
84
+ { (data !== undefined && data.length > 0) && <Booking data={ data } step1={ step1 } t={ t } /> }
85
+
86
+ { showControls && (
87
+ <>
88
+ <Summary shown={ refined.length } total={ items.length } range={ range } t={ t }>
89
+ <Sort value={ refinement.sort } t={ t } onChange={ sort => onRefine({ ...refinement, sort }) } />
90
+ </Summary>
91
+
92
+ <Picks
93
+ items={ refined }
94
+ sort={ refinement.sort }
95
+ t={ t }
96
+ onSelect={ sort => onRefine({ ...refinement, sort }) }
97
+ />
98
+ </>
99
+ ) }
100
+
101
+ <Results>
102
+ { showControls && (
103
+ <Sidebar>
104
+ <Filters
105
+ open={ open }
106
+ refinement={ refinement }
107
+ facets={ facets }
108
+ range={ bounds }
109
+ currency={ currency }
110
+ t={ t }
111
+ onToggle={ () => setOpen(!open) }
112
+ onChange={ onRefine }
113
+ />
114
+ </Sidebar>
115
+ ) }
116
+
117
+ <Listing>
118
+ <Header t={ t } />
119
+
120
+ <Orders
121
+ loading={ loading }
122
+ data={ refined }
123
+ empty={ items.length === 0 }
124
+ refined={ isRefined(refinement) }
125
+ sort={ refinement.sort }
126
+ type={ type }
127
+ passengers={ passengers }
128
+ preferredStop={ Number(preferredStop) }
129
+ t={ t }
130
+ onReset={ () => onRefine({ ...refinement, ...{ operators: [], buckets: [], direct: false, fromStops: [], toStops: [], features: [], maxPrice: null } }) }
131
+ onSave={ onSave }
132
+ />
133
+ </Listing>
134
+ </Results>
135
+ </>
136
+ );
137
+ };
138
+
139
+ export default Found;
@@ -0,0 +1,66 @@
1
+ import { useMemo, useState } from 'react';
2
+ import { TFunction } from 'i18next';
3
+ import Route from '../Route/Route';
4
+ import { departureOf } from '../refine';
5
+ import { Container, Title, Times, Time } from './styles';
6
+ import { FoundData } from '@autobusal/providers/types/routes';
7
+
8
+ interface Props {
9
+ items: FoundData[]
10
+ type: 'favorite' | 'normal'
11
+ passengers: number
12
+ t: TFunction<'common'>
13
+ onSave: (data: FoundData) => void
14
+ }
15
+
16
+ /**
17
+ * One card for a set of interchangeable departures.
18
+ *
19
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
20
+ *
21
+ * The card shows a real departure - never an abstract summary - and the
22
+ * other times sit under it as a picker. Whatever is selected is what gets
23
+ * booked, so there is no separate "now choose a time" step to forget: the
24
+ * card always represents a bookable trip, starting with the earliest.
25
+ */
26
+ const Group = ({ items, type, passengers, t, onSave }: Props): JSX.Element => {
27
+ // chronological for the picker, regardless of how the list itself is
28
+ // sorted - a row of times out of time order is just confusing
29
+ const departures = useMemo(() => (
30
+ [ ...items ].sort((first, second) => departureOf(first) - departureOf(second))
31
+ ), [ items ]);
32
+
33
+ const [ selected, setSelected ] = useState<number>(0);
34
+
35
+ const current = departures[selected] ?? departures[0];
36
+
37
+ return (
38
+ <Route
39
+ type={ type }
40
+ data={ current }
41
+ passengers={ passengers }
42
+ t={ t }
43
+ onSave={ onSave }
44
+ >
45
+ <Container>
46
+ <Title>{ t('routes_order.step2.route.departures', { total: departures.length }) }</Title>
47
+
48
+ <Times>
49
+ { departures.map((item, index) => (
50
+ <Time
51
+ key={ item.id }
52
+ type="button"
53
+ $active={ index === selected }
54
+ aria-pressed={ index === selected }
55
+ onClick={ () => setSelected(index) }
56
+ >
57
+ { item.locations.from.departure }
58
+ </Time>
59
+ )) }
60
+ </Times>
61
+ </Container>
62
+ </Route>
63
+ );
64
+ };
65
+
66
+ export default Group;
@@ -0,0 +1,44 @@
1
+ import styled, { css } from 'styled-components';
2
+
3
+ export const Container = styled.div`
4
+ display: flex;
5
+ flex-direction: column;
6
+ gap: 8px;
7
+ padding: 12px;
8
+ background: ${ props => props.theme.background.neutral };
9
+ border-radius: ${ props => props.theme.borderRadius };
10
+ `;
11
+
12
+ export const Title = styled.h6`
13
+ margin-bottom: 0;
14
+ font-size: ${ props => props.theme.size.xs };
15
+ text-transform: uppercase;
16
+ color: ${ props => props.theme.font.faded };
17
+ `;
18
+
19
+ export const Times = styled.div`
20
+ display: flex;
21
+ flex-wrap: wrap;
22
+ gap: 6px;
23
+ `;
24
+
25
+ export const Time = styled.button<{ $active: boolean }>`
26
+ padding: 5px 12px;
27
+ font-size: ${ props => props.theme.size.s };
28
+ font-weight: 700;
29
+ border-radius: 100px;
30
+ border: 1px solid ${ props => props.theme.inputs.border };
31
+ background: ${ props => props.theme.background.normal };
32
+ color: ${ props => props.theme.font.normal };
33
+ transition: all 0.2s ease;
34
+
35
+ &:hover {
36
+ border-color: ${ props => props.theme.primary.normal };
37
+ }
38
+
39
+ ${ props => props.$active && css`
40
+ color: ${ props => props.theme.primary.contrast };
41
+ background: ${ props => props.theme.primary.normal };
42
+ border-color: ${ props => props.theme.primary.normal };
43
+ ` }
44
+ `;
@@ -1,20 +1,27 @@
1
1
  import { TFunction } from 'i18next';
2
2
  import { Inline } from '@autobusal/common';
3
3
  import Route from '../Route/Route';
4
+ import Group from '../Group/Group';
4
5
  import { getFavorites, getNormal } from './utilities';
5
- import { Container, ContainerNotFound } from './styles';
6
+ import { sortItems, groupItems, SortKey } from '../refine';
7
+ import { Container, ContainerNotFound, Reset } from './styles';
6
8
  import { FoundData } from '@autobusal/providers/types/routes';
7
9
 
8
10
  interface Props {
9
11
  loading: boolean
10
- data?: FoundData[]
12
+ data: FoundData[]
13
+ empty: boolean
14
+ refined: boolean
15
+ sort: SortKey
11
16
  type: 'departure' | '_return'
17
+ passengers: number
12
18
  preferredStop?: number
13
19
  t: TFunction<'common'>
20
+ onReset: () => void
14
21
  onSave: (data: FoundData) => void
15
22
  }
16
23
 
17
- const Orders = ({ loading, data, type, preferredStop, t, onSave }: Props): JSX.Element => {
24
+ const Orders = ({ loading, data, empty, refined, sort, type, passengers, preferredStop, t, onReset, onSave }: Props): JSX.Element => {
18
25
  if (loading) {
19
26
  return (
20
27
  <div className="box">
@@ -23,33 +30,67 @@ const Orders = ({ loading, data, type, preferredStop, t, onSave }: Props): JSX.E
23
30
  );
24
31
  }
25
32
 
26
- // undefined means the search errored (e.g. same origin and destination) -
27
- // show the not-found message instead of an empty shell
28
- if (data === undefined || data.length === 0) {
33
+ // no results at all - the search itself found nothing (or errored, e.g.
34
+ // same origin and destination) - show the not-found message
35
+ if (empty) {
29
36
  return (
30
37
  <ContainerNotFound className="box">{ t('routes_order.step2.not_found') }</ContainerNotFound>
31
38
  );
32
39
  }
33
40
 
34
- const favorites = getFavorites(type, data, preferredStop).map(item => (
35
- <Route
36
- key={ item.id }
37
- type="favorite"
38
- data={ item }
39
- t={ t }
40
- onSave={ onSave }
41
- />
42
- ));
43
-
44
- const items = getNormal(type, data, preferredStop).map(item => (
45
- <Route
46
- key={ item.id }
47
- type="normal"
48
- data={ item }
49
- t={ t }
50
- onSave={ onSave }
51
- />
52
- ));
41
+ // Edited: Ferjolt Ozuni - Date: 2026-08-01
42
+ // Results exist but the user's own filters hid all of them. Saying
43
+ // "we couldn't find any tickets" there would be a lie that sends people
44
+ // away from a page that does have inventory - offer the way out instead.
45
+ if (data.length === 0 && refined) {
46
+ return (
47
+ <ContainerNotFound className="box">
48
+ { t('routes_order.step2.filters.empty') }
49
+
50
+ <Reset type="button" onClick={ onReset }>{ t('routes_order.step2.filters.reset') }</Reset>
51
+ </ContainerNotFound>
52
+ );
53
+ }
54
+
55
+ /**
56
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
57
+ *
58
+ * Preferred-stop pinning stays the OUTER partition and each side is
59
+ * sorted within itself. Sorting the whole list first would have let
60
+ * "cheapest" quietly un-pin a stop the user deliberately saved.
61
+ *
62
+ * Grouping runs AFTER sorting: groupItems keeps input order, so a
63
+ * collapsed set of shuttles lands wherever its best member sorted to.
64
+ */
65
+ const render = (list: FoundData[], kind: 'favorite' | 'normal'): JSX.Element[] => (
66
+ groupItems(sortItems(list, sort)).map(group => (
67
+ group.length > 1
68
+ ? (
69
+ <Group
70
+ key={ group[0].id }
71
+ items={ group }
72
+ type={ kind }
73
+ passengers={ passengers }
74
+ t={ t }
75
+ onSave={ onSave }
76
+ />
77
+ )
78
+ : (
79
+ <Route
80
+ key={ group[0].id }
81
+ type={ kind }
82
+ data={ group[0] }
83
+ passengers={ passengers }
84
+ t={ t }
85
+ onSave={ onSave }
86
+ />
87
+ )
88
+ ))
89
+ );
90
+
91
+ const favorites = render(getFavorites(type, data, preferredStop), 'favorite');
92
+
93
+ const items = render(getNormal(type, data, preferredStop), 'normal');
53
94
 
54
95
  return (
55
96
  <Container>
@@ -59,4 +100,4 @@ const Orders = ({ loading, data, type, preferredStop, t, onSave }: Props): JSX.E
59
100
  );
60
101
  };
61
102
 
62
- export default Orders;
103
+ export default Orders;
@@ -7,6 +7,20 @@ export const Container = styled.div`
7
7
  `;
8
8
 
9
9
  export const ContainerNotFound = styled.div`
10
+ display: flex;
11
+ flex-direction: column;
12
+ align-items: center;
13
+ gap: 12px;
10
14
  text-align: center;
11
15
  color: ${ props => props.theme.font.faded };
16
+ `;
17
+
18
+ export const Reset = styled.button`
19
+ font-weight: 700;
20
+ color: ${ props => props.theme.primary.normal };
21
+ transition: all 0.2s ease;
22
+
23
+ &:hover {
24
+ opacity: .8;
25
+ }
12
26
  `;
@@ -0,0 +1,68 @@
1
+ import { TFunction } from 'i18next';
2
+ import { Container, Pick, Label, Value, Meta } from './styles';
3
+ import { SortKey, sortItems } from '../refine';
4
+ import { FoundData } from '@autobusal/providers/types/routes';
5
+
6
+ interface Props {
7
+ items: FoundData[]
8
+ sort: SortKey
9
+ t: TFunction<'common'>
10
+ onSelect: (sort: SortKey) => void
11
+ }
12
+
13
+ const OFFERED: SortKey[] = [ 'cheapest', 'fastest', 'recommended' ];
14
+
15
+ /**
16
+ * Three cards naming the best trip on each axis.
17
+ *
18
+ * Edited: Ferjolt Ozuni - Date: 2026-08-01
19
+ *
20
+ * They answer "what are my options here" before anyone reads a single row,
21
+ * and clicking one applies that ordering to the list below - so the card is
22
+ * both an answer and a control.
23
+ *
24
+ * Computed from the FILTERED list, so a pick never advertises a trip the
25
+ * user's own filters have hidden.
26
+ *
27
+ * "Recommended" is merit-first - price rank plus duration rank, with paid
28
+ * subscription priority only breaking ties. See `recommend` in refine.ts.
29
+ */
30
+ const Picks = ({ items, sort, t, onSelect }: Props): JSX.Element | null => {
31
+ // below a handful of results the cards just restate the list underneath
32
+ if (items.length < 3) {
33
+ return null;
34
+ }
35
+
36
+ return (
37
+ <Container>
38
+ { OFFERED.map(key => {
39
+ const best = sortItems(items, key)[0];
40
+
41
+ if (best === undefined) {
42
+ return null;
43
+ }
44
+
45
+ return (
46
+ <Pick
47
+ key={ key }
48
+ type="button"
49
+ $active={ sort === key }
50
+ aria-pressed={ sort === key }
51
+ onClick={ () => onSelect(key) }
52
+ >
53
+ <Label>{ t(`routes_order.step2.sort.${ key }`) }</Label>
54
+
55
+ <Value>{ best.price.display }</Value>
56
+
57
+ <Meta>
58
+ { best.duration !== '-' && `${ best.duration } · ` }
59
+ { best.operator.company }
60
+ </Meta>
61
+ </Pick>
62
+ );
63
+ }) }
64
+ </Container>
65
+ );
66
+ };
67
+
68
+ export default Picks;
@@ -0,0 +1,55 @@
1
+ import styled, { css } from 'styled-components';
2
+
3
+ export const Container = styled.div`
4
+ display: grid;
5
+ gap: 10px;
6
+ margin-bottom: 15px;
7
+ grid-template-columns: 1fr;
8
+
9
+ @media (min-width: 640px) {
10
+ grid-template-columns: repeat(3, 1fr);
11
+ }
12
+ `;
13
+
14
+ export const Pick = styled.button<{ $active: boolean }>`
15
+ display: flex;
16
+ flex-direction: column;
17
+ align-items: flex-start;
18
+ gap: 4px;
19
+ padding: 12px 15px;
20
+ text-align: left;
21
+ border-radius: ${ props => props.theme.borderRadius };
22
+ border: 1px solid ${ props => props.theme.inputs.border };
23
+ background: ${ props => props.theme.background.normal };
24
+ transition: all 0.2s ease;
25
+
26
+ &:hover {
27
+ border-color: ${ props => props.theme.primary.normal };
28
+ }
29
+
30
+ ${ props => props.$active && css`
31
+ border-color: ${ props => props.theme.primary.normal };
32
+ box-shadow: inset 0 0 0 1px ${ props => props.theme.primary.normal };
33
+ ` }
34
+ `;
35
+
36
+ export const Label = styled.span`
37
+ font-size: ${ props => props.theme.size.xs };
38
+ text-transform: uppercase;
39
+ letter-spacing: .5px;
40
+ font-weight: 700;
41
+ color: ${ props => props.theme.font.faded };
42
+ `;
43
+
44
+ export const Value = styled.strong`
45
+ font-size: ${ props => props.theme.size.l };
46
+ `;
47
+
48
+ export const Meta = styled.span`
49
+ overflow: hidden;
50
+ max-width: 100%;
51
+ font-size: ${ props => props.theme.size.xs };
52
+ color: ${ props => props.theme.font.faded };
53
+ text-overflow: ellipsis;
54
+ white-space: nowrap;
55
+ `;
@@ -1,9 +1,11 @@
1
1
  import { useState } from 'react';
2
2
  import { TFunction } from 'i18next';
3
3
  import { HiOutlineArrowNarrowRight, HiOutlineDocumentText } from 'react-icons/hi';
4
+ import { BiHourglass } from 'react-icons/bi';
4
5
  import { RiBus2Line } from 'react-icons/ri';
6
+ import { formatDuration } from '../refine';
5
7
  import { Button, RouteFeature, Policy } from '@autobusal/common';
6
- import { Container, SpecialOffer, FavoriteStop, Trip, Company, Location, Time, Price, LinkCompany, Details, Detail, ButtonPolicy, TitleDetail, LinkRoute, Features, TitleFeatures, FeaturesItems } from './styles';
8
+ import { Container, SpecialOffer, FavoriteStop, Trip, Company, Location, Stop, Iso, Time, Price, PriceNotice, LinkCompany, Details, Detail, ButtonPolicy, TitleDetail, LinkRoute, Features, TitleFeatures, FeaturesItems } from './styles';
7
9
  import { FoundData } from '@autobusal/providers/types/routes';
8
10
 
9
11
  // neutral placeholder so a missing/broken operator logo doesn't render the
@@ -13,11 +15,16 @@ const LOGO_FALLBACK = "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/s
13
15
  interface Props {
14
16
  data: FoundData
15
17
  type: 'favorite' | 'normal'
18
+ passengers: number
16
19
  t: TFunction<'common'>
17
20
  onSave: (data: FoundData) => void
21
+ // Edited: Ferjolt Ozuni - Date: 2026-08-01
22
+ // Slot under the trip row, used by Group to hang a departure-time picker
23
+ // on a card that stands for several interchangeable departures.
24
+ children?: React.ReactNode
18
25
  }
19
26
 
20
- const Route = ({ data, type, t, onSave }: Props): JSX.Element => {
27
+ const Route = ({ data, type, passengers, t, onSave, children }: Props): JSX.Element => {
21
28
  const [ policy, setPolicy ] = useState<boolean>(false);
22
29
 
23
30
  const customs = Object.values(data.customs).join(', ');
@@ -49,23 +56,53 @@ const Route = ({ data, type, t, onSave }: Props): JSX.Element => {
49
56
  </Company>
50
57
 
51
58
  <Location>
52
- <h2>{ data.locations.from.city.name } ({ data.locations.from.city.country?.iso_code }) { data.locations.from.departure }</h2>
53
- <span>{ data.locations.from.stop?.name }</span>
59
+ <h2>
60
+ { data.locations.from.city.name }
61
+ <Iso>({ data.locations.from.city.country?.iso_code })</Iso>
62
+ { data.locations.from.departure }
63
+ </h2>
64
+
65
+ <Stop>{ data.locations.from.stop?.name }</Stop>
54
66
  </Location>
55
67
 
56
68
  <Time>
57
69
  <HiOutlineArrowNarrowRight />
58
- <span>{ data.duration }</span>
70
+
71
+ { /* Edited: Ferjolt Ozuni - Date: 2026-08-01
72
+ The clock is the point: this number sits between two real
73
+ departure times, and "08:30" gave nothing to distinguish a
74
+ trip length from a time of day. */ }
75
+ <span><BiHourglass />{ formatDuration(data.duration, t) }</span>
59
76
  </Time>
60
77
 
61
78
  <Location>
62
- <h2>{ data.locations.to.city.name } ({ data.locations.to.city.country?.iso_code }) { data.locations.to.arrival }</h2>
63
- <span>{ data.locations.to.stop?.name }</span>
79
+ <h2>
80
+ { data.locations.to.city.name }
81
+ <Iso>({ data.locations.to.city.country?.iso_code })</Iso>
82
+ { data.locations.to.arrival }
83
+ </h2>
84
+
85
+ <Stop>{ data.locations.to.stop?.name }</Stop>
64
86
  </Location>
65
87
 
66
88
  <Price>
67
89
  <strong>{ data.price.display }</strong>
68
90
 
91
+ { /* Edited: Ferjolt Ozuni - Date: 2026-08-01
92
+ obtapi's price.value is the total for the WHOLE party
93
+ (adult * adults + child * children + baby * babies), not a
94
+ per-person fare - so this says how many people it covers
95
+ rather than the "per adult" line the competitors use, which
96
+ would be wrong here for any multi-passenger search. */ }
97
+ <PriceNotice>
98
+ { t(
99
+ passengers === 1
100
+ ? 'routes_order.step2.route.price_notice_single'
101
+ : 'routes_order.step2.route.price_notice_multiple',
102
+ { total: passengers }
103
+ ) }
104
+ </PriceNotice>
105
+
69
106
  <Button
70
107
  type="button"
71
108
  size="medium"
@@ -81,6 +118,8 @@ const Route = ({ data, type, t, onSave }: Props): JSX.Element => {
81
118
  </Price>
82
119
  </Trip>
83
120
 
121
+ { children }
122
+
84
123
  <Details>
85
124
  <Detail>
86
125
  <TitleDetail>{ t('routes_order.step2.route.name') }</TitleDetail>