Compare commits

...

14 Commits

18 changed files with 330 additions and 1279 deletions

View File

@ -1,2 +1,4 @@
# Elwig REST API
# Elwig website
https://elwig.at/

View File

@ -19,15 +19,3 @@ function authenticate(): void {
http_401_unauthorized();
}
}
function authenticate_client(string $client): void {
global $CLIENT_CREDENTIALS;
$credentials = $CLIENT_CREDENTIALS[$client];
if (!array_key_exists('PHP_AUTH_USER', $_SERVER) ||
!array_key_exists('PHP_AUTH_PW', $_SERVER) ||
!array_key_exists($_SERVER['PHP_AUTH_USER'], $credentials) ||
$_SERVER['PHP_AUTH_PW'] !== $credentials[$_SERVER['PHP_AUTH_USER']])
{
http_401_unauthorized();
}
}

View File

@ -1,32 +1,9 @@
<?php
global $GITEA_TOKEN;
global $CREDENTIALS;
global $CLIENT_CREDENTIALS;
global $CLIENT_ACCESS;
global $COMBINED_ACCESS;
$GITEA_TOKEN = 'token';
$CREDENTIALS = [
'username' => 'password',
];
$CLIENT_CREDENTIALS = [
'name' => [
'username' => 'password',
],
];
$CLIENT_ACCESS = [
'WGX' => [
'name' => 'Winzergenossenschaft',
'api' => 'https://example.com/elwig/api/v1',
],
];
$COMBINED_ACCESS = [
'HOLDING' => [
'name' => 'Name',
'clients' => ['WGX'],
],
];

View File

@ -1,100 +0,0 @@
<?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 ($_SERVER['PATH_INFO'] !== "/$client[id]/") {
header('Status: 308');
header("Location: /access/$client[id]/");
exit();
} else if ($client === null) {
header('Status: 404');
header('Content-Length: 0');
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-AT">
<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>

View File

@ -1,267 +0,0 @@
<?php
require ".php/format.inc";
require ".php/auth.inc";
require ".php/credentials.inc";
global $CLIENT_CREDENTIALS;
$clients = array_keys($CLIENT_CREDENTIALS);
$format = get_fmt();
function get_zip_meta($path): array {
$meta = 'null';
$fp = fopen($path, "rb");
$zipHdr1 = fread($fp, 30);
if (strlen($zipHdr1) !== 30 || !str_starts_with($zipHdr1, "PK\x03\x04") ||
$zipHdr1[8] !== "\x00" || $zipHdr1[9] !== "\x00" || $zipHdr1[26] !== "\x07" || $zipHdr1[27] !== "\x00")
{
fclose($fp);
return array($meta, null, null);
}
$extraFieldLen1 = unpack("v", substr($zipHdr1, 28, 2))[1];
$name1 = fread($fp, 7);
fseek($fp, $extraFieldLen1, SEEK_CUR);
$data1 = fread($fp, unpack("V", substr($zipHdr1, 18, 4))[1]);
if ($name1 !== "version" || !str_starts_with($data1, "elwig:")) {
fclose($fp);
return array($meta, null, null);
}
$version = (int)substr($data1, 6);
$zipHdr2 = fread($fp, 30);
if (strlen($zipHdr2) !== 30 || !str_starts_with($zipHdr2, "PK\x03\x04") ||
$zipHdr2[8] !== "\x00" || $zipHdr2[9] !== "\x00" || $zipHdr2[26] !== "\x09" || $zipHdr2[27] !== "\x00")
{
fclose($fp);
return array($meta, $version, null);
}
$extraFieldLen2 = unpack("v", substr($zipHdr2, 28, 2))[1];
$name2 = fread($fp, 9);
fseek($fp, $extraFieldLen2, SEEK_CUR);
if ($name2 !== "meta.json") {
fclose($fp);
return array($meta, $version, null);
}
$meta = fread($fp, unpack("V", substr($zipHdr2, 18, 4))[1]);
$files = "{";
$first = true;
while (!feof($fp)) {
$zipHdr3 = fread($fp, 30);
if (strlen($zipHdr3) !== 30 || !str_starts_with($zipHdr3, "PK\x03\x04"))
continue;
$compSize = unpack("V", substr($zipHdr3, 18, 4))[1];
$uncompSize = unpack("V", substr($zipHdr3, 22, 4))[1];
$name = fread($fp, unpack("v", substr($zipHdr3, 26, 2))[1]);
$crc = unpack("V", substr($zipHdr3, 14, 4))[1];
fseek($fp, $compSize, SEEK_CUR);
$hex = substr("00000000" . dechex($crc), -8);
if (!$first) $files .= ", ";
$files .= "\"$name\": {\"compressed_size\": $compSize, \"uncompressed_size\": $uncompSize, \"crc32\": \"$hex\"}";
$first = false;
}
$files .= "}";
fclose($fp);
return array($meta, $version, $files);
}
$path = $_SERVER['PATH_INFO'];
if ($path == '') {
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
header('Status: 405');
header('Allow: GET');
if ($format === 'text') {
header('Content-Type: text/plain; charset=UTF-8');
header('Content-Length: 23');
echo "405 Method Not Allowed\n";
} else if ($format === 'json') {
header('Content-Type: application/json; charset=UTF-8');
header('Content-Length: 48');
echo "{\"errors\": [{\"message\": \"Method not allowed\"}]}\n";
} else {
header('Content-Type: text/html; charset=UTF-8');
header('Content-Length: 0');
}
exit();
}
if ($format === 'text') {
header('Content-Type: text/plain; charset=UTF-8');
foreach ($clients as $c)
echo "$c\n";
} else if ($format === 'json') {
header('Content-Type: application/json; charset=UTF-8');
echo "{\"data\": [";
$first = true;
foreach ($clients as $c) {
if (!$first) echo ",";
echo "\n {\"name\": \"$c\"}";
$first = false;
}
echo "\n]}\n";
} else if ($format === 'html') {
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-AT">
<head>
<meta charset="UTF-8"/>
<title>Mandanten - Elwig</title>
<meta name="description" content="Elektronische Winzergenossenschaftsverwaltung"/>
<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"/>
</head>
<body>
<main>
<section>
<h3>Mandanten</h3>
<p class="center"><a href="/">Startseite</a></p>
<table>
<thead><tr><th>Name</th></tr></thead>
<tbody>
<?php foreach ($clients as $c) {
echo " <tr><td><a href='clients/$c'>$c</a></td></tr>\n";
} ?>
</tbody>
</table>
<p class="center"><a href="?format=json">JSON-Format</a></p>
</section>
</main>
</body>
</html>
<?php }
exit();
}
foreach ($clients as $c) {
if ($path !== "/$c" && !str_starts_with($path, "/$c/"))
continue;
header('Content-Type: text/plain; charset=UTF-8');
authenticate_client($c);
if ($path === "/$c") {
header('Status: 308');
header("Location: $c/");
header('Content-Length: 23');
exit("308 Permanent Redirect\n");
} elseif ($path === "/$c/") {
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
header("Status: 405");
header("Allow: GET");
header('Content-Length: 23');
exit("405 Method Not Allowed\n");
}
header('Content-Type: application/json; charset=UTF-8');
echo "{\"data\": [\n";
$first = true;
foreach (scandir(".data/clients/$c/") as $file) {
if (str_starts_with($file, ".") || str_ends_with($file, ".php") || str_ends_with($file, ".inc")) continue;
if (!$first) echo ",\n";
$path = ".data/clients/$c/$file";
$size = filesize($path);
$url = "https://elwig.at/clients/$c/$file";
$mod = date(DATE_ATOM, filemtime($path));
$cre = date(DATE_ATOM, filectime($path));
$datetime = "null";
$zwstid = "null";
if (str_ends_with($file, ".elwig.zip") && substr_count($file, "_") === 2) {
$parts = explode("_", substr($file, 0, -10));
$time = str_replace("-", ":", $parts[1]);
$dt = DateTime::createFromFormat("Y-m-d H:i:s", "$parts[0] $time");
$datetime = '"' . $dt->format(DateTimeInterface::RFC3339) . '"';
$zwstid = "\"$parts[2]\"";
}
list($meta, $version, $files) = get_zip_meta($path);
$files ??= "null";
$version ??= "null";
echo " {\"name\": \"$file\", \"timestamp\": $datetime, \"zwstid\": $zwstid, \"meta\": $meta, \"files\": $files, " .
"\"version\": $version, \"url\": \"$url\", \"size\": $size, \"created\": \"$cre\", \"modified\": \"$mod\"}";
$first = false;
}
echo "\n]}\n";
exit();
}
$file = substr($path, strlen("/$c/"));
$path = ".data/clients/$c/$file";
if (!preg_match_all('/[A-Za-z0-9_.-]+/', $file) && !($file === '*' && $_SERVER['REQUEST_METHOD'] === 'DELETE')) {
header('Status: 400');
header('Content-Length: 16');
exit("400 Bad Request\n");
} elseif ($_SERVER['REQUEST_METHOD'] === 'GET') {
$size = filesize($path);
if ($size === false) {
header('Status: 404');
header('Content-Length: 14');
exit("404 Not Found\n");
}
$type = mime_content_type($path);
header("Content-Type: $type");
header("Content-Disposition: attachment; filename=\"$file\"");
header("Content-Length: $size");
readfile($path);
} elseif ($_SERVER['REQUEST_METHOD'] === 'PUT') {
$putdata = fopen('php://input', 'r');
$fp = fopen($path, 'wb');
if ($fp === false) {
header("Status: 500");
header("Content-Length: 26");
exit("500 Internal Server Error\n");
}
while ($data = fread($putdata, 4096))
fwrite($fp, $data);
fclose($fp);
fclose($putdata);
header("Status: 201");
header('Content-Length: 12');
exit("201 Created\n");
} elseif ($_SERVER['REQUEST_METHOD'] === 'DELETE') {
if ($file === '*') {
foreach (scandir(".data/clients/$c/") as $f) {
if (str_starts_with($f, ".") || str_ends_with($f, ".php") || str_ends_with($f, ".inc")) continue;
if (unlink(".data/clients/$c/$f") === false) {
header("Status: 500");
exit("500 Internal Server Error\n");
}
echo "Deleted $f\n";
}
} else if (!is_file($path)) {
header("Status: 404");
header("Content-Length: 14");
exit("404 Not Found\n");
} else if (unlink($path) === false) {
header("Status: 500");
exit("500 Internal Server Error\n");
}
exit("200 OK\n");
} else {
header("Status: 405");
header("Allow: GET, PUT, DELETE");
header("Content-Length: 23");
exit("405 Method Not Allowed\n");
}
exit();
}
header("Status: 404");
if ($format === 'text') {
header('Content-Type: text/plain; charset=UTF-8');
header('Content-Length: 14');
echo "404 Not Found\n";
} else if ($format === 'json') {
header('Content-Type: application/json; charset=UTF-8');
header('Content-Length: 39');
echo "{\"errors\": [{\"message\": \"Not found\"}]}\n";
} else {
header('Content-Type: text/html; charset=UTF-8');
header('Content-Length: 0');
}
exit();

View File

@ -1,11 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml" lang="de-AT" prefix="og: https://ogp.me/ns#">
<html xmlns="http://www.w3.org/1999/xhtml" lang="de" prefix="og: https://ogp.me/ns#">
<head>
<meta charset="UTF-8"/>
<title>Elwig</title>
<title>Elwig - Elektronische Winzergenossenschaftsverwaltung</title>
<meta name="description" content="Elektronische Winzergenossenschaftsverwaltung"/>
<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"/>
<link rel="stylesheet" href="/res/style.css?v=2025-01-12"/>
<link rel="alternate" href="/en/" hreflang="en"/>
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<meta name="theme-color" content="#A040C0"/>
<meta name="mobile-web-app-capable" content="yes"/>
@ -20,7 +21,7 @@
<meta property="og:image:type" content="image/jpeg"/>
<meta property="og:image:width" content="1200"/>
<meta property="og:image:height" content="630"/>
<meta property="og:locale" content="de_AT"/>
<meta property="og:locale" content="de"/>
<meta property="og:ttl" content="60"/>
<meta name="twitter:card" content="summary_large_image"/>
</head>
@ -28,16 +29,18 @@
<header>
<nav class="index">
<div>
<a href="https://elwig.at/"><img src="/res/elwig.png" alt="Elwig Logo"/></a>
<a href="https://elwig.at/de/"><img src="/res/elwig.png" alt="Elwig Logo"/></a>
</div>
<ul>
<li><a href="/#">Start</a></li>
<li><a href="/#about">Über</a></li>
<li><a href="/#clients">Genossenschaften</a></li>
<li><a href="#">Start</a></li>
<li><a href="#about">Über</a></li>
<li><a href="#clients">Genossenschaften</a></li>
<li><a href="/files/">Downloads</a></li>
<li><a href="https://git.necronda.net/winzer/">Quellcode</a></li>
</ul>
<div/>
<div>
<a href="/en/" class="flag"><div/></a>
</div>
</nav>
</header>
@ -105,9 +108,9 @@
<div class="filling"/>
<div>
<h4>WG Weinland</h4>
<h5><span>Winzergenossenschaft Weinland,</span> <span>mit dem Sitz in Großinzersdorf,</span><br/><span>reg. Gen.m.b.H.</span></h5>
<h5><span>Winzergenossenschaft Weinland,</span> <span>mit dem Sitz in Groß-Inzersdorf,</span><br/><span>reg. Gen.m.b.H.</span></h5>
<h6>Zweigstellen:</h6>
<div class="branches">Großinzersdorf</div>
<div class="branches">Groß-Inzersdorf</div>
<p class="link"></p>
<div class="edge"/><div class="edge"/><div class="edge"/><div class="edge"/>
</div>

149
www/en/index.xhtml Normal file
View File

@ -0,0 +1,149 @@
<?xml version="1.0" encoding="UTF-8"?>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" prefix="og: https://ogp.me/ns#">
<head>
<meta charset="UTF-8"/>
<title>Elwig - Electronic Management for Vintners' Cooperatives</title>
<meta name="description" content="Electronic Management for Vintners' Cooperatives"/>
<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?v=2025-01-12"/>
<link rel="alternate" href="/de/" hreflang="de"/>
<meta name="viewport" content="width=device-width,initial-scale=1.0"/>
<meta name="theme-color" content="#A040C0"/>
<meta name="mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-capable" content="yes"/>
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"/>
<meta property="og:site_name" content="Elwig"/>
<meta property="og:title" content="Elwig"/>
<meta property="og:description" content="Electronic Management for Vintners' Cooperatives"/>
<meta property="og:type" content="website"/>
<meta property="og:url" content="https://elwig.at/"/>
<meta property="og:image" content="https://elwig.at/res/images/preview.jpg"/>
<meta property="og:image:type" content="image/jpeg"/>
<meta property="og:image:width" content="1200"/>
<meta property="og:image:height" content="630"/>
<meta property="og:locale" content="de"/>
<meta property="og:ttl" content="60"/>
<meta name="twitter:card" content="summary_large_image"/>
</head>
<body class="header-footer" style="background-color: var(--bg-color);">
<header>
<nav class="index">
<div>
<a href="https://elwig.at/en/"><img src="/res/elwig.png" alt="Elwig Logo"/></a>
</div>
<ul>
<li><a href="#">Start</a></li>
<li><a href="#about">About</a></li>
<li><a href="#clients">Cooperatives</a></li>
<li><a href="/files/">Downloads</a></li>
<li><a href="https://git.necronda.net/winzer/">Source&#160;Code</a></li>
</ul>
<div>
<a href="/de/" class="flag"><div/></a>
</div>
</nav>
</header>
<main>
<section class="home background">
<a href="#about">
<div>
<img src="/res/elwig.png" alt="Elwig"/>
<div style="max-width: 16em; margin-bottom: 0.5em;">
<h1>Elwig</h1>
<h2>Electronic Management for Vintners' Cooperatives</h2>
</div>
</div>
</a>
<div class="blur bottom"/>
</section>
<section class="about">
<span id="about"/>
<h3>Elwig</h3>
<p>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore etdolore magna aliquyam erat, sed diam voluptua.
At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
</p>
<p>
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore etdolore magna aliquyam erat, sed diam voluptua.
At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
</p>
</section>
<section class="picture-1 background">
<div class="blur top"/>
<div class="blur bottom"/>
</section>
<section class="clients">
<span id="clients"/>
<h3>The Cooperatives</h3>
<div class="container background">
<div>
<h4>WG Matzen</h4>
<h5><span>Winzergenossenschaft</span> <span>für Matzen und Umgebung</span><br/><span>reg. Gen.m.b.H.</span></h5>
<h6>Branches:</h6>
<div class="branches">Matzen</div>
<p class="link">
<a href="https://winzermatzen.at/">winzermatzen.at</a>
</p>
<div class="edge"/><div class="edge"/><div class="edge"/><div class="edge"/>
</div>
<div class="filling"/>
<div>
<h4>Winzerkeller im Weinviertel</h4>
<h5><span>Winzerkeller im Weinviertel</span><br/><span>reg. Gen.m.b.H.</span></h5>
<h6>Branches:</h6>
<div class="branches">Wolkersdorf, Haugsdorf, Sitzendorf</div>
<p class="link">
<a href="http://www.winzerkeller.eu/">winzerkeller.eu</a>
</p>
<div class="edge"/><div class="edge"/><div class="edge"/><div class="edge"/>
</div>
<div class="filling"/>
<div>
<h4>WG Weinland</h4>
<h5><span>Winzergenossenschaft Weinland,</span> <span>mit dem Sitz in Groß-Inzersdorf,</span><br/><span>reg. Gen.m.b.H.</span></h5>
<h6>Branches:</h6>
<div class="branches">Groß-Inzersdorf</div>
<p class="link"></p>
<div class="edge"/><div class="edge"/><div class="edge"/><div class="edge"/>
</div>
<div class="filling"/>
<div>
<h4>WG Baden</h4>
<h5><span>Winzergenossenschaft</span> <span>Baden - Bad Vöslau</span><br/><span>reg. Gen.m.b.H.</span></h5>
<h6>Zweigstellen:</h6>
<div class="branches">Baden</div>
<p class="link">
<a href="http://www.wg-baden.at/">wg-baden.at</a>
</p>
<div class="edge"/><div class="edge"/><div class="edge"/><div class="edge"/>
</div>
</div>
</section>
<section class="picture-2 background">
<div class="blur top"/>
</section>
</main>
<footer>
<a href="https://elwig.at/"><img src="/res/elwig.png" alt="Elwig"/></a>
<p>
<strong>Imprint</strong><br/>
Lorenz Stechauner, Thomas Hilscher<br/>
Austria<br/>
</p>
<p>
<strong>Contact</strong><br/>
E-Mail: <a href="mailto:contact@necronda.net">contact@necronda.net</a><br/>
</p>
</footer>
</body>
</html>

28
www/files/debian/.update.sh Executable file
View File

@ -0,0 +1,28 @@
#!/bin/bash
set -x
dpkg-scanpackages --arch all pool/ > dists/stable/main/binary-all/Packages
cat dists/stable/main/binary-all/Packages | gzip -9 > dists/stable/main/binary-all/Packages.gz
do_hash() {
HASH_NAME=$1
HASH_CMD=$2
echo "${HASH_NAME}:"
for f in $(find dists/stable -type f -not -name '.*' -not -name 'Release'); do
echo " $(${HASH_CMD} $f | cut -d" " -f1) $(wc -c $f | sed 's|dists/stable/||')"
done
}
cat > dists/stable/Release << EOF
Origin: Elwig
Suite: stable
Codename: stable
Version: 1.0
Architectures: all
Components: main
Date: $(date -Ru)
EOF
do_hash "MD5Sum" "md5sum" >> dists/stable/Release
do_hash "SHA1" "sha1sum" >> dists/stable/Release
do_hash "SHA256" "sha256sum" >> dists/stable/Release

View File

View File

@ -5,22 +5,40 @@ require "../.php/auth.inc";
if ($_SERVER['REQUEST_METHOD'] === 'PUT') {
authenticate();
header('Content-Type: text/plain; charset=UTF-8');
$name = substr($_SERVER['PATH_INFO'], 1);
if (str_contains($name, "..") || str_contains($name, "/")) {
header('Status: 403');
header('Content-Type: text/plain; charset=UTF-8');
header('Content-Length: 14');
exit("403 Forbidden\n");
} else if (!isset($_SERVER['HTTP_CONTENT_LENGTH'])) {
header('Status: 411');
header('Content-Length: 20');
exit("411 Length Required\n");
}
$upload = fopen("php://input", "r");
$fp = fopen($name, "wb+");
$fp = fopen("/tmp/upload-$name", "wb+");
if (!$upload || !$fp) {
fclose($fp);
fclose($upload);
header('Status: 500');
header('Content-Length: 26');
exit("500 Internal Server Error\n");
}
while ($data = fread($upload, 4096)) fwrite($fp, $data);
fclose($fp);
fclose($upload);
if (!rename("/tmp/upload-$name", $name)) {
header('Status: 500');
header('Content-Length: 26');
exit("500 Internal Server Error\n");
}
header('Status: 201');
header('Content-Type: text/plain; charset=UTF-8');
header('Content-Length: 12');
exit("201 Created\n");
} else if ($_SERVER['REQUEST_METHOD'] !== 'GET' && $_SERVER['REQUEST_METHOD'] !== 'HEAD') {
@ -120,7 +138,7 @@ if ($format === 'json') {
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-AT" prefix="og: https://ogp.me/ns#">
<html xmlns="http://www.w3.org/1999/xhtml" lang="de" prefix="og: https://ogp.me/ns#">
<head>
<meta charset="UTF-8"/>
<title>Downloads - Elwig</title>
@ -141,7 +159,7 @@ if ($format === 'json') {
<meta property="og:image:type" content="image/jpeg"/>
<meta property="og:image:width" content="1200"/>
<meta property="og:image:height" content="630"/>
<meta property="og:locale" content="de_AT"/>
<meta property="og:locale" content="de"/>
<meta property="og:ttl" content="60"/>
<meta name="twitter:card" content="summary_large_image"/>
</head>

View File

@ -1,9 +1,26 @@
<?php
header("Content-Length: 0");
switch ($_SERVER['PATH_INFO']) {
case '':
case '/':
$lang = 'de'; // prefer german
if (isset($_SERVER['HTTP_ACCEPT_LANGUAGE'])) {
foreach (explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']) as $pref) {
$l = substr($pref, 0, 2);
if ($l === 'de') {
$lang = 'de';
break; // force german
} else if ($l === 'en') {
$lang = 'en'; // use english only, if user specifically asks for it
}
}
}
header("Status: 303");
header("Location: /$lang/");
break;
case '/changelog':
header("Status: 303");
header("Location: https://git.necronda.net/winzer/elwig/src/branch/main/CHANGELOG.md");
header("Location: https://git.necronda.net/winzer/elwig/src/branch/main/CHANGELOG.md#changelog");
break;
case '/vcs':
case '/git':
@ -13,6 +30,22 @@ switch ($_SERVER['PATH_INFO']) {
header("Status: 303");
header("Location: https://git.necronda.net/winzer/");
break;
case '/access':
header("Status: 303");
header("Location: https://access.elwig.at/");
break;
case '/clients':
header("Status: 303");
header("Location: https://sync.elwig.at/");
break;
default:
header("Status: 404");
if (str_starts_with($_SERVER['PATH_INFO'], '/access/')) {
header("Status: 308");
header("Location: https://access.elwig.at/" . substr($_SERVER['PATH_INFO'], 8));
} else if (str_starts_with($_SERVER['PATH_INFO'], '/clients/')) {
header("Status: 308");
header("Location: https://sync.elwig.at/" . substr($_SERVER['PATH_INFO'], 9));
} else {
header("Status: 404");
}
}

View File

@ -1,206 +0,0 @@
"use strict";
function getCredentialsUsername(client) {
return window.localStorage.getItem(`${CLIENT}/${client}/username`);
}
function getCredentialsPassword(client) {
return window.localStorage.getItem(`${CLIENT}/${client}/password`);
}
function getBasicAuth(client) {
return {
'Authorization': 'Basic ' + btoa(getCredentialsUsername(client) + ':' + getCredentialsPassword(client)),
}
}
async function _get(client, path) {
const res = await fetch(`${CLIENTS[client]['api']}${path}`, {
method: 'GET',
headers: {...getBasicAuth(client)},
});
const json = await res.json();
if (!res.ok) throw new ApiError(res.status, json['message']);
return json;
}
async function get(client, path) {
return (await _get(client, path))['data'];
}
async function getWineVarieties(client) {
return Object.fromEntries((await get(client, '/wine/varieties')).map(item => [item['sortid'], item]));
}
async function getWineQualityLevels(client) {
return Object.fromEntries((await get(client, '/wine/quality_levels')).map(item => [item['qualid'], item]));
}
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 load(client) {
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.WINE_VARIETIES = await getWineVarieties(client);
window.WINE_QUALITY_LEVELS = await getWineQualityLevels(client);
return true;
} catch (e) {
if (form) {
window.localStorage.removeItem(`${CLIENT}/${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 = `#/${client}/login`;
}
return false;
}
}
async function init() {
//await load();
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.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 = '';
let client = null;
for (const id in CLIENTS) {
if (hash.startsWith(`#/${id}/`) || hash === `#/${id}`) {
client = id;
}
}
if (client === null) {
window.location.hash = `#/${Object.keys(CLIENTS)[0]}`;
return;
}
nav.children[Object.keys(CLIENTS).indexOf(client)].className = 'active';
if ((!getCredentialsUsername(client) || !getCredentialsPassword(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="${getCredentialsUsername(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>`;
updateOverview(client).then();
} else {
window.location.hash = `#/${client}`;
}
}
function update() {
const hash = window.location.hash;
let client = null;
for (const id in CLIENTS) {
if (hash.startsWith(`#/${id}/`) || hash === `#/${id}`) {
client = id;
}
}
if (document.hidden) {
// do nothing
} else {
if (hash === `#/${client}`) {
updateOverview(client).then();
}
}
}
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) {
window.localStorage.setItem(`${CLIENT}/${form.client.value}/username`, form.username.value);
window.localStorage.setItem(`${CLIENT}/${form.client.value}/password`, form.password.value);
load(form.client.value).then(success => {
if (success) window.location.hash = `#/${form.client.value}`;
});
return false;
}

View File

@ -1,331 +0,0 @@
"use strict";
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();
});
window.addEventListener('pageshow', update)
document.addEventListener('visibilitychange', update);
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();
}

View File

@ -1,79 +0,0 @@
"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.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;
}

View File

@ -81,11 +81,63 @@ nav li.active a{
color: var(--main-color);
}
/**** Index ****/
nav a.flag {
text-decoration: none;
}
main span[id] {
position: relative;
top: -8em;
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,
@ -93,6 +145,22 @@ table a {
color: var(--main-color);
}
main section h3 {
text-align: center;
font-size: 2em;
}
.center {
text-align: center;
}
/**** Index ****/
main span[id] {
position: relative;
top: -8em;
}
main .background {
background-image: var(--img);
background-repeat: no-repeat;
@ -204,15 +272,6 @@ main section p {
text-align: justify;
}
.center {
text-align: center;
}
main section h3 {
text-align: center;
font-size: 2em;
}
main .about {
padding: 0 0 1em 0;
}
@ -331,29 +390,6 @@ table .unit {
padding-top: 0;
}
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 (min-width: 1921px) {
main .picture-1 {
height: calc(36em + 4rem);
@ -374,16 +410,6 @@ footer a {
}
@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;
}
body.header-footer {
min-height: calc(100vh + 20em);
padding-bottom: 20em;
@ -422,197 +448,3 @@ footer a {
font-size: 0.5em;
}
}
/**** 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;
}
}

View File

@ -1,4 +1,3 @@
# robots.txt for elwig.at
User-Agent: *
Disallow: /clients/
Sitemap: https://elwig.at/sitemap.xml

View File

@ -1,10 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="https://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://elwig.at/</loc>
<loc>https://elwig.at/de/</loc>
<changefreq>monthly</changefreq>
<priority>1.0</priority>
</url>
<url>
<loc>https://elwig.at/en/</loc>
<changefreq>monthly</changefreq>
<priority>0.9</priority>
</url>
<url>
<loc>https://elwig.at/files/</loc>
<changefreq>monthly</changefreq>