Defined app routes & layout for footer & modal components

This commit is contained in:
2023-07-29 13:34:17 +02:00
parent aef21b7a68
commit c75d887268
10 changed files with 568 additions and 0 deletions

View File

@@ -0,0 +1,91 @@
<script lang="ts">
import { page } from '$app/stores';
import HiveIcon from '$lib/icons/hive.svelte';
import DashboardIcon from '$lib/icons/DashboardIcon.svelte';
import AlarmIcon from '$lib/icons/AlarmIcon.svelte';
import SettingIcon from '$lib/icons/SettingIcon.svelte';
const navItems = [
{ title: 'Hives', path: '/', component: HiveIcon },
{ title: 'Dashboard', path: '/dashboard', component: DashboardIcon },
{ title: 'Alarms', path: '/alarms', component: AlarmIcon },
{ title: 'Settings', path: '/settings', component: SettingIcon }
];
</script>
<footer>
{#each navItems as nav}
<a
href={nav.path}
class="item"
aria-current={$page.url.pathname == nav.path ? 'page' : undefined}
>
<div class="icon">
<svelte:component this={nav.component} />
</div>
<span class="title">{nav.title}</span>
</a>
{/each}
</footer>
<style lang="scss">
footer {
position: fixed;
bottom: 0;
width: 100%;
background-color: var(--highlight);
display: flex;
justify-content: space-evenly;
a.item {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
padding: 0.6rem 0.5rem 0.2rem;
color: var(--color);
font-size: 0.8rem;
transition: all 200ms ease;
will-change: font-weight;
.icon {
width: 20px;
height: 20px;
fill: var(--color);
transition: inherit;
margin-bottom: 0.2rem;
}
&::after {
transition: inherit;
content: '';
position: absolute;
top: 0;
width: 50%;
height: 3px;
background-color: var(--brand);
opacity: 0;
}
&[aria-current='page'] {
color: var(--brand);
font-weight: bold;
letter-spacing: -0.3px;
.icon {
fill: var(--brand) !important;
}
&::after {
opacity: 1;
}
}
}
}
:global(a.item[aria-current='page'] svg) {
fill: var(--brand) !important;
}
</style>

View File

@@ -0,0 +1,70 @@
<script lang="ts">
import { onDestroy } from 'svelte';
import { browser } from '$app/environment';
import { swipe } from 'svelte-gestures';
import { modal, modalOpen, resetModal } from '$lib/store';
import type { Unsubscriber } from 'svelte/store';
let isOpenSubscription: Unsubscriber;
let modalElement: HTMLElement;
function swipeHandler(event: CustomEvent) {
if (event.detail.direction === 'right') resetModal();
}
function toggleHandler(isOpen: boolean) {
if (!browser || !modalElement) return;
const app = document.getElementById('app');
if (!app) return
if (isOpen) {
app.classList.add('no-scroll');
app.inert = true;
app.setAttribute('aria-hidden', 'true')
} else {
setTimeout(() => (modalElement.scrollTop = 0), 500);
app.classList.remove('no-scroll');
app.inert = false;
app.setAttribute('aria-hidden', 'false')
}
}
isOpenSubscription = modalOpen.subscribe(toggleHandler);
onDestroy(() => isOpenSubscription());
</script>
<svelte:body aria-haspopup="dialog" />
<div
class={`modal ${$modalOpen ? 'open' : ''}`}
use:swipe={{ timeframe: 1000, minSwipeDistance: 1, touchAction: 'pan-right' }}
on:swipe={swipeHandler}
bind:this={modalElement}
role="dialog"
aria-modal="true"
aria-live="assertive"
aria-labelledby="modal-title"
>
{#if $modal?.opens}
<svelte:component this={$modal?.opens} data={$modal?.data} />
{/if}
</div>
<style lang="scss">
.modal {
position: fixed;
top: 0;
left: 100vw;
background-color: var(--background);
height: 100vh;
height: -webkit-fill-available;
width: calc(100vw - 2rem);
transition: left 400ms ease;
padding: 0 1rem;
overflow-y: scroll;
&.open {
left: 0;
}
}
</style>

View File

@@ -0,0 +1,69 @@
<script lang="ts">
import { onMount } from 'svelte';
import { gatewayMessageQueue } from '$lib/store';
import ModalHeader from './ModalHeader.svelte';
import HighTemperature from '../cards/HighTemperature.svelte';
import LowBattery from '../cards/LowBattery.svelte';
import NoData from '../cards/NoData.svelte';
import SegmentedControls from '../SegmentedControls.svelte';
import NetworkIcon from '$lib/icons/network.svelte';
import SyncDisplay from '../displays/SyncDisplay.svelte';
import TemperatureDisplay from '../displays/TemperatureDisplay.svelte';
import DateSeparator from '../DateSeparator.svelte';
import type { IGatewayTelemetry } from '$lib/interfaces/ITelemetry';
export let data: any = {};
let telemetry: IGatewayTelemetry;
const segments = ['general', 'activity', 'alarms'];
let selectedSection: string = segments[0];
function segmentSelected(event: CustomEvent) {
selectedSection = event.detail;
}
onMount(() =>
gatewayMessageQueue.subscribe((msg) => {
if (msg.gateway_name === data.gateway_name) telemetry = msg;
})
);
</script>
<div>
<ModalHeader title={data?.gateway_name} subtitle="ID273827" icon={NetworkIcon} />
<SegmentedControls {segments} on:change={segmentSelected} />
{#if selectedSection === segments[0]}
<section class="display">
<SyncDisplay date="{telemetry?.t || data?.t }" />
<TemperatureDisplay temperature="{telemetry?.temperature}" />
</section>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat
non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
{:else if selectedSection === segments[1]}
<HighTemperature temperature="24" borderLess={true} />
<DateSeparator date={new Date()} />
<!-- <LowTemperature temperature="24" /> -->
<LowBattery battery="9" borderLess={true} />
<DateSeparator date={new Date(1689845512000)} />
<HighTemperature temperature="23" borderLess={true} />
<DateSeparator date={new Date(1682580799000)} />
{:else if selectedSection === segments[2]}
<NoData time="0.3" />
{/if}
</div>
<style lang="scss">
section.display {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 1rem;
}
</style>

View File

@@ -0,0 +1,94 @@
<script lang="ts">
import { onMount } from 'svelte';
import { hiveMessageQueue } from '$lib/store';
import ModalHeader from './ModalHeader.svelte';
import LowTemperature from '../cards/LowTemperature.svelte';
import HighTemperature from '../cards/HighTemperature.svelte';
import NoData from '../cards/NoData.svelte';
import SegmentedControls from '../SegmentedControls.svelte';
import HiveLogo from '$lib/icons/hive.svelte';
import HumidityDisplay from '../displays/HumidityDisplay.svelte';
import TemperatureDisplay from '../displays/TemperatureDisplay.svelte';
import QueenDisplay from '../displays/QueenDisplay.svelte';
import WeightDisplay from '../displays/WeightDisplay.svelte';
import DateSeparator from '../DateSeparator.svelte';
import WeightChanged from '../cards/WeightChanged.svelte';
import BatteryDisplay from '../displays/BatteryDisplay.svelte';
import type { IHiveTelemetry } from '$lib/interfaces/ITelemetry';
export let data: any = {};
let telemetry: IHiveTelemetry;
const segments = ['general', 'activity', 'alarms'];
let selectedSection: string = segments[0];
function segmentSelected(event: CustomEvent) {
selectedSection = event.detail;
}
onMount(() =>
hiveMessageQueue.subscribe((msg) => {
if (msg.hive_name === data.hive_name) telemetry = msg;
})
);
</script>
<div>
<ModalHeader title={data?.hive_name} subtitle="Rosendal" icon={HiveLogo} />
<SegmentedControls {segments} on:change={segmentSelected} />
{#if selectedSection === segments[0]}
<section class="display">
<QueenDisplay />
<WeightDisplay weight={telemetry?.weight} />
<TemperatureDisplay temperature={telemetry?.temperature} />
<HumidityDisplay humidity={telemetry?.humidity} />
<BatteryDisplay battery={telemetry?.battery_level} />
</section>
{:else if selectedSection === segments[1]}
<HighTemperature temperature="32" borderLess={true} />
<DateSeparator date={new Date()} />
<LowTemperature temperature="8" borderLess={true} />
<DateSeparator date={new Date(1689845512000)} />
<WeightChanged from="21" to="22" borderLess={true} />
<DateSeparator date={new Date(1683717412000)} />
<HighTemperature temperature="26" borderLess={true} />
<DateSeparator date={new Date(1682580799000)} />
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat
non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat
non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
<p>
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut
labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in
voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat
non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
</p>
{:else if selectedSection === segments[2]}
<NoData time="0.3" />
<LowTemperature temperature="24" />
{/if}
</div>
<style lang="scss">
section.display {
display: grid;
grid-template-columns: 1fr 1fr;
grid-gap: 1rem;
}
</style>

View File

@@ -0,0 +1,139 @@
<script lang="ts">
import { onMount } from 'svelte';
import { resetModal } from '$lib/store';
import ArrowLeft from '$lib/icons/ArrowLeft.svelte';
export let title: string;
export let subtitle: string;
export let icon: any;
let bodyTitleEl: HTMLElement;
let headerTitleEl: HTMLElement;
function createObserver() {
const observer = new IntersectionObserver(handleIntersect, {
root: null,
rootMargin: '-20px 10000px 0px 0px',
threshold: 1
});
observer.observe(bodyTitleEl);
}
function handleIntersect(entries: IntersectionObserverEntry[]) {
entries.forEach((entry) => toggleTitle(!entry.isIntersecting));
}
function toggleTitle(show = true) {
if (!headerTitleEl || !bodyTitleEl) return;
if (show) {
headerTitleEl.classList.add('show');
bodyTitleEl.classList.add('hide');
} else {
headerTitleEl.classList.remove('show');
bodyTitleEl.classList.remove('hide');
}
}
onMount(() => createObserver());
</script>
<div class="top">
<button class="back" on:click={resetModal} aria-label="Close">
<ArrowLeft fill="var(--color)" />
</button>
<span id="header-title" bind:this={headerTitleEl}>{title}</span>
</div>
<div bind:this={bodyTitleEl} class="title-container">
<div class="hive-icon">
<div class="img">
<svelte:component this={icon} />
</div>
</div>
<div>
<h1 id="modal-title">{title}</h1>
<span>{subtitle}</span>
</div>
</div>
<style lang="scss">
.top {
position: sticky;
top: 0;
display: flex;
background-color: var(--background);
padding: 1rem 0;
z-index: 10;
#header-title {
display: block;
width: 100%;
text-align: center;
align-items: center;
line-height: 1.8;
font-size: 1.2rem;
text-transform: capitalize;
font-weight: bold;
opacity: 0;
transition: visibility 350ms ease, opacity 350ms ease;
visibility: hidden;
}
}
button.back {
height: 35px;
width: 35px;
padding: 0.2rem;
border-radius: unset;
border: unset;
font-size: unset;
font-weight: unset;
font-family: unset;
background-color: unset;
transition: unset;
}
.title-container {
display: flex;
transition: opacity 350ms ease;
opacity: 100%;
margin-bottom: 1.75rem;
align-items: center;
.hive-icon {
background-color: var(--highlight);
display: grid;
place-items: center;
min-height: 65px;
min-width: 65px;
max-height: 65px;
max-width: 65px;
border-radius: 1rem;
margin-right: 1rem;
.img {
height: 40px;
width: 40px;
}
}
h1 {
font-size: 1.66rem;
text-transform: capitalize;
margin: 0;
margin-bottom: 0.2rem;
}
}
:global(#header-title.show) {
visibility: visible !important;
opacity: 100% !important;
}
:global(.title-container.hide) {
opacity: 0 !important;
}
</style>

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

@@ -0,0 +1,17 @@
<script lang="ts">
import { onMount } from 'svelte';
import { PUBLIC_MQTT_BROKER_WS_URL } from '$env/static/public';
import FooterNavigation from '$lib/components/FooterNavigation.svelte';
import Modal from '$lib/components/Modal.svelte';
import setupMQTTClient from '$lib/mqttClient';
onMount(() => setupMQTTClient(PUBLIC_MQTT_BROKER_WS_URL));
</script>
<div id="app">
<slot />
<FooterNavigation />
</div>
<Modal />

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

@@ -0,0 +1,29 @@
<script lang="ts">
import HiveSummary from '$lib/components/HiveSummary.svelte';
import GatewaySummary from '$lib/components/GatewaySummary.svelte';
import HighTemperature from '$lib/components/cards/HighTemperature.svelte';
import LowTemperature from '$lib/components/cards/LowTemperature.svelte';
import LowBattery from '$lib/components/cards/LowBattery.svelte';
import NoData from '$lib/components/cards/NoData.svelte';
import WeightChanged from '$lib/components/cards/WeightChanged.svelte';
</script>
<main>
<h1>Hive monitor</h1>
<span class="header">Hives</span>
<HiveSummary />
<span class="header">Gateways</span>
<GatewaySummary />
<span class="header">Alerts</span>
<HighTemperature temperature="32" />
<LowTemperature temperature="10" />
<LowBattery battery="19" />
<NoData time="1" />
<WeightChanged from="19" to="20" />
</main>
<style lang="scss">
</style>

View File

@@ -0,0 +1,28 @@
<script lang="ts">
import HighTemperature from '$lib/components/cards/HighTemperature.svelte';
import LowTemperature from '$lib/components/cards/LowTemperature.svelte';
import LowBattery from '$lib/components/cards/LowBattery.svelte';
import NoData from '$lib/components/cards/NoData.svelte';
import WeightChanged from '$lib/components/cards/WeightChanged.svelte';
</script>
<main>
<h1>Alarms</h1>
<span class="header">Alerts</span>
<HighTemperature temperature="32" />
<LowTemperature temperature="10" />
<LowBattery battery="19" />
<NoData time="1" />
<WeightChanged from="21" to="22" />
<p class="read-the-docs">
Click on the Vite and Svelte logos to learn more
</p>
</main>
<style lang="scss">
</style>

View File

@@ -0,0 +1,13 @@
<script lang="ts">
import GatewaySummary from '$lib/components/GatewaySummary.svelte';
</script>
<main>
<h1>Dashboard</h1>
<span class="header">Gateways</span>
<GatewaySummary />
</main>
<style lang="scss">
</style>

View File

@@ -0,0 +1,18 @@
<script lang="ts">
</script>
<main>
<h1>Settings</h1>
</main>
<style lang="scss">
</style>
<!--
from machine import ADC, Pin
adc = ADC(Pin(35))
(adc.read() / 4095) * 2 * 3.3 * 1.1
let battery_voltage = (ADC.read(battery)/4095)*2*3.3*1.1; -->