You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

427 lines
13 KiB

  1. <template>
  2. <div :class="['page-container', { 'nav-is-showing': isPickerVisible }]">
  3. <header>
  4. <div class="header__col">
  5. <template v-if="!isPickerVisible">
  6. <button class="btn btn--primary" @click="showPicker">
  7. tap the map or <svg
  8. xmlns="http://www.w3.org/2000/svg"
  9. class="icon icon-tabler icon-tabler-search"
  10. width="24"
  11. height="24"
  12. viewBox="0 0 24 24"
  13. stroke-width="3"
  14. stroke="#ffffff"
  15. fill="none"
  16. stroke-linecap="round"
  17. stroke-linejoin="round"
  18. >
  19. <path stroke="none" d="M0 0h24v24H0z" fill="none" />
  20. <circle cx="10" cy="10" r="7" />
  21. <line x1="21" y1="21" x2="15" y2="15" />
  22. </svg>
  23. </button>
  24. </template>
  25. <template v-if="isPickerVisible">
  26. <button class="btn btn--primary" @click="showPicker">
  27. hide search <svg
  28. xmlns="http://www.w3.org/2000/svg"
  29. class="icon icon-tabler icon-tabler-arrow-bar-to-up"
  30. width="24"
  31. height="24"
  32. viewBox="0 0 24 24"
  33. stroke-width="3"
  34. stroke="#ffffff"
  35. fill="none"
  36. stroke-linecap="round"
  37. stroke-linejoin="round"
  38. >
  39. <path stroke="none" d="M0 0h24v24H0z" fill="none" />
  40. <line x1="12" y1="10" x2="12" y2="20" />
  41. <line x1="12" y1="10" x2="16" y2="14" />
  42. <line x1="12" y1="10" x2="8" y2="14" />
  43. <line x1="4" y1="4" x2="20" y2="4" />
  44. </svg>
  45. </button>
  46. </template>
  47. </div>
  48. <div class="header__col">
  49. <div class="logo-wrapper">
  50. <Logo />
  51. <div v-if="!selectedOrig.iata && !isPickerVisible && isMapReady" class="logo-blurb">
  52. from one of <strong>{{ airports_orig.length }}</strong> <span style="color: white;">local</span> airports
  53. </div>
  54. <div v-if="selectedOrig.iata && !selectedDest.iata && airports_dest.length > 1 && !isPickerVisible && isMapReady" class="logo-blurb">
  55. to one of <strong>{{ airports_dest.length }}</strong> {{ getCompliment() }} destinations
  56. </div>
  57. </div>
  58. </div>
  59. <div class="header__col" />
  60. </header>
  61. <!-- <pre style="position: fixed; z-index: 500; right: 0; top: 2rem; font-size: 0.6rem; color: gray;">
  62. {{ selectedOrig }}
  63. {{ selectedDest }}
  64. </pre> -->
  65. <main>
  66. <nav v-if="isMapReady" :class="['nav', { 'nav--show': isPickerVisible },{ 'nav--hide': !isPickerVisible }]">
  67. <AirportPicker
  68. :airports="airports_orig"
  69. :selected-airport="selectedOrig"
  70. @select-airport="makeOrigin"
  71. @deselect-airport="clearOrigin"
  72. />
  73. <AirportPicker
  74. v-show="selectedOrig.iata"
  75. :airports="airports_dest"
  76. :selected-airport="selectedDest"
  77. :origin="selectedOrig"
  78. @select-airport="makeDestination"
  79. @deselect-airport="clearDestination"
  80. />
  81. </nav>
  82. <Map
  83. ref="mapComponent"
  84. :airports-orig="airports_orig"
  85. :airports-dest="airports_dest"
  86. :selected-orig="selectedOrig"
  87. :selected-dest="selectedDest"
  88. @make-origin="makeOrigin"
  89. @make-destination="makeDestination"
  90. @clear-origin="clearOrigin"
  91. @clear-destination="clearDestination"
  92. @map-ready="mapReady"
  93. />
  94. <div :class="['flyout-container',{ 'flyout-container--hide': !selectedOrig.iata || !selectedDest.iata },{ 'flyout-container--show': selectedOrig.iata && selectedDest.iata }]">
  95. <FlightsFlyout
  96. v-if="selectedOrig.iata && selectedDest.iata"
  97. :selected-orig="selectedOrig"
  98. :selected-dest="selectedDest"
  99. />
  100. </div>
  101. </main>
  102. </div>
  103. </template>
  104. <script>
  105. // import AirportPicker from '../components/AirportPicker.vue'
  106. export default {
  107. // components: { AirportPicker },
  108. data () {
  109. return {
  110. isMapReady: false,
  111. color: 'red',
  112. isSidebarVisible: false,
  113. isPickerVisible: false,
  114. airports_orig: [],
  115. airports_dest: [],
  116. airportFetch_filterFormula: 'AND(Is_Origin=1,{IsCurrent-AsOrigin}=\'Yes\')',
  117. airportFetch_fields: [
  118. 'Airport_IATA',
  119. 'Icon_Url',
  120. 'Latitude_Deg',
  121. 'Longitude_Deg',
  122. 'Municipality',
  123. 'Airport_Name',
  124. 'Type',
  125. 'Search_Field'
  126. ],
  127. airportFetch_sort: '&sort[0][field]=Airport_IATA&sort[0][direction]=asc',
  128. selectedOrig: {
  129. iata: '',
  130. lat: '',
  131. long: '',
  132. icon: '',
  133. name: '',
  134. municipality: '',
  135. type: '',
  136. search: ''
  137. },
  138. selectedDest: {
  139. iata: '',
  140. lat: '',
  141. long: '',
  142. icon: '',
  143. name: '',
  144. municipality: '',
  145. type: '',
  146. search: ''
  147. },
  148. emptyAirport: {
  149. iata: '',
  150. lat: '',
  151. long: '',
  152. icon: '',
  153. name: '',
  154. municipality: '',
  155. type: '',
  156. search: ''
  157. }
  158. }
  159. },
  160. async fetch () {
  161. let offset
  162. let data = []
  163. while (true) {
  164. const offsetParam = offset ? `&offset=${offset}` : ''
  165. const res = await fetch(`https://api.airtable.com/v0/appiQwfVZixRgRICe/Airports_IATA?filterByFormula=${this.airportFetch_filterFormula}${this.airportFetch_fields_string}${this.airportFetch_sort}${offsetParam}`, {
  166. method: 'GET',
  167. headers: {
  168. 'Content-Type': 'application/x-www-form-urlencoded',
  169. Authorization: 'Bearer keyJ2ht64ZSN57AG1'
  170. }
  171. })
  172. const json = await res.json()
  173. offset = await json.offset
  174. data = [...data, ...await json.records]
  175. await console.log(data.length)
  176. if (await !offset) { break } // Were done let's stop this thing
  177. }
  178. // console.log('Look ma! I waited!') // Won't run till the while is done
  179. this.airports_orig = data.map((record) => {
  180. return {
  181. iata: record.fields.Airport_IATA,
  182. lat: record.fields.Latitude_Deg,
  183. long: record.fields.Longitude_Deg,
  184. icon: record.fields.Icon_Url,
  185. name: record.fields.Airport_Name,
  186. municipality: record.fields.Municipality,
  187. type: record.fields.Type,
  188. search: record.fields.Search_Field
  189. }
  190. })
  191. },
  192. computed: {
  193. airportFetch_fields_string () {
  194. const vm = this
  195. const array = vm.airportFetch_fields.map((field) => {
  196. return '&fields[]=' + field
  197. })
  198. return array.join('')
  199. }
  200. },
  201. beforeCreate () {
  202. this.isMapReady = false
  203. },
  204. created () {
  205. // console.log(this.$route.path)
  206. if (this.$route && this.$route.params && this.$route.params.o) {
  207. this.fetchSingleAirport(this.$route.params.o, true)
  208. }
  209. if (this.$route && this.$route.params && this.$route.params.d) {
  210. this.fetchSingleAirport(this.$route.params.d, false)
  211. }
  212. },
  213. // watch: {
  214. // history (to, from) {
  215. // console.log(`routed from ${from} to ${to}`)
  216. // }
  217. // },
  218. methods: {
  219. mapReady () {
  220. this.isMapReady = true
  221. },
  222. getCompliment () {
  223. const arr = [
  224. 'beautiful', 'gorgeous', 'breathtaking', 'wild', 'adventurous', 'amazing', 'majestic', 'uncharted', 'unpredictable', 'pristine', 'untamed'
  225. ]
  226. return arr[Math.floor(Math.random() * arr.length)]
  227. },
  228. makeOrigin (airport) {
  229. // console.log(airport)
  230. this.selectedOrig = { ...airport }
  231. this.selectedDest = { ...this.emptyAirport }
  232. this.$segment.track('go__select_orig', {
  233. origin: airport.iata
  234. });
  235. history.pushState(
  236. {},
  237. null,
  238. '/go/' + encodeURIComponent(airport.iata)
  239. )
  240. this.fetchDestinations(airport.iata)
  241. },
  242. makeDestination (airport) {
  243. this.selectedDest = { ...airport }
  244. this.$segment.track('go__select_dest', {
  245. origin: this.selectedOrig.iata,
  246. destination: airport.iata
  247. });
  248. this.isPickerVisible = false
  249. history.pushState(
  250. {},
  251. null,
  252. '/go/' + encodeURIComponent(this.selectedOrig.iata) + '/' + encodeURIComponent(airport.iata)
  253. )
  254. },
  255. clearOrigin () {
  256. this.$segment.track('go__clear_orig',{
  257. origin: this.selectedOrig.iata
  258. })
  259. this.selectedOrig = { ...this.emptyAirport }
  260. this.selectedDest = { ...this.emptyAirport }
  261. history.pushState(
  262. {},
  263. null,
  264. '/go/'
  265. )
  266. },
  267. clearDestination () {
  268. this.$segment.track('go__clear_dest', {
  269. origin: this.selectedOrig.iata,
  270. destination: this.selectedDest.iata
  271. })
  272. this.selectedDest = { ...this.emptyAirport }
  273. history.pushState(
  274. {},
  275. null,
  276. '/go/' + encodeURIComponent(this.selectedOrig.iata)
  277. )
  278. },
  279. async fetchDestinations (iata) {
  280. const airportFetchDestfilterFormula = `AND(Is_Destination=1,{IsCurrent-AsDest}="Yes",(FIND("${iata}", Associated_Origin_Search)>0))`
  281. let offset
  282. let data = []
  283. while (true) {
  284. const offsetParam = offset ? `&offset=${offset}` : ''
  285. const res = await fetch(`https://api.airtable.com/v0/appiQwfVZixRgRICe/Airports_IATA?filterByFormula=${airportFetchDestfilterFormula}${this.airportFetch_fields_string}${this.airportFetch_sort}${offsetParam}`, {
  286. method: 'GET',
  287. headers: {
  288. 'Content-Type': 'application/x-www-form-urlencoded',
  289. Authorization: 'Bearer keyJ2ht64ZSN57AG1'
  290. }
  291. })
  292. const json = await res.json()
  293. offset = await json.offset
  294. data = [...data, ...await json.records]
  295. // await console.log(data.length)
  296. if (await !offset) { break } // Were done let's stop this thing
  297. }
  298. // console.log('Look ma! I waited!') // Won't run till the while is done
  299. this.airports_dest = data.map((record) => {
  300. return {
  301. iata: record.fields.Airport_IATA,
  302. lat: record.fields.Latitude_Deg,
  303. long: record.fields.Longitude_Deg,
  304. icon: record.fields.Icon_Url,
  305. name: record.fields.Airport_Name,
  306. municipality: record.fields.Municipality,
  307. type: record.fields.Type,
  308. search: record.fields.Search_Field
  309. }
  310. })
  311. },
  312. async fetchSingleAirport (iata, isOrig) {
  313. const airportFetchFilterFormula =
  314. isOrig
  315. ? `AND(Is_Origin=1,{IsCurrent-AsOrigin}="Yes",Airport_IATA="${iata}")`
  316. : `AND(Is_Destination=1,{IsCurrent-AsDest}="Yes",Airport_IATA="${iata}")`
  317. const response = await fetch(`https://api.airtable.com/v0/appiQwfVZixRgRICe/Airports_IATA?filterByFormula=${airportFetchFilterFormula}${this.airportFetch_fields_string}`, {
  318. method: 'GET',
  319. headers: {
  320. 'Content-Type': 'application/x-www-form-urlencoded',
  321. Authorization: 'Bearer keyJ2ht64ZSN57AG1'
  322. }
  323. })
  324. const mapData = await response.json()
  325. const thisAirport = {
  326. iata: mapData.records[0].fields.Airport_IATA,
  327. lat: mapData.records[0].fields.Latitude_Deg,
  328. long: mapData.records[0].fields.Longitude_Deg,
  329. icon: mapData.records[0].fields.Icon_Url,
  330. name: mapData.records[0].fields.Airport_Name,
  331. municipality: mapData.records[0].fields.Municipality,
  332. type: mapData.records[0].fields.Type,
  333. search: mapData.records[0].fields.Search_Field
  334. }
  335. switch (isOrig) {
  336. case true:
  337. this.selectedOrig = { ...thisAirport }
  338. this.fetchDestinations(thisAirport.iata)
  339. break
  340. case false:
  341. this.selectedDest = { ...thisAirport }
  342. break
  343. default:
  344. break
  345. }
  346. },
  347. showPicker () {
  348. const vm = this
  349. vm.isPickerVisible = !vm.isPickerVisible
  350. setTimeout(function () {
  351. vm.$refs.mapComponent.resize()
  352. }, 500)
  353. if(vm.isPickerVisible) {
  354. this.$segment.track('go__show_picker')
  355. } else {
  356. this.$segment.track('go__hide_picker')
  357. }
  358. },
  359. showSidebar () {
  360. this.isSidebarVisible = !this.isSidebarVisible
  361. if(this.isSidebarVisible) {
  362. this.$segment.track('go__show_sidebar')
  363. } else {
  364. this.$segment.track('go__hide_sidebar')
  365. }
  366. }
  367. }
  368. }
  369. </script>
  370. <style scoped>
  371. main {
  372. background: black;
  373. }
  374. .logo-wrapper {
  375. position: relative;
  376. }
  377. .logo-blurb {
  378. position: absolute;
  379. font-size: clamp(10px, 1rem, 18px);
  380. color: white;
  381. --logo-v-offset: -0.65rem;
  382. --logo-h-offset: 0.35rem;
  383. top: clamp(20px + var(--logo-v-offset), 4rem + var(--logo-v-offset), 50px + var(--logo-v-offset));
  384. left: clamp(20px + var(--logo-h-offset), 4rem + var(--logo-h-offset), 50px + var(--logo-h-offset));
  385. white-space: nowrap;
  386. }
  387. </style>