iceholidays-frontend 0.3.0 → 0.5.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 (108) 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 +2194 -0
  4. data/app/assets/stylesheets/iceholidays/frontend/common.scss +317 -0
  5. data/app/assets/stylesheets/iceholidays/frontend/layout.scss +281 -0
  6. data/app/assets/stylesheets/iceholidays/frontend/utils/_antd_overrides.scss +122 -0
  7. data/app/assets/stylesheets/iceholidays/frontend/utils/_variables.scss +4 -0
  8. data/app/assets/stylesheets/iceholidays/frontend/widgets/filter_pills.scss +52 -0
  9. data/app/assets/stylesheets/iceholidays/frontend/widgets/search_bar.scss +116 -0
  10. data/app/controllers/iceholidays/frontend/posts_controller.rb +14 -0
  11. data/app/controllers/iceholidays/frontend/site_controller.rb +32 -0
  12. data/app/javascript/api-services/agents-api.service.ts +33 -0
  13. data/app/javascript/api-services/banners-api.service.ts +28 -0
  14. data/app/javascript/api-services/locations-api.service.ts +71 -0
  15. data/app/javascript/api-services/search-api.service.ts +16 -0
  16. data/app/javascript/api-services/series-api.service.ts +64 -0
  17. data/app/javascript/api-services/testimonials-api.service.ts +27 -0
  18. data/app/javascript/interfaces/agent.interface.ts +11 -0
  19. data/app/javascript/interfaces/banner.interface.ts +10 -0
  20. data/app/javascript/interfaces/country.interface.ts +19 -0
  21. data/app/javascript/interfaces/itinerary.interface.ts +111 -0
  22. data/app/javascript/interfaces/testimonial.interface.ts +6 -0
  23. data/app/javascript/react/App.tsx +32 -0
  24. data/app/javascript/react/components/Destinations.tsx +84 -143
  25. data/app/javascript/react/components/PriceDetails.tsx +146 -0
  26. data/app/javascript/react/components/Testimonials.tsx +68 -61
  27. data/app/javascript/react/components/shared/ContactAgentsForm.tsx +44 -0
  28. data/app/javascript/react/components/shared/Headline.tsx +30 -0
  29. data/app/javascript/react/components/shared/LocationDropdown.tsx +34 -0
  30. data/app/javascript/react/components/shared/LocationPostcards.tsx +52 -0
  31. data/app/javascript/react/components/shared/RibbonSection.tsx +21 -0
  32. data/app/javascript/react/index.js +3 -5
  33. data/app/javascript/react/layouts/MainFooter.tsx +97 -0
  34. data/app/javascript/react/layouts/MainHeader.tsx +83 -0
  35. data/app/javascript/react/layouts/MainLayout.tsx +21 -0
  36. data/app/javascript/react/pages/AboutUsPage.tsx +95 -0
  37. data/app/javascript/react/pages/BlogPage.tsx +81 -0
  38. data/app/javascript/react/pages/BlogShowPage.tsx +43 -0
  39. data/app/javascript/react/pages/ContactAgentsPage.tsx +185 -0
  40. data/app/javascript/react/pages/ContactUsPage.tsx +122 -0
  41. data/app/javascript/react/pages/CountriesPage.tsx +57 -0
  42. data/app/javascript/react/pages/Homepage.tsx +100 -0
  43. data/app/javascript/react/pages/ListingPage.tsx +292 -0
  44. data/app/javascript/react/pages/ShowPage.tsx +402 -0
  45. data/app/javascript/react/widgets/FilterPills.tsx +111 -0
  46. data/app/javascript/react/widgets/SearchBarWidget.tsx +58 -0
  47. data/app/views/iceholidays/frontend/posts/index.html.erb +9 -0
  48. data/app/views/iceholidays/frontend/posts/show.html.erb +2 -0
  49. data/app/views/iceholidays/frontend/site/index.html.erb +1 -24
  50. data/app/views/layouts/iceholidays/frontend/application.html.erb +2 -6
  51. data/config/routes.rb +10 -0
  52. data/lib/iceholidays/frontend/version.rb +1 -1
  53. data/public/iceholidays-assets/application.css +2638 -0
  54. data/public/iceholidays-assets/application.js +212 -651
  55. data/public/iceholidays-assets/application.js.map +4 -4
  56. data/public/iceholidays-assets/images/about-us_logo_mobile.png +0 -0
  57. data/public/iceholidays-assets/images/about_us.png +0 -0
  58. data/public/iceholidays-assets/images/about_us2.png +0 -0
  59. data/public/iceholidays-assets/images/blog.png +0 -0
  60. data/public/iceholidays-assets/images/blog1.png +0 -0
  61. data/public/iceholidays-assets/images/certificate1.png +0 -0
  62. data/public/iceholidays-assets/images/certificate2.png +0 -0
  63. data/public/iceholidays-assets/images/china_southern_airlines.png +0 -0
  64. data/public/iceholidays-assets/images/china_southern_airlines_icon.png +0 -0
  65. data/public/iceholidays-assets/images/competitiveness.png +0 -0
  66. data/public/iceholidays-assets/images/contact_agents.png +0 -0
  67. data/public/iceholidays-assets/images/contact_us.png +0 -0
  68. data/public/iceholidays-assets/images/contact_us_form.png +0 -0
  69. data/public/iceholidays-assets/images/destinations_logo.png +0 -0
  70. data/public/iceholidays-assets/images/ethical.png +0 -0
  71. data/public/iceholidays-assets/images/footer-bg_mobile.png +0 -0
  72. data/public/iceholidays-assets/images/hw_logo.png +0 -0
  73. data/public/iceholidays-assets/images/innovative.png +0 -0
  74. data/public/iceholidays-assets/images/logo_mobile.png +0 -0
  75. data/public/iceholidays-assets/images/plane.png +0 -0
  76. data/public/iceholidays-assets/images/social/ico_fb.png +0 -0
  77. data/public/iceholidays-assets/images/social/ico_ig.png +0 -0
  78. data/public/iceholidays-assets/images/social/ico_twitter.png +0 -0
  79. data/public/iceholidays-assets/images/social/ico_yt.png +0 -0
  80. data/public/iceholidays-assets/images/social.png +0 -0
  81. metadata +74 -71
  82. data/app/assets/stylesheets/iceholidays/frontend/application.scss +0 -904
  83. data/app/javascript/react/components/Homepage.tsx +0 -15
  84. data/app/javascript/react/components/HomepageBanner.tsx +0 -62
  85. data/app/views/layouts/iceholidays/frontend/shared/_footer.html.erb +0 -42
  86. data/app/views/layouts/iceholidays/frontend/shared/_header.html.erb +0 -20
  87. data/public/iceholidays-assets/images/Frame71.png +0 -0
  88. data/public/iceholidays-assets/images/africa.png +0 -0
  89. data/public/iceholidays-assets/images/banner1.png +0 -0
  90. data/public/iceholidays-assets/images/banner2.png +0 -0
  91. data/public/iceholidays-assets/images/china.png +0 -0
  92. data/public/iceholidays-assets/images/china2.png +0 -0
  93. data/public/iceholidays-assets/images/guangzhou.png +0 -0
  94. data/public/iceholidays-assets/images/guilin.png +0 -0
  95. data/public/iceholidays-assets/images/harbin.png +0 -0
  96. data/public/iceholidays-assets/images/hongkong.png +0 -0
  97. data/public/iceholidays-assets/images/inner_mongolia.png +0 -0
  98. data/public/iceholidays-assets/images/jiangxi.png +0 -0
  99. data/public/iceholidays-assets/images/kenya.png +0 -0
  100. data/public/iceholidays-assets/images/kenya2.png +0 -0
  101. data/public/iceholidays-assets/images/kunming.png +0 -0
  102. data/public/iceholidays-assets/images/slikroad.png +0 -0
  103. data/public/iceholidays-assets/images/southafrica.png +0 -0
  104. data/public/iceholidays-assets/images/tanzania.png +0 -0
  105. data/public/iceholidays-assets/images/uganda.png +0 -0
  106. /data/public/iceholidays-assets/images/{Group_71.png → about-us_logo.png} +0 -0
  107. /data/public/iceholidays-assets/images/{chongqing.png → china_listings_cover.png} +0 -0
  108. /data/public/iceholidays-assets/images/{logo_container.png → logo.png} +0 -0
@@ -0,0 +1,292 @@
1
+ import React, { Component } from "react";
2
+ import { Badge, Breadcrumb, Button, Flex, Modal, Space, Select, Row, Col, notification } from "antd";
3
+ import FilterPills from "../widgets/FilterPills";
4
+ import { Link, useSearchParams } from "react-router-dom";
5
+ import { mdiDotsHorizontalCircleOutline, mdiFileDownload, mdiFilterVariant, mdiInformationOutline, mdiMapMarkerOutline } from "@mdi/js";
6
+ import Icon from "@mdi/react";
7
+ import Headline from "../components/shared/Headline";
8
+ import LocationsApi from "../../api-services/locations-api.service";
9
+ import { Country } from "../../interfaces/country.interface";
10
+ import SeriesApi from "../../api-services/series-api.service";
11
+ import { Itinerary } from "../../interfaces/itinerary.interface";
12
+
13
+ import createDOMPurify from 'dompurify'
14
+ import LocationDropdown from "../components/shared/LocationDropdown";
15
+
16
+ const DOMPurify = createDOMPurify(window)
17
+
18
+
19
+ const bannerPath = '/iceholidays-assets/images/china_listings_cover.png';
20
+
21
+ const legends = [
22
+ { id: "guaranteed", label: "Guaranteed", color: "#DCB062" },
23
+ { id: "almost-guaranteed", label: "Selling Fast", color: "#23D1C0" }
24
+ ]
25
+
26
+ const getColorByType = (type:string) => {
27
+ var legend = legends.find(l => l.id == type);
28
+ return legend ? legend.color : "#999999";
29
+ }
30
+
31
+ function withSearchParams(Component) {
32
+ return props => <Component {...props} searchParams={useSearchParams()} />;
33
+ }
34
+
35
+ class ListingPage extends React.Component <{searchParams}> {
36
+ locationsApi = new LocationsApi;
37
+ seriesApi = new SeriesApi;
38
+
39
+ state = {
40
+ countries: [],
41
+ selectedCountry: {cities: []},
42
+ searchParamsObj: {keyword: "", year: "", month: "", location_id: null},
43
+ itineraries: [],
44
+ location: {name: "", cover: ""},
45
+ breadcrumbs: [{ title: <a href="/app">Home</a> }],
46
+ descriptionModalOpen: false,
47
+ itineraryModalOpen: false,
48
+ dateModalOpen: false,
49
+ descriptionData: "",
50
+ fileUrlData: "",
51
+ departureDatesData: []
52
+ }
53
+
54
+ constructor(props:any){
55
+ super(props);
56
+ }
57
+
58
+ componentDidMount() {
59
+ this.locationsApi.getCountries()
60
+ .then(locationsData => {
61
+ this.setState({countries: locationsData, selectedCountry: locationsData[0]})
62
+ })
63
+ .catch(error => {
64
+ notification.error({ message: 'An error occured while loading countries.'});
65
+ });
66
+
67
+ this.setSearchParamsObj();
68
+
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
+ });
110
+ }
111
+
112
+ getItineraries(searchParams){
113
+ this.seriesApi.getItineraries(searchParams)
114
+ .then(itinerariesData => {
115
+ this.setBreadcrumbs();
116
+ this.setState({itineraries: itinerariesData})
117
+ })
118
+ .catch(error => {
119
+ notification.error({ message: 'An error occured while loading itineraries.'});
120
+ });
121
+ }
122
+
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
+ }
136
+
137
+ breadcrumbs[breadcrumbs.length-1] = {title: this.searchParamsToText()};
138
+ }
139
+
140
+ this.setState({breadcrumbs});
141
+ }
142
+
143
+ showModal(modalName:string, propName: string, value: any){
144
+ this.setState({[modalName]: true, [propName]: value});
145
+ }
146
+
147
+ closeModal(modalName:string) {
148
+ this.setState({[modalName]:false});
149
+ }
150
+
151
+ selectCountry = (country) => {
152
+ const selectedCountry:any = this.state.countries.find((c:Country) => c.id == country);
153
+ if(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);
160
+ }
161
+ }
162
+
163
+
164
+ render(){
165
+ const {countries, selectedCountry, searchParamsObj, itineraries, location, breadcrumbs, descriptionData, fileUrlData, departureDatesData } = this.state;
166
+
167
+ return <div id="listing-page">
168
+
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>
181
+ </Space>
182
+
183
+ <div id="legends">
184
+ <Space size={17}>
185
+ {
186
+ legends.map(legend => <Badge key={legend.id} color={legend.color} text={legend.label} />)
187
+ }
188
+ </Space>
189
+ </div>
190
+
191
+ <div id="tours">
192
+ <Flex vertical gap={30}>
193
+ {
194
+ itineraries.map((itinerary:Itinerary) => {
195
+ const departureDates = itinerary.departureDate ? itinerary.departureDate.map(ddate => {
196
+ var type = (itinerary.guranteedDepartureDates && itinerary.guranteedDepartureDates.includes(ddate)) ? "guaranteed" :
197
+ (itinerary.almostGuaranteedDepartureDates && itinerary.almostGuaranteedDepartureDates.includes(ddate)) ? "almost-guaranteed" : "";
198
+
199
+ return { type, date: ddate }
200
+ }) : [];
201
+
202
+ return (
203
+ <Row key={itinerary.id} className="tour">
204
+ <Col flex="1 0 25%" className="column">
205
+ <div className="tour_image"> <img src={itinerary.images[0]}/> </div>
206
+ </Col>
207
+ <Col flex="1 0 55%" className="column">
208
+ <div className="tour_details">
209
+ <Space size={20} direction="vertical" style={{ display: 'flex' }}>
210
+ <Space size={10} direction="vertical">
211
+ <div className="tour_details_title"> {itinerary.caption} </div>
212
+ <div className="tour_details_info">
213
+ <span onClick={()=>this.showModal("descriptionModalOpen", "descriptionData", itinerary.description)}><Icon path={mdiInformationOutline} size={1}/></span>
214
+ </div>
215
+ <div>
216
+ <span className="tour_details_country"> <Icon path={mdiMapMarkerOutline} size="18px" /> {itinerary.country} </span>
217
+ <span className="tour_details_code" onClick={()=>this.showModal("itineraryModalOpen", "fileUrlData", itinerary.fileUrl)}> <Icon path={mdiFileDownload} size="18px" /> {itinerary.code} </span>
218
+ </div>
219
+ </Space>
220
+ <div className="tour_details_dates">
221
+ <Space size={6} direction="vertical" style={{ display: 'flex' }}>
222
+ <label>Departure Date(s)</label>
223
+ <div className="date-selector">
224
+ {
225
+ departureDates && departureDates.map((ddate, index) => <span key={index} style={{borderColor: getColorByType(ddate.type)}}> {ddate.date} </span>)
226
+ }
227
+ {
228
+ departureDates.length >= 9 && <div className="show-all-dates" onClick={()=>this.showModal("dateModalOpen", "departureDatesData", departureDates)}> <Icon path={mdiDotsHorizontalCircleOutline} size="15px" /> Show All </div>
229
+ }
230
+ </div>
231
+ </Space>
232
+ </div>
233
+ </Space>
234
+ </div>
235
+ </Col>
236
+ <Col flex="1 0 20%" className="column">
237
+ <div className="tour_pricing">
238
+ <Space size={20} direction="vertical" align="center">
239
+ <Flex className="tour_pricing_details">
240
+ <span>From</span>
241
+ <span className="price">{itinerary.priceCurrency} {itinerary.price}</span>
242
+ <span>All In</span>
243
+ </Flex>
244
+ <Link className="select-tour" to={`/app/itinerary/${itinerary.id}`}>Select</Link>
245
+ </Space>
246
+ </div>
247
+ </Col>
248
+ </Row>
249
+ )
250
+ })
251
+ }
252
+ </Flex>
253
+ </div>
254
+ </div>
255
+
256
+ <Modal title="Description" open={this.state.descriptionModalOpen} onCancel={()=>this.closeModal("descriptionModalOpen")} footer={null} width={1000} centered className="tour_details_description">
257
+ <div className="pre-wrap">{ <span dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(descriptionData) }} /> }</div>
258
+ </Modal>
259
+
260
+ <Modal title="Itinerary" open={this.state.itineraryModalOpen} onCancel={()=>this.closeModal("itineraryModalOpen")} footer={null} width={1000} centered className="tour_details_itinerary">
261
+ <div id="itinerary-file"><img src={fileUrlData}/></div>
262
+ </Modal>
263
+
264
+ <Modal title={
265
+ <Flex justify="space-between" align="center">
266
+ <div className="custom-modal-header-title"> Departure Date(s) </div>
267
+ <div className="custom-modal-header-legends">
268
+ <Space size={17}>
269
+ {
270
+ legends.map(legend => <Badge key={legend.id} color={legend.color} text={legend.label} />)
271
+ }
272
+ </Space>
273
+ </div>
274
+ </Flex>
275
+ } footer={
276
+ <Button>Confirm</Button>
277
+ }
278
+ open={this.state.dateModalOpen} onCancel={()=>this.closeModal("dateModalOpen")} width={940} centered className="tour_details_all_dates">
279
+ <div className="date-selector">
280
+ <Space size={8} wrap>
281
+ {
282
+ departureDatesData && departureDatesData.map((ddate:any) => <span style={{borderColor: getColorByType(ddate.type)}}> {ddate.date} </span>)
283
+ }
284
+ </Space>
285
+ </div>
286
+ </Modal>
287
+ </div>
288
+ }
289
+ }
290
+
291
+
292
+ export default withSearchParams(ListingPage);