New and improved in sveltekit

This commit is contained in:
2023-05-22 22:29:43 +02:00
parent 4c5ede45a1
commit 23d4b727e6
78 changed files with 6148 additions and 648 deletions

View File

@@ -1,6 +1,10 @@
{
"useTabs": true,
"useTabs": false,
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100
}
"printWidth": 100,
"plugins": ["prettier-plugin-svelte"],
"pluginSearchDirs": ["."],
"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }],
"svelteStrictMode": true
}

BIN
elasticQueries/-X Normal file

Binary file not shown.

View File

@@ -0,0 +1,57 @@
{
"aggs": {
"2": {
"date_histogram": {
"field": "@timestamp",
"fixed_interval": "30s",
"time_zone": "Europe/Oslo",
"min_doc_count": 1
},
"aggs": {
"1": {
"max": {
"field": "temperature"
}
}
}
}
},
"size": 0,
"fields": [
{
"field": "@timestamp",
"format": "date_time"
}
],
"script_fields": {},
"stored_fields": ["*"],
"_source": {
"excludes": []
},
"query": {
"bool": {
"must": [],
"filter": [
{
"match_all": {}
},
{
"match_phrase": {
"location.keyword": "inside"
}
},
{
"range": {
"@timestamp": {
"gte": "2022-04-03T13:04:26.460Z",
"lte": "2022-04-03T13:19:26.460Z",
"format": "strict_date_optional_time"
}
}
}
],
"should": [],
"must_not": []
}
}
}

View File

@@ -1,31 +1,41 @@
{
"name": "brewpi",
"name": "schleppe-brew",
"version": "0.0.1",
"scripts": {
"dev": "svelte-kit dev",
"build": "svelte-kit build",
"package": "svelte-kit package",
"preview": "svelte-kit preview",
"check": "svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .",
"format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ."
"dev": "vite dev",
"build": "vite build",
"preview": "vite preview",
"check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
"check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
"lint": "prettier --plugin-search-dir . --check src && eslint src",
"format": "prettier --plugin-search-dir . --write src"
},
"devDependencies": {
"@sveltejs/adapter-auto": "next",
"@sveltejs/kit": "next",
"@typescript-eslint/eslint-plugin": "^4.31.1",
"@typescript-eslint/parser": "^4.31.1",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-svelte3": "^3.2.1",
"prettier": "^2.4.1",
"prettier-plugin-svelte": "^2.4.0",
"svelte": "^3.44.0",
"svelte-check": "^2.2.6",
"svelte-preprocess": "^4.9.4",
"tslib": "^2.3.1",
"typescript": "^4.4.3"
"@sveltejs/adapter-node": "^1.2.4",
"@sveltejs/adapter-static": "^1.0.0",
"@sveltejs/kit": "^1.0.0",
"@typescript-eslint/eslint-plugin": "^5.46.1",
"@typescript-eslint/parser": "^5.46.1",
"eslint": "^8.29.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-svelte3": "^4.0.0",
"prettier": "^2.8.1",
"prettier-plugin-svelte": "^2.9.0",
"sass": "^1.56.2",
"svelte": "^3.55.0",
"svelte-check": "^2.10.2",
"svelte-preprocess": "^5.0.0",
"tslib": "^2.4.1",
"typescript": "^4.9.4",
"vite": "^4.0.1"
},
"type": "module"
}
"type": "module",
"dependencies": {
"chart.js": "^3.8.0",
"chartjs-plugin-zoom": "^1.2.1",
"d3": "^7.8.4",
"d3-selection": "^3.0.0",
"d3-timeseries": "^1.0.1",
"d3-transition": "^3.0.1"
}
}

View File

@@ -3,11 +3,17 @@
<head>
<meta charset="utf-8" />
<meta name="description" content="" />
<link rel="icon" href="%svelte.assets%/favicon.png" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<link rel="stylesheet" type="text/css" href="/global.css" />
<link rel="stylesheet" type="text/css" href="/variables.css" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%svelte.head%
%sveltekit.head%
</head>
<body>
<div id="svelte">%svelte.body%</div>
<body data-sveltekit-prefetch>
<div>%sveltekit.body%</div>
</body>
</html>
<style lang="scss"></style>

91
src/brews.json Normal file
View File

@@ -0,0 +1,91 @@
[{
"beer": {
"name": "Kveldsbris",
"brewery": "Kinn Bryggeri",
"category": "Pilsner/Lys Lager",
"description": ""
},
"date": "1682272800",
"by": ["Alf", "Kevin"],
"abv": "5.6",
"description": "",
"image": "kinn_kveldsbris.png",
"recipe": "https://docs.google.com/document/d/1FL7ibXxW1r_zFNLK338pyjfMiCCaTOi2fzuMoInA3dQ",
"order_page": "https://oslo.bryggselv.no/finest/104923/finest-originals-utepils-allgrain-ølsett-25-liter",
"untapped": "https://untappd.com/b/kinn-bryggeri-kveldsbris/695024"
}, {
"beer": {
"name": "FUCK YEAH IPA",
"brewery": "Finest",
"category": "American IPA",
"description": ""
},
"date": "1648922400",
"by": ["Alf", "Kevin"],
"abv": "7",
"description": "",
"image": "finest_fuck-yeah-IPA.jpg",
"recipe": "https://docs.google.com/document/d/1FL7ibXxW1r_zFNLK338pyjfMiCCaTOi2fzuMoInA3dQ",
"order_page": "https://web.archive.org/web/20210225043236/https://www.bryggselv.no/finest/105943/fuck-yeah-ipa-ultra-american-west-coast-ipa-25-liter",
"untapped": "https://untappd.com/b/kinn-bryggeri-kveldsbris/695024"
}, {
"beer": {
"name": "Love in a canoe",
"brewery": "Finest",
"category": "Mexican Lager",
"description": ""
},
"date": "1646420400",
"by": ["Alf", "Kevin"],
"abv": "4.7",
"description": "",
"image": "finest_love-in-a-canoe.jpeg",
"recipe": "https://docs.google.com/document/d/1FL7ibXxW1r_zFNLK338pyjfMiCCaTOi2fzuMoInA3dQ",
"order_page": "https://oslo.bryggselv.no/finest/104092/love-in-a-canoe-allgrain-ølsett-25-liter",
"untapped": "https://untappd.com/b/kinn-bryggeri-kveldsbris/695024"
}, {
"beer": {
"name": "Utepils",
"brewery": "Finest",
"category": "",
"description": ""
},
"date": "1637694000",
"by": ["Alf", "Kevin"],
"abv": "5.0",
"description": "",
"image": "finest_utepils.jpeg",
"recipe": "https://docs.google.com/document/d/1FL7ibXxW1r_zFNLK338pyjfMiCCaTOi2fzuMoInA3dQ",
"order_page": "https://www.bryggselv.no/finest/105932/kinn-kveldsbris-allgrain-ølsett-25-liter",
"untapped": "https://untappd.com/b/kinn-bryggeri-kveldsbris/695024"
}, {
"beer": {
"name": "HELLES Tysk Lager",
"brewery": "Münchener Helles",
"category": "Tysk Lager",
"description": ""
},
"date": "1629396000",
"by": ["Adrian", "Kevin", "Mats"],
"abv": "5.3",
"description": "",
"image": "helles_tysk-lager.jpeg",
"recipe": "https://docs.google.com/document/d/1FL7ibXxW1r_zFNLK338pyjfMiCCaTOi2fzuMoInA3dQ",
"order_page": "https://oslo.bryggselv.no/finest/106231/finest-helles-allgrain-ølsett-25-liter",
"untapped": "https://untappd.com/b/kinn-bryggeri-kveldsbris/695024"
}, {
"beer": {
"name": "Lazy Days Weiss",
"brewery": "Finest",
"category": "Weissbier",
"description": ""
},
"date": "1621706400",
"by": ["Alf", "Kevin", "Kristian"],
"abv": "5.3",
"description": "",
"image": "finest_lazy-days.jpeg",
"recipe": "https://docs.google.com/document/u/0/d/1I6qX4l4jDzK51GxBt3IdEv-HyNQHAx8ijc5dMlG1Xkk",
"order_page": "https://oslo.bryggselv.no/finest/106231/finest-helles-allgrain-ølsett-25-liter",
"untapped": "https://untappd.com/b/kinn-bryggeri-kveldsbris/695024"
}]

7
src/global.d.ts vendored
View File

@@ -1 +1,6 @@
/// <reference types="@sveltejs/kit" />
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
// and what to do when importing types
declare namespace App {
string;
}

View File

@@ -0,0 +1,25 @@
<div class="button-circle">
<slot></slot>
</div>
<style lang="scss" module="scoped">
.button-circle {
position: absolute;
top: 1rem;
right: 1rem;
height: 40px;
cursor: pointer;
user-select: none;
-webkit-user-select: none;
background-color: var(--backdrop);
padding: 0.5rem;
border-radius: 50%;
transition: all 0.25s ease-in-out;
&:hover {
transition: scale(1.15);
scale: 1.15;
}
}
</style>

View File

@@ -0,0 +1,46 @@
<script lang="ts">
import { onMount } from 'svelte'
// import { setTheme, theme } from '../themeStore'
import type { Theme } from '../types'
function toggleDarkmode() {
// setTheme(nextTheme);
// document.body.className = $theme
}
function systemDarkModeEnabled() {
const computedStyle = window.getComputedStyle(document.body);
if (computedStyle['colorScheme'] != null) {
return computedStyle.colorScheme.includes('dark');
}
return false;
}
$: icon = true ? '🌝' : '🌚';
// $: icon = $theme === 'dark' ? '🌝' : '🌚';
// $: nextTheme = ($theme === 'dark' ? 'light' : 'dark') as Theme
// onMount(() => document.body.className = $theme)
</script>
<div class="darkToggle">
<span on:click={() => toggleDarkmode()}>{icon}</span>
</div>
<style lang="scss" module="scoped">
.darkToggle {
height: 25px;
width: 25px;
cursor: pointer;
// background-color: red;
position: fixed;
bottom: 1rem;
right: 2rem;
z-index: 10;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
</style>

View File

@@ -0,0 +1,199 @@
<script lang="ts">
import { onMount, afterUpdate } from 'svelte';
import {
Chart,
LineElement,
LineController,
CategoryScale,
LinearScale,
PointElement,
Title,
Legend
} from 'chart.js';
import type { ChartDataset } from 'chart.js';
import type IChartFrame from '../interfaces/IChartFrame';
Chart.register(
LineElement,
LineController,
CategoryScale,
LinearScale,
PointElement,
Title,
Legend
);
export let name: string;
export let dataFrames: IChartFrame[];
export let beginAtZero: boolean = true;
let chartCanvas: HTMLCanvasElement;
let chart: Chart;
let prevData: any = {};
interface IDataset {
labels: string[];
data?: ChartDataset<'line', number[]>;
}
interface ITemperatureDataset extends IDataset {
inside: ChartDataset<'line', number[]>;
outside?: ChartDataset<'line', number[]>;
}
interface IHumidityDataset extends IDataset {}
interface IPressureDataset extends IDataset {}
function pad(num) {
if (num < 10) {
return `0${num}`;
}
return num;
}
function prettierDateString(date) {
return `${pad(date.getDate())}.${pad(date.getMonth() + 1)}.${pad(date.getYear() - 100)}`;
}
function computeTemperatureDataset(): ITemperatureDataset {
const labels: string[] = dataFrames.map(
(frame) => prettierDateString(new Date(frame.key)) || String(frame.key_as_string)
);
const data: number[] = dataFrames.map((frame) => frame.value);
return {
labels,
inside: {
label: '℃ inside',
borderColor: '#10e783',
backgroundColor: '#c8f9df',
lineTension: 0.5,
borderWidth: 3,
data
}
};
}
function computeHumidityDataset(): IHumidityDataset {
const labels: string[] = dataFrames.map(
(frame) => prettierDateString(new Date(frame.key)) || String(frame.key_as_string)
);
const data: number[] = dataFrames.map((frame) => frame.value);
return {
labels,
data: {
label: '% humidity',
borderColor: '#57d2fb',
backgroundColor: '#d4f2fe',
lineTension: 0.5,
borderWidth: 3,
data
}
};
}
function computePressureDataset(): IPressureDataset {
const labels: string[] = dataFrames.map(
(frame) => prettierDateString(new Date(frame.key)) || String(frame.key_as_string)
);
const data: number[] = dataFrames.map((frame) => frame.value);
return {
labels,
data: {
label: 'Bar of pressure',
borderColor: '#ef5878',
backgroundColor: '#fbd7de',
lineTension: 0.5,
borderWidth: 3,
data
}
};
}
function renderChart() {
const context: CanvasRenderingContext2D = chartCanvas.getContext('2d');
let dataset: IDataset | ITemperatureDataset | IHumidityDataset | IPressureDataset;
if (name === 'Temperature') dataset = computeTemperatureDataset();
else if (name === 'Humidity') dataset = computeHumidityDataset();
else if (name === 'Pressure') dataset = computePressureDataset();
chart = new Chart(context, {
type: 'line',
data: {
labels: dataset.labels,
datasets: [dataset?.inside || dataset.data]
},
options: {
elements: {
point: {
radius: 1
}
},
maintainAspectRatio: false,
plugins: {
title: {
display: true,
position: 'left',
text: `${name} over time`,
font: {
size: 20
}
},
legend: {
display: true,
usePointStyle: true,
borderRadius: 10,
labels: {
padding: 12,
boxWidth: 20,
usePointStyle: true
}
},
zoom: {
zoom: {
wheel: {
enabled: true,
speed: 0.001
},
// pinch: {
// enabled: true
// },
mode: 'xy'
}
}
},
scales: {
y: {
beginAtZero: false,
offset: true,
ticks: {
color: 'black'
},
grid: {
color: 'rgba(0,0,0,0.06)'
}
},
x: {
grid: {
color: 'rgba(0,0,0,0.06)'
}
}
}
}
});
chart.update();
}
onMount(() => renderChart());
afterUpdate(() => {
console.log('after update run');
chart.destroy();
renderChart();
});
</script>
<canvas id="{name}" bind:this="{chartCanvas}" width="400" height="400"></canvas>

View File

@@ -0,0 +1,193 @@
<script lang="ts">
import { fly } from 'svelte/transition';
import Navigation from './Navigation.svelte';
import GithubIcon from '../icons/Github.svelte';
let open: boolean = false;
function toggleMenu() {
open = !open;
}
function close() {
open = false;
}
$: headerText = !open ? 'Menu' : 'Close';
</script>
{#if open}
<div class="slideout-menu" transition:fly="{{ x: 550, duration: 300 }}">
<h1>Navigation</h1>
<Navigation on:click={close} />
<ul class="bottom-content">
<li>
<a href="https://github.com/kevinmidboe/brewpi">
<GithubIcon />
<span class="meta">View on Github</span>
</a>
</li>
</ul>
</div>
{/if}
<header>
<div on:click={toggleMenu} class:open aria-label="Open menu" class="menu">
{#if !open}
<span class="page-header-buttons__open">
<span /> <span /> <span />
</span>
{:else}
<span class="page-header-buttons__close">
<span />
<span />
</span>
{/if}
<span class="page-header-text">{headerText}</span>
</div>
</header>
<style lang="scss" module="scoped">
.slideout-menu {
position: fixed;
display: flex;
flex-direction: column;
height: 100vh;
width: 100vw;
max-width: 550px;
right: 0;
top: 0;
z-index: 1;
background-color: #fff3f6;
color: black;
padding: calc(100px + 2rem) 2rem 1rem;
border-top-left-radius: 4rem;
h1 {
padding-bottom: 4rem;
}
@media screen and (max-width: 640px) {
padding: 100px 2rem 2rem;
}
.bottom-content {
margin-top: auto;
li, li a {
display: flex;
align-items: center;
.meta {
margin-left: 1rem;
}
}
li:not(:last-of-type) {
margin-bottom: 1rem;
}
}
}
header {
top: 0;
right: 0;
display: flex;
position: fixed;
z-index: 10;
padding: 0 1rem;
justify-content: flex-end;
align-items: center;
width: 100%;
height: var(--header-height);
background-color: transparent;
pointer-events: none;
.menu {
display: flex;
place-items: center;
background-color: var(--green);
color: #fff3f6;
padding: 14px 20px;
border-radius: 25px;
-webkit-transition: all 0.3s ease;
transition: all 0.3s ease;
cursor: pointer;
pointer-events: auto;
-webkit-user-select: none;
user-select: none;
&.open {
background-color: salmon;
color: black;
}
&:hover {
transform: scale(1.04);
}
}
.page-header-text {
padding-left: 11px;
display: inline-block;
}
.page-header-buttons__open {
position: relative;
display: inline-block;
width: 24px;
height: 24px;
span {
display: block;
width: 22px;
height: 2px;
background: currentColor;
position: absolute;
left: 1px;
&:first-child {
top: 4px;
}
&:nth-child(2) {
top: 11px;
}
&:nth-child(3) {
top: 18px;
}
}
}
.page-header-buttons__close {
position: relative;
display: grid;
width: 24px;
height: 24px;
place-items: center;
span {
display: block;
width: 22px;
height: 2px;
background: currentColor;
position: absolute;
left: 1px;
&:first-child {
transform: rotate(-45deg);
}
&:nth-child(2) {
transform: rotate(45deg);
}
}
}
}
</style>

View File

@@ -0,0 +1,52 @@
<div class="livestream-container">
<img src="https://i.imgur.com/T4fCMI5.png" alt="livestream" />
<div class="pulse"></div>
</div>
<style lang="scss" module="scoped">
.livestream-container {
position: relative;
border-radius: 1rem;
border: 0.5rem solid var(--background);
transition: border-color var(--color-transition-duration) ease-in-out;
display: inline-block;
height: fit-content;
img {
border-radius: 0.5rem;
width: 100%;
max-width: 860px;
}
.pulse {
position: absolute;
top: 1.2rem;
left: 1.2rem;
border-radius: 50%;
background-color: rgba(255, 82, 82, 1);
box-shadow: 0 0 0 0 rgba(255, 82, 82, 1);
height: 1.7rem;
width: 1.7rem;
transform: scale(1);
animation: pulse-red 2s infinite;
@keyframes pulse-red {
0% {
transform: scale(0.9);
box-shadow: 0 0 0 0 rgba(255, 82, 82, 0.7);
}
70% {
transform: scale(1);
box-shadow: 0 0 0 12px rgba(255, 82, 82, 0);
}
100% {
transform: scale(0.9);
box-shadow: 0 0 0 0 rgba(255, 82, 82, 0);
}
}
}
}
</style>

View File

@@ -0,0 +1,57 @@
<script lang="ts">
import ArrowRight from "../icons/ArrowRight.svelte";
interface IRoute {
name: string
path: string
}
const routes: Array<IRoute> = [{
name: 'Home',
path: '/'
}, {
name: 'Past brews',
path: '/brews'
}, {
name: 'Graphs',
path: '/graphs'
}]
</script>
<ul class="navigation-cards" on:click>
{#each routes as route}
<a href={route.path}>
<li>
<span>{ route.name }</span>
<ArrowRight />
</li>
</a>
{/each}
</ul>
<style lang="scss" module="scoped">
.navigation-cards a {
display: block;
border-radius: 2rem;
background: var(--green);
transition: background-color var(--color-transition-duration) ease-in-out, transform 0.2s ease;
&:hover {
transform: scale(1.04);
}
margin: 1rem 0;
&:first-of-type {
margin: 0;
}
li {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem 1.75rem;
color: white;
font-size: 1.3rem;
}
}
</style>

View File

@@ -0,0 +1,15 @@
<script lang="ts">
import Navigation from './Navigation.svelte'
</script>
<div class="front-menu">
<Navigation />
</div>
<style lang="scss">
.front-menu {
display: flex;
flex-wrap: wrap;
width: 100%;
}
</style>

View File

@@ -0,0 +1,27 @@
<div class="page-header">
<h1>Schleppe Brew</h1>
<span class="subtitle">Monitor beer brewing refridgerator</span>
</div>
<style lang="scss" module="scoped">
@import '../../styles/media-queries.scss';
.page-header {
width: 100%;
text-align: center;
font-family: 'Overpass';
}
h1 {
font-size: 3rem;
margin-bottom: 0.2rem;
@include tablet {
font-size: 5rem;
}
}
.subtitle {
font-size: 2rem;
}
</style>

View File

@@ -0,0 +1,90 @@
<script lang="ts">
// import { toggleRelay } from '$lib/server/relayToggle'
import Switch from './Switch.svelte';
export let relays = [];
function toggleRelay(location) {
const url = `/api/relay/${location}`;
const options = {
method: 'POST'
};
fetch(url, options)
.then((resp) => resp.json())
.then(console.log);
}
function handleChange(event) {
let isChecked = event.detail.checked;
// Perform any desired actions based on the new value
console.log('New value:', isChecked);
}
</script>
<div class="relays-container">
<h1>Manual relay controls</h1>
<div class="vertical-sensor-display">
{#each relays as relay}
<div>
<h2>{relay.location} relay</h2>
<div class="sensor-reading">
<Switch checked="{relay.state}" on:change="{() => toggleRelay(relay.location)}" />
</div>
</div>
{/each}
</div>
</div>
<style lang="scss" module="scoped">
@import '../../styles/media-queries.scss';
.relays-container {
height: fit-content;
border-radius: 12px;
background-color: var(--background);
transition: background-color var(--color-transition-duration) ease-in-out;
padding: 2.25rem 1rem;
@include tablet {
padding: 2.25rem 3rem;
}
h1 {
margin-top: 0;
}
}
.vertical-sensor-display {
position: relative;
height: fit-content;
display: flex;
justify-content: space-between;
flex-wrap: wrap;
}
h2 {
font-size: 1.4rem;
margin-bottom: 1.5rem;
font-weight: 400;
color: var(--text-color);
text-transform: capitalize;
@include tablet {
font-size: 1.6rem;
}
}
.sensor-reading {
display: flex;
justify-content: center;
margin-bottom: 1.75rem;
font-size: 2.5rem;
line-height: 1;
font-weight: 500;
color: var(--text-color);
}
</style>

View File

@@ -0,0 +1,177 @@
<script lang="ts">
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
export let checked = false;
function handleChange() {
dispatch('change', { checked });
}
</script>
<div class="switch-wrapper">
<div class="switch-button-container">
<input class="switch-checkbox" type="checkbox" bind:checked="{checked}" on:change="{handleChange}" >
<div class="switch-button">
<div class="switch-button-top">
<svg class="switch-icon on" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 44 44">
<path d="M19 3h6v38h-6z"/>
</svg>
<svg class="switch-icon off" viewBox="0 0 44 44" xmlns="http://www.w3.org/2000/svg">
<path d="M44 22C44 34.1503 34.1503 44 22 44C9.84974 44 0 34.1503 0 22C0 9.84974 9.84974 0 22 0C34.1503 0 44 9.84974 44 22ZM5.5 22C5.5 31.1127 12.8873 38.5 22 38.5C31.1127 38.5 38.5 31.1127 38.5 22C38.5 12.8873 31.1127 5.5 22 5.5C12.8873 5.5 5.5 12.8873 5.5 22Z"/>
</svg>
</div>
</div>
</div>
</div>
<style lang="scss">
.switch-wrapper {
border-radius: 3px;
padding: 10px;
width: 100px;
height: 125px;
background-image: linear-gradient(to bottom, #414049, #30282c);
box-shadow:
0 0 1px #050506,
inset 0 0 0 2px #050506,
inset 0 3px 1px #66646c;
}
.switch-button-container {
position: relative;
border-radius: 3px;
padding: 3px 2px;
width: 100%;
height: 100%;
background-color: #000;
box-shadow: 0 0 1px #000;
}
.switch-checkbox {
-webkit-appearance: none;
appearance: none;
position: absolute;
z-index: 1;
top: 0;
left: 0;
width: 100%;
height: 100%;
cursor: pointer;
}
.switch-button {
position: relative;
z-index: 0;
border-radius: 5px 5px 2px 2px / 16px 16px 2px 2px;
width: 100%;
height: 100%;
background-color: #7f070d;
background-image:
linear-gradient(to bottom, rgba(#770505, .6) 40%, rgba(#ff8d93, .6) 60% 75%, rgba(#fff, .9) 90%),
linear-gradient(to bottom, rgba(#710206, .6), rgba(#d12127, .6))
;
background-size: 100% calc(15% + 1px), 100% 0%;
background-position: top, bottom;
background-repeat: no-repeat;
box-shadow:
inset 0 -3px 2px rgba(#000, .4),
inset 0 3px 1px rgba(#000, .8),
inset 1px 0 0 rgba(#691016, .8),
inset -1px 0 0 rgba(#691016, .8),
inset 1px 0 0 rgba(#d6585f, .8),
inset -1px 0 0 rgba(#d6585f, .8);
transition: border-radius .4s, background-size .4s;
overflow: hidden;
.switch-checkbox:checked + & {
border-radius: 2px 2px 5px 5px / 2px 2px 16px 16px;
background-size: 100% 0, 100% 15%;
}
}
.switch-button-top {
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
position: absolute;
top: 15%;
width: 100%;
height: 85%;
background-color: rgba(#770505, .8);
background-image: radial-gradient(rgba(#900006, .8) 1px, transparent 0);
background-size: 3px 3px;
background-position: 50%;
box-shadow:
inset 1px 0 0 rgba(#691016, .8),
inset -1px 0 0 rgba(#691016, .8),
inset 1px 0 0 rgba(#d6585f, .8),
inset -1px 0 0 rgba(#d6585f, .8),
inset 0 -1px 0 rgba(#fff, .2),
inset 0 1px 0 rgba(#fff, .9);
transition: all .4s;
.switch-checkbox:checked + .switch-button > & {
top: 0;
background-color: rgba(#e30320, .2);
}
&::before {
content: '';
position: absolute;
z-index: -1;
top: 50%;
left: 50%;
width: 100%;
height: 100%;
transform: translate(-50%, -50%);
background-image: radial-gradient(closest-side at 50% 50%, #ffaf0f 0% 10%, #fc071e 30%, #4b0100);
opacity: 0;
.switch-checkbox:checked + .switch-button > & {
opacity: 1;
animation: flick .2s infinite;
}
}
&::after {
content: '';
position: absolute;
bottom: 0;
width: 100%;
height: 50%;
background-color: rgba(#fdd8d8, .14);
filter: blur(4px);
transition: transform .4s, background-color .4s;
.switch-checkbox:checked + .switch-button > & {
background-color: rgba(#fdd8d8, .2);
transform: translateY(-100%);
}
}
}
.switch-icon {
width: 22px;
height: 22px;
fill: #aa9094;
transition: fill .4s;
.switch-checkbox:checked + .switch-button > .switch-button-top > & {
fill: #f3d5df;
}
}
@keyframes flick {
0% {
opacity: 1;
}
80% {
opacity: .8;
}
100% {
opacity: 1;
}
}
</style>

View File

@@ -0,0 +1,135 @@
<script lang="ts">
import { onMount } from 'svelte'
import CardButton from "./CardButton.svelte";
import Activity from "../icons/Activity.svelte";
export let inside;
export let outside;
let loadedTime: number = new Date().getTime();
let currentTime: number = new Date().getTime()
function updateTime() {
currentTime = new Date().getTime();
}
function flipCard(): void {
console.log("flip-a-delphia")
}
onMount(() => setInterval(updateTime, 1000));
$: secondsSinceUpdate = Math.floor((currentTime - loadedTime) / 1000)
</script>
<div class="vertical-sensor-display">
<CardButton>
<Activity on:click={flipCard} />
</CardButton>
<h2>Current target temperature</h2>
<div class="sensor-reading">
<div class="red">
<span class="value">16</span>
<span class="unit">°C</span>
</div>
</div>
<h2>Inside temperature</h2>
<div class="sensor-reading">
<div class="blue">
<span class="value">{ inside?.temperature }</span>
<span class="unit">{ inside?.temperature_unit }</span>
</div>
<div>
<span class="value">{ Math.floor(inside?.humidity) }</span>
<span class="unit">{ inside?.humidity_unit || '%' }</span>
</div>
</div>
<h2>Outside temperature</h2>
<div class="sensor-reading">
<div class="blue">
<span class="value">{ outside?.temperature }</span>
<span class="unit">{ outside?.temperature_unit }</span>
</div>
<div>
<span class="value">{ Math.floor(outside?.humidity) }</span>
<span class="unit">{ outside?.humidity_unit }</span>
</div>
</div>
<h2>Pressure</h2>
<div class="sensor-reading">
<span class="value">{ inside?.pressure || 0}</span>
<span class="unit">bar</span>
</div>
<div class="button-timer">
<span>Updated { secondsSinceUpdate === 0 ? 'now' : secondsSinceUpdate + 's ago' }</span>
</div>
</div>
<style lang="scss" module="scoped">
@import '../../styles/media-queries.scss';
.vertical-sensor-display {
position: relative;
padding: 2.25rem 1rem;
border-radius: 12px;
background-color: var(--background);
transition: background-color var(--color-transition-duration) ease-in-out;
@include tablet {
padding: 2.25rem 3rem;
}
}
h2 {
font-size: 1.4rem;
margin-bottom: 1.5rem;
font-weight: 400;
color: var(--text-color);
@include tablet {
font-size: 1.6rem;
}
}
.sensor-reading {
display: flex;
justify-content: space-between;
margin-bottom: 1.75rem;
font-size: 3rem;
line-height: 1;
font-weight: 500;
color: var(--text-color);
@include tablet {
font-size: 4.5rem;
}
.unit {
font-weight: 300;
}
.red {
color: var(--red);
}
.blue {
color: var(--blue);
}
}
.button-timer {
width: 100%;
text-align: right;
color: rgba(0, 0, 0, .5);
}
</style>

View File

@@ -0,0 +1,20 @@
<script lang="ts">
export const title: string = "Temperature"
let temp = 23
</script>
<div>
<header>{ title }</header>
<div class="body">
<p>Inside temperature: {23}</p>
</div>
</div>
<style lang="scss" module="scoped">
.body {
display: flex;
flex-direction: column;
}
</style>

View File

@@ -0,0 +1,270 @@
import type IESTelemetry from './interfaces/IESTelemetry';
import type IChartFrame from './interfaces/IChartFrame';
const TELEMETRY_ENDPOINT = 'REPLACE_WITH_ES_HOST/brewlogger-*/_search';
const ES_APIKEY = '***REMOVED***';
function dateToESString(date: Date) {
return date.toISOString();
}
const buildLatestQuery = (place: string) => {
return {
sort: [{ '@timestamp': { order: 'desc', mode: 'max' } }],
query: {
bool: {
filter: [
{
match_all: {}
},
{
match_phrase: {
'location.keyword': place
}
},
{
match_phrase: {
'message.keyword': 'Sensor readings'
}
}
]
}
},
size: 1
};
};
function buildQuery(field: String, from: Date, to: Date, interval: String) {
const fromDateString: string = dateToESString(from);
const toDateString: string = dateToESString(to);
return {
aggs: {
data: {
date_histogram: {
field: '@timestamp',
fixed_interval: interval,
time_zone: 'Europe/Oslo',
min_doc_count: 1
},
aggs: {
maxValue: {
max: {
field: field
}
}
}
}
},
size: 0,
fields: [
{
field: '@timestamp',
format: 'date_time'
}
],
script_fields: {},
stored_fields: ['*'],
_source: {
excludes: []
},
query: {
bool: {
must: [],
filter: [
{
match_all: {}
},
{
match_phrase: {
'location.keyword': 'inside'
}
},
{
range: {
'@timestamp': {
gte: toDateString,
lte: fromDateString,
format: 'strict_date_optional_time'
}
}
}
],
should: [],
must_not: []
}
}
};
}
function roundInterval(interval) {
switch (true) {
case interval <= 500:
return '100ms';
case interval <= 5e3:
return '1s';
case interval <= 7500:
return '5s';
case interval <= 15e3:
return '10s';
case interval <= 45e3:
return '30s';
case interval <= 18e4:
return '1m';
case interval <= 45e4:
return '5m';
case interval <= 12e5:
return '10m';
case interval <= 27e5:
return '30m';
case interval <= 72e5:
return '1h';
case interval <= 216e5:
return '3h';
case interval <= 864e5:
return '12h';
case interval <= 6048e5:
return '24h';
case interval <= 18144e5:
return '3d';
case interval < 36288e5:
return '30d';
default:
return '1y';
}
}
function calculateInterval(from, to, interval, size) {
if (interval !== 'auto') {
return interval;
}
const dateMathInterval = roundInterval((from - to) / size);
// const dateMathIntervalMs = toMS(dateMathInterval);
// const minMs = toMS(min);
// if (dateMathIntervalMs !== undefined && minMs !== undefined && dateMathIntervalMs < minMs) {
// return min;
// }
return dateMathInterval;
}
function parseTempResponse(data: IESTelemetry): IChartFrame[] {
return data?.aggregations?.data?.buckets.map((bucket) => {
return {
value: bucket?.maxValue?.value,
key_as_string: bucket?.key_as_string,
key: bucket?.key
};
});
}
function parseLatestResponse(data: IESTelemetry) {
return data?.hits?.hits[0]?._source;
}
export function fetchTemperature(
from: Date,
to: Date,
size: number = 50,
fetch: Function
): Promise<IChartFrame[]> {
const fromMS = from.getTime();
const toMS = to.getTime();
const interval = calculateInterval(fromMS, toMS, 'auto', size);
const fieldName = 'temperature';
const esSearchQuery = buildQuery(fieldName, from, to, interval);
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `ApiKey ${ES_APIKEY}`
},
body: JSON.stringify(esSearchQuery)
};
return fetch(TELEMETRY_ENDPOINT, options)
.then((resp) => resp.json())
.then(parseTempResponse);
}
export function fetchHumidity(
from: Date,
to: Date,
size: number = 50,
fetch: Function
): Promise<IChartFrame[]> {
const fromMS = from.getTime();
const toMS = to.getTime();
const interval = calculateInterval(fromMS, toMS, 'auto', size);
const fieldName = 'humidity';
const esSearchQuery = buildQuery(fieldName, from, to, interval);
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `ApiKey ${ES_APIKEY}`
},
body: JSON.stringify(esSearchQuery)
};
return fetch(TELEMETRY_ENDPOINT, options)
.then((resp) => resp.json())
.then(parseTempResponse);
}
export function fetchPressure(
from: Date,
to: Date,
size: number = 50,
fetch: Function
): Promise<IChartFrame[]> {
const fromMS = from.getTime();
const toMS = to.getTime();
const interval = calculateInterval(fromMS, toMS, 'auto', size);
const fieldName = 'pressure';
const esSearchQuery = buildQuery(fieldName, from, to, interval);
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `ApiKey ${ES_APIKEY}`
},
body: JSON.stringify(esSearchQuery)
};
return fetch(TELEMETRY_ENDPOINT, options)
.then((resp) => resp.json())
.then(parseTempResponse);
}
export function getLatestInsideReadings(fetch: Function) {
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `ApiKey ${ES_APIKEY}`
},
body: JSON.stringify(buildLatestQuery('inside'))
};
return fetch(TELEMETRY_ENDPOINT, options)
.then((resp) => resp.json())
.then(parseLatestResponse);
}
export function getLatestOutsideReadings(fetch: Function) {
const options = {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `ApiKey ${ES_APIKEY}`
},
body: JSON.stringify(buildLatestQuery('outside'))
};
return fetch(TELEMETRY_ENDPOINT, options)
.then((resp) => resp.json())
.then(parseLatestResponse);
}

View File

@@ -0,0 +1 @@
<svg on:click xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-activity"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"></polyline></svg>

After

Width:  |  Height:  |  Size: 291 B

View File

@@ -0,0 +1 @@
<svg on:click xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-align-left"><line x1="17" y1="10" x2="3" y2="10"></line><line x1="21" y1="6" x2="3" y2="6"></line><line x1="21" y1="14" x2="3" y2="14"></line><line x1="17" y1="18" x2="3" y2="18"></line></svg>

After

Width:  |  Height:  |  Size: 405 B

View File

@@ -0,0 +1 @@
<svg on:click xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-right"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>

After

Width:  |  Height:  |  Size: 323 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>

After

Width:  |  Height:  |  Size: 814 B

View File

@@ -0,0 +1 @@
<svg on:click xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-trending-up"><polyline points="23 6 13.5 15.5 8.5 10.5 1 18"></polyline><polyline points="17 6 23 6 23 12"></polyline></svg>

After

Width:  |  Height:  |  Size: 338 B

View File

@@ -0,0 +1,5 @@
export default interface IChartFrame {
value: number;
key: number;
key_as_string: string;
}

View File

@@ -0,0 +1,52 @@
type ESRelation = 'eq' | 'lt' | 'gt';
interface IESHit {
_index: string;
_type: string;
_id: string;
_score: number;
_source: IESSource;
}
interface IESSource {
temperature: number;
humidity: number;
location: string;
severity: string;
message: string;
'@timestamp': string;
sessionID: string;
}
export default interface ESTelemetry {
took: number;
timed_out: boolean;
_shards: {
total: number;
successful: number;
skipped: number;
failed: number;
};
hits: {
total: {
value: number;
relation: ESRelation;
};
max_score: null;
hits: Array<IESHit>;
};
aggregations: {
data: {
buckets: [
{
maxValue: {
value: number;
};
key_as_string: string;
key: number;
doc_count: number;
}
];
};
};
}

17
src/lib/themeStore.ts Normal file
View File

@@ -0,0 +1,17 @@
import { writable, get, derived } from 'svelte/store';
import type { Writable } from 'svelte/store';
// import { session } from '$app/stores';
import type { Theme } from './types';
export const theme = derived<Writable<App.Session>, Theme>(session, ($session, set) => {
if ($session.theme) {
set($session.theme);
} else if (browser) {
set(window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
}
});
export const setTheme = (theme: Theme) => {
session.update(($session) => ({ ...$session, theme }));
fetch('/theme', { method: 'PUT', body: theme });
};

4
src/lib/types.ts Normal file
View File

@@ -0,0 +1,4 @@
const themes = ['light', 'dark'] as const;
export type Theme = typeof themes[number];
export const isTheme = (theme: string): theme is Theme => themes.includes(theme as Theme);

29
src/routes/+layout.svelte Normal file
View File

@@ -0,0 +1,29 @@
<script lang="ts">
import HeaderComponent from '$lib/components/Header.svelte';
// import Darkmode from '$lib/components/Darkmode.svelte'
</script>
<HeaderComponent />
<!-- <Darkmode/> -->
<div class="page-content">
<slot></slot>
</div>
<style lang="scss">
@import '../styles/media-queries.scss';
.page-content {
display: flex;
flex-direction: column;
width: 100%;
min-height: 100vh;
margin: 0 auto;
padding: 2.5em;
margin-top: var(--header-height);
@include mobile {
padding: 1em;
}
}
</style>

View File

@@ -0,0 +1,38 @@
import { getLatestInsideReadings, getLatestOutsideReadings } from '$lib/graphQueryGenerator';
import type { PageServerLoad } from './$types';
let DEFAULT_MINUTES = 14400;
const host = 'http://brewpi.schleppe:5000';
const sensorsUrl = `${host}/api/sensors`;
const relaysUrl = `${host}/api/relays`;
async function getSensors() {
return fetch(sensorsUrl)
.then((resp) => resp.json())
.then((response) => {
return response?.sensors;
});
}
async function getRelays() {
return fetch(relaysUrl)
.then((resp) => resp.json())
.then((response) => {
return response?.relays || [];
});
}
export const load: PageServerLoad = async () => {
const [sensors, relays] = await Promise.all([getSensors(), getRelays()]);
console.log('got sensors and relays');
console.log(sensors, relays);
const inside = sensors.find((sensor) => sensor.location === 'inside');
const outside = sensors.find((sensor) => sensor.location === 'outside');
return {
inside: inside || null,
outside: outside || null,
relays
};
};

41
src/routes/+page.svelte Normal file
View File

@@ -0,0 +1,41 @@
<script lang="ts">
import PageHeader from '$lib/components/PageHeader.svelte'
import Display from '$lib/components/display.svelte'
import VerticalSensorDisplay from '$lib/components/VerticalSensorDisplay.svelte'
import Livestream from '$lib/components/Livestream.svelte'
import Navigation from '$lib/components/Navigation.svelte';
import type { PageData } from './$types';
import RelayControls from '../lib/components/RelayControls.svelte';
export let data: PageData
const { inside, outside, relays } = data;
</script>
<PageHeader />
<div class="vertical-grid">
<Navigation />
<VerticalSensorDisplay {inside} {outside} />
<RelayControls {relays} />
<!-- <Livestream /> -->
</div>
<style lang="scss">
@import '../styles/media-queries.scss';
.vertical-grid {
display: grid;
grid-template-columns: 1fr;
column-gap: 2rem;
row-gap: 15px;
margin: 1rem;
@include desktop {
grid-template-columns: 1fr 2fr 3fr;
margin: 2rem;
}
}
</style>

View File

@@ -0,0 +1,14 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
// const BREWPI_URL = ''
const BREWPI_URL = 'http://brewpi.schleppe:5000';
export const POST = (async ({ request }) => {
const { pathname } = new URL(request.url);
const options = { method: 'POST' };
return fetch(BREWPI_URL + pathname, options)
.then((resp) => resp.json())
.then((response) => json(response));
}) satisfies RequestHandler;

View File

@@ -0,0 +1,6 @@
import brews from '../../brews.json'
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async () => {
return { brews };
};

View File

@@ -0,0 +1,38 @@
<script lang="ts">
export let data;
const brews = data?.brews || [];
const path = (date: string) => '/brews/' + String(date);
const dateFormat = { year: 'numeric', month: 'short', day: 'numeric' };
const dateString = (date) => new Date(date * 1000).toLocaleDateString('no-NB', dateFormat);
</script>
<main class="page-content">
<h1>Past brews</h1>
<ul>
{#each brews as brew}
<li><a href="{path(brew.date)}">{ brew.beer.name } av { brew.beer.brewery }</a> - {dateString(brew.date)}</li>
{/each}
</ul>
</main>
<style lang="scss">
main.page-content {
ul {
margin-left: 1.2em;
}
ul li {
list-style-type: disc;
line-height: 1.5;
a {
color: #19A786;
}
}
}
</style>

View File

View File

@@ -0,0 +1,14 @@
import { error } from '@sveltejs/kit';
import brews from '../../../brews.json';
import type { PageLoad } from './$types';
export const load = (({ params }) => {
const { date } = params;
const brew = brews.find((b) => b?.date === date);
if (!brew) {
throw error(404, 'Brew not found');
}
return { brew };
}) satisfies PageLoad;

View File

@@ -0,0 +1,182 @@
<script lang="ts">
let height: number;
// let brew = {
// recipe: 'https://docs.google.com/document/d/1FL7ibXxW1r_zFNLK338pyjfMiCCaTOi2fzuMoInA3dQ',
// bryggselv: 'https://www.bryggselv.no/finest/105932/kinn-kveldsbris-allgrain-ølsett-25-liter',
// untapped: 'https://untappd.com/b/kinn-bryggeri-kveldsbris/695024'
// }
export let data;
let brew = data.brew;
const dateFormat = { weekday: 'long', year: 'numeric', month: 'short', day: 'numeric' };
const dateString = new Date(Number(brew.date * 1000)).toLocaleDateString('no-NB', dateFormat);
const wizards = brew.by.join(', ');
</script>
<section>
<div class="desktop-only image-container" style="height: {height}px">
<img src="/images/{ brew.image }" alt="Tuborg Sommerøl" aria-label="Tuborg Sommerøl" />
</div>
<div class="beer-container" bind:clientHeight="{height}">
<h1>{brew.beer.name}</h1>
<div class="links">
<a href="{brew.recipe}" target="_blank" rel="noreferrer">Recipe</a>
<a href="{brew.order_page}" target="_blank" rel="noreferrer">Bryggselv</a>
<a href="{brew.untapped}" target="_blank" rel="noreferrer">Untapped</a>
</div>
<table>
<tbody>
<tr>
<td><b>Brygget:</b></td>
<td>{ dateString }</td>
</tr>
<tr>
<td><b>Laget av:</b></td>
<td>{ wizards }</td>
</tr>
<tr>
<td><b>Kategori:</b></td>
<td>{ brew.beer.category }</td>
</tr>
<tr>
<td><b>Alkoholprosent:</b></td>
<td>~ { brew.abv }%</td>
</tr>
</tbody>
</table>
<div class="mobile-only image-container">
<img src="/images/{ brew.image }" alt="Tuborg Sommerøl" aria-label="Tuborg Sommerøl" />
</div>
<h3>Historie</h3>
<p>
I 1873 ble Tuborg Bryggeri grunnlagt av Carl Frederik Tietgen på Hellerud i Danmark. I 1970
ble Tuborg Bryggeri en del av Carlsberg.
</p>
<h3>Smak</h3>
<p>
Tuborg Sommerøl er en nordisk pilsner med en svært lys strågul farge. Aromaen er preget av
fruktighet fra gjær, noter av blomster fra humle og lette noter av halm og honning fra malt.
Ølet har en svært lett karakter med en lett maltsødme som er godt balansert mot en lav
bitterhet.
</p>
<h3>Mat</h3>
<p>
Tuborg Sommerøl egner seg godt til lys sommermat som pizza, pastaretter, salat, fisk og
skalldyr.
</p>
<p>Bruk av alkohol kan gi ulike skadevirkninger. Nærmere informasjon finner du her.</p>
</div>
</section>
<style lang="scss">
@import '../../../styles/media-queries.scss';
section {
@import url('https://fonts.googleapis.com/css2?family=Epilogue:wght@200;300;400;500;600;700;800&display=swap');
font-family: 'Epilogue', sans-serif;
position: absolute;
top: 0;
left: 0;
}
.image-container {
display: flex;
justify-content: center;
min-height: 1px;
background-color: #93a4a0;
padding: 3rem 1rem;
@include tablet {
width: 33.33%;
float: left;
padding: 5rem 1rem;
}
@include mobile {
margin: 2rem 0;
}
img {
display: block;
height: 100%;
object-fit: contain;
object-position: top;
width: calc(100% - 3rem);
}
}
.beer-container {
background-color: rgba(215, 224, 223, 0.6);
padding: 2rem 1rem;
@include tablet {
float: left;
width: 66.66%;
padding: 4rem 3rem;
}
h1 {
margin-bottom: 2.4rem;
font-size: 3rem;
}
.links {
margin-bottom: 2rem;
a {
position: relative;
&:not(&:first-of-type) {
margin-left: 1rem;
}
}
a::before {
content: '';
position: absolute;
width: 100%;
height: 1px;
bottom: 1.5px;
background-color: black;
}
}
table {
width: 100%;
max-width: 550px;
list-style: none;
tr:last-child td {
border-bottom: 1px solid #bdc8ca;
}
td {
border-top: 1px solid #bdc8ca;
padding: 1rem 0;
}
}
h3 {
letter-spacing: 0.4px;
}
p {
line-height: 1.2;
font-weight: 300;
}
}
</style>

View File

@@ -0,0 +1,38 @@
import { fetchTemperature, fetchHumidity, fetchPressure } from '$lib/graphQueryGenerator';
import type { PageServerLoad } from './$types';
import type IChartFrame from '$lib/interfaces/IChartFrame';
let DEFAULT_MINUTES = 10080;
export const load: PageServerLoad = async ({ fetch }) => {
const temperatureData: IChartFrame[] = await getTemp(DEFAULT_MINUTES, fetch);
const humidityData: IChartFrame[] = await getHumidity(DEFAULT_MINUTES, fetch);
const pressureData: IChartFrame[] = await getPressure(DEFAULT_MINUTES, fetch);
return {
temperatureData,
humidityData,
pressureData,
DEFAULT_MINUTES
};
};
function getSensor(func: Function, minutes: number, fetch: Function) {
const from: Date = new Date();
const to = new Date(from.getTime() - minutes * 60 * 1000);
const size = 40;
return func(from, to, size, fetch);
}
function getTemp(minutes: number, fetch: Function): IChartFrame[] {
return getSensor(fetchTemperature, minutes, fetch);
}
function getHumidity(minutes: number, fetch: Function): IChartFrame[] {
return getSensor(fetchHumidity, minutes, fetch);
}
function getPressure(minutes: number, fetch: Function): IChartFrame[] {
return getSensor(fetchPressure, minutes, fetch);
}

View File

@@ -0,0 +1,128 @@
<script lang="ts">
import { onMount } from 'svelte';
import { fetchTemperature, fetchHumidity, fetchPressure } from '$lib/graphQueryGenerator';
import Graph from '$lib/components/Graph.svelte';
import type IChartFrame from '$lib/interfaces/IChartFrame';
import type { PageData } from './$types';
export let data: PageData
let temperatureData: IChartFrame[] = data?.temperatureData;
let humidityData: IChartFrame[] = data?.temperatureData;
let pressureData: IChartFrame[] = data?.temperatureData;
let DEFAULT_MINUTES: number = data?.DEFAULT_MINUTES
let minutes: number = DEFAULT_MINUTES;
const buttonMinutes = [{
value: 15,
name: 'Last 15 minutes'
}, {
value: 60,
name: 'Last hour'
}, {
value: 1440,
name: 'Last day'
}, {
value: 10080,
name: 'Last week'
}, {
value: 43200,
name: 'Last month'
}, {
value: 129600,
name: 'Last 3 months'
}, {
value: 259200,
name: 'Last 6 months'
}, {
value: 518400,
name: 'Last year',
}]
function reload(mins: number) {
minutes = mins
const from: Date = new Date();
const to = new Date(from.getTime() - minutes * 60 * 1000);
const size = 40;
fetchTemperature(from, to, size, window.fetch).then((resp) => (temperatureData = resp));
fetchHumidity(from, to, size, window.fetch).then((resp) => (humidityData = resp));
fetchPressure(from, to, size, window.fetch).then((resp) => (pressureData = resp));
}
</script>
<!-- <h1>Server: {emoji.emoji}</h1> -->
<input type="number" bind:value={minutes} on:input={() => reload(minutes)} />
<div class="button-wrapper">
{#each buttonMinutes as button}
<button on:click={() => reload(button.value)} class="{button.value === minutes ? 'selected' : ''}">{ button.name }</button>
{/each}
</div>
<section class="graphs">
{#if temperatureData}
<div class="graphWrapper">
<Graph dataFrames={temperatureData} name="Temperature" />
</div>
{/if}
{#if humidityData}
<div class="graphWrapper">
<Graph dataFrames={humidityData} name="Humidity" beginAtZero={false} />
</div>
{/if}
{#if pressureData}
<div class="graphWrapper">
<Graph dataFrames={pressureData} name="Pressure" beginAtZero={false} />
</div>
{/if}
</section>
<style lang="scss" module="scoped">
@import '../../styles/media-queries.scss';
.graphs {
display: grid;
grid-gap: 1rem;
grid-template-columns: repeat(2, 1fr);
width: 100%;
@include mobile {
grid-template-columns: 1fr;
}
.graphWrapper {
max-width: 100vw;
}
}
.button-wrapper {
display: flex;
width: min-content;
}
button {
display: block;
border-radius: 2rem;
white-space: nowrap;
font-size: 1.1rem;
border: none;
width: content;
padding: 0.5rem 1rem;
margin: 0.3rem;
color: white;
cursor: pointer;
background: var(--green);
transition: background-color var(--color-transition-duration) ease-in-out, transform 0.2s ease;
&:hover {
transform: scale(1.04);
}
&.selected {
background-color: salmon;
color: black;
}
}
</style>

View File

@@ -1,2 +0,0 @@
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://kit.svelte.dev">kit.svelte.dev</a> to read the documentation</p>

View File

@@ -0,0 +1,32 @@
$tablet-width: 1200px;
$mobile-width: 768px;
@mixin tablet {
@media (min-width: #{$mobile-width}) {
@content;
}
}
@mixin mobile {
@media (max-width: #{$mobile-width}) {
@content;
}
}
@mixin desktop {
@media (min-width: #{$tablet-width + 1px}) {
@content;
}
}
.desktop-only {
@include mobile {
display: none !important;
}
}
.mobile-only {
@include tablet {
display: none !important;
}
}

35
src/styles/variables.css Normal file
View File

@@ -0,0 +1,35 @@
:root {
--background: white;
--backdrop: #f5f5f7;
--text-color: black;
--red: #ff97a3;
--blue: #9ad9ff;
--header-height: 200px;
--color-transition-duration: 0.4s;
}
.dark {
--background: pink;
--backdrop: #202124;
--text-color: white;
}
* {
box-sizing: border-box;
}
a {
color: inherit; /* blue colors for links too */
text-decoration: inherit; /* no underline */
}
ul,
li {
margin: 0;
padding: 0;
}
li {
list-style-type: none;
}

View File

@@ -0,0 +1 @@
export const ssr = false;

View File

@@ -0,0 +1,85 @@
<script lang="ts">
import * as d3 from 'd3';
import { onMount } from 'svelte';
import d3_timeseries from './lib';
function createRandomData(n, range, rand) {
if (range == null) range = [0, 100];
if (rand == null) rand = 1 / 20;
var num = range[0] + Math.floor(Math.random() * (range[1] - range[0]));
var num2 = range[0] + Math.floor(Math.random() * (range[1] - range[0]));
var num3 = num;
var d = new Date('2013-01-01');
var data = [];
var rgen = d3.randomNormal(0, (range[1] - range[0]) * rand);
for (var i = 0; i < n; i++) {
data.push({
date: d,
n: num,
n2: num2,
n3: num3,
ci_up: num3 * 1.05,
ci_down: num3 * 0.95
});
d = new Date(d.getTime() + 1000 * 60 * 60 * 24);
num = num + rgen();
num3 = num + rgen() / 3;
num = Math.min(Math.max(num, range[0]), range[1]);
num2 = num2 + rgen();
num2 = Math.min(Math.max(num2, range[0]), range[1]);
}
return data;
}
var data = createRandomData(80, [0, 1000], 0.01);
// [{date:new Date('2013-01-01'),n:120,n3:200},...]
/*
var chart = d3_timeseries()
.addSerie(data, { x: 'date', y: 'n' }, { interpolate: 'monotone', color: '#333' })
.width(820);
*/
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);
const gradient = DOM.uid();
svg.append("g")
.call(xAxis);
svg.append("g")
.call(yAxis);
svg.append("linearGradient")
.attr("id", gradient.id)
.attr("gradientUnits", "userSpaceOnUse")
.attr("x1", 0)
.attr("y1", height - margin.bottom)
.attr("x2", 0)
.attr("y2", margin.top)
.selectAll("stop")
.data(d3.ticks(0, 1, 10))
.join("stop")
.attr("offset", d => d)
.attr("stop-color", color.interpolator());
svg.append("path")
.datum(data)
.attr("fill", "none")
.attr("stroke", gradient)
.attr("stroke-width", 1.5)
.attr("stroke-linejoin", "round")
.attr("stroke-linecap", "round")
.attr("d", line);
const chart = svg.node();
onMount(() => chart('#chart'));
</script>
<h1>Chart goes here</h1>
<div id="chart"></div>
<p>Hopefully above 👆</p>

620
src_route_history/lib.ts Normal file
View File

@@ -0,0 +1,620 @@
import * as d3 from 'd3';
import { transition } from 'd3-transition';
var defaultColors = [
'#a6cee3',
'#ff7f00',
'#b2df8a',
'#1f78b4',
'#fdbf6f',
'#33a02c',
'#cab2d6',
'#6a3d9a',
'#fb9a99',
'#e31a1c',
'#ffff99',
'#b15928'
];
// utils
function functorkey(v) {
return typeof v === 'function'
? v
: function(d) {
return d[v];
};
}
function functorkeyscale(v, scale) {
var f =
typeof v === 'function'
? v
: function(d) {
return d[v];
};
return function(d) {
return scale(f(d));
};
}
function keyNotNull(k) {
return function(d) {
return d.hasOwnProperty(k) && d[k] !== null && !isNaN(d[k]);
};
}
function fk(v) {
return function(d) {
return d[v];
};
}
function main() {
// default
var height = 480;
var width = 600;
var drawerHeight = 80;
var drawerTopMargin = 10;
var margin = { top: 10, bottom: 20, left: 30, right: 10 };
var series = [];
var yscale = d3.scaleLinear();
var xscale = d3.scaleTime();
yscale.label = '';
xscale.label = '';
var brush = d3.brushX();
var svg, container, serieContainer, annotationsContainer, drawerContainer, mousevline;
var fullxscale, tooltipDiv;
yscale.setformat = function(n) {
return n.toLocaleString();
};
xscale.setformat = xscale.tickFormat();
// default tool tip function
var _tipFunction = function(date, series) {
var spans =
'<table style="border:none">' +
series
.filter(function(d) {
console.log('DDD:', d);
return d.item !== undefined && d.item !== null;
})
.map(function(d) {
return (
'<tr><td style="color:' +
d.options.color +
'">' +
d.options.label +
' </td>' +
'<td style="color:#333333;text-align:right">' +
yscale.setformat(d.item[d.aes.y]) +
'</td></tr>'
);
})
.join('') +
'</table>';
return '<h4>' + xscale.setformat(d3.timeDay(date)) + '</h4>' + spans;
};
function createLines(serie) {
// https://github.com/d3/d3-shape/blob/master/README.md#curves
var aes = serie.aes;
if (!serie.options.interpolate) {
serie.options.interpolate = 'linear';
} else {
// translate curvenames
serie.options.interpolate =
serie.options.interpolate == 'monotone'
? 'monotoneX'
: serie.options.interpolate == 'step-after'
? 'stepAfter'
: serie.options.interpolate == 'step-before'
? 'stepBefore'
: serie.options.interpolate;
}
// to uppercase for d3 curve name
var curveName =
'curve' + serie.options.interpolate[0].toUpperCase() + serie.options.interpolate.slice(1);
serie.interpolationFunction = d3[curveName] || d3.curveLinear;
var line = d3
.line()
.x(functorkeyscale(aes.x, xscale))
.y(functorkeyscale(aes.y, yscale))
.curve(serie.interpolationFunction)
.defined(keyNotNull(aes.y));
serie.line = line;
serie.options.label =
serie.options.label || serie.options.name || serie.aes.label || serie.aes.y;
if (aes.ci_up && aes.ci_down) {
var ciArea = d3
.area()
.x(functorkeyscale(aes.x, xscale))
.y0(functorkeyscale(aes.ci_down, yscale))
.y1(functorkeyscale(aes.ci_up, yscale))
.curve(serie.interpolationFunction);
serie.ciArea = ciArea;
}
if (aes.diff) {
serie.diffAreas = [
d3
.area()
.x(functorkeyscale(aes.x, xscale))
.y0(functorkeyscale(aes.y, yscale))
.y1(function(d) {
if (d[aes.y] > d[aes.diff]) return yscale(d[aes.diff]);
return yscale(d[aes.y]);
})
.curve(serie.interpolationFunction),
d3
.area()
.x(functorkeyscale(aes.x, xscale))
.y1(functorkeyscale(aes.y, yscale))
.y0(function(d) {
if (d[aes.y] < d[aes.diff]) return yscale(d[aes.diff]);
return yscale(d[aes.y]);
})
.curve(serie.interpolationFunction)
];
}
serie.find = function(date) {
var bisect = d3.bisector(fk(aes.x)).left;
var i = bisect(serie.data, date) - 1;
if (i == -1) {
return null;
}
// look to far after serie is defined
if (
i == serie.data.length - 1 &&
serie.data.length > 1 &&
Number(date) - Number(serie.data[i][aes.x]) >
Number(serie.data[i][aes.x]) - Number(serie.data[i - 1][aes.x])
) {
return null;
}
return serie.data[i];
};
}
function drawSerie(serie) {
if (!serie.linepath) {
console.log(series);
const sorted = [...series[0].data];
sorted.sort((a, b) => (a.n > b.n ? 1 : -1));
const min = sorted[0].n;
const max = sorted[sorted.length - 1].n;
console.log('max:', max);
console.log('min:', min);
const midean = (max + min) / 2;
console.log('midean:', midean);
var linepath = serieContainer
.append('path')
.datum(serie.data.filter((e) => e.n <= midean))
.attr('class', 'd3_timeseries line')
.attr('d', serie.line)
// .attr('stroke', serie.options.color)
.attr('stroke', 'red')
.attr('stroke-linecap', 'round')
.attr('stroke-width', serie.options.width || 1.5)
.attr('fill', 'none');
if (serie.options.dashed) {
if (serie.options.dashed == true || serie.options.dashed == 'dashed') {
serie['stroke-dasharray'] = '5,5';
} else if (serie.options.dashed == 'long') {
serie['stroke-dasharray'] = '10,10';
} else if (serie.options.dashed == 'dot') {
serie['stroke-dasharray'] = '2,4';
} else {
serie['stroke-dasharray'] = serie.options.dashed;
}
linepath.attr('stroke-dasharray', serie['stroke-dasharray']);
}
serie.linepath = linepath;
// serie.hotLine = hotLine;
if (serie.ciArea) {
serie.cipath = serieContainer
.insert('path', ':first-child')
.datum(serie.data)
.attr('class', 'd3_timeseries ci-area')
.attr('d', serie.ciArea)
.attr('stroke', 'none')
.attr('fill', serie.options.color)
.attr('opacity', serie.options.ci_opacity || 0.3);
}
if (serie.diffAreas) {
serie.diffpaths = serie.diffAreas.map(function(area, i) {
var c = (serie.options.diff_colors ? serie.options.diff_colors : ['green', 'red'])[i];
return serieContainer
.insert('path', function() {
return linepath.node();
})
.datum(serie.data)
.attr('class', 'd3_timeseries diff-area')
.attr('d', area)
.attr('stroke', 'none')
.attr('fill', c)
.attr('opacity', serie.options.diff_opacity || 0.5);
});
}
} else {
serie.linepath.attr('d', serie.line);
serie.linepath.attr('d', serie.hotLine);
if (serie.ciArea) {
serie.cipath.attr('d', serie.ciArea);
}
if (serie.diffAreas) {
serie.diffpaths[0].attr('d', serie.diffAreas[0]);
serie.diffpaths[1].attr('d', serie.diffAreas[1]);
}
}
}
function updatefocusRing(xdate) {
var s = annotationsContainer.selectAll('circle.d3_timeseries.focusring');
if (xdate == null) {
s = s.data([]);
} else {
s = s.data(
series
.map(function(s) {
return { x: xdate, item: s.find(xdate), aes: s.aes, color: s.options.color };
})
.filter(function(d) {
return (
d.item !== undefined &&
d.item !== null &&
d.item[d.aes.y] !== null &&
!isNaN(d.item[d.aes.y])
);
})
);
}
const t = transition().duration(50);
/*
.attr('cx', function(d) {
console.log('aDDD:', d);
return xscale(d.item[d.aes.n]);
})
.attr('cy', function(d) {
return yscale(d.item[d.aes.date]);
});
*/
s.transition(t);
s.enter()
.append('circle')
.attr('class', 'd3_timeseries focusring')
.attr('fill', 'none')
.attr('stroke-width', 2)
.attr('r', 5)
.attr('stroke', fk('color'));
s.exit().remove();
}
function updateTip(xdate) {
if (xdate == null) {
tooltipDiv.style('opacity', 0);
} else {
var s = series.map(function(s) {
return { item: s.find(xdate), aes: s.aes, options: s.options };
});
tooltipDiv
.style('opacity', 0.9)
.style('left', margin.left + 5 + xscale(xdate) + 'px')
.style('top', '0px')
.html(_tipFunction(xdate, s));
}
}
function drawMiniDrawer() {
var smallyscale = yscale.copy().range([drawerHeight - drawerTopMargin, 0]);
var serie = series[0];
var line = d3
.line()
.x(functorkeyscale(serie.aes.x, fullxscale))
.y(functorkeyscale(serie.aes.y, smallyscale))
.curve(serie.interpolationFunction)
.defined(keyNotNull(serie.aes.y));
var linepath = drawerContainer
.insert('path', ':first-child')
.datum(serie.data)
.attr('class', 'd3_timeseries.line')
.attr('transform', 'translate(0,' + drawerTopMargin + ')')
.attr('d', line)
.attr('stroke', serie.options.color)
.attr('stroke-width', serie.options.width || 1.5)
.attr('fill', 'none');
if (serie.hasOwnProperty('stroke-dasharray')) {
linepath.attr('stroke-dasharray', serie['stroke-dasharray']);
}
}
function mouseMove() {
var x = d3.pointer(container.node())[0];
x = xscale.invert(x);
mousevline.datum({ x: x, visible: true });
mousevline.update();
updatefocusRing(x);
updateTip(x);
}
function mouseOut() {
mousevline.datum({ x: null, visible: false });
mousevline.update();
updatefocusRing(null);
updateTip(null);
}
var chart = function(elem) {
// compute mins max on all series
series = series.map(function(s) {
var extent = d3.extent(s.data.map(functorkey(s.aes.y)));
s.min = extent[0];
s.max = extent[1];
extent = d3.extent(s.data.map(functorkey(s.aes.x)));
s.dateMin = extent[0];
s.dateMax = extent[1];
return s;
});
// set scales
yscale
.range([height - margin.top - margin.bottom - drawerHeight - drawerTopMargin, 0])
.domain([d3.min(series.map(fk('min'))), d3.max(series.map(fk('max')))])
.nice();
xscale
.range([0, width - margin.left - margin.right])
.domain([d3.min(series.map(fk('dateMin'))), d3.max(series.map(fk('dateMax')))])
.nice();
// if user specify domain
if (yscale.fixedomain) {
// for showing 0 :
// chart.addSerie(...)
// .yscale.domain([0])
if (yscale.fixedomain.length == 1) {
yscale.fixedomain.push(yscale.domain()[1]);
}
yscale.domain(yscale.fixedomain);
}
if (xscale.fixedomain) {
xscale.domain(yscale.fixedomain);
}
fullxscale = xscale.copy();
// create svg
svg = d3.select(elem).append('svg').attr('width', width).attr('height', height);
// clipping for scrolling in focus area
svg
.append('defs')
.append('clipPath')
.attr('id', 'clip')
.append('rect')
.attr('width', width - margin.left - margin.right)
.attr('height', height - margin.bottom - drawerHeight - drawerTopMargin)
.attr('y', -margin.top);
// container for focus area
container = svg
.insert('g', 'rect.mouse-catch')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.attr('clip-path', 'url(#clip)');
serieContainer = container.append('g');
annotationsContainer = container.append('g');
// mini container at the bottom
drawerContainer = svg
.append('g')
.attr(
'transform',
'translate(' + margin.left + ',' + (height - drawerHeight - margin.bottom) + ')'
);
// vertical line moving with mouse tip
mousevline = svg.append('g').datum({
x: new Date(),
visible: false
});
mousevline
.append('line')
.attr('x1', 0)
.attr('x2', 0)
.attr('y1', yscale.range()[0])
.attr('y2', yscale.range()[1])
.attr('class', 'd3_timeseries mousevline');
// update mouse vline
mousevline.update = function() {
this.attr('transform', function(d) {
return 'translate(' + (margin.left + xscale(d.x)) + ',' + margin.top + ')';
}).style('opacity', function(d) {
return d.visible ? 1 : 0;
});
};
mousevline.update();
var xAxis = d3.axisBottom().scale(xscale).tickFormat(xscale.setformat);
var yAxis = d3.axisLeft().scale(yscale).tickFormat(yscale.setformat);
brush
.extent([
[fullxscale.range()[0], 0],
[fullxscale.range()[1], drawerHeight - drawerTopMargin]
])
.on('brush', () => {
let selection = d3.event.selection;
xscale.domain(selection.map(fullxscale.invert, fullxscale));
series.forEach(drawSerie);
svg.select('.focus.x.axis').call(xAxis);
mousevline.update();
updatefocusRing();
})
.on('end', () => {
let selection = d3.event.selection;
if (selection === null) {
xscale.domain(fullxscale.domain());
series.forEach(drawSerie);
svg.select('.focus.x.axis').call(xAxis);
mousevline.update();
updatefocusRing();
}
});
svg
.append('g')
.attr('class', 'd3_timeseries focus x axis')
.attr(
'transform',
'translate(' +
margin.left +
',' +
(height - margin.bottom - drawerHeight - drawerTopMargin) +
')'
)
.call(xAxis);
drawerContainer
.append('g')
.attr('class', 'd3_timeseries x axis')
.attr('transform', 'translate(0,' + drawerHeight + ')')
.call(xAxis);
drawerContainer
.append('g')
.attr('class', 'd3_timeseries brush')
.call(brush)
.attr('transform', `translate(0, ${drawerTopMargin})`)
.attr('height', drawerHeight - drawerTopMargin);
svg
.append('g')
.attr('class', 'd3_timeseries y axis')
.attr('transform', 'translate(' + margin.left + ',' + margin.top + ')')
.call(yAxis)
.append('text')
.attr('transform', 'rotate(-90)')
.attr('x', -margin.top - d3.mean(yscale.range()))
.attr('dy', '.71em')
.attr('y', -margin.left + 5)
.style('text-anchor', 'middle')
.text(yscale.label);
// catch event for mouse tip
svg
.append('rect')
.attr('width', width)
.attr('class', 'd3_timeseries mouse-catch')
.attr('height', height - drawerHeight)
// .style('fill','green')
.style('opacity', 0)
.on('mousemove', mouseMove)
.on('mouseout', mouseOut);
tooltipDiv = d3
.select(elem)
.style('position', 'relative')
.append('div')
.attr('class', 'd3_timeseries tooltip')
.style('opacity', 0);
series.forEach(createLines);
series.forEach(drawSerie);
drawMiniDrawer();
};
chart.width = function(_) {
if (!arguments.length) return width;
width = _;
return chart;
};
chart.height = function(_) {
if (!arguments.length) return height;
height = _;
return chart;
};
chart.margin = function(_) {
if (!arguments.length) return margin;
margin = _;
return chart;
};
// accessors for margin.left(), margin.right(), margin.top(), margin.bottom()
// UNDEFINED
/*
d3.keys(margin).forEach(function(k) {
chart.margin[k] = function(_) {
if (!arguments.length) return margin[k];
margin[k] = _;
return chart;
};
});
*/
// scales accessors
var scaleGetSet = function(scale) {
return {
tickFormat: function(_) {
if (!arguments.length) return scale.setformat;
scale.setformat = _;
return chart;
},
label: function(_) {
if (!arguments.length) return scale.label;
scale.label = _;
return chart;
},
domain: function(_) {
if (!arguments.length && scale.fixedomain) return scale.fixedomain;
if (!arguments.length) return null;
scale.fixedomain = _;
return chart;
}
};
};
chart.yscale = scaleGetSet(yscale);
chart.xscale = scaleGetSet(xscale);
chart.addSerie = function(data, aes, options) {
if (!data && series.length > 0) data = series[0].data;
if (!options.color) options.color = defaultColors[series.length % defaultColors.length];
series.push({ data: data, aes: aes, options: options });
return chart;
};
return chart;
}
export default main;

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

45
static/global.css Normal file
View File

@@ -0,0 +1,45 @@
body {
font-family: 'Roboto';
background-color: var(--backdrop);
color: var(--text-color);
transition: background-color var(--color-transition-duration) ease-in-out,
border-color var(--color-transition-duration) ease-in-out,
color var(--color-transition-duration) ease-in-out,
font-family var(--color-transition-duration) ease-in-out;
}
/* Nunito regular */
@font-face {
font-family: 'Nunito';
src: url('/fonts/Nunito-Regular.eot?#iefix') format('embedded-opentype'),
url('/fonts/Nunito-Regular.woff') format('woff'),
url('/fonts/Nunito-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
}
/* Overpass regular */
@font-face {
font-family: 'Overpass';
src: url('/fonts/Overpass-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
}
/* Roboto regular */
@font-face {
font-family: 'Roboto';
src: url('/fonts/Roboto-Regular.eot?#iefix') format('embedded-opentype'),
url('/fonts/Roboto-Regular.woff') format('woff'),
url('/fonts/Roboto-Regular.ttf') format('truetype');
font-weight: 400;
font-style: normal;
}
@font-face {
font-family: 'Roboto';
src: url('/fonts/Roboto-Light.ttf') format('truetype');
font-weight: 300;
font-style: normal;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 226 KiB

BIN
static/stream.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 MiB

41
static/variables.css Normal file
View File

@@ -0,0 +1,41 @@
:root {
--background: white;
--backdrop: #f5f5f7;
--text-color: black;
--red: #ff97a3;
--blue: #9ad9ff;
--green: #19a786;
--header-height: 70px;
--color-transition-duration: 0.4s;
}
.dark {
--background: black;
--backdrop: #202124;
--text-color: white;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
}
a {
color: inherit; /* blue colors for links too */
text-decoration: inherit; /* no underline */
}
ul,
li {
margin: 0;
padding: 0;
}
li {
list-style-type: none;
}

View File

@@ -1,17 +1,16 @@
import adapter from '@sveltejs/adapter-auto';
import preprocess from 'svelte-preprocess';
import adapter from '@sveltejs/adapter-node';
/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: preprocess(),
kit: {
adapter: adapter(),
// hydrate the <div id="svelte"> element in src/app.html
target: '#svelte'
csrf: {
checkOrigin: false
}
}
};

View File

@@ -1,31 +1,17 @@
{
// "extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"moduleResolution": "node",
"module": "es2020",
"lib": ["es2020", "DOM"],
"target": "es2020",
/**
svelte-preprocess cannot figure out whether you have a value or a type, so tell TypeScript
to enforce using \`import type\` instead of \`import\` for Types.
*/
"importsNotUsedAsValues": "error",
"isolatedModules": true,
"resolveJsonModule": true,
/**
To have warnings/errors of the Svelte compiler at the correct position,
enable source maps by default.
*/
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"baseUrl": ".",
"allowJs": true,
"checkJs": true,
"paths": {
"$lib": ["src/lib"],
"$lib/*": ["src/lib/*"]
}
},
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"]
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"sourceMap": true,
"strict": true
}
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
//
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
// from the referenced tsconfig.json - TypeScript does not merge them in
}

8
vite.config.ts Normal file
View File

@@ -0,0 +1,8 @@
import { sveltekit } from '@sveltejs/kit/vite';
import type { UserConfig } from 'vite';
const config: UserConfig = {
plugins: [sveltekit()]
};
export default config;

1722
yarn-error.log Normal file

File diff suppressed because it is too large Load Diff

2002
yarn.lock

File diff suppressed because it is too large Load Diff