init
This commit is contained in:
158
web/QRgen.html
Normal file
158
web/QRgen.html
Normal file
@ -0,0 +1,158 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>FTracker</title>
|
||||
<meta name="theme-color" content="#c50e1f">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
background: #ddd;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
text-transform: uppercase;
|
||||
color: #eee;
|
||||
background: #c50e1f;
|
||||
text-align: center;
|
||||
}
|
||||
form {
|
||||
padding: 16px;
|
||||
max-width: 512px;
|
||||
margin: auto;
|
||||
}
|
||||
input {
|
||||
border: none;
|
||||
padding: 16px;
|
||||
margin: 4px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
input[type=text] {
|
||||
color: #000;
|
||||
width: calc(100% - 32px);
|
||||
}
|
||||
input[type=submit] {
|
||||
background: #c50e1f;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
}
|
||||
.print {
|
||||
display: none;
|
||||
text-align: center;
|
||||
}
|
||||
.print * {
|
||||
display: inline-block;
|
||||
margin: 42px auto;
|
||||
}
|
||||
.print .link {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
@media print {
|
||||
.print h1 {
|
||||
margin-top: 64px;
|
||||
color: #000;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main id="formView">
|
||||
<h1>FTracker<br>Door Sign Genrator</h1>
|
||||
<form id="roomform">
|
||||
<label>
|
||||
Room Nr/Name:<br>
|
||||
<input type="text" name="room" id="room" placeholder="123" required>
|
||||
</label>
|
||||
<input type="submit" value="Print">
|
||||
</form>
|
||||
</main>
|
||||
<main id="printA" class="print">
|
||||
<h1 class="title"></h1><br>
|
||||
<div class="qr"></div><br>
|
||||
<span class="link"></span><br>
|
||||
<span>
|
||||
Made with FTracker<br>
|
||||
https://git.fasttube.de/FaSTTUBe/ftracker<br>
|
||||
© 2020 Oskar / FaSTTUBe
|
||||
</span>
|
||||
</main>
|
||||
<main id="printD" class="print">
|
||||
<h1 class="title"></h1><br>
|
||||
<div class="qr"></div><br>
|
||||
<span class="link"></span><br>
|
||||
<span>
|
||||
Made with FTracker<br>
|
||||
https://git.fasttube.de/FaSTTUBe/ftracker<br>
|
||||
© 2020 Oskar / FaSTTUBe
|
||||
</span>
|
||||
</main>
|
||||
<script src="/qrcodejs/qrcode.min.js"></script>
|
||||
<script>
|
||||
var fv = document.getElementById('formView')
|
||||
var pa = document.getElementById('printA')
|
||||
var pd = document.getElementById('printD')
|
||||
|
||||
var rform = document.getElementById('roomform')
|
||||
rform.onsubmit = function(e) {
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
var room = e.srcElement[0].value
|
||||
|
||||
writePage(pa, room, 'arrival')
|
||||
writePage(pd, room, 'departure')
|
||||
|
||||
printPage(pa, 'ftracker-arrival-'+room)
|
||||
printPage(pd, 'ftracker-departure-'+room)
|
||||
|
||||
}
|
||||
|
||||
function writePage(el, room, type) {
|
||||
|
||||
var base = location.href.split('/').slice(0,3).join('/')
|
||||
|
||||
var url = base + '/?' + type + '=' + room
|
||||
|
||||
var title = el.querySelector('.title')
|
||||
var qr = el.querySelector('.qr')
|
||||
var link = el.querySelector('.link')
|
||||
|
||||
title.innerHTML =
|
||||
'Scan here to log ' + type + '<br> in room ' + room
|
||||
link.innerHTML = url
|
||||
qr.innerHTML = ''
|
||||
new QRCode(qr, {
|
||||
text: url,
|
||||
width: 320,
|
||||
height: 320
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function printPage(el, name) {
|
||||
|
||||
var t = document.title
|
||||
document.title = name
|
||||
|
||||
fv.style.display = 'none'
|
||||
el.style.display = 'block'
|
||||
|
||||
window.print()
|
||||
|
||||
el.style.display = 'none'
|
||||
fv.style.display = 'block'
|
||||
|
||||
document.title = t
|
||||
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
BIN
web/favicon.ico
Normal file
BIN
web/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
53
web/index.html
Normal file
53
web/index.html
Normal file
@ -0,0 +1,53 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Schnitzeljagt</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="theme-color" content="#c50e1f">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<link href="style.css" rel="stylesheet" type="text/css">
|
||||
<script>
|
||||
// 1st script, prepares values needed for writing document
|
||||
function getParams() {
|
||||
var qparams = document.location.search.substr(1)
|
||||
if (qparams == "") return {}
|
||||
qparams = qparams.split('&')
|
||||
var qps = {}
|
||||
for (var qparam of qparams) {
|
||||
var vals = qparam.split('=')
|
||||
qps[vals[0]] = vals[1] || null
|
||||
}
|
||||
return qps
|
||||
}
|
||||
var qp = getParams()
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1><script>
|
||||
document.write(qp.checkpoint ? ("Checkpoint<br>" + qp.checkpoint) : 'Schnitzeljagt<br>V 0.1')
|
||||
</script></h1>
|
||||
<div id="startpage">
|
||||
This is a web app to track games at the 20J FaSTTUBe festival.<br><br>
|
||||
If you've reached this page that either means your're testing
|
||||
things or something has gone quite wrong with the URL.<br>
|
||||
In the former case: Yay it works! In the latter you should
|
||||
probably contact an admin or a dev nearby :(<br><br>
|
||||
Here are a few links for testing:<br>
|
||||
<a href="/view">View Data</a>,
|
||||
<a href="/QRgen">Door Sign Generator</a>,
|
||||
<a href="/?game=schnitzeljagd&checkpoint=42">Test Checkpoint</a>,
|
||||
<br><br>
|
||||
© 2025 made by <a target="_blank" href="mailto:o.winkels@fasttube.de">Oskar</a>
|
||||
for <a target="_blank" href="//fasttube.de">FaSTTUBe</a>.<br>
|
||||
For source code & licensing see <a href="//git.fasttube.de/FaSTTUBe/schnitzeljagt">git repo</a>
|
||||
</div>
|
||||
<form id="mainform" action="javascript:void(0);" style="display: none">
|
||||
<label>
|
||||
Vollständiger Name:<br>
|
||||
<input type="text" name="name" id="name" placeholder="Max Mustermensch" required disabled>
|
||||
</label>
|
||||
<input type="submit" value="Einchecken">
|
||||
</form>
|
||||
<script src="main.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
93
web/main.js
Normal file
93
web/main.js
Normal file
@ -0,0 +1,93 @@
|
||||
var spage = document.getElementById('startpage')
|
||||
var mform = document.getElementById('mainform')
|
||||
|
||||
if (qp.checkpoint) {
|
||||
spage.style.display = 'none'
|
||||
mform.style.display = 'block'
|
||||
}
|
||||
|
||||
// Get the name field if it was successfully entered before
|
||||
var savedName = localStorage.getItem('name')
|
||||
if (qp && qp.name) {
|
||||
// Forced Admin checkout - prefill qp name and auto-agree
|
||||
document.getElementById('name').value = qp.name.replace(/-/g, ' ').toUpperCase();
|
||||
document.getElementById('agree').checked = true
|
||||
document.getElementById('agree').parentElement.style.display = 'none'
|
||||
} else if (savedName && qp) {
|
||||
// Prefill the client's locally saved name
|
||||
document.getElementById('name').value = savedName
|
||||
}
|
||||
|
||||
if (qp.askname)
|
||||
document.getElementById('name').disabled = false
|
||||
|
||||
// 2nd script, server API communication
|
||||
var name, datetime, agreed, tested
|
||||
mform.onsubmit = function(e) {
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
var i = 0;
|
||||
name = e.srcElement[i++].value
|
||||
if (qp.edittime && qp.edittime == 1) {
|
||||
var value = e.srcElement[i++].value
|
||||
datetime = new Date(value).toISOString()
|
||||
}
|
||||
agreed = e.srcElement[i++].checked
|
||||
if (qp.action && qp.action == 'arrival')
|
||||
tested = e.srcElement[i++].checked
|
||||
|
||||
|
||||
sendMainData()
|
||||
|
||||
}
|
||||
|
||||
function sendMainData() {
|
||||
|
||||
// POST JSON. See docs/API.md
|
||||
var payload = {
|
||||
'game': qp.game,
|
||||
'checkpoint': qp.checkpoint,
|
||||
'name': name
|
||||
}
|
||||
|
||||
post("/checkin", payload)
|
||||
|
||||
}
|
||||
|
||||
function post(url, payload) {
|
||||
|
||||
console.log("Sending payload:", payload)
|
||||
|
||||
return fetch(url, {
|
||||
method: "POST",
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(payload)
|
||||
}).then(res => {
|
||||
handleResponse(res)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
function handleResponse(res) {
|
||||
|
||||
console.log("Request complete! response:", res)
|
||||
|
||||
if (Math.floor(res.status / 100) == 2) {
|
||||
|
||||
// Success
|
||||
mform = document.getElementById('mainform')
|
||||
smsg = qp.success_message ? decodeURI(qp.success_message) : "Eingecheckt. Weiter so!"
|
||||
mform.innerHTML = "<h2>"+smsg+"</h2>"
|
||||
localStorage.setItem('name', name)
|
||||
|
||||
} else {
|
||||
|
||||
// Any other generic error
|
||||
res.text().then(function (text) {
|
||||
alert(text)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
4
web/qrcodejs/.gitignore
vendored
Normal file
4
web/qrcodejs/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
.DS_Store
|
||||
|
||||
.idea
|
||||
.project
|
||||
14
web/qrcodejs/LICENSE
Normal file
14
web/qrcodejs/LICENSE
Normal file
@ -0,0 +1,14 @@
|
||||
The MIT License (MIT)
|
||||
---------------------
|
||||
Copyright (c) 2012 davidshimjs
|
||||
|
||||
Permission is hereby granted, free of charge,
|
||||
to any person obtaining a copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
|
||||
subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
1
web/qrcodejs/qrcode.min.js
vendored
Normal file
1
web/qrcodejs/qrcode.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
web/robots.txt
Normal file
2
web/robots.txt
Normal file
@ -0,0 +1,2 @@
|
||||
User-agent: *
|
||||
Disallow: /
|
||||
79
web/style.css
Normal file
79
web/style.css
Normal file
@ -0,0 +1,79 @@
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
background: #ddd;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
h1 {
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
text-transform: uppercase;
|
||||
color: #eee;
|
||||
background: #c50e1f;
|
||||
text-align: center;
|
||||
}
|
||||
form, #startpage {
|
||||
padding: 16px;
|
||||
max-width: 512px;
|
||||
margin: auto;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
margin-bottom: 16px;
|
||||
color: #444;
|
||||
}
|
||||
label.checkbox {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
}
|
||||
label span {
|
||||
width: calc(100% - 50px);
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
line-height: normal;
|
||||
}
|
||||
input {
|
||||
border: none;
|
||||
padding: 16px;
|
||||
margin: 4px 0;
|
||||
font-size: 16px;
|
||||
}
|
||||
input[type=text] {
|
||||
color: #000;
|
||||
width: calc(100% - 32px);
|
||||
}
|
||||
input[type=submit], input[type=button] {
|
||||
background: #c50e1f;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
input[type=checkbox] {
|
||||
transform: translateY(-3px);
|
||||
float: left;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
.request {
|
||||
display: block;
|
||||
position: fixed;
|
||||
background: #ddd;
|
||||
top: 16px;
|
||||
left: 16px;
|
||||
width: calc(100% - 32px);
|
||||
box-shadow: 0 1px 4px 0;
|
||||
}
|
||||
input[type=datetime-local] {
|
||||
width: calc(100% - 24px);
|
||||
padding: 12px;
|
||||
font-size: 12px;
|
||||
background: #fff;
|
||||
}
|
||||
124
web/view.css
Normal file
124
web/view.css
Normal file
@ -0,0 +1,124 @@
|
||||
html, body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
font-family: sans-serif;
|
||||
}
|
||||
header {
|
||||
height: calc(38px - 16px);
|
||||
padding: 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
main {
|
||||
height: calc(100% - 38px);
|
||||
vertical-align: top;
|
||||
}
|
||||
main > section {
|
||||
height: calc(100% - 32px);
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
main > .viewheader {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
}
|
||||
main > section.names, #nameheader {
|
||||
width: 199px;
|
||||
overflow: hidden;
|
||||
border-right: 1px solid gray;
|
||||
font-weight: bold;
|
||||
text-transform: capitalize;
|
||||
white-space: nowrap;
|
||||
}
|
||||
main > section.times, #timeheader {
|
||||
width: calc(100% - 200px);
|
||||
overflow: hidden;
|
||||
}
|
||||
.scroll {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: scroll;
|
||||
}
|
||||
.row, .row #timelabels {
|
||||
position: relative;
|
||||
height: 32px;
|
||||
background-size: calc(15 * 5px) 100%; /* segment size in minutes * PX_PER_MINUTE */
|
||||
}
|
||||
.names .row:nth-child(even) { background: #ddd; }
|
||||
.names .row:nth-child(odd) { background: #eee; }
|
||||
.times .row:nth-child(even) {
|
||||
background-image: linear-gradient(to right, #bbb 1px, #ddd 1px);
|
||||
}
|
||||
.times .row:nth-child(odd) {
|
||||
background-image: linear-gradient(to right, #bbb 1px, #eee 1px);
|
||||
}
|
||||
.row span {
|
||||
height: 18px;
|
||||
padding: 7px;
|
||||
display: inline-block;
|
||||
}
|
||||
.times span, #timeheader span {
|
||||
position: absolute;
|
||||
margin-right: 16px;
|
||||
}
|
||||
.times span {
|
||||
background: #c50e1f;
|
||||
color: #ddd;
|
||||
font-weight: bold;
|
||||
-webkit-text-stroke: .4px #c50e1f;
|
||||
}
|
||||
.times span.blue {
|
||||
background: #1205a6;
|
||||
color: #ddd;
|
||||
font-weight: bold;
|
||||
-webkit-text-stroke: .4px #1205a6;
|
||||
}
|
||||
.viewheader.row {
|
||||
height: 30px;
|
||||
background: #ddd !important;
|
||||
border-top: 1px solid gray;
|
||||
border-bottom: 1px solid gray;
|
||||
}
|
||||
.viewheader span {
|
||||
background: none !important;
|
||||
color: #000 !important;
|
||||
padding-left: 4px;
|
||||
}
|
||||
#credprompt {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 95%;
|
||||
max-width: 320px;
|
||||
margin: auto;
|
||||
background: #ddd;
|
||||
box-shadow: 0 0 0 10000px rgba(0,0,0,.75);
|
||||
}
|
||||
#credprompt h1 {
|
||||
margin: 0;
|
||||
padding: 16px;
|
||||
text-transform: uppercase;
|
||||
color: #eee;
|
||||
background: #c50e1f;
|
||||
text-align: center;
|
||||
}
|
||||
#credprompt input {
|
||||
display: block;
|
||||
border: none;
|
||||
margin: 16px auto;
|
||||
padding: 16px;
|
||||
font-size: 16px;
|
||||
}
|
||||
#credprompt input[type=text],
|
||||
#credprompt input[type=password] {
|
||||
color: #000;
|
||||
width: calc(100% - 64px);
|
||||
}
|
||||
#credprompt input[type=submit] {
|
||||
background: #c50e1f;
|
||||
text-transform: uppercase;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
width: calc(100% - 32px);
|
||||
}
|
||||
36
web/view.html
Normal file
36
web/view.html
Normal file
@ -0,0 +1,36 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>FTracker Data</title>
|
||||
<meta charset="utf-8">
|
||||
<meta name="theme-color" content="#c50e1f">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<link href="view.css" rel="stylesheet" type="text/css">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
Start: <input type="datetime-local" id="start" value="2025-08-30T08:00" onchange="renderData()">
|
||||
Ende: <input type="datetime-local" id="end" onchange="renderData()">
|
||||
Spiel: <input type="text" id="game" placeholder=".* (regex)" onchange="renderData()">
|
||||
<input type="button" value="Log Out" style="float:right" onclick="localStorage.removeItem('dataauth'); location.reload()">
|
||||
<input type="button" value="Export CSV" style="float:right; margin-right: 8px;" onclick="exportCSV()">
|
||||
</header>
|
||||
<main>
|
||||
<div class="row viewheader" id="nameheader">
|
||||
<span><b>Names</b></span>
|
||||
</div><!--
|
||||
--><div class="row viewheader" id="timeheader">
|
||||
<div id="timelabels" style="padding-right: 256px;"></div>
|
||||
</div>
|
||||
<section class="names">
|
||||
<div id="names" style="padding-bottom: 256px;"></div>
|
||||
</section><!--
|
||||
--><section class="times">
|
||||
<div class="scroll">
|
||||
<div id="times"></div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<script src="view.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
278
web/view.js
Normal file
278
web/view.js
Normal file
@ -0,0 +1,278 @@
|
||||
var PX_PER_MINUTE = 5
|
||||
|
||||
var data = null;
|
||||
|
||||
var names = document.querySelector('main #names')
|
||||
var times = document.querySelector('main #times')
|
||||
|
||||
function exportCSV() {
|
||||
|
||||
if (data == null) {
|
||||
alert('No data found.')
|
||||
return
|
||||
}
|
||||
|
||||
var startInput = document.querySelector('input#start')
|
||||
var endInput = document.querySelector('input#end')
|
||||
var gameInput = document.querySelector('input#game')
|
||||
|
||||
var startDate = new Date(startInput.value)
|
||||
var endDate = new Date(endInput.value)
|
||||
var roomRE = new RegExp(gameInput.value || '.*')
|
||||
|
||||
csv = '"ftracker-export",'
|
||||
days = []
|
||||
|
||||
var tc = new Date(startDate.getTime())
|
||||
tc.setHours(1,0,0,0)
|
||||
while (tc < endDate) {
|
||||
var isodate = tc.toISOString().split('T')[0]
|
||||
csv += ('"' + isodate + '",')
|
||||
days.push(isodate)
|
||||
tc.setDate(tc.getDate() + 1);
|
||||
}
|
||||
|
||||
csv = csv.replace(/,$/, '')
|
||||
|
||||
csv += '\n'
|
||||
|
||||
for (var [name, list] of Object.entries(data)) {
|
||||
|
||||
csv += '"' + name + '"'
|
||||
|
||||
for (day of days) {
|
||||
|
||||
csv += ',"'
|
||||
|
||||
daytexts = []
|
||||
|
||||
for (entry of list) {
|
||||
|
||||
if (entry.room.match(roomRE) == null)
|
||||
continue
|
||||
|
||||
var arrD = new Date(entry.arrival)
|
||||
var depD = entry.departure ? new Date(entry.departure) : new Date()
|
||||
|
||||
if (depD < startDate || arrD > endDate)
|
||||
continue
|
||||
|
||||
var [arrDay, arrT] = localISOTimeMinutes(arrD).split('T')
|
||||
var [depDay, depT] = localISOTimeMinutes(depD).split('T')
|
||||
|
||||
if ((arrDay == day) && (depDay == day)) {
|
||||
daytexts.push(arrT + '-' + depT + ' (' + entry.room + ')')
|
||||
} else if (arrDay == day) {
|
||||
daytexts.push(arrT + '-... (' + entry.room + ')')
|
||||
} else if (depDay == day) {
|
||||
daytexts.push('...-' + depT + ' (' + entry.room + ')')
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
csv += daytexts.join('\n')
|
||||
|
||||
csv += '"'
|
||||
|
||||
}
|
||||
|
||||
csv += '\n'
|
||||
|
||||
}
|
||||
|
||||
var element = document.createElement('a');
|
||||
element.setAttribute('href', 'data:text/plain;charset=utf-8,' + encodeURIComponent(csv));
|
||||
element.setAttribute('download', 'ftracker-export.csv');
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
|
||||
}
|
||||
|
||||
function renderData() {
|
||||
|
||||
if (data == null) {
|
||||
alert('No data found.')
|
||||
return
|
||||
}
|
||||
|
||||
names = document.querySelector('main #names')
|
||||
times = document.querySelector('main #times')
|
||||
|
||||
names.innerHTML = ''
|
||||
times.innerHTML = ''
|
||||
|
||||
var startInput = document.querySelector('input#start')
|
||||
var endInput = document.querySelector('input#end')
|
||||
var gameInput = document.querySelector('input#game')
|
||||
|
||||
var startDate = new Date(startInput.value)
|
||||
var endDate = new Date(endInput.value)
|
||||
var gameRE = new RegExp(gameInput.value || '.*')
|
||||
|
||||
// Clear everything below hours because that would lead to
|
||||
// misalignments with the day grid
|
||||
startDate.setMinutes(0,0,0)
|
||||
|
||||
var tc = new Date(startDate.getTime())
|
||||
var content = ''
|
||||
while (tc < endDate) {
|
||||
var h = tc.getHours()
|
||||
var m = tc.getMinutes()
|
||||
var t = (h == 0) ?
|
||||
'<b>'+tc.getDate()+'.'+(tc.getMonth()+1)+'.</b>' :
|
||||
h+':'+m.toString().padStart(2, "0")
|
||||
var left = ((tc - startDate) / (1000 * 60))
|
||||
content += '<span style="left:'+(PX_PER_MINUTE*left)+'px;">'+t+'</span>'
|
||||
tc.setTime(tc.getTime() + (15*60*1000));
|
||||
}
|
||||
var timeheader = document.getElementById('timelabels')
|
||||
timeheader.innerHTML = content
|
||||
|
||||
var viewwidth = ((endDate - startDate) / (1000 * 60))
|
||||
timeheader.style.width = (PX_PER_MINUTE * viewwidth) + 'px'
|
||||
times.style.width = (PX_PER_MINUTE * viewwidth) + 'px'
|
||||
|
||||
var rowCount = 0
|
||||
|
||||
for (var [name, list] of Object.entries(data)) {
|
||||
|
||||
var row = document.createElement('div')
|
||||
row.classList.add('row')
|
||||
|
||||
var rowHasBlock = false
|
||||
|
||||
for (entry of list) {
|
||||
|
||||
if (entry.game.match(game) == null)
|
||||
continue
|
||||
|
||||
var arrD = new Date(entry.arrival)
|
||||
var depD = new Date(arrD) + 1000 * 60 * 5
|
||||
|
||||
if (depD < startDate || arrD > endDate)
|
||||
continue
|
||||
|
||||
rowHasBlock = true
|
||||
|
||||
// Minutes since start date / beginning
|
||||
var arr = (arrD - startDate) / (1000 * 60)
|
||||
var dep = (depD - startDate) / (1000 * 60)
|
||||
var dur = dep - arr
|
||||
|
||||
var block = document.createElement('span')
|
||||
block.innerHTML = entry.checkpoint
|
||||
block.style.left = (PX_PER_MINUTE * arr) + 'px' // 1px/min
|
||||
block.style.width = (PX_PER_MINUTE * Math.max(0,(dur-14))) + 'px' // 1px/min
|
||||
if (entry.game == "zeitfahren")
|
||||
block.classList.add('blue')
|
||||
|
||||
row.appendChild(block)
|
||||
|
||||
}
|
||||
|
||||
if (rowHasBlock) {
|
||||
var vname = name.replace(/-/g, ' ')
|
||||
names.innerHTML += '<div class="row"><span>'+vname+'</span></div>'
|
||||
times.appendChild(row)
|
||||
rowCount += 12
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//var viewheight = rowCount * 32;
|
||||
//times.style.height = viewheight + 'px'
|
||||
|
||||
var tw = document.querySelector('main .scroll')
|
||||
tw.scrollLeft = tw.scrollWidth
|
||||
|
||||
}
|
||||
|
||||
function saveData(rdata) {
|
||||
|
||||
data = rdata.reduce((acc, entry) => {
|
||||
var name = entry.name
|
||||
delete entry.name
|
||||
acc[name] = [...acc[name] || [], entry];
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
console.log(data)
|
||||
renderData()
|
||||
|
||||
}
|
||||
|
||||
function submitCredentials() {
|
||||
var cp = document.querySelector('#credprompt')
|
||||
var user = cp.querySelector('#user').value
|
||||
var pass = cp.querySelector('#pass').value
|
||||
cp.remove()
|
||||
var auth = btoa(user + ":" + pass)
|
||||
localStorage.setItem('dataauth', auth)
|
||||
loadData()
|
||||
}
|
||||
|
||||
function loadData() {
|
||||
|
||||
var auth = localStorage.getItem('dataauth')
|
||||
if (auth == null) {
|
||||
var prompt = document.createElement('div')
|
||||
prompt.id = 'credprompt'
|
||||
prompt.innerHTML = '<h1>Credentials Required</h1>\
|
||||
<input type="text" id="user" placeholder="username" onkeydown="if (event.keyCode == 13) {submitCredentials()}">\
|
||||
<input type="password" id="pass" placeholder="password" onkeydown="if (event.keyCode == 13) {submitCredentials()}">\
|
||||
<input type="submit" onclick="submitCredentials()">'
|
||||
document.body.appendChild(prompt)
|
||||
document.querySelector('#credprompt #user').focus()
|
||||
return // Abort load, wait for submit
|
||||
}
|
||||
|
||||
var headers = new Headers()
|
||||
headers.append('Authorization', 'Basic ' + auth)
|
||||
|
||||
var fetchopts = {
|
||||
method: 'GET',
|
||||
headers: headers
|
||||
}
|
||||
|
||||
fetch('/data', fetchopts)
|
||||
.then(res => {
|
||||
if (Math.floor(res.status / 100) == 2)
|
||||
return res.json()
|
||||
else
|
||||
localStorage.removeItem('dataauth')
|
||||
res.text().then(function (text) {
|
||||
alert(text)
|
||||
location.reload()
|
||||
})
|
||||
})
|
||||
.then(rdata => saveData(rdata))
|
||||
|
||||
}
|
||||
|
||||
function localISOTimeMinutes(date) {
|
||||
|
||||
var tzoffset = date.getTimezoneOffset() * 60000; //offset in milliseconds
|
||||
var localISOTime = (new Date(date - tzoffset)).toISOString().slice(0, -1);
|
||||
|
||||
return localISOTime.split(':').slice(0,2).join(':')
|
||||
|
||||
}
|
||||
|
||||
var now = new Date()
|
||||
var startDate = new Date()
|
||||
startDate.setDate(now.getDate())
|
||||
startDate.setHours(8,0,0,0)
|
||||
document.querySelector('input#start').value = localISOTimeMinutes(startDate)
|
||||
document.querySelector('input#end').value = localISOTimeMinutes(now)
|
||||
|
||||
var scrollbox = document.querySelector('.scroll')
|
||||
var timehead = document.querySelector('#timeheader')
|
||||
var namebox = document.querySelector('section.names')
|
||||
scrollbox.onscroll = function() {
|
||||
timehead.scrollLeft = scrollbox.scrollLeft
|
||||
namebox.scrollTop = scrollbox.scrollTop
|
||||
}
|
||||
|
||||
loadData()
|
||||
Reference in New Issue
Block a user