mirror of
https://github.com/csd4ni3l/grass_touching_captcha.git
synced 2026-01-01 12:33:45 +01:00
Add achievements, profile and profile settings, add flash when accessing unathorized content
This commit is contained in:
14
constants.py
14
constants.py
@@ -12,4 +12,18 @@ DATABASE_FILE = "data.db"
|
|||||||
MINIMUM_OCR_SIMILARITY = 0.7
|
MINIMUM_OCR_SIMILARITY = 0.7
|
||||||
OCR_CHALLENGE_LENGTH = 1
|
OCR_CHALLENGE_LENGTH = 1
|
||||||
|
|
||||||
|
ACHIEVEMENTS = [
|
||||||
|
[1, "I went outside!", "Brag to your friends with this one! You went outside the first time in your life. Continue on your journey."],
|
||||||
|
[3, "Keeping up the streak!", "You went outside 3 times. Great job! (You should get one, btw)"],
|
||||||
|
[7, "Out for a week!", "7 days of breathing fresh air, you're practically a nature veteran."],
|
||||||
|
[14, "Two Weeks in the Wild", "Careful, you might be starting to get a tan."],
|
||||||
|
[30, "One with the Outdoors", "An entire month! Are you sure you’re still a gamer?"],
|
||||||
|
[50, "Grass Connoisseur", "You can now identify at least three different types of grass by touch alone."],
|
||||||
|
[100, "Master of Chlorophyll", "The grass respects you now."],
|
||||||
|
[200, "Photosynthesis Apprentice", "You spend so much time outside that plants start thinking you're one of them."],
|
||||||
|
[365, "Solar-Powered", "A whole year of going outside, you've unlocked infinite vitamin D."],
|
||||||
|
[500, "Grass Whisperer", "You hear the lawn speaking to you. It's weirdly supportive."],
|
||||||
|
[1000, "Legendary Lawn Treader", "Songs will be sung of your bravery and your sandals."],
|
||||||
|
]
|
||||||
|
|
||||||
UPLOAD_DIR = os.path.join(os.getcwd(), UPLOAD_DIR)
|
UPLOAD_DIR = os.path.join(os.getcwd(), UPLOAD_DIR)
|
||||||
201
main.py
201
main.py
@@ -1,6 +1,6 @@
|
|||||||
from flask import Flask, redirect, url_for, render_template, request, Response, send_from_directory, g, flash
|
from flask import Flask, redirect, url_for, render_template, request, Response, send_from_directory, g, flash
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from constants import RICKROLL_LINK, UPLOAD_DIR, MINIMUM_COSINE_SIMILARITY, MINIMUM_OCR_SIMILARITY, DATABASE_FILE
|
from constants import RICKROLL_LINK, UPLOAD_DIR, MINIMUM_COSINE_SIMILARITY, MINIMUM_OCR_SIMILARITY, DATABASE_FILE, ACHIEVEMENTS
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
|
|
||||||
from jina import get_grass_touching_similarity
|
from jina import get_grass_touching_similarity
|
||||||
@@ -22,6 +22,22 @@ challenges = {}
|
|||||||
|
|
||||||
os.makedirs("uploads", exist_ok=True)
|
os.makedirs("uploads", exist_ok=True)
|
||||||
|
|
||||||
|
def time_ago(seconds):
|
||||||
|
seconds = int(seconds)
|
||||||
|
if seconds < 60:
|
||||||
|
return f"{seconds} second{'s' if seconds != 1 else ''} ago"
|
||||||
|
elif seconds < 3600:
|
||||||
|
minutes = seconds // 60
|
||||||
|
return f"{minutes} minute{'s' if minutes != 1 else ''} ago"
|
||||||
|
elif seconds < 86400:
|
||||||
|
hours = seconds // 3600
|
||||||
|
return f"{hours} hour{'s' if hours != 1 else ''} ago"
|
||||||
|
else:
|
||||||
|
days = seconds // 86400
|
||||||
|
return f"{days} day{'s' if days != 1 else ''} ago"
|
||||||
|
|
||||||
|
app.jinja_env.filters['timeago'] = time_ago
|
||||||
|
|
||||||
def get_db():
|
def get_db():
|
||||||
db = getattr(g, '_database', None)
|
db = getattr(g, '_database', None)
|
||||||
if db is None:
|
if db is None:
|
||||||
@@ -46,6 +62,37 @@ def get_db():
|
|||||||
db.commit()
|
db.commit()
|
||||||
return db
|
return db
|
||||||
|
|
||||||
|
class User(flask_login.UserMixin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@login_manager.user_loader
|
||||||
|
def user_loader(user_id):
|
||||||
|
user = User()
|
||||||
|
user.id = user_id
|
||||||
|
return user
|
||||||
|
|
||||||
|
@login_manager.unauthorized_handler
|
||||||
|
def unauthorized_handler():
|
||||||
|
flash("Why are you trying to access content reserved for logged in users? Just go outside, touch some grass and register your own account.", "error")
|
||||||
|
return redirect(url_for("login"))
|
||||||
|
|
||||||
|
@app.before_request
|
||||||
|
def check_banned():
|
||||||
|
if not hasattr(flask_login.current_user, "id"):
|
||||||
|
return
|
||||||
|
|
||||||
|
username = flask_login.current_user.id
|
||||||
|
|
||||||
|
cur = get_db().cursor()
|
||||||
|
cur.execute("SELECT banned FROM Users WHERE username = ?", (username,))
|
||||||
|
row = cur.fetchone()
|
||||||
|
cur.close()
|
||||||
|
|
||||||
|
if row is None or row[0]:
|
||||||
|
flash("Imagine forgetting to touch grass so you get banned from my app. Such a discord moderator you are. You have no life. Just go outside.", "error")
|
||||||
|
flask_login.logout_user()
|
||||||
|
return redirect("/")
|
||||||
|
|
||||||
@app.teardown_appcontext
|
@app.teardown_appcontext
|
||||||
def close_connection(exception):
|
def close_connection(exception):
|
||||||
db = getattr(g, '_database', None)
|
db = getattr(g, '_database', None)
|
||||||
@@ -69,36 +116,6 @@ def check_grass_touching_bans():
|
|||||||
|
|
||||||
threading.Thread(target=check_grass_touching_bans, daemon=True).start()
|
threading.Thread(target=check_grass_touching_bans, daemon=True).start()
|
||||||
|
|
||||||
class User(flask_login.UserMixin):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@login_manager.user_loader
|
|
||||||
def user_loader(user_id):
|
|
||||||
user = User()
|
|
||||||
user.id = user_id
|
|
||||||
return user
|
|
||||||
|
|
||||||
@login_manager.unauthorized_handler
|
|
||||||
def unauthorized_handler():
|
|
||||||
return redirect(url_for("login"))
|
|
||||||
|
|
||||||
@app.before_request
|
|
||||||
def check_banned():
|
|
||||||
if not hasattr(flask_login.current_user, "id"):
|
|
||||||
return
|
|
||||||
|
|
||||||
username = flask_login.current_user.id
|
|
||||||
|
|
||||||
cur = get_db().cursor()
|
|
||||||
cur.execute("SELECT banned FROM Users WHERE username = ?", (username,))
|
|
||||||
row = cur.fetchone()
|
|
||||||
cur.close()
|
|
||||||
|
|
||||||
if row is None or row[0]:
|
|
||||||
flash("Imagine forgetting to touch grass so you get banned from my app. Such a discord moderator you are. You have no life. Just go outside.", "error")
|
|
||||||
flask_login.logout_user()
|
|
||||||
return redirect("/")
|
|
||||||
|
|
||||||
def resize_image_file(path, max_side=256, fmt="JPEG"):
|
def resize_image_file(path, max_side=256, fmt="JPEG"):
|
||||||
img = Image.open(path)
|
img = Image.open(path)
|
||||||
scale = max_side / max(img.size)
|
scale = max_side / max(img.size)
|
||||||
@@ -220,6 +237,60 @@ def leaderboard():
|
|||||||
|
|
||||||
return render_template("leaderboard.jinja2", users=users, current_username=username)
|
return render_template("leaderboard.jinja2", users=users, current_username=username)
|
||||||
|
|
||||||
|
@app.route("/achievements")
|
||||||
|
@flask_login.login_required
|
||||||
|
def achievements():
|
||||||
|
username = flask_login.current_user.id
|
||||||
|
|
||||||
|
cur = get_db().cursor()
|
||||||
|
cur.execute("SELECT grass_touching_count FROM Users WHERE username = ?", (username,))
|
||||||
|
|
||||||
|
row = cur.fetchone()
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
return Response("DB is not healthy.")
|
||||||
|
|
||||||
|
return render_template("achievements.jinja2", achievements=ACHIEVEMENTS, grass_touching_count=row[0])
|
||||||
|
|
||||||
|
@app.route("/profile")
|
||||||
|
@flask_login.login_required
|
||||||
|
def profile():
|
||||||
|
username = flask_login.current_user.id
|
||||||
|
|
||||||
|
cur = get_db().cursor()
|
||||||
|
|
||||||
|
cur.execute("SELECT grass_touching_count, last_grass_touch_time FROM Users WHERE username = ?", (username,))
|
||||||
|
|
||||||
|
row = cur.fetchone()
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
return Response("DB is not healthy.")
|
||||||
|
|
||||||
|
grass_touching_count, last_grass_touch_time = row
|
||||||
|
|
||||||
|
return render_template("profile.jinja2", username=username, grass_touching_count=grass_touching_count, last_grass_touch_time=last_grass_touch_time, your_account=True)
|
||||||
|
|
||||||
|
@app.route("/profile/<username>")
|
||||||
|
def public_profile(username):
|
||||||
|
cur = get_db().cursor()
|
||||||
|
|
||||||
|
cur.execute("SELECT grass_touching_count, last_grass_touch_time FROM Users WHERE username = ?", (username,))
|
||||||
|
|
||||||
|
row = cur.fetchone()
|
||||||
|
|
||||||
|
cur.close()
|
||||||
|
|
||||||
|
if not row:
|
||||||
|
return Response("DB is not healthy.")
|
||||||
|
|
||||||
|
grass_touching_count, last_grass_touch_time = row
|
||||||
|
|
||||||
|
return render_template("profile.jinja2", username=username, grass_touching_count=grass_touching_count, last_grass_touch_time=last_grass_touch_time, your_account=False)
|
||||||
|
|
||||||
@app.route("/login", methods=["GET", "POST"])
|
@app.route("/login", methods=["GET", "POST"])
|
||||||
def login():
|
def login():
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
@@ -281,6 +352,74 @@ def register():
|
|||||||
|
|
||||||
return redirect(url_for("login"))
|
return redirect(url_for("login"))
|
||||||
|
|
||||||
|
@app.route("/change_username", methods=["POST"])
|
||||||
|
@flask_login.login_required
|
||||||
|
def change_username():
|
||||||
|
username = flask_login.current_user.id
|
||||||
|
new_username = request.form["new_username"]
|
||||||
|
|
||||||
|
cur = get_db().cursor()
|
||||||
|
|
||||||
|
cur.execute("UPDATE Users SET username = ? WHERE username = ?", (new_username, username))
|
||||||
|
cur.execute("UPDATE Images SET username = ? WHERE username = ?", (new_username, username))
|
||||||
|
|
||||||
|
get_db().commit()
|
||||||
|
cur.close()
|
||||||
|
|
||||||
|
flask_login.logout_user()
|
||||||
|
return redirect(url_for("login"))
|
||||||
|
|
||||||
|
@app.route("/change_password", methods=["POST"])
|
||||||
|
@flask_login.login_required
|
||||||
|
def change_password():
|
||||||
|
username = flask_login.current_user.id
|
||||||
|
new_password, confirm_password = request.form["new_password"], request.form["confirm_password"]
|
||||||
|
|
||||||
|
if not secrets.compare_digest(new_password, confirm_password):
|
||||||
|
return Response("Passwords do not match.")
|
||||||
|
|
||||||
|
cur = get_db().cursor()
|
||||||
|
|
||||||
|
salt = bcrypt.gensalt()
|
||||||
|
hashed_password = bcrypt.hashpw(new_password.encode(), salt)
|
||||||
|
|
||||||
|
cur.execute("UPDATE Users SET password = ?, password_salt = ? WHERE username = ?", (hashed_password, salt, username))
|
||||||
|
|
||||||
|
get_db().commit()
|
||||||
|
cur.close()
|
||||||
|
|
||||||
|
flask_login.logout_user()
|
||||||
|
return redirect(url_for("login"))
|
||||||
|
|
||||||
|
@app.route("/delete_data", methods=["POST"])
|
||||||
|
@flask_login.login_required
|
||||||
|
def delete_data():
|
||||||
|
username = flask_login.current_user.id
|
||||||
|
|
||||||
|
cur = get_db().cursor()
|
||||||
|
|
||||||
|
cur.execute("DELETE FROM Users WHERE username = ?", (username,))
|
||||||
|
|
||||||
|
get_db().commit()
|
||||||
|
cur.close()
|
||||||
|
|
||||||
|
flask_login.logout_user()
|
||||||
|
return redirect(url_for("login"))
|
||||||
|
|
||||||
|
@app.route("/reset_data", methods=["POST"])
|
||||||
|
@flask_login.login_required
|
||||||
|
def reset_data():
|
||||||
|
username = flask_login.current_user.id
|
||||||
|
|
||||||
|
cur = get_db().cursor()
|
||||||
|
|
||||||
|
cur.execute("UPDATE Users SET last_grass_touch_time = ?, grass_touching_count = ? WHERE username = ?", (time.time(), 1, username))
|
||||||
|
|
||||||
|
get_db().commit()
|
||||||
|
cur.close()
|
||||||
|
|
||||||
|
return redirect("/")
|
||||||
|
|
||||||
@app.route("/uploads/<filename>")
|
@app.route("/uploads/<filename>")
|
||||||
def uploads(filename):
|
def uploads(filename):
|
||||||
return send_from_directory("uploads", filename)
|
return send_from_directory("uploads", filename)
|
||||||
|
|||||||
51
templates/achievements.jinja2
Normal file
51
templates/achievements.jinja2
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{% extends "base.jinja2" %}
|
||||||
|
{% block title %}Grass Touching Achievements{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
|
<div class="container-fluid d-flex justify-content-center">
|
||||||
|
<a class="navbar-brand" href="/">Grass Touching Captcha</a>
|
||||||
|
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||||
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/">Home</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/info">Information</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/leaderboard">Leaderboard</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/submit_grasstouching">Submit Grass Touching</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link active" aria-current="page" href="/achievements">Achievements</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/profile">Profile</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/logout">Logout</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container my-4">
|
||||||
|
<div class="row g-4">
|
||||||
|
{% for achievement in achievements %}
|
||||||
|
{% set unlocked = grass_touching_count >= achievement[0] %}
|
||||||
|
<div class="col-12 col-sm-6 col-md-4 col-lg-3">
|
||||||
|
<div class="card h-100 {% if unlocked %}border-success shadow{% else %}border-secondary text-muted{% endif %}">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">
|
||||||
|
{% if unlocked %}
|
||||||
|
✅ {{ achievement[1] }}
|
||||||
|
{% else %}
|
||||||
|
🔒 {{ achievement[1] }}
|
||||||
|
{% endif %}
|
||||||
|
</h5>
|
||||||
|
<h6 class="card-subtitle mb-2 {% if unlocked %}text-success{% else %}text-secondary{% endif %}">
|
||||||
|
You have to go outside {{ achievement[0] }} times to get this!
|
||||||
|
</h6>
|
||||||
|
<p class="card-text">{{ achievement[2] }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<div class="modal-dialog">
|
<div class="modal-dialog">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header bg-danger text-white">
|
<div class="modal-header bg-danger text-white">
|
||||||
<h5 class="modal-title" id="errorModalLabel">Error</h5>
|
<h5 class="modal-title" id="errorModalLabel">Imagine not going outside.</h5>
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body" id="errorModalBody">
|
<div class="modal-body" id="errorModalBody">
|
||||||
|
|||||||
@@ -29,9 +29,15 @@
|
|||||||
<a class="nav-link" href="/login">Login</a>
|
<a class="nav-link" href="/login">Login</a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/achievements">Achievements</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="/submit_grasstouching">Submit Grass Touching</a>
|
<a class="nav-link" href="/submit_grasstouching">Submit Grass Touching</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/profile">Profile</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="/logout">Logout</a>
|
<a class="nav-link" href="/logout">Logout</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -29,9 +29,15 @@
|
|||||||
<a class="nav-link" href="/login">Login</a>
|
<a class="nav-link" href="/login">Login</a>
|
||||||
</li>
|
</li>
|
||||||
{% else %}
|
{% else %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/achievements">Achievements</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="/submit_grasstouching">Submit Grass Touching</a>
|
<a class="nav-link" href="/submit_grasstouching">Submit Grass Touching</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/profile">Profile</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="/logout">Logout</a>
|
<a class="nav-link" href="/logout">Logout</a>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
124
templates/profile.jinja2
Normal file
124
templates/profile.jinja2
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
{% extends "base.jinja2" %}
|
||||||
|
{% block title %}Grass Touching Profile{% endblock %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<nav class="navbar navbar-expand-lg navbar-dark bg-dark">
|
||||||
|
<div class="container-fluid d-flex justify-content-center">
|
||||||
|
<a class="navbar-brand" href="/">Grass Touching Captcha</a>
|
||||||
|
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
|
||||||
|
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||||
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/">Home</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/info">Information</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/leaderboard">Leaderboard</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/submit_grasstouching">Submit Grass Touching</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/achievements">Achievements</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link active" aria-current="page" href="/profile">Profile</a></li>
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/logout">Logout</a></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<div class="container my-4">
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title">Profile Overview{% if not your_account%} of {{ username}} {% endif %}</h2>
|
||||||
|
|
||||||
|
{% if your_account %}
|
||||||
|
<p class="mb-1"><strong>Logged in as:</strong> {{ username }}</p>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<p class="mb-1">Grass touches: <strong>{{ grass_touching_count }}</strong></p>
|
||||||
|
<p class="mb-3">Last touch: <strong>{{ (time.time() - last_grass_touch_time) | timeago }}</strong></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title">Achievements{% if not your_account%} of {{ username}} {% endif %}</h2>
|
||||||
|
|
||||||
|
<div class="row g-4">
|
||||||
|
{% for achievement in achievements %}
|
||||||
|
{% set unlocked = grass_touching_count >= achievement[0] %}
|
||||||
|
<div class="col-12 col-sm-6 col-md-4 col-lg-3">
|
||||||
|
<div class="card h-100 {% if unlocked %}border-success shadow{% else %}border-secondary text-muted{% endif %}">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">
|
||||||
|
{% if unlocked %}
|
||||||
|
✅ {{ achievement[1] }}
|
||||||
|
{% else %}
|
||||||
|
🔒 {{ achievement[1] }}
|
||||||
|
{% endif %}
|
||||||
|
</h5>
|
||||||
|
<h6 class="card-subtitle mb-2 {% if unlocked %}text-success{% else %}text-secondary{% endif %}">
|
||||||
|
You have to go outside {{ achievement[0] }} times to get this!
|
||||||
|
</h6>
|
||||||
|
<p class="card-text">{{ achievement[2] }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if your_account %}
|
||||||
|
<div class="card shadow-sm mt-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4>Change Username</h4>
|
||||||
|
<form method="POST" action="/change_username">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="newUsername" class="form-label">New Username</label>
|
||||||
|
<input type="text" class="form-control" id="newUsername" name="new_username" placeholder="Enter new username" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Update Username</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm mt-4">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4>Change Password</h4>
|
||||||
|
<form method="POST" action="/change_password">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="currentPassword" class="form-label">Current Password</label>
|
||||||
|
<input type="password" class="form-control" id="currentPassword" name="current_password" placeholder="Enter current password" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="newPassword" class="form-label">New Password</label>
|
||||||
|
<input type="password" class="form-control" id="newPassword" name="new_password" placeholder="Enter new password" required>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="confirmPassword" class="form-label">Confirm New Password</label>
|
||||||
|
<input type="password" class="form-control" id="confirmPassword" name="confirm_password" placeholder="Re-enter new password" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Update Password</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card shadow-sm mt-4 border-danger">
|
||||||
|
<div class="card-body">
|
||||||
|
<h4 class="text-danger">Danger Zone</h4>
|
||||||
|
<p class="text-muted">These actions cannot be undone!</p>
|
||||||
|
<form method="POST" action="/delete_account" class="d-inline">
|
||||||
|
<button type="submit" class="btn btn-danger me-2" onclick="return confirm('Are you sure you want to delete your account?');">
|
||||||
|
Delete Account
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<form method="POST" action="/reset_data" class="d-inline">
|
||||||
|
<button type="submit" class="btn btn-warning" onclick="return confirm('Are you sure you want to reset all your data?');">
|
||||||
|
Reset All Data
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
@@ -21,7 +21,13 @@
|
|||||||
<a class="nav-link" href="/leaderboard">Leaderboard</a>
|
<a class="nav-link" href="/leaderboard">Leaderboard</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link active" href="/submit_grasstouching">Submit Grass Touching</a>
|
<a class="nav-link" href="/achievements">Achievements</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link active" aria-current="page" href="/submit_grasstouching">Submit Grass Touching</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="/profile">Profile</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" href="/logout">Logout</a>
|
<a class="nav-link" href="/logout">Logout</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user