iceholidays-frontend 0.6.0 → 0.8.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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/iceholidays/frontend/_slick-theme.scss +194 -0
  3. data/app/assets/stylesheets/iceholidays/frontend/_slick.scss +100 -0
  4. data/app/assets/stylesheets/iceholidays/frontend/application.sass.scss +56 -3
  5. data/app/assets/stylesheets/iceholidays/frontend/common.scss +14 -0
  6. data/app/assets/stylesheets/iceholidays/frontend/layout.scss +11 -2
  7. data/app/assets/stylesheets/iceholidays/frontend/utils/_slick_overrides.scss +3 -0
  8. data/app/javascript/api-services/brochure-api.service.ts +17 -0
  9. data/app/javascript/api-services/contact-us-api.service.ts +19 -0
  10. data/app/javascript/api-services/posts-api.service.ts +47 -0
  11. data/app/javascript/api-services/testimonials-api.service.ts +1 -1
  12. data/app/javascript/interfaces/blog.interface.ts +8 -0
  13. data/app/javascript/interfaces/testimonial.interface.ts +1 -1
  14. data/app/javascript/react/App.tsx +1 -1
  15. data/app/javascript/react/components/Destinations.tsx +21 -10
  16. data/app/javascript/react/components/Testimonials.tsx +5 -2
  17. data/app/javascript/react/components/shared/LocationDropdown.tsx +3 -1
  18. data/app/javascript/react/index.js +2 -1
  19. data/app/javascript/react/layouts/MainFooter.tsx +43 -35
  20. data/app/javascript/react/layouts/MainHeader.tsx +80 -32
  21. data/app/javascript/react/layouts/MainLayout.tsx +1 -1
  22. data/app/javascript/react/pages/BlogPage.tsx +82 -66
  23. data/app/javascript/react/pages/BlogShowPage.tsx +50 -35
  24. data/app/javascript/react/pages/ContactUsPage.tsx +128 -101
  25. data/app/javascript/react/pages/ListingPage.tsx +42 -18
  26. data/app/javascript/react/pages/ShowPage.tsx +84 -52
  27. data/app/javascript/react/widgets/FilterPills.tsx +21 -18
  28. data/app/views/iceholidays/frontend/site/index.html.erb +7 -1
  29. data/config/routes.rb +1 -1
  30. data/lib/iceholidays/frontend/version.rb +1 -1
  31. data/public/iceholidays-assets/application.css +304 -9
  32. data/public/iceholidays-assets/application.js +161 -121
  33. data/public/iceholidays-assets/application.js.map +4 -4
  34. data/public/iceholidays-assets/images/TST Ribbon@2x.png +0 -0
  35. data/public/iceholidays-assets/images/TST Ribbon@3x.png +0 -0
  36. metadata +11 -2
@@ -1,8 +1,9 @@
1
1
  import React from "react";
2
2
  import Headline from "../components/shared/Headline";
3
- import { Button, Col, Flex, Form, Input, Row, Select, Space } from "antd";
3
+ import { Button, Col, Flex, Form, Input, notification, Row, Select, Space } from "antd";
4
4
  import { mdiFacebook, mdiInstagram, mdiTwitter, mdiYoutube } from "@mdi/js";
5
5
  import Icon from "@mdi/react";
6
+ import ContactUsApi from "../../api-services/contact-us-api.service";
6
7
  const { TextArea } = Input;
7
8
 
8
9
  const bannerPath = '/iceholidays-assets/images/contact_us.png';
@@ -11,112 +12,138 @@ const breadcrumbs = [
11
12
  { title: 'Contact Us' }
12
13
  ]
13
14
 
14
- const ContactUsPage = React.FC = () => {
15
- const [form] = Form.useForm();
16
-
17
- return (
18
- <div id="contact-us-page">
19
- <Headline bannerImage={bannerPath} breadcrumbs={breadcrumbs} title="contact us"/>
20
-
21
- <div id="contact-us-page_body">
22
- <img src="/iceholidays-assets/images/contact_us_form.png"/>
23
-
24
- <div id="contact-us-form">
25
- <div id="contact-us-form_header">
26
- <h2>Get in touch</h2>
27
- <span>Send us your enquiry</span>
28
- </div>
29
- <Form
30
- form={form}
31
- autoComplete="off"
32
- layout="vertical"
33
- variant="filled"
34
- >
35
- <Form.Item label="Enter Name" name="name">
36
- <Input placeholder="Enter Here" />
37
- </Form.Item>
38
15
 
39
- <Row gutter={20}>
40
- <Col span={12}>
41
- <Form.Item label="Enter Email" name="email">
42
- <Input placeholder="Enter Here" />
43
- </Form.Item>
44
- </Col>
45
- <Col span={12}>
46
- <Form.Item label="Enter Phone Number" name="phoneNumber">
47
- <Input placeholder="Enter Here" />
48
- </Form.Item>
49
- </Col>
50
- </Row>
16
+ const validateMessages = {
17
+ required: '${label} is required!',
18
+ types: {
19
+ email: '${label} is not a valid email!',
20
+ number: '${label} is not a valid number!',
21
+ },
22
+ number: {
23
+ range: '${label} must be between ${min} and ${max}',
24
+ },
25
+ };
51
26
 
52
- <Form.Item label="Select State" name="state">
53
- <Select
54
- placeholder="Please Select"
55
- defaultValue="lucy"
56
- options={[
57
- { value: 'jack', label: 'Jack' },
58
- ]}
59
- />
60
- </Form.Item>
61
27
 
62
- <Form.Item label="Select Area" name="area">
63
- <Select
64
- placeholder="Please Select"
65
- defaultValue="lucy"
66
- options={[
67
- { value: 'jack', label: 'Jack' },
68
- ]}
69
- />
70
- </Form.Item>
28
+ export default class ContactUsPage extends React.Component {
29
+ api = new ContactUsApi;
71
30
 
72
- <Form.Item label="Select Subject" name="subject">
73
- <Select
74
- placeholder="Please Select"
75
- defaultValue="lucy"
76
- options={[
77
- { value: 'jack', label: 'Jack' },
78
- ]}
79
- />
80
- </Form.Item>
31
+ state = {
32
+ states: []
33
+ }
81
34
 
82
- <Form.Item label="Write your message" name="message">
83
- <TextArea rows={4} placeholder="Write Here"/>
84
- </Form.Item>
85
35
 
86
- <Flex align="center" justify="center">
87
- <Button type="primary" htmlType="submit">
88
- Submit
89
- </Button>
90
- </Flex>
36
+ componentDidMount() {
37
+ this.api.getStates()
38
+ .then(statesData => {
39
+ this.setState({states: statesData})
40
+ })
41
+ .catch(error => {
42
+ notification.error({ message: 'An error occured while loading states.'});
43
+ });
44
+ }
91
45
 
92
- </Form>
93
- </div>
94
- </div>
46
+ submitForm(formValues) {
47
+ console.log(formValues)
48
+ }
95
49
 
96
- <div id="contact-us-page_footer">
97
- <Row gutter={[29, 20]} >
98
- <Col xs={24} sm={24} lg={12}>
99
- <div>
100
- <label>Contact Us</label>
101
- <span>feedback@gd.my</span>
102
- </div>
103
- </Col>
104
- <Col xs={24} sm={24} lg={12}>
105
- <div>
106
- <label>Connect with Us</label>
107
- <Space size={29}>
108
- <Icon path={mdiFacebook} size={1} />
109
- <Icon path={mdiInstagram} size={1} />
110
- <Icon path={mdiTwitter} size={1} />
111
- <Icon path={mdiYoutube} size={1} />
112
- <img src="/iceholidays-assets/images/social.png" className="social-icon"/>
113
- </Space>
50
+ render(){
51
+ return (
52
+ <div id="contact-us-page">
53
+ <Headline bannerImage={bannerPath} breadcrumbs={breadcrumbs} title="contact us"/>
54
+
55
+ <div id="contact-us-page_body">
56
+ <img src="/iceholidays-assets/images/contact_us_form.png"/>
57
+
58
+ <div id="contact-us-form">
59
+ <div id="contact-us-form_header">
60
+ <h2>Get in touch</h2>
61
+ <span>Send us your enquiry</span>
114
62
  </div>
115
- </Col>
116
- </Row>
117
- </div>
118
- </div>
119
- );
120
- };
121
-
122
- export default ContactUsPage;
63
+ <Form
64
+ autoComplete="off"
65
+ layout="vertical"
66
+ variant="filled"
67
+ validateMessages={validateMessages}
68
+ onFinish={(formValues)=>this.submitForm(formValues)}
69
+ >
70
+ <Form.Item label="Enter Name" name="name">
71
+ <Input placeholder="Enter Here" />
72
+ </Form.Item>
73
+
74
+ <Row gutter={20}>
75
+ <Col span={12}>
76
+ <Form.Item label="Enter Email" name="email" rules={[{ type: 'email' }]}>
77
+ <Input placeholder="Enter Here" />
78
+ </Form.Item>
79
+ </Col>
80
+ <Col span={12}>
81
+ <Form.Item label="Enter Phone Number" name="phoneNumber">
82
+ <Input placeholder="Enter Here" />
83
+ </Form.Item>
84
+ </Col>
85
+ </Row>
86
+
87
+ <Form.Item label="Select State" name="state">
88
+ <Select
89
+ placeholder="Please Select"
90
+ options={this.state?.states}
91
+ />
92
+ </Form.Item>
93
+
94
+ <Form.Item label="Select Area" name="area">
95
+ <Input placeholder="Enter Here" />
96
+ </Form.Item>
97
+
98
+ <Form.Item label="Select Subject" name="subject">
99
+ <Input placeholder="Enter Here" />
100
+ </Form.Item>
101
+
102
+ <Form.Item label="Write your message" name="message">
103
+ <TextArea rows={4} placeholder="Write Here"/>
104
+ </Form.Item>
105
+
106
+ <Flex align="center" justify="center">
107
+ <Button type="primary" htmlType="submit">
108
+ Submit
109
+ </Button>
110
+ </Flex>
111
+
112
+ </Form>
113
+ </div>
114
+ </div>
115
+
116
+ <div id="contact-us-page_footer">
117
+ <Row gutter={[29, 20]} >
118
+ <Col xs={24} sm={24} lg={12}>
119
+ <div>
120
+ <label>Contact Us</label>
121
+ <span>feedback@gd.my</span>
122
+ </div>
123
+ </Col>
124
+ <Col xs={24} sm={24} lg={12}>
125
+ <div>
126
+ <label>Connect with Us</label>
127
+ <Space size={29}>
128
+ <a target="_blank" href="https://www.facebook.com/thesignaturetour">
129
+ <Icon path={mdiFacebook} size={1} />
130
+ </a>
131
+ <a target="_blank" href="https://www.instagram.com/thesignaturetours">
132
+ <Icon path={mdiInstagram} size={1} />
133
+ </a>
134
+ <a target="_blank" href="javascript:void(0)" style={{pointerEvents: "none"}}>
135
+ <Icon path={mdiTwitter} size={1} />
136
+ </a>
137
+ <a target="_blank" href="javascript:void(0)" style={{pointerEvents: "none"}}>
138
+ <Icon path={mdiYoutube} size={1} />
139
+ </a>
140
+ <img src="/iceholidays-assets/images/social.png" className="social-icon"/>
141
+ </Space>
142
+ </div>
143
+ </Col>
144
+ </Row>
145
+ </div>
146
+ </div>
147
+ );
148
+ }
149
+ }
@@ -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
- this.setState({countries: locationsData, selectedCountry: locationsData[0]})
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.setState({searchParamsObj});
79
-
80
- const locationId = searchParamsObj.location_id;
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
- return <span>{ this.state.location?.name || keyword}{`${month && `, ${month}`} ${year}`} </span>
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>
@@ -254,13 +278,13 @@ class ListingPage extends React.Component <{searchParams}> {
254
278
  </Flex>
255
279
  </div>
256
280
  </>
257
- ) : <h1 id="no-tours-found">Not tour package is found.</h1>
281
+ ) : <h1 id="no-tours-found">No tour package is found.</h1>
258
282
  }
259
283
 
260
284
  </div>
261
285
 
262
286
  <Modal title="Description" open={this.state.descriptionModalOpen} onCancel={()=>this.closeModal("descriptionModalOpen")} footer={null} width={1000} centered className="tour_details_description">
263
- <div className="pre-wrap">{ <span dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(descriptionData) }} /> }</div>
287
+ <Markdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>{descriptionData}</Markdown>
264
288
  </Modal>
265
289
 
266
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, { useState } from "react";
2
- import { Button, Carousel, Col, Flex, Form, Layout, Modal, notification, Row, Select, Skeleton, Space } from "antd";
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},
@@ -171,6 +207,7 @@ class ShowPage extends React.Component <{params;}> {
171
207
  const dates = itinerary.tours.length == 0 ? [] : itinerary.tours.map((tour:any) => {
172
208
  return {id: tour.id, date: tour.departure_date, price: `${itinerary.priceCurrency} ${tour.price}`}
173
209
  });
210
+ const tourInclusives = thingsToKnow(itinerary.includings);
174
211
 
175
212
  return (
176
213
  <div id="show-page">
@@ -191,31 +228,21 @@ class ShowPage extends React.Component <{params;}> {
191
228
  </Headline>
192
229
 
193
230
  <div id="date-selector">
194
- <Carousel infinite={false} arrows dots={false} draggable={true} slidesToScroll= {1} slidesToShow={8}
195
- prevArrow={
196
- <SlickButtonFix>
197
- <Icon path={mdiMenuLeft} size={2} />
198
- </SlickButtonFix>
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
+ })
199
244
  }
200
- nextArrow={
201
- <SlickButtonFix>
202
- <Icon path={mdiMenuRight} size={2} />
203
- </SlickButtonFix>
204
- }>
205
- {
206
- dates.map((tour, index) => {
207
- return <div key={index} className={`date-box ${selectedTour?.id == tour.id && "selected"}`} onClick={()=>this.selectTour(tour.id)}>
208
- <Flex justify="space-between" align="center" vertical gap={8}>
209
- <div className="date-box_date"> {tour.date} </div>
210
- <div>
211
- <span>From</span>
212
- <div className="date-box_price"> {tour.price} </div>
213
- </div>
214
- </Flex>
215
- </div>
216
- })
217
- }
218
- </Carousel>
245
+ </Slider>
219
246
  </div>
220
247
  </>
221
248
 
@@ -231,36 +258,41 @@ class ShowPage extends React.Component <{params;}> {
231
258
  ) : (
232
259
  <div className="details">
233
260
  <Flex gap={10} vertical>
234
- <div className="details-container">
235
- <div className="details-container_header"> Things to know </div>
236
- <div className="details-container_content">
237
- <div id="things-to-know">
238
- {
239
- thingsToKnow(itinerary.includings)
240
- .filter(item => item.visible)
241
- .map(item => (
242
- <div className="item">
243
- <div className="icon"> {item.icon} </div>
244
- <label>{item.label}</label>
245
- </div>
246
- ))
247
- }
248
- {
249
- selectedTour?.guaranteed_departure && (
250
- <div className="item guaranteed">
251
- <div className="icon"> <Icon path={mdiShieldAirplane} size={1} /></div>
252
- <label>Guaranteed Departure</label>
253
- </div>
254
- )
255
- }
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>
256
286
  </div>
257
- </div>
258
- </div>
287
+ )
288
+ }
259
289
 
260
290
  <div className="details-container">
261
291
  <div className="details-container_header"> Description </div>
262
292
  <div className="details-container_content">
263
- <div id="description" className={`pre-wrap ${isCollapsed && 'collapsed'}`}>{itinerary?.description}</div>
293
+ <div id="description" className={`${isCollapsed && 'collapsed'}`}>
294
+ <Markdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypeRaw]}>{itinerary?.description}</Markdown>
295
+ </div>
264
296
  <Button className="toggle-description" color="default" variant="filled" block onClick={this.toggleDescription}>{ isCollapsed ? 'Expand' : 'Collapse'}
265
297
  <Icon path={isCollapsed ? mdiMenuDown : mdiMenuUp} size="19px" />
266
298
  </Button>
@@ -19,10 +19,10 @@ function FilterPills(
19
19
  props: {
20
20
  items:any[];
21
21
  title?:string;
22
- initialValue:{keyword?:string, year?:string, month?: string, location_id?:any}
23
- bindLabel?:string;
24
- bindValue?:string;
25
- allOption?:boolean;
22
+ initialValue:{keyword?:string, year?:any, month?: string, location_id?:any, type?: any}
23
+ bindLabel:string;
24
+ bindValue:string;
25
+ allOption:boolean;
26
26
  selectFilter;
27
27
  }){
28
28
 
@@ -32,7 +32,7 @@ function FilterPills(
32
32
 
33
33
  const allHasValues = () => {
34
34
  const {keyword, ...noKeyword} = selected;
35
- return Object.entries(noKeyword).every(o => o[1] != "");
35
+ return Object.entries(noKeyword).every(o => o[1] != "" && o[1] != null);
36
36
  }
37
37
  const [collapseFilters, setcollapseFilters] = useState(allHasValues);
38
38
 
@@ -57,16 +57,15 @@ function FilterPills(
57
57
  { title && <div className="filter-title"> {title} </div> }
58
58
  <Space size={10} wrap>
59
59
  {
60
- allOption && <div className="filter-pill selected"> <span> All </span></div>
60
+ allOption && <div onClick={()=>selectFilter(bindValue, null)} className={`filter-pill ${(selected[bindValue] == null || selected[bindValue] == '') && 'selected'}`}> <span> All </span></div>
61
61
  }
62
62
  {
63
63
  items.map((item, index) => {
64
64
  //special case for location id
65
65
  const id:any = bindValue == "location_id" ? "id" : bindValue;
66
- const label = bindLabel ? item[bindLabel] : item;
67
66
  const value = bindValue ? item[id] : item;
68
67
  const selectedItem = selected && bindValue ? selected[bindValue]: item ;
69
- return <div key={index} onClick={()=>selectFilter(bindValue, value)} className={`filter-pill ${selectedItem == value && 'selected'}`}> <span>{ label } </span></div>
68
+ return <div key={index} onClick={()=>selectFilter(bindValue, value)} className={`filter-pill ${selectedItem == value && 'selected'}`}> <span>{ item[bindLabel] } </span></div>
70
69
  })
71
70
  }
72
71
  </Space>
@@ -77,26 +76,24 @@ function FilterPills(
77
76
  <div className="filter-title"> Year </div>
78
77
  <Space size={10} wrap>
79
78
  {
80
- allOption && <div className="filter-pill selected"> <span> All </span></div>
79
+ allOption && <div onClick={()=>selectFilter("year", "")} className={`filter-pill ${selected.year == '' && 'selected'}`}> <span> All </span></div>
81
80
  }
82
81
  {
83
- years().map((year, index) => {
84
- const selectedItem = selected && selected.year;
85
- return <div key={index} onClick={()=>selectFilter("year", year)} className={`filter-pill default-filter ${selectedItem == year.toString() && 'selected'}`}> <span>{year} </span></div>
86
- })
82
+ years().map((year, index) => (
83
+ <div key={index} onClick={()=>selectFilter("year", year)} className={`filter-pill default-filter ${selected.year == year && 'selected'}`}> <span>{year} </span></div>
84
+ ))
87
85
  }
88
86
  </Space>
89
87
 
90
88
  <div className="filter-title"> Month </div>
91
89
  <Row gutter={[10, 10]} justify="space-between">
92
90
  {
93
- allOption && <Col className="filter-pill selected"> <span> All </span></Col>
91
+ allOption && <div onClick={()=>selectFilter("month", "")} className={`filter-pill ${selected.month == '' && 'selected'}`}> <span> All </span></div>
94
92
  }
95
93
  {
96
- months.map((month, index) => {
97
- const selectedItem = selected && selected.month ;
98
- return <Col xs={8} md={6} lg={4}> <div key={index} onClick={()=>selectFilter("month", month.shortMonth)} className={`filter-pill default-filter ${selectedItem?.toString() == month.shortMonth && 'selected'}`}> <span>{month.name} </span></div> </Col>
99
- })
94
+ months.map((month, index) => (
95
+ <Col key={index} xs={8} md={6} lg={4}> <div onClick={()=>selectFilter("month", month.shortMonth)} className={`filter-pill default-filter ${selected.month == month.shortMonth && 'selected'}`}> <span>{month.name} </span></div> </Col>
96
+ ))
100
97
  }
101
98
  </Row>
102
99
  </Space>
@@ -108,4 +105,10 @@ function FilterPills(
108
105
 
109
106
  }
110
107
 
108
+ FilterPills.defaultProps = {
109
+ allOption: false,
110
+ bindLabel: "label",
111
+ bindValue: "id"
112
+ }
113
+
111
114
  export default FilterPills;