Initial commit

This commit is contained in:
Sebastian Seedorf
2021-04-17 22:31:16 +02:00
commit a1df114a5a
20 changed files with 3001 additions and 0 deletions

234
.gitignore vendored Normal file
View File

@@ -0,0 +1,234 @@
# Created by https://www.toptal.com/developers/gitignore/api/node,webstorm
# Edit at https://www.toptal.com/developers/gitignore?templates=node,webstorm
### Node ###
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
.env*.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
# Nuxt.js build / generate output
.nuxt
dist
# Storybook build outputs
.out
.storybook-out
storybook-static
# rollup.js default build output
dist/
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# Temporary folders
tmp/
temp/
### WebStorm ###
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### WebStorm Patch ###
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
# *.iml
# modules.xml
# .idea/misc.xml
# *.ipr
# Sonarlint plugin
# https://plugins.jetbrains.com/plugin/7973-sonarlint
.idea/**/sonarlint/
# SonarQube Plugin
# https://plugins.jetbrains.com/plugin/7238-sonarqube-community-plugin
.idea/**/sonarIssues.xml
# Markdown Navigator plugin
# https://plugins.jetbrains.com/plugin/7896-markdown-navigator-enhanced
.idea/**/markdown-navigator.xml
.idea/**/markdown-navigator-enh.xml
.idea/**/markdown-navigator/
# Cache file creation bug
# See https://youtrack.jetbrains.com/issue/JBR-2257
.idea/$CACHE_FILE$
# CodeStream plugin
# https://plugins.jetbrains.com/plugin/12206-codestream
.idea/codestream.xml
# End of https://www.toptal.com/developers/gitignore/api/node,webstorm

5
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

6
.idea/jsLibraryMappings.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<includedPredefinedLibrary name="Node.js Core" />
</component>
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/wannistesvorbei.iml" filepath="$PROJECT_DIR$/.idea/wannistesvorbei.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

11
.idea/wannistesvorbei.iml generated Normal file
View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

4
.idea/watcherTasks.xml generated Normal file
View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectTasksOptions" suppressed-tasks="Pug/Jade" />
</project>

17
Dockerfile Normal file
View File

@@ -0,0 +1,17 @@
FROM node:15-alpine
WORKDIR /app
COPY ./public /app/public
COPY ./src /app/src
COPY ./views /app/views
COPY ./package.json /app/package.json
COPY ./package-lock.json /app/package-lock.json
COPY ./tsconfig.json /app/tsconfig.json
RUN npm install && ./node_modules/.bin/tsc && rm -rf node_modules src tsconfig.json
ENV NODE_ENV=production
RUN npm install --only=production && rm -rf package-lock.json
ENTRYPOINT npm start
EXPOSE 3000

2345
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
package.json Normal file
View File

@@ -0,0 +1,27 @@
{
"name": "wannistesvorbei",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node ./dist/index.js",
"dev": "tsc-watch --onSuccess \"node ./dist/index.js\""
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@types/express": "^4.17.11",
"@types/node": "^14.14.41",
"@types/node-fetch": "^2.5.10",
"@types/pug": "^2.0.4",
"tsc-watch": "^4.2.9",
"typescript": "^4.2.4"
},
"dependencies": {
"express": "^4.17.1",
"node-fetch": "^2.6.1",
"pug": "^3.0.2"
}
}

BIN
public/homescreen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

15
public/manifest.json Normal file
View File

@@ -0,0 +1,15 @@
{
"name": "Wie lange noch?",
"short_name": "Corona-Uhr",
"lang": "de-DE",
"start_url": "/",
"display": "standalone",
"orientation": "any",
"theme_color": "#FD1D1D",
"icons": [
{
"src": "homescreen.png",
"sizes": "172x172"
}
]
}

14
public/script.js Normal file
View File

@@ -0,0 +1,14 @@
function onloaded() {
const elems = document.getElementsByClassName("big-number");
for (let i = 0; i < elems.length; i++) {
const elem = elems[i];
elem.innerText = parseInt(elem.dataset.value).toLocaleString();
}
const megaElems = document.getElementsByClassName("mega-date");
for (let i = 0; i < megaElems.length; i++) {
const elem = megaElems[i];
const date = new Date();
date.setTime(elem.dataset.value);
elem.innerText = date.toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' });
}
}

78
public/style.css Normal file
View File

@@ -0,0 +1,78 @@
html {
min-height: 100%;
}
body {
font-family: 'Lato', sans-serif;
color: white;
background: rgb(131,58,180);
background: linear-gradient(11deg, rgba(131,58,180,1) 0%, rgba(253,29,29,1) 50%, rgb(255,171,13) 100%);
margin: 4rem;
}
.index {
font-size: 30pt;
text-align: center;
}
.index p {
line-height: 40pt;
}
.big-number {
text-decoration: underline;
color: #41ffc3;
}
.mega-date {
font-weight: 900;
font-size: 50pt;
margin-top: 8rem;
margin-bottom: 8rem;
}
.footer {
font-size: 12pt;
}
.margin-top {
margin-top: 8rem;
}
a, a:visited {
color: white;
}
a:hover {
color: #41ffc3;
}
h1:not(:first-child) {
margin-top: 3em;
}
h2:not(:first-child) {
margin-top: 2em;
}
h3:not(:first-child) {
margin-top: 1em;
}
@media (max-width:600px) {
.index {
font-size: 20pt;
}
body {
margin: 1rem;
}
}
@media (max-width:400px) {
.index p {
line-height: unset;
}
}

53
src/index.ts Normal file
View File

@@ -0,0 +1,53 @@
import * as express from "express";
import fetch from "node-fetch";
import {slice} from "./utils";
const app = express();
const data = {
dayCount: 500000,
firstVac: 1400000,
secondVac: 5000000,
finalDate: new Date()
};
(async function fetchData() {
const res = await fetch("https://impfdashboard.de/static/data/germany_vaccinations_timeseries_v2.tsv");
const html = await res.text();
const arrayOfLines = html.split(/[\r\n]+/);
let sumOfTodays = 0;
let countDays = 0;
for (const line of slice(arrayOfLines, arrayOfLines.length-8)) {
const arr = line.split(/\s+/);
if (arr.length < 10) continue;
if (!Number.isNaN(+(arr[2]))) {
sumOfTodays += +(arr[2]);
countDays++;
}
data.firstVac = +(arr[5]) || data.firstVac;
data.secondVac = +(arr[9]) || data.secondVac;
}
data.dayCount = sumOfTodays / countDays;
const daysLeft = (83703925*2 - data.firstVac - data.secondVac) / data.dayCount * 0.8;
const date = new Date();
date.setTime(date.getTime() + daysLeft * 24 * 60 * 60 * 1000);
data.finalDate = date;
setTimeout(fetchData, 1000*60*60);
})()
app.set('view engine', 'pug')
app.set('views', './views')
app.use("/public", express.static("./public"))
app.get("/", (req, res) => {
res.render("index", data);
})
app.get("/impressum", (req, res) => {
res.render("impressum");
})
const listener = app.listen(
3000,
() => {
const addr = listener.address();
console.log(`App listening on port ${typeof addr === "object" ? addr.port : addr}!`);
}
)

12
src/utils.ts Normal file
View File

@@ -0,0 +1,12 @@
export function* slice(src, start, finish = src.length) {
let index = 0;
for (const value of src) {
if (index >= finish) {
return;
}
if (index >= start) {
yield value;
}
++index;
}
}

14
tsconfig.json Normal file
View File

@@ -0,0 +1,14 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"sourceMap": true,
"outDir": "dist"
},
"exclude": [
"node_modules"
],
"include": [
"src"
]
}

97
views/impressum.pug Normal file
View File

@@ -0,0 +1,97 @@
extends layout.pug
block body
- const mail = 'web<span style="display: none;">REMOVE</span>{at<!-- hehehe -->}s<!-- hehehe -->eb<span style="display: none;">DEL<!-- hehehe -->ETE</span>se[dot]de'
.impressum
h1 Impressum
h2 Angaben gem&auml;&szlig; &sect; 5 TMG:
p
| Sebastian Seedorf
h3 Postanschrift:
p
| Charlottenburger Ufer 3a
br
| 10587 Berlin
br
h3 Kontakt:
p E-Mail: !{mail}
h2 Information gem&auml;&szlig; &sect; 36 VSBG
p Gem&auml;&szlig; &sect; 36 VSBG (Verbraucherstreitbeilegungsgesetz &ndash; Gesetz &uuml;ber die alternative Streitbeilegung in Verbrauchersachen) erkl&auml;rt der Betreiber dieser Website:
p Wir sind weder bereit noch verpflichtet, an Streitbeilegungsverfahren vor einer Verbraucherschlichtungsstelle teilzunehmen.
p
em
| Das Impressum wurde mit dem
a(href='https://www.activemind.de/datenschutz/impressums-generator/') Impressums-Generator der activeMind AG
| erstellt.
h1 Datenschutzerkl&auml;rung
p Verantwortlicher im Sinne der Datenschutzgesetze, insbesondere der EU-Datenschutzgrundverordnung (DSGVO), ist:
p
| Sebastian Seedorf
br
| !{mail}
h2 Ihre Betroffenenrechte
p Unter den angegebenen Kontaktdaten unseres Datenschutzbeauftragten k&ouml;nnen Sie jederzeit folgende Rechte aus&uuml;ben:
ul
li Auskunft &uuml;ber Ihre bei uns gespeicherten Daten und deren Verarbeitung (Art. 15 DSGVO),
li Berichtigung unrichtiger personenbezogener Daten (Art. 16 DSGVO),
li L&ouml;schung Ihrer bei uns gespeicherten Daten (Art. 17 DSGVO),
li Einschr&auml;nkung der Datenverarbeitung, sofern wir Ihre Daten aufgrund gesetzlicher Pflichten noch nicht l&ouml;schen d&uuml;rfen (Art. 18 DSGVO),
li Widerspruch gegen die Verarbeitung Ihrer Daten bei uns (Art. 21 DSGVO) und
li Daten&uuml;bertragbarkeit, sofern Sie in die Datenverarbeitung eingewilligt haben oder einen Vertrag mit uns abgeschlossen haben (Art. 20 DSGVO).
p Sofern Sie uns eine Einwilligung erteilt haben, k&ouml;nnen Sie diese jederzeit mit Wirkung f&uuml;r die Zukunft widerrufen.
p Sie k&ouml;nnen sich jederzeit mit einer Beschwerde an eine Aufsichtsbeh&ouml;rde wenden, z. B. an die zust&auml;ndige Aufsichtsbeh&ouml;rde des Bundeslands Ihres Wohnsitzes oder an die f&uuml;r uns als verantwortliche Stelle zust&auml;ndige Beh&ouml;rde.
p Eine Liste der Aufsichtsbeh&ouml;rden (f&uuml;r den nicht&ouml;ffentlichen Bereich) mit Anschrift finden Sie unter:
a(href='https://www.bfdi.bund.de/DE/Infothek/Anschriften_Links/anschriften_links-node.html' target='_blank' rel='nofollow noopener')
| https://www.bfdi.bund.de/DE/Infothek/Anschriften_Links/anschriften_links-node.html
| .
h2 Erfassung allgemeiner Informationen beim Besuch unserer Website
h3 Art und Zweck der Verarbeitung:
p Wenn Sie auf unsere Website zugreifen, d.h., wenn Sie sich nicht registrieren oder anderweitig Informationen &uuml;bermitteln, werden automatisch Informationen allgemeiner Natur erfasst. Diese Informationen (Server-Logfiles) beinhalten etwa die Art des Webbrowsers, das verwendete Betriebssystem, den Domainnamen Ihres Internet-Service-Providers, Ihre IP-Adresse und &auml;hnliches.
p Sie werden insbesondere zu folgenden Zwecken verarbeitet:
ul
li Sicherstellung eines problemlosen Verbindungsaufbaus der Website,
li Sicherstellung einer reibungslosen Nutzung unserer Website,
li Auswertung der Systemsicherheit und -stabilit&auml;t sowie
li zur Optimierung unserer Website.
p Wir verwenden Ihre Daten nicht, um R&uuml;ckschl&uuml;sse auf Ihre Person zu ziehen. Informationen dieser Art werden von uns ggfs. anonymisiert statistisch ausgewertet, um unseren Internetauftritt und die dahinterstehende Technik zu optimieren.
h3 Rechtsgrundlage und berechtigtes Interesse:
p Die Verarbeitung erfolgt gem&auml;&szlig; Art. 6 Abs. 1 lit. f DSGVO auf Basis unseres berechtigten Interesses an der Verbesserung der Stabilit&auml;t und Funktionalit&auml;t unserer Website.
h3 Empf&auml;nger:
p Empf&auml;nger der Daten sind ggf. technische Dienstleister, die f&uuml;r den Betrieb und die Wartung unserer Webseite als Auftragsverarbeiter t&auml;tig werden.
h3 Speicherdauer:
p Die Daten werden gel&ouml;scht, sobald diese f&uuml;r den Zweck der Erhebung nicht mehr erforderlich sind. Dies ist f&uuml;r die Daten, die der Bereitstellung der Website dienen, grunds&auml;tzlich der Fall, wenn die jeweilige Sitzung beendet ist.
p
h3 Bereitstellung vorgeschrieben oder erforderlich:
p Die Bereitstellung der vorgenannten personenbezogenen Daten ist weder gesetzlich noch vertraglich vorgeschrieben. Ohne die IP-Adresse ist jedoch der Dienst und die Funktionsf&auml;higkeit unserer Website nicht gew&auml;hrleistet. Zudem k&ouml;nnen einzelne Dienste und Services nicht verf&uuml;gbar oder eingeschr&auml;nkt sein. Aus diesem Grund ist ein Widerspruch ausgeschlossen.
h2 Verwendung von Scriptbibliotheken (Google Webfonts)
p Um unsere Inhalte browser&uuml;bergreifend korrekt und grafisch ansprechend darzustellen, verwenden wir auf dieser Website &bdquo;Google Web Fonts&ldquo; der Google LLC (1600 Amphitheatre Parkway, Mountain View, CA 94043, USA; nachfolgend &bdquo;Google&ldquo;) zur Darstellung von Schriften.
p
| Weitere Informationen zu Google Web Fonts finden Sie unter
a(href='https://developers.google.com/fonts/faq' rel='noopener nofollow' target='_blank') https://developers.google.com/fonts/faq
| und in der Datenschutzerkl&auml;rung von Google:
a(href='https://www.google.com/policies/privacy/' rel='noopener nofollow' target='_blank') https://www.google.com/policies/privacy/
| .
h2 SSL-Verschl&uuml;sselung
p
| Um die Sicherheit Ihrer Daten bei der &Uuml;bertragung zu sch&uuml;tzen, verwenden wir dem aktuellen Stand der Technik entsprechende Verschl&uuml;sselungsverfahren (z. B. SSL) &uuml;ber HTTPS.
hr
h2 Information &uuml;ber Ihr Widerspruchsrecht nach Art. 21 DSGVO
h3 Einzelfallbezogenes Widerspruchsrecht
p Sie haben das Recht, aus Gr&uuml;nden, die sich aus Ihrer besonderen Situation ergeben, jederzeit gegen die Verarbeitung Sie betreffender personenbezogener Daten, die aufgrund Art. 6 Abs. 1 lit. f DSGVO (Datenverarbeitung auf der Grundlage einer Interessenabw&auml;gung) erfolgt, Widerspruch einzulegen; dies gilt auch f&uuml;r ein auf diese Bestimmung gest&uuml;tztes Profiling im Sinne von Art. 4 Nr. 4 DSGVO.
p Legen Sie Widerspruch ein, werden wir Ihre personenbezogenen Daten nicht mehr verarbeiten, es sei denn, wir k&ouml;nnen zwingende schutzw&uuml;rdige Gr&uuml;nde f&uuml;r die Verarbeitung nachweisen, die Ihre Interessen, Rechte und Freiheiten &uuml;berwiegen, oder die Verarbeitung dient der Geltendmachung, Aus&uuml;bung oder Verteidigung von Rechtsanspr&uuml;chen.
h3 Empf&auml;nger eines Widerspruchs
p
| Sebastian Seedorf
br
| !{mail}
hr
h2 &Auml;nderung unserer Datenschutzbestimmungen
p Wir behalten uns vor, diese Datenschutzerkl&auml;rung anzupassen, damit sie stets den aktuellen rechtlichen Anforderungen entspricht oder um &Auml;nderungen unserer Leistungen in der Datenschutzerkl&auml;rung umzusetzen, z.B. bei der Einf&uuml;hrung neuer Services. F&uuml;r Ihren erneuten Besuch gilt dann die neue Datenschutzerkl&auml;rung.
h2 Fragen an den Datenschutzbeauftragten
p Wenn Sie Fragen zum Datenschutz haben, schreiben Sie uns bitte eine E-Mail oder wenden Sie sich direkt an die f&uuml;r den Datenschutz verantwortliche Person in unserer Organisation:
p
em
| Die Datenschutzerkl&auml;rung wurde mithilfe der activeMind AG erstellt, den Experten f&uuml;r
a(href='https://www.activemind.de/datenschutz/datenschutzbeauftragter/' target='_blank' rel='noopener') externe Datenschutzbeauftragte
| (Version #2020-09-30).

21
views/index.pug Normal file
View File

@@ -0,0 +1,21 @@
extends layout.pug
block body
.index
p
| Aktuell werden täglich &#32;
span.big-number(data-value=Math.floor(dayCount)) #{Math.floor(dayCount)}
| Impfungen durchgeführt. &#32;
span.big-number(data-value=firstVac) #{firstVac}
| haben bisher mindestens eine Impfung erhalten, davon &#32;
span.big-number(data-value=secondVac) #{secondVac}
| schon die zweite Impfung.
p
| Wenn das so weiter geht, dann haben 80% am
p.mega-date(data-value=finalDate.getTime()) #{finalDate.toLocaleDateString(locale, { year: 'numeric', month: 'numeric', day: 'numeric' })}
p
| zwei Impfungen erhalten.
p.footer.margin-top Quelle: &#32;
a(href="https://impfdashboard.de/") Offizelle Datensätze tagesaktuell bereitgestellt durch das RKI und das BMG
p.footer Quelle: &#32;
a(href="/impressum") Impressum & Datenschutzerklärung

34
views/layout.pug Normal file
View File

@@ -0,0 +1,34 @@
doctype html
html(lang='de')
head
title Wann ist es vorbei?
meta(name='title' content='Wann ist es vorbei?')
meta(name='description' content='Wann sind endlich genug Leute geimpft? Wann kehrt endlich wieder Normalität ein?')
meta(name='keywords' content='Corona, Impfung, Datum, RKI, AstraZeneca, Normalität')
meta(name='robots' content='index, follow')
meta(http-equiv='Content-Type' content='text/html; charset=utf-8')
meta(name='language' content='German')
meta(name='revisit-after' content='7 days')
meta(name='author' content='Sebastian Seedorf')
// Webapp
link(rel='manifest' href='/public/manifest.json')
meta(name='mobile-web-app-capable' content='yes')
meta(name='apple-mobile-web-app-capable' content='yes')
meta(name='application-name' content='Corona-Uhr')
meta(name='apple-mobile-web-app-title' content='Corona-Uhr')
meta(name='theme-color' content='#FD1D1D')
meta(name='msapplication-navbutton-color' content='#FD1D1D')
meta(name='apple-mobile-web-app-status-bar-style' content='black-translucent')
meta(name='msapplication-starturl' content='/')
meta(name='viewport' content='width=device-width, initial-scale=1, shrink-to-fit=no')
link(rel='icon' sizes='172x172' href='/public/homescreen.png')
link(rel='apple-touch-icon' sizes='172x172' href='/public/homescreen.png')
block scripts
script(src="/public/script.js")
link(rel="preconnect", href="https://fonts.gstatic.com")
link(rel="stylesheet", href="https://fonts.googleapis.com/css2?family=Lato:ital,wght@0,400;0,900;1,400;1,900&display=swap")
link(rel="stylesheet", href="/public/style.css")
body(onload="onloaded()")
block body