Fix Commit history

This commit is contained in:
Michał Gdula 2023-06-11 20:44:00 +01:00
parent a29f06dfee
commit d26d8cb021
70 changed files with 674 additions and 477 deletions

3
.gitignore vendored
View file

@ -1,3 +1,6 @@
.idea/
.vscode/
.uuid
.env
/.env

View file

@ -1,9 +1,9 @@
{$DOMAIN_HIGHSCORE} {
reverse_proxy highscore:8000
{$THE_FRONT_ROOMS_DOMAIN} {
reverse_proxy tfr:8000
encode gzip
}
{$DOMAIN_EXPO} {
{$GAME_EXPO_DOMAIN} {
reverse_proxy expo:5000
encode gzip
}

File diff suppressed because one or more lines are too long

View file

@ -1,8 +0,0 @@
function keepRatio(){let games=document.querySelectorAll(".game-box");games.forEach((game)=>{game.style.height=(game.offsetWidth*1.5)+"px";});}
window.onscroll=()=>{scrollFunction();checkSection();};window.onload=()=>{keepRatio()
resizeNav();scrollFunction();checkSection();};window.onresize=()=>{keepRatio()
resizeNav();checkSection();};const defaultTitle="DV8 Game Expo <span>2023</span>";let navSpacing=(5*16);let prevElement=null;function resizeNav(){if(window.innerWidth>600){navSpacing=(5*16);}else{navSpacing=(8*16);}}
function scrollFunction(){let nav=document.querySelector("nav");let scrollHeight=0;if(document.body.scrollTop>scrollHeight||document.documentElement.scrollTop>scrollHeight){nav.classList.add("scrolled");}else{nav.classList.remove("scrolled");}}
function checkSection(){let navTitle=document.querySelector(".title > p");let sections=document.querySelectorAll("section");if((window.pageYOffset+navSpacing)<sections[0].offsetTop||window.pageYOffset===0){if(prevElement===null)return;navTitle.innerHTML=defaultTitle;navTitle.style.animation="title-change 0.2s ease-in-out";prevElement=null;setTimeout(()=>{navTitle.style.animation="";},200);return;}
sections.forEach((section)=>{let top=section.offsetTop;let bottom=section.offsetTop+section.offsetHeight;if((window.pageYOffset+navSpacing)>=top&&window.pageYOffset<(bottom-navSpacing)){if(prevElement===section)return;navTitle.innerHTML=section.id.split("_").join(" ");navTitle.style.animation="title-change 0.2s ease-in-out";prevElement=section;setTimeout(()=>{navTitle.style.animation="";},200);}});}
document.querySelectorAll("nav > ul > li > a").forEach((element)=>{element.onclick=()=>{let anchor=location.hash.split("#")[1].toString();let element=document.getElementById(anchor);if(element===null){window.scrollTo({top:0,behavior:"smooth"});}else{window.scrollTo({top:(element.offsetTop+navSpacing),behavior:"smooth"});}}});

View file

@ -6,11 +6,11 @@ from website.config import INSTANCE_DIR, MIGRATION_DIR
from website import routes
app = Flask(__name__) # instance_path=INSTANCE_DIR
app = Flask(__name__, instance_path=INSTANCE_DIR)
app.config.from_pyfile("config.py")
db.init_app(app)
migrate.init_app(app, db) # directory=MIGRATION_DIR
migrate.init_app(app, db, directory=MIGRATION_DIR)
with app.app_context():
db.create_all()

View file

Before

Width:  |  Height:  |  Size: 2.1 MiB

After

Width:  |  Height:  |  Size: 2.1 MiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Before After
Before After

View file

@ -1,98 +0,0 @@
import uuid
from flask import Blueprint, request, jsonify
from flask_login import login_required, current_user
from server.models import Tokens, Scores
from server.extensions import db
from server.config import BEARER_TOKEN
blueprint = Blueprint("api", __name__, url_prefix="/api")
@blueprint.route("/tokens", methods=["DELETE", "POST"])
@login_required
def tokens():
if request.method == "DELETE":
token_id = request.form["token_id"]
if not token_id:
return jsonify({"error": "No token ID provided!"}), 400
token = Tokens.query.filter_by(id=token_id).first()
if not token:
return jsonify({"error": "Token not found!"}), 404
if token.holder != current_user.id:
return jsonify({"error": "You do not own this token!"}), 403
db.session.delete(token)
db.session.commit()
return jsonify({"success": "Token deleted!"}), 200
elif request.method == "POST":
if len(Tokens.query.filter_by(holder=current_user.id).all()) >= 5:
return jsonify({"error": "You already have 5 tokens!"}), 403
token = Tokens(token=str(uuid.uuid4()), holder=current_user.id)
db.session.add(token)
db.session.commit()
return jsonify({"success": "Token added!"}), 200
@blueprint.route("/post", methods=["POST"])
def post():
form = request.form
if not form:
return "Invalid form", 400
if not request.headers.get("Authentication"):
return "Invalid authentication", 401
if not isinstance(form["score"], int):
return "Score must be an integer", 400
if int(form["score"]) < 0:
return "Score must be greater than 0", 400
if form["difficulty"] not in [0, 1, 2, 3, 4]:
# 0 = Easy, Level 1
# 1 = Easy, Level 2
# 2 = Easy, Level 3
# 3 = Normal
# 4 = Hard
return "Invalid difficulty", 400
if token_data := Tokens.query.filter_by(
token=request.headers.get("Authentication")
).first():
# User is authenticated
# This is a registered user
score = Scores(
score=form["score"],
difficulty=form["difficulty"],
achievements=form["achievements"],
user_id=token_data.holder,
)
db.session.add(score)
db.session.commit()
return "Success!", 200
elif request.headers.get("Authentication") == BEARER_TOKEN:
# User is not authenticated, but has the correct token
# This is an anonymous user
if not form["playerName"] or len(form["playerId"]) != 4:
return "Invalid player name", 400
score = Scores(
anonymous=True,
username=form["playerName"],
score=form["score"],
difficulty=form["difficulty"],
)
db.session.add(score)
db.session.commit()
return "Success!", 200
return "Authentication failed", 401

View file

@ -1,24 +0,0 @@
import os
# Purely to make the code a bit more readable
def env(key):
return os.getenv(key)
SECRET_KEY = env("FLASK_KEY")
BEARER_TOKEN = env("BEARER_TOKEN")
user = env("DB_USER")
password = env("DB_PASSWORD")
host = env("DB_HOST")
database = env("DB_NAME")
SQLALCHEMY_DATABASE_URI = (
f"postgresql+psycopg2://{user}:{password}@{host}:5432/{database}"
)
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_POOL_RECYCLE = 621
MIGRATION_DIR = "/data/storage/migrations"
INSTANCE_DIR = "/data/storage/instance"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 MiB

View file

@ -1,6 +0,0 @@
function addFlashMessage(message,type='success'){let flask=document.createElement('p');flask.onclick=()=>flask.remove();flask.classList.add(type);flask.innerHTML=message;let close=document.createElement('span');close.innerHTML='<i class="ph-bold ph-x"></i>';flask.appendChild(close);document.querySelector('.flash').appendChild(flask);}
function ajax(url,form,callback,method='POST'){console.log(form)
fetch(url,{method:method,body:form,}).then(response=>response.json()).then(data=>callback(data)).catch(error=>addFlashMessage(error.error,'error'));}
function deleteToken(id){let form=new FormData();form.append('token_id',id);ajax('/api/tokens',form,(data)=>{if(data.success){addFlashMessage(data.success,'success');document.querySelector(`#token-${id}`).remove();}else{addFlashMessage(data.error,'error');}},'DELETE');}
function addToken(){ajax('/api/tokens',null,(data)=>{if(data.success){window.location.reload();}else{addFlashMessage(data.error,'error');}});}
function viewToken(id){let token=document.querySelector(`#token-${id}`);let hidden=token.children[2];hidden.classList.toggle('hidden');}

File diff suppressed because one or more lines are too long

View file

@ -1,66 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Front Rooms Highscores</title>
<script src="https://unpkg.com/@phosphor-icons/web"></script>
{% assets "scripts" %}
<script src="{{ ASSET_URL }}"></script>
{% endassets %}
{% assets "styles" %}
<link rel="stylesheet" href="{{ ASSET_URL }}" type="text/css">
{% endassets %}
</head>
<body>
<img src="{{ url_for('static', filename='bg.png') }}" alt="The Front Rooms pause menu" class="background">
<div class="app">
<!-- Get flashed lol -->
<div class="flash">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<p class="{{ category }}" onclick="this.remove()">
<span><i class="ph-bold ph-x"></i></span>
{{ message }}
</p>
{% endfor %}
{% endif %}
{% endwith %}
</div>
<header>
<img src="{{ url_for('static', filename='title.png') }}" alt="The Front Rooms logo" class="title" height="60px">
<nav>
<a href="{{ url_for('views.index') }}" class="button">Scores</a>
<a href="{{ url_for('views.about') }}" class="button"><i class="ph ph-info"></i></a>
<a href="#" class="button"><i class="ph ph-download-simple"></i></a>
<!-- This is a spacer -->
<span></span>
{% if current_user.is_authenticated %}
<a href="{{ url_for('auth.account') }}" class="button primary">{{ current_user.username }}</a>
{% else %}
<a href="{{ url_for('auth.auth') }}" class="button primary"><i class="ph ph-identification-card"></i></a>
{% endif %}
</nav>
<!-- Secondary nav bar for page specific content -->
{% block nav %}{% endblock %}
</header>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<p>By Project Redacted | <a href="https://github.com/Fluffy-Bean/GameExpo23">Server Source</a></p>
</footer>
</div>
</body>
</html>

View file

@ -1,42 +0,0 @@
{% extends "base.html" %}
{% block nav %}
<nav>
<a href="{{ url_for('views.index', diff=0) }}" class="button">Level 1</a>
<a href="{{ url_for('views.index', diff=1) }}" class="button">Level 2</a>
<a href="{{ url_for('views.index', diff=2) }}" class="button">Level 3</a>
<a href="{{ url_for('views.index', diff=3) }}" class="button">Normal</a>
<a href="{{ url_for('views.index', diff=4) }}" class="button">Hard</a>
<!-- This is a spacer -->
<span></span>
</nav>
{% endblock %}
{% block content %}
{% if scores %}
<table>
<tr>
<th>Position</th>
<th>Player</th>
<th>Difficulty</th>
<th>Score</th>
</tr>
{% for score in top_scores %}
<tr>
<td>{{ loop.index }}</td>
{% if score.anonymous %}
<td>{{ score.username }}</td>
{% else %}
<td>{{ score.scorer.username }}</td>
{% endif %}
<td>{{ score.difficulty }}</td>
<td>{{ score.score }}</td>
</tr>
{% endfor %}
</table>
{% else %}
<div class="center-text">
<h2>No scores yet</h2>
<p>Set some!</p>
</div>
{% endif %}
{% endblock %}

View file

@ -1,24 +0,0 @@
from flask import Blueprint, request, render_template
from server.models import Scores
blueprint = Blueprint("views", __name__)
@blueprint.route("/")
# @cache.cached(timeout=60)
def index():
difficulty = request.args.get("diff", 0)
top_scores = (
Scores.query.order_by(Scores.score.desc())
.filter_by(difficulty=difficulty)
.limit(10)
.all()
)
return render_template("scores.html", top_scores=top_scores)
@blueprint.route("/about")
def about():
return render_template("about.html")

View file

@ -161,3 +161,4 @@ cython_debug/
# remove development files
/storage
/logs
/server/static/gen/

View file

@ -1,5 +1,6 @@
Gunicorn
psycopg2-binary
shortuuid
Flask
Flask-SQLAlchemy
Flask-Migrate

View file

@ -26,4 +26,4 @@ fi
# Start server!!!!
echo "Starting server..."
gunicorn --bind highscore:8000 server:app
gunicorn --bind tfr:8000 server:app

View file

@ -4,17 +4,16 @@ from flask import Flask, render_template, abort
from flask_assets import Bundle
from werkzeug.exceptions import HTTPException
from server.config import MIGRATION_DIR, INSTANCE_DIR
from server.extensions import db, migrate, cache, assets, login_manager
from server.models import Users
from server import views, auth, api
app = Flask(__name__, instance_path=INSTANCE_DIR)
app = Flask(__name__)
app.config.from_pyfile("config.py")
db.init_app(app)
migrate.init_app(app, db, directory=MIGRATION_DIR)
migrate.init_app(app, db)
with app.app_context():
db.create_all()

97
TFR/server/api.py Normal file
View file

@ -0,0 +1,97 @@
import shortuuid
from flask import Blueprint, request, jsonify
from flask_login import login_required, current_user
from server.models import Tokens, Scores, Users
from server.extensions import db
from server.config import GAME_VERSION, GAME_VERSIONS, GAME_DIFFICULTIES, USER_MAX_TOKENS, MAX_SEARCH_RESULTS
blueprint = Blueprint("api", __name__, url_prefix="/api")
@blueprint.route("/tokens", methods=["DELETE", "POST"])
@login_required
def tokens():
if request.method == "DELETE":
token_id = request.form["token_id"]
if not token_id:
return jsonify({"error": "No token ID provided!"}), 400
token = Tokens.query.filter_by(id=token_id).first()
if not token:
return jsonify({"error": "Token not found!"}), 404
if token.user_id != current_user.id:
return jsonify({"error": "You do not own this token!"}), 403
db.session.delete(token)
db.session.commit()
return jsonify({"success": "Token deleted!"}), 200
elif request.method == "POST":
if len(Tokens.query.filter_by(user_id=current_user.id).all()) >= USER_MAX_TOKENS:
return jsonify({"error": f"You already have {USER_MAX_TOKENS} tokens!"}), 403
new_string = str(shortuuid.ShortUUID().random(length=20))
token = Tokens(token=new_string, user_id=current_user.id)
db.session.add(token)
db.session.commit()
return jsonify({"success": "Token added!"}), 200
@blueprint.route("/post", methods=["POST"])
def post():
form = request.form
errors = []
if not form:
errors += "No form data provided!"
if not form["token"]:
errors += "No token provided!"
if not form["version"]:
errors += "No version provided!"
if errors:
return jsonify(errors), 400
try:
int(form["score"])
int(form["difficulty"])
except TypeError:
errors += "Invalid score and difficulty must be valid numbers!"
if int(form["difficulty"]) not in GAME_DIFFICULTIES:
errors += "Invalid difficulty!"
token_data = Tokens.query.filter_by(token=form["token"]).first()
if not token_data:
errors += "Authentication failed!"
if errors:
return jsonify(errors), 400
score = Scores(
score=int(form["score"]),
difficulty=int(form["difficulty"]),
version=form["version"],
user_id=token_data.user_id,
)
db.session.add(score)
db.session.commit()
return "Success!", 200
@blueprint.route("/users", methods=["GET"])
def users():
search = request.args.get("search")
if not search:
return "No search query provided!", 400
search_results = Users.query.filter(Users.username.contains(search)).limit(MAX_SEARCH_RESULTS).all()
return jsonify([result.username for result in search_results]), 200

View file

@ -31,7 +31,7 @@ def account():
if action == "password":
flash("Insert password change function", "error")
token_list = Tokens.query.filter_by(holder=current_user.id).all()
token_list = Tokens.query.filter_by(user_id=current_user.id).all()
return render_template("account.html", token_list=token_list)

32
TFR/server/config.py Normal file
View file

@ -0,0 +1,32 @@
import os
GAME_VERSION = "alpha"
GAME_VERSIONS = ["alpha"]
GAME_DIFFICULTIES = [0, 1, 2, 3, 4]
USER_MAX_TOKENS = 3
MAX_TOP_SCORES = 15
MAX_SEARCH_RESULTS = 5
"""
# Postgres
SECRET_KEY = os.getenv("FLASK_KEY")
user = os.getenv("DB_USER")
password = os.getenv("DB_PASSWORD")
host = os.getenv("DB_HOST")
db = os.getenv("DB_NAME")
SQLALCHEMY_DATABASE_URI = f"postgresql+psycopg2://{user}:{password}@{host}:5432/{db}"
SQLALCHEMY_TRACK_MODIFICATIONS = False
SQLALCHEMY_POOL_RECYCLE = 621
MIGRATION_DIR = "/data/storage/migrations"
INSTANCE_DIR = "/data/storage/instance"
"""
# SQLite
SECRET_KEY = "dev"
SQLALCHEMY_DATABASE_URI = "sqlite:///tfr.db"

View file

@ -4,22 +4,18 @@ Database models for the server
import uuid
from flask_login import UserMixin
from server.extensions import db
from server.config import GAME_VERSION
class Scores(db.Model):
"""
Post table
Scores supports anonymous posting, and instead just wants to post a score,
then the username must be provided.Otherwise, it's grabbed from the user table
then the username must be provided. Otherwise, it's grabbed from the user
table
"""
__tablename__ = "scores"
id = db.Column(db.Integer, primary_key=True)
anonymous = db.Column(db.Boolean, nullable=False, default=False)
username = db.Column(db.String(32), nullable=True)
score = db.Column(db.Float, nullable=False)
difficulty = db.Column(db.Integer, nullable=False)
scored_at = db.Column(
@ -27,17 +23,34 @@ class Scores(db.Model):
nullable=False,
server_default=db.func.now(),
)
posted_at = db.Column(
db.DateTime,
nullable=False,
server_default=db.func.now(),
)
scorer = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True)
version = db.Column(db.String, default=GAME_VERSION)
user_id = db.Column(db.Integer, db.ForeignKey("users.id", use_alter=True))
class Tokens(db.Model):
"""
Token table
"""
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("users.id", use_alter=True))
token = db.Column(db.String, nullable=False, unique=True)
created_at = db.Column(
db.DateTime,
nullable=False,
server_default=db.func.now(),
)
class Users(db.Model, UserMixin):
"""
User table
"""
__tablename__ = "users"
id = db.Column(db.Integer, primary_key=True)
alt_id = db.Column(db.String, nullable=False, unique=True)
@ -49,25 +62,9 @@ class Users(db.Model, UserMixin):
server_default=db.func.now(),
)
scores = db.relationship("Scores", backref="user", lazy=True)
tokens = db.relationship("Tokens", backref="user", lazy=True)
scores = db.relationship("Scores", backref=db.backref('users', lazy=True))
tokens = db.relationship("Tokens", backref=db.backref('users', lazy=True))
def get_id(self):
return str(self.alt_id)
class Tokens(db.Model):
"""
Token table
"""
__tablename__ = "tokens"
id = db.Column(db.Integer, primary_key=True)
holder = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False)
token = db.Column(db.String, nullable=False, unique=True)
created_at = db.Column(
db.DateTime,
nullable=False,
server_default=db.func.now(),
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 570 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View file

Before

Width:  |  Height:  |  Size: 400 KiB

After

Width:  |  Height:  |  Size: 400 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 165 KiB

After

Width:  |  Height:  |  Size: 165 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 708 KiB

After

Width:  |  Height:  |  Size: 708 KiB

Before After
Before After

View file

@ -1,16 +1,4 @@
function addFlashMessage(message, type='success') {
/**
* Add a flash message to the page
*
* @param {string} message
* @return {void}
*
* @example
* addFlashMessage('Hello World!', 'success')
*
* @example
* addFlashMessage('Oopsie!', 'error')
*/
let flask = document.createElement('p');
flask.onclick = () => flask.remove();
flask.classList.add(type);
@ -24,20 +12,6 @@ function addFlashMessage(message, type='success') {
}
function ajax(url, form, callback, method='POST') {
/**
* Send a request to the server and get a response
* Mostly a wrapper for fetch(), since most of the
* requests are made with FormData and POST method
*
* @param {string} url
* @param {FormData} form
* @param {function} callback
* @param {string} method
* @return {void}
*
* @example
* ajax('/api', formData, callback = (data) => { console.log(data) }, 'POST')
*/
console.log(form)
fetch(url, {
method: method,
@ -49,15 +23,6 @@ function ajax(url, form, callback, method='POST') {
}
function deleteToken(id) {
/**
* Delete user token
*
* @return {void}
* @{integer} id
*
* @example
* deleteToken(id)
*/
let form = new FormData();
form.append('token_id', id);
@ -72,14 +37,6 @@ function deleteToken(id) {
}
function addToken() {
/**
* Add a new token
*
* @return {void}
*
* @example
* addToken()
*/
ajax('/api/tokens', null, (data) => {
if (data.success) {
window.location.reload();
@ -90,15 +47,6 @@ function addToken() {
}
function viewToken(id) {
/**
* View a token
*
* @return {void}
* @{integer} id
*
* @example
* viewToken(id)
*/
let token = document.querySelector(`#token-${id}`);
let hidden = token.children[2];

View file

@ -0,0 +1,88 @@
function showHint() {
let search = document.querySelector('.search > input');
let searchPos = search.getBoundingClientRect();
let hint = document.querySelector('.search-hint');
hint.style.width = search.offsetWidth + 'px';
hint.style.left = searchPos.left + 'px';
hint.style.top = searchPos.bottom + 'px';
hint.style.display = 'flex';
}
function hideHint() {
let hint = document.querySelector('.search-hint');
hint.style.display = 'none';
}
function updateHint() {
let search = document.querySelector('.search > input');
let searchPos = search.getBoundingClientRect();
let hint = document.querySelector('.search-hint');
hint.style.width = search.offsetWidth + 'px';
hint.style.left = searchPos.left + 'px';
hint.style.top = searchPos.bottom + 'px';
}
function getSearch() {
let search = document.querySelector('.search > input').value;
let hint = document.querySelector('.search-hint');
if (search.length === 0) {
hint.innerHTML = '<p>Start typing to see results...</p>';
return;
}
fetch('/api/users?search=' + search.toString(), {
method: 'GET',
})
.then(response => response.json())
.then(data => {
if (data.length === 0) {
hint.innerHTML = '<p>No results found...</p>';
return;
}
hint.innerHTML = '';
data.forEach(user => {
let el = document.createElement('button');
el.innerHTML = user;
el.onmousedown = function (event) {
event.preventDefault();
search = document.querySelector('.search > input');
search.value = user.toString();
}
hint.appendChild(el);
});
})
.catch(() => {
hint.innerHTML = '<p>Something went wrong...</p>';
});
}
window.onload = () => {
let typingTimer;
let search = document.querySelector('.search > input');
if (search === null) {
return;
}
window.addEventListener('resize', updateHint);
search.addEventListener('focus', showHint);
search.addEventListener('blur', hideHint);
search.addEventListener('keydown', () => {
clearTimeout(typingTimer);
});
search.addEventListener('keyup', () => {
clearTimeout(typingTimer);
typingTimer = setTimeout(getSearch, 250);
});
}

View file

@ -0,0 +1,87 @@
.button
margin: auto 0
padding: 0.5rem 0.7rem
display: flex
align-items: center
gap: 0.5rem
text-decoration: none
white-space: nowrap
font-size: 0.9em
background-color: RGBA($white, 0.02)
color: RGB($white)
border-radius: 2px
border: 0 solid transparent
transition: background-color 0.1s ease-in-out
&:hover
background-color: RGBA($white, 0.3)
&.primary
background-color: RGBA($primary, 0.02)
color: RGB($primary)
&:hover
background-color: RGBA($primary, 0.3)
&.secondary
background-color: RGBA($secondary, 0.02)
color: RGB($secondary)
&:hover
background-color: RGBA($secondary, 0.3)
> i
font-size: 1.25em
display: block
.search
margin: auto 0
width: 100%
position: relative
display: flex
flex-direction: row
> label
padding: 0.5rem 0.7rem
text-decoration: none
white-space: nowrap
font-size: 0.9em
background-color: RGBA($white, 0.02)
color: RGB($white)
border-radius: 2px 0 0 2px
> input
margin: 0
padding: 0.5rem 0.7rem
width: 100%
text-decoration: none
white-space: nowrap
font-size: 0.9em
background-color: RGBA($white, 0.02)
color: RGB($white)
border: 0 solid transparent
border-left: 1px solid RGBA($white, 0.1)
border-radius: 0 2px 2px 0
&:hover
background-color: RGBA($white, 0.1)
border-left: 1px solid transparent
&:focus-visible, &:focus
background-color: RGBA($white, 0.1)
outline: 0 solid transparent
border-left: 1px solid transparent
border-radius: 0 2px 0 0

View file

@ -0,0 +1,51 @@
.search-hint
position: absolute
display: none
flex-direction: column
text-decoration: none
white-space: nowrap
font-size: 0.9em
background-color: RGBA($white, 0.1)
backdrop-filter: blur(10px)
border-top: 1px solid RGBA($white, 0.1)
border-radius: 0 0 2px 2px
overflow: hidden
z-index: 999
> p
margin: 0
padding: 0.5rem 0.75rem
width: 100%
white-space: nowrap
text-overflow: ellipsis
color: RGB($white)
overflow: hidden
> button
padding: 0.5rem 0.75rem
width: 100%
white-space: nowrap
text-overflow: ellipsis
font-size: 0.9rem
text-align: left
background-color: transparent
color: RGB($white)
border: 0 solid transparent
overflow: hidden
&:hover
background-color: RGBA($white, 0.1)
color: RGB($primary)
border: 0 solid transparent

View file

@ -5,33 +5,19 @@ $secondary: var(--secondary)
$gold: var(--gold)
$silver: var(--silver)
$bronze: var(--bronze)
$darkBlue: var(--darkBlue)
@mixin button($color)
text-decoration: none
text-align: center
white-space: nowrap
background-color: RGBA($color, 0.02)
color: RGB($color)
border-radius: 2px
border: 0 solid transparent
transition: background-color 0.2s ease-in-out, transform 0.1s ease-in-out
&:hover
background-color: RGBA($color, 0.3)
transform: translateY(-0.1rem)
@import url('https://fonts.googleapis.com/css2?family=Merriweather:ital,wght@0,300;0,400;0,700;0,900;1,300;1,400;1,700&display=swap')
\:root
--black: 21, 21, 21
--black: 31, 27, 21
--white: 232, 227, 227
--primary: 210, 206, 97
--secondary: 185, 77, 77
--gold: 255, 222, 70
--silver: 229, 220, 206
--bronze: 193, 145, 69
--darkBlue: 9, 9, 39
@import url('https://fonts.googleapis.com/css2?family=Merriweather:ital,wght@0,300;0,400;0,700;0,900;1,300;1,400;1,700&display=swap')
@import "button"
@import "hint"
*
box-sizing: border-box
@ -46,16 +32,19 @@ body
padding: 0
display: flex
flex-direction: row
background-color: RGB($darkBlue)
background-color: RGB($black)
color: RGB($white)
.background
position: fixed
inset: 0
overflow: hidden
z-index: 1
> img
width: 100%
height: 100%
object-fit: cover
position: absolute
z-index: 1
.app
margin: 0 auto
@ -68,58 +57,86 @@ body
display: flex
flex-direction: column
background-color: rgba($darkBlue, 0.7)
background-color: rgba($black, 0.4)
backdrop-filter: blur(5px)
z-index: 2
> table
width: 100%
header
padding: 1rem
background-color: RGBA($darkBlue, 0.7)
> img
margin-bottom: 1rem
.table
width: 100%
height: auto
text-align: center
background-color: rgba($black, 0.7)
border-radius: 2px
overflow: hidden
nav
margin-top: 0.3rem
> table
width: 100%
border-collapse: collapse
> tbody
> tr
> th
padding: 0.6rem
background-image: linear-gradient(to bottom, RGBA($white, 0.05), transparent)
text-align: left
font-weight: bolder
> td
padding: 0.5rem
text-align: left
&.player
color: RGB($secondary)
text-shadow: 0 0 5px RGBA($secondary, 0.7)
i
padding: 0.25rem
color: RGB($black)
border-radius: 2px
&.first
background-color: RGBA($gold, 0.6)
&.second
background-color: RGBA($silver, 0.6)
&.third
background-color: RGBA($bronze, 0.6)
&:nth-child(odd) > td
background-color: RGBA($white, 0.01)
header
padding: 1rem 1rem 0
display: flex
flex-direction: column
gap: 0.4rem
background-image: linear-gradient(to bottom, RGB(var(--black)), transparent)
> hr
margin: 0
width: 100%
border: none
border-bottom: 1px solid RGBA($white, 0.1)
.title
margin-bottom: 1rem
height: auto
> img
width: 100%
height: auto
max-width: 100%
nav, nav > form
padding: 0
width: 100%
display: flex
flex-direction: row
justify-content: center
gap: 0.4rem
> span
.spacer
width: 100%
> a
margin: auto 0.15rem
padding: 0.5rem 0.7rem
text-decoration: none
white-space: nowrap
font-size: 0.9em
color: RGB($primary)
&.button
@include button($white)
&.primary
@include button($primary)
&.secondary
@include button($secondary)
> i
font-size: 1.25em
display: block
.flash
display: flex
flex-direction: column
@ -133,7 +150,7 @@ nav
width: 100%
position: relative
background-color: RGB($darkBlue)
background-color: RGB($black)
color: RGB($primary)
transition: background-color 0.2s ease-in-out, padding 0.2s ease-in-out
@ -230,7 +247,7 @@ main
display: flex
flex-direction: column
background-color: rgba($darkBlue, 0.7)
background-color: rgba($black, 0.7)
border-radius: 2px
> h2
@ -242,28 +259,6 @@ main
margin: 0 0 1rem 0
font-size: 1em
.button
margin: 0
padding: 0.5rem 0.7rem
text-decoration: none
white-space: nowrap
font-size: 0.9em
color: RGB($primary)
> i
font-size: 1.25em
display: block
@include button($white)
&.primary
@include button($primary)
&.secondary
@include button($secondary)
> table
width: 100%
border-collapse: collapse
@ -300,7 +295,7 @@ main
border: 1px solid RGB($white)
border-radius: 2px
background-color: RGB($darkBlue)
background-color: RGB($black)
color: RGB($white)
&:focus
@ -344,7 +339,7 @@ footer
width: 100%
display: flex
flex-direction: row
background-color: RGBA($darkBlue, 0.7)
background-image: linear-gradient(to top, RGB(var(--black)), transparent)
> p
margin: 0

View file

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 152 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View file

@ -1,9 +1,9 @@
{% extends "base.html" %}
{% block nav %}
<hr>
<nav>
<a href="{{ url_for('auth.account', action='logout') }}" class="button">Your Profile</a>
<!-- This is a spacer -->
<span></span>
<span class="spacer"></span>
<a href="{{ url_for('auth.account', action='logout') }}" class="button secondary">Logout</a>
</nav>
{% endblock %}

View file

@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Front Rooms Highscores</title>
<script src="https://unpkg.com/@phosphor-icons/web"></script>
{% assets "scripts" %}<script src="{{ ASSET_URL }}"></script>{% endassets %}
{% assets "styles" %}<link rel="stylesheet" href="{{ ASSET_URL }}" type="text/css">{% endassets %}
</head>
<body>
<div class="search-hint">
<p>Start typing to see results...</p>
</div>
<picture class="background">
<source srcset="{{ url_for('static', filename='background.webp') }}">
<img src="{{ url_for('static', filename='background.png') }}" alt="The Front Rooms Level select render">
</picture>
<div class="app">
<!-- Get flashed lol -->
<div class="flash">
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
{% for category, message in messages %}
<p class="{{ category }}" onclick="this.remove()"><span><i class="ph-bold ph-x"></i></span>{{ message }}</p>
{% endfor %}
{% endif %}
{% endwith %}
</div>
<header>
<picture class="title">
<source srcset="{{ url_for('static', filename='title.webp') }}">
<img src="{{ url_for('static', filename='title.png') }}" alt="The Front Rooms logo">
</picture>
<nav>
<a href="{{ url_for('views.index') }}" class="button">Scores</a>
<a href="{{ url_for('views.about') }}" class="button">About</a>
<a href="{{ url_for('views.about') }}" class="button">Updates</a>
<span class="spacer"></span>
{% if current_user.is_authenticated %}
<a href="{{ url_for('auth.account') }}" class="button primary">{{ current_user.username }}</a>
{% else %}
<a href="{{ url_for('auth.auth') }}" class="button primary"><i class="ph ph-user-circle"></i></a>
{% endif %}
</nav>
<!-- Secondary nav bar for page specific content -->
{% block nav %}{% endblock %}
</header>
<main>{% block content %}{% endblock %}</main>
<footer><p>By Project Redacted | <a href="https://github.com/Fluffy-Bean/GameExpo23">Server Source</a></p></footer>
</div>
</body>
</html>

View file

@ -0,0 +1,72 @@
{% extends "base.html" %}
{% block nav %}
<hr>
<nav>
<form method="GET" action="{{ url_for('views.index') }}">
<select name="diff" class="button">
<option value="0" {% if diff==0 %}selected{% endif %}>Level 1</option>
<option value="1" {% if diff==1 %}selected{% endif %}>Level 2</option>
<option value="2" {% if diff==2 %}selected{% endif %}>Level 3</option>
<option value="3" {% if diff==3 %}selected{% endif %}>Normal</option>
<option value="4" {% if diff==4 %}selected{% endif %}>Hard</option>
</select>
<select name="ver" class="button">
<option value="alpha" {% if ver=="alpha" %}selected{% endif %}>Alpha</option>
<option value="beta" {% if ver=="beta" %}selected{% endif %}>Beta</option>
<option value="1.0" {% if ver=="1.0" %}selected{% endif %}>1.0</option>
<option value="1.1" {% if ver=="1.1" %}selected{% endif %}>1.1</option>
</select>
<span class="search">
<label for="user">Username</label>
<input type="text" name="user" {% if user %}value="{{ user }}"{% endif %} autocomplete="off"/>
</span>
<button class="button"><i class="ph ph-magnifying-glass"></i></button>
</form>
</nav>
{% endblock %}
{% block content %}
{% if scores %}
<div class="table">
<table>
<tr>
<th>Position</th>
<th>Player</th>
<th>Time</th>
<th>Scored at</th>
<th>Version</th>
</tr>
{% for score in scores %}
<tr>
{% if loop.index == 1 %}
<td><i class="first ph-fill ph-crown"></i></td>
{% elif loop.index == 2 %}
<td><i class="second ph-duotone ph-crown"></i></td>
{% elif loop.index == 3 %}
<td><i class="third ph ph-crown"></i></td>
{% else %}
<td>{{ loop.index }}</td>
{% endif %}
{% if score.users.id == current_user.id %}
<td class="player">{{ score.users.username }}</td>
{% else %}
<td>{{ score.users.username }}</td>
{% endif %}
<td>{{ score.score }}</td>
<td>{{ score.scored_at }}</td>
<td>{{ score.version }}</td>
</tr>
{% endfor %}
</table>
</div>
{% else %}
<div class="center-text">
<h2>No scores</h2>
<p>Go set some!</p>
</div>
{% endif %}
{% endblock %}

33
TFR/server/views.py Normal file
View file

@ -0,0 +1,33 @@
from flask import Blueprint, request, render_template, abort
from server.models import Scores, Users
from server.config import GAME_VERSION, MAX_TOP_SCORES
blueprint = Blueprint("views", __name__)
@blueprint.route("/")
def index():
diff_arg = request.args.get("diff", 0)
ver_arg = request.args.get("ver", GAME_VERSION)
user_arg = request.args.get("user", None)
scores = Scores.query.filter_by(difficulty=diff_arg)
if ver_arg:
scores = scores.filter_by(version=ver_arg)
if user_arg:
if user := Users.query.filter_by(username=user_arg).first():
scores = scores.filter_by(user_id=user.id)
print(user.id)
else:
abort(404, "User not found")
scores = scores.order_by(Scores.score.asc()).limit(MAX_TOP_SCORES).all()
return render_template("scores.html", scores=scores, diff=int(diff_arg), ver=ver_arg, user=user_arg)
@blueprint.route("/about")
def about():
return render_template("about.html")

View file

@ -12,45 +12,44 @@ services:
- ./Caddy/data:/data
- ./Caddy/config:/config
environment:
DOMAIN_HIGHSCORE: tfr.leggy.dev
DOMAIN_EXPO: expo.leggy.dev
THE_FRONT_ROOMS_DOMAIN: ${THE_FRONT_ROOMS_DOMAIN}
GAME_EXPO_DOMAIN: ${GAME_EXPO_DOMAIN}
links:
- highscore
- tfr
- expo
db:
image: postgres:alpine
restart: unless-stopped
# ports:
# - "5432:5432"
ports:
- "5432:5432"
volumes:
- ./Postgres/data:/var/lib/postgresql/data
environment:
POSTGRES_USER: pguser
POSTGRES_PASSWORD: secret
POSTGRES_DB: database
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
links:
- highscore
- tfr
highscore:
build: ./Highscore-Server
tfr:
build: TFR
restart: unless-stopped
volumes:
- ./Highscore-Server/storage:/data/storage
- ./Highscore-Server/logs:/data/logs
- ./TFR/storage:/data/storage
- ./TFR/logs:/data/logs
environment:
FLASK_KEY: secret
BEARER_TOKEN: 1234
DB_USER: pguser
DB_PASSWORD: secret
FLASK_KEY: ${THE_FRONT_ROOMS_SECRETE_KEY}
DB_USER: ${POSTGRES_USER}
DB_PASSWORD: ${POSTGRES_PASSWORD}
DB_HOST: db
DB_NAME: database
DB_NAME: ${POSTGRES_DB}
expo:
build: ./DV8-Expo
build: GameExpo
restart: unless-stopped
volumes:
- ./DV8-Expo/storage:/data/storage
- ./DV8-Expo/logs:/data/logs
- ./GameExpo/storage:/data/storage
- ./GameExpo/logs:/data/logs
environment:
FLASK_KEY: secret
FLASK_KEY: ${GAME_EXPO_SECRETE_KEY}