diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client.ts | 4 | ||||
-rw-r--r-- | src/components/Nav.svelte | 60 | ||||
-rw-r--r-- | src/components/PredictedSpaceWeather.svelte | 174 | ||||
-rw-r--r-- | src/components/PredictedSpaceWeatherThing.svelte | 64 | ||||
-rw-r--r-- | src/components/WeatherCurrent.svelte | 210 | ||||
-rw-r--r-- | src/routes/_error.svelte | 40 | ||||
-rw-r--r-- | src/routes/_layout.svelte | 54 | ||||
-rw-r--r-- | src/routes/index.svelte | 28 | ||||
-rw-r--r-- | src/server.ts | 18 | ||||
-rw-r--r-- | src/service-worker.ts | 82 | ||||
-rw-r--r-- | src/template.html | 35 |
11 files changed, 769 insertions, 0 deletions
diff --git a/src/client.ts b/src/client.ts new file mode 100644 index 0000000..1a2fbc3 --- /dev/null +++ b/src/client.ts @@ -0,0 +1,4 @@ +import * as sapper from '@sapper/app'; +sapper.start({ + target: document.querySelector('.app') +}); diff --git a/src/components/Nav.svelte b/src/components/Nav.svelte new file mode 100644 index 0000000..3240ecf --- /dev/null +++ b/src/components/Nav.svelte @@ -0,0 +1,60 @@ +<script lang="ts"> + export let segment: string; +</script> + +<style> + nav { + border-bottom: 1px solid rgba(255,62,0,0.1); + font-weight: 300; + padding: 0 1em; + } + + ul { + margin: 0; + padding: 0; + } + + /* clearfix */ + ul::after { + content: ''; + display: block; + clear: both; + } + + li { + display: block; + float: left; + } + + [aria-current] { + position: relative; + display: inline-block; + } + + [aria-current]::after { + position: absolute; + content: ''; + width: calc(100% - 1em); + height: 2px; + background-color: rgb(255,62,0); + display: block; + bottom: -1px; + } + + a { + text-decoration: none; + padding: 1em 0.5em; + display: block; + } +</style> + +<nav> + <ul> + <li><a aria-current="{segment === undefined ? 'page' : undefined}" href=".">home</a></li> + <li><a aria-current="{segment === 'about' ? 'page' : undefined}" href="about">about</a></li> + + <!-- for the blog link, we're using rel=prefetch so that Sapper prefetches + the blog data when we hover over the link or tap it on a touchscreen --> + <li><a rel=prefetch aria-current="{segment === 'blog' ? 'page' : undefined}" href="blog">blog</a></li> + </ul> +</nav> diff --git a/src/components/PredictedSpaceWeather.svelte b/src/components/PredictedSpaceWeather.svelte new file mode 100644 index 0000000..9194447 --- /dev/null +++ b/src/components/PredictedSpaceWeather.svelte @@ -0,0 +1,174 @@ +<script lang="ts"> + // import { onMount } from 'svelte'; + import PredictedSpaceWeatherThing from './PredictedSpaceWeatherThing.svelte'; + + let predictions; + // async function haschange() { + // let data = await fetch(`https://api.met.no/weatherapi/locationforecast/2.0/compact?lat=${latitude}&lon=${longitude}`).then(res => res.json()); + // let data = await fetch("https://services.swpc.noaa.gov/products/noaa-planetary-k-index-forecast.json").then(res => res.json()); + // data.shift(); + // let updatedPredictions = []; + // data.forEach((pred, i) => { + // if (pred[2] != "observed") { + // updatedPredictions.push(pred); + // } + // }); + // predictions = updatedPredictions; + // } + // + // onMount(haschange); + + + + import { onMount } from 'svelte'; + const monthNames = ["January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" + ]; + + let longitude; + let latitude; + let locationSupported; + let dataLoading = true; + let defaultLocation = false; + let weather; + let days = Array(); + + async function getWeather(longitude, latitude) { + let yr_data = await fetch(`https://api.met.no/weatherapi/locationforecast/2.0/compact?lat=${latitude}&lon=${longitude}`).then(res => res.json()); + + let kp_data = await fetch("https://services.swpc.noaa.gov/products/noaa-planetary-k-index-forecast.json").then(res => res.json()); + kp_data.shift(); + let updatedPredictions = []; + kp_data.forEach((pred, i) => { + if (pred[2] != "observed") { + let temp; + let clouds; + let cDate = new Date(pred[0]); + let closestDate = new Date(0,0,0); + yr_data["properties"]["timeseries"].forEach((pred, i) => { + let predDate = new Date(pred["time"]); + if (Math.abs(predDate.getTime() - cDate.getTime()) < Math.abs(closestDate.getTime() - cDate.getTime())) { + closestDate = predDate; + temp = (pred["data"]["instant"]["details"]["air_temperature"]); + clouds = pred["data"]["instant"]["details"]["cloud_area_fraction"]; + } + }) + + updatedPredictions.push({ + "time": pred[0], + "kp": pred[1], + "temp": temp, + "clouds": clouds + }); + } + }); + predictions = updatedPredictions; + } + function getDays(daily) { + var data = daily.data.slice(0,5) + data.forEach(element => { + var a = new Date(element.time*1000) + var dayStrings = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; + days.push(dayStrings[a.getDay()]) + }); + } + function getLocation() { + if (navigator.geolocation) { + dataLoading = true + locationSupported = true + navigator.geolocation.getCurrentPosition(setLocation, locationError) + } else { + locationSupported = false + noLocation() + } + } + + function setLocation(position) { + longitude = position.coords.longitude + latitude = position.coords.latitude + getWeather(longitude, latitude) + } + + function locationError(err) { + noLocation() + } + + function noLocation() { + longitude = 28.283333 + latitude = -15.416667 + toggleLoading() + } + function toggleLoading() { + dataLoading = !dataLoading + } + function toggleDefault() { + defaultLocation = !defaultLocation + } + onMount(getLocation); +</script> + +<style> + .predicted-weather { + --bg-opacity: 1; + background-color: #f7fafc; + background-color: rgba(247, 250, 252, var(--bg-opacity)); + padding: 1.5rem; + --text-opacity: 1; + color: #1a202c; + color: rgba(26, 32, 44, var(--text-opacity)); + height: 100%; + overflow: hidden; + + /* border-top-left-radius: 1rem; + border-top-right-radius: 1rem; + transform: translatey(-1rem); */ + } + + @media (min-width: 640px), (min-height: 720px) { + .predicted-weather { + padding: 2rem; + padding-top: 1.5rem; + } + } + + @media (min-width: 640px) { + .predicted-weather { + border-bottom-right-radius: 1rem; + border-bottom-left-radius: 1rem; + } + } + + .predicted-weather h2 { + text-transform: uppercase; + font-size: 0.875rem; + letter-spacing: 0.1em; + font-weight: 700; + margin-top: 0.25rem; + margin-bottom: 0.5rem; + } + + .prediction-table { + height: 100%; + overflow-y: scroll; + padding-bottom: 1rem; + } + + .prediction-table::-webkit-scrollbar { + display: none; + } +</style> + +<div class="predicted-weather"> + <div className="flex flex-row justify-between items-top"> + <h2>Predicted</h2> + </div> + <div class="prediction-table"> + {#if predictions} + {#each predictions as prediction, i} + <PredictedSpaceWeatherThing {prediction}/> + {/each} + {:else} + Cannot connect to NOAA + {/if} + </div> +</div> diff --git a/src/components/PredictedSpaceWeatherThing.svelte b/src/components/PredictedSpaceWeatherThing.svelte new file mode 100644 index 0000000..0a40e79 --- /dev/null +++ b/src/components/PredictedSpaceWeatherThing.svelte @@ -0,0 +1,64 @@ +<script> + export let prediction; + + const monthNames = ["January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" + ]; + + let kp = prediction["kp"]; + let dateTime = prediction["time"].split(" "); + let date = Number(dateTime[0].split("-")[2]) + ". " + monthNames[Number(dateTime[0].split("-")[1])]; + let time = dateTime[1].substring(0,5); + let temp = prediction["temp"]; + let clouds = prediction["clouds"]; +</script> + +<style> + .prediction-details { + display: flex; + justify-content: space-between; + border-bottom-width: 1px; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + font-size: 0.75rem; + letter-spacing: 0.05em; + } + + .prediction-details:last-of-type { + border-width: 0; + padding-bottom: 0; + } + + .prediction-details h3 { + font-size: 0.875rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + margin-right: 0.5rem; + } + + .prediction-details .data { + display: flex; + flex-direction: row; + } + + .prediction-details .data h2 { + margin-right: 1rem; + font-size: 1.5rem; + } + +</style> + +<div class="prediction-details"> + <div> + <h3>{time}</h3> + <p>{date}</p> + </div> + <div class="data"> + <h2>{kp}</h2> + <div> + <p><i class="fas fa-thermometer-half"></i> {temp}°C</p> + <p><i class="fas fa-cloud"></i> {clouds}%</p> + </div> + </div> +</div> diff --git a/src/components/WeatherCurrent.svelte b/src/components/WeatherCurrent.svelte new file mode 100644 index 0000000..6f92b20 --- /dev/null +++ b/src/components/WeatherCurrent.svelte @@ -0,0 +1,210 @@ +<script lang="ts"> + import { onMount } from 'svelte'; + const monthNames = ["January", "February", "March", "April", "May", "June", + "July", "August", "September", "October", "November", "December" + ]; + + let longitude; + let latitude; + let locationSupported; + let dataLoading = true; + let defaultLocation = false; + let weather; + let days = Array(); + + let location = "The earth"; + let date = "-"; + let kp_now = "-"; + let kp_min = "-"; + let kp_max = "-"; + let bz = "-"; + let clouds = "-"; + + async function getWeather(longitude, latitude) { + let yr_data = await fetch(`https://api.met.no/weatherapi/locationforecast/2.0/compact?lat=${latitude}&lon=${longitude}`).then(res => res.json()); + clouds = yr_data["properties"]["timeseries"][0]["data"]["instant"]["details"]["cloud_area_fraction"]; + bz = (await fetch("https://services.swpc.noaa.gov/products/summary/solar-wind-mag-field.json").then(res => res.json()))["Bz"]; + //console.log (await fetch(`http://maps.googleapis.com/maps/api/geocode/json?latlng=${latitude},${longitude}&sensor=true`).then(res => res.json())); + let kp_data = await fetch('https://services.swpc.noaa.gov/products/noaa-planetary-k-index-forecast.json').then(res => res.json()); + kp_data.shift(); + let cDate = new Date(); + let closestDate = new Date(0,0,0); + let minkp = 1000; // Just a larger number than any plausable value + let maxkp = 0; + + kp_data.forEach((pred, i) => { + if (pred[1] > maxkp) { + maxkp = pred[1]; + } + if (pred[1] < minkp) { + minkp = pred[1]; + } + let predDate = new Date(pred[0]); + if (Math.abs(predDate.getTime() - cDate.getTime()) < Math.abs(closestDate.getTime() - cDate.getTime())) { + closestDate = predDate; + kp_now = pred[1]; + } + }); + kp_min = minkp.toString(); + kp_max = maxkp.toString(); + + date = cDate.getDay() + ". " + monthNames[cDate.getMonth()] + " " + cDate.getHours() + ":" + cDate.getMinutes(); + } + function getDays(daily) { + var data = daily.data.slice(0,5) + data.forEach(element => { + var a = new Date(element.time*1000) + var dayStrings = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday']; + days.push(dayStrings[a.getDay()]) + }); + } + function getLocation() { + if (navigator.geolocation) { + dataLoading = true + locationSupported = true + navigator.geolocation.getCurrentPosition(setLocation, locationError) + } else { + locationSupported = false + noLocation() + } + } + + function setLocation(position) { + longitude = position.coords.longitude + latitude = position.coords.latitude + getWeather(longitude, latitude) + } + + function locationError(err) { + noLocation() + } + + function noLocation() { + longitude = 28.283333 + latitude = -15.416667 + toggleLoading() + } + function toggleLoading() { + dataLoading = !dataLoading + } + function toggleDefault() { + defaultLocation = !defaultLocation + } + onMount(getLocation); +</script> + +<style> + + .weatherCurrent-wrapper { + height: 100%; + font-family: Roboto, sans-serif; + font-size: 1rem; + letter-spacing: 0.05em; + --bg-opacity: 1; + background-color: #1a202c; + background-color: rgba(26, 32, 44, var(--bg-opacity)); + background: + linear-gradient( + rgba(0, 0, 0, 0.5), + rgba(0, 0, 0, 0.5) + ), url(/aurora.jpg); + background-size: cover; + background-repeat: no-repeat; + background-position: center; + } + + .weatherCurrent-data { + width: 100%; + padding: 1.5rem; + flex-grow: 1; + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + --text-opacity: 1; + color: #fff; + color: rgba(255, 255, 255, var(--text-opacity)); + text-align: center; + letter-spacing: 0.05em; + text-shadow: 1px 1px 2px rgba(0,0,0,.75); + } + + .weatherCurrent-data-location { + display: flex; + justify-content: center; + align-content: center; + } + + .weatherCurrent-data-location .symbol { + width: 1.5rem; + height: 1.5rem; + margin-right: 1rem; + } + + + .weatherCurrent-data-location h1 { + text-transform: uppercase; + font-family: Roboto Condensed, sans-serif; + font-size: 1.125rem; + letter-spacing: 0.1em; + margin-bottom: 0; + } + + .weatherCurrent-data-kp h2 { + font-weight: 700; + font-size: 3rem; + letter-spacing: 0.05em; + line-height: 1.25; + } + + .current-details { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + grid-gap: 1rem; + gap: 1rem; + text-shadow: 1px 1px 2px rgba(0,0,0,.75); + } + + .current-details p { + line-height: 1.375; + } + + +</style> + +<div class="weatherCurrent-wrapper"> + <div class="weatherCurrent-data"> + <div class="weatherCurrent-data-location"> + <i class="symbol fas fa-map-marker-alt"></i> + <h1>{location}</h1> + </div> + + <div class="weatherCurrent-data-date"> + <p>{date}</p> + </div> + + <div class="weatherCurrent-data-kp"> + <h2>KP {kp_now}</h2> + <p> + <span className="pr-2">↑ KP {kp_max}</span> + <span className="pl-2">↓ KP {kp_min}</span> + </p> + </div> + + <div class="current-details"> + <div> + <p>BZ</p> + <p>{bz}</p> + </div> + <div> + <p>Probability</p> + <p>10%</p> + </div> + <div> + <p>Clouds</p> + <p>{clouds}%</p> + </div> + </div> + + </div> +</div> diff --git a/src/routes/_error.svelte b/src/routes/_error.svelte new file mode 100644 index 0000000..92fcca8 --- /dev/null +++ b/src/routes/_error.svelte @@ -0,0 +1,40 @@ +<script lang="ts"> + export let status: number; + export let error: Error; + + const dev = process.env.NODE_ENV === 'development'; +</script> + +<style> + h1, p { + margin: 0 auto; + } + + h1 { + font-size: 2.8em; + font-weight: 700; + margin: 0 0 0.5em 0; + } + + p { + margin: 1em auto; + } + + @media (min-width: 480px) { + h1 { + font-size: 4em; + } + } +</style> + +<svelte:head> + <title>{status}</title> +</svelte:head> + +<h1>{status}</h1> + +<p>{error.message}</p> + +{#if dev && error.stack} + <pre>{error.stack}</pre> +{/if} diff --git a/src/routes/_layout.svelte b/src/routes/_layout.svelte new file mode 100644 index 0000000..bab4188 --- /dev/null +++ b/src/routes/_layout.svelte @@ -0,0 +1,54 @@ +<script lang="ts"> +</script> + +<style> + .app-container { + font-family: Roboto, sans-serif; + font-size: 1rem; + letter-spacing: 0.05em; + width: 100%; + height: 100%; + margin: 0; + padding: 0; + --bg-opacity: 1; + background-color: #1a202c; + background-color: rgba(26, 32, 44, var(--bg-opacity)); + background-size: cover; + background-repeat: no-repeat; + background-position: center; + } + + @media (min-width: 640px) { + .app-container { + border-radius: 1rem; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + font-family: Roboto, sans-serif; + font-size: 1rem; + letter-spacing: 0.05em; + height: 100%; + background-size: cover; + background-repeat: no-repeat; + background-position: center; + width: 375px; + height: 812px; + } + } + + .menu-button { + position: relative; + top: .25rem; + left: .75rem; + color: #ffffff; + font-size: 1.5rem; + + } + +</style> + +<div class="app-container"> + <!-- <div class="menu-button"> + <i class="fas fa-bars"></i> + </div> --> + + <slot></slot> +</div> diff --git a/src/routes/index.svelte b/src/routes/index.svelte new file mode 100644 index 0000000..655f0c4 --- /dev/null +++ b/src/routes/index.svelte @@ -0,0 +1,28 @@ +<script lang="ts"> + import WeatherCurrent from '../components/WeatherCurrent.svelte'; + import PredictedSpaceWeather from '../components/PredictedSpaceWeather.svelte'; +</script> + + +<style> + .homescreen { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + --bg-opacity: 1; + background-color: #1a202c; + background-color: rgba(26, 32, 44, var(--bg-opacity)); + --bg-opacity: 0.25; + } +</style> + +<svelte:head> + <title>Aurora data</title> +</svelte:head> + + +<div class="homescreen"> + <WeatherCurrent/> + <PredictedSpaceWeather /> +</div> diff --git a/src/server.ts b/src/server.ts new file mode 100644 index 0000000..8425b2c --- /dev/null +++ b/src/server.ts @@ -0,0 +1,18 @@ +import sirv from 'sirv'; +import polka from 'polka'; +import compression from 'compression'; +import * as sapper from '@sapper/server'; + +const { PORT, NODE_ENV } = process.env; +const dev = NODE_ENV === 'development'; + +polka() // You can also use Express + .use( + 'aurora-data', + compression({ threshold: 0 }), + sirv('static', { dev }), + sapper.middleware() + ) + .listen(PORT, err => { + if (err) console.log('error', err); + }); diff --git a/src/service-worker.ts b/src/service-worker.ts new file mode 100644 index 0000000..1441676 --- /dev/null +++ b/src/service-worker.ts @@ -0,0 +1,82 @@ +import { timestamp, files, shell, routes } from '@sapper/service-worker'; + +const ASSETS = `cache${timestamp}`; + +// `shell` is an array of all the files generated by the bundler, +// `files` is an array of everything in the `static` directory +const to_cache = (shell as string[]).concat(files as string[]); +const cached = new Set(to_cache); + +self.addEventListener('install', <EventType extends ExtendableEvent>(event: EventType) => { + event.waitUntil( + caches + .open(ASSETS) + .then(cache => cache.addAll(to_cache)) + .then(() => { + ((self as any) as ServiceWorkerGlobalScope).skipWaiting(); + }) + ); +}); + +self.addEventListener('activate', <EventType extends ExtendableEvent>(event: EventType) => { + event.waitUntil( + caches.keys().then(async keys => { + // delete old caches + for (const key of keys) { + if (key !== ASSETS) await caches.delete(key); + } + + ((self as any) as ServiceWorkerGlobalScope).clients.claim(); + }) + ); +}); + +self.addEventListener('fetch', <EventType extends FetchEvent>(event: EventType) => { + if (event.request.method !== 'GET' || event.request.headers.has('range')) return; + + const url = new URL(event.request.url); + + // don't try to handle e.g. data: URIs + if (!url.protocol.startsWith('http')) return; + + // ignore dev server requests + if (url.hostname === self.location.hostname && url.port !== self.location.port) return; + + // always serve static files and bundler-generated assets from cache + if (url.host === self.location.host && cached.has(url.pathname)) { + event.respondWith(caches.match(event.request)); + return; + } + + // for pages, you might want to serve a shell `service-worker-index.html` file, + // which Sapper has generated for you. It's not right for every + // app, but if it's right for yours then uncomment this section + /* + if (url.origin === self.origin && routes.find(route => route.pattern.test(url.pathname))) { + event.respondWith(caches.match('/service-worker-index.html')); + return; + } + */ + + if (event.request.cache === 'only-if-cached') return; + + // for everything else, try the network first, falling back to + // cache if the user is offline. (If the pages never change, you + // might prefer a cache-first approach to a network-first one.) + event.respondWith( + caches + .open(`offline${timestamp}`) + .then(async cache => { + try { + const response = await fetch(event.request); + cache.put(event.request, response.clone()); + return response; + } catch(err) { + const response = await cache.match(event.request); + if (response) return response; + + throw err; + } + }) + ); +}); diff --git a/src/template.html b/src/template.html new file mode 100644 index 0000000..f7382aa --- /dev/null +++ b/src/template.html @@ -0,0 +1,35 @@ +<!doctype html> +<html lang="en"> +<head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width,initial-scale=1.0"> + <meta name="theme-color" content="#333333"> + + %sapper.base% + + <link rel="stylesheet" href="index.css"> + <link rel="manifest" href="manifest.json" crossorigin="use-credentials"> + <link rel="icon" type="image/png" href="favicon.png"> + <script src="https://kit.fontawesome.com/9fa565d9ec.js" crossorigin="anonymous"></script> + + <!-- Sapper creates a <script> tag containing `src/client.js` + and anything else it needs to hydrate the app and + initialise the router --> + %sapper.scripts% + + <!-- Sapper generates a <style> tag containing critical CSS + for the current page. CSS for the rest of the app is + lazily loaded when it precaches secondary pages --> + %sapper.styles% + + <!-- This contains the contents of the <svelte:head> component, if + the current page has one --> + %sapper.head% +</head> +<body> + <div id="svelte" class="app"> + %sapper.html% + </div> + <!-- <div id="sapper">%sapper.html%</div> --> +</body> +</html> |