diff --git a/src/typefight/auth.py b/src/typefight/auth.py index 3f9bb9f..c021ecb 100644 --- a/src/typefight/auth.py +++ b/src/typefight/auth.py @@ -7,6 +7,7 @@ from typefight.db import get_db from typefight.utils import validate_country # from datetime import datetime +import functools import secrets # import hashlib @@ -18,7 +19,6 @@ def auth(): @bp.route("/register", methods=["POST"]) def register(): - # when method is POST, user is sending the register form player_name = request.form["username"] password = request.form["password"] country = validate_country(request.form["country"]) @@ -33,6 +33,8 @@ def register(): error = "Username is required." elif not password: error = "Password is required." + elif not country: + error = "Not a valid country." else: cur.execute( "SELECT player_uid FROM players WHERE player_name = %s;", (player_name,) @@ -116,3 +118,15 @@ def load_logged_in_user(): ) g.user = cur.fetchone() cur.close() + +def login_required(view): + @functools.wraps(view) + def wrapped_view(**kwargs): + + if g.user is not None: + return view(**kwargs) + else: + #TODO redirect to a login page or something + return "You need to be logged in to access this content" + + return wrapped_view diff --git a/src/typefight/game.py b/src/typefight/game.py index 5447153..be2cc1c 100644 --- a/src/typefight/game.py +++ b/src/typefight/game.py @@ -1,8 +1,13 @@ -from flask import render_template, Blueprint, jsonify +from flask import ( + render_template, Blueprint, jsonify, request, g, session +) from psycopg2.extras import RealDictCursor from typefight.db import get_db from typefight.utils import make_serializable +from typefight.auth import login_required + +import contextlib bp = Blueprint("game", __name__) @@ -12,25 +17,111 @@ def index(): @bp.route("/highscores") def get_highscores(): - # add POST method here so highscore will be updated if its higher than - # their previous one. - db = get_db() cur = db.cursor(cursor_factory=RealDictCursor) - cur.execute( - """ - SELECT scores.score, players.player_name, players.country FROM scores - LEFT JOIN players - ON players.player_uid = scores.player_uid; - """ - ) + quote_id = g.quote_id + scores_table = [] + + try: + cur.execute( + """ + SELECT scores.score, players.player_name, players.country + FROM scores + LEFT JOIN players + ON players.player_uid = scores.player_uid + WHERE scores.quote_uid = %s; + """, + (quote_id,) + ) + scores_table = cur.fetchall() + + except Exception as e: + print(f"An exception has occured during {request}:", e) + print("Exception TYPE: ", type(e)) - scores_table = cur.fetchall() cur.close() return jsonify(make_serializable(scores_table)) +@bp.route("/highscores/", methods=["POST"]) +@login_required +def set_highscore(score): + #TODO make sure "score" doesn't contain slash (/) + db = get_db() + + server_response = { + "success": False, + "message": None + } + + player_name = g.user["player_name"] + quote_id = g.quote_id + existing_score = None + + with contextlib.closing(db.cursor(cursor_factory=RealDictCursor)) as cur: + cur.execute( + """ + SELECT player_uid + FROM players + WHERE player_name = %s; + """, + (player_name, ) + ) + player_uid = cur.fetchone()["player_uid"] + + try: + cur.execute( + """ + SELECT players.player_uid, scores.score + FROM scores + LEFT JOIN players + ON players.player_uid = scores.player_uid + WHERE player_name = %s + AND quote_uid = %s; + """, + (player_name, quote_id) + ) + existing_score = cur.fetchone() + + except Exception as e: + server_response["message"] = e + print(f"An exception has occured during {request}:", e) + print("Exception TYPE:", type(e)) + + if existing_score is None: + cur.execute( + """ + INSERT INTO scores(player_uid, quote_uid, score) + VALUES (%s, %s, %s); + """, + (player_uid, quote_id, score) + ) + db.commit() + + server_response["success"] = True + server_response["message"] = f"Your new score {score} has been saved." + + elif score < existing_score["score"]: + cur.execute( + """ + UPDATE scores + SET score = %s + WHERE player_uid = %s + AND quote_uid = %s; + """, + (score, player_uid, quote_id) + ) + db.commit() + + server_response["success"] = True + server_response["message"] = f"New record! Your new score was faster than your previous one. Old score: {existing_score['score']} New score: {score}" + else: + server_response["success"] = True + server_response["message"] = f"Too slow! Best score: {existing_score['score']} Current score: {score}" + + return server_response + @bp.route("/quote") def get_quote(): db = get_db() @@ -38,11 +129,23 @@ def get_quote(): cur.execute( """ - SELECT quote FROM quotes ORDER BY RANDOM() LIMIT 1; + SELECT quote, quote_uid AS quote_id + FROM quotes + ORDER BY RANDOM() + LIMIT 1; """ ) - quote = cur.fetchone() + session["quote"] = quote cur.close() - return jsonify(quote) \ No newline at end of file + return jsonify(quote) + +@bp.before_app_request +def load_current_quote(): + quote = session.get("quote") + + if quote is None: + g.quote_id = None + else: + g.quote_id = quote["quote_id"] diff --git a/src/typefight/static/js/index.js b/src/typefight/static/js/index.js index a180ad6..41ee67c 100644 --- a/src/typefight/static/js/index.js +++ b/src/typefight/static/js/index.js @@ -3,12 +3,17 @@ const timer = document.getElementById("timer"); const startBtn = document.getElementById("start-btn"); const quoteElement = document.getElementById("quote"); const typedValueElement = document.getElementById("typed-value"); -const typedValueContainerElement = document.getElementById("typed-value-container"); +const typedValueContainerElement = document.getElementById( + "typed-value-container" +); typedValueElement.value = ""; // Getting all css custom properties const styles = getComputedStyle(document.documentElement); +let score = 0.0; +let quote = ""; +let quoteID = ""; let words = []; let wordIndex = 0; @@ -18,103 +23,112 @@ let start = 0; let timerInterval; async function startGame() { - const quote = await getQuote(); - start = new Date().getTime(); - - timer.innerText = "00:00"; - startBtn.innerText = "Restart"; - startBtn.setAttribute("onclick", "resetGame()"); - - typedValueElement.addEventListener("input", gameManager); - typedValueElement.removeAttribute("tabindex"); - words = quote.split(' '); - wordIndex = 0; + const { resQuote, resQuoteID } = await getQuote(); + quote = resQuote; + quoteID = resQuoteID; + + start = new Date().getTime(); + + timer.innerText = "00:00"; + startBtn.innerText = "Restart"; + startBtn.setAttribute("onclick", "resetGame()"); + + typedValueElement.addEventListener("input", gameManager); + typedValueElement.removeAttribute("tabindex"); + words = quote.split(" "); + wordIndex = 0; + + const spanWords = words.map((word) => `${word} `); + quoteElement.innerHTML = spanWords.join(""); + quoteElement.childNodes[0].className = "highlight"; + typedValueElement.value = ""; + typedValueElement.focus(); + + timerInterval = setInterval(() => { + seconds++; + if (seconds === 60) { + seconds = 0; + minutes++; + } - const spanWords = words.map(word => `${word} `) - quoteElement.innerHTML = spanWords.join(''); - quoteElement.childNodes[0].className = "highlight"; - typedValueElement.value = ""; - typedValueElement.focus(); - - timerInterval = setInterval(() => { - seconds++; - if (seconds === 60) { - seconds = 0; - minutes++; - } - - timer.innerText = - (minutes < 10 ? ("0" + minutes) : minutes) + ":" + - (seconds < 10 ? ("0" + seconds) : seconds); - }, 1000) + timer.innerText = + (minutes < 10 ? "0" + minutes : minutes) + + ":" + + (seconds < 10 ? "0" + seconds : seconds); + }, 1000); } function resetGame() { - clearInterval(timerInterval); - console.log(((new Date().getTime() - start) / 1000).toFixed(2)); + clearInterval(timerInterval); - typedValueElement.setAttribute("tabindex", "-1"); - startBtn.setAttribute("onclick", "startGame()"); - startBtn.innerText = "Start"; - startBtn.focus(); + typedValueElement.setAttribute("tabindex", "-1"); + startBtn.setAttribute("onclick", "startGame()"); + startBtn.innerText = "Start"; + startBtn.focus(); - seconds = 0; - minutes = 0; - start = 0; + seconds = 0; + minutes = 0; + start = 0; } function gameManager(e) { - const currentWord = words[wordIndex]; - const typedValue = typedValueElement.value; - - if (typedValue === currentWord && wordIndex === words.length - 1) { - finishGame(); - } else if (typedValue.endsWith(" ") && typedValue.trim() === currentWord) { - typedValueElement.value = ""; - wordIndex++; - - for (const wordElement of quoteElement.childNodes) { - wordElement.className = ""; - } - quoteElement.childNodes[wordIndex].className = "highlight"; - } else if (currentWord.startsWith(typedValue)) { - typedValueElement.className = ""; + const currentWord = words[wordIndex]; + const typedValue = typedValueElement.value; + + if (typedValue === currentWord && wordIndex === words.length - 1) { + finishGame(); + } else if (typedValue.endsWith(" ") && typedValue.trim() === currentWord) { + typedValueElement.value = ""; + wordIndex++; + + for (const wordElement of quoteElement.childNodes) { + wordElement.className = ""; } + quoteElement.childNodes[wordIndex].className = "highlight"; + } else if (currentWord.startsWith(typedValue)) { + typedValueElement.className = ""; + } } -function finishGame() { - clearInterval(timerInterval); - console.log(((new Date().getTime() - start) / 1000).toFixed(2)); +async function finishGame() { + clearInterval(timerInterval); + score = ((new Date().getTime() - start) / 1000).toFixed(2); + await fetch(`/highscores/${score}`, { + method: "POST", + }); - createHighscoresTable(game); + createHighscoresTable(game); - startBtn.remove(); - quoteElement.remove(); - typedValueContainerElement.remove(); + startBtn.remove(); + quoteElement.remove(); + typedValueContainerElement.remove(); - timer.style.color = styles.getPropertyValue("--color-text-accent"); - typedValueElement.removeEventListener("input", gameManager); + timer.style.color = styles.getPropertyValue("--color-text-accent"); + typedValueElement.removeEventListener("input", gameManager); } async function getQuote() { - const res = await fetch("/quote"); - const { quote } = await res.json(); + const res = await fetch("/quote"); + const { quote, quote_id } = await res.json(); - return quote; + return { + resQuote: quote, + resQuoteID: quote_id, + }; } async function getHighscores() { - const res = await fetch("/highscores"); - const highscores = await res.json(); + const res = await fetch("/highscores"); + const highscores = await res.json(); - return highscores; + return highscores; } async function createHighscoresTable(containerElement) { - const highscores = await getHighscores(); - const tableContainer = document.createElement("div"); - const scoresTable = document.createElement("table"); - const headers = ` + const highscores = await getHighscores(); + const tableContainer = document.createElement("div"); + const scoresTable = document.createElement("table"); + const headers = ` Highscores Name @@ -122,23 +136,23 @@ async function createHighscoresTable(containerElement) { `; - // can't do highscores.map here because it appends a weird "," - // after every , since it returns an array. - let highscoresData = ""; - for (const score of highscores) { - highscoresData += ` + // can't do highscores.map here because it appends a weird "," + // after every , since it returns an array. + let highscoresData = ""; + for (const score of highscores) { + highscoresData += ` ${score.score} ${score.player_name} ${score.country ? score.country : "-"} `; - } + } - scoresTable.innerHTML = headers; - scoresTable.lastElementChild.innerHTML = highscoresData; - tableContainer.className = "highscores-container"; - scoresTable.className = "highscores"; + scoresTable.innerHTML = headers; + scoresTable.lastElementChild.innerHTML = highscoresData; + tableContainer.className = "highscores-container"; + scoresTable.className = "highscores"; - tableContainer.appendChild(scoresTable); - containerElement.appendChild(tableContainer); -} \ No newline at end of file + tableContainer.appendChild(scoresTable); + containerElement.appendChild(tableContainer); +}