Initial commit for the web interface

This commit is contained in:
nemunaire 2022-10-01 19:37:12 +02:00
commit 4eea7769ff
36 changed files with 1186 additions and 0 deletions

19
ui/src/app.html Normal file
View file

@ -0,0 +1,19 @@
<!DOCTYPE html>
<html lang="fr" class="d-flex flex-column mh-100 h-100">
<head>
<meta charset="utf-8" />
<meta name="description" content="" />
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#ffffff"/>
<link rel="apple-touch-icon" sizes="192x192" href="/img/apple-touch-icon.png">
<meta name="author" content="nemucorp">
<meta name="robots" content="none">
<base href="/">
%sveltekit.head%
</head>
<body class="flex-fill d-flex flex-column">
<div class="flex-fill d-flex flex-column justify-content-between" style="min-height: 100%">%sveltekit.body%</div>
</body>
</html>

View file

@ -0,0 +1,81 @@
<script>
import {
Icon,
Navbar,
NavbarBrand,
Nav,
NavItem,
NavLink,
} from 'sveltestrap';
const version = fetch('api/version', {headers: {'Accept': 'application/json'}}).then((res) => res.json())
export let activemenu = '';
export { className as class };
let className = '';
</script>
<Navbar class={className} color="primary" dark expand="xs" style="overflow-x: auto">
<NavbarBrand href="." class="d-none d-md-block" style="padding: 0; margin: -.5rem 0;">
Réveil
</NavbarBrand>
<Nav navbar>
<NavItem>
<NavLink
active={activemenu === 'recipes'}
class="text-center"
href="alarms"
>
<Icon name="alarm-fill" /><br class="d-inline d-md-none">
Réveils
</NavLink>
</NavItem>
<NavItem>
<NavLink
href="musiks"
class="text-center"
active={activemenu === 'explore'}
>
<Icon name="music-note-list" /><br class="d-inline d-md-none">
Musiques
</NavLink>
</NavItem>
<NavItem>
<NavLink
href="routines"
class="text-center"
active={activemenu === 'routines'}
>
<Icon name="activity" /><br class="d-inline d-md-none">
Routines
</NavLink>
</NavItem>
<NavItem>
<NavLink
href="history"
class="text-center"
active={activemenu === 'history'}
>
<Icon name="clipboard-pulse" /><br class="d-inline d-md-none">
Historique
</NavLink>
</NavItem>
<NavItem>
<NavLink
href="settings"
class="text-center"
active={activemenu === 'settings'}
>
<Icon name="gear-fill" /><br class="d-inline d-md-none">
Paramètres
</NavLink>
</NavItem>
</Nav>
<Nav class="ms-auto text-light" navbar>
<NavItem>
{#await version then v}
{v.version}
{/await}
</NavItem>
</Nav>
</Navbar>

View file

@ -0,0 +1,22 @@
<script>
import {
Toast,
ToastBody,
ToastHeader,
} from 'sveltestrap';
import { ToastsStore } from '../stores/toasts';
</script>
<div class="toast-container position-absolute top-0 end-0 p-3">
{#each $ToastsStore.toasts as toast}
<Toast>
<ToastHeader toggle={toast.close} icon={toast.color}>
{#if toast.title}{toast.title}{:else}Gustus{/if}
</ToastHeader>
<ToastBody>
{toast.msg}
</ToastBody>
</Toast>
{/each}
</div>

1
ui/src/global.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="@sveltejs/kit" />

34
ui/src/reveil.scss Normal file
View file

@ -0,0 +1,34 @@
// Your variable overrides can go here, e.g.:
// $h1-font-size: 3rem;
//$primary: #ff485a;
//$secondary: #ff7b88;
$blue: #2a9fd6;
$indigo: #6610f2;
$purple: #6f42c1;
$pink: #e83e8c;
$red: #c00;
$orange: #fd7e14;
$yellow: #f80;
$green: #77b300;
$teal: #20c997;
$cyan: #93c;
$primary: $pink;
$success: $green;
$info: $cyan;
$warning: $yellow;
$danger: $red;
$min-contrast-ratio: 2.25;
$enable-shadows: true;
$enable-gradients: true;
$enable-responsive-font-sizes: true;
$link-color: $primary;
$navbar-padding-y: 0;
$nav-link-padding-y: 0.2rem;
@import "bootstrap/scss/bootstrap";

2
ui/src/routes/+layout.js Normal file
View file

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

View file

@ -0,0 +1,34 @@
<script>
import '../reveil.scss'
import "bootstrap-icons/font/bootstrap-icons.css";
import {
//Styles,
} from 'sveltestrap';
import Header from '../components/Header.svelte';
import Toaster from '../components/Toaster.svelte';
</script>
<svelte:head>
<title>Réveil</title>
</svelte:head>
<!--Styles /-->
<Header
class="d-none d-lg-flex py-2"
/>
<div class="flex-fill pb-4 pb-lg-2">
<slot></slot>
</div>
<Toaster />
<Header
class="d-flex d-lg-none py-1 fixed-bottom"
/>
<style>
:global(a.badge) {
text-decoration: none;
}
</style>

View file

@ -0,0 +1,48 @@
<script>
import {
Container,
Icon,
} from 'sveltestrap';
</script>
<Container class="mh-100 h-100 d-flex flex-column justify-content-center text-center">
<figure>
<blockquote class="blockquote text-muted">
<p class="display-6">A well-known quote, contained in a blockquote element.</p>
</blockquote>
<figcaption class="blockquote-footer">
Someone famous in <cite title="Source Title">Source Title</cite>
</figcaption>
</figure>
<div class="display-5 mb-4">
Prochain réveil&nbsp;: demain matin à 7h10 (dans 5 cycles)
</div>
<div class="d-flex gap-3 justify-content-center">
<button class="btn btn-primary">
<Icon name="node-plus" />
Programmer un nouveau réveil
</button>
<button class="btn btn-info">
<Icon name="node-plus" />
5 cycles
</button>
<button class="btn btn-info">
<Icon name="node-plus" />
6 cycles
</button>
</div>
<div class="d-flex gap-3 mt-3 justify-content-center">
<button class="btn btn-outline-info">
<Icon name="skip-end-fill" />
Chanson suivante
</button>
<button class="btn btn-danger">
<Icon name="stop-circle" />
Éteindre le réveil
</button>
<button class="btn btn-outline-info">
<Icon name="fast-forward-fill" />
Passer cette étape de la routine
</button>
</div>
</Container>

80
ui/src/service-worker.ts Normal file
View file

@ -0,0 +1,80 @@
/// <reference lib="webworker" />
import { build, files, timestamp } from '$service-worker';
const worker = (self as unknown) as ServiceWorkerGlobalScope;
const FILES = `cache${timestamp}`;
// `build` is an array of all the files generated by the bundler,
// `files` is an array of everything in the `static` directory
const to_cache = build.concat(files);
const staticAssets = new Set(to_cache);
worker.addEventListener('install', (event) => {
event.waitUntil(
caches
.open(FILES)
.then((cache) => cache.addAll(to_cache))
.then(() => {
worker.skipWaiting();
})
);
});
worker.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then(async (keys) => {
// delete old caches
for (const key of keys) {
if (key !== FILES) await caches.delete(key);
}
worker.clients.claim();
})
);
});
/**
* Fetch the asset from the network and store it in the cache.
* Fall back to the cache if the user is offline.
*/
async function fetchAndCache(request: Request) {
const cache = await caches.open(`offline${timestamp}`);
try {
const response = await fetch(request);
cache.put(request, response.clone());
return response;
} catch (err) {
const response = await cache.match(request);
if (response) return response;
throw err;
}
}
worker.addEventListener('fetch', (event) => {
if (event.request.method !== 'GET' || event.request.headers.has('range')) return;
const url = new URL(event.request.url);
// don't try to handle e.g. data: URIs
const isHttp = url.protocol.startsWith('http');
const isDevServerRequest =
url.hostname === self.location.hostname && url.port !== self.location.port;
const isStaticAsset = url.host === self.location.host && staticAssets.has(url.pathname);
const skipBecauseUncached = event.request.cache === 'only-if-cached' && !isStaticAsset;
if (isHttp && !isDevServerRequest && !skipBecauseUncached) {
event.respondWith(
(async () => {
// always serve static files and bundler-generated assets from cache.
// if your application has other URLs with data that will never change,
// set this variable to true for them and they will only be fetched once.
const cachedAsset = isStaticAsset && (await caches.match(event.request));
return cachedAsset || fetchAndCache(event.request);
})()
);
}
});

41
ui/src/stores/toasts.js Normal file
View file

@ -0,0 +1,41 @@
import { writable } from 'svelte/store';
function createToastsStore() {
const { subscribe, update } = writable({toasts: []});
const addToast = (o) => {
o.timestamp = new Date();
o.close = () => {
update((i) => {
i.toasts = i.toasts.filter((j) => {
return !(j.title === o.title && j.msg === o.msg && j.timestamp === o.timestamp)
});
return i;
});
}
update((i) => {
i.toasts.unshift(o);
return i;
});
o.cancel = setTimeout(o.close, o.dismiss?o.dismiss:5000);
};
const addErrorToast = (o) => {
if (!o.title) o.title = 'Une erreur est survenue !';
if (!o.color) o.color = 'danger';
return addToast(o);
};
return {
subscribe,
addToast,
addErrorToast,
};
}
export const ToastsStore = createToastsStore();