Compare commits
25 commits
Author | SHA1 | Date | |
---|---|---|---|
6810cc315c | |||
|
c60f596816 | ||
|
b343af8dfe | ||
|
bff08ceddf | ||
|
84371968e5 | ||
|
3a4602be36 | ||
|
8eab8ada15 | ||
|
8b4ff7df58 | ||
|
203777512a | ||
|
70ff903457 | ||
|
33f4054d63 | ||
|
dcf67c5a7b | ||
|
d6f88dacf8 | ||
|
1353351c0e | ||
|
7d5c522e6a | ||
|
c19ded10b1 | ||
|
cf59ff986f | ||
|
321731f49e | ||
|
973cce5509 | ||
|
35ee1b06e5 | ||
|
da1aca9e69 | ||
|
b58c6dca64 | ||
|
2e74a5ba3d | ||
|
7c435efbdb | ||
|
1986be119b |
13 changed files with 396 additions and 165 deletions
36
README.md
36
README.md
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
Simple choice game.
|
Simple choice game.
|
||||||
|
|
||||||
|
## Demo
|
||||||
|
|
||||||
|
https://ab.21x9.org
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
0. Install `python3` (and `pip` if needed)
|
0. Install `python3` (and `pip` if needed)
|
||||||
|
@ -28,3 +32,35 @@ Or just run `python3 ./game.py`
|
||||||
## Use
|
## Use
|
||||||
|
|
||||||
Open your browser and navigate to `http://localhost:5000` or whatever you configured as `bind` and `port` in your `config.ini`.
|
Open your browser and navigate to `http://localhost:5000` or whatever you configured as `bind` and `port` in your `config.ini`.
|
||||||
|
|
||||||
|
## Reverse Proxy
|
||||||
|
|
||||||
|
To run this game in an production environment it's strongly recommended to set up an reverse proxy upfront.
|
||||||
|
|
||||||
|
### nginx
|
||||||
|
|
||||||
|
Make sure you understand the config and replace the `location` and/or `proxy_pass` value if necessary.
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
location / {
|
||||||
|
proxy_pass http://127.0.0.1:5000/;
|
||||||
|
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
|
||||||
|
client_max_body_size 32M;
|
||||||
|
proxy_redirect off;
|
||||||
|
proxy_intercept_errors off;
|
||||||
|
proxy_read_timeout 86400s;
|
||||||
|
proxy_ignore_client_abort on;
|
||||||
|
proxy_connect_timeout 120s;
|
||||||
|
|
||||||
|
proxy_buffer_size 128k;
|
||||||
|
proxy_buffers 4 256k;
|
||||||
|
proxy_busy_buffers_size 256k;
|
||||||
|
|
||||||
|
proxy_headers_hash_max_size 512;
|
||||||
|
proxy_buffering on;
|
||||||
|
proxy_cache_bypass $http_pragma $http_authorization $cookie_nocache;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
6
ab.txt
6
ab.txt
|
@ -769,7 +769,7 @@ Zucker;Salz
|
||||||
Crushed Eis;Eiswürfel
|
Crushed Eis;Eiswürfel
|
||||||
Locken;glatte Haare
|
Locken;glatte Haare
|
||||||
im Laden kaufen;Online Shopping
|
im Laden kaufen;Online Shopping
|
||||||
Nutella mit;ohne Butter
|
Frühling;Herbst
|
||||||
Beziehung;Single-Life
|
Beziehung;Single-Life
|
||||||
Löffel;Gabel
|
Löffel;Gabel
|
||||||
Meer;Berge
|
Meer;Berge
|
||||||
|
@ -792,7 +792,7 @@ Jogginghose;Jeans
|
||||||
Samstag;Sonntag
|
Samstag;Sonntag
|
||||||
Schwitzen;Frieren
|
Schwitzen;Frieren
|
||||||
Die;das Nutella
|
Die;das Nutella
|
||||||
Android;iPhone
|
Frühling;Winter
|
||||||
Stadt;Dorf
|
Stadt;Dorf
|
||||||
McDonald's;Burger King
|
McDonald's;Burger King
|
||||||
Schokolade;Chips
|
Schokolade;Chips
|
||||||
|
@ -1035,7 +1035,7 @@ Multiplayer;Einzelspieler
|
||||||
Pizza Hawaii;keine Pizza
|
Pizza Hawaii;keine Pizza
|
||||||
Pacman;Tetris
|
Pacman;Tetris
|
||||||
Tischfußball;Tischtennis
|
Tischfußball;Tischtennis
|
||||||
Computer;Spielkonsolen
|
PC;Spielkonsolen
|
||||||
Live-Action-Rollenspiele;Online-Spiele
|
Live-Action-Rollenspiele;Online-Spiele
|
||||||
Wrestling;Mix Martial Arts
|
Wrestling;Mix Martial Arts
|
||||||
Bowling;Tennis
|
Bowling;Tennis
|
||||||
|
|
20
config.ini
20
config.ini
|
@ -3,7 +3,11 @@ port = 5000
|
||||||
bind = 0.0.0.0
|
bind = 0.0.0.0
|
||||||
base_url = https://ab.21x9.org
|
base_url = https://ab.21x9.org
|
||||||
timezone = Europe/Berlin
|
timezone = Europe/Berlin
|
||||||
mail = abgame@21x9.org
|
mailform = false
|
||||||
|
mailserver = localhost:25
|
||||||
|
mailto = abgame@21x9.org
|
||||||
|
mailfrom = abgame@21x9.org
|
||||||
|
mailsubject = A/B Game Questions
|
||||||
separator_char = ;
|
separator_char = ;
|
||||||
theme = dark
|
theme = dark
|
||||||
animations = 1
|
animations = 1
|
||||||
|
@ -12,15 +16,15 @@ animations = 1
|
||||||
lang = de
|
lang = de
|
||||||
more = Weiter »»»
|
more = Weiter »»»
|
||||||
title = A oder B
|
title = A oder B
|
||||||
mailtext = Einreichen!
|
mail_link = Einreichen!
|
||||||
help_title = Hilfe
|
help_link = Hilfe?
|
||||||
help_1 = Wähle die Antworten durch Klicken oder mit der Tastatur (a/b) aus. Sobald alle Antworten ausgewählt wurden, werden nach einer kurzen Wartezeit automatisch neue Fragen geladen.
|
help = <strong>Hilfe</strong><p>Wähle die Antworten durch Klicken/Tippen oder mit der Tastatur durch Drücken der Tasten "a" bzw. "b" aus (beantwortet die jeweils nächste unbeantwortete Frage), eine Frage kann mit "o" übersprungen werden. Sobald alle Antworten ausgewählt wurden, werden nach einer kurzen Wartezeit automatisch neue Fragen geladen.</p><p>Möchtest Du nicht alle Fragen beantworten, klicke einfach auf die Schaltfläche "Weiter" unten rechts oder drücke "F5" (beides klappt auch, während der Countdown läuft) bzw. "s" für Skip (mit kurzer Wartezeit).</p><p>Klicke/tippe außerhalb dieses Fensters oder bewege die Maus, um fortzufahren.</p><p>Diese Webseite zeichnet keine Daten auf und verwendet keine Cookies.</p>
|
||||||
help_2 = Möchtest du nicht alle Fragen beantworten, klicke einfach auf die Schaltfläche "Weiter" unten rechts (das klappt auch, während der Countdown läuft).
|
|
||||||
help_3 = Klicke/tippe außerhalb dieses Fensters oder bewege die Maus, um fortzufahren.
|
|
||||||
help_4 = Diese Webseite zeichnet keine Daten auf und verwendet keine Cookies.
|
|
||||||
help_5 =
|
|
||||||
separator = oder
|
separator = oder
|
||||||
questions_prefix = Es gibt derzeit
|
questions_prefix = Es gibt derzeit
|
||||||
questions_suffix = Fragen.
|
questions_suffix = Fragen.
|
||||||
send_questions = Einreichen!
|
send_questions = Einreichen!
|
||||||
desc = Ein simples Entweder/Oder-Entscheidungsspiel
|
desc = Ein simples Entweder/Oder-Entscheidungsspiel
|
||||||
|
submit = Absenden
|
||||||
|
submit_questions = <p>Bitte gib deine Fragenvorschläge in das untenstehende Feld ein.</p><p>Wenn du mir einen Gefallen tun möchtest, gib die Vorschläge im Format "A;B" an und schreibe immer nur ein Fragenpaar in eine Zeile.</p><p>Danke!</p>
|
||||||
|
submit_thanks_title = Danke!
|
||||||
|
submit_thanks = <p>Vielen Dank für die Einsendung.</p>
|
||||||
|
|
236
game.py
236
game.py
|
@ -1,12 +1,16 @@
|
||||||
#! /bin/python3
|
#! /bin/python3
|
||||||
|
|
||||||
from random import choice
|
"""
|
||||||
from flask import Flask, render_template, request
|
Simple A/B choice game
|
||||||
|
"""
|
||||||
|
|
||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from email.message import EmailMessage
|
||||||
|
import smtplib
|
||||||
import random
|
import random
|
||||||
import logging
|
|
||||||
import pytz
|
import pytz
|
||||||
|
from flask import Flask, render_template, request
|
||||||
|
|
||||||
app = Flask(__name__,
|
app = Flask(__name__,
|
||||||
static_url_path='',
|
static_url_path='',
|
||||||
|
@ -15,116 +19,150 @@ app = Flask(__name__,
|
||||||
|
|
||||||
config = ConfigParser()
|
config = ConfigParser()
|
||||||
config.read('config.ini')
|
config.read('config.ini')
|
||||||
logging.basicConfig(encoding='utf-8', level=logging.INFO, format='%(message)s')
|
|
||||||
logger = logging.getLogger('waitress')
|
|
||||||
tz = pytz.timezone(config.get('main', 'timezone'))
|
tz = pytz.timezone(config.get('main', 'timezone'))
|
||||||
|
|
||||||
@app.after_request
|
i18n = {
|
||||||
def log_the_request(response):
|
'lang': config.get('i18n', 'lang'),
|
||||||
now = datetime.now(tz=tz)
|
'title': config.get('i18n', 'title'),
|
||||||
|
'more': config.get('i18n', 'more'),
|
||||||
|
'desc': config.get('i18n', 'desc'),
|
||||||
|
'questions_prefix': config.get('i18n', 'questions_prefix'),
|
||||||
|
'questions_suffix': config.get('i18n', 'questions_suffix'),
|
||||||
|
'separator': config.get('i18n', 'separator'),
|
||||||
|
'mailtext': config.get('i18n', 'mail_link'),
|
||||||
|
'helptext': config.get('i18n', 'help_link'),
|
||||||
|
'help': config.get('i18n', 'help'),
|
||||||
|
'submit': config.get('i18n', 'submit'),
|
||||||
|
'submit_questions': config.get('i18n', 'submit_questions'),
|
||||||
|
'submit_thanks_title': config.get('i18n', 'submit_thanks_title'),
|
||||||
|
'submit_thanks': config.get('i18n', 'submit_thanks')
|
||||||
|
}
|
||||||
|
conf = {
|
||||||
|
'separator_char': config.get('main', 'separator_char'),
|
||||||
|
'mailform': config.get('main', 'mailform').lower(),
|
||||||
|
'mailserver': config.get('main', 'mailserver'),
|
||||||
|
'mailto': config.get('main', 'mailto'),
|
||||||
|
'mailfrom': config.get('main', 'mailfrom'),
|
||||||
|
'mailsubject': config.get('main', 'mailsubject'),
|
||||||
|
'url': config.get('main', 'base_url'),
|
||||||
|
'theme': config.get('main', 'theme'),
|
||||||
|
'animations': config.get('main', 'animations')
|
||||||
|
}
|
||||||
|
|
||||||
if 'X-Forwarded-For' in request.headers:
|
with open("ab.txt", "r", encoding="utf-8") as f:
|
||||||
remote_addr = request.headers['X-Forwarded-For']
|
num_lines = sum(1 for _ in f)
|
||||||
else:
|
|
||||||
remote_addr = request.remote_addr
|
|
||||||
|
|
||||||
if not request.remote_user:
|
def get_epoch():
|
||||||
remote_user = "-"
|
"""
|
||||||
else:
|
Get current time as epoch timestamp
|
||||||
remote_user = request.remote_user
|
"""
|
||||||
|
|
||||||
if not request.referrer:
|
|
||||||
referrer = "-"
|
|
||||||
else:
|
|
||||||
referrer = request.referrer
|
|
||||||
|
|
||||||
if request.full_path[-1] == '?':
|
|
||||||
full_path = request.full_path[:-1]
|
|
||||||
else:
|
|
||||||
full_path = request.full_path
|
|
||||||
|
|
||||||
log = {
|
|
||||||
'remote_addr': remote_addr,
|
|
||||||
'remote_user': remote_user,
|
|
||||||
'url': full_path,
|
|
||||||
'date': now.strftime("%d/%b/%Y:%H:%M:%S %z"),
|
|
||||||
'referrer': referrer,
|
|
||||||
'user_agent': request.user_agent.string,
|
|
||||||
'method': request.method,
|
|
||||||
'content_length': response.content_length,
|
|
||||||
'status_code': response.status_code
|
|
||||||
}
|
|
||||||
|
|
||||||
logfile = "{} - {} [{}] \"{} {}\" {} {} \"{}\" \"{}\"".format(log['remote_addr'], log['remote_user'], log['date'], log['method'], log['url'], log['status_code'], log['content_length'], log['referrer'], log['user_agent'])
|
|
||||||
logger.info(logfile)
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
@app.errorhandler(404)
|
|
||||||
def page_not_found(e):
|
|
||||||
lang = config.get('i18n', 'lang')
|
|
||||||
title = config.get('i18n', 'title')
|
|
||||||
url = config.get('main', 'base_url')
|
|
||||||
theme = config.get('main', 'theme')
|
|
||||||
desc = config.get('i18n', 'desc')
|
|
||||||
return render_template('404.html', title=title, lang=lang, desc=desc, theme=theme, url=url), 404
|
|
||||||
|
|
||||||
@app.errorhandler(500)
|
|
||||||
def internal_server_error(e):
|
|
||||||
lang = config.get('i18n', 'lang')
|
|
||||||
title = config.get('i18n', 'title')
|
|
||||||
url = config.get('main', 'base_url')
|
|
||||||
theme = config.get('main', 'theme')
|
|
||||||
desc = config.get('i18n', 'desc')
|
|
||||||
return render_template('500.html', title=title, lang=lang, desc=desc, theme=theme, url=url), 500
|
|
||||||
|
|
||||||
@app.route("/")
|
|
||||||
def hello():
|
|
||||||
lang = config.get('i18n', 'lang')
|
|
||||||
title = config.get('i18n', 'title')
|
|
||||||
more = config.get('i18n', 'more')
|
|
||||||
desc = config.get('i18n', 'desc')
|
|
||||||
questions_prefix = config.get('i18n', 'questions_prefix')
|
|
||||||
questions_suffix = config.get('i18n', 'questions_suffix')
|
|
||||||
send_questions = config.get('i18n', 'send_questions')
|
|
||||||
separator = config.get('i18n', 'separator')
|
|
||||||
separator_char = config.get('main', 'separator_char')
|
|
||||||
mail = config.get('main', 'mail')
|
|
||||||
mailtext = config.get('i18n', 'mailtext')
|
|
||||||
help = config.get('i18n', 'help_title')
|
|
||||||
help1 = config.get('i18n', 'help_1')
|
|
||||||
help2 = config.get('i18n', 'help_2')
|
|
||||||
help3 = config.get('i18n', 'help_3')
|
|
||||||
help4 = config.get('i18n', 'help_4')
|
|
||||||
help5 = config.get('i18n', 'help_5')
|
|
||||||
url = config.get('main', 'base_url')
|
|
||||||
theme = config.get('main', 'theme')
|
|
||||||
animations = config.get('main', 'animations')
|
|
||||||
ablines = []
|
|
||||||
|
|
||||||
now = datetime.now(tz=tz)
|
now = datetime.now(tz=tz)
|
||||||
epoch = now.timestamp()
|
epoch = now.timestamp()
|
||||||
epoch = int(epoch)
|
epoch = int(epoch)
|
||||||
|
|
||||||
lines = getContent()
|
return epoch
|
||||||
while len(lines) < 2:
|
|
||||||
logger.error('Error reading content')
|
|
||||||
print(lines)
|
|
||||||
lines = getContent()
|
|
||||||
|
|
||||||
for line in lines:
|
@app.errorhandler(404)
|
||||||
ab = line.split(separator_char)
|
def page_not_found():
|
||||||
ablines.append(
|
"""
|
||||||
{'A': str(ab[0]), 'B': str(ab[1])}
|
404 Error Page
|
||||||
|
"""
|
||||||
|
epoch = get_epoch()
|
||||||
|
return render_template('404.html', config=conf, i18n=i18n, epoch=epoch), 404
|
||||||
|
|
||||||
|
@app.errorhandler(500)
|
||||||
|
def internal_server_error():
|
||||||
|
"""
|
||||||
|
500 Error Page
|
||||||
|
"""
|
||||||
|
epoch = get_epoch()
|
||||||
|
return render_template(
|
||||||
|
'500.html',
|
||||||
|
config=conf,
|
||||||
|
i18n=i18n,
|
||||||
|
epoch=epoch
|
||||||
|
), 500
|
||||||
|
|
||||||
|
if conf['mailform'] == "true":
|
||||||
|
@app.route("/form")
|
||||||
|
def mailform():
|
||||||
|
"""
|
||||||
|
Mail form
|
||||||
|
"""
|
||||||
|
epoch = get_epoch()
|
||||||
|
return render_template(
|
||||||
|
'mailform.html',
|
||||||
|
config=conf,
|
||||||
|
i18n=i18n,
|
||||||
|
epoch=epoch,
|
||||||
|
num_lines=num_lines
|
||||||
)
|
)
|
||||||
|
|
||||||
with open("ab.txt", "r") as f:
|
@app.route("/send", methods=['POST', 'GET'])
|
||||||
num_lines = sum(1 for _ in f)
|
def sendmail():
|
||||||
|
"""
|
||||||
|
Send form data via E-Mail
|
||||||
|
"""
|
||||||
|
epoch = get_epoch()
|
||||||
|
|
||||||
return render_template('index.html', title=title, separator=separator, content=ablines, num_lines=num_lines, epoch=epoch, mailto=mail, more=more, questions_prefix=questions_prefix, questions_suffix=questions_suffix, send_questions=send_questions, lang=lang, url=url, desc=desc, theme=theme, mailtext=mailtext, help=help, help_1=help1, help_2=help2, help_3=help3, help_4=help4, help_5=help5, animations=animations)
|
if request.method == 'POST':
|
||||||
|
mailcontent = request.form['questions']
|
||||||
|
else:
|
||||||
|
mailcontent = request.args.get('questions')
|
||||||
|
|
||||||
def getContent():
|
message = EmailMessage()
|
||||||
lines = [a.strip() for a in open("ab.txt", "r").readlines()]
|
message.set_content(mailcontent)
|
||||||
|
message['Subject'] = conf['mailsubject']
|
||||||
|
message['From'] = conf['mailfrom']
|
||||||
|
message['To'] = conf['mailto']
|
||||||
|
|
||||||
|
smtp_server = smtplib.SMTP(conf['mailserver'])
|
||||||
|
smtp_server.send_message(message)
|
||||||
|
smtp_server.quit()
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'thanks.html',
|
||||||
|
config=conf,
|
||||||
|
i18n=i18n,
|
||||||
|
epoch=epoch,
|
||||||
|
questions=mailcontent
|
||||||
|
)
|
||||||
|
|
||||||
|
@app.route("/")
|
||||||
|
def hello():
|
||||||
|
"""
|
||||||
|
Default/Main page
|
||||||
|
"""
|
||||||
|
ablines = []
|
||||||
|
epoch = get_epoch()
|
||||||
|
|
||||||
|
lines = get_content()
|
||||||
|
while len(lines) < 2:
|
||||||
|
print('Error reading content')
|
||||||
|
print(lines)
|
||||||
|
lines = get_content()
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
abq = line.split(conf['separator_char'])
|
||||||
|
ablines.append(
|
||||||
|
{'A': str(abq[0]), 'B': str(abq[1])}
|
||||||
|
)
|
||||||
|
|
||||||
|
return render_template(
|
||||||
|
'index.html',
|
||||||
|
content=ablines,
|
||||||
|
config=conf,
|
||||||
|
i18n=i18n,
|
||||||
|
num_lines=num_lines,
|
||||||
|
epoch=epoch
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_content():
|
||||||
|
"""
|
||||||
|
Read content from file
|
||||||
|
"""
|
||||||
|
lines = [a.strip() for a in open("ab.txt", "r", encoding="utf-8").readlines()]
|
||||||
result = random.sample(lines, 5)
|
result = random.sample(lines, 5)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
|
@ -1,10 +1,34 @@
|
||||||
|
label.a.slide1 {
|
||||||
label.a.slide {
|
|
||||||
animation-duration: 1s;
|
animation-duration: 1s;
|
||||||
animation-name: slideinfromleft;
|
animation-name: slideinfromleft;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label.a.slide2 {
|
||||||
|
animation-duration: 1.25s;
|
||||||
|
animation-name: slideinfromleft;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
label.a.slide3 {
|
||||||
|
animation-duration: 1.5s;
|
||||||
|
animation-name: slideinfromleft;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
label.a.slide4 {
|
||||||
|
animation-duration: 1.75s;
|
||||||
|
animation-name: slideinfromleft;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
label.a.slide5 {
|
||||||
|
animation-duration: 2s;
|
||||||
|
animation-name: slideinfromleft;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@keyframes slideinfromleft {
|
@keyframes slideinfromleft {
|
||||||
from {
|
from {
|
||||||
left: -640px;
|
left: -640px;
|
||||||
|
@ -15,12 +39,36 @@ label.a.slide {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
label.b.slide {
|
label.b.slide1 {
|
||||||
animation-duration: 1s;
|
animation-duration: 1s;
|
||||||
animation-name: slideinfromright;
|
animation-name: slideinfromright;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
label.b.slide2 {
|
||||||
|
animation-duration: 1.25s;
|
||||||
|
animation-name: slideinfromright;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
label.b.slide3 {
|
||||||
|
animation-duration: 1.5s;
|
||||||
|
animation-name: slideinfromright;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
label.b.slide4 {
|
||||||
|
animation-duration: 1.75s;
|
||||||
|
animation-name: slideinfromright;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
label.b.slide5 {
|
||||||
|
animation-duration: 2s;
|
||||||
|
animation-name: slideinfromright;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes slideinfromright {
|
@keyframes slideinfromright {
|
||||||
from {
|
from {
|
||||||
left: 640px;
|
left: 640px;
|
||||||
|
|
|
@ -30,12 +30,13 @@ a #help {
|
||||||
|
|
||||||
a:hover #help {
|
a:hover #help {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 65px;
|
top: 105px;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
height: auto;
|
height: auto;
|
||||||
width: 50%;
|
width: 50%;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
|
padding-bottom: 5px;
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: var(--bg-a);
|
background-color: var(--bg-a);
|
||||||
|
@ -45,6 +46,39 @@ a:hover #help {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
transform: translate(-50%);
|
transform: translate(-50%);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea {
|
||||||
|
height: 250px;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--bg-b);
|
||||||
|
color: var(--fg-b);
|
||||||
|
border: 1px solid var(--bg-a);
|
||||||
|
}
|
||||||
|
|
||||||
|
#background {
|
||||||
|
overflow: hidden;
|
||||||
|
position: absolute;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#background::before {
|
||||||
|
content: ' ';
|
||||||
|
display: block;
|
||||||
|
background: url('/ab.jpg');
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: 50% 0;
|
||||||
|
background-size: cover;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0.1;
|
||||||
}
|
}
|
||||||
|
|
||||||
#header {
|
#header {
|
||||||
|
@ -64,15 +98,19 @@ a:hover #help {
|
||||||
min-width: 400px;
|
min-width: 400px;
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
width: 65%;
|
width: 65%;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
position: absolute;
|
position: relative;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.ab {
|
.ab {
|
||||||
float: none;
|
float: none;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
@ -151,6 +189,9 @@ input[type="radio"] {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
color: var(--fg-sep);
|
color: var(--fg-sep);
|
||||||
|
border-radius: 10px;
|
||||||
|
z-index: 5;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
#footer {
|
#footer {
|
||||||
|
@ -204,6 +245,33 @@ input[type="radio"] {
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width:640px) {
|
@media only screen and (max-width:640px) {
|
||||||
|
#header {
|
||||||
|
padding-top: 15px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover #help {
|
||||||
|
top: 5px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.a {
|
||||||
|
padding: 15px;
|
||||||
|
font-size: small;
|
||||||
|
text-indent: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.b {
|
||||||
|
padding: 15px;
|
||||||
|
font-size: small;
|
||||||
|
text-indent: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
#content {
|
#content {
|
||||||
min-width: 95%;
|
min-width: 95%;
|
||||||
transform: none;
|
transform: none;
|
||||||
|
@ -215,7 +283,8 @@ input[type="radio"] {
|
||||||
|
|
||||||
#next {
|
#next {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
margin-top: 20px;
|
margin-top: 10px;
|
||||||
|
height: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#questions {
|
#questions {
|
||||||
|
|
|
@ -9,7 +9,7 @@ function countdown() {
|
||||||
document.getElementById("next").innerHTML = timeleft;
|
document.getElementById("next").innerHTML = timeleft;
|
||||||
}
|
}
|
||||||
timeleft -= 1;
|
timeleft -= 1;
|
||||||
}, 1000);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function autoReload() {
|
function autoReload() {
|
||||||
|
|
|
@ -53,6 +53,12 @@ window.addEventListener("keydown", function (event) {
|
||||||
case "b":
|
case "b":
|
||||||
check("b");
|
check("b");
|
||||||
break;
|
break;
|
||||||
|
case "o":
|
||||||
|
check("o");
|
||||||
|
break;
|
||||||
|
case "s":
|
||||||
|
countdown();
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
checkCount();
|
checkCount();
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -4,6 +4,6 @@ directory=/opt/ab/
|
||||||
autostart=true
|
autostart=true
|
||||||
autorestart=true
|
autorestart=true
|
||||||
redirect_stderr=true
|
redirect_stderr=true
|
||||||
stdout_logfile_maxbytes=32MB
|
stdout_logfile_maxbytes=1MB
|
||||||
stdout_logfile_backups=10
|
stdout_logfile_backups=10
|
||||||
user=www-data
|
user=www-data
|
||||||
|
|
|
@ -1,19 +1,13 @@
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
{%- if theme == "light" %}
|
<html lang="{{ i18n.lang }}" data-theme="{{ config.theme }}">
|
||||||
<html lang="{{ lang }}" data-theme="light">
|
|
||||||
{%- else %}
|
|
||||||
<html lang="{{ lang }}" data-theme="dark">
|
|
||||||
{%- endif %}
|
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<title>{{ title }}</title>
|
<title>{{ i18n.title }}</title>
|
||||||
<script type="text/javascript" src="/js/toggle.js?t={{ epoch }}"></script>
|
<script type="text/javascript" src="{{ url_for('static', filename='/js/toggle.js') }}?t={{ epoch }}"></script>
|
||||||
<script type="text/javascript" src="/js/autoReload.js?t={{ epoch }}"></script>
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='/css/base.css') }}?t={{ epoch }}">
|
||||||
<script type="text/javascript" src="/js/shortcut.js?t={{ epoch }}"></script>
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='/css/colors.css') }}?t={{ epoch }}">
|
||||||
<link rel="stylesheet" type="text/css" href="/css/base.css?t={{ epoch }}">
|
{%- if config.animations == "1" or config.animations == "true" %}
|
||||||
<link rel="stylesheet" type="text/css" href="/css/colors.css?t={{ epoch }}">
|
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename='/css/animations.css') }}?t={{ epoch }}">
|
||||||
{%- if animations == "1" or animations == "true" %}
|
|
||||||
<link rel="stylesheet" type="text/css" href="/css/animations.css?t={{ epoch }}">
|
|
||||||
{%- endif %}
|
{%- endif %}
|
||||||
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='apple-touch-icon.png') }}">
|
<link rel="apple-touch-icon" sizes="180x180" href="{{ url_for('static', filename='apple-touch-icon.png') }}">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='favicon-32x32.png') }}">
|
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='favicon-32x32.png') }}">
|
||||||
|
@ -24,25 +18,25 @@
|
||||||
<meta name="theme-color" content="#ffffff">
|
<meta name="theme-color" content="#ffffff">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
<meta name="robots" content="noindex, nofollow">
|
<meta name="robots" content="noindex, nofollow">
|
||||||
<meta property="og:title" content="{{ title }}">
|
<meta property="og:title" content="{{ i18n.title }}">
|
||||||
<meta property="og:site_name" content="{{ title }}">
|
<meta property="og:site_name" content="{{ i18n.title }}">
|
||||||
<meta property="og:url" content="{{ url }}">
|
<meta property="og:url" content="{{ config.url }}">
|
||||||
<meta property="og:description" content="{{ desc }}">
|
<meta property="og:description" content="{{ i18n.desc }}">
|
||||||
<meta property="og:type" content="website">
|
<meta property="og:type" content="website">
|
||||||
<meta property="og:image" content="{{ url_for('static', filename='ab.jpg') }}">
|
<meta property="og:image" content="{{ url_for('static', filename='ab.jpg') }}">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div id="content">
|
<div id="background">
|
||||||
<div id="header">{{ title }}</div>
|
<div id="content">
|
||||||
<hr>
|
<div id="header">{{ i18n.title }}</div>
|
||||||
{%- block main %}
|
<hr>
|
||||||
{%- endblock %}
|
{%- block main %}
|
||||||
|
{%- endblock %}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
initToggle();
|
initToggle();
|
||||||
autoReload();
|
|
||||||
monitorRadio();
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
|
@ -1,46 +1,44 @@
|
||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
<script type="text/javascript" src="{{ url_for('static', filename='/js/autoReload.js') }}?t={{ epoch }}"></script>
|
||||||
|
<script type="text/javascript" src="{{ url_for('static', filename='/js/shortcut.js') }}?t={{ epoch }}"></script>
|
||||||
|
|
||||||
{%- set id = namespace(value=0) %}
|
{%- set id = namespace(value=0) %}
|
||||||
{%- for question in content %}
|
{%- for question in content %}
|
||||||
{%- set id.value = id.value + 1 %}
|
{%- set id.value = id.value + 1 %}
|
||||||
<div class="ab">
|
<div class="ab">
|
||||||
<input type="radio" id="a{{ id.value }}" class="a" name="group-{{ id.value }}">
|
<input type="radio" id="a{{ id.value }}" class="a" name="group-{{ id.value }}">
|
||||||
<label for="a{{ id.value }}" class="a slide" title="{{ question['A'] }}"> A) {{ question['A'] }}</label>
|
<label for="a{{ id.value }}" class="a slide{{ id.value }}" title="{{ question['A'] }}"> A) {{ question['A'] }}</label>
|
||||||
<div class="separator">{{ separator }}</div>
|
<input type="radio" id="o{{ id.value }}" class="separator" name="group-{{ id.value }}">
|
||||||
|
<label for="o{{ id.value }}" class="separator">{{ i18n.separator }}</label>
|
||||||
<input type="radio" id="b{{ id.value }}" class="b" name="group-{{ id.value }}">
|
<input type="radio" id="b{{ id.value }}" class="b" name="group-{{ id.value }}">
|
||||||
<label for="b{{ id.value }}" class="b slide" title="{{ question['B'] }}">B) {{ question['B'] }}</label>
|
<label for="b{{ id.value }}" class="b slide{{ id.value }}" title="{{ question['B'] }}">B) {{ question['B'] }}</label>
|
||||||
</div>
|
</div>
|
||||||
<hr>
|
<hr>
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
<div id="footer">
|
<div id="footer">
|
||||||
<div id="questions">
|
<div id="questions">
|
||||||
{{ questions_prefix }} {{ num_lines }} {{ questions_suffix }}
|
{{ i18n.questions_prefix }} {{ num_lines }} {{ i18n.questions_suffix }}
|
||||||
<br>
|
<br>
|
||||||
<a href="mailto:{{ mailto }}">{{ mailtext }}</a> -
|
{%- if config.mailform == "true" %}
|
||||||
<a href="#">{{ help }}?
|
<a href="/form">{{ i18n.mailtext }}</a> -
|
||||||
|
{%- endif %}
|
||||||
|
<a href="#">{{ i18n.helptext }}
|
||||||
<div id="help">
|
<div id="help">
|
||||||
<strong>{{ help }}</strong>
|
{{ i18n.help|safe }}
|
||||||
<p>{{ help_1 }}</p>
|
|
||||||
{%- if help_2 %}
|
|
||||||
<p>{{ help_2 }}</p>
|
|
||||||
{%- endif %}
|
|
||||||
{%- if help_3 %}
|
|
||||||
<p>{{ help_3 }}</p>
|
|
||||||
{%- endif %}
|
|
||||||
{%- if help_4 %}
|
|
||||||
<p>{{ help_4 }}</p>
|
|
||||||
{%- endif %}
|
|
||||||
{%- if help_5 %}
|
|
||||||
<p>{{ help_5 }}</p>
|
|
||||||
{%- endif %}
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
{% include 'toggle.html' %}
|
{% include 'toggle.html' %}
|
||||||
<form action="/" method="get">
|
<form action="/" method="get">
|
||||||
<input type="hidden" name="t" value="{{ epoch }}">
|
<input type="hidden" name="t" value="{{ epoch }}">
|
||||||
<button id="next" type="submit">{{ more }}</button>
|
<button id="next" type="submit">{{ i18n.more }}</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
|
||||||
|
<script type="text/javascript">
|
||||||
|
autoReload();
|
||||||
|
monitorRadio();
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
|
|
22
templates/mailform.html
Normal file
22
templates/mailform.html
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<form action="/send" method="post">
|
||||||
|
<input type="hidden" name="t" value="{{ epoch }}">
|
||||||
|
<div class="ab">
|
||||||
|
<div class="form">
|
||||||
|
<div><label for="questions">{{ i18n.submit_questions | safe }}</label></div>
|
||||||
|
<div><textarea name="questions" required></textarea></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="footer">
|
||||||
|
<div id="questions">
|
||||||
|
{{ i18n.questions_prefix }} {{ num_lines }} {{ i18n.questions_suffix }}
|
||||||
|
<br>
|
||||||
|
<a href="/">Home</a>
|
||||||
|
</div>
|
||||||
|
{% include 'toggle.html' %}
|
||||||
|
<button id="next" type="submit">{{ i18n.submit }}</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
16
templates/thanks.html
Normal file
16
templates/thanks.html
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
<div class="ab">
|
||||||
|
<div class="a">
|
||||||
|
<h1>{{ i18n.submit_thanks_title }}</h1>
|
||||||
|
{{ i18n.submit_thanks | safe }}
|
||||||
|
<p>[<a href="/">Home</a>]</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="footer">
|
||||||
|
<div class="box"> </div>
|
||||||
|
{% include 'toggle.html' %}
|
||||||
|
<div class="box"> </div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
Loading…
Add table
Reference in a new issue