├── .gitignore ├── .postcssrc ├── .eslintrc ├── image └── screenshot.jpg ├── README.md ├── netlify.toml ├── src ├── index.html ├── modules │ ├── renderInfo.js │ └── mapStyle.js ├── index.css └── main.js ├── package.json └── functions └── api.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | .cache 4 | -------------------------------------------------------------------------------- /.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["autoprefixer"] 3 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "afuh", 3 | "env": { 4 | "browser": true 5 | } 6 | } -------------------------------------------------------------------------------- /image/screenshot.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/afuh/iss/HEAD/image/screenshot.jpg -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Where is the International Space Station right now? 2 | 3 | **View it [here](https://iss.axelfuhrmann.com/)** 4 | 5 | ![screenshot](image/screenshot.jpg) 6 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [dev] 2 | command = "npm run dev" 3 | targetPort = 3000 4 | framework = "#custom" 5 | [build] 6 | publish = "dist" 7 | functions = "functions" 8 | framework = "#custom" 9 | [[redirects]] 10 | from = "/api" 11 | to = "/.netlify/functions/api" 12 | status = 200 -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Current position of the ISS 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "iss", 3 | "version": "1.0.0", 4 | "description": "Current position of the ISS", 5 | "main": "dist/index.html", 6 | "scripts": { 7 | "dev": "parcel src/index.html --port 3000", 8 | "build": "parcel build src/index.html", 9 | "prebuild": "rm -rf dist" 10 | }, 11 | "devDependencies": { 12 | "autoprefixer": "^9.8.6", 13 | "babel-eslint": "^10.1.0", 14 | "eslint": "^7.9.0", 15 | "eslint-config-afuh": "^0.2.0", 16 | "eslint-plugin-import": "^2.22.0", 17 | "parcel-bundler": "^1.12.4" 18 | }, 19 | "dependencies": { 20 | "isomorphic-fetch": "^2.2.1" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /functions/api.js: -------------------------------------------------------------------------------- 1 | const fetch = require('isomorphic-fetch') 2 | 3 | const fetcher = url => fetch(url).then(res => res.json()) 4 | 5 | const OPEN_NOTIFY = 'http://api.open-notify.org' 6 | const WHERE_THE_ISS_AT = 'https://api.wheretheiss.at/v1' 7 | 8 | exports.handler = async () => { 9 | try { 10 | const [info, astros] = await Promise.all([ 11 | fetcher(`${WHERE_THE_ISS_AT}/satellites/25544`), 12 | fetcher(`${OPEN_NOTIFY}/astros.json`) 13 | ]) 14 | 15 | return { 16 | statusCode: 200, 17 | body: JSON.stringify({ info, people: astros.people }) 18 | } 19 | } catch (err) { 20 | console.log(err) 21 | return { 22 | statusCode: 500, 23 | body: JSON.stringify({ error: err.message }) 24 | } 25 | } 26 | } -------------------------------------------------------------------------------- /src/modules/renderInfo.js: -------------------------------------------------------------------------------- 1 | import project from '../../package.json' 2 | 3 | const wiki = 'https://en.wikipedia.org/wiki/' 4 | 5 | const renderInfoBox = ({ people, info }) => { 6 | const el = document.querySelector('div.info') 7 | el.style = "display: flex;" 8 | 9 | const humans = people.map((human) => ` 10 |
  • 11 | ${human.name} 12 |
  • ` 13 | ).join(' ') 14 | 15 | el.innerHTML = ` 16 |
    17 | ${project.description} 18 | 40 |
    41 | 42 |
    43 | There are ${people.length} humans in space 44 | 47 |
    48 | ` 49 | } 50 | 51 | export default renderInfoBox 52 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary: #00BCD4; 3 | --transition: all 0.2s; 4 | --unit: 6px; 5 | } 6 | 7 | * { 8 | box-sizing: border-box; 9 | text-decoration: none; 10 | } 11 | 12 | body { 13 | margin: 0; 14 | padding: 0; 15 | font-family: -apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen-Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif; 16 | } 17 | 18 | a, span { 19 | color: whitesmoke; 20 | } 21 | 22 | ul { 23 | padding-left: 0; 24 | margin: 0; 25 | } 26 | 27 | ul li { 28 | list-style-type: none; 29 | } 30 | 31 | #map { 32 | width: 100%; 33 | height: 100vh; 34 | } 35 | 36 | .info { 37 | border-top-right-radius: var(--unit); 38 | border-bottom-right-radius: var(--unit); 39 | flex-flow: column; 40 | padding: calc(var(--unit)*3); 41 | position: absolute; 42 | top: 25%; 43 | left: 0; 44 | background: #000; 45 | opacity: 0.1; 46 | transition: var(--transition); 47 | } 48 | 49 | .info:hover { 50 | opacity: 0.8; 51 | } 52 | 53 | .info .block:not(:last-child) { 54 | margin-bottom: calc(var(--unit)*3); 55 | } 56 | 57 | .info .header { 58 | display: block; 59 | font-weight: 700; 60 | font-size: calc(var(--unit)*3); 61 | margin-bottom: calc(var(--unit)*3); 62 | } 63 | 64 | .info .item { 65 | font-weight: 200; 66 | margin-bottom: calc(var(--unit)*2); 67 | } 68 | 69 | .info .item .title { 70 | font-weight: 700; 71 | } 72 | 73 | .info .humans .item { 74 | font-weight: 500; 75 | } 76 | 77 | .info .header li.name span { 78 | font-weight: 200; 79 | } 80 | 81 | .info .human-length { 82 | color: var(--primary); 83 | } 84 | 85 | .info a { 86 | text-decoration: none; 87 | border-bottom: 1px solid var(--primary); 88 | transition: var(--transition); 89 | } 90 | 91 | a:hover { 92 | color: var(--primary); 93 | border: none; 94 | } -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | /*global google*/ 2 | import 'regenerator-runtime/runtime' 3 | 4 | import './index.css' 5 | import styles from './modules/mapStyle' 6 | import renderInfoBox from './modules/renderInfo' 7 | 8 | const REFRESH_TIME = 5000 9 | const ROOT = document.getElementById('map') 10 | const COLOR = '#fff' 11 | 12 | const fetchApi = () => fetch('/api').then(res => res.json()) 13 | const getLatLng = ({ info }) => new google.maps.LatLng(info.latitude, info.longitude) 14 | 15 | const init = ({ center }) => { 16 | const path = [center] 17 | 18 | return { 19 | circle(map, data) { 20 | const drawCircle = new google.maps.Circle({ 21 | strokeColor: COLOR, 22 | strokeOpacity: 0.8, 23 | strokeWeight: 2, 24 | fillOpacity: 1, 25 | fillColor: COLOR, 26 | center, 27 | radius: 20 * 1000 28 | }) 29 | 30 | drawCircle.setMap(map) 31 | renderInfoBox(data) 32 | }, 33 | line(map, data) { 34 | path.push(getLatLng({ info: data.info })) 35 | 36 | const drawLine = new google.maps.Polyline({ 37 | path, 38 | strokeColor: COLOR, 39 | strokeOpacity: 1, 40 | strokeWeight: 5 41 | }) 42 | 43 | drawLine.setMap(map) 44 | renderInfoBox(data) 45 | path.shift() 46 | } 47 | } 48 | } 49 | 50 | window.onload = async () => { 51 | const data = await fetchApi() 52 | const center = getLatLng({ info: data.info }) 53 | const render = init({ center }) 54 | 55 | const map = new google.maps.Map(ROOT, { 56 | center, 57 | zoom: 4, 58 | scrollwheel: false, 59 | streetViewControl: false, 60 | fullscreenControl: true, 61 | styles: styles[data.info.visibility] 62 | }) 63 | 64 | render.circle(map, data) 65 | 66 | setInterval(async () => { 67 | const data = await fetchApi() 68 | render.line(map, data) 69 | }, REFRESH_TIME) 70 | } 71 | -------------------------------------------------------------------------------- /src/modules/mapStyle.js: -------------------------------------------------------------------------------- 1 | export default { 2 | daylight: [ 3 | { 4 | 'elementType': 'geometry', 5 | 'stylers': [ 6 | { 7 | 'color': '#ebe3cd' 8 | } 9 | ] 10 | }, 11 | { 12 | 'elementType': 'labels.text.fill', 13 | 'stylers': [ 14 | { 15 | 'color': '#523735' 16 | } 17 | ] 18 | }, 19 | { 20 | 'elementType': 'labels.text.stroke', 21 | 'stylers': [ 22 | { 23 | 'color': '#f5f1e6' 24 | } 25 | ] 26 | }, 27 | { 28 | 'featureType': 'administrative', 29 | 'elementType': 'geometry', 30 | 'stylers': [ 31 | { 32 | 'visibility': 'off' 33 | } 34 | ] 35 | }, 36 | { 37 | 'featureType': 'administrative', 38 | 'elementType': 'geometry.stroke', 39 | 'stylers': [ 40 | { 41 | 'color': '#c9b2a6' 42 | } 43 | ] 44 | }, 45 | { 46 | 'featureType': 'administrative.land_parcel', 47 | 'elementType': 'geometry.stroke', 48 | 'stylers': [ 49 | { 50 | 'color': '#dcd2be' 51 | } 52 | ] 53 | }, 54 | { 55 | 'featureType': 'administrative.land_parcel', 56 | 'elementType': 'labels.text.fill', 57 | 'stylers': [ 58 | { 59 | 'color': '#ae9e90' 60 | } 61 | ] 62 | }, 63 | { 64 | 'featureType': 'landscape.natural', 65 | 'elementType': 'geometry', 66 | 'stylers': [ 67 | { 68 | 'color': '#dfd2ae' 69 | } 70 | ] 71 | }, 72 | { 73 | 'featureType': 'poi', 74 | 'stylers': [ 75 | { 76 | 'visibility': 'off' 77 | } 78 | ] 79 | }, 80 | { 81 | 'featureType': 'poi', 82 | 'elementType': 'geometry', 83 | 'stylers': [ 84 | { 85 | 'color': '#dfd2ae' 86 | } 87 | ] 88 | }, 89 | { 90 | 'featureType': 'poi', 91 | 'elementType': 'labels.text.fill', 92 | 'stylers': [ 93 | { 94 | 'color': '#93817c' 95 | } 96 | ] 97 | }, 98 | { 99 | 'featureType': 'poi.park', 100 | 'elementType': 'geometry.fill', 101 | 'stylers': [ 102 | { 103 | 'color': '#a5b076' 104 | } 105 | ] 106 | }, 107 | { 108 | 'featureType': 'poi.park', 109 | 'elementType': 'labels.text.fill', 110 | 'stylers': [ 111 | { 112 | 'color': '#447530' 113 | } 114 | ] 115 | }, 116 | { 117 | 'featureType': 'road', 118 | 'stylers': [ 119 | { 120 | 'visibility': 'off' 121 | } 122 | ] 123 | }, 124 | { 125 | 'featureType': 'road', 126 | 'elementType': 'geometry', 127 | 'stylers': [ 128 | { 129 | 'color': '#f5f1e6' 130 | } 131 | ] 132 | }, 133 | { 134 | 'featureType': 'road', 135 | 'elementType': 'labels.icon', 136 | 'stylers': [ 137 | { 138 | 'visibility': 'off' 139 | } 140 | ] 141 | }, 142 | { 143 | 'featureType': 'road.arterial', 144 | 'elementType': 'geometry', 145 | 'stylers': [ 146 | { 147 | 'color': '#fdfcf8' 148 | } 149 | ] 150 | }, 151 | { 152 | 'featureType': 'road.highway', 153 | 'elementType': 'geometry', 154 | 'stylers': [ 155 | { 156 | 'color': '#f8c967' 157 | } 158 | ] 159 | }, 160 | { 161 | 'featureType': 'road.highway', 162 | 'elementType': 'geometry.stroke', 163 | 'stylers': [ 164 | { 165 | 'color': '#e9bc62' 166 | } 167 | ] 168 | }, 169 | { 170 | 'featureType': 'road.highway.controlled_access', 171 | 'elementType': 'geometry', 172 | 'stylers': [ 173 | { 174 | 'color': '#e98d58' 175 | } 176 | ] 177 | }, 178 | { 179 | 'featureType': 'road.highway.controlled_access', 180 | 'elementType': 'geometry.stroke', 181 | 'stylers': [ 182 | { 183 | 'color': '#db8555' 184 | } 185 | ] 186 | }, 187 | { 188 | 'featureType': 'road.local', 189 | 'elementType': 'labels.text.fill', 190 | 'stylers': [ 191 | { 192 | 'color': '#806b63' 193 | } 194 | ] 195 | }, 196 | { 197 | 'featureType': 'transit', 198 | 'stylers': [ 199 | { 200 | 'visibility': 'off' 201 | } 202 | ] 203 | }, 204 | { 205 | 'featureType': 'transit.line', 206 | 'elementType': 'geometry', 207 | 'stylers': [ 208 | { 209 | 'color': '#dfd2ae' 210 | } 211 | ] 212 | }, 213 | { 214 | 'featureType': 'transit.line', 215 | 'elementType': 'labels.text.fill', 216 | 'stylers': [ 217 | { 218 | 'color': '#8f7d77' 219 | } 220 | ] 221 | }, 222 | { 223 | 'featureType': 'transit.line', 224 | 'elementType': 'labels.text.stroke', 225 | 'stylers': [ 226 | { 227 | 'color': '#ebe3cd' 228 | } 229 | ] 230 | }, 231 | { 232 | 'featureType': 'transit.station', 233 | 'elementType': 'geometry', 234 | 'stylers': [ 235 | { 236 | 'color': '#dfd2ae' 237 | } 238 | ] 239 | }, 240 | { 241 | 'featureType': 'water', 242 | 'elementType': 'geometry.fill', 243 | 'stylers': [ 244 | { 245 | 'color': '#b9d3c2' 246 | } 247 | ] 248 | }, 249 | { 250 | 'featureType': 'water', 251 | 'elementType': 'labels.text.fill', 252 | 'stylers': [ 253 | { 254 | 'color': '#92998d' 255 | } 256 | ] 257 | } 258 | ], 259 | eclipsed: [ 260 | { 261 | 'elementType': 'geometry', 262 | 'stylers': [ 263 | { 264 | 'color': '#242f3e' 265 | } 266 | ] 267 | }, 268 | { 269 | 'elementType': 'labels.text.fill', 270 | 'stylers': [ 271 | { 272 | 'color': '#746855' 273 | } 274 | ] 275 | }, 276 | { 277 | 'elementType': 'labels.text.stroke', 278 | 'stylers': [ 279 | { 280 | 'color': '#242f3e' 281 | } 282 | ] 283 | }, 284 | { 285 | 'featureType': 'administrative', 286 | 'elementType': 'geometry', 287 | 'stylers': [ 288 | { 289 | 'visibility': 'off' 290 | } 291 | ] 292 | }, 293 | { 294 | 'featureType': 'administrative.locality', 295 | 'elementType': 'labels.text.fill', 296 | 'stylers': [ 297 | { 298 | 'color': '#d59563' 299 | } 300 | ] 301 | }, 302 | { 303 | 'featureType': 'poi', 304 | 'stylers': [ 305 | { 306 | 'visibility': 'off' 307 | } 308 | ] 309 | }, 310 | { 311 | 'featureType': 'poi', 312 | 'elementType': 'labels.text.fill', 313 | 'stylers': [ 314 | { 315 | 'color': '#d59563' 316 | } 317 | ] 318 | }, 319 | { 320 | 'featureType': 'poi.park', 321 | 'elementType': 'geometry', 322 | 'stylers': [ 323 | { 324 | 'color': '#263c3f' 325 | } 326 | ] 327 | }, 328 | { 329 | 'featureType': 'poi.park', 330 | 'elementType': 'labels.text.fill', 331 | 'stylers': [ 332 | { 333 | 'color': '#6b9a76' 334 | } 335 | ] 336 | }, 337 | { 338 | 'featureType': 'road', 339 | 'stylers': [ 340 | { 341 | 'visibility': 'off' 342 | } 343 | ] 344 | }, 345 | { 346 | 'featureType': 'road', 347 | 'elementType': 'geometry', 348 | 'stylers': [ 349 | { 350 | 'color': '#38414e' 351 | } 352 | ] 353 | }, 354 | { 355 | 'featureType': 'road', 356 | 'elementType': 'geometry.stroke', 357 | 'stylers': [ 358 | { 359 | 'color': '#212a37' 360 | } 361 | ] 362 | }, 363 | { 364 | 'featureType': 'road', 365 | 'elementType': 'labels.icon', 366 | 'stylers': [ 367 | { 368 | 'visibility': 'off' 369 | } 370 | ] 371 | }, 372 | { 373 | 'featureType': 'road', 374 | 'elementType': 'labels.text.fill', 375 | 'stylers': [ 376 | { 377 | 'color': '#9ca5b3' 378 | } 379 | ] 380 | }, 381 | { 382 | 'featureType': 'road.highway', 383 | 'elementType': 'geometry', 384 | 'stylers': [ 385 | { 386 | 'color': '#746855' 387 | } 388 | ] 389 | }, 390 | { 391 | 'featureType': 'road.highway', 392 | 'elementType': 'geometry.stroke', 393 | 'stylers': [ 394 | { 395 | 'color': '#1f2835' 396 | } 397 | ] 398 | }, 399 | { 400 | 'featureType': 'road.highway', 401 | 'elementType': 'labels.text.fill', 402 | 'stylers': [ 403 | { 404 | 'color': '#f3d19c' 405 | } 406 | ] 407 | }, 408 | { 409 | 'featureType': 'transit', 410 | 'stylers': [ 411 | { 412 | 'visibility': 'off' 413 | } 414 | ] 415 | }, 416 | { 417 | 'featureType': 'transit', 418 | 'elementType': 'geometry', 419 | 'stylers': [ 420 | { 421 | 'color': '#2f3948' 422 | } 423 | ] 424 | }, 425 | { 426 | 'featureType': 'transit.station', 427 | 'elementType': 'labels.text.fill', 428 | 'stylers': [ 429 | { 430 | 'color': '#d59563' 431 | } 432 | ] 433 | }, 434 | { 435 | 'featureType': 'water', 436 | 'elementType': 'geometry', 437 | 'stylers': [ 438 | { 439 | 'color': '#17263c' 440 | } 441 | ] 442 | }, 443 | { 444 | 'featureType': 'water', 445 | 'elementType': 'labels.text.fill', 446 | 'stylers': [ 447 | { 448 | 'color': '#515c6d' 449 | } 450 | ] 451 | }, 452 | { 453 | 'featureType': 'water', 454 | 'elementType': 'labels.text.stroke', 455 | 'stylers': [ 456 | { 457 | 'color': '#17263c' 458 | } 459 | ] 460 | } 461 | ] } --------------------------------------------------------------------------------