Initial commit
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
.idea
|
||||||
|
credentials.*
|
||||||
|
!*.sample.*
|
6
README.md
Normal file
6
README.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
# Elwig Access Frontend
|
||||||
|
|
||||||
|
The web frontend for accessing Elwig online.
|
||||||
|
|
||||||
|
https://access.elwig.at/
|
18
www/.php/credentials.sample.inc
Normal file
18
www/.php/credentials.sample.inc
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
global $CLIENT_ACCESS;
|
||||||
|
global $COMBINED_ACCESS;
|
||||||
|
|
||||||
|
$CLIENT_ACCESS = [
|
||||||
|
'WGX' => [
|
||||||
|
'name' => 'Winzergenossenschaft Ort',
|
||||||
|
'short' => 'Ort',
|
||||||
|
'api' => 'https://example.com/elwig/api/v1',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
$COMBINED_ACCESS = [
|
||||||
|
'HOLDING' => [
|
||||||
|
'name' => 'Name',
|
||||||
|
'clients' => ['WGX'],
|
||||||
|
],
|
||||||
|
];
|
BIN
www/favicon.ico
Normal file
BIN
www/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 229 KiB |
100
www/index.php
Normal file
100
www/index.php
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
require ".php/credentials.inc";
|
||||||
|
global $CLIENT_ACCESS;
|
||||||
|
global $COMBINED_ACCESS;
|
||||||
|
|
||||||
|
$info = explode('/', $_SERVER['PATH_INFO']);
|
||||||
|
$client = null;
|
||||||
|
foreach ($CLIENT_ACCESS as $name => $data) {
|
||||||
|
if ($name === $info[1] && (sizeof($info) === 2 || $info[2] === '')) {
|
||||||
|
$client = $data;
|
||||||
|
$client['id'] = $name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
foreach ($COMBINED_ACCESS as $name => $data) {
|
||||||
|
if ($name === $info[1] && (sizeof($info) === 2 || $info[2] === '')) {
|
||||||
|
$client = $data;
|
||||||
|
$client['id'] = $name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($client === null) {
|
||||||
|
header('Status: 200');
|
||||||
|
header('Content-Length: 0');
|
||||||
|
exit();
|
||||||
|
} else if ($_SERVER['PATH_INFO'] !== "/$client[id]/") {
|
||||||
|
header('Status: 308');
|
||||||
|
header("Location: /$client[id]/");
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($client['api']) {
|
||||||
|
$data = "window.ELWIG_API = \"$client[api]\";";
|
||||||
|
} else {
|
||||||
|
$data = "window.CLIENTS = {";
|
||||||
|
$first = true;
|
||||||
|
foreach ($client['clients'] as $id) {
|
||||||
|
$c = $CLIENT_ACCESS[$id];
|
||||||
|
if (!$first) $data .= ", ";
|
||||||
|
$data .= "\"$id\": {\"name\": \"$c[name]\", \"short\": \"$c[short]\", \"api\": \"$c[api]\"}";
|
||||||
|
$first = false;
|
||||||
|
}
|
||||||
|
$data .= "};";
|
||||||
|
}
|
||||||
|
|
||||||
|
header('Content-Type: application/xhtml+xml; charset=UTF-8');
|
||||||
|
echo "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"; ?>
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" lang="de">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<title><?php echo $client['name']; ?> - Elwig</title>
|
||||||
|
<link rel="icon" href="/favicon.ico" sizes="16x16 20x20 24x24 30x30 32x32 36x36 40x40 48x48 60x60 64x64 72x72 80x80 96x96 128x128 256x256"/>
|
||||||
|
<link rel="stylesheet" href="/res/style.css"/>
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
|
||||||
|
<script>window.CLIENT = "<?php echo $info[1]; ?>"; <?php echo $data; ?></script>
|
||||||
|
<script src="/res/access.js" type="application/javascript"/>
|
||||||
|
<script src="<?php echo $client['api'] ? "/res/access-single.js" : "/res/access-multiple.js"; ?>" type="application/javascript"/>
|
||||||
|
</head>
|
||||||
|
<body class="header-footer">
|
||||||
|
<header>
|
||||||
|
<nav>
|
||||||
|
<div>
|
||||||
|
<a href="https://elwig.at/"><img src="/res/elwig.png" alt="Elwig Logo"/></a>
|
||||||
|
</div>
|
||||||
|
<ul>
|
||||||
|
<?php if (isset($client['clients'])) {
|
||||||
|
foreach ($client['clients'] as $id) {
|
||||||
|
$c = $CLIENT_ACCESS[$id];
|
||||||
|
echo " <li><a href=\"#/$id\">$c[short]</a></li>\n";
|
||||||
|
}
|
||||||
|
} else { ?>
|
||||||
|
<li><a href="#/">Übersicht</a></li>
|
||||||
|
<li><a href="#/mitglied">Mitglied</a></li>
|
||||||
|
<li><a href="#/lieferungen">Lieferungen</a></li>
|
||||||
|
<li><a href="#/anmeldungen">Anmeldungen</a></li>
|
||||||
|
<?php } ?>
|
||||||
|
</ul>
|
||||||
|
<div>
|
||||||
|
<?php if (!isset($client['clients'])) { ?>
|
||||||
|
<a href="#/mitglied" id="user">
|
||||||
|
<div id="usertext"></div>
|
||||||
|
<img src="/res/avatar.png" alt="Avatar" style="border-radius: 50%; height: 2.25em; margin: 0.5em;"/>
|
||||||
|
</a>
|
||||||
|
<?php } ?>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
<main id="access"/>
|
||||||
|
<footer>
|
||||||
|
<a href="https://elwig.at/"><img src="/res/elwig.png" alt="Elwig"/></a>
|
||||||
|
<p>
|
||||||
|
<strong>Impressum</strong><br/>
|
||||||
|
Lorenz Stechauner, Thomas Hilscher<br/>
|
||||||
|
Österreich (Austria)<br/>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Kontakt</strong><br/>
|
||||||
|
E-Mail: <a href="mailto:contact@necronda.net">contact@necronda.net</a><br/>
|
||||||
|
</p>
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
</html>
|
189
www/res/access-multiple.js
Normal file
189
www/res/access-multiple.js
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
window.CLIENT = window.CLIENT || null;
|
||||||
|
window.CLIENTS = window.CLIENTS || {};
|
||||||
|
|
||||||
|
function getStoredUsername(client) {
|
||||||
|
return window.localStorage.getItem(`${CLIENT}/${client}/username`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStoredToken(client) {
|
||||||
|
return window.localStorage.getItem(`${CLIENT}/${client}/token`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAuthorizationHeader(client) {
|
||||||
|
return {
|
||||||
|
'Authorization': 'Bearer ' + window.localStorage.getItem(`${CLIENT}/${client}/token`),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function authenticate(client, username, password) {
|
||||||
|
const res = await fetch(`${CLIENTS[client]['api']}/auth`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {'Authorization': 'Basic ' + btoa(username + ':' + password)},
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
if (!res.ok) throw new ApiError(res.status, json['message']);
|
||||||
|
return json['token'];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get(client, path) {
|
||||||
|
const res = await fetch(`${CLIENTS[client]['api']}${path}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {...getAuthorizationHeader(client)},
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
if (!res.ok) throw new ApiError(res.status, json['message']);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDeliverySchedules(client, filters, limit, offset) {
|
||||||
|
const query = [];
|
||||||
|
if (!!filters) query.push(`filters=${filters.join(',')}`);
|
||||||
|
if (!!limit) query.push(`limit=${limit}`);
|
||||||
|
if (!!offset) query.push(`offset=${offset}`);
|
||||||
|
return await get(client, `/delivery_schedules${!!query ? '?' : ''}${query.join('&')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateOverview(client) {
|
||||||
|
const [schedules] = await Promise.all([getDeliverySchedules(client, [`year=${getCurrentLastSeason()}`])]);
|
||||||
|
const rows = [];
|
||||||
|
const days = groupBy(schedules['data'], 'date');
|
||||||
|
const now = new Date();
|
||||||
|
for (const [dateString, day] of Object.entries(days)) {
|
||||||
|
const date = new Date(dateString);
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'day';
|
||||||
|
if (now.getFullYear() === date.getFullYear() && now.getMonth() === date.getMonth() && now.getDate() === date.getDate())
|
||||||
|
row.classList.add('today');
|
||||||
|
row.innerHTML = `<div><span style="font-size: 0.75em; display: block">${fmtDateWeekday(date)}</span>${fmtDate(date)}</div>`;
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'schedule-container';
|
||||||
|
for (const schedule of day) {
|
||||||
|
const from = schedule.announcement_from !== null ? new Date(schedule.announcement_from) : null;
|
||||||
|
const to = schedule.announcement_to !== null ? new Date(schedule.announcement_to) : null;
|
||||||
|
const status = from === null && to === null ? 'Anmeldung offen' : from > now ? `Anmeldung ab ${fmtDateTime(from)}` : to > now ? `Anmeldung bis ${fmtDateTime(new Date(to - 1))}` : 'Anmeldefrist vorbei';
|
||||||
|
const link = document.createElement('a');
|
||||||
|
if (schedule.is_cancelled) link.className = 'cancelled';
|
||||||
|
link.innerHTML += `<div><span style="font-size: 0.75em; display: block;">${escapeHTML(schedule.branch.name)}</span><span>${escapeHTML(schedule.description)}</span><span style="font-size: 0.75em; display: block;">${status}</span></div>`;
|
||||||
|
if (schedule.delivered_weight > 0) {
|
||||||
|
link.innerHTML += `
|
||||||
|
<span>
|
||||||
|
<span><strong>${fmtInt(schedule.delivered_weight)} kg</strong></span> /
|
||||||
|
<span class="min-kg">${fmtInt(schedule.announced_weight)} kg</span>
|
||||||
|
(<span class="min-percent">${fmtInt(Math.round(schedule.delivered_weight / schedule.announced_weight * 100))}%</span>)
|
||||||
|
</span>`;
|
||||||
|
} else if (schedule.max_weight !== null) {
|
||||||
|
link.innerHTML += `
|
||||||
|
<span>
|
||||||
|
<span>${fmtInt(schedule.announced_weight)} kg</span> /
|
||||||
|
<span class="min-kg">${fmtInt(schedule.max_weight)} kg</span>
|
||||||
|
(<span class="min-percent">${fmtInt(Math.round(schedule.announced_weight / schedule.max_weight * 100))}%</span>)
|
||||||
|
</span>`;
|
||||||
|
} else {
|
||||||
|
link.innerHTML += `<span><span>${fmtInt(schedule.announced_weight)} kg</span></span>`;
|
||||||
|
}
|
||||||
|
container.append(link);
|
||||||
|
}
|
||||||
|
row.appendChild(container);
|
||||||
|
rows.push(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
const main = document.getElementsByTagName('main')[0];
|
||||||
|
main.replaceChildren(main.firstElementChild, ...rows);
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
const hash = window.location.hash;
|
||||||
|
const main = document.getElementById("access");
|
||||||
|
const nav = document.getElementsByTagName("nav")[0].getElementsByTagName("ul")[0];
|
||||||
|
for (const li of nav.children) li.className = '';
|
||||||
|
|
||||||
|
const client = Object.keys(CLIENTS).find(id => hash.startsWith(`#/${id}/`) || hash === `#/${id}`);
|
||||||
|
if (client === undefined) {
|
||||||
|
window.location.hash = `#/${Object.keys(CLIENTS).find(id => !!getStoredUsername(id) && !!getStoredToken(id)) || Object.keys(CLIENTS)[0]}`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nav.children[Object.keys(CLIENTS).indexOf(client)].className = 'active';
|
||||||
|
|
||||||
|
if ((!getStoredUsername(client) || !getStoredToken(client)) && window.location.hash !== `#/${client}/login`) {
|
||||||
|
window.location.hash = `#/${client}/login`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hash === `#/${client}/login`) {
|
||||||
|
main.className = 'login';
|
||||||
|
main.innerHTML = `
|
||||||
|
<form onsubmit="return actionLogin(this);">
|
||||||
|
<h1>Anmelden</h1>
|
||||||
|
<input type="text" name="username" placeholder="Benutzername" value="${getStoredUsername(client) ?? ''}"/>
|
||||||
|
<input type="password" name="password" placeholder="Kennwort"/>
|
||||||
|
<input type="hidden" name="client" value="${client}"/>
|
||||||
|
<button type="submit">Anmelden</button>
|
||||||
|
</form>`;
|
||||||
|
} else if (hash === `#/${client}`) {
|
||||||
|
main.className = 'overview';
|
||||||
|
main.innerHTML = `
|
||||||
|
<h1>${CLIENTS[client].name}</h1>`;
|
||||||
|
update();
|
||||||
|
} else {
|
||||||
|
window.location.hash = `#/${client}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
const hash = window.location.hash;
|
||||||
|
const client = Object.keys(CLIENTS).find(id => hash.startsWith(`#/${id}/`) || hash === `#/${id}`);
|
||||||
|
if (document.hidden || client === undefined) {
|
||||||
|
// do nothing
|
||||||
|
} else {
|
||||||
|
if (hash === `#/${client}`) {
|
||||||
|
updateOverview(client)
|
||||||
|
.then()
|
||||||
|
.catch(e => {
|
||||||
|
if (e instanceof ApiError && e.statusCode === 401) {
|
||||||
|
window.localStorage.removeItem(`${CLIENT}/${client}/token`);
|
||||||
|
window.location.hash = `#/${client}/login`;
|
||||||
|
} else {
|
||||||
|
alert(e.localizedMessage ?? ERROR_MESSAGES[e.message] ?? `Unbekannter Fehler: ${e.message}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
await init();
|
||||||
|
setInterval(update, 60_000);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('hashchange', render);
|
||||||
|
|
||||||
|
window.addEventListener('pageshow', update)
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', update);
|
||||||
|
|
||||||
|
function actionLogin(form) {
|
||||||
|
const elements = form.getElementsByClassName('error');
|
||||||
|
for (const e of elements) form.removeChild(e);
|
||||||
|
|
||||||
|
const client = form['client'].value;
|
||||||
|
window.localStorage.setItem(`${CLIENT}/${client}/username`, form['username'].value);
|
||||||
|
|
||||||
|
authenticate(client, form['username'].value, form['password'].value)
|
||||||
|
.then(token => {
|
||||||
|
window.localStorage.setItem(`${CLIENT}/${client}/token`, token);
|
||||||
|
window.location.hash = `#/${client}`;
|
||||||
|
}).catch(e => {
|
||||||
|
const error = document.createElement('div');
|
||||||
|
error.className = 'error';
|
||||||
|
error.innerText = e.localizedMessage ?? ERROR_MESSAGES[e.message] ?? `Unbekannter Fehler\n(${e.message})`;
|
||||||
|
form.insertBefore(error, form.lastChild.previousSibling);
|
||||||
|
});
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
330
www/res/access-single.js
Normal file
330
www/res/access-single.js
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
window.CLIENT = window.CLIENT || null;
|
||||||
|
window.ELWIG_API = window.ELWIG_API || null;
|
||||||
|
|
||||||
|
function getCredentialsUsername() {
|
||||||
|
return window.localStorage.getItem(`${CLIENT}/username`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCredentialsPassword() {
|
||||||
|
return window.localStorage.getItem(`${CLIENT}/password`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBasicAuth() {
|
||||||
|
return {
|
||||||
|
'Authorization': 'Basic ' + btoa(getCredentialsUsername() + ':' + getCredentialsPassword()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function _get(path) {
|
||||||
|
const res = await fetch(`${ELWIG_API}${path}`, {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {...getBasicAuth()},
|
||||||
|
});
|
||||||
|
const json = await res.json();
|
||||||
|
if (!res.ok) throw new ApiError(res.status, json['message']);
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get(path) {
|
||||||
|
return (await _get(path))['data'];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getMember(mgnr) {
|
||||||
|
return await get(`/members/${mgnr}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getWineVarieties() {
|
||||||
|
return Object.fromEntries((await get('/wine/varieties')).map(item => [item['sortid'], item]));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getWineQualityLevels() {
|
||||||
|
return Object.fromEntries((await get('/wine/quality_levels')).map(item => [item['qualid'], item]));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getWineAttributes() {
|
||||||
|
return Object.fromEntries((await get('/wine/attributes')).map(item => [item['attrid'], item]));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getWineCultivations() {
|
||||||
|
return Object.fromEntries((await get('/wine/cultivations')).map(item => [item['cultid'], item]));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getModifiers() {
|
||||||
|
const list = await get('/modifiers');
|
||||||
|
const dict = {};
|
||||||
|
for (const item of list) {
|
||||||
|
if (!dict[item['year']]) dict[item['year']] = {};
|
||||||
|
dict[item['year']][item['modid']] = item;
|
||||||
|
}
|
||||||
|
return dict;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDeliveries(filters, limit, offset) {
|
||||||
|
const query = ['sort=reverse'];
|
||||||
|
if (!!filters) query.push(`filters=${filters.join(',')}`);
|
||||||
|
if (!!limit) query.push(`limit=${limit}`);
|
||||||
|
if (!!offset) query.push(`offset=${offset}`);
|
||||||
|
return await _get(`/deliveries${!!query ? '?' : ''}${query.join('&')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDeliveryStats(filters, detail) {
|
||||||
|
const query = [];
|
||||||
|
if (!!filters) query.push(`filters=${filters.join(',')}`);
|
||||||
|
if (!!detail) query.push(`detail=${detail}`);
|
||||||
|
return await _get(`/deliveries/stat${!!query ? '?' : ''}${query.join('&')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDeliverySchedules(filters, limit, offset) {
|
||||||
|
const query = [];
|
||||||
|
if (!!filters) query.push(`filters=${filters.join(',')}`);
|
||||||
|
if (!!limit) query.push(`limit=${limit}`);
|
||||||
|
if (!!offset) query.push(`offset=${offset}`);
|
||||||
|
return await _get(`/delivery_schedules${!!query ? '?' : ''}${query.join('&')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDeliveryAnnouncements(filters, limit, offset) {
|
||||||
|
const query = [];
|
||||||
|
if (!!filters) query.push(`filters=${filters.join(',')}`);
|
||||||
|
if (!!limit) query.push(`limit=${limit}`);
|
||||||
|
if (!!offset) query.push(`offset=${offset}`);
|
||||||
|
return await _get(`/delivery_announcements${!!query ? '?' : ''}${query.join('&')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
const main = document.getElementById("access");
|
||||||
|
const form = main.getElementsByTagName("form")[0];
|
||||||
|
if (form) {
|
||||||
|
const elements = form.getElementsByClassName('error');
|
||||||
|
for (const e of elements) form.removeChild(e);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
window.MEMBER = await getMember(getCredentialsUsername());
|
||||||
|
const txt = document.getElementById('usertext');
|
||||||
|
txt.innerHTML = `${MEMBER.prefix ?? ''} ${MEMBER.given_name ?? ''} ${MEMBER.middle_names ?? ''} ${MEMBER.name ?? ''} ${MEMBER.suffix ?? ''}<br/><div>MgNr. ${MEMBER.mgnr}</div>`;
|
||||||
|
window.WINE_VARIETIES = await getWineVarieties();
|
||||||
|
window.WINE_QUALITY_LEVELS = await getWineQualityLevels();
|
||||||
|
window.WINE_ATTRIBUTES = await getWineAttributes();
|
||||||
|
window.WINE_CULTIVATIONS = await getWineCultivations();
|
||||||
|
window.MODIFIERS = await getModifiers();
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
if (form) {
|
||||||
|
window.localStorage.removeItem(`${CLIENT}/password`);
|
||||||
|
const error = document.createElement('div');
|
||||||
|
error.className = 'error';
|
||||||
|
error.innerText = e.localizedMessage ?? ERROR_MESSAGES[e.message] ?? 'Unbekannter Fehler';
|
||||||
|
form.insertBefore(error, form.lastChild.previousSibling);
|
||||||
|
} else {
|
||||||
|
window.location.hash = '#/login';
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
if (!getCredentialsUsername() || !getCredentialsPassword()) {
|
||||||
|
window.location.hash = '#/login';
|
||||||
|
render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await load();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateOverview() {
|
||||||
|
const [schedules] = await Promise.all([getDeliverySchedules([`year=${getCurrentLastSeason()}`])]);
|
||||||
|
|
||||||
|
const main = document.getElementsByTagName('main')[0];
|
||||||
|
const days = groupBy(schedules.data, 'date');
|
||||||
|
for (const [dateString, day] of Object.entries(days)) {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'day';
|
||||||
|
const date = new Date(dateString);
|
||||||
|
row.innerHTML = `<div><span style="font-size: 0.75em; display: block">${fmtDateWeekday(date)}</span>${fmtDate(date)}</div>`;
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.className = 'schedule-container';
|
||||||
|
for (const schedule of day) {
|
||||||
|
const now = new Date();
|
||||||
|
const from = schedule.announcement_from !== null ? new Date(schedule.announcement_from) : null;
|
||||||
|
const to = schedule.announcement_to !== null ? new Date(schedule.announcement_to) : null;
|
||||||
|
const status = from === null && to === null ? 'Anmeldung offen' : from > now ? `Anmeldung ab ${fmtDateTime(from)}` : to > now ? `Anmeldung bis ${fmtDateTime(new Date(to - 1))}` : 'Anmeldefrist vorbei';
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = `#/anmelden/${schedule.year}/${schedule.dsnr}`
|
||||||
|
link.innerHTML += `<div><span>${schedule.description}</span><span style="font-size: 0.75em; display: block;">${status}</span></div>`;
|
||||||
|
if (schedule.delivered_weight > 0) {
|
||||||
|
link.innerHTML += `
|
||||||
|
<span>
|
||||||
|
<span><strong>${fmtInt(schedule.delivered_weight)} kg</strong></span> /
|
||||||
|
<span class="min-kg">${fmtInt(schedule.announced_weight)} kg</span>
|
||||||
|
(<span class="min-percent">${fmtInt(Math.round(schedule.delivered_weight / schedule.announced_weight * 100))}%</span>)
|
||||||
|
</span>`;
|
||||||
|
} else {
|
||||||
|
link.innerHTML += `
|
||||||
|
<span>
|
||||||
|
<span>${fmtInt(schedule.announced_weight)} kg</span> /
|
||||||
|
<span class="min-kg">${fmtInt(schedule.max_weight)} kg</span>
|
||||||
|
(<span class="min-percent">${fmtInt(Math.round(schedule.announced_weight / schedule.max_weight * 100))}%</span>)
|
||||||
|
</span>`;
|
||||||
|
}
|
||||||
|
container.append(link);
|
||||||
|
}
|
||||||
|
row.appendChild(container);
|
||||||
|
main.appendChild(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateDeliveries(year) {
|
||||||
|
const filters = [`mgnr=${MEMBER.mgnr}`, `year=${year}`];
|
||||||
|
const [deliveries, stat] = await Promise.all([getDeliveries(filters), getDeliveryStats(filters)]);
|
||||||
|
|
||||||
|
const tbody = document.getElementById('delivery-list');
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
for (const delivery of deliveries.data) {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.style.background = '#C0C0C0';
|
||||||
|
tr.innerHTML = `
|
||||||
|
<th colspan="2">${delivery.lsnr}</th>
|
||||||
|
<td>${fmtDate(new Date(delivery.date))}</td>
|
||||||
|
<td>${delivery.time.substring(0, 5)}</td>
|
||||||
|
<td colspan="2">${delivery.branch.name}</td>
|
||||||
|
<td colspan="7">${delivery.comment ?? ''}</td>`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
for (const part of delivery.parts) {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
const defaultQualityLevel = getDefaultQualityLevel(part.gradation.kmw);
|
||||||
|
tr.innerHTML = `
|
||||||
|
<th>${part.dpnr}</th>
|
||||||
|
<td colspan="2">${WINE_VARIETIES[part.variety.sortid]?.name ?? ''}</td>
|
||||||
|
<td colspan="2" style="font-weight: bold;">${WINE_CULTIVATIONS[part.cultivation?.cultid]?.name ?? ''}</td>
|
||||||
|
<td colspan="2" style="font-weight: bold;">${WINE_ATTRIBUTES[part.attribute?.attrid]?.name ?? ''}</td>
|
||||||
|
<td class="${defaultQualityLevel['qualid'] != part.quality_level.qualid ? 'abgewertet' : ''}">${WINE_QUALITY_LEVELS[part.quality_level.qualid]?.name ?? ''}</td>
|
||||||
|
<td class="center">${fmtOe(part.gradation.oe)}</td>
|
||||||
|
<td class="center">${fmtKmw(part.gradation.kmw)}</td>
|
||||||
|
<td class="number">${fmtInt(part.weight)}</td>
|
||||||
|
<td>${part.modifiers.map(m => MODIFIERS[delivery.year][m.modid]).sort(m => m.ordering).map(m => m.name).join(' / ')}</td>
|
||||||
|
<td>${part.comment ?? ''}</td>`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const element = document.getElementById('delivery-stat');
|
||||||
|
element.innerText = `(Teil-)Lieferungen: ${fmtInt(stat.data.total.count)} (${fmtInt(stat.data.total.parts)}), Gewicht: ${fmtInt(stat.data.total.weight.sum)} kg`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
const hash = window.location.hash;
|
||||||
|
const main = document.getElementById("access");
|
||||||
|
const nav = document.getElementsByTagName("nav")[0].getElementsByTagName("ul")[0];
|
||||||
|
for (const li of nav.children) li.className = '';
|
||||||
|
if (hash === '#/login') {
|
||||||
|
main.className = 'login';
|
||||||
|
main.innerHTML = `
|
||||||
|
<form onsubmit="return actionLogin(this);">
|
||||||
|
<h1>Anmelden</h1>
|
||||||
|
<input type="text" name="username" placeholder="Mitgliedsnummer" value="${getCredentialsUsername() ?? ''}"/>
|
||||||
|
<input type="password" name="password" placeholder="Kennwort"/>
|
||||||
|
<button type="submit">Anmelden</button>
|
||||||
|
</form>`;
|
||||||
|
} else if (hash === '#/') {
|
||||||
|
nav.children[0].className = 'active';
|
||||||
|
main.className = 'overview';
|
||||||
|
main.innerHTML = `
|
||||||
|
<h1>Übersicht</h1>`;
|
||||||
|
updateOverview().then();
|
||||||
|
} else if (hash === '#/mitglied') {
|
||||||
|
nav.children[1].className = 'active';
|
||||||
|
main.className = 'member';
|
||||||
|
main.innerHTML = `
|
||||||
|
<h1>Mitglied</h1>
|
||||||
|
<button onclick="actionLogout()">Abmelden</button>
|
||||||
|
<pre>${JSON.stringify(MEMBER, null, 2)}</pre>`;
|
||||||
|
} else if (hash === '#/lieferungen') {
|
||||||
|
nav.children[2].className = 'active';
|
||||||
|
main.className = 'deliveries';
|
||||||
|
main.innerHTML = `
|
||||||
|
<h1>Lieferungen</h1>
|
||||||
|
<form>
|
||||||
|
<div>
|
||||||
|
<label for="season">Saison:</label>
|
||||||
|
<input name="season" type="number" min="1900" max="9999"
|
||||||
|
value="${getCurrentLastSeason()}" onchange="updateDeliveries(this.value).then()"/>
|
||||||
|
</div>
|
||||||
|
<div id="delivery-stat"/>
|
||||||
|
</form>
|
||||||
|
<table style="width: 100%;">
|
||||||
|
<colgroup>
|
||||||
|
<col style="width: 50px;"/>
|
||||||
|
<col style="width: 70px;"/>
|
||||||
|
<col style="width: 100px;"/>
|
||||||
|
<col style="width: 60px;"/>
|
||||||
|
<col style="width: 50px;"/>
|
||||||
|
<col style="width: 70px;"/>
|
||||||
|
<col style="width: 50px;"/>
|
||||||
|
<col style="width: 120px;"/>
|
||||||
|
<col style="width: 50px;"/>
|
||||||
|
<col style="width: 50px;"/>
|
||||||
|
<col style="width: 60px;"/>
|
||||||
|
<col style="min-width: 80px;"/>
|
||||||
|
</colgroup>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th rowspan="2"></th>
|
||||||
|
<th rowspan="2" colspan="2">Sorte</th>
|
||||||
|
<th rowspan="2" colspan="2">Bewirt.</th>
|
||||||
|
<th rowspan="2" colspan="2">Attribut</th>
|
||||||
|
<th rowspan="2">Qualitätsstufe</th>
|
||||||
|
<th colspan="2" class="center">Gradation</th>
|
||||||
|
<th class="center">Gewicht</th>
|
||||||
|
<th rowspan="2">Zu-/Abschläge</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th class="unit">[°Oe]</th>
|
||||||
|
<th class="unit">[°KMW]</th>
|
||||||
|
<th class="unit">[kg]</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="delivery-list"></tbody>
|
||||||
|
</table>`;
|
||||||
|
updateDeliveries(getCurrentLastSeason()).then();
|
||||||
|
} else if (hash === '#/anmeldungen') {
|
||||||
|
nav.children[3].className = 'active';
|
||||||
|
main.className = 'announcements';
|
||||||
|
main.innerHTML = '<h1>Anmeldungen</h1>';
|
||||||
|
} else if (hash.startsWith('#/anmelden/')) {
|
||||||
|
nav.children[3].className = 'active';
|
||||||
|
main.className = 'announce';
|
||||||
|
main.innerHTML = '<h1>Anmelden</h1>';
|
||||||
|
} else {
|
||||||
|
window.location.hash = `#/`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', async () => {
|
||||||
|
await init();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('hashchange', () => {
|
||||||
|
if ((!getCredentialsUsername() || !getCredentialsPassword()) && window.location.hash !== '#/login') {
|
||||||
|
window.location.hash = '#/login';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
function actionLogin(form) {
|
||||||
|
window.localStorage.setItem(`${CLIENT}/username`, form.username.value);
|
||||||
|
window.localStorage.setItem(`${CLIENT}/password`, form.password.value);
|
||||||
|
load().then(success => {
|
||||||
|
if (success) window.location.hash = '#/';
|
||||||
|
});
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function actionLogout() {
|
||||||
|
window.localStorage.removeItem(`${CLIENT}/username`);
|
||||||
|
window.localStorage.removeItem(`${CLIENT}/password`);
|
||||||
|
window.location.reload();
|
||||||
|
}
|
80
www/res/access.js
Normal file
80
www/res/access.js
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
"use strict";
|
||||||
|
|
||||||
|
const ERROR_MESSAGES = {
|
||||||
|
401: 'Ungültige Anmeldedaten',
|
||||||
|
'NetworkError when attempting to fetch resource.': 'Netzwerkfehler',
|
||||||
|
};
|
||||||
|
|
||||||
|
class ApiError extends Error {
|
||||||
|
constructor(statusCode, message) {
|
||||||
|
super(statusCode + ' - ' + message);
|
||||||
|
this.statusCode = statusCode;
|
||||||
|
this.name = 'ApiError';
|
||||||
|
this.localizedMessage = ERROR_MESSAGES[statusCode];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHTML(str) {
|
||||||
|
const p = document.createElement("p");
|
||||||
|
p.appendChild(document.createTextNode(str));
|
||||||
|
return p.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtDate(date) {
|
||||||
|
return date.toLocaleDateString('de-AT', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
year: 'numeric',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtDateTime(date) {
|
||||||
|
return date.toLocaleDateString('de-AT', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtDateWeekday(date) {
|
||||||
|
return date.toLocaleDateString('de-AT', {
|
||||||
|
weekday: 'long',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtOe(oe) {
|
||||||
|
return oe.toLocaleString('de-AT', {minimumFractionDigits: 0, maximumFractionDigits: 0});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtKmw(kmw) {
|
||||||
|
return kmw.toLocaleString('de-AT', {minimumFractionDigits: 1, maximumFractionDigits: 1});
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmtInt(num) {
|
||||||
|
return num.toLocaleString('de-AT', {minimumFractionDigits: 0, maximumFractionDigits: 0});
|
||||||
|
}
|
||||||
|
|
||||||
|
function groupBy(list, key) {
|
||||||
|
return list.reduce((groups, value) => {
|
||||||
|
(groups[value[key]] = groups[value[key]] ?? []).push(value);
|
||||||
|
return groups;
|
||||||
|
}, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentLastSeason() {
|
||||||
|
const date = new Date();
|
||||||
|
return date.getFullYear() - ((date.getMonth() + 1) <= 6 ? 1 : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDefaultQualityLevel(kmw) {
|
||||||
|
const list = Object.values(WINE_QUALITY_LEVELS).filter(q => !q['is_predicate']).sort((a, b) => a['min_kmw'] - b['min_kmw']);
|
||||||
|
let last = list[0];
|
||||||
|
for (const q of list) {
|
||||||
|
if (q['min_kmw'] > kmw) {
|
||||||
|
return last;
|
||||||
|
}
|
||||||
|
last = q;
|
||||||
|
}
|
||||||
|
return last;
|
||||||
|
}
|
BIN
www/res/avatar.png
Normal file
BIN
www/res/avatar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.8 KiB |
BIN
www/res/elwig.png
Normal file
BIN
www/res/elwig.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
340
www/res/style.css
Normal file
340
www/res/style.css
Normal file
@ -0,0 +1,340 @@
|
|||||||
|
|
||||||
|
/**** Shared ****/
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
font-family: 'Arial', sans-serif;
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
--main-color: #A040C0;
|
||||||
|
--light-color: #D098E0;
|
||||||
|
--accent-color: #C0F080;
|
||||||
|
--bg-color: #EBEFE7;
|
||||||
|
--blur-color: #60804020;
|
||||||
|
--blur: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.header-footer {
|
||||||
|
min-height: calc(100vh + 8em);
|
||||||
|
padding: 3em 0 8em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 3em;
|
||||||
|
box-shadow: 0 0 0.5em #00000060;
|
||||||
|
background-color: #FFFFFF;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
header img {
|
||||||
|
height: 2.5em;
|
||||||
|
margin: 0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav > *:first-child,
|
||||||
|
nav > *:last-child {
|
||||||
|
flex: 100px 1 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav > *:last-child {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav ul {
|
||||||
|
display: flex;
|
||||||
|
list-style-type: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav li {
|
||||||
|
flex: 100px 1 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav li a {
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 1em 2em;
|
||||||
|
color: #808080;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav li a:hover,
|
||||||
|
nav li.active a{
|
||||||
|
color: var(--main-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a.flag {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a.flag div {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 1.25em;
|
||||||
|
width: 1em;
|
||||||
|
margin: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a.flag[href='/de/'] div::before,
|
||||||
|
nav a.flag[href='/en/']:hover div::before,
|
||||||
|
nav a.flag[href='/en/']:focus div::before {
|
||||||
|
content: '\1F1EC\1F1E7'; /* GB */
|
||||||
|
}
|
||||||
|
|
||||||
|
nav a.flag[href='/en/'] div::before,
|
||||||
|
nav a.flag[href='/de/']:hover div::before,
|
||||||
|
nav a.flag[href='/de/']:focus div::before {
|
||||||
|
content: '\1F1E6\1F1F9'; /* AT */
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
height: 8em;
|
||||||
|
padding: 1em;
|
||||||
|
width: 100%;
|
||||||
|
box-shadow: 0 0 0.5rem #00000060;
|
||||||
|
background-color: #404040;
|
||||||
|
color: #FFFFFF;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 2em;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer img {
|
||||||
|
height: 4em;
|
||||||
|
width: 4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer a {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 800px) {
|
||||||
|
footer {
|
||||||
|
flex-direction: column;
|
||||||
|
height: unset;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1em;
|
||||||
|
padding-top: 3em;
|
||||||
|
}
|
||||||
|
footer > *:not(:first-child) {
|
||||||
|
width: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p a,
|
||||||
|
table a {
|
||||||
|
color: var(--main-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**** Access ****/
|
||||||
|
|
||||||
|
main#access {
|
||||||
|
width: calc(100% - 2em);
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 4em auto 4em auto;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-shadow: 0 0 0.5em #00000060;
|
||||||
|
padding: 1em 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
main#access h1 {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
main#access.login {
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
main#access.login h1 {
|
||||||
|
margin: 1rem 0 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
main#access.login form {
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 1em;
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
main#access.login form input,
|
||||||
|
main#access.login form button {
|
||||||
|
font-size: 1em;
|
||||||
|
border: 1px solid #C0C0C0;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.25em 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
main#access.login form .error {
|
||||||
|
font-size: 1em;
|
||||||
|
color: #C00000;
|
||||||
|
border: 1px solid #C00000;
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 0.25em 0.5em;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#user {
|
||||||
|
display: flex;
|
||||||
|
justify-content: end;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#user:hover {
|
||||||
|
color: var(--main-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
#usertext {
|
||||||
|
font-size: 0.875em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#usertext > div {
|
||||||
|
font-size: 0.75em;
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
main .number {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.deliveries form {
|
||||||
|
margin: 1em;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
align-content: center;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.abgewertet {
|
||||||
|
color: #C00000;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.overview .day {
|
||||||
|
border-radius: 4px;
|
||||||
|
padding-left: 1.5em;
|
||||||
|
background: #E0E0E0;
|
||||||
|
margin: 1em 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2em;
|
||||||
|
height: 10em;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.overview .day.today {
|
||||||
|
background-color: var(--light-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
main.overview .schedule-container {
|
||||||
|
background: #00000020;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.overview .schedule-container a {
|
||||||
|
border-radius: 4px;
|
||||||
|
color: unset;
|
||||||
|
text-decoration: none;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 1em;
|
||||||
|
transition: background-color 0.125s;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.overview .schedule-container a.cancelled > div {
|
||||||
|
color: #A00000;
|
||||||
|
text-decoration: line-through;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.overview .schedule-container a.cancelled > span {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
main.overview .schedule-container a:hover {
|
||||||
|
background-color: #00000020;
|
||||||
|
}
|
||||||
|
|
||||||
|
.min-percent {
|
||||||
|
min-width: 2.625em;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.min-kg {
|
||||||
|
min-width: 5em;
|
||||||
|
display: inline-block;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 1000px) {
|
||||||
|
main#access {
|
||||||
|
padding: 1em 0.5em;
|
||||||
|
margin-left: 0;
|
||||||
|
margin-right: 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
main.overview .day {
|
||||||
|
flex-direction: column;
|
||||||
|
height: unset;
|
||||||
|
padding-left: 0;
|
||||||
|
gap: 0.5em;
|
||||||
|
padding-top: 0.5em;
|
||||||
|
}
|
||||||
|
main.overview .day > div:first-child {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
main.overview .schedule-container {
|
||||||
|
height: unset;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
main.overview .schedule-container a {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (max-width: 650px) {
|
||||||
|
main.overview .schedule-container a {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
main.overview .schedule-container a > *:first-child {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
main.overview .schedule-container a > *:last-child {
|
||||||
|
width: 100%;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
3
www/robots.txt
Normal file
3
www/robots.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# robots.txt for access.elwig.at
|
||||||
|
User-Agent: *
|
||||||
|
Disallow: /
|
Reference in New Issue
Block a user