iceholidays-frontend 0.5.0 → 0.7.0
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.
- checksums.yaml +4 -4
- data/app/assets/stylesheets/iceholidays/frontend/_slick-theme.scss +194 -0
- data/app/assets/stylesheets/iceholidays/frontend/_slick.scss +100 -0
- data/app/assets/stylesheets/iceholidays/frontend/application.sass.scss +64 -4
- data/app/assets/stylesheets/iceholidays/frontend/common.scss +17 -2
- data/app/assets/stylesheets/iceholidays/frontend/layout.scss +1 -1
- data/app/assets/stylesheets/iceholidays/frontend/utils/_slick_overrides.scss +3 -0
- data/app/javascript/api-services/brochure-api.service.ts +17 -0
- data/app/javascript/api-services/posts-api.service.ts +47 -0
- data/app/javascript/interfaces/blog.interface.ts +8 -0
- data/app/javascript/react/App.tsx +1 -1
- data/app/javascript/react/components/Destinations.tsx +21 -10
- data/app/javascript/react/components/PriceDetails.tsx +6 -6
- data/app/javascript/react/components/Testimonials.tsx +4 -1
- data/app/javascript/react/components/shared/LocationDropdown.tsx +3 -1
- data/app/javascript/react/layouts/MainHeader.tsx +80 -32
- data/app/javascript/react/pages/AboutUsPage.tsx +4 -4
- data/app/javascript/react/pages/BlogPage.tsx +82 -66
- data/app/javascript/react/pages/BlogShowPage.tsx +50 -35
- data/app/javascript/react/pages/ListingPage.tsx +115 -85
- data/app/javascript/react/pages/ShowPage.tsx +124 -87
- data/app/javascript/react/widgets/FilterPills.tsx +21 -18
- data/config/routes.rb +1 -1
- data/lib/iceholidays/frontend/version.rb +1 -1
- data/public/iceholidays-assets/application.css +306 -11
- data/public/iceholidays-assets/application.js +161 -121
- data/public/iceholidays-assets/application.js.map +4 -4
- data/public/iceholidays-assets/images/TST Ribbon@2x.png +0 -0
- data/public/iceholidays-assets/images/TST Ribbon@3x.png +0 -0
- data/public/iceholidays-assets/images/TSTRibbon.png +0 -0
- metadata +11 -2
|
@@ -12,6 +12,11 @@ import { Itinerary } from "../../interfaces/itinerary.interface";
|
|
|
12
12
|
|
|
13
13
|
import createDOMPurify from 'dompurify'
|
|
14
14
|
import LocationDropdown from "../components/shared/LocationDropdown";
|
|
15
|
+
import ReactMarkdown from "react-markdown";
|
|
16
|
+
import Markdown from "react-markdown";
|
|
17
|
+
import remarkGfm from "remark-gfm";
|
|
18
|
+
import rehypeRaw from "rehype-raw";
|
|
19
|
+
import rehypeSanitize from "rehype-sanitize";
|
|
15
20
|
|
|
16
21
|
const DOMPurify = createDOMPurify(window)
|
|
17
22
|
|
|
@@ -38,7 +43,7 @@ class ListingPage extends React.Component <{searchParams}> {
|
|
|
38
43
|
|
|
39
44
|
state = {
|
|
40
45
|
countries: [],
|
|
41
|
-
selectedCountry: {cities: []},
|
|
46
|
+
selectedCountry: {name: "", cities: []},
|
|
42
47
|
searchParamsObj: {keyword: "", year: "", month: "", location_id: null},
|
|
43
48
|
itineraries: [],
|
|
44
49
|
location: {name: "", cover: ""},
|
|
@@ -56,15 +61,22 @@ class ListingPage extends React.Component <{searchParams}> {
|
|
|
56
61
|
}
|
|
57
62
|
|
|
58
63
|
componentDidMount() {
|
|
64
|
+
this.setSearchParamsObj();
|
|
65
|
+
|
|
59
66
|
this.locationsApi.getCountries()
|
|
60
67
|
.then(locationsData => {
|
|
61
|
-
|
|
68
|
+
var selectedCountry = locationsData[0];
|
|
69
|
+
const searchParams = this.state.searchParamsObj;
|
|
70
|
+
if(searchParams.location_id){
|
|
71
|
+
const country = locationsData.find(loc => loc.id == searchParams.location_id);
|
|
72
|
+
if(country) selectedCountry = country;
|
|
73
|
+
}
|
|
74
|
+
this.setState({countries: locationsData, selectedCountry})
|
|
75
|
+
this.getItineraries(searchParams, true);
|
|
62
76
|
})
|
|
63
77
|
.catch(error => {
|
|
64
78
|
notification.error({ message: 'An error occured while loading countries.'});
|
|
65
79
|
});
|
|
66
|
-
|
|
67
|
-
this.setSearchParamsObj();
|
|
68
80
|
|
|
69
81
|
}
|
|
70
82
|
|
|
@@ -74,19 +86,18 @@ class ListingPage extends React.Component <{searchParams}> {
|
|
|
74
86
|
searchParams.keys().forEach(key => {
|
|
75
87
|
searchParamsObj[key] = searchParams.get(key);
|
|
76
88
|
});
|
|
77
|
-
|
|
78
|
-
this.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if(locationId) this.getLocation(locationId);
|
|
82
|
-
|
|
83
|
-
this.getItineraries(searchParamsObj);
|
|
89
|
+
|
|
90
|
+
const {breadcrumbs} = this.state;
|
|
91
|
+
breadcrumbs[1] = {title: this.searchParamsToText()};
|
|
92
|
+
this.setState({searchParamsObj, breadcrumbs});
|
|
84
93
|
}
|
|
85
94
|
|
|
86
95
|
|
|
87
96
|
searchParamsToText() {
|
|
88
97
|
var {keyword, year, month} = this.state.searchParamsObj;
|
|
89
|
-
|
|
98
|
+
const monthYear = (month || year) && `${month} ${year}`;
|
|
99
|
+
const text = [(this.state.location?.name || keyword), monthYear].filter(val => val).join(", ");
|
|
100
|
+
return <span>{ text } </span>
|
|
90
101
|
}
|
|
91
102
|
|
|
92
103
|
getLocation(locationId){
|
|
@@ -109,7 +120,17 @@ class ListingPage extends React.Component <{searchParams}> {
|
|
|
109
120
|
});
|
|
110
121
|
}
|
|
111
122
|
|
|
112
|
-
getItineraries(searchParams){
|
|
123
|
+
getItineraries(searchParams, init = false){
|
|
124
|
+
const locationId = searchParams.location_id;
|
|
125
|
+
const storedSearchParamsStr = localStorage.getItem("searchParams");
|
|
126
|
+
if(storedSearchParamsStr != null){
|
|
127
|
+
const storedSearchParams = JSON.parse(storedSearchParamsStr);
|
|
128
|
+
const storedLocationId = storedSearchParams.location_id;
|
|
129
|
+
if(locationId != storedLocationId || (init && locationId)) this.getLocation(locationId);
|
|
130
|
+
}else{
|
|
131
|
+
if(locationId) this.getLocation(locationId);
|
|
132
|
+
}
|
|
133
|
+
|
|
113
134
|
this.seriesApi.getItineraries(searchParams)
|
|
114
135
|
.then(itinerariesData => {
|
|
115
136
|
this.setBreadcrumbs();
|
|
@@ -118,13 +139,16 @@ class ListingPage extends React.Component <{searchParams}> {
|
|
|
118
139
|
.catch(error => {
|
|
119
140
|
notification.error({ message: 'An error occured while loading itineraries.'});
|
|
120
141
|
});
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
localStorage.setItem("searchParams", JSON.stringify(searchParams));
|
|
121
145
|
}
|
|
122
146
|
|
|
123
147
|
setBreadcrumbs(){
|
|
124
148
|
var breadcrumbs = this.state.breadcrumbs;
|
|
125
149
|
if(breadcrumbs.length > 1){
|
|
126
150
|
const {keyword, ...noKeyword} = this.state.searchParamsObj;
|
|
127
|
-
const allHasValues = Object.entries(noKeyword).every(o => o[1] != "");
|
|
151
|
+
const allHasValues = Object.entries(noKeyword).every(o => o[1] != "" && o[1] != null);
|
|
128
152
|
if(allHasValues){
|
|
129
153
|
const searchResultsCrumb = {title: <span>Search results</span>};
|
|
130
154
|
if(breadcrumbs.length > 2){
|
|
@@ -153,7 +177,7 @@ class ListingPage extends React.Component <{searchParams}> {
|
|
|
153
177
|
if(selectedCountry){
|
|
154
178
|
const searchParamsObj = this.state.searchParamsObj;
|
|
155
179
|
searchParamsObj.location_id = selectedCountry.id;
|
|
156
|
-
this.getLocation(selectedCountry.id);
|
|
180
|
+
// this.getLocation(selectedCountry.id);
|
|
157
181
|
|
|
158
182
|
this.setState({selectedCountry, searchParamsObj});
|
|
159
183
|
this.getItineraries(searchParamsObj);
|
|
@@ -170,7 +194,7 @@ class ListingPage extends React.Component <{searchParams}> {
|
|
|
170
194
|
<div id="listing-page_header">
|
|
171
195
|
<Flex vertical gap={10}>
|
|
172
196
|
<h1>{this.searchParamsToText()}</h1>
|
|
173
|
-
<LocationDropdown locations={countries} selectLocation={this.selectCountry}/>
|
|
197
|
+
<LocationDropdown initialValue={selectedCountry.name} locations={countries} selectLocation={this.selectCountry}/>
|
|
174
198
|
</Flex>
|
|
175
199
|
</div>
|
|
176
200
|
</Headline>
|
|
@@ -180,81 +204,87 @@ class ListingPage extends React.Component <{searchParams}> {
|
|
|
180
204
|
<FilterPills items={selectedCountry.cities} bindLabel="name" bindValue="location_id" initialValue={searchParamsObj} selectFilter={(selected) => this.getItineraries(selected)}></FilterPills>
|
|
181
205
|
</Space>
|
|
182
206
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
<
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
<
|
|
210
|
-
<
|
|
211
|
-
<
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
<
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
207
|
+
{
|
|
208
|
+
itineraries.length > 0 ? (
|
|
209
|
+
<>
|
|
210
|
+
<div id="legends">
|
|
211
|
+
<Space size={17}>
|
|
212
|
+
{
|
|
213
|
+
legends.map(legend => <Badge key={legend.id} color={legend.color} text={legend.label} />)
|
|
214
|
+
}
|
|
215
|
+
</Space>
|
|
216
|
+
</div>
|
|
217
|
+
<div id="tours">
|
|
218
|
+
<Flex vertical gap={30}>
|
|
219
|
+
{
|
|
220
|
+
itineraries.map((itinerary:Itinerary) => {
|
|
221
|
+
const departureDates = itinerary.departureDate ? itinerary.departureDate.map(ddate => {
|
|
222
|
+
var type = (itinerary.guranteedDepartureDates && itinerary.guranteedDepartureDates.includes(ddate)) ? "guaranteed" :
|
|
223
|
+
(itinerary.almostGuaranteedDepartureDates && itinerary.almostGuaranteedDepartureDates.includes(ddate)) ? "almost-guaranteed" : "";
|
|
224
|
+
|
|
225
|
+
return { type, date: ddate }
|
|
226
|
+
}) : [];
|
|
227
|
+
|
|
228
|
+
return (
|
|
229
|
+
<Row key={itinerary.id} className="tour">
|
|
230
|
+
<Col flex="1 0 25%" className="column">
|
|
231
|
+
<div className="tour_image"> <img src={itinerary.images[0]}/> </div>
|
|
232
|
+
</Col>
|
|
233
|
+
<Col flex="1 0 55%" className="column">
|
|
234
|
+
<div className="tour_details">
|
|
235
|
+
<Space size={20} direction="vertical" style={{ display: 'flex' }}>
|
|
236
|
+
<Space size={10} direction="vertical">
|
|
237
|
+
<div className="tour_details_title"> {itinerary.caption} </div>
|
|
238
|
+
<div className="tour_details_info">
|
|
239
|
+
<span onClick={()=>this.showModal("descriptionModalOpen", "descriptionData", itinerary.description)}><Icon path={mdiInformationOutline} size={1}/></span>
|
|
240
|
+
</div>
|
|
241
|
+
<div>
|
|
242
|
+
<span className="tour_details_country"> <Icon path={mdiMapMarkerOutline} size="18px" /> {itinerary.country} </span>
|
|
243
|
+
<span className="tour_details_code" onClick={()=>this.showModal("itineraryModalOpen", "fileUrlData", itinerary.fileUrl)}> <Icon path={mdiFileDownload} size="18px" /> {itinerary.code} </span>
|
|
244
|
+
</div>
|
|
245
|
+
</Space>
|
|
246
|
+
<div className="tour_details_dates">
|
|
247
|
+
<Space size={6} direction="vertical" style={{ display: 'flex' }}>
|
|
248
|
+
<label>Departure Date(s)</label>
|
|
249
|
+
<div className="date-selector">
|
|
250
|
+
{
|
|
251
|
+
departureDates && departureDates.map((ddate, index) => <span key={index} style={{borderColor: getColorByType(ddate.type)}}> {ddate.date} </span>)
|
|
252
|
+
}
|
|
253
|
+
{
|
|
254
|
+
departureDates.length >= 9 && <div className="show-all-dates" onClick={()=>this.showModal("dateModalOpen", "departureDatesData", departureDates)}> <Icon path={mdiDotsHorizontalCircleOutline} size="15px" /> Show All </div>
|
|
255
|
+
}
|
|
256
|
+
</div>
|
|
257
|
+
</Space>
|
|
230
258
|
</div>
|
|
231
259
|
</Space>
|
|
232
260
|
</div>
|
|
233
|
-
</
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
</
|
|
246
|
-
</
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
261
|
+
</Col>
|
|
262
|
+
<Col flex="1 0 20%" className="column">
|
|
263
|
+
<div className="tour_pricing">
|
|
264
|
+
<Space size={20} direction="vertical" align="center">
|
|
265
|
+
<Flex className="tour_pricing_details">
|
|
266
|
+
<span>From</span>
|
|
267
|
+
<span className="price">{itinerary.priceCurrency} {itinerary.price}</span>
|
|
268
|
+
<span>All In</span>
|
|
269
|
+
</Flex>
|
|
270
|
+
<Link className="select-tour" to={`/app/itinerary/${itinerary.id}`}>Select</Link>
|
|
271
|
+
</Space>
|
|
272
|
+
</div>
|
|
273
|
+
</Col>
|
|
274
|
+
</Row>
|
|
275
|
+
)
|
|
276
|
+
})
|
|
277
|
+
}
|
|
278
|
+
</Flex>
|
|
279
|
+
</div>
|
|
280
|
+
</>
|
|
281
|
+
) : <h1 id="no-tours-found">No tour package is found.</h1>
|
|
282
|
+
}
|
|
283
|
+
|
|
254
284
|
</div>
|
|
255
285
|
|
|
256
286
|
<Modal title="Description" open={this.state.descriptionModalOpen} onCancel={()=>this.closeModal("descriptionModalOpen")} footer={null} width={1000} centered className="tour_details_description">
|
|
257
|
-
<
|
|
287
|
+
<Markdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>{descriptionData}</Markdown>
|
|
258
288
|
</Modal>
|
|
259
289
|
|
|
260
290
|
<Modal title="Itinerary" open={this.state.itineraryModalOpen} onCancel={()=>this.closeModal("itineraryModalOpen")} footer={null} width={1000} centered className="tour_details_itinerary">
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import React
|
|
2
|
-
import { Button,
|
|
3
|
-
import SlickButtonFix from "../components/shared/SlickButtonFix";
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Button, Col, Flex, Layout, Modal, notification, Row, Skeleton, Space } from "antd";
|
|
4
3
|
import { mdiAccountCash, mdiAccountGroup, mdiAirplane, mdiBagChecked, mdiBedKing, mdiClose, mdiEmailOutline, mdiFileDownload, mdiFlagTriangle, mdiMapMarker, mdiMapMarkerOutline, mdiMenuDown, mdiMenuLeft, mdiMenuRight, mdiMenuUp, mdiPhoneInTalkOutline, mdiReceiptText, mdiShieldAccount, mdiShieldAirplane, mdiSilverwareForkKnife, mdiWhatsapp } from "@mdi/js";
|
|
5
4
|
import Icon from "@mdi/react";
|
|
6
5
|
import Headline from "../components/shared/Headline";
|
|
@@ -13,11 +12,48 @@ import AgentsApi from "../../api-services/agents-api.service";
|
|
|
13
12
|
import { Agent } from "../../interfaces/agent.interface";
|
|
14
13
|
import PriceDetails from "../components/PriceDetails";
|
|
15
14
|
import ContactAgentsForm from "../components/shared/ContactAgentsForm";
|
|
15
|
+
import Markdown from "react-markdown";
|
|
16
|
+
import rehypeRaw from "rehype-raw";
|
|
17
|
+
import remarkGfm from "remark-gfm";
|
|
18
|
+
import Slider from "react-slick";
|
|
16
19
|
|
|
17
20
|
const breadcrumbs = [
|
|
18
21
|
{ title: 'Home' },
|
|
19
22
|
]
|
|
20
23
|
|
|
24
|
+
|
|
25
|
+
const settings = {
|
|
26
|
+
dots: false,
|
|
27
|
+
arrows: true,
|
|
28
|
+
slidesToShow: 7.5,
|
|
29
|
+
pauseOnHover:false,
|
|
30
|
+
speed: 500,
|
|
31
|
+
infinite: false,
|
|
32
|
+
slidesToScroll: 1,
|
|
33
|
+
prevArrow: <Icon path={mdiMenuLeft} size={2} />,
|
|
34
|
+
nextArrow: <Icon path={mdiMenuRight} size={2} />,
|
|
35
|
+
responsive: [
|
|
36
|
+
{
|
|
37
|
+
breakpoint: 1024,
|
|
38
|
+
settings: {
|
|
39
|
+
slidesToShow: 5.5,
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
breakpoint: 768,
|
|
44
|
+
settings: {
|
|
45
|
+
slidesToShow: 3.5,
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
breakpoint: 480,
|
|
50
|
+
settings: {
|
|
51
|
+
slidesToShow: 2.5,
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
]
|
|
55
|
+
};
|
|
56
|
+
|
|
21
57
|
const thingsToKnow = (inclusionsData) => {
|
|
22
58
|
return [
|
|
23
59
|
{icon: <Icon path={mdiShieldAccount} size={1} />, label: "Free Travel Insurance", visible: inclusionsData.acf},
|
|
@@ -86,11 +122,10 @@ class ShowPage extends React.Component <{params;}> {
|
|
|
86
122
|
getAgents(){
|
|
87
123
|
this.agentsApi.getAgents()
|
|
88
124
|
.then((agentsData:Agent[]) => {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
this.setState({agents: agentsData, allAgents: agentsData, loadingAgents:false, states, cities});
|
|
125
|
+
this.setState({agents: agentsData, allAgents: agentsData});
|
|
126
|
+
this.resetAgents(agentsData);
|
|
92
127
|
})
|
|
93
|
-
|
|
128
|
+
.finally(()=>this.setState({loading: false}))
|
|
94
129
|
.catch(error => {
|
|
95
130
|
notification.error({ message: 'An error occured while loading agents.'});
|
|
96
131
|
});
|
|
@@ -138,11 +173,13 @@ class ShowPage extends React.Component <{params;}> {
|
|
|
138
173
|
this.setState({states: results});
|
|
139
174
|
}
|
|
140
175
|
|
|
141
|
-
resetAgents(){
|
|
142
|
-
const allAgents:Agent[] = this.state.allAgents;
|
|
143
|
-
const states = allAgents.filter(a => a.state !=
|
|
144
|
-
const cities = allAgents.filter(a => a.city !=
|
|
145
|
-
|
|
176
|
+
resetAgents(agents?){
|
|
177
|
+
const allAgents:Agent[] = agents || this.state.allAgents;
|
|
178
|
+
const states = allAgents.filter(a => a.state?.replace(/\s/g, "") != '').map(a => a.state);
|
|
179
|
+
const cities = allAgents.filter(a => a.city?.replace(/\s/g, "") != '').map(a => a.city);
|
|
180
|
+
let uniqStates = [... new Set(states.map(x=>x))];
|
|
181
|
+
let uniqCities = [... new Set(cities.map(x=>x))];
|
|
182
|
+
this.setState({states: uniqStates, cities: uniqCities});
|
|
146
183
|
}
|
|
147
184
|
|
|
148
185
|
findAgents = (formValues) => {
|
|
@@ -170,6 +207,7 @@ class ShowPage extends React.Component <{params;}> {
|
|
|
170
207
|
const dates = itinerary.tours.length == 0 ? [] : itinerary.tours.map((tour:any) => {
|
|
171
208
|
return {id: tour.id, date: tour.departure_date, price: `${itinerary.priceCurrency} ${tour.price}`}
|
|
172
209
|
});
|
|
210
|
+
const tourInclusives = thingsToKnow(itinerary.includings);
|
|
173
211
|
|
|
174
212
|
return (
|
|
175
213
|
<div id="show-page">
|
|
@@ -190,31 +228,21 @@ class ShowPage extends React.Component <{params;}> {
|
|
|
190
228
|
</Headline>
|
|
191
229
|
|
|
192
230
|
<div id="date-selector">
|
|
193
|
-
<
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
<
|
|
197
|
-
|
|
231
|
+
<Slider {...settings}>
|
|
232
|
+
{
|
|
233
|
+
dates.map((tour, index) => {
|
|
234
|
+
return <div key={index} className={`date-box ${selectedTour?.id == tour.id && "selected"}`} onClick={()=>this.selectTour(tour.id)}>
|
|
235
|
+
<Flex justify="space-between" align="center" vertical gap={8}>
|
|
236
|
+
<div className="date-box_date"> {tour.date} </div>
|
|
237
|
+
<div>
|
|
238
|
+
<span>From</span>
|
|
239
|
+
<div className="date-box_price"> {tour.price} </div>
|
|
240
|
+
</div>
|
|
241
|
+
</Flex>
|
|
242
|
+
</div>
|
|
243
|
+
})
|
|
198
244
|
}
|
|
199
|
-
|
|
200
|
-
<SlickButtonFix>
|
|
201
|
-
<Icon path={mdiMenuRight} size={2} />
|
|
202
|
-
</SlickButtonFix>
|
|
203
|
-
}>
|
|
204
|
-
{
|
|
205
|
-
dates.map((tour, index) => {
|
|
206
|
-
return <div key={index} className={`date-box ${selectedTour?.id == tour.id && "selected"}`} onClick={()=>this.selectTour(tour.id)}>
|
|
207
|
-
<Flex justify="space-between" align="center" vertical gap={8}>
|
|
208
|
-
<div className="date-box_date"> {tour.date} </div>
|
|
209
|
-
<div>
|
|
210
|
-
<span>From</span>
|
|
211
|
-
<div className="date-box_price"> {tour.price} </div>
|
|
212
|
-
</div>
|
|
213
|
-
</Flex>
|
|
214
|
-
</div>
|
|
215
|
-
})
|
|
216
|
-
}
|
|
217
|
-
</Carousel>
|
|
245
|
+
</Slider>
|
|
218
246
|
</div>
|
|
219
247
|
</>
|
|
220
248
|
|
|
@@ -230,36 +258,41 @@ class ShowPage extends React.Component <{params;}> {
|
|
|
230
258
|
) : (
|
|
231
259
|
<div className="details">
|
|
232
260
|
<Flex gap={10} vertical>
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
261
|
+
{
|
|
262
|
+
(tourInclusives.every(x => x.visible) || selectedTour?.guaranteed_departure) && (
|
|
263
|
+
<div className="details-container">
|
|
264
|
+
<div className="details-container_header"> Tour Inclusive </div>
|
|
265
|
+
<div className="details-container_content">
|
|
266
|
+
<div id="things-to-know">
|
|
267
|
+
{
|
|
268
|
+
tourInclusives.filter(item => item.visible)
|
|
269
|
+
.map(item => (
|
|
270
|
+
<div className="item">
|
|
271
|
+
<div className="icon"> {item.icon} </div>
|
|
272
|
+
<label>{item.label}</label>
|
|
273
|
+
</div>
|
|
274
|
+
))
|
|
275
|
+
}
|
|
276
|
+
{
|
|
277
|
+
selectedTour?.guaranteed_departure && (
|
|
278
|
+
<div className="item guaranteed">
|
|
279
|
+
<div className="icon"> <Icon path={mdiShieldAirplane} size={1} /></div>
|
|
280
|
+
<label>Guaranteed Departure</label>
|
|
281
|
+
</div>
|
|
282
|
+
)
|
|
283
|
+
}
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
255
286
|
</div>
|
|
256
|
-
|
|
257
|
-
|
|
287
|
+
)
|
|
288
|
+
}
|
|
258
289
|
|
|
259
290
|
<div className="details-container">
|
|
260
291
|
<div className="details-container_header"> Description </div>
|
|
261
292
|
<div className="details-container_content">
|
|
262
|
-
<div id="description" className={
|
|
293
|
+
<div id="description" className={`${isCollapsed && 'collapsed'}`}>
|
|
294
|
+
<Markdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>{itinerary?.description}</Markdown>
|
|
295
|
+
</div>
|
|
263
296
|
<Button className="toggle-description" color="default" variant="filled" block onClick={this.toggleDescription}>{ isCollapsed ? 'Expand' : 'Collapse'}
|
|
264
297
|
<Icon path={isCollapsed ? mdiMenuDown : mdiMenuUp} size="19px" />
|
|
265
298
|
</Button>
|
|
@@ -272,41 +305,45 @@ class ShowPage extends React.Component <{params;}> {
|
|
|
272
305
|
<div className="details-container_header"> Schedule Flight(s) </div>
|
|
273
306
|
<div className="details-container_content">
|
|
274
307
|
<Flex gap={20} vertical>
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
<
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
<label> Kuala Lumpur (KUL) > Guangzhou (CAN) </label>
|
|
282
|
-
<div className="flight-details_airline">
|
|
283
|
-
<img src="/iceholidays-assets/images/china_southern_airlines_icon.png" />
|
|
284
|
-
<span>China Southern Airlines CZ366</span>
|
|
285
|
-
</div>
|
|
286
|
-
</div>
|
|
287
|
-
<div className="flight-time">
|
|
288
|
-
<Flex gap={13}>
|
|
289
|
-
<div className="from">
|
|
290
|
-
<label>08:55</label>
|
|
291
|
-
<span>Kuala Lumpur (KUL)</span>
|
|
308
|
+
{
|
|
309
|
+
selectedTour?.flights.map((flight:any) => (
|
|
310
|
+
<div className="schedule" key={flight.id}>
|
|
311
|
+
<div className="flight-date">
|
|
312
|
+
<Icon path={mdiAirplane} size="12px" />
|
|
313
|
+
<span>{flight.departure_date}</span>
|
|
292
314
|
</div>
|
|
293
|
-
<div className="flight-
|
|
294
|
-
<
|
|
315
|
+
<div className="flight-details">
|
|
316
|
+
<label> {flight.from_airport} > {flight.to_airport} </label>
|
|
317
|
+
<div className="flight-details_airline">
|
|
318
|
+
<img src={flight.airline_logo} />
|
|
319
|
+
<span>{flight.airline}</span>
|
|
320
|
+
</div>
|
|
295
321
|
</div>
|
|
296
|
-
<div className="
|
|
297
|
-
<
|
|
298
|
-
|
|
322
|
+
<div className="flight-time">
|
|
323
|
+
<Flex gap={13}>
|
|
324
|
+
<div className="from">
|
|
325
|
+
<label>{flight.departure_time}</label>
|
|
326
|
+
<span>{flight.from_airport}</span>
|
|
327
|
+
</div>
|
|
328
|
+
<div className="flight-time_icon">
|
|
329
|
+
<img src="/iceholidays-assets/images/plane.png" />
|
|
330
|
+
</div>
|
|
331
|
+
<div className="to">
|
|
332
|
+
<label>{flight.arrival_time}</label>
|
|
333
|
+
<span>{flight.to_airport}</span>
|
|
334
|
+
</div>
|
|
335
|
+
</Flex>
|
|
299
336
|
</div>
|
|
300
|
-
</
|
|
301
|
-
|
|
302
|
-
|
|
337
|
+
</div>
|
|
338
|
+
))
|
|
339
|
+
}
|
|
303
340
|
</Flex>
|
|
304
341
|
</div>
|
|
305
342
|
</div>
|
|
306
343
|
)
|
|
307
344
|
}
|
|
308
345
|
|
|
309
|
-
<PriceDetails priceCurrency={itinerary.priceCurrency} prices={selectedTour?.prices}/>
|
|
346
|
+
{selectedTour?.prices.length > 0 && <PriceDetails priceCurrency={itinerary.priceCurrency} prices={selectedTour?.prices}/>}
|
|
310
347
|
|
|
311
348
|
</Flex>
|
|
312
349
|
</div>
|