New and improved in sveltekit
| @@ -1,6 +1,10 @@ | |||||||
| { | { | ||||||
| 	"useTabs": true, | 	"useTabs": false, | ||||||
| 	"singleQuote": true, | 	"singleQuote": true, | ||||||
| 	"trailingComma": "none", | 	"trailingComma": "none", | ||||||
| 	"printWidth": 100 | 	"printWidth": 100, | ||||||
|  | 	"plugins": ["prettier-plugin-svelte"], | ||||||
|  | 	"pluginSearchDirs": ["."], | ||||||
|  | 	"overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }], | ||||||
|  | 	"svelteStrictMode": true | ||||||
| } | } | ||||||
							
								
								
									
										
											BIN
										
									
								
								elasticQueries/-X
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										57
									
								
								elasticQueries/temp_15min_30s.json
									
									
									
									
									
										Normal 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": [] | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										58
									
								
								package.json
									
									
									
									
									
								
							
							
						
						| @@ -1,31 +1,41 @@ | |||||||
| { | { | ||||||
|   "name": "brewpi", |   "name": "schleppe-brew", | ||||||
|   "version": "0.0.1", |   "version": "0.0.1", | ||||||
|   "scripts": { |   "scripts": { | ||||||
|     "dev": "svelte-kit dev", |     "dev": "vite dev", | ||||||
|     "build": "svelte-kit build", |     "build": "vite build", | ||||||
|     "package": "svelte-kit package", |     "preview": "vite preview", | ||||||
|     "preview": "svelte-kit preview", |     "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", | ||||||
|     "check": "svelte-check --tsconfig ./tsconfig.json", |     "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", | ||||||
|     "check:watch": "svelte-check --tsconfig ./tsconfig.json --watch", |     "lint": "prettier --plugin-search-dir . --check src && eslint src", | ||||||
|     "lint": "prettier --ignore-path .gitignore --check --plugin-search-dir=. . && eslint --ignore-path .gitignore .", |     "format": "prettier --plugin-search-dir . --write src" | ||||||
|     "format": "prettier --ignore-path .gitignore --write --plugin-search-dir=. ." |  | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@sveltejs/adapter-auto": "next", |     "@sveltejs/adapter-node": "^1.2.4", | ||||||
|     "@sveltejs/kit": "next", |     "@sveltejs/adapter-static": "^1.0.0", | ||||||
|     "@typescript-eslint/eslint-plugin": "^4.31.1", |     "@sveltejs/kit": "^1.0.0", | ||||||
|     "@typescript-eslint/parser": "^4.31.1", |     "@typescript-eslint/eslint-plugin": "^5.46.1", | ||||||
|     "eslint": "^7.32.0", |     "@typescript-eslint/parser": "^5.46.1", | ||||||
|     "eslint-config-prettier": "^8.3.0", |     "eslint": "^8.29.0", | ||||||
|     "eslint-plugin-svelte3": "^3.2.1", |     "eslint-config-prettier": "^8.5.0", | ||||||
|     "prettier": "^2.4.1", |     "eslint-plugin-svelte3": "^4.0.0", | ||||||
|     "prettier-plugin-svelte": "^2.4.0", |     "prettier": "^2.8.1", | ||||||
|     "svelte": "^3.44.0", |     "prettier-plugin-svelte": "^2.9.0", | ||||||
|     "svelte-check": "^2.2.6", |     "sass": "^1.56.2", | ||||||
|     "svelte-preprocess": "^4.9.4", |     "svelte": "^3.55.0", | ||||||
|     "tslib": "^2.3.1", |     "svelte-check": "^2.10.2", | ||||||
|     "typescript": "^4.4.3" |     "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" | ||||||
|  |   } | ||||||
| } | } | ||||||
							
								
								
									
										14
									
								
								src/app.html
									
									
									
									
									
								
							
							
						
						| @@ -3,11 +3,17 @@ | |||||||
| 	<head> | 	<head> | ||||||
| 		<meta charset="utf-8" /> | 		<meta charset="utf-8" /> | ||||||
| 		<meta name="description" content="" /> | 		<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" /> | 		<meta name="viewport" content="width=device-width, initial-scale=1" /> | ||||||
| 		%svelte.head% |  | ||||||
|  | 		%sveltekit.head% | ||||||
| 	</head> | 	</head> | ||||||
| 	<body> |  | ||||||
| 		<div id="svelte">%svelte.body%</div> | 	<body data-sveltekit-prefetch> | ||||||
|  | 		<div>%sveltekit.body%</div> | ||||||
| 	</body> | 	</body> | ||||||
| </html> | </html> | ||||||
|  |  | ||||||
|  | <style lang="scss"></style> | ||||||
|   | |||||||
							
								
								
									
										91
									
								
								src/brews.json
									
									
									
									
									
										Normal 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
									
									
								
							
							
						
						| @@ -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; | ||||||
|  | } | ||||||
|   | |||||||
							
								
								
									
										25
									
								
								src/lib/components/CardButton.svelte
									
									
									
									
									
										Normal 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> | ||||||
							
								
								
									
										46
									
								
								src/lib/components/Darkmode.svelte
									
									
									
									
									
										Normal 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> | ||||||
							
								
								
									
										199
									
								
								src/lib/components/Graph.svelte
									
									
									
									
									
										Normal 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> | ||||||
							
								
								
									
										193
									
								
								src/lib/components/Header.svelte
									
									
									
									
									
										Normal 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> | ||||||
							
								
								
									
										52
									
								
								src/lib/components/Livestream.svelte
									
									
									
									
									
										Normal 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> | ||||||
							
								
								
									
										57
									
								
								src/lib/components/Navigation.svelte
									
									
									
									
									
										Normal 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> | ||||||
							
								
								
									
										15
									
								
								src/lib/components/NavigationFrontMenu.svelte
									
									
									
									
									
										Normal 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> | ||||||
							
								
								
									
										27
									
								
								src/lib/components/PageHeader.svelte
									
									
									
									
									
										Normal 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> | ||||||
							
								
								
									
										90
									
								
								src/lib/components/RelayControls.svelte
									
									
									
									
									
										Normal 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> | ||||||
							
								
								
									
										177
									
								
								src/lib/components/Switch.svelte
									
									
									
									
									
										Normal 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> | ||||||
							
								
								
									
										135
									
								
								src/lib/components/VerticalSensorDisplay.svelte
									
									
									
									
									
										Normal 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> | ||||||
							
								
								
									
										20
									
								
								src/lib/components/display.svelte
									
									
									
									
									
										Normal 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> | ||||||
							
								
								
									
										270
									
								
								src/lib/graphQueryGenerator.ts
									
									
									
									
									
										Normal 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); | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								src/lib/icons/Activity.svelte
									
									
									
									
									
										Normal 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 | 
							
								
								
									
										1
									
								
								src/lib/icons/AlignLeft.svelte
									
									
									
									
									
										Normal 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 | 
							
								
								
									
										1
									
								
								src/lib/icons/ArrowRight.svelte
									
									
									
									
									
										Normal 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 | 
							
								
								
									
										1
									
								
								src/lib/icons/Github.svelte
									
									
									
									
									
										Normal 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 | 
							
								
								
									
										1
									
								
								src/lib/icons/TrendingUp.svelte
									
									
									
									
									
										Normal 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 | 
							
								
								
									
										5
									
								
								src/lib/interfaces/IChartFrame.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,5 @@ | |||||||
|  | export default interface IChartFrame { | ||||||
|  |   value: number; | ||||||
|  |   key: number; | ||||||
|  |   key_as_string: string; | ||||||
|  | } | ||||||
							
								
								
									
										52
									
								
								src/lib/interfaces/IESTelemetry.ts
									
									
									
									
									
										Normal 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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -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
									
								
							
							
						
						| @@ -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> | ||||||
							
								
								
									
										38
									
								
								src/routes/+page.server.ts
									
									
									
									
									
										Normal 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
									
								
							
							
						
						| @@ -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> | ||||||
							
								
								
									
										14
									
								
								src/routes/api/relay/[location]/+server.ts
									
									
									
									
									
										Normal 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; | ||||||
							
								
								
									
										6
									
								
								src/routes/brews/+page.server.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1,6 @@ | |||||||
|  | import brews from '../../brews.json' | ||||||
|  | import type { PageServerLoad } from './$types'; | ||||||
|  |  | ||||||
|  | export const load: PageServerLoad = async () => { | ||||||
|  |   return { brews }; | ||||||
|  | }; | ||||||
							
								
								
									
										38
									
								
								src/routes/brews/+page.svelte
									
									
									
									
									
										Normal 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> | ||||||
							
								
								
									
										0
									
								
								src/routes/brews/[date]/+error.svelte
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										14
									
								
								src/routes/brews/[date]/+page.server.ts
									
									
									
									
									
										Normal 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; | ||||||
							
								
								
									
										182
									
								
								src/routes/brews/[date]/+page.svelte
									
									
									
									
									
										Normal 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> | ||||||
							
								
								
									
										38
									
								
								src/routes/graphs/+page.server.ts
									
									
									
									
									
										Normal 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); | ||||||
|  | } | ||||||
							
								
								
									
										128
									
								
								src/routes/graphs/+page.svelte
									
									
									
									
									
										Normal 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> | ||||||
| @@ -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> |  | ||||||
							
								
								
									
										32
									
								
								src/styles/media-queries.scss
									
									
									
									
									
										Normal 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
									
								
							
							
						
						| @@ -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; | ||||||
|  | } | ||||||
							
								
								
									
										1
									
								
								src_route_history/+layout.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						| @@ -0,0 +1 @@ | |||||||
|  | export const ssr = false; | ||||||
							
								
								
									
										85
									
								
								src_route_history/+page.svelte
									
									
									
									
									
										Normal 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
									
								
							
							
						
						| @@ -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; | ||||||
							
								
								
									
										
											BIN
										
									
								
								static/fonts/Nunito-Bold.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								static/fonts/Nunito-Bold.woff
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								static/fonts/Nunito-Italic.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								static/fonts/Nunito-Italic.woff
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								static/fonts/Nunito-Medium.woff
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								static/fonts/Nunito-Regular.eot
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								static/fonts/Nunito-Regular.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								static/fonts/Overpass-Regular.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								static/fonts/Roboto-Bold.eot
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								static/fonts/Roboto-Bold.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								static/fonts/Roboto-Bold.woff
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								static/fonts/Roboto-Italic.eot
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								static/fonts/Roboto-Italic.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								static/fonts/Roboto-Italic.woff
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								static/fonts/Roboto-Light.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								static/fonts/Roboto-Regular.eot
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								static/fonts/Roboto-Regular.ttf
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										
											BIN
										
									
								
								static/fonts/Roboto-Regular.woff
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										45
									
								
								static/global.css
									
									
									
									
									
										Normal 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; | ||||||
|  | } | ||||||
							
								
								
									
										
											BIN
										
									
								
								static/images/finest_fuck-yeah-IPA.jpg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 94 KiB | 
							
								
								
									
										
											BIN
										
									
								
								static/images/finest_lazy-days.jpeg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 53 KiB | 
							
								
								
									
										
											BIN
										
									
								
								static/images/finest_love-in-a-canoe.jpeg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 244 KiB | 
							
								
								
									
										
											BIN
										
									
								
								static/images/finest_utepils.jpeg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 273 KiB | 
							
								
								
									
										
											BIN
										
									
								
								static/images/helles_tysk-lager.jpeg
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 282 KiB | 
							
								
								
									
										
											BIN
										
									
								
								static/images/kinn_kveldsbris.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 226 KiB | 
							
								
								
									
										
											BIN
										
									
								
								static/stream.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| After Width: | Height: | Size: 4.5 MiB | 
							
								
								
									
										41
									
								
								static/variables.css
									
									
									
									
									
										Normal 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; | ||||||
|  | } | ||||||
| @@ -1,17 +1,16 @@ | |||||||
| import adapter from '@sveltejs/adapter-auto'; |  | ||||||
| import preprocess from 'svelte-preprocess'; | import preprocess from 'svelte-preprocess'; | ||||||
|  | import adapter from '@sveltejs/adapter-node'; | ||||||
|  |  | ||||||
| /** @type {import('@sveltejs/kit').Config} */ | /** @type {import('@sveltejs/kit').Config} */ | ||||||
| const config = { | const config = { | ||||||
| 	// Consult https://github.com/sveltejs/svelte-preprocess | 	// Consult https://github.com/sveltejs/svelte-preprocess | ||||||
| 	// for more information about preprocessors | 	// for more information about preprocessors | ||||||
| 	preprocess: preprocess(), | 	preprocess: preprocess(), | ||||||
|  |  | ||||||
| 	kit: { | 	kit: { | ||||||
| 		adapter: adapter(), | 		adapter: adapter(), | ||||||
|  | 		csrf: { | ||||||
| 		// hydrate the <div id="svelte"> element in src/app.html | 			checkOrigin: false | ||||||
| 		target: '#svelte' | 		} | ||||||
| 	} | 	} | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,31 +1,17 @@ | |||||||
| { | { | ||||||
|  | 	// "extends": "./.svelte-kit/tsconfig.json", | ||||||
| 	"compilerOptions": { | 	"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, | 		"allowJs": true, | ||||||
| 		"checkJs": true, | 		"checkJs": true, | ||||||
| 		"paths": { | 		"esModuleInterop": true, | ||||||
| 			"$lib": ["src/lib"], | 		"forceConsistentCasingInFileNames": true, | ||||||
| 			"$lib/*": ["src/lib/*"] | 		"resolveJsonModule": true, | ||||||
|  | 		"skipLibCheck": true, | ||||||
|  | 		"sourceMap": true, | ||||||
|  | 		"strict": true | ||||||
| 	} | 	} | ||||||
| 	}, | 	// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias | ||||||
| 	"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.ts", "src/**/*.svelte"] | 	// | ||||||
|  | 	// 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
									
								
							
							
						
						| @@ -0,0 +1,8 @@ | |||||||
|  | import { sveltekit } from '@sveltejs/kit/vite'; | ||||||
|  | import type { UserConfig } from 'vite'; | ||||||
|  |  | ||||||
|  | const config: UserConfig = { | ||||||
|  |   plugins: [sveltekit()] | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default config; | ||||||