rest api basics working, still some old crap here
commit
f2e8e074b6
@ -0,0 +1,2 @@
|
||||
__pycache__
|
||||
venv
|
@ -0,0 +1,2 @@
|
||||
# Default ignored files
|
||||
/workspace.xml
|
@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="JavaScriptSettings">
|
||||
<option name="languageLevel" value="ES6" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.8 (family)" project-jdk-type="Python SDK" />
|
||||
</project>
|
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/totpal.iml" filepath="$PROJECT_DIR$/.idea/totpal.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="Flask">
|
||||
<option name="enabled" value="true" />
|
||||
</component>
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.8 (family)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
<component name="TemplatesService">
|
||||
<option name="TEMPLATE_CONFIGURATION" value="Jinja2" />
|
||||
<option name="TEMPLATE_FOLDERS">
|
||||
<list>
|
||||
<option value="$MODULE_DIR$/templates" />
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</module>
|
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
@ -0,0 +1,11 @@
|
||||
FROM python:3.8
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
COPY requirements.txt ./
|
||||
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD [ "python", "app.py" ]
|
@ -0,0 +1,26 @@
|
||||
from os import getenv
|
||||
from flask import Flask
|
||||
from flask_socketio import SocketIO
|
||||
|
||||
from server import setup
|
||||
|
||||
import eventlet
|
||||
eventlet.monkey_patch()
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
app.secret_key = getenv('APP_SECRET')
|
||||
|
||||
if not app.secret_key:
|
||||
print("Please set a secret key via environment variable APP_SECRET")
|
||||
import sys
|
||||
sys.exit(1)
|
||||
|
||||
socketio = SocketIO(app, async_mode='eventlet')
|
||||
|
||||
setup(app, socketio)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("starting")
|
||||
socketio.run(app, debug=(getenv('DEV', 'false') == 'true'), port=int(getenv('PORT', 5000)), host='0.0.0.0')
|
@ -0,0 +1,11 @@
|
||||
with import <nixpkgs> {};
|
||||
|
||||
stdenv.mkDerivation rec {
|
||||
name = "totpal-run-env";
|
||||
|
||||
buildInputs = [
|
||||
(python3.withPackages (ps: with ps; [
|
||||
flask flask-socketio eventlet pylint
|
||||
]))
|
||||
];
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
version: '3'
|
||||
services:
|
||||
neo4j:
|
||||
image: neo4j:4.1
|
||||
volumes:
|
||||
- 'db-data:/data'
|
||||
environment:
|
||||
NEO4J_AUTH: neo4j/my_password
|
||||
ports:
|
||||
- '127.0.0.1:7474:7474'
|
||||
- '127.0.0.1:7687:7687'
|
||||
|
||||
python:
|
||||
build: .
|
||||
environment:
|
||||
PORT: 5000
|
||||
NEO4J_HOST: 'neo4j'
|
||||
NEO4J_USER: 'neo4j'
|
||||
NEO4J_PW: 'my_password'
|
||||
UPLOAD_DIR: '/run/uploads'
|
||||
DEV: 'true'
|
||||
APP_SECRET: 'dev'
|
||||
volumes:
|
||||
- 'uploads:/run/uploads'
|
||||
- './:/usr/src/app'
|
||||
ports:
|
||||
- '127.0.0.1:5000:5000'
|
||||
|
||||
volumes:
|
||||
db-data:
|
||||
uploads:
|
@ -0,0 +1,4 @@
|
||||
flask
|
||||
flask-socketio
|
||||
eventlet
|
||||
neomodel
|
@ -0,0 +1,15 @@
|
||||
from os import getenv
|
||||
from neomodel import config
|
||||
|
||||
pw = getenv('NEO4J_PW')
|
||||
user = getenv('NEO4J_USER')
|
||||
host = getenv('NEO4J_HOST')
|
||||
port = getenv('NEO4J_PORT', 7687)
|
||||
|
||||
config.DATABASE_URL = f'bolt://{user}:{pw}@{host}:{port}'
|
||||
|
||||
from .api import attach_to_flask
|
||||
|
||||
|
||||
def setup(app, socketio):
|
||||
attach_to_flask(app)
|
@ -0,0 +1,120 @@
|
||||
import sys
|
||||
from .models import *
|
||||
from flask import Flask, request, jsonify
|
||||
from typing import Dict, Type, List, Final
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class ApiError(BaseException):
|
||||
def __init__(self, msg, status=400):
|
||||
super().__init__()
|
||||
self.msg = msg
|
||||
self.status = status
|
||||
|
||||
|
||||
def connect_callback(attr, target):
|
||||
"""returns a callback to connect attr with target"""
|
||||
return lambda: attr.connect(target)
|
||||
|
||||
|
||||
def construct_object_from_request(cls, uid=None):
|
||||
if uid:
|
||||
obj: Final = cls.nodes.get_or_none(uid=uid)
|
||||
|
||||
if not obj:
|
||||
raise ApiError(f"No {cls.__name__} found with uid {request.form.get('uid')}!", status=404)
|
||||
else:
|
||||
obj: Final = cls()
|
||||
|
||||
relationship_attach_callbacks = list()
|
||||
|
||||
for name, value in cls.__dict__.items():
|
||||
if name[0] == '_' or not name[0].islower():
|
||||
continue
|
||||
|
||||
cls_name = value.__class__.__name__
|
||||
convert_with = None
|
||||
|
||||
if cls_name == 'StringProperty':
|
||||
convert_with = str
|
||||
elif cls_name == 'DateProperty':
|
||||
convert_with = datetime.fromisoformat
|
||||
elif cls_name == 'IntegerProperty':
|
||||
convert_with = int
|
||||
elif cls_name == 'FloatProperty':
|
||||
convert_with = float
|
||||
elif cls_name == 'RelationshipDefinition':
|
||||
if name not in request.form:
|
||||
continue
|
||||
|
||||
target_cls = value._raw_class
|
||||
# convert class string to class by loading it from module object
|
||||
if isinstance(target_cls, str):
|
||||
target_cls = getattr(cls.__module__, target_cls)
|
||||
|
||||
for val in request.form.getlist(name):
|
||||
target = target_cls.nodes.first_or_none(uid=val)
|
||||
if not target:
|
||||
raise ApiError(f'Cannot find referenced object uid={val} of type {target_cls.__name__}!')
|
||||
|
||||
relationship_attach_callbacks.append(connect_callback(getattr(obj, name), target))
|
||||
if not convert_with:
|
||||
# skip this property
|
||||
continue
|
||||
|
||||
if name in request.form:
|
||||
setattr(obj, name, convert_with(request.form.get(name)))
|
||||
|
||||
return obj, lambda: [c() for c in relationship_attach_callbacks]
|
||||
|
||||
|
||||
def func_api_obj(cls: Type):
|
||||
"""basic method for handling /api/type calls"""
|
||||
if request.method == 'GET':
|
||||
return jsonify([p.json(include_relations=False) for p in cls.nodes.all()])
|
||||
if request.method == 'POST':
|
||||
p, attach_relationships = construct_object_from_request(cls)
|
||||
p.save()
|
||||
attach_relationships()
|
||||
return jsonify(p.json())
|
||||
|
||||
|
||||
def func_api_obj_id(cls: Type, uid: str):
|
||||
if request.method == 'GET':
|
||||
person = Person.nodes.get_or_none(uid=uid)
|
||||
if not person:
|
||||
return "Not found", 404
|
||||
return jsonify(person.json())
|
||||
if request.method == 'PUT':
|
||||
p, attach_relationships = construct_object_from_request(cls, uid=uid)
|
||||
p.save()
|
||||
attach_relationships()
|
||||
return jsonify(p.json())
|
||||
|
||||
|
||||
def attach_to_flask(app: Flask):
|
||||
@app.route('/api/person', methods=['POST', 'GET'])
|
||||
def func_api_person():
|
||||
return func_api_obj(Person)
|
||||
|
||||
@app.route('/api/person/<uid>', methods=['PUT', 'GET'])
|
||||
def func_api_person_id(uid):
|
||||
return func_api_obj_id(Person, uid)
|
||||
|
||||
@app.route('/api/event', methods=['POST', 'GET'])
|
||||
def func_api_event():
|
||||
return func_api_obj(Event)
|
||||
|
||||
@app.route('/api/event/<uid>', methods=['PUT', 'GET'])
|
||||
def func_api_event_id(uid):
|
||||
return func_api_obj_id(Event, uid)
|
||||
|
||||
@app.route('/api/place', methods=['POST', 'GET'])
|
||||
def func_api_place():
|
||||
return func_api_obj(Place)
|
||||
|
||||
@app.route('/api/place/<uid>', methods=['PUT', 'GET'])
|
||||
def func_api_place_id(uid):
|
||||
return func_api_obj_id(Place, uid)
|
||||
|
||||
|
@ -0,0 +1 @@
|
||||
from .models import Place, Person, Event, Image, VisitRel
|
@ -0,0 +1,71 @@
|
||||
from neomodel import StructuredNode, StringProperty, RelationshipTo, RelationshipFrom, \
|
||||
UniqueIdProperty, DateProperty, StructuredRel, RelationshipManager
|
||||
from neomodel.cardinality import ZeroOrOne, ZeroOrMore, One, OneOrMore
|
||||
|
||||
|
||||
class Jsonifiable(object):
|
||||
def json(self, include_relations=True):
|
||||
json = dict()
|
||||
for name, value in vars(self).items():
|
||||
if name == 'id':
|
||||
continue
|
||||
print(name, value)
|
||||
if isinstance(value, RelationshipManager):
|
||||
if not include_relations:
|
||||
continue
|
||||
if isinstance(value, (One, ZeroOrOne)):
|
||||
val = value.get_or_none()
|
||||
print(val)
|
||||
json[name] = val.json(include_relations=False) if val else None
|
||||
elif isinstance(value, (OneOrMore, ZeroOrMore)):
|
||||
json[name] = [val.json(include_relations=False) for val in value.all()]
|
||||
else:
|
||||
json[name] = value
|
||||
return json
|
||||
|
||||
|
||||
class VisitRel(StructuredRel, Jsonifiable):
|
||||
title = StringProperty(required=True)
|
||||
description = StringProperty()
|
||||
date = DateProperty()
|
||||
|
||||
|
||||
class Person(StructuredNode, Jsonifiable):
|
||||
uid = UniqueIdProperty()
|
||||
name = StringProperty(required=True)
|
||||
father = RelationshipTo('Person', 'FATHER', cardinality=ZeroOrOne)
|
||||
mother = RelationshipTo('Person', 'MOTHER', cardinality=ZeroOrOne)
|
||||
birthdate = DateProperty()
|
||||
deceased = DateProperty()
|
||||
description = StringProperty()
|
||||
picture = StringProperty()
|
||||
places = RelationshipTo('Place', 'VISITOR', ZeroOrMore, model=VisitRel)
|
||||
|
||||
|
||||
class Event(StructuredNode, Jsonifiable):
|
||||
uid = UniqueIdProperty()
|
||||
name = StringProperty(required=True)
|
||||
participants = RelationshipTo(Person, 'PARTICIPANT', cardinality=ZeroOrMore)
|
||||
places = RelationshipTo('Place', 'HAPPENED_AT', cardinality=ZeroOrMore)
|
||||
date = DateProperty()
|
||||
end_date = DateProperty()
|
||||
related_events = RelationshipTo('Event', 'RELATED_EVENT')
|
||||
|
||||
|
||||
class Place(StructuredNode, Jsonifiable):
|
||||
uid = UniqueIdProperty()
|
||||
name = StringProperty(required=True)
|
||||
description = StringProperty()
|
||||
visitors = RelationshipFrom(Person, 'VISITOR', ZeroOrMore, model=VisitRel)
|
||||
|
||||
|
||||
class Image(StructuredNode, Jsonifiable):
|
||||
uid = StringProperty(unique_index=True, required=True)
|
||||
description = StringProperty()
|
||||
pictured = RelationshipTo(Person, 'PICTURED')
|
||||
place = RelationshipTo(Place, 'TAKEN_AT')
|
||||
date = DateProperty()
|
||||
|
||||
|
||||
|
||||
|
@ -0,0 +1,21 @@
|
||||
with import <nixpkgs> {};
|
||||
let
|
||||
extensions = (with pkgs.vscode-extensions; [
|
||||
ms-python.python
|
||||
]);
|
||||
vscode-with-extensions = pkgs.vscode-with-extensions.override {
|
||||
vscodeExtensions = extensions;
|
||||
};
|
||||
in
|
||||
stdenv.mkDerivation rec {
|
||||
name = "totpal-dev-env";
|
||||
|
||||
buildInputs = [
|
||||
(python3.withPackages (ps: with ps; [
|
||||
flask flask-socketio eventlet pylint
|
||||
]))
|
||||
git
|
||||
openjdk11
|
||||
jetbrains.pycharm-professional
|
||||
];
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
onload(() => {
|
||||
const socket = io.connect(document.location.host);
|
||||
|
||||
socket.on('flash_msg', flash_msg);
|
||||
|
||||
socket.on('player_state', (data) => {
|
||||
if (!data.secret) {
|
||||
// our secret seems to be to old
|
||||
localStorage.removeItem('secret');
|
||||
return register();
|
||||
}
|
||||
//flash_msg("Logged in!");
|
||||
localStorage.setItem('secret', data.secret);
|
||||
localStorage.setItem('name', data.username);
|
||||
document.cookie = `secret=${data.secret}`;
|
||||
window.username = data.username;
|
||||
$('.username').innerText = 'playing as ' + window.username;
|
||||
});
|
||||
|
||||
function register() {
|
||||
const name = localStorage.getItem('name') || prompt("Whats your username?");
|
||||
socket.emit('signup', name)
|
||||
}
|
||||
|
||||
if (!localStorage.getItem('secret')) {
|
||||
register()
|
||||
} else {
|
||||
socket.emit('resume', localStorage.getItem('secret'))
|
||||
}
|
||||
|
||||
window.socket = socket;
|
||||
});
|
@ -0,0 +1,83 @@
|
||||
html, body {
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
font-family: 'roboto', 'open-sans', sans-serif;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.flex-grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.container {
|
||||
width: 70%;
|
||||
max-width: 1080px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
footer {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
/* specific styles */
|
||||
|
||||
html {
|
||||
background: rgb(35, 36, 37);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
nav {
|
||||
background: rgb(22, 22, 22);
|
||||
margin-bottom: 32px;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
nav h1 {
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
nav h1 span {
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
nav .container div {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
nav .container .username {
|
||||
padding: 16px;
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
a {
|
||||
color: rgb(85, 150, 255);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.flash-msg {
|
||||
border: 1px solid;
|
||||
padding: 8px 16px;
|
||||
border-radius: 3px;
|
||||
margin: 16px;
|
||||
margin-top: -16px;
|
||||
margin-bottom: 32px;
|
||||
background: rgb(48, 50, 51)
|
||||
}
|
||||
|
||||
.flash-msg .close-btn {
|
||||
cursor: pointer;
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script>(() => {onload_list = []; window.onload = (f) => {onload_list.push(f)}; window.__is_loaded = () => {onload_list.forEach(x => x());window.__is_loaded = () => undefined} })()</script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.1/socket.io.js"></script>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>TOTPAL - {{ title }}</title>
|
||||
<script src="{{ url_for('static', filename='site.js') }}"></script>
|
||||
{% block head %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<nav><div class="container flex">
|
||||
{% block nav %}
|
||||
<div class="">
|
||||
<h1>TOTPAL <span>{{ title }}</span> </h1>
|
||||
</div>
|
||||
<div class="username flex-grow"></div>
|
||||
<div>
|
||||
{% block links %}
|
||||
<a href="/">Home</a>
|
||||
{% endblock %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
</div></nav>
|
||||
<main class="flex-grow">
|
||||
<div class="container">
|
||||
{% for message in get_flashed_messages() %}
|
||||
<div class="flash-msg flex"><span class="flex-grow">{{ message | safe }}</span> <span class="close-btn">×</span></div>
|
||||
{% endfor %}
|
||||
|
||||
{% block content %}
|
||||
No content
|
||||
{% endblock %}
|
||||
</div>
|
||||
</main>
|
||||
<footer>
|
||||
<div class="container">
|
||||
{% block footer %}
|
||||
© Copyright by AntonLydike - play at your own risk
|
||||
{% endblock %}
|
||||
</div>
|
||||
</footer>
|
||||
<script>
|
||||
if (__is_loaded) {__is_loaded()}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
@ -0,0 +1,6 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Error: {{ name }}</h1>
|
||||
<p>{{ message }}</p>
|
||||
{% endblock %}
|
@ -0,0 +1,78 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block head %}
|
||||
<script src="{{ url_for('static', filename='socket.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Game phase: <span class="game-phase">None</span></h1>
|
||||
<h2>Players:</h2>
|
||||
<ul class="player-list"></ul>
|
||||
<div class="state-show state-searching">
|
||||
<button class="start-game">Start game</button>
|
||||
</div>
|
||||
|
||||
<div class="state-show state-selecting">
|
||||
<p>The player is <span class="guesser-name"></span></p>
|
||||
<form id="select-url">
|
||||
<input type="text" name="url" placeholder="url"/>
|
||||
<input type="text" name="title" placeholder="title"/>
|
||||
<button type="submit">Select</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="state-show state-guessing state-resolution">
|
||||
<p>The articles title is: <span class="article-title"></span>...</p>
|
||||
<button class="show-resolution state-guessing state-show">We are done guessing</button>
|
||||
</div>
|
||||
<div class="state-show state-resolution">
|
||||
<p>The article was submitted by <span class="selected-player"></span>!</p>
|
||||
</div>
|
||||
<script>
|
||||
onload(() => {
|
||||
socket.on('room_state', update_state);
|
||||
|
||||
function update_state(state) {
|
||||
console.log(state);
|
||||
print_players(state);
|
||||
$('.article-title').innerText = state.article;
|
||||
$('.selected-player').innerText = state.selected_player ? state.selected_player.name : '';
|
||||
$('.game-phase').innerText = state.state.toLowerCase();
|
||||
$('.guesser-name').innerText = state.guesser ? state.guesser.name : '';
|
||||
|
||||
$$('.state-show').forEach(elm => {
|
||||
elm.hidden = !elm.classList.contains('state-' + state.state.toLowerCase());
|
||||
})
|
||||
}
|
||||
|
||||
function print_players(state) {
|
||||
const root = $('.player-list');
|
||||
root.innerHTML = "";
|
||||
|
||||
state.players.forEach(player => {
|
||||
const elm = create_elm(`<li>${(player.online ? '●' : '○')} ${player.name}</li>`)[0]
|
||||
root.appendChild(elm)
|
||||
})
|
||||
}
|
||||
|
||||
update_state({{ room | safe }});
|
||||
|
||||
$('form#select-url').addEventListener('submit', e => {
|
||||
e.preventDefault();
|
||||
let url = e.target.elements.url.value;
|
||||
let title = e.target.elements.title.value;
|
||||
|
||||
socket.emit('add_article', title, url)
|
||||
});
|
||||
|
||||
$('button.start-game').addEventListener('click', e => {
|
||||
socket.emit('start_selecting')
|
||||
});
|
||||
|
||||
$('button.show-resolution').addEventListener('click', e => {
|
||||
socket.emit('start_resolving')
|
||||
});
|
||||
})
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
@ -0,0 +1,42 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block head %}
|
||||
<script src="{{ url_for('static', filename='socket.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Games waiting:</h1>
|
||||
|
||||
<form action="/room" method="POST" id="create-room">
|
||||
<input type="text" placeholder="Name" name="name">
|
||||
<button class="create-room" type="submit">Create game</button>
|
||||
<input type="hidden" name="secret" class="insert-secret"/>
|
||||
</form>
|
||||
|
||||
<ul class="active-lobbies"></ul>
|
||||
|
||||
|
||||
<script>
|
||||
onload(() => {
|
||||
socket.on('room_list', draw_rooms);
|
||||
|
||||
function draw_rooms(list) {
|
||||
console.log(list);
|
||||
const root = $('.active-lobbies');
|
||||
root.innerHTML = "";
|
||||
|
||||
list.forEach(room => {
|
||||
const elm = create_elm(`<li>${room.name} with ${room.players.map(x => (x.online ? '● ' : '○ ') + x.name).join(", ")} <a href="/room/${room.id}">JOIN</a></li>`)[0]
|
||||
root.appendChild(elm)
|
||||
})
|
||||
}
|
||||
|
||||
$('form#create-room').addEventListener('submit', e => {
|
||||
e.target.elements.secret.value = localStorage.getItem('secret')
|
||||
});
|
||||
|
||||
draw_rooms({{ rooms | safe }})
|
||||
})
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
Loading…
Reference in New Issue