mirror of
https://github.com/KevinMidboe/hivemonitor.git
synced 2025-10-29 01:20:25 +00:00
Defined app routes & layout for footer & modal components
This commit is contained in:
91
src/lib/components/FooterNavigation.svelte
Normal file
91
src/lib/components/FooterNavigation.svelte
Normal 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>
|
||||
70
src/lib/components/Modal.svelte
Normal file
70
src/lib/components/Modal.svelte
Normal 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>
|
||||
69
src/lib/components/modals/GatewayModal.svelte
Normal file
69
src/lib/components/modals/GatewayModal.svelte
Normal 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>
|
||||
94
src/lib/components/modals/HiveModal.svelte
Normal file
94
src/lib/components/modals/HiveModal.svelte
Normal 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>
|
||||
139
src/lib/components/modals/ModalHeader.svelte
Normal file
139
src/lib/components/modals/ModalHeader.svelte
Normal 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
17
src/routes/+layout.svelte
Normal 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
29
src/routes/+page.svelte
Normal 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>
|
||||
28
src/routes/alarms/+page.svelte
Normal file
28
src/routes/alarms/+page.svelte
Normal 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>
|
||||
13
src/routes/dashboard/+page.svelte
Normal file
13
src/routes/dashboard/+page.svelte
Normal 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>
|
||||
18
src/routes/settings/+page.svelte
Normal file
18
src/routes/settings/+page.svelte
Normal 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; -->
|
||||
Reference in New Issue
Block a user