iceholidays-frontend 0.4.0 → 0.6.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 (72) hide show
  1. checksums.yaml +4 -4
  2. data/app/assets/stylesheets/iceholidays/frontend/actiontext.scss +119 -0
  3. data/app/assets/stylesheets/iceholidays/frontend/application.sass.scss +968 -355
  4. data/app/assets/stylesheets/iceholidays/frontend/common.scss +159 -74
  5. data/app/assets/stylesheets/iceholidays/frontend/layout.scss +218 -125
  6. data/app/assets/stylesheets/iceholidays/frontend/utils/_antd_overrides.scss +22 -7
  7. data/app/assets/stylesheets/iceholidays/frontend/utils/_variables.scss +2 -1
  8. data/app/assets/stylesheets/iceholidays/frontend/widgets/filter_pills.scss +19 -12
  9. data/app/assets/stylesheets/iceholidays/frontend/widgets/search_bar.scss +4 -0
  10. data/app/controllers/iceholidays/frontend/posts_controller.rb +14 -0
  11. data/app/javascript/api-services/agents-api.service.ts +33 -0
  12. data/app/javascript/api-services/locations-api.service.ts +23 -1
  13. data/app/javascript/api-services/series-api.service.ts +48 -28
  14. data/app/javascript/interfaces/agent.interface.ts +11 -0
  15. data/app/javascript/interfaces/country.interface.ts +4 -3
  16. data/app/javascript/interfaces/itinerary.interface.ts +101 -8
  17. data/app/javascript/react/App.tsx +1 -1
  18. data/app/javascript/react/components/Destinations.tsx +30 -20
  19. data/app/javascript/react/components/PriceDetails.tsx +146 -0
  20. data/app/javascript/react/components/shared/ContactAgentsForm.tsx +44 -0
  21. data/app/javascript/react/components/shared/Headline.tsx +2 -1
  22. data/app/javascript/react/components/shared/LocationDropdown.tsx +34 -0
  23. data/app/javascript/react/components/shared/{Postcard.tsx → LocationPostcards.tsx} +22 -1
  24. data/app/javascript/react/layouts/MainFooter.tsx +64 -39
  25. data/app/javascript/react/layouts/MainHeader.tsx +68 -30
  26. data/app/javascript/react/pages/AboutUsPage.tsx +8 -8
  27. data/app/javascript/react/pages/BlogPage.tsx +6 -4
  28. data/app/javascript/react/pages/ContactAgentsPage.tsx +174 -5
  29. data/app/javascript/react/pages/ContactUsPage.tsx +5 -5
  30. data/app/javascript/react/pages/CountriesPage.tsx +3 -8
  31. data/app/javascript/react/pages/Homepage.tsx +23 -13
  32. data/app/javascript/react/pages/ListingPage.tsx +197 -145
  33. data/app/javascript/react/pages/ShowPage.tsx +275 -265
  34. data/app/javascript/react/widgets/FilterPills.tsx +83 -49
  35. data/app/javascript/react/widgets/SearchBarWidget.tsx +24 -8
  36. data/app/views/iceholidays/frontend/posts/index.html.erb +9 -0
  37. data/app/views/iceholidays/frontend/posts/show.html.erb +2 -0
  38. data/config/routes.rb +2 -1
  39. data/lib/iceholidays/frontend/version.rb +1 -1
  40. data/public/iceholidays-assets/application.css +1209 -437
  41. data/public/iceholidays-assets/application.js +91 -104
  42. data/public/iceholidays-assets/application.js.map +4 -4
  43. data/public/iceholidays-assets/images/TSTRibbon.png +0 -0
  44. data/public/iceholidays-assets/images/about-us_logo_mobile.png +0 -0
  45. data/public/iceholidays-assets/images/destinations_logo.png +0 -0
  46. data/public/iceholidays-assets/images/footer-bg_mobile.png +0 -0
  47. data/public/iceholidays-assets/images/logo_mobile.png +0 -0
  48. metadata +19 -27
  49. data/public/iceholidays-assets/images/8D7N.png +0 -0
  50. data/public/iceholidays-assets/images/Frame71.png +0 -0
  51. data/public/iceholidays-assets/images/africa.png +0 -0
  52. data/public/iceholidays-assets/images/banner1.png +0 -0
  53. data/public/iceholidays-assets/images/banner2.png +0 -0
  54. data/public/iceholidays-assets/images/china.png +0 -0
  55. data/public/iceholidays-assets/images/china2.png +0 -0
  56. data/public/iceholidays-assets/images/chongqing.png +0 -0
  57. data/public/iceholidays-assets/images/guangzhou.png +0 -0
  58. data/public/iceholidays-assets/images/guilin.png +0 -0
  59. data/public/iceholidays-assets/images/harbin.png +0 -0
  60. data/public/iceholidays-assets/images/hongkong.png +0 -0
  61. data/public/iceholidays-assets/images/inner_mongolia.png +0 -0
  62. data/public/iceholidays-assets/images/jiangxi.png +0 -0
  63. data/public/iceholidays-assets/images/kenya.png +0 -0
  64. data/public/iceholidays-assets/images/kenya2.png +0 -0
  65. data/public/iceholidays-assets/images/kunming.png +0 -0
  66. data/public/iceholidays-assets/images/slikroad.png +0 -0
  67. data/public/iceholidays-assets/images/southafrica.png +0 -0
  68. data/public/iceholidays-assets/images/tanzania.png +0 -0
  69. data/public/iceholidays-assets/images/tour1.png +0 -0
  70. data/public/iceholidays-assets/images/uganda.png +0 -0
  71. /data/public/iceholidays-assets/images/{Group_71.png → about-us_logo.png} +0 -0
  72. /data/public/iceholidays-assets/images/{logo_container.png → logo.png} +0 -0
@@ -11,25 +11,21 @@ import SeriesApi from "../../api-services/series-api.service";
11
11
  import { Itinerary } from "../../interfaces/itinerary.interface";
12
12
 
13
13
  import createDOMPurify from 'dompurify'
14
+ import LocationDropdown from "../components/shared/LocationDropdown";
14
15
 
15
16
  const DOMPurify = createDOMPurify(window)
16
17
 
17
18
 
18
19
  const bannerPath = '/iceholidays-assets/images/china_listings_cover.png';
19
- const breadcrumbs = [
20
- { title: 'Home' },
21
- { title: 'China' },
22
- { title: 'Chongqing' }
23
- ]
24
20
 
25
21
  const legends = [
26
22
  { id: "guaranteed", label: "Guaranteed", color: "#DCB062" },
27
- { id: "selling-fast", label: "Selling Fast", color: "#23D1C0" }
23
+ { id: "almost-guaranteed", label: "Selling Fast", color: "#23D1C0" }
28
24
  ]
29
25
 
30
26
  const getColorByType = (type:string) => {
31
27
  var legend = legends.find(l => l.id == type);
32
- return legend ? legend.color : "#000000";
28
+ return legend ? legend.color : "#999999";
33
29
  }
34
30
 
35
31
  function withSearchParams(Component) {
@@ -43,11 +39,15 @@ class ListingPage extends React.Component <{searchParams}> {
43
39
  state = {
44
40
  countries: [],
45
41
  selectedCountry: {cities: []},
46
- keyword: "",
42
+ searchParamsObj: {keyword: "", year: "", month: "", location_id: null},
47
43
  itineraries: [],
48
- setIsModalOpen: false,
49
- setIsDateModalOpen: false,
44
+ location: {name: "", cover: ""},
45
+ breadcrumbs: [{ title: <a href="/app">Home</a> }],
46
+ descriptionModalOpen: false,
47
+ itineraryModalOpen: false,
48
+ dateModalOpen: false,
50
49
  descriptionData: "",
50
+ fileUrlData: "",
51
51
  departureDatesData: []
52
52
  }
53
53
 
@@ -64,16 +64,55 @@ class ListingPage extends React.Component <{searchParams}> {
64
64
  notification.error({ message: 'An error occured while loading countries.'});
65
65
  });
66
66
 
67
- const [searchParams] = this.props.searchParams;
68
- const keyword = searchParams.get('keyword');
69
- this.setState({keyword: keyword})
67
+ this.setSearchParamsObj();
70
68
 
71
- this.getItineraries({keyword});
69
+ }
70
+
71
+ private setSearchParamsObj(){
72
+ const [searchParams] = this.props.searchParams;
73
+ const {searchParamsObj} = this.state;
74
+ searchParams.keys().forEach(key => {
75
+ searchParamsObj[key] = searchParams.get(key);
76
+ });
77
+
78
+ this.setState({searchParamsObj});
79
+
80
+ const locationId = searchParamsObj.location_id;
81
+ if(locationId) this.getLocation(locationId);
82
+
83
+ this.getItineraries(searchParamsObj);
84
+ }
85
+
86
+
87
+ searchParamsToText() {
88
+ var {keyword, year, month} = this.state.searchParamsObj;
89
+ return <span>{ this.state.location?.name || keyword}{`${month && `, ${month}`} ${year}`} </span>
90
+ }
91
+
92
+ getLocation(locationId){
93
+ //resets the breadcrumbs
94
+ this.setState({breadcrumbs: [{ title: <a href="/app">Home</a> }]});
95
+
96
+ this.locationsApi.getLocation(locationId)
97
+ .then(locationData => {
98
+ var locationCrumbs = this.state.breadcrumbs;
99
+ if(locationData.ancestry?.length && locationData.ancestry?.length > 0){
100
+ locationData.ancestry.forEach(a => {
101
+ locationCrumbs.push({title: <span key={a.id}> {a.name} </span>})
102
+ });
103
+ }
104
+ locationCrumbs.push({title: <span>{locationData.name}</span>});
105
+ this.setState({location: locationData, breadcrumbs: locationCrumbs})
106
+ })
107
+ .catch(error => {
108
+ notification.error({ message: 'An error occured while loading location.'});
109
+ });
72
110
  }
73
111
 
74
112
  getItineraries(searchParams){
75
113
  this.seriesApi.getItineraries(searchParams)
76
- .then(itinerariesData => {
114
+ .then(itinerariesData => {
115
+ this.setBreadcrumbs();
77
116
  this.setState({itineraries: itinerariesData})
78
117
  })
79
118
  .catch(error => {
@@ -81,163 +120,176 @@ class ListingPage extends React.Component <{searchParams}> {
81
120
  });
82
121
  }
83
122
 
84
- showModal(description){
85
- this.setState({setIsModalOpen:true, descriptionData: description});
86
- }
123
+ setBreadcrumbs(){
124
+ var breadcrumbs = this.state.breadcrumbs;
125
+ if(breadcrumbs.length > 1){
126
+ const {keyword, ...noKeyword} = this.state.searchParamsObj;
127
+ const allHasValues = Object.entries(noKeyword).every(o => o[1] != "");
128
+ if(allHasValues){
129
+ const searchResultsCrumb = {title: <span>Search results</span>};
130
+ if(breadcrumbs.length > 2){
131
+ breadcrumbs[breadcrumbs.length-2] = searchResultsCrumb;
132
+ }else{
133
+ breadcrumbs.splice(1, 0, searchResultsCrumb);
134
+ }
135
+ }
87
136
 
88
- closeModal() {
89
- this.setState({setIsModalOpen:false});
137
+ breadcrumbs[breadcrumbs.length-1] = {title: this.searchParamsToText()};
138
+ }
139
+
140
+ this.setState({breadcrumbs});
90
141
  }
91
142
 
92
- showDatesModal(dates) {
93
- this.setState({setIsDateModalOpen:true, departureDatesData: dates});
143
+ showModal(modalName:string, propName: string, value: any){
144
+ this.setState({[modalName]: true, [propName]: value});
94
145
  }
95
146
 
96
- closeDatesModalatesModal(){
97
- this.setState({setIsDateModalOpen:false});
147
+ closeModal(modalName:string) {
148
+ this.setState({[modalName]:false});
98
149
  }
99
150
 
100
- selectCountry(country){
101
- const selectedCountry = this.state.countries.find((c:Country) => c.id == country);
151
+ selectCountry = (country) => {
152
+ const selectedCountry:any = this.state.countries.find((c:Country) => c.id == country);
102
153
  if(selectedCountry){
103
- this.setState({selectedCountry})
154
+ const searchParamsObj = this.state.searchParamsObj;
155
+ searchParamsObj.location_id = selectedCountry.id;
156
+ this.getLocation(selectedCountry.id);
157
+
158
+ this.setState({selectedCountry, searchParamsObj});
159
+ this.getItineraries(searchParamsObj);
104
160
  }
105
161
  }
106
162
 
107
163
 
108
164
  render(){
109
- const {countries, selectedCountry, keyword, itineraries, descriptionData, departureDatesData } = this.state;
165
+ const {countries, selectedCountry, searchParamsObj, itineraries, location, breadcrumbs, descriptionData, fileUrlData, departureDatesData } = this.state;
110
166
 
111
167
  return <div id="listing-page">
112
168
 
113
- <Headline bannerImage={bannerPath} breadcrumbs={breadcrumbs}>
114
- <div id="listing-page_header">
115
- <Flex justify="space-between" align="center">
116
- <h1>{keyword}</h1>
117
- <Select
118
- className="country-filter"
119
- suffixIcon={<Icon path={mdiFilterVariant} size={1} />}
120
- defaultValue="china"
121
- value={selectedCountry["name"]}
122
- options={countries}
123
- fieldNames={{label: 'name', value: 'id'}}
124
- dropdownStyle={{padding: "15px 10px 15px 20px", borderRadius: "20px"}}
125
- optionRender={(option) => (
126
- <div className="country-filter_option"> {option.label} </div>
127
- )}
128
- onChange={(country)=>this.selectCountry(country)}
129
- />
130
- </Flex>
131
- </div>
132
- </Headline>
133
-
134
- <div id="listing-page_body">
135
- <Space size={12} direction="vertical">
136
- <FilterPills items={selectedCountry.cities} label="name" initialValue={{keyword}} selectFilter={(selected) => this.getItineraries(selected)}></FilterPills>
137
- </Space>
138
-
139
- <div id="legends">
140
- <Space size={17}>
141
- {
142
- legends.map(legend => <Badge key={legend.id} color={legend.color} text={legend.label} />)
143
- }
169
+ <Headline bannerImage={location?.cover || bannerPath} breadcrumbs={breadcrumbs}>
170
+ <div id="listing-page_header">
171
+ <Flex vertical gap={10}>
172
+ <h1>{this.searchParamsToText()}</h1>
173
+ <LocationDropdown locations={countries} selectLocation={this.selectCountry}/>
174
+ </Flex>
175
+ </div>
176
+ </Headline>
177
+
178
+ <div id="listing-page_body">
179
+ <Space size={12} direction="vertical">
180
+ <FilterPills items={selectedCountry.cities} bindLabel="name" bindValue="location_id" initialValue={searchParamsObj} selectFilter={(selected) => this.getItineraries(selected)}></FilterPills>
144
181
  </Space>
145
- </div>
146
182
 
147
- <div id="tours">
148
- <Flex vertical gap={30}>
149
- {
150
- itineraries.map((itinerary:Itinerary) => {
151
- const departureDates = itinerary.departureDate.map(ddate => {
152
- return {
153
- type: itinerary.guranteedDepartureDates.includes(ddate) ? "guaranteed" : "",
154
- date: ddate
183
+ {
184
+ itineraries.length > 0 ? (
185
+ <>
186
+ <div id="legends">
187
+ <Space size={17}>
188
+ {
189
+ legends.map(legend => <Badge key={legend.id} color={legend.color} text={legend.label} />)
155
190
  }
156
- });
157
-
158
- return (
159
- <Row className="tour" wrap={false}>
160
- <Col flex="300px">
161
- <div className="tour_image"> <img src={itinerary.images[0]}/> </div>
162
- </Col>
163
- <Col flex="auto">
164
- <div className="tour_details">
165
- <Space size={20} direction="vertical" style={{ display: 'flex' }}>
166
- <Space size={10} direction="vertical">
167
- <div className="tour_details_title"> {itinerary.caption} </div>
168
- <div className="tour_details_subtitle"> {itinerary.otherCaption} </div>
169
- <div className="tour_details_info">
170
- <span onClick={()=>this.showModal(itinerary.description)}><Icon path={mdiInformationOutline} size={1}/></span>
191
+ </Space>
192
+ </div>
193
+ <div id="tours">
194
+ <Flex vertical gap={30}>
195
+ {
196
+ itineraries.map((itinerary:Itinerary) => {
197
+ const departureDates = itinerary.departureDate ? itinerary.departureDate.map(ddate => {
198
+ var type = (itinerary.guranteedDepartureDates && itinerary.guranteedDepartureDates.includes(ddate)) ? "guaranteed" :
199
+ (itinerary.almostGuaranteedDepartureDates && itinerary.almostGuaranteedDepartureDates.includes(ddate)) ? "almost-guaranteed" : "";
200
+
201
+ return { type, date: ddate }
202
+ }) : [];
203
+
204
+ return (
205
+ <Row key={itinerary.id} className="tour">
206
+ <Col flex="1 0 25%" className="column">
207
+ <div className="tour_image"> <img src={itinerary.images[0]}/> </div>
208
+ </Col>
209
+ <Col flex="1 0 55%" className="column">
210
+ <div className="tour_details">
211
+ <Space size={20} direction="vertical" style={{ display: 'flex' }}>
212
+ <Space size={10} direction="vertical">
213
+ <div className="tour_details_title"> {itinerary.caption} </div>
214
+ <div className="tour_details_info">
215
+ <span onClick={()=>this.showModal("descriptionModalOpen", "descriptionData", itinerary.description)}><Icon path={mdiInformationOutline} size={1}/></span>
216
+ </div>
217
+ <div>
218
+ <span className="tour_details_country"> <Icon path={mdiMapMarkerOutline} size="18px" /> {itinerary.country} </span>
219
+ <span className="tour_details_code" onClick={()=>this.showModal("itineraryModalOpen", "fileUrlData", itinerary.fileUrl)}> <Icon path={mdiFileDownload} size="18px" /> {itinerary.code} </span>
220
+ </div>
221
+ </Space>
222
+ <div className="tour_details_dates">
223
+ <Space size={6} direction="vertical" style={{ display: 'flex' }}>
224
+ <label>Departure Date(s)</label>
225
+ <div className="date-selector">
226
+ {
227
+ departureDates && departureDates.map((ddate, index) => <span key={index} style={{borderColor: getColorByType(ddate.type)}}> {ddate.date} </span>)
228
+ }
229
+ {
230
+ departureDates.length >= 9 && <div className="show-all-dates" onClick={()=>this.showModal("dateModalOpen", "departureDatesData", departureDates)}> <Icon path={mdiDotsHorizontalCircleOutline} size="15px" /> Show All </div>
231
+ }
232
+ </div>
233
+ </Space>
234
+ </div>
235
+ </Space>
171
236
  </div>
172
- <div>
173
- <span className="tour_details_country"> <Icon path={mdiMapMarkerOutline} size="18px" /> {itinerary.country} </span>
174
- <span className="tour_details_code"> <Icon path={mdiFileDownload} size="18px" /> {itinerary.code} </span>
237
+ </Col>
238
+ <Col flex="1 0 20%" className="column">
239
+ <div className="tour_pricing">
240
+ <Space size={20} direction="vertical" align="center">
241
+ <Flex className="tour_pricing_details">
242
+ <span>From</span>
243
+ <span className="price">{itinerary.priceCurrency} {itinerary.price}</span>
244
+ <span>All In</span>
245
+ </Flex>
246
+ <Link className="select-tour" to={`/app/itinerary/${itinerary.id}`}>Select</Link>
247
+ </Space>
175
248
  </div>
176
- </Space>
177
- <div className="tour_details_dates">
178
- <Space size={6} direction="vertical" style={{ display: 'flex' }}>
179
- <label>Departure Date(s)</label>
180
- <div className="date-selector">
181
- {
182
- departureDates && departureDates.map(ddate => <span style={{borderColor: getColorByType(ddate.type)}}> {ddate.date} </span>)
183
- }
184
- {
185
- departureDates.length >= 9 && <div className="show-all-dates" onClick={()=>this.showDatesModal(departureDates)}> <Icon path={mdiDotsHorizontalCircleOutline} size="15px" /> Show All </div>
186
- }
187
- </div>
188
- </Space>
189
- </div>
190
- </Space>
191
- </div>
192
- </Col>
193
- <Col flex="210px">
194
- <div className="tour_pricing">
195
- <Space size={20} direction="vertical" align="center">
196
- <Flex vertical>
197
- <span>From</span>
198
- <span className="price">{itinerary.priceCurrency} {itinerary.price}</span>
199
- <span>All In</span>
200
- </Flex>
201
- <Link className="select-tour" to="/app/show">Select</Link>
202
- </Space>
203
- </div>
204
- </Col>
205
- </Row>
206
- )
207
- })
208
- }
209
- </Flex>
249
+ </Col>
250
+ </Row>
251
+ )
252
+ })
253
+ }
254
+ </Flex>
255
+ </div>
256
+ </>
257
+ ) : <h1 id="no-tours-found">Not tour package is found.</h1>
258
+ }
259
+
210
260
  </div>
211
- </div>
212
261
 
262
+ <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>
264
+ </Modal>
213
265
 
214
- <Modal title="Description" open={this.state.setIsModalOpen} onCancel={()=>this.closeModal()} footer={null} width={1000} centered className="tour_details_description">
215
- <div className="pre-wrap">{ <span dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(descriptionData) }} /> }</div>
216
- </Modal>
266
+ <Modal title="Itinerary" open={this.state.itineraryModalOpen} onCancel={()=>this.closeModal("itineraryModalOpen")} footer={null} width={1000} centered className="tour_details_itinerary">
267
+ <div id="itinerary-file"><img src={fileUrlData}/></div>
268
+ </Modal>
217
269
 
218
- <Modal title={
219
- <Flex justify="space-between" align="center">
220
- <div className="custom-modal-header-title"> Departure Date(s) </div>
221
- <div className="custom-modal-header-legends">
222
- <Space size={17}>
270
+ <Modal title={
271
+ <Flex justify="space-between" align="center">
272
+ <div className="custom-modal-header-title"> Departure Date(s) </div>
273
+ <div className="custom-modal-header-legends">
274
+ <Space size={17}>
275
+ {
276
+ legends.map(legend => <Badge key={legend.id} color={legend.color} text={legend.label} />)
277
+ }
278
+ </Space>
279
+ </div>
280
+ </Flex>
281
+ } footer={
282
+ <Button>Confirm</Button>
283
+ }
284
+ open={this.state.dateModalOpen} onCancel={()=>this.closeModal("dateModalOpen")} width={940} centered className="tour_details_all_dates">
285
+ <div className="date-selector">
286
+ <Space size={8} wrap>
223
287
  {
224
- legends.map(legend => <Badge key={legend.id} color={legend.color} text={legend.label} />)
288
+ departureDatesData && departureDatesData.map((ddate:any) => <span style={{borderColor: getColorByType(ddate.type)}}> {ddate.date} </span>)
225
289
  }
226
- </Space>
227
- </div>
228
- </Flex>
229
- } footer={
230
- <Button>Confirm</Button>
231
- }
232
- open={this.state.setIsDateModalOpen} onCancel={()=>this.closeDatesModalatesModal()} width={940} centered className="tour_details_all_dates">
233
- <div className="date-selector">
234
- <Space size={8} wrap>
235
- {
236
- departureDatesData && departureDatesData.map((ddate:any) => <span style={{borderColor: getColorByType(ddate.type)}}> {ddate.date} </span>)
237
- }
238
- </Space>
239
- </div>
240
- </Modal>
290
+ </Space>
291
+ </div>
292
+ </Modal>
241
293
  </div>
242
294
  }
243
295
  }