Compare commits

..

20 Commits

Author SHA1 Message Date
4d77088764 Feat/prerender and state (#8)
* pre-load data behind link elements on hover

* Enable SSR and prerendering of pages in 'auto' mode

* merge relay & regulator state in /api/state

This helps when updating relays and receiving state of relays and regulator state (idle,
heating or cooling) after emitting relay change.
2025-01-12 18:34:16 +01:00
34018285a0 grid layout for larger devices 2024-10-13 17:11:21 +02:00
1c66c841ab Updated drink state to 'completed' 2023-08-27 15:38:46 +02:00
6f80aee823 Handle state being undefined without crashing 2023-08-27 15:38:31 +02:00
699a67161b Merge branch 'main' of github.com:KevinMidboe/brewPi 2023-08-27 13:57:50 +02:00
20f4a173d4 Added Pilsner category to Utepils 2023-08-27 13:57:22 +02:00
c146ffccfd Some margin under logo 2023-08-27 13:56:32 +02:00
c0a46b8cca Height & width 100% to grow to parent 2023-08-27 13:56:13 +02:00
d5e46af091 Fill with white 2023-08-27 13:55:46 +02:00
2da44956be .link animated style on hover & --orange 2023-08-27 13:55:20 +02:00
b34b18ad37 Navigation arrow top left for going to /brews list page 2023-08-27 13:54:16 +02:00
0071bd3791 Every brew takes more spaces & displays details on card 2023-08-27 13:52:46 +02:00
38e42bb37c Resize grid template on ~ tablet sizes
Also set max image width for brew progress to have more predictable
height.
2023-08-27 13:50:53 +02:00
3a8587b2ad Better use of entire viewport size 2023-08-27 13:45:20 +02:00
c1953517e7 Call /api/state after relay is switched 2023-08-27 13:44:03 +02:00
f5f759ca0a DTO interfaces for API responses. 2023-08-27 13:43:04 +02:00
b9a5fde53f Make top right hamburger a button for accessibility 2023-08-27 13:39:45 +02:00
0218c048e9 Expanded brewprogress w/ brew details & image. Better CTA 2023-08-27 13:38:24 +02:00
89b17ea714 Header color and form fit better w/ rest of page colors 2023-08-27 13:30:53 +02:00
c16bce0a1a Gradient background color under graph lines & display NO DATA if none 2023-08-27 13:28:48 +02:00
23 changed files with 588 additions and 163 deletions

View File

@@ -75,7 +75,7 @@
"beer": {
"name": "Utepils",
"brewery": "Finest",
"category": "",
"category": "Pilsner",
"description": ""
},
"date": "1637694000",

View File

@@ -28,13 +28,12 @@
const brew: IBrew = {
name: beer?.name,
by: beer?.brewery,
date: latestBrew.date,
dates: {
brew: dates?.brew || 'June 24',
ferment: 'July 8',
bottle: 'July 8',
consume: 'July 22'
ferment: 'July 12',
bottle: 'July 12',
consume: 'July 26'
}
};
@@ -57,22 +56,22 @@
icon: 'M19.803 5h1.572c.226 0 .443.244.603.404l1.772 1.85c.16.16.25.453.25.68v2.832c0 .015 0 .234-1 .234-.415-.854-1.116-1.287-2.124-1.287-.426 0-1.05.173-1.403.356-.14.072-.473.086-.473-.07V5.803c0-.442.36-.803.803-.803zM9.263 3h7.83c.5 0 .907.406.907.906v6.188c0 .5-.406.906-.906.906h-2.138c-.115 0-.214.206-.26.1-.397-.9-1.297-1.387-2.338-1.387-1.04 0-1.94.418-2.338 1.32-.046.104-.145-.033-.26-.033H8.672c-.37 0-.672-.3-.672-.672V4.265C8 3.57 8.57 3 9.264 3zm11.676 7.978c.828 0 1.5.67 1.5 1.5 0 .828-.672 1.5-1.5 1.5-.83 0-1.5-.672-1.5-1.5 0-.83.67-1.5 1.5-1.5zm-8.582-.07c.828 0 1.5.67 1.5 1.5 0 .828-.672 1.5-1.5 1.5s-1.5-.672-1.5-1.5c0-.83.672-1.5 1.5-1.5z',
name: 'Ferment',
date: brew.dates.ferment,
state: 'in-progress',
state: 'completed',
description: `The fermented stage is where the magic happens. The cooled wort is transferred to a fermentation vessel, typically a large container called a fermenter. Yeast is added to the wort, and the fermentation process begins. Yeast consumes the fermentable sugars in the wort and converts them into alcohol and carbon dioxide through a process called fermentation. This stage can last from a few days to several weeks, depending on the beer style and desired characteristics. The temperature and conditions during fermentation play a crucial role in shaping the beer's flavors and aromas.`
},
{
icon: 'M23.45 10.99c-.162-.307-.54-.42-.84-.257l-4.582 2.45-6.557-11.93H8.62c-.347 0-.62.283-.62.628 0 .346.273.628.62.628h2.118l4.825 8.783c-.037-.006-.074-.006-.112-.006-1.37 0-2.482 1.123-2.482 2.508s1.11 2.508 2.483 2.508c1.038 0 1.92-.64 2.293-1.543l5.445-2.92c.304-.164.422-.54.26-.847zm-8 3.874c-.59 0-1.06-.476-1.06-1.072 0-.596.47-1.072 1.06-1.072.59 0 1.063.476 1.063 1.072 0 .596-.472 1.072-1.062 1.072zm8.994-6.698l-5.848 3.288-2.718-4.93 5.847-3.287 2.72 4.93zm-4.288-5.482l-4.882 2.744-1.48-2.683L18.675 0l1.48 2.684z',
name: 'Bottle',
date: brew.dates.bottle,
state: '',
state: 'completed',
description:
'Once fermentation is complete, the beer is ready for packaging. The beer is carefully transferred from the fermentation vessel to a bottling bucket or keg. During this transfer, care is taken to avoid disturbing the sediment, known as trub, that has settled at the bottom of the fermenter. If desired, additional priming sugar can be added at this stage to provide carbonation in the bottles. The beer is then filled into clean and sanitized bottles or kegs, ensuring that the containers are properly sealed to prevent oxygen exposure and contamination.'
},
{
icon: 'M15.623 5.014l-4.29 3.577c-.196.168-.327.362-.327.62v6.206c0 .322.335.584.656.584h2.004c.32 0 .584-.262.584-.584l-.033-3.115c0-.16.13-.29.29-.29h2.918c.16 0 .292.13.292.29l.033 3.116c0 .322.263.584.584.584h2.09c.322 0 .585-.262.585-.584V9.48c0-.257-.172-.626-.37-.792l-4.263-3.674c-.218-.184-.536-.184-.754 0zm7.17 2.374l-5.967-5.046C16.606 2.122 16.312 2 16 2c-.312 0-.606.123-.79.31L9.207 7.388c-.245.208-.276.576-.068.822.115.136.28.206.446.206.133 0 .266-.044.376-.137l5.69-4.847c.208-.155.49-.157.697-.002 1.286.962 5.693 4.85 5.693 4.85.246.206.614.177.822-.07.208-.246.177-.614-.068-.822z',
name: 'Carbonate',
name: 'Ready to drink',
date: brew.dates.consume,
state: '',
state: 'completed',
description:
'After the beer is bottled or kegged, it enters the carbonation stage. If priming sugar was added during the bottling stage, the remaining yeast in the beer consumes the sugar and produces carbon dioxide, naturally carbonating the beer over time. The sealed bottles or kegs are stored at a controlled temperature for a period of time to allow carbonation to occur. This process typically takes a few weeks, during which the flavors and aromas continue to develop. If kegging, carbonation can also be achieved through forced carbonation, where carbon dioxide is directly injected into the keg under pressure.'
}
@@ -82,11 +81,19 @@
<div class="card">
<h1>Brew progress</h1>
<a href="{`/brews/${brew.date}`}" class="brew">
<h2>{brew.name} <span class="company">av {brew.by}</span></h2>
<ArrowRight />
<p>View progress of the latest brew, click for more info</p>
<a href="{`/brews/${brew.date}`}" class="brew-details">
<img src="/images/{latestBrew.image}" alt="Beer label of {beer.name}" />
<ul>
<li>Name: <span>{beer.name}</span></li>
<li>Brewery: <span>{beer.brewery}</span></li>
<li>Brewed by: <span>{latestBrew.by.join(', ')}</span></li>
</ul>
<i class="arrow"><ArrowRight /></i>
</a>
<ol class="os-timeline">
{#each steps as step, index}
<li
@@ -121,6 +128,52 @@
</div>
<style lang="scss">
.brew-details {
position: relative;
display: flex;
border-radius: 1rem;
transition: all 0.3s ease;
&:hover {
}
img {
width: 30%;
max-width: 140px;
margin-right: 1rem;
border-radius: 1rem;
}
ul {
display: flex;
flex-direction: column;
justify-content: center;
}
li {
display: flex;
flex-direction: column;
font-weight: bold;
span {
margin-top: 0.1rem;
margin-bottom: 0.5rem;
font-weight: normal;
font-size: 1.2rem;
}
}
}
.arrow {
position: absolute;
top: calc(50% - 0.75rem);
right: 0;
height: 1.5rem;
width: 1.5rem;
transition: all 0.3s ease;
}
a.brew {
display: flex;
align-items: center;
@@ -140,8 +193,9 @@
}
}
:global(a.brew:hover svg, a.brew svg.animate) {
:global(.brew-details:hover svg, .brew-details svg.animate) {
animation: bounce 2s infinite;
stroke: var(--green);
}
@keyframes bounce {
@@ -152,10 +206,10 @@
100% {
transform: translateX(0);
}
40% {
10% {
transform: translateX(-5px);
}
60% {
30% {
transform: translateX(-3px);
}
}

View File

@@ -9,7 +9,8 @@
PointElement,
Tooltip,
Title,
Legend
Legend,
Filler
} from 'chart.js';
import { getRelativePosition } from 'chart.js/helpers';
@@ -24,7 +25,8 @@
PointElement,
Tooltip,
Title,
Legend
Legend,
Filler
);
export let name: string;
@@ -41,6 +43,8 @@
// Converts Date to format suitable for the current range displayed
function dateLabelsFormatedBasedOnResolution(dataFrames: IChartFrame[]): string[] {
if (dataFrames.length < 2) return ['NO DATA'];
const firstFrame = dataFrames[0];
const lastFrame = dataFrames[dataFrames.length - 1];
const deltaSeconds =
@@ -69,25 +73,49 @@
return dataFrames.map((frame) => scaledDate.format(frame.key));
}
function hexToRgb(hex: string) {
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result
? {
r: parseInt(result[1], 16),
g: parseInt(result[2], 16),
b: parseInt(result[3], 16)
}
: null;
}
function createLineBackgroundGradient(hex: string, context: CanvasRenderingContext2D) {
const gradient = context.createLinearGradient(0, 0, 0, 400);
const c = hexToRgb(hex);
if (c == null) return;
gradient.addColorStop(0.2, `rgb(${c.r}, ${c.g}, ${c.b}, 0.8)`);
gradient.addColorStop(1, `rgb(${c.r}, ${c.g}, ${c.b}, 0.2)`);
return gradient;
}
// set dataset label & colors matching the name sent as prop
function setDataColorAndName(data: ChartDataset) {
function setDataColorAndName(data: ChartDataset, context: CanvasRenderingContext2D) {
if (name === 'Pressure') {
Object.assign(data, {
label: 'Bar of pressure',
borderColor: '#ef5878',
backgroundColor: '#fbd7de'
fill: true,
backgroundColor: createLineBackgroundGradient('#fbd7de', context)
});
} else if (name === 'Humidity') {
Object.assign(data, {
label: '% humidity',
borderColor: '#57d2fb',
backgroundColor: '#d4f2fe'
fill: true,
backgroundColor: createLineBackgroundGradient('#d4f2fe', context)
});
} else if (name === 'Temperature') {
Object.assign(data, {
label: '℃ inside',
borderColor: '#10e783',
backgroundColor: '#c8f9df'
fill: true,
backgroundColor: createLineBackgroundGradient('#c8f9df', context)
});
}
}
@@ -97,13 +125,14 @@
if (!context) return;
// create labels and singular dataset (data)
const labels: string[] = dateLabelsFormatedBasedOnResolution(dataFrames);
const labels = dateLabelsFormatedBasedOnResolution(dataFrames);
const data: ChartDataset = {
data: dataFrames.map((frame) => frame.value),
borderWidth: 3
};
// based on name, add label and color options to dataset
setDataColorAndName(data);
setDataColorAndName(data, context);
// create chart instance, most here is chart options
chart = new Chart(context, {
@@ -152,7 +181,7 @@
speed: 0.001
},
// pinch: {
// enabled: true
// enabled: true
// },
mode: 'xy'
}
@@ -174,7 +203,6 @@
scales: {
y: {
beginAtZero: false,
offset: true,
ticks: {
color: 'black'
},

View File

@@ -3,7 +3,7 @@
import Navigation from './Navigation.svelte';
import GithubIcon from '../icons/Github.svelte';
let open: boolean = false;
let open = false;
function toggleMenu() {
open = !open;
@@ -17,37 +17,35 @@
</script>
{#if open}
<div class="slideout-menu" transition:fly="{{ x: 550, duration: 300 }}">
<h1>Navigation</h1>
<div class="slideout-menu" transition:fly="{{ x: 550, duration: 300 }}">
<h1>Page navigation</h1>
<Navigation on:click="{close}" />
<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>
<ul class="bottom-content">
<li>
<a class="link" 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="{open}" aria-label="Open menu" class="menu">
<button on:click="{toggleMenu}" class:open="{open}" aria-label="Open menu" class="menu">
{#if !open}
<span class="page-header-buttons__open">
<span></span> <span></span> <span></span>
</span>
<span class="page-header-buttons__open"> <span></span> <span></span> <span></span> </span>
{:else}
<span class="page-header-buttons__close">
<span></span>
<span></span>
</span>
<span class="page-header-buttons__close">
<span></span>
<span></span>
</span>
{/if}
<span class="page-header-text">{headerText}</span>
</div>
</button>
</header>
<style lang="scss" module="scoped">
@@ -55,19 +53,23 @@
position: fixed;
display: flex;
flex-direction: column;
height: 100vh;
height: calc(100vh + 4px);
width: 100vw;
max-width: 550px;
right: 0;
top: 0;
top: -2px;
z-index: 10;
border-left: 2px solid white;
border-top: 2px solid white;
background-color: #fff3f6;
background-color: var(--green);
color: black;
padding: calc(100px + 2rem) 2rem 1rem;
border-top-left-radius: 4rem;
color: var(--background);
h1 {
font-size: 3rem;
padding-bottom: 4rem;
}
@@ -119,12 +121,15 @@
transition: all 0.3s ease;
cursor: pointer;
pointer-events: auto;
-webkit-style: none;
border: none;
font-size: 1rem;
-webkit-user-select: none;
user-select: none;
&.open {
background-color: salmon;
background-color: var(--background);
color: black;
}

View File

@@ -192,5 +192,6 @@
.desktop-only {
display: grid;
max-height: 80px;
margin-bottom: 4rem;
}
</style>

View File

@@ -20,10 +20,13 @@ const routes: Array<IRoute> = [{
<ul class="navigation-cards" on:click>
{#each routes as route}
<a href={route.path}>
<a href={route.path} data-sveltekit-preload-data="hover">
<li>
<span>{ route.name }</span>
<i class="arrow">
<ArrowRight />
</i>
</li>
</a>
{/each}
@@ -33,7 +36,8 @@ const routes: Array<IRoute> = [{
.navigation-cards a {
display: block;
border-radius: 2rem;
background: var(--green);
color: var(--text-color);
background: var(--background);
transition: background-color var(--color-transition-duration) ease-in-out, transform 0.2s ease;
&:hover {
@@ -50,8 +54,12 @@ const routes: Array<IRoute> = [{
align-items: center;
justify-content: space-between;
padding: 1rem 1.75rem;
color: white;
font-size: 1.3rem;
i {
width: 1.5rem;
height: 1.5rem;
}
}
}
</style>
</style>

View File

@@ -1,37 +1,39 @@
<script lang="ts">
// import { toggleRelay } from '$lib/server/relayToggle'
import { createEventDispatcher } from 'svelte';
import Switch from './Switch.svelte';
import type { IRelaysDTO } from '../interfaces/IRelaysDTO';
export let relays = [];
export let relays: IRelaysDTO[] = [];
const dispatch = createEventDispatcher();
function toggleRelay(location) {
const url = `/api/relay/${location}`;
async function toggleRelay(controls: string) {
const url = `/api/relay/${controls}`;
const options = {
method: 'POST'
};
fetch(url, options)
await fetch(url, options)
.then((resp) => resp.json())
.then(console.log);
}
.then((response) => {
const changedRelay = relays.findIndex((relay) => relay.pin === response.pin);
relays[changedRelay] = response;
});
function handleChange(event) {
let isChecked = event.detail.checked;
// Perform any desired actions based on the new value
console.log('New value:', isChecked);
dispatch('relaySwitched');
}
</script>
<div class="card">
<h1>Manual relay controls</h1>
<h1>Manual fridge controls</h1>
<div class="vertical-sensor-display">
{#each relays as relay}
<div>
<h2>{relay.location} relay</h2>
<h2>{relay.controls} relay</h2>
<div class="sensor-reading">
<Switch checked="{relay.state}" on:change="{() => toggleRelay(relay.location)}" />
<Switch checked="{relay.state}" on:change="{() => toggleRelay(relay.controls)}" />
</div>
</div>
{/each}

View File

@@ -1,22 +1,31 @@
<script lang="ts">
import { onMount } from 'svelte'
import CardButton from "./CardButton.svelte";
import Activity from "../icons/Activity.svelte";
import { onMount } from 'svelte';
import CardButton from './CardButton.svelte';
import Activity from '../icons/Activity.svelte';
import { ISensorDTO } from '../interfaces/ISensorDTO';
import { IRelaysDTO } from '../interfaces/IRelaysDTO';
import { IStateDTO } from '../interfaces/IStateDTO';
export let inside;
export let outside;
export let inside: ISensorDTO;
export let outside: ISensorDTO;
export let relays: IRelaysDTO[] = [];
export let state: IStateDTO;
let loadedTime: number = new Date().getTime();
let currentTime: number = new Date().getTime();
let autoReload = false;
const currentGoal = 18.5;
function updateTime() {
currentTime = new Date().getTime();
}
function flipCard(): void {
console.log("flip-a-delphia")
console.log('flip-a-delphia');
}
function tempToStateClass(temp: number | undefined) {
if (temp === undefined || !!isNaN(temp)) return 'idle';
return Number(temp) > 14 ? 'heating' : 'cooling';
}
onMount(() => setInterval(updateTime, 1000));
@@ -24,41 +33,47 @@
</script>
<div class="card">
<h1>Fridge sensors</h1>
<CardButton>
<Activity on:click={flipCard} />
<Activity on:click="{flipCard}" />
</CardButton>
<h2>Current target temperature</h2>
<div class="sensor-reading">
<div class="blue">
<span class="value">{currentGoal}</span>
<div class="{state?.state}">
<span class="value">{state?.goal || 0}</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="value"></span>
<span class="unit">{state?.state || 'unknown'}</span>
</div>
</div>
<h2>Inside frigde temperature</h2>
<div class="sensor-reading">
<div class="{tempToStateClass(inside?.temperature)}">
<span class="value">{inside?.temperature || 0}</span>
<span class="unit">{inside?.temperature_unit || '°C'}</span>
</div>
<div>
<span class="value">{Math.floor(inside?.humidity || 0)}</span>
<span class="unit">{inside?.humidity_unit || '%'}</span>
</div>
</div>
<h2>Outside temperature</h2>
<div class="sensor-reading">
<div class="red">
<span class="value">{outside?.temperature}</span>
<span class="unit">{outside?.temperature_unit}</span>
<div class="{tempToStateClass(outside?.temperature)}">
<span class="value">{outside?.temperature || 0}</span>
<span class="unit">{outside?.temperature_unit || '°C'}</span>
</div>
<div>
<span class="value">{Math.floor(outside?.humidity)}</span>
<span class="unit">{outside?.humidity_unit}</span>
<span class="value">{Math.floor(outside?.humidity || 0)}</span>
<span class="unit">{outside?.humidity_unit || '%'}</span>
</div>
</div>
@@ -70,14 +85,16 @@
<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';
.card {
position: relative;
}
h2 {
font-size: 1.4rem;
margin-bottom: 1.5rem;
@@ -92,6 +109,7 @@
.sensor-reading {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
margin-bottom: 1.75rem;
font-size: 2.2rem;
@@ -117,13 +135,24 @@
margin-left: 0.3rem;
}
.red {
.info {
font-size: 1rem;
width: 100%;
margin-bottom: 0;
font-weight: 500;
}
.heating {
color: var(--red);
}
.blue {
.cooling {
color: var(--blue);
}
.idle {
color: var(--green);
}
}
.button-timer {

View File

@@ -1 +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>
<svg on:click xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" 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>

Before

Width:  |  Height:  |  Size: 323 B

After

Width:  |  Height:  |  Size: 328 B

View File

@@ -1 +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>
<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" fill="white"/></svg>

Before

Width:  |  Height:  |  Size: 814 B

After

Width:  |  Height:  |  Size: 828 B

View File

@@ -0,0 +1,17 @@
enum Controls {
heating = 'heating',
cooling = 'cooling',
fan = 'fan',
lights = 'lights'
}
interface IRelaysDTO {
controls: Controls;
pin: number;
state: boolean;
}
export {
IRelaysDTO,
Controls
}

View File

@@ -0,0 +1,19 @@
enum Location {
inside = 'inside',
outside = 'outside'
}
interface ISensorDTO {
location: Location;
temperature?: number
temperature_unit?: string
humidity?: number
humidity_unit?: string
pressure?: number
pressure_unit?: string
}
export {
ISensorDTO,
Location
}

View File

@@ -0,0 +1,15 @@
enum State {
idle = 'idle',
cooling = 'cooling',
heating = 'heating'
}
interface IStateDTO {
goal: number;
state: State
}
export {
IStateDTO,
State
}

View File

@@ -4,24 +4,31 @@
import '../styles/global.css';
</script>
<HeaderComponent />
<!-- <Darkmode/> -->
<div class="app">
<HeaderComponent />
<!-- <Darkmode/> -->
<div class="page-content">
<slot />
<main>
<slot />
</main>
</div>
<style lang="scss">
@import '../styles/media-queries.scss';
.page-content {
.app {
min-height: calc(100vh - var(--header-height));
}
main {
display: flex;
flex-direction: column;
min-height: calc(100vh - var(--header-height));
min-height: 95vh;
margin: var(--header-height) 2.5rem 0;
@include mobile {
margin: var(--header-height) 1rem 0;
min-height: 85vh;
}
}
</style>

16
src/routes/+layout.ts Normal file
View File

@@ -0,0 +1,16 @@
// if you want to generate a static html file
// for your page.
// Documentation: https://kit.svelte.dev/docs/page-options#prerender
export const prerender = 'auto';
// if you want to Generate a SPA
// you have to set ssr to false.
// This is not the case (so set as true or comment the line)
// Documentation: https://kit.svelte.dev/docs/page-options#ssr
export const ssr = true;
// How to manage the trailing slashes in the URLs
// the URL for about page witll be /about with 'ignore' (default)
// the URL for about page witll be /about/ with 'always'
// https://kit.svelte.dev/docs/page-options#trailingslash
export const trailingSlash = 'ignore';

View File

@@ -1,34 +1,58 @@
import { BREWLOGGER_HOST } from '$env/static/private';
import type { PageServerLoad } from './$types';
import type { PageServerLoad, RequestEvent } from './$types';
import type { ISensorDTO } from '../lib/interfaces/ISensorDTO';
import { IRelaysDTO } from '../lib/interfaces/IRelaysDTO';
import { IStateDTO } from '../lib/interfaces/IStateDTO';
const sensorsUrl = `${BREWLOGGER_HOST}/api/sensors`;
const relaysUrl = `${BREWLOGGER_HOST}/api/relays`;
export const prerender = true; // explicitly pre-render
async function getSensors() {
async function fetchSensors(fetch: Fetch): Promise<ISensorDTO[]> {
return fetch(sensorsUrl)
.then((resp) => resp.json())
.then((response) => {
return response?.sensors;
.then((data) => data?.sensors || [])
.catch((error) => {
console.error('failed to fetch sensors.');
console.error(error);
return null;
});
}
async function getRelays() {
return fetch(relaysUrl)
// calls internal sveltekit api endpoint.
// this allows unified response in svelte app, even
// though it requires multiple calls to regulator server
async function fetchState(fetch: Fetch) {
return fetch('/api/state')
.then((resp) => resp.json())
.then((response) => {
return response?.relays || [];
.catch((error) => {
console.error('failed to fetch state');
console.error(error);
return null;
});
}
export const load: PageServerLoad = async () => {
const [sensors, relays] = await Promise.all([getSensors(), getRelays()]);
type Fetch = typeof fetch;
type HandleFetch = {
event: RequestEvent;
request: Request;
fetch: Fetch;
};
const inside = sensors.find((sensor) => sensor.location === 'inside');
const outside = sensors.find((sensor) => sensor.location === 'outside');
export const load: PageServerLoad = async (input: HandleFetch) => {
const [stateResponse, sensorsResponse] = await Promise.all([
fetchState(input.fetch),
fetchSensors(input.fetch)
]);
const sensors: ISensorDTO[] = sensorsResponse || [];
const relays: IRelaysDTO[] = stateResponse?.relays || [];
const regulator: IStateDTO = stateResponse?.regulator;
const inside = sensors.find((sensor: ISensorDTO) => sensor.location === 'inside');
const outside = sensors.find((sensor: ISensorDTO) => sensor.location === 'outside');
return {
inside: inside || null,
outside: outside || null,
relays
state: { regulator, relays }
};
};

View File

@@ -1,13 +1,20 @@
<script lang="ts">
import Logo from '../lib/components/Logo.svelte';
import VerticalSensorDisplay from '../lib/components/VerticalSensorDisplay.svelte';
// import Livestream from '$lib/components/Livestream.svelte'
import BrewProgress from '../lib/components/BrewProgress.svelte';
import type { PageData } from './$types';
import RelayControls from '../lib/components/RelayControls.svelte';
import type { PageData } from './$types';
import type { IStateDTO } from '../lib/interfaces/IStateDTO';
export let data: PageData;
const { inside, outside, relays } = data;
const { inside, outside } = data;
let { relays, regulator } = data.state;
const updateState = () => {
fetch('/api/state')
.then((resp) => resp.json())
.then((response: IStateDTO) => ({ relays, regulator } = response));
}
</script>
<Logo />
@@ -15,11 +22,14 @@
<div class="vertical-grid">
<BrewProgress />
<VerticalSensorDisplay inside="{inside}" outside="{outside}" />
<VerticalSensorDisplay
inside="{inside}"
outside="{outside}"
relays="{relays}"
state="{regulator}"
/>
<RelayControls relays="{relays}" />
<!-- <Livestream /> -->
<RelayControls bind:relays="{relays}" on:relaySwitched="{updateState}" />
</div>
<style lang="scss">
@@ -36,5 +46,15 @@
grid-template-columns: 2fr 2fr 3fr;
margin: 2rem;
}
@media (min-width: 900px) and (max-width: 1550px) {
grid-template-columns: 2fr 2fr;
}
}
:global(.vertical-grid > div:nth-child(3)) {
@media (min-width: 900px) and (max-width: 1550px) {
margin-top: -185px;
}
}
</style>

View File

@@ -0,0 +1,19 @@
import { json, RequestEvent } from '@sveltejs/kit';
import { BREWLOGGER_HOST } from '$env/static/private';
import type { RequestHandler } from './$types';
import { IRelaysDTO } from '../../../lib/interfaces/IRelaysDTO';
async function fetchRegulator(): Promise<Response | IRelaysDTO[]> {
return fetch(BREWLOGGER_HOST + '/api/regulator').then((resp) => resp.json());
}
async function fetchRelays(): Promise<Response | IRelaysDTO[]> {
return fetch(BREWLOGGER_HOST + '/api/relays')
.then((resp) => resp.json())
.then((data) => data?.relays || []);
}
export const GET = (async () => {
const [regulatorState, relaysState] = await Promise.all([fetchRegulator(), fetchRelays()]);
return json({ regulator: regulatorState, relays: relaysState });
}) satisfies RequestHandler;

View File

@@ -1,12 +1,14 @@
<script lang="ts">
import ArrowRight from '../../lib/icons/ArrowRight.svelte';
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: number) => new Date(date * 1000).toLocaleDateString('no-NB', dateFormat);
const dateString = (date: number) =>
new Date(date * 1000).toLocaleDateString('no-NB', dateFormat);
</script>
<main class="card">
@@ -14,26 +16,140 @@
<ul>
{#each brews as brew}
<li><a href="{path(brew.date)}">{ brew.beer.name } av { brew.beer.brewery }</a> - {dateString(brew.date)}</li>
<li class="brew">
<a href="{path(brew.date)}" data-sveltekit-preload-data="hover">
<img src="/images/{brew.image}" alt="Beer label of {brew.beer.name}" />
<div class="details">
<h2>
{brew.beer.name}
<span>{brew.beer.category}</span>
</h2>
<p>By: {brew.by.join(', ')}</p>
<p>Date: {dateString(brew.date)}</p>
<p>Recipe by: {brew.beer.brewery}</p>
</div>
<div class="nav-arrow"><ArrowRight /></div>
</a>
</li>
{/each}
</ul>
</main>
<style lang="scss">
main.card {
height: calc(100vh - var(--header-height) * 2);
@import '../../styles/media-queries.scss';
ul {
margin-left: 1.2em;
main.card {
min-height: calc(100vh - var(--header-height) * 2);
margin-bottom: 2rem;
padding: 0;
h1 {
margin: 1.5rem 0;
font-size: 3rem;
}
ul li {
list-style-type: disc;
line-height: 1.5;
ul {
display: grid;
grid-template-columns: 1fr;
margin: 0;
border-radius: inherit;
a {
font-size: 1.2rem;
color: #19A786;
@include tablet {
grid-template-columns: repeat(2, 1fr);
}
@include desktop {
grid-template-columns: repeat(3, 1fr);
}
}
}
.brew {
list-style-type: none;
line-height: 1.5;
padding: 1.5rem;
transition: all 0.2s ease-in-out;
background-color: white;
z-index: 1;
border-radius: inherit;
&:not(:last-of-type) {
border-bottom: 1px solid #f5f5f7;
}
&:hover {
// transform: scale(1.01);
scale: 1.01;
will-change: transform;
box-shadow: 0px 2px 15px -3px rgba(25, 167, 134, 0.2);
z-index: 2;
}
a {
font-size: 1.2rem;
color: #19a786;
display: grid;
grid-template-columns: 160px 1fr 30px;
h2 {
margin: 0 0 0.75rem;
color: var(--green);
span {
display: block;
font-style: italic;
font-size: 1.2rem;
}
}
img {
width: 100%;
max-width: 160px;
border-radius: 0.5rem;
}
div.details {
margin-left: 2rem;
color: var(--text-color);
p {
margin: 0;
}
}
div.nav-arrow {
display: flex;
align-items: center;
}
}
}
@include mobile {
.brew {
padding-left: 1rem !important;
}
.brew a {
grid-template-columns: 120px 1fr;
img {
margin-top: 0.5rem;
background-color: pink;
}
div.details {
margin-left: 1rem;
h2 {
font-size: 1.5rem;
}
}
div.nav-arrow {
display: none;
}
}
}

View File

@@ -4,8 +4,9 @@ import { fetchHumidity, fetchTemperature } from '../../../lib/server/graphQueryG
import type { PageLoad } from './$types';
async function fetchGraphData(brew) {
const start = new Date(brew.date * 1000 - 86400000);
const end = new Date(brew.date * 1000 + 4838400000);
const { date } = brew;
const start = new Date(date * 1000 - 86400000);
const end = new Date(date * 1000 + 4838400000);
const size = 200;
const [temperature, humidity] = await Promise.all([
@@ -13,10 +14,7 @@ async function fetchGraphData(brew) {
fetchHumidity(start, end, size)
]);
return {
temperature,
humidity
};
return { temperature, humidity };
}
export const load = (async ({ params }) => {

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import Graph from '../../../lib/components/Graph.svelte';
import ArrowRight from '../../../lib/icons/ArrowRight.svelte';
import IChartFrame from '../../../lib/interfaces/IChartFrame';
let height: number;
@@ -19,12 +20,23 @@
const wizards = brew.by.join(', ');
</script>
<a href="/brews" class="nav-back">
<ArrowRight />
</a>
<section class="card">
<div class="desktop-only image-container" style="height: {height}px; background-color: {brew.color_primary || '#93a4a0'}">
<div
class="desktop-only image-container"
style="height: {height}px; background-color: {brew.color_primary || '#93a4a0'}"
>
<img src="/images/{brew.image}" alt="Tuborg Sommerøl" aria-label="Tuborg Sommerøl" />
</div>
<div class="beer-container" bind:clientHeight="{height}" style="background-color: {brew.color_secondary || '#DFE6E5'}">
<div
class="beer-container"
bind:clientHeight="{height}"
style="background-color: {brew.color_secondary || '#DFE6E5'}"
>
<h1>{brew.beer.name}</h1>
<div class="links">
@@ -57,7 +69,10 @@
</tbody>
</table>
<div class="mobile-only image-container" style="background-color: {brew.color_primary || '#93a4a0'}">
<div
class="mobile-only image-container"
style="background-color: {brew.color_primary || '#93a4a0'}"
>
<img src="/images/{brew.image}" alt="Tuborg Sommerøl" aria-label="Tuborg Sommerøl" />
</div>
@@ -66,17 +81,15 @@
<div class="graph-container">
{#if temperatureData && temperatureData?.length}
<div class="graph">
<h3>Temperature during fermentation</h3>
<Graph dataFrames="{temperatureData}" name="Temperature" hideTitle="{true}" />
</div>
{/if}
{#if humidityData && temperatureData?.length}
<div class="graph">
<h3>Humidity during carbonation</h3>
<Graph dataFrames="{humidityData}" name="Humidity" hideTitle="{true}" />
</div>
<div class="graph">
<h3>Temperature during fermentation</h3>
<Graph dataFrames="{temperatureData}" name="Temperature" hideTitle="{true}" />
</div>
{/if} {#if humidityData && temperatureData?.length}
<div class="graph">
<h3>Humidity during carbonation</h3>
<Graph dataFrames="{humidityData}" name="Humidity" hideTitle="{true}" />
</div>
{/if}
</div>
@@ -101,6 +114,24 @@
<style lang="scss">
@import '../../../styles/media-queries.scss';
.nav-back {
position: fixed;
top: 1.5rem;
left: 1rem;
width: 2rem;
height: 2rem;
transform: rotate(180deg);
cursor: pointer;
transition: all 0.3s ease;
&:hover,
&:focus {
scale: 1.3;
will-change: transform;
color: var(--green);
}
}
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;

View File

@@ -6,6 +6,7 @@
--text-color: black;
--red: #ff97a3;
--blue: #9ad9ff;
--orange: orange;
--green: #19a786;
--header-height: 80px;
@@ -39,6 +40,21 @@ a {
text-decoration: inherit; /* no underline */
}
a.link {
--color-text: white;
text-decoration: none;
transition: all 0.3s ease;
-webkit-transition: -webkit-transform 0.15s linear;
transition: transform 0.15s linear;
-webkit-transform-origin: 50% 80%;
transform-origin: 50% 80%;
border-bottom: 2px solid var(--green);
}
a.link:hover {
border-color: white;
transform: skew(-15deg);
}
ul,
li {
margin: 0;

View File

@@ -1,14 +1,14 @@
$tablet-width: 1200px;
$mobile-width: 768px;
@mixin tablet {
@media (min-width: #{$mobile-width}) {
@mixin mobile {
@media (max-width: #{$mobile-width}) {
@content;
}
}
@mixin mobile {
@media (max-width: #{$mobile-width}) {
@mixin tablet {
@media (min-width: #{$mobile-width}) {
@content;
}
}
@@ -29,4 +29,4 @@ $mobile-width: 768px;
@include tablet {
display: none !important;
}
}
}