Compare commits
20 Commits
bfb1b8a21e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8e67e714d0 | |||
| 7c9aaa04ba | |||
| 93ecd4999f | |||
| 0d367e43a6 | |||
| 3afab4c7fa | |||
| 885d2559af | |||
| 8f1e4e60e1 | |||
| a47c2a3886 | |||
| 11d8621f9b | |||
| b683a2721a | |||
| 7a60ca468d | |||
| 1a5380e17e | |||
| 6e92e40f1c | |||
| b4e6b4c296 | |||
| 9700a5dc7f | |||
| 067f0e189d | |||
| 639a8e2fb0 | |||
| 25b03aa105 | |||
| 53cd3852aa | |||
| f67f377be1 |
@@ -1,33 +1,65 @@
|
||||
name: Build and push container image
|
||||
run-name: ${{ gitea.actor }} is building and pushing container image
|
||||
name: Deploy website
|
||||
run-name: ${{ gitea.actor }} is deploying update
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
env:
|
||||
GITEA_DOMAIN: git.alfieking.dev
|
||||
GITEA_REGISTRY_USER: acetheking987
|
||||
RESULT_IMAGE_NAME: acetheking987/alfieking.dev
|
||||
|
||||
jobs:
|
||||
build-and-push-image:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Log in to registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.GITEA_DOMAIN }}
|
||||
username: ${{ env.GITEA_REGISTRY_USER }}
|
||||
password: ${{ secrets.CONTAINER_REGISTRY_TOKEN }}
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
tags: ${{ env.GITEA_DOMAIN }}/${{ env.RESULT_IMAGE_NAME }}:latest
|
||||
- name: Setup SSH
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
ssh-keyscan ${{ secrets.SSH_HOST }} >> ~/.ssh/known_hosts
|
||||
|
||||
- name: Deploy
|
||||
run: |
|
||||
ssh ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << 'EOF'
|
||||
set -e
|
||||
|
||||
APP_DIR=/var/www/alfieking.dev
|
||||
RELEASES_DIR=$APP_DIR/releases
|
||||
TIMESTAMP=$(date +%Y%m%d%H%M%S)
|
||||
NEW_RELEASE=$RELEASES_DIR/$TIMESTAMP
|
||||
KEEP_RELEASES=5
|
||||
|
||||
echo "Creating release directory..."
|
||||
mkdir -p $NEW_RELEASE
|
||||
|
||||
echo "Cloning public repository..."
|
||||
git clone --depth 1 -b main \
|
||||
https://git.alfieking.dev/acetheking987/alfieking.dev.git \
|
||||
$NEW_RELEASE
|
||||
|
||||
echo "Installing dependencies..."
|
||||
source $APP_DIR/venv/bin/activate
|
||||
pip install -r $NEW_RELEASE/requirements.txt
|
||||
|
||||
echo "Switching symlink..."
|
||||
ln -sfn $NEW_RELEASE $APP_DIR/current
|
||||
|
||||
echo "Changing ownership..."
|
||||
chown -R www-data:www-data /var/www/alfieking.dev/releases/$TIMESTAMP
|
||||
chown -R www-data:www-data /var/www/alfieking.dev/current
|
||||
|
||||
echo "Restarting Gunicorn (downtime incomming XD)..."
|
||||
systemctl restart alfieking.dev.service
|
||||
|
||||
echo "Cleaning old releases..."
|
||||
CURRENT_TARGET=$(readlink -f $APP_DIR/current)
|
||||
|
||||
cd $RELEASES_DIR
|
||||
for dir in $(ls -dt */ | tail -n +$((KEEP_RELEASES+1))); do
|
||||
FULL_PATH="$RELEASES_DIR/${dir%/}"
|
||||
if [ "$FULL_PATH" != "$CURRENT_TARGET" ]; then
|
||||
rm -rf "$FULL_PATH"
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Deployment successful."
|
||||
EOF
|
||||
5
.gitignore
vendored
@@ -1,6 +1,3 @@
|
||||
.venv
|
||||
.env
|
||||
db.sqlite
|
||||
flask_session
|
||||
__pycache__
|
||||
app.log
|
||||
.venv
|
||||
22
dockerfile
@@ -1,22 +0,0 @@
|
||||
FROM python:alpine
|
||||
|
||||
# Set the working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the requirements file into the container
|
||||
COPY requirements.txt .
|
||||
|
||||
# Install the required packages
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
RUN pip install gunicorn
|
||||
|
||||
# Copy the rest of the application code into the container
|
||||
COPY src src
|
||||
COPY templates templates
|
||||
COPY static static
|
||||
|
||||
# Expose the port the app runs on
|
||||
EXPOSE 5000
|
||||
|
||||
# run the application
|
||||
ENTRYPOINT [ "gunicorn", "-b", ":5000", "--access-logfile", "-", "--error-logfile", "-", "src.wsgi:app" ]
|
||||
@@ -1,5 +1,7 @@
|
||||
#psycopg2-binary
|
||||
psycopg2-binary
|
||||
python-dotenv
|
||||
flask-session
|
||||
requests
|
||||
flask
|
||||
flask
|
||||
markdown
|
||||
gunicorn
|
||||
5
run.sh
@@ -1,5 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
[ ! -f .env ] || export $(grep -v '^#' .env | xargs)
|
||||
|
||||
flask --app src.wsgi.py --debug run
|
||||
60
src/dynamic_routes.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# Imports
|
||||
from flask import Blueprint, render_template, abort, request
|
||||
import os, markdown
|
||||
|
||||
try:
|
||||
from src.name import get_name
|
||||
except ImportError:
|
||||
from name import get_name
|
||||
|
||||
|
||||
# Create blueprint
|
||||
bp = Blueprint('dynamic_routes', __name__)
|
||||
template_folder = "templates"
|
||||
|
||||
|
||||
# helper func
|
||||
def get_path(file) -> str:
|
||||
return os.path.join(template_folder, "pages", file)
|
||||
|
||||
|
||||
# Get all files in folder
|
||||
def ListFiles(path):
|
||||
path = get_path(path)
|
||||
files = []
|
||||
for root, dirs, files_in_dir in os.walk(path):
|
||||
for file in files_in_dir:
|
||||
files.append(os.path.relpath(os.path.join(root, file), path))
|
||||
for dir in dirs:
|
||||
files.append(os.path.relpath(os.path.join(root, dir), path) + '/')
|
||||
return files
|
||||
|
||||
|
||||
# Catch-all route for generic pages
|
||||
@bp.route('/<path:filename>')
|
||||
def catch_all(filename):
|
||||
if os.path.exists(get_path(filename)):
|
||||
if os.path.isdir(get_path(filename)):
|
||||
return render_template(
|
||||
'bases/directory.html',
|
||||
directory=filename + "/" if not filename.endswith('/') else filename,
|
||||
pages=ListFiles(filename),
|
||||
name=get_name(request)
|
||||
)
|
||||
|
||||
return render_template(f'pages/{filename}', name=get_name(request))
|
||||
|
||||
elif os.path.exists(get_path(filename + '.html')):
|
||||
return render_template(f'pages/{filename}.html', name=get_name(request))
|
||||
|
||||
elif os.path.exists(get_path(filename + '.md')):
|
||||
output = markdown.markdown(open(get_path(filename + '.md'), "r").read())
|
||||
return render_template(
|
||||
f'bases/md.html',
|
||||
title = filename.split("/")[-1],
|
||||
markdown = output,
|
||||
name=get_name(request)
|
||||
)
|
||||
|
||||
else:
|
||||
abort(404, f"'{filename}' not found")
|
||||
44
src/errors.py
Normal file
@@ -0,0 +1,44 @@
|
||||
from flask import Blueprint, render_template, request
|
||||
from werkzeug.exceptions import HTTPException
|
||||
|
||||
try:
|
||||
from src.name import get_name
|
||||
except ImportError:
|
||||
from name import get_name
|
||||
|
||||
|
||||
bp = Blueprint("errors", __name__)
|
||||
|
||||
|
||||
@bp.route('/500')
|
||||
@bp.app_errorhandler(500)
|
||||
def internal_server_error(error:HTTPException=None):
|
||||
return render_template('errors/500.html', error=error, name=get_name(request)), 500
|
||||
|
||||
|
||||
@bp.route('/404')
|
||||
@bp.app_errorhandler(404)
|
||||
def not_found(error:HTTPException=None):
|
||||
return render_template('errors/404.html', error=error, name=get_name(request)), 404
|
||||
|
||||
|
||||
@bp.route('/400')
|
||||
@bp.app_errorhandler(400)
|
||||
def bad_request(error:HTTPException=None):
|
||||
return render_template('errors/400.html', error=error, name=get_name(request)), 400
|
||||
|
||||
|
||||
@bp.route('/idk')
|
||||
@bp.app_errorhandler(Exception)
|
||||
def idk(error:HTTPException=None):
|
||||
if not isinstance(error, HTTPException):
|
||||
error = HTTPException("I honestly dont know")
|
||||
error.code = 418
|
||||
|
||||
return render_template(
|
||||
'errors/error.html',
|
||||
code = error.code,
|
||||
description = error.description,
|
||||
err_name = error.name,
|
||||
name=get_name(request)
|
||||
), error.code
|
||||
80
src/main.py
Normal file
@@ -0,0 +1,80 @@
|
||||
# IMPORTS
|
||||
from flask import Flask, render_template, request
|
||||
from os import getenv as env
|
||||
import logging
|
||||
|
||||
try:
|
||||
import src.dynamic_routes as dynamic_routes
|
||||
import src.errors as errors
|
||||
import src.pg_log as pg_log
|
||||
from src.name import get_name
|
||||
except ImportError:
|
||||
import dynamic_routes, errors, pg_log
|
||||
from name import get_name
|
||||
from dotenv import load_dotenv
|
||||
load_dotenv()
|
||||
|
||||
|
||||
# LOGGING
|
||||
pg_log_handler = pg_log.PgLog(
|
||||
host = env("PG_HOST"),
|
||||
port = env("PG_PORT"),
|
||||
dbname = env("PG_DBNAME"),
|
||||
user= env("PG_USER"),
|
||||
password = env("PG_PASSWORD")
|
||||
)
|
||||
pg_log_handler.setLevel(logging.DEBUG)
|
||||
|
||||
stream_log_handler = logging.StreamHandler()
|
||||
stream_log_handler.setFormatter(
|
||||
logging.Formatter("%(asctime)s - %(levelname)s: %(message)s")
|
||||
)
|
||||
stream_log_handler.setLevel(logging.INFO)
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(logging.DEBUG)
|
||||
log.addHandler(stream_log_handler)
|
||||
log.addHandler(pg_log_handler)
|
||||
|
||||
werkzeug_logger = logging.getLogger("werkzeug")
|
||||
werkzeug_logger.setLevel(logging.DEBUG)
|
||||
werkzeug_logger.addHandler(pg_log_handler)
|
||||
werkzeug_logger.addHandler(stream_log_handler)
|
||||
|
||||
log.info("Logging initialized.")
|
||||
|
||||
|
||||
# CREATE FLASK APP
|
||||
app = Flask(
|
||||
__name__,
|
||||
template_folder="../templates",
|
||||
static_folder="../static"
|
||||
)
|
||||
log.info("Flask initialized.")
|
||||
|
||||
|
||||
# BLUEPRINTS
|
||||
app.register_blueprint(errors.bp, url_prefix="/errors")
|
||||
app.register_blueprint(dynamic_routes.bp, url_prefix="/")
|
||||
log.info("Blueprints registered.")
|
||||
|
||||
|
||||
# ROUTES
|
||||
@app.route("/")
|
||||
def index():
|
||||
return render_template("index.html", name=get_name(request))
|
||||
|
||||
@app.route("/toaster")
|
||||
def toaster():
|
||||
return render_template("toaster.html", name=get_name(request))
|
||||
|
||||
@app.route("/terminal")
|
||||
def terminal():
|
||||
return render_template("terminal.html", name=get_name(request))
|
||||
|
||||
|
||||
|
||||
# DEBUG (DONT RUN LIKE THIS IN PROD)
|
||||
if __name__ == "__main__":
|
||||
log.warning(f"RUNNING IN DEBUG MODE DO NOT USE FOR PRODUCTION!")
|
||||
app.run(debug=True)
|
||||
5
src/name.py
Normal file
@@ -0,0 +1,5 @@
|
||||
def get_name(req):
|
||||
if req.headers.get("Host") == "proot.uk":
|
||||
return "Toaster"
|
||||
else:
|
||||
return "Alfie King"
|
||||
35
src/pg_log.py
Normal file
@@ -0,0 +1,35 @@
|
||||
import psycopg2, logging
|
||||
|
||||
class PgLog(logging.StreamHandler):
|
||||
def __init__(self, host, port, dbname, user, password):
|
||||
super().__init__()
|
||||
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.dbname = dbname
|
||||
self.user = user
|
||||
self.password = password
|
||||
|
||||
self.conn = psycopg2.connect(
|
||||
database = dbname,
|
||||
user = user,
|
||||
password = password,
|
||||
host = host,
|
||||
port = port
|
||||
)
|
||||
|
||||
self.cursor = self.conn.cursor()
|
||||
|
||||
def __del__(self):
|
||||
self.cursor.close()
|
||||
self.conn.close()
|
||||
|
||||
def emit(self, record):
|
||||
self.cursor.execute(
|
||||
f"INSERT INTO logs (level, message) VALUES (%s, %s)",
|
||||
(
|
||||
record.levelname,
|
||||
record.getMessage(),
|
||||
)
|
||||
)
|
||||
self.conn.commit()
|
||||
@@ -1,48 +0,0 @@
|
||||
# Imports
|
||||
from flask import Blueprint, render_template
|
||||
from os import getenv as env
|
||||
import logging
|
||||
|
||||
import src.routes.snake as snake
|
||||
|
||||
|
||||
# Create blueprint
|
||||
bp = Blueprint(
|
||||
'error_handlers',
|
||||
__name__,
|
||||
template_folder=env('TEMPLATE_FOLDER', default='../templates'),
|
||||
static_folder=env('STATIC_FOLDER', default='../static')
|
||||
)
|
||||
|
||||
|
||||
# Create logger
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Route for 500 error
|
||||
@bp.route('/500')
|
||||
@bp.app_errorhandler(500)
|
||||
def internal_server_error(error=None):
|
||||
if error is not None:
|
||||
log.error("Internal server error: %s", error)
|
||||
return render_template('errors/500.html'), 500
|
||||
|
||||
|
||||
# Route for 404 error
|
||||
@bp.route('/404')
|
||||
@bp.app_errorhandler(404)
|
||||
def not_found(error=None):
|
||||
if error is not None:
|
||||
log.warning("Page not found: %s", error)
|
||||
scores = snake.get_leaderboard()
|
||||
token = snake.generate_start_token()
|
||||
return render_template('errors/404.html', scores=scores, token=token, cap_key=env('CAP_KEY', default='')), 404 if error is not None else 200
|
||||
|
||||
|
||||
# Route for 400 error
|
||||
@bp.route('/400')
|
||||
@bp.app_errorhandler(400)
|
||||
def bad_request(error=None):
|
||||
if error is not None:
|
||||
log.warning("Bad request: %s", error)
|
||||
return render_template('errors/400.html', error=error), 400
|
||||
@@ -1,65 +0,0 @@
|
||||
# Imports
|
||||
from flask import Blueprint, render_template, request, abort, send_file
|
||||
from os import getenv as env
|
||||
import logging, os
|
||||
|
||||
|
||||
# Create blueprint
|
||||
bp = Blueprint(
|
||||
'generic',
|
||||
__name__,
|
||||
template_folder=env('TEMPLATE_FOLDER', default='../templates'),
|
||||
static_folder=env('STATIC_FOLDER', default='../static')
|
||||
)
|
||||
|
||||
|
||||
# Create logger
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Route for index page
|
||||
@bp.route('/')
|
||||
def index():
|
||||
return render_template('index.html')
|
||||
|
||||
|
||||
# Route for favicon
|
||||
@bp.route('/favicon.ico')
|
||||
def favicon():
|
||||
return send_file('../static/content/other/favicon.ico')
|
||||
|
||||
|
||||
# Route for robots.txt
|
||||
@bp.route('/robots.txt')
|
||||
def robots():
|
||||
return send_file('../static/content/other/robots.txt')
|
||||
|
||||
|
||||
# Route for sitemap.xml
|
||||
@bp.route('/sitemap.xml')
|
||||
def sitemap():
|
||||
return send_file('../static/content/other/sitemap.xml')
|
||||
|
||||
|
||||
# Catch-all route for generic pages
|
||||
@bp.route('/<path:filename>')
|
||||
def catch_all(filename):
|
||||
try: return render_template(f'pages/{filename if filename.endswith(".html") else filename + ".html"}')
|
||||
except Exception as e:
|
||||
# If the template is not found, check if it is a directory
|
||||
os_path = os.path.join(bp.template_folder, 'pages', filename)[3:]
|
||||
if os.path.isdir(os_path):
|
||||
# walk through the directory and find all files
|
||||
pages = []
|
||||
for root, dirs, files_in_dir in os.walk(os_path):
|
||||
for file in files_in_dir:
|
||||
pages.append(os.path.relpath(os.path.join(root, file), os_path))
|
||||
for dir in dirs:
|
||||
pages.append(os.path.relpath(os.path.join(root, dir), os_path) + '/')
|
||||
|
||||
# If it is a directory, render a directory page
|
||||
if not filename.endswith('/'): filename += '/'
|
||||
return render_template('bases/directory.html', directory=filename, pages=pages)
|
||||
|
||||
# If it is a file, return a 404 error
|
||||
abort(404, f"Template '{filename}' not found: {e}")
|
||||
@@ -1,146 +0,0 @@
|
||||
# Imports
|
||||
from flask import Blueprint, abort, request, redirect
|
||||
from os import urandom, getenv as env
|
||||
import src.utils.database as database
|
||||
import src.utils.cap as cap
|
||||
import logging, datetime, threading, time
|
||||
|
||||
|
||||
# Create blueprint
|
||||
bp = Blueprint(
|
||||
'snake',
|
||||
__name__,
|
||||
template_folder=env('TEMPLATE_FOLDER', default='../templates'),
|
||||
static_folder=env('STATIC_FOLDER', default='../static')
|
||||
)
|
||||
|
||||
|
||||
# Create logger
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Create database instance
|
||||
db = database.Database(db_name=env('DB_NAME', default='db.sqlite'))
|
||||
db.execute('CREATE TABLE IF NOT EXISTS snake_scores (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, score INTEGER)')
|
||||
db.execute('''CREATE TABLE IF NOT EXISTS snake_tokens (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
token TEXT UNIQUE NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
ip TEXT UNIQUE NOT NULL
|
||||
)''')
|
||||
|
||||
# Input validation function
|
||||
def valid_length(value, min_length=1, max_length=100):
|
||||
if not isinstance(value, str):
|
||||
return False
|
||||
return min_length <= len(value) <= max_length
|
||||
|
||||
|
||||
def valid_score(score, game_token):
|
||||
start_time = db.execute('SELECT created_at FROM snake_tokens WHERE token = ?', (game_token,)).fetchone()
|
||||
if not start_time:
|
||||
log.error("Game token not found.")
|
||||
return False
|
||||
|
||||
start_time = datetime.datetime.fromisoformat(start_time[0])
|
||||
current_time = datetime.datetime.now()
|
||||
elapsed_time = (current_time - start_time).total_seconds()
|
||||
|
||||
if elapsed_time < score / 10 * 3 + 10: # assuming that each point takes 3 seconds to achieve and 10 seconds to start the game and do captcha
|
||||
log.error("Score is too high for the elapsed time.")
|
||||
return False
|
||||
|
||||
if score <= 0 or score > 10000: # Arbitrary upper limit for scores
|
||||
log.error("Score is out of valid range.")
|
||||
return False
|
||||
|
||||
if score % 10 != 0:
|
||||
log.error("Score is not a multiple of 10.")
|
||||
return False
|
||||
|
||||
# delete the token after score validation
|
||||
db.execute('DELETE FROM snake_tokens WHERE token = ?', (game_token,))
|
||||
log.info(f"Score {score} validated successfully for token {game_token}.")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
# Route for score submission
|
||||
@bp.route('/snake/submit', methods=['POST'])
|
||||
def submit_score():
|
||||
name = request.form.get('name')
|
||||
score = request.form.get('score')
|
||||
captcha_token = request.form.get('cap-token')
|
||||
game_token = request.form.get('game_token')
|
||||
|
||||
if not cap.verify_captcha(captcha_token):
|
||||
log.error("Captcha verification failed.")
|
||||
abort(400, "Captcha verification failed")
|
||||
|
||||
if not name or not score or not captcha_token or not game_token:
|
||||
log.error("Name, score, captcha token, or game token is missing.")
|
||||
abort(400, "Missing required fields")
|
||||
|
||||
if not valid_length(name, min_length=3, max_length=15):
|
||||
log.error("Invalid name length.")
|
||||
abort(400, "Name must be between 3 and 15 characters long.")
|
||||
|
||||
if not valid_score(int(score), game_token):
|
||||
log.error("Invalid score.")
|
||||
abort(400, "Score not vilid, so either you are trying to cheat the leaderboard or something is seriously wrong.")
|
||||
|
||||
try:
|
||||
db.execute('INSERT INTO snake_scores (name, score) VALUES (?, ?)', (name, int(score)))
|
||||
db.execute('DELETE FROM snake_tokens WHERE token = ?', (game_token,))
|
||||
log.info(f"Score submitted: {name} - {score}")
|
||||
return redirect('/404')
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"Database error: {e}")
|
||||
abort(500, "Internal server error while submitting score.")
|
||||
|
||||
|
||||
# Generate a unique game token
|
||||
def generate_start_token():
|
||||
"""Generate a unique start token for the game."""
|
||||
token = urandom(16).hex()
|
||||
ip = request.headers.get('X-Forwarded-For', request.remote_addr)
|
||||
|
||||
ip_token = db.execute('SELECT token FROM snake_tokens WHERE ip = ?', (ip,)).fetchone()
|
||||
if ip_token:
|
||||
log.info(f"Token already exists for IP: {ip}, reusing token.")
|
||||
return ip_token[0]
|
||||
|
||||
log.info(f"Generated start token: {token}")
|
||||
db.execute('INSERT INTO snake_tokens (token, ip) VALUES (?, ?)', (token, ip))
|
||||
return token
|
||||
|
||||
|
||||
# Get leaderboard scores
|
||||
def get_leaderboard():
|
||||
"""Fetch scores from the leaderboard."""
|
||||
try:
|
||||
scores = db.execute('SELECT name, score FROM snake_scores ORDER BY score DESC').fetchall()
|
||||
leaderboard = [{'position': i + 1, 'name': score[0], 'score': score[1]} for i, score in enumerate(scores)]
|
||||
log.info("Leaderboard fetched successfully.")
|
||||
return leaderboard
|
||||
except Exception as e:
|
||||
log.error(f"Error fetching leaderboard: {e}")
|
||||
return []
|
||||
|
||||
|
||||
# Clear all tokens older than 1 hour
|
||||
def clear_old_tokens():
|
||||
while True:
|
||||
try:
|
||||
one_hour_ago = datetime.datetime.now() - datetime.timedelta(hours=1)
|
||||
db.execute('DELETE FROM snake_tokens WHERE created_at < ?', (one_hour_ago,))
|
||||
log.info("Old tokens cleared.")
|
||||
except Exception as e:
|
||||
log.error(f"Error clearing old tokens: {e}")
|
||||
time.sleep(3600) # Run every hour
|
||||
|
||||
|
||||
# Start the token clearing thread
|
||||
token_thread = threading.Thread(target=clear_old_tokens, daemon=True)
|
||||
token_thread.start()
|
||||
@@ -1,42 +0,0 @@
|
||||
# Imports
|
||||
from os import getenv as env
|
||||
import requests, logging
|
||||
|
||||
|
||||
# Create logger
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# Function to verify CAPTCHA response
|
||||
def verify_captcha(token: str) -> bool:
|
||||
"""
|
||||
Verify the CAP response token with the CAP server.
|
||||
|
||||
Args:
|
||||
token (str): The CAP response token to verify.
|
||||
|
||||
Returns:
|
||||
bool: True if the token is valid, False otherwise.
|
||||
"""
|
||||
if not token:
|
||||
return False
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
f"https://cap.alfieking.dev/{env('CAP_KEY', default='')}/siteverify",
|
||||
json={
|
||||
'secret': env('CAP_SECRET', default=''),
|
||||
'response': token,
|
||||
},
|
||||
timeout=10
|
||||
)
|
||||
|
||||
response.raise_for_status()
|
||||
if response.status_code != 200:
|
||||
log.error("CAPTCHA verification failed with status code: %s", response.status_code)
|
||||
return False
|
||||
return response.json().get('success', False)
|
||||
|
||||
except Exception as e:
|
||||
log.error("Error verifying CAPTCHA: %s", e)
|
||||
return False
|
||||
@@ -1,18 +0,0 @@
|
||||
# Imports
|
||||
import sqlite3
|
||||
|
||||
# Database class
|
||||
class Database:
|
||||
def __init__(self, db_name='db.sqlite'):
|
||||
self.connection = sqlite3.connect(db_name, check_same_thread=False)
|
||||
self.cursor = self.connection.cursor()
|
||||
|
||||
def execute(self, query, params=None):
|
||||
if params is None:
|
||||
params = []
|
||||
self.cursor.execute(query, params)
|
||||
self.connection.commit()
|
||||
return self.cursor
|
||||
|
||||
def close(self):
|
||||
self.connection.close()
|
||||
58
src/wsgi.py
@@ -1,58 +0,0 @@
|
||||
# Imports
|
||||
from flask import Flask
|
||||
from flask_session import Session
|
||||
|
||||
from dotenv import load_dotenv
|
||||
from os import getenv as env, listdir
|
||||
import logging, importlib
|
||||
|
||||
|
||||
# Load env
|
||||
load_dotenv()
|
||||
|
||||
|
||||
# Create console log handler
|
||||
console_log = logging.StreamHandler()
|
||||
console_log.setFormatter(logging.Formatter("\033[1;32m%(asctime)s\033[0m - \033[1;34m%(levelname)s\033[0m - \033[1;31m%(name)s\033[0m - %(message)s"))
|
||||
console_log.setLevel(logging.INFO)
|
||||
|
||||
# Create file log handler
|
||||
file_log = logging.FileHandler(env('LOG_FILE', default='app.log'), mode=env('LOG_MODE', default='a'))
|
||||
file_log.setFormatter(logging.Formatter("%(asctime)s - %(levelname)s - %(name)s - %(message)s"))
|
||||
file_log.setLevel(logging.DEBUG)
|
||||
|
||||
# Add handlers to the logger
|
||||
log = logging.getLogger()
|
||||
log.setLevel(logging.DEBUG)
|
||||
log.addHandler(console_log)
|
||||
log.addHandler(file_log)
|
||||
log.info("Logging initialized")
|
||||
|
||||
|
||||
# Create flask app
|
||||
app = Flask(
|
||||
__name__,
|
||||
template_folder=env('TEMPLATE_FOLDER', default='../templates'),
|
||||
static_folder=env('STATIC_FOLDER', default='../static')
|
||||
)
|
||||
|
||||
# Configure sessions
|
||||
app.config["SESSION_PERMANENT"] = True
|
||||
app.config["SESSION_TYPE"] = "filesystem"
|
||||
Session(app)
|
||||
|
||||
|
||||
# Load routes
|
||||
routes_dir = env('ROUTES_DIR', default='src/routes')
|
||||
for filename in listdir(routes_dir):
|
||||
if not filename.endswith('.py') and filename.startswith('__'):
|
||||
continue
|
||||
|
||||
module_name = f"{routes_dir.replace('/', '.')}.{filename[:-3]}"
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
if hasattr(module, 'bp'):
|
||||
app.register_blueprint(module.bp)
|
||||
log.info(f"Registered blueprint: {module_name}")
|
||||
except Exception as e:
|
||||
log.error(f"Failed to register blueprint {module_name}: {e}")
|
||||
BIN
static/content/buttons/8831.png
Normal file
|
After Width: | Height: | Size: 1.4 KiB |
BIN
static/content/buttons/beeps.gif
Normal file
|
After Width: | Height: | Size: 13 KiB |
BIN
static/content/buttons/blinkiescafe.gif
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
static/content/buttons/emmixis.gif
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
static/content/buttons/hijpixel.gif
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
static/content/buttons/insia.gif
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
static/content/buttons/lensdeer.gif
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
static/content/buttons/ne0nbandit.png
Normal file
|
After Width: | Height: | Size: 3.0 KiB |
BIN
static/content/buttons/nekoweb.gif
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
static/content/buttons/neocities.gif
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
@@ -3,7 +3,6 @@ https://cyber.dabamos.de/88x31/anythingbut.gif
|
||||
https://cyber.dabamos.de/88x31/bestdesktop.gif
|
||||
https://kopawz.neocities.org/buttonhoard/buttonsfldr2/diagnosedwithGAY.gif
|
||||
https://kopawz.neocities.org/indexgraphics/buttondecor/ilikecomputer.png
|
||||
https://identity-crisis.carrd.co/assets/images/gallery04/ad4f8d52.jpg?v=4e55d939
|
||||
https://anlucas.neocities.org/best_viewed_with_eyes.gif
|
||||
https://anlucas.neocities.org/html_learn_it_today.gif
|
||||
https://highway.eightyeightthirty.one/badge/5d58a8f32b007d4897db6f862a895a81674fb35f5cc3947fc66595817ca174db
|
||||
BIN
static/content/buttons/s1nez.gif
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
static/content/buttons/thnlqd.png
Normal file
|
After Width: | Height: | Size: 965 B |
BIN
static/content/buttons/tuxedodragon.gif
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
static/content/fonts/avali-scratch.otf.woff2
Normal file
BIN
static/content/fonts/ultrakill-font-2.woff2
Normal file
BIN
static/content/fonts/ultrakill-font.woff2
Normal file
@@ -1,8 +1,6 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Disallow: /404
|
||||
Disallow: /500
|
||||
Disallow: /400
|
||||
Disallow: /errors
|
||||
|
||||
Sitemap: https://alfieking.dev/sitemap.xml
|
||||
|
||||
|
||||
@@ -6,13 +6,4 @@
|
||||
<url>
|
||||
<loc>https://alfieking.dev/toaster</loc>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://alfieking.dev/events</loc>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://alfieking.dev/events/paws-n-pistons</loc>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://alfieking.dev/events/crittersmk</loc>
|
||||
</url>
|
||||
</urlset>
|
||||
|
Before Width: | Height: | Size: 3.6 MiB After Width: | Height: | Size: 3.6 MiB |
|
Before Width: | Height: | Size: 4.6 MiB After Width: | Height: | Size: 4.6 MiB |
|
Before Width: | Height: | Size: 3.2 MiB After Width: | Height: | Size: 3.2 MiB |
|
Before Width: | Height: | Size: 2.0 MiB After Width: | Height: | Size: 2.0 MiB |
|
Before Width: | Height: | Size: 4.6 MiB After Width: | Height: | Size: 4.6 MiB |
|
Before Width: | Height: | Size: 3.3 MiB After Width: | Height: | Size: 3.3 MiB |
|
Before Width: | Height: | Size: 3.5 MiB After Width: | Height: | Size: 3.5 MiB |
|
Before Width: | Height: | Size: 2.7 MiB After Width: | Height: | Size: 2.7 MiB |
|
Before Width: | Height: | Size: 3.5 MiB After Width: | Height: | Size: 3.5 MiB |
|
After Width: | Height: | Size: 4.4 MiB |
|
After Width: | Height: | Size: 4.9 MiB |
|
After Width: | Height: | Size: 5.1 MiB |
|
Before Width: | Height: | Size: 3.3 MiB After Width: | Height: | Size: 3.3 MiB |
|
Before Width: | Height: | Size: 4.0 MiB After Width: | Height: | Size: 4.0 MiB |
|
Before Width: | Height: | Size: 4.5 MiB After Width: | Height: | Size: 4.5 MiB |
|
Before Width: | Height: | Size: 4.7 MiB After Width: | Height: | Size: 4.7 MiB |
BIN
static/content/smileos/KITR_Build.webp
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
static/content/smileos/Shopmusic.ogx
Normal file
BIN
static/content/smileos/SmileOS2.webp
Normal file
|
After Width: | Height: | Size: 710 B |
BIN
static/content/smileos/SmileOS2Click.ogx
Normal file
BIN
static/content/smileos/SmileOS2Startup.ogx
Normal file
BIN
static/content/smileos/SmileOS_2_Box.webp
Normal file
|
After Width: | Height: | Size: 170 B |
BIN
static/content/smileos/SmileOS_2_Button_Background.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
static/content/smileos/SmileOS_2_Button_transparent.png
Normal file
|
After Width: | Height: | Size: 126 B |
BIN
static/content/smileos/SmileOS_2_Content.webp
Normal file
|
After Width: | Height: | Size: 86 B |
BIN
static/content/smileos/SmileOS_2_Header.webp
Normal file
|
After Width: | Height: | Size: 170 B |
BIN
static/content/smileos/SmileOS_2_icon_smile.webp
Normal file
|
After Width: | Height: | Size: 88 B |
BIN
static/content/smileos/SmileOS_2_icon_tip.webp
Normal file
|
After Width: | Height: | Size: 82 B |
BIN
static/content/smileos/SmileOS_2_inset_panel.webp
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
static/content/smileos/SmileOS_2_top_button_3.png
Normal file
|
After Width: | Height: | Size: 134 B |
BIN
static/content/smileos/SmileOS_2_top_button_4.png
Normal file
|
After Width: | Height: | Size: 122 B |
BIN
static/content/smileos/SmileOS_2_top_button_5.png
Normal file
|
After Width: | Height: | Size: 116 B |
BIN
static/content/toaster/Toaster_v1.0_sticker.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
static/content/toaster/Toaster_v1.1.png
Normal file
|
After Width: | Height: | Size: 830 KiB |
@@ -1,155 +0,0 @@
|
||||
#snakeContainer {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
canvas#snakeCanvas {
|
||||
box-sizing: border-box;
|
||||
border: 2px solid var(--secondary-background-color);
|
||||
border-radius: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: min-content;
|
||||
gap: 1rem;
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
form input[type="text"] {
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--secondary-background-color);
|
||||
border-radius: 6px;
|
||||
background-color: var(--secondary-background-color-but-slightly-transparent);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
form button[type="submit"] {
|
||||
padding: 10px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--secondary-background-color);
|
||||
border-radius: 6px;
|
||||
background-color: var(--secondary-background-color-but-slightly-transparent);
|
||||
color: var(--text-color);
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.flex-row {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.min-width {
|
||||
width: min-content;
|
||||
}
|
||||
|
||||
.max-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#snakeLeaderboardSection {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
max-height: 272px ;
|
||||
}
|
||||
|
||||
#snakeLeaderboard {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
width: 100%;
|
||||
overflow-y: scroll;
|
||||
}
|
||||
|
||||
#snakeLeaderboard li {
|
||||
padding: 5px 20px;
|
||||
background-color: var(--secondary-background-color-but-slightly-transparent);
|
||||
color: var(--text-color);
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#snakeLeaderboard li:nth-child(even) {
|
||||
background-color: var(--secondary-background-color);
|
||||
}
|
||||
|
||||
dialog {
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
padding: 20px;
|
||||
background-color: var(--background-color);
|
||||
color: var(--text-color);
|
||||
border: 2px solid var(--secondary-background-color);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
dialog button {
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
border: 2px solid var(--secondary-background-color);
|
||||
border-radius: 6px;
|
||||
background-color: var(--secondary-background-color-but-slightly-transparent);
|
||||
color: var(--text-color);
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
dialog h2 {
|
||||
margin: 0;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
dialog p {
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 850px) {
|
||||
.flex-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
form {
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.min-width {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 650px) {
|
||||
.mobileOnly {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.pcOnly {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (min-width: 651px) {
|
||||
.mobileOnly {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.pcOnly {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,24 @@
|
||||
font-weight:normal;
|
||||
font-style:normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family:"Scratch";
|
||||
src:url("/static/content/fonts/avali-scratch.otf.woff2") format("woff2");
|
||||
font-weight:normal;
|
||||
font-style:normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family:"Ultrafont";
|
||||
src:url("/static/content/fonts/ultrakill-font.woff2") format("woff2");
|
||||
font-weight:normal;
|
||||
font-style:normal;
|
||||
}
|
||||
@font-face {
|
||||
font-family:"Ultrafont2";
|
||||
src:url("/static/content/fonts/ultrakill-font-2.woff2") format("woff2");
|
||||
font-weight:normal;
|
||||
font-style:normal;
|
||||
}
|
||||
|
||||
:root {
|
||||
--primary-color: #5cdd8b;
|
||||
@@ -17,6 +35,11 @@
|
||||
--font-family: "Space Mono", "serif";
|
||||
--title-font: 'Roboto Mono', sans-serif;
|
||||
--irken-font: 'Irken';
|
||||
--scratch-font: 'Scratch';
|
||||
--ultrafont-font: 'Ultrafont';
|
||||
--smileos2-box: url(/static/content/smileos/SmileOS_2_Box.webp) 17 3 3 fill / 51px 9px 9px;
|
||||
--smileos2-font: 'Ultrafont2';
|
||||
--smileos2-emphasis: #FF4343;
|
||||
}
|
||||
|
||||
body {
|
||||
@@ -220,6 +243,15 @@ main section a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.smileos {
|
||||
border-image: var(--smileos2-box);
|
||||
padding: 54px 15px 12px;
|
||||
image-rendering: pixelated;
|
||||
font-size: 1.25rem;
|
||||
font-family: var(--smileos2-font);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
#furry {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
@@ -244,6 +276,14 @@ main section a {
|
||||
font-family: var(--irken-font);
|
||||
}
|
||||
|
||||
.scratch {
|
||||
font-family: var(--scratch-font);
|
||||
}
|
||||
|
||||
.ultrafont {
|
||||
font-family: var(--ultrafont-font);
|
||||
}
|
||||
|
||||
#alt-nav {
|
||||
display: none;
|
||||
backdrop-filter: blur(2px) brightness(0.6);
|
||||
@@ -308,6 +348,41 @@ a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
code {
|
||||
background-color: var(--secondary-background-color-but-slightly-transparent);
|
||||
padding: 4px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.9rem;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
#toaster-wave {
|
||||
position: absolute;
|
||||
left: -145px;
|
||||
top: 200px;
|
||||
}
|
||||
|
||||
.webring {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.webring a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-color);
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.webring a:hover {
|
||||
font-weight: 900;
|
||||
text-shadow: 0px 0px 10px var(--primary-color-but-slightly-transparent);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1240px) {
|
||||
#toaster-wave {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1000px) {
|
||||
body {
|
||||
background-color: var(--background-color);
|
||||
@@ -1,20 +0,0 @@
|
||||
cap-widget {
|
||||
--cap-background: var(--secondary-background-color-but-slightly-transparent);
|
||||
--cap-border-color: var(--secondary-background-color);
|
||||
--cap-border-radius: 14px;
|
||||
--cap-widget-width: 230px;
|
||||
--cap-widget-padding: 14px;
|
||||
--cap-gap: 15px;
|
||||
--cap-color: var(--text-color);
|
||||
--cap-checkbox-size: 25px;
|
||||
--cap-checkbox-border: 1px solid var(--secondary-background-color);
|
||||
--cap-checkbox-border-radius: 6px;
|
||||
--cap-checkbox-background: none;
|
||||
--cap-checkbox-margin: 2px;
|
||||
--cap-font: "Space Mono", "serif";
|
||||
--cap-spinner-color: var(--primary-color);
|
||||
--cap-spinner-background-color: var(--secondary-background-color-but-slightly-transparent);
|
||||
--cap-spinner-thickness: 5px;
|
||||
--cap-credits-font-size: 12px;
|
||||
--cap-opacity-hover: 0.8;
|
||||
}
|
||||
@@ -1,17 +1,23 @@
|
||||
.gallery {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.gallery img {
|
||||
.gallery .gallery-images {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.gallery .gallery-images img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.gallery-date {
|
||||
margin: 1rem 0 .25rem 0;
|
||||
font-size: 2rem;
|
||||
.gallery h2.gallery-date {
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
131
static/css/terminal.css
Normal file
@@ -0,0 +1,131 @@
|
||||
@font-face {
|
||||
font-family:"Ultrafont2";
|
||||
src:url("/static/content/fonts/ultrakill-font-2.woff2") format("woff2");
|
||||
font-weight:normal;
|
||||
font-style:normal;
|
||||
}
|
||||
|
||||
#terminal {
|
||||
aspect-ratio: 4/3;
|
||||
padding: 0;
|
||||
font-family: "Ultrafont2";
|
||||
font-size: 1.2rem;
|
||||
color: #ffffff;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.smileos-header {
|
||||
height: 9%;
|
||||
border-image: url(/static/content/smileos/SmileOS_2_Header.webp) 3 fill / 9px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0;
|
||||
font-size: 1.4rem;
|
||||
font-weight: 900;
|
||||
padding-left: 7px;
|
||||
padding-right: 7px;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.smileos-header img {
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
#terminal-container {
|
||||
border-image: url(/static/content/smileos/SmileOS_2_Content.webp) 1 3 3 fill / 3px 9px 9px;
|
||||
height: 100%;
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"logo window"
|
||||
"buttons window";
|
||||
grid-template-columns: 40% 1fr;
|
||||
grid-template-rows: 25% 1fr;
|
||||
padding: 15px;
|
||||
box-sizing: border-box;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
#smileos-logo {
|
||||
width: 250px;
|
||||
grid-area: logo;
|
||||
margin: auto;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
#terminal-window {
|
||||
height: 100%;
|
||||
grid-area: window;
|
||||
}
|
||||
|
||||
#window-container {
|
||||
border-image: url(/static/content/smileos/SmileOS_2_Content.webp) 1 3 3 fill / 3px 9px 9px;
|
||||
height: 91%;
|
||||
padding: 35px;
|
||||
box-sizing: border-box;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
#window-content {
|
||||
border-image: url(/static/content/smileos/SmileOS_2_inset_panel.webp) 1 fill / 3px;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
image-rendering: pixelated;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: #ff4343;
|
||||
}
|
||||
|
||||
#terminal-buttons {
|
||||
grid-area: buttons;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.terminal-button {
|
||||
border-image: url(/static/content/smileos/SmileOS_2_Button_transparent.png) 2 / 9px;
|
||||
background: url(/static/content/smileos/SmileOS_2_Button_Background.png);
|
||||
background-repeat: repeat-x;
|
||||
background-size: 100% 100%;
|
||||
border-radius: 15px;
|
||||
height: 75px;
|
||||
width: 270px;
|
||||
image-rendering: pixelated;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font-family: "Ultrafont2";
|
||||
font-size: 1.2rem;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.terminal-button:hover {
|
||||
filter: contrast(110%);
|
||||
}
|
||||
|
||||
.terminal-button:active {
|
||||
filter: contrast(125%);
|
||||
}
|
||||
|
||||
#smileos-window-content img {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#smileos-restart-music {
|
||||
color: #FF4343;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#smileos-restart-music:hover {
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -81,4 +81,8 @@ ul#toaster-specs li {
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
#toaster-wave {
|
||||
display: none;
|
||||
}
|
||||
@@ -72,25 +72,33 @@ typing();
|
||||
|
||||
// HIDDEN STUFF (shh don't tell anyone >:3)
|
||||
|
||||
let last5Chars = "";
|
||||
let last15Chars = "";
|
||||
|
||||
document.addEventListener('keydown', function(event) {
|
||||
last5Chars += event.key;
|
||||
if (last5Chars == "furry") {
|
||||
last15Chars += event.key;
|
||||
if (last15Chars.includes("furry")) {
|
||||
console.log("owo, whats this?");
|
||||
document.getElementById('furry').style.display = 'block';
|
||||
last15Chars = "";
|
||||
}
|
||||
if (last5Chars == "irken") {
|
||||
if (last15Chars.includes("irken")) {
|
||||
console.log("doom doom doom!");
|
||||
document.querySelector(":root").style.setProperty('--font-family', 'Irken');
|
||||
document.querySelector(":root").style.setProperty('--title-font', '1.5em');
|
||||
last15Chars = "";
|
||||
}
|
||||
while (last5Chars.length >= 5) {
|
||||
last5Chars = last5Chars.slice(1);
|
||||
if (last15Chars.includes("scratch")) {
|
||||
console.log("space chicken");
|
||||
document.querySelector(":root").style.setProperty('--font-family', 'Scratch');
|
||||
document.querySelector(":root").style.setProperty('--title-font', '1em');
|
||||
last15Chars = "";
|
||||
}
|
||||
while (last15Chars.length >= 15) {
|
||||
last15Chars = last15Chars.slice(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Spotify API
|
||||
// Spotify API (now lastfm)
|
||||
|
||||
function getSpotify() {
|
||||
fetch('https://api.alfieking.dev/spotify/nowplaying/xz02oolstlvwxqu1pfcua9exz').then(response => {
|
||||
@@ -115,10 +123,11 @@ if (document.getElementById('spotify')) {
|
||||
setInterval(getSpotify, 15000);
|
||||
}
|
||||
|
||||
|
||||
// load buttons
|
||||
|
||||
function loadButtons() {
|
||||
fetch('/static/content/other/buttons.txt').then(response => {
|
||||
fetch('/static/content/buttons/non_link_buttons.txt').then(response => {
|
||||
return response.text();
|
||||
}).then(data => {
|
||||
container = document.getElementById('button-collection');
|
||||
|
||||
106
static/js/smileos.js
Normal file
@@ -0,0 +1,106 @@
|
||||
const tips_of_the_day = [
|
||||
`The Revolver deals <span style="color:var(--smileos2-emphasis)">locational damage</span>.<br>A <span style="color:var(--smileos2-emphasis)">headshot</span> deals <span style="color:var(--smileos2-emphasis)">2x</span> damage and a <span style="color:var(--smileos2-emphasis)">limbshot</span> deals <span style="color:var(--smileos2-emphasis)">1.5x</span> damage.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">Dash</span>: Fully invincible, costs stamina<br><span style="color:var(--smileos2-emphasis)">Slide</span>: Greater distance, no invincibility<br><span style="color:var(--smileos2-emphasis)">Jump</span>: Quickly out of melee range, less control`,
|
||||
`<span style="color:var(--smileos2-emphasis)">Shotgun parries</span>: A <span style="color:var(--smileos2-emphasis)">point-blank</span> Shotgun shot to the torso right before an enemy attack lands will deal massive damage.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">Attack sound cues</span> allow you to keep track of enemies who are off screen.`,
|
||||
`Some enemies make <span style="color:var(--smileos2-emphasis)">idle sounds</span> to make them easier to track.`,
|
||||
`Use the <span style="color:cyan">ATTRACTOR NAILGUN</span>'s magnets to form concentrated <span style="color:var(--smileos2-emphasis)">orbs</span> of nails that can be <span style="color:var(--smileos2-emphasis)">moved</span> around using the pull force of <span style="color:var(--smileos2-emphasis)">other magnets</span>.`,
|
||||
`Enemies <span style="color:var(--smileos2-emphasis)">can hurt</span> other enemy types. With quick thinking and positioning, powerful enemies can turn into powerful weapons.`,
|
||||
`The <span style="color:cyan">ATTRACTOR NAILGUN</span>'s magnets can be attached to enemies to make mobile targets easy to hit with nails.`,
|
||||
`Enemies <span style="color:var(--smileos2-emphasis)">scream</span> when falling from a <span style="color:var(--smileos2-emphasis)">fatal height</span>.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">SLAM BOUNCING</span>: Jump immediately after landing from a <span style="color:var(--smileos2-emphasis)">ground slam</span> to jump higher. The longer the ground slam fall, the higher the bounce.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">RAILCANNON</span> variations all share the same cooldown. Choose wisely which variation best fits the situation.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">SLIDING</span> will retain previous momentum for a short amount of time. Chaining quick <span style="color:var(--smileos2-emphasis)">SLIDE JUMPS</span> after a <span style="color:var(--smileos2-emphasis)">DASH JUMP</span> will give you incredible sustained speed.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">Environmental hazards</span> such as harmful liquids will hurt enemies as well.`,
|
||||
`If you're having trouble keeping up with a tough enemy, <span style="color:var(--smileos2-emphasis)">stand back and observe</span>. Every enemy has its <span style="color:var(--smileos2-emphasis)">tells</span> and <span style="color:var(--smileos2-emphasis)">patterns</span> and learning those can be your key to victory.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">HITSCAN</span> weapons can be used to hit the shotgun's <span style="color:cyan">CORE EJECT</span> in mid-air to increase its damage and blast radius.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">POWER-UPS</span> can be stacked.`,
|
||||
`Hitting an enemy with only the <span style="color:var(--smileos2-emphasis)">edge</span> of an <span style="color:var(--smileos2-emphasis)">explosion</span> will launch them without dealing much damage, making it a risky but effective tool against <span style="color:var(--smileos2-emphasis)">Stalkers</span>.`,
|
||||
`Airborne <span style="color:var(--smileos2-emphasis)">coins</span> can be shot with any <span style="color:var(--smileos2-emphasis)">hitscan</span> weapon.`,
|
||||
`Falling <span style="color:var(--smileos2-emphasis)">underwater</span> is slow, but a <span style="color:var(--smileos2-emphasis)">ground slam</span> allows for a quick return to the ground.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">Sliding</span> onto water will cause one to <span style="color:var(--smileos2-emphasis)">skip across</span> its surface.`,
|
||||
`If an enemy is <span style="color:var(--smileos2-emphasis)">blessed</span> by an <span style="color:var(--smileos2-emphasis)">Idol</span>, a direct visible connection is formed between the two, allowing one to easily <span style="color:var(--smileos2-emphasis)">track down</span> and destroy the protector.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">Explosions</span> deflect <span style="color:var(--smileos2-emphasis)">projectiles</span>.<br><br>If an explosion is caused right from where an enemy shoots a projectile, it can <span style="color:var(--smileos2-emphasis)">backfire</span> and <span style="color:var(--smileos2-emphasis)">hit them</span> instead.`,
|
||||
`The space of a <span style="color:var(--smileos2-emphasis)">large</span> arena can be used to one's advantage. Using the environment to <span style="color:var(--smileos2-emphasis)">break line-of-sight</span> with enemies allows for some breathing room and time to consider <span style="color:var(--smileos2-emphasis)">target prioritization</span>.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">DON'T</span> WASTE STAMINA! Dashing <span style="color:var(--smileos2-emphasis)">needlessly</span> while fighting very <span style="color:var(--smileos2-emphasis)">aggressive foes</span> will quickly cause one to have none left when it is most needed.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">Homing projectiles</span> may be more difficult to dodge, but their tracking and slower speed makes them much <span style="color:var(--smileos2-emphasis)">easier</span> to <span style="color:var(--smileos2-emphasis)">parry</span>.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">Mannequins</span> can be hard to hit due to their speed, but they lose air control when <span style="color:var(--smileos2-emphasis)">launched</span> or <span style="color:var(--smileos2-emphasis)">shot down</span> from a surface, making them <span style="color:var(--smileos2-emphasis)">unable to move</span> for a short moment.`,
|
||||
`The <span style="color:red">Knuckleblaster</span>'s <span style="color:var(--smileos2-emphasis)">blast wave</span> is also capable of breaking a <span style="color:var(--smileos2-emphasis)">Gutterman's shield</span>, and is much easier to land in a chaotic scenario.`,
|
||||
`A <span style="color:var(--smileos2-emphasis)">direct hit</span> from the <span style="color:red">Knuckleblaster</span> has extremely powerful <span style="color:var(--smileos2-emphasis)">knockback</span>, making it extremely powerful for launching enemies into <span style="color:var(--smileos2-emphasis)">pits</span> and other <span style="color:var(--smileos2-emphasis)">environmental hazards</span>.`,
|
||||
`Didn't expect me, huh?`,
|
||||
`<span style="color:#FF0078">Magenta</span> colored attacks can <span style="color:var(--smileos2-emphasis)">not</span> be dashed through and must be <span style="color:var(--smileos2-emphasis)">avoided entirely</span>.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">Blood Puppets</span> do not grant kills or style points, but their <span style="color:var(--smileos2-emphasis)">blood</span> can still <span style="color:var(--smileos2-emphasis)">heal</span>.`,
|
||||
`When facing down <span style="color:var(--smileos2-emphasis)">a difficult foe</span>, it may be beneficial to first get rid of the <span style="color:var(--smileos2-emphasis)">fodder</span> to reduce distractions.`,
|
||||
`Sometimes it may be more beneficial to <span style="color:var(--smileos2-emphasis)">stay at a distance</span> and wait for an opening <span style="color:var(--smileos2-emphasis)">before</span> getting close.`,
|
||||
`If you're having <span style="color:var(--smileos2-emphasis)">trouble</span> with a specific encounter, take a moment to <span style="color:var(--smileos2-emphasis)">weigh your options</span>. <br> There may be some <span style="color:var(--smileos2-emphasis)">trick</span>, <span style="color:var(--smileos2-emphasis)">tool</span> or <span style="color:var(--smileos2-emphasis)">alternative prioritization</span> that will tip the scales in your favor.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">ENEMY STEP</span>: Jump while in mid-air <span style="color:var(--smileos2-emphasis)">near an enemy</span> to jump off the enemy. This resets the amount of available walljumps without needing to land.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">Parries</span> can be used as a powerful healing tool.<br><br>Parrying any enemy projectile or melee attack will <span style="color:red">fully replenish your health</span> up to the hard damage limit.`,
|
||||
`H a v e f u n .`,
|
||||
`If blown too far off the arena, <span style="color:lime">PUMP CHARGE</span>'s overcharge is a good way to get back.`,
|
||||
`<span style="color:var(--smileos2-emphasis)">CHEATS</span> can be enabled in other levels by inputting <span style="color:var(--smileos2-emphasis)">🡡 🡡 🡣 🡣 🡠 🡢 🡠 🡢 B A</span> Enabling cheats will disable ranks`,
|
||||
`You can pick up the cut weapons on the second floor.`
|
||||
]
|
||||
|
||||
|
||||
var click = new Audio('/static/content/smileos/SmileOS2Click.ogx');
|
||||
|
||||
document.getElementById("smileos-about").addEventListener("click", function() {
|
||||
click.play();
|
||||
document.getElementById("smileos-window-title").innerHTML = "About";
|
||||
document.getElementById("smileos-window-content").innerHTML = `
|
||||
<span class="red">SmileOS</span>: is the operating system found on most digital interfaces found throughout
|
||||
<span class="red">Hell</span>. <span class="red">SmileOS 1.0</span> is employed on pannels despite its poor user and developer experience to conserve
|
||||
blood after the war, while <span class="red">SmileOS 2.0</span> is used for terminals requireing higher blood consumption.
|
||||
`;
|
||||
});
|
||||
|
||||
document.getElementById("smileos-snake").addEventListener("click", function() {
|
||||
click.play();
|
||||
document.getElementById("smileos-window-title").innerHTML = "Snake";
|
||||
document.getElementById("smileos-window-content").innerHTML = `
|
||||
<img src="/static/content/smileos/KITR_Build.webp" alt="under construction">
|
||||
`;
|
||||
});
|
||||
|
||||
document.getElementById("smileos-leaderboard").addEventListener("click", function() {
|
||||
click.play();
|
||||
document.getElementById("smileos-window-title").innerHTML = "Leaderboard";
|
||||
document.getElementById("smileos-window-content").innerHTML = `
|
||||
<img src="/static/content/smileos/KITR_Build.webp" alt="under construction">
|
||||
`;
|
||||
});
|
||||
|
||||
document.getElementById("smileos-tip").addEventListener("click", function() {
|
||||
click.play();
|
||||
document.getElementById("smileos-window-title").innerHTML = "Tip of the Day";
|
||||
document.getElementById("smileos-window-content").innerHTML = tips_of_the_day[Math.floor(Math.random() * tips_of_the_day.length)];
|
||||
});
|
||||
|
||||
async function playAudio(url) {
|
||||
return new Promise((resolve) => {
|
||||
const audio = new Audio(url);
|
||||
audio.addEventListener('ended', resolve); // Resolve the promise when audio ends
|
||||
audio.volume = 0.4;
|
||||
audio.play();
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async function() {
|
||||
document.getElementById("smileos-window-title").innerHTML = "Tip of the Day";
|
||||
document.getElementById("smileos-window-content").innerHTML = tips_of_the_day[Math.floor(Math.random() * tips_of_the_day.length)];
|
||||
await playAudio('/static/content/smileos/SmileOS2Startup.ogx');
|
||||
|
||||
var music = new Audio('/static/content/smileos/Shopmusic.ogx');
|
||||
music.loop = true;
|
||||
music.volume = 0.2;
|
||||
music.play();
|
||||
});
|
||||
|
||||
document.getElementById("smileos-restart-music").addEventListener("click", async function() {
|
||||
await playAudio('/static/content/smileos/SmileOS2Startup.ogx');
|
||||
|
||||
var music = new Audio('/static/content/smileos/Shopmusic.ogx');
|
||||
music.loop = true;
|
||||
music.volume = 0.2;
|
||||
music.play();
|
||||
});
|
||||
@@ -1,174 +0,0 @@
|
||||
const canvas = document.getElementById('snakeCanvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
const gridSize = 20;
|
||||
const tileSize = 100;
|
||||
const snakeSize = 60;
|
||||
const foodSize = 80;
|
||||
canvas.width = gridSize * tileSize;
|
||||
canvas.height = gridSize * tileSize;
|
||||
|
||||
let snake = [{ x: 10, y: 10 }, { x: 10, y: 11 }, { x: 10, y: 12 }];
|
||||
let direction = { x: 0, y: 0 };
|
||||
let food = { x: Math.floor(Math.random() * gridSize), y: Math.floor(Math.random() * gridSize) };
|
||||
let score = 0;
|
||||
let gameOver = false;
|
||||
|
||||
function draw() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
// draw grid of checkerboard pattern
|
||||
for (let x = 0; x < gridSize; x++) {
|
||||
for (let y = 0; y < gridSize; y++) {
|
||||
ctx.fillStyle = (x + y) % 2 === 0 ?
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--background-color') :
|
||||
getComputedStyle(document.documentElement).getPropertyValue('--secondary-background-color');
|
||||
ctx.fillRect(x * tileSize, y * tileSize, tileSize, tileSize);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw snake
|
||||
snake.forEach(segment => {
|
||||
let nextVec = { x: 0, y: 0 };
|
||||
// if there is a segment after the current segment
|
||||
if (snake.indexOf(segment) < snake.length - 1) {
|
||||
const nextSegment = snake[snake.indexOf(segment) + 1];
|
||||
nextVec.x = nextSegment.x - segment.x;
|
||||
nextVec.y = nextSegment.y - segment.y;
|
||||
}
|
||||
|
||||
ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--primary-color');
|
||||
if (nextVec.x === 0 && nextVec.y === 0) {
|
||||
ctx.fillRect(
|
||||
segment.x * tileSize + (tileSize - snakeSize) / 2,
|
||||
segment.y * tileSize + (tileSize - snakeSize) / 2,
|
||||
snakeSize,
|
||||
snakeSize
|
||||
);
|
||||
} else if (nextVec.x > 0 || nextVec.y > 0) {
|
||||
ctx.fillRect(
|
||||
segment.x * tileSize + (tileSize - snakeSize) / 2,
|
||||
segment.y * tileSize + (tileSize - snakeSize) / 2,
|
||||
snakeSize + nextVec.x * (tileSize - snakeSize),
|
||||
snakeSize + nextVec.y * (tileSize - snakeSize)
|
||||
);
|
||||
} else {
|
||||
ctx.fillRect(
|
||||
segment.x * tileSize + (tileSize - snakeSize) / 2 + nextVec.x * (tileSize - snakeSize),
|
||||
segment.y * tileSize + (tileSize - snakeSize) / 2 + nextVec.y * (tileSize - snakeSize),
|
||||
snakeSize + Math.abs(nextVec.x) * (tileSize - snakeSize),
|
||||
snakeSize + Math.abs(nextVec.y) * (tileSize - snakeSize)
|
||||
);
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
// Draw food
|
||||
ctx.fillStyle = '#ff4d4d';
|
||||
ctx.fillRect(
|
||||
food.x * tileSize + (tileSize - foodSize) / 2,
|
||||
food.y * tileSize + (tileSize - foodSize) / 2,
|
||||
foodSize,
|
||||
foodSize
|
||||
);
|
||||
}
|
||||
|
||||
function update() {
|
||||
if (gameOver) return;
|
||||
|
||||
// Move snake
|
||||
const head = { x: snake[0].x + direction.x, y: snake[0].y + direction.y };
|
||||
|
||||
// Add new head
|
||||
snake.unshift(head);
|
||||
|
||||
// Check for food collision
|
||||
if (head.x === food.x && head.y === food.y) {
|
||||
score += 10; // Increase score
|
||||
placeFood();
|
||||
} else {
|
||||
snake.pop(); // Remove tail if no food eaten
|
||||
}
|
||||
|
||||
// Check for wall collision
|
||||
if (head.x < 0 || head.x >= gridSize || head.y < 0 || head.y >= gridSize) {
|
||||
gameOver = true;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for self collision
|
||||
for (let i = 1; i < snake.length; i++) {
|
||||
if (head.x === snake[i].x && head.y === snake[i].y) {
|
||||
gameOver = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function placeFood() {
|
||||
do {
|
||||
food.x = Math.floor(Math.random() * gridSize);
|
||||
food.y = Math.floor(Math.random() * gridSize);
|
||||
} while (snake.some(segment => segment.x === food.x && segment.y === food.y));
|
||||
}
|
||||
|
||||
function changeDirection(event) {
|
||||
switch (event.key) {
|
||||
case 'w':
|
||||
if (direction.y === 0) direction = { x: 0, y: -1 };
|
||||
break;
|
||||
case 's':
|
||||
if (direction.y === 0) direction = { x: 0, y: 1 };
|
||||
break;
|
||||
case 'a':
|
||||
if (direction.x === 0) direction = { x: -1, y: 0 };
|
||||
break;
|
||||
case 'd':
|
||||
if (direction.x === 0) direction = { x: 1, y: 0 };
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Menu to start the game
|
||||
function menu() {
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--background-color');
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillStyle = getComputedStyle(document.documentElement).getPropertyValue('--text-color');
|
||||
ctx.font = '200px Arial';
|
||||
ctx.fillText('Snake Game', canvas.width / 2, canvas.height / 2);
|
||||
ctx.font = '100px Arial';
|
||||
ctx.fillText('Press W/A/S/D to move', canvas.width / 2, canvas.height / 2 + 100);
|
||||
ctx.fillText('Click to start', canvas.width / 2, canvas.height / 2 + 200);
|
||||
|
||||
canvas.addEventListener('click', startGame);
|
||||
}
|
||||
|
||||
function gameLoop() {
|
||||
if (!gameOver) {
|
||||
update();
|
||||
draw();
|
||||
setTimeout(gameLoop, 100);
|
||||
} else {
|
||||
document.removeEventListener('keydown', changeDirection);
|
||||
document.getElementById('score').value = score;
|
||||
alert(`Game Over! Your score: ${score}`);
|
||||
menu();
|
||||
}
|
||||
}
|
||||
|
||||
function startGame() {
|
||||
snake = [{ x: 10, y: 10 }, { x: 10, y: 11 }, { x: 10, y: 12 }];
|
||||
direction = { x: 1, y: 0 };
|
||||
food = { x: Math.floor(Math.random() * gridSize), y: Math.floor(Math.random() * gridSize) };
|
||||
score = 0;
|
||||
gameOver = false;
|
||||
canvas.removeEventListener('click', startGame);
|
||||
document.addEventListener('keydown', changeDirection);
|
||||
gameLoop();
|
||||
}
|
||||
|
||||
menu();
|
||||
@@ -3,19 +3,19 @@
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Alfie's basement{% endblock %}</title>
|
||||
<link rel="icon" href="/static/content/general_images/icon.webp">
|
||||
<link rel="stylesheet" href="/static/css/base.css">
|
||||
<title>{% block title %}{{ name }}'s basement{% endblock %}</title>
|
||||
<link rel="icon" href="{% block icon %}/static/content/{% if name == 'Toaster' %}toaster/Toaster_v1.0_sticker.png{% else %}general_images/icon.webp{% endif %}{% endblock %}">
|
||||
<link rel="stylesheet" href="/static/css/bases/base.css">
|
||||
<meta name="description" content="{% block description %}server backend survivor{% endblock %}">
|
||||
<meta name="keywords" content="{% block keywords %}Alfie King, Alfie, King, Alfieking, Alfieking.dev, dev, server, developer, backend, selfhost, homelab{% endblock %}">
|
||||
<meta name="author" content="Alfie King">
|
||||
<meta name="robots" content="all">
|
||||
<meta name="theme-color" content="#63de90" data-react-helmet="true">
|
||||
<meta property="og:site_name" content="Alfieking.dev">
|
||||
<meta property="og:url" content="https://alfieking.dev/">
|
||||
<meta property="og:site_name" content="{% if name == 'Toaster' %}proot.uk{% else %}alfieking.dev{% endif %}">
|
||||
<meta property="og:url" content="https://{% if name == 'Toaster' %}proot.uk{% else %}alfieking.dev{% endif %}/">
|
||||
<meta property="og:title" content="{{ self.title() }}">
|
||||
<meta property="og:description" content="{{ self.description() }}">
|
||||
<meta property="og:image" content="{% block og_image %}/static/content/general_images/icon.webp{% endblock %}">
|
||||
<meta property="og:image" content="{% block og_image %}/static/content/{% if name == 'Toaster' %}toaster/Toaster_v1.0_sticker.png{% else %}general_images/icon.webp{% endif %}{% endblock %}">
|
||||
{% block head %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
@@ -28,6 +28,7 @@
|
||||
<ul>
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/toaster">Toaster</a></li>
|
||||
<li><a href="/terminal" class="ultrafont">Terminal</a></li>
|
||||
<li><a href="/events">Events</a></li>
|
||||
<li><a href="https://git.alfieking.dev/acetheking987">Gitea</a></li>
|
||||
<li><a href="https://www.last.fm/user/acetheking987">LastFm</a></li>
|
||||
@@ -37,29 +38,60 @@
|
||||
<li><a href="https://www.youtube.com/@acetheking987">YouTube</a></li>
|
||||
<li><a href="https://acetheking987.tumblr.com/">Tumblr</a></li>
|
||||
<li><a href="https://www.reddit.com/user/acetheking987">Reddit</a></li>
|
||||
<li><a href="/404">404 >:3</a></li>
|
||||
</ul>
|
||||
</section>
|
||||
</nav>
|
||||
<section>
|
||||
<h6 class="irken">heya, try typing "furry" and "irken" into this page!</h6>
|
||||
<h6 class="irken">heya, try typing "furry", "irken" or "<span class="scratch">scratch</span>" into this page!</h6>
|
||||
</section>
|
||||
<section id="buttons">
|
||||
<h1>BUTTONS</h1>
|
||||
<ul>
|
||||
<li><a href="https://dimden.dev/"><img src="https://dimden.dev/services/images/88x31.gif" alt="dimden"></a></li>
|
||||
<li><a href="https://ne0nbandit.neocities.org/"><img src="https://ne0nbandit.github.io/assets/img/btn/mine/nbbanner.png" alt="ne0nbandit"></a></li>
|
||||
<li><a href="https://thinliquid.dev"><img src="https://thinliquid.dev/thnlqd.png" alt="thinliquid"></a></li>
|
||||
<li><a href="https://nekoweb.org/"><img src="https://nekoweb.org/assets/buttons/button6.gif" alt="nekoweb"></a><!-- button by s1nez.nekoweb.org --></li>
|
||||
<li><a href="https://s1nez.nekoweb.org/"><img src="https://s1nez.nekoweb.org/BUTTON.gif" alt="s1nez"></a></li>
|
||||
<li><a href="https://beeps.website"><img src="https://beeps.website/assets/images/88x31-d.gif" alt="beeps"></a></li>
|
||||
<li><a href="https://itsnotstupid.com"><img src="https://itsnotstupid.com/pics/button1.gif" alt="itsnotstupid"></a></li>
|
||||
<li><a href='https://blinkies.cafe'><img src='https://blinkies.cafe/b/display/blinkiesCafe-badge.gif' alt='blinkies.cafe | make your own blinkies!'></a></li>
|
||||
<li><a href="https://eightyeightthirty.one"><img src="https://eightyeightthirty.one/88x31.png" alt="88x31"></a></li>
|
||||
<li><a href="https://neocities.org"><img src="https://cyber.dabamos.de/88x31/neocities-now.gif" alt="neocities"></a></li>
|
||||
<li><a href="https://tuxedodragon.art"><img src="https://tuxedodragon.art/tuxedodragon%2088x31.gif" alt="tuxedodragon"></a></li>
|
||||
<li><a herf="https://hijpixel.nekoweb.org/"><img src="/static/content/buttons/hijpixel.gif" alt="hijpixel"></a></li>
|
||||
<li><a href="https://lensdeer.neocities.org/"><img src="/static/content/buttons/lensdeer.gif" alt="lensdeer"></a></li>
|
||||
<li><a href="https://emmixis.net/"><img src="/static/content/buttons/emmixis.gif" alt="emmixis"></a></li>
|
||||
<li><a href="https://dimden.dev/"><img src="https://dimden.dev/services/images/88x31.gif" alt="dimden"></a></li><!-- hotlink on purpose -->
|
||||
<li><a href="https://ne0nbandit.neocities.org/"><img src="/static/content/buttons/ne0nbandit.png" alt="ne0nbandit"></a></li>
|
||||
<li><a href="https://thinliquid.dev"><img src="/static/content/buttons/thnlqd.png" alt="thinliquid"></a></li>
|
||||
<li><a href="https://nekoweb.org/"><img src="/static/content/buttons/nekoweb.gif" alt="nekoweb"></a><!-- button by s1nez.nekoweb.org --></li>
|
||||
<li><a href="https://s1nez.nekoweb.org/"><img src="/static/content/buttons/s1nez.gif" alt="s1nez"></a></li>
|
||||
<li><a href="https://beeps.website"><img src="/static/content/buttons/beeps.gif" alt="beeps"></a></li>
|
||||
<li><a href="https://itsnotstupid.com"><img src="/static/content/buttons/insia.gif" alt="itsnotstupid"></a></li>
|
||||
<li><a href='https://blinkies.cafe'><img src='/static/content/buttons/blinkiescafe.gif' alt='blinkies.cafe | make your own blinkies!'></a></li>
|
||||
<li><a href="https://eightyeightthirty.one"><img src="/static/content/buttons/8831.png" alt="88x31"></a></li>
|
||||
<li><a href="https://neocities.org"><img src="/static/content/buttons/neocities.gif" alt="neocities"></a></li>
|
||||
<li><a href="https://tuxedodragon.art"><img src="/static/content/buttons/tuxedodragon.gif" alt="tuxedodragon"></a></li>
|
||||
<li><a href="https://beepi.ng"><img src="https://beepi.ng/88x31.png" width="88" height="31" alt="unnick"></a></li>
|
||||
<li><a href="https://sneexy.synth.download"><img src="https://synth.download/assets/buttons/sneexy.svg" alt="Sneexy"></a></li>
|
||||
<li><a href="https://kraafter.me/"><img src="https://kraafter.me/assets/img/button.png" alt="Kraafter.me button" title="kraaftersite"></a></li>
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
<div id='furnix'>
|
||||
<script type="text/javascript" src="https://tapeykatt.neocities.org/furnix/onionring-variables.js"></script>
|
||||
<script type="text/javascript" src="https://tapeykatt.neocities.org/furnix/onionring-widget.js"></script>
|
||||
</div>
|
||||
</section>
|
||||
<section class="webring">
|
||||
<a href="https://stellophiliac.github.io/roboring">Roboring</a><br>
|
||||
<a href="https://stellophiliac.github.io/roboring/Toaster/previous"><--</a>
|
||||
<a href="https://stellophiliac.github.io/roboring/Toaster/next">--></a>
|
||||
</section>
|
||||
<section>
|
||||
<div id='ultraring'>
|
||||
<script type="text/javascript" src="https://jack-dawlia.neocities.org/page/shrines/ultrakill/ULTRARING/onionring-variables.js"></script>
|
||||
<script type="text/javascript" src="https://jack-dawlia.neocities.org/page/shrines/ultrakill/ULTRARING/onionring-widget-1.js"></script>
|
||||
<noscript>
|
||||
This site is part of <a href="https://jack-dawlia.neocities.org/page/shrines/ultrakill/ULTRARING">ULTRARING</a>!
|
||||
</noscript>
|
||||
</div>
|
||||
</section>
|
||||
<section class="webring">
|
||||
<a href="https://keithhacks.cyou/furryring.php">Furryring</a><br>
|
||||
<a href="https://keithhacks.cyou/furryring.php?prev=alfieking.dev"><--</a>
|
||||
<a href="https://keithhacks.cyou/furryring.php?next=alfieking.dev">--></a>
|
||||
</section>
|
||||
<iframe width="150" height="450" style="border:none" src="https://dogspit.nekoweb.org/sidelink.html" name="sidelink"></iframe>
|
||||
<section>
|
||||
<pre class="vsmoltext"> |\ _,,,---,,_<br>ZZZzz /,`.-'`' -. ;-;;,_<br> |,4- ) )-,_. ,\ ( `'-'<br> '---''(_/--' `-'\_)</pre>
|
||||
</section>
|
||||
@@ -68,12 +100,15 @@
|
||||
<main id="main">
|
||||
<header id="home">
|
||||
<div class="row">
|
||||
<img src="/static/content/general_images/icon.webp">
|
||||
<img src="/static/content/{% if name == 'Toaster' %}toaster/Toaster_v1.0_sticker.png{% else %}general_images/icon.webp{% endif %}">
|
||||
<div>
|
||||
<h1>Alfie King</h1>
|
||||
<h1>{{ name }}</h1>
|
||||
<h2 id="typing">server backend survivor</h2>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/toaster" id="toaster-wave">
|
||||
<img src="/static/content/toaster/Toaster_v1.1.png" alt="toaster">
|
||||
</a>
|
||||
</header>
|
||||
<nav id="alt-nav">
|
||||
<ul>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
{% block description %}server backend survivor{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="/static/css/directory.css">
|
||||
<link rel="stylesheet" href="/static/css/bases/directory.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
|
||||
10
templates/bases/md.html
Normal file
@@ -0,0 +1,10 @@
|
||||
{% extends "bases/base.html" %}
|
||||
|
||||
{% block title %}{{ title }} - Alfie's basement{% endblock %}
|
||||
{% block description %}server backend survivor{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
{{ markdown|safe }}
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -4,7 +4,7 @@
|
||||
{% block description %}Bad request. The server could not understand the request due to invalid syntax.{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="/static/css/400.css">
|
||||
<link rel="stylesheet" href="/static/css/errors/400.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
@@ -3,80 +3,17 @@
|
||||
{% block title %}404 - Not Found{% endblock %}
|
||||
{% block description %}The page you are looking for does not exist.{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="/static/css/404.css">
|
||||
<link rel="stylesheet" href="/static/css/cap.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
<h1>404</h1>
|
||||
<p>
|
||||
It seems like the thing you are looking for is not here :[
|
||||
<br><br>
|
||||
<span class="pcOnly">
|
||||
while you're here, why not play some snake?
|
||||
</span>
|
||||
<span class="mobileOnly">
|
||||
You can't play snake on mobile, sorry :(
|
||||
</span>
|
||||
It seems like the thing you are looking for does not exist or <code>rm -rf</code> itself out of exsistance.
|
||||
</p>
|
||||
<div class="pcOnly" id="snakeContainer">
|
||||
<canvas id="snakeCanvas"></canvas>
|
||||
</div>
|
||||
</section>
|
||||
<section class="pcOnly flex-row">
|
||||
<section class="min-width">
|
||||
<h2>Submit score</h2>
|
||||
<form action="/snake/submit" method="POST" id="snakeForm">
|
||||
<input type="text" id="name" name="name" maxlength=15 minlength=3 placeholder="Your name" required>
|
||||
<cap-widget id="captcha" data-cap-api-endpoint="https://cap.alfieking.dev/{{ cap_key }}/"></cap-widget>
|
||||
<input type="hidden" id="score" name="score" value="0">
|
||||
<input type="hidden" id="game_token" name="game_token" value="{{ token}}">
|
||||
<button type="submit" id="submit">Submit</button>
|
||||
</form>
|
||||
</section>
|
||||
<section class="max-width" id="snakeLeaderboardSection">
|
||||
<h2>Leaderboard</h2>
|
||||
<ul id="snakeLeaderboard">
|
||||
{% for score in scores %}
|
||||
<li>
|
||||
<span>{{ score.position }}</span>
|
||||
<span>{{ score.name }}</span>
|
||||
<span>{{ score.score }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Actual error</h2>
|
||||
<p>
|
||||
{{ error }}
|
||||
</p>
|
||||
</section>
|
||||
<section class="max-width mobileOnly" id="snakeLeaderboardSection">
|
||||
<h2>Leaderboard</h2>
|
||||
<ul id="snakeLeaderboard">
|
||||
{% for score in scores %}
|
||||
<li>
|
||||
<span>{{ score.position }}</span>
|
||||
<span>{{ score.name }}</span>
|
||||
<span>{{ score.score }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
{% if error %}
|
||||
<dialog id="errorDialog">
|
||||
<h2>Error</h2>
|
||||
<p>{{ error }}</p>
|
||||
<button onclick="errorDialog.close()">Close</button>
|
||||
</dialog>
|
||||
<script>
|
||||
const errorDialog = document.getElementById('errorDialog');
|
||||
if (errorDialog) {
|
||||
errorDialog.showModal();
|
||||
}
|
||||
</script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/static/js/snake.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/@cap.js/widget"></script>
|
||||
{% endblock %}
|
||||
@@ -4,7 +4,7 @@
|
||||
{% block description %}An unexpected error occurred on the server.{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="/static/css/500.css">
|
||||
<link rel="stylesheet" href="/static/css/errors/500.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
@@ -14,4 +14,10 @@
|
||||
Oopsie Woopsie! Uwu We made a fucky wucky!! A wittle fucko boingo! The code monkeys at our headquarters are working VEWY HAWD to fix this!
|
||||
</p>
|
||||
</section>
|
||||
<section>
|
||||
<h2>Actual error</h2>
|
||||
<p>
|
||||
{{ error }}
|
||||
</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
16
templates/errors/error.html
Normal file
@@ -0,0 +1,16 @@
|
||||
{% extends "bases/base.html" %}
|
||||
|
||||
{% block title %}{{ code }} - {{ err_name }}{% endblock %}
|
||||
{% block description %}The page you are looking for does not exist.{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section>
|
||||
<img src="https://http.cat/images/{{ code }}.jpg" alt="">
|
||||
</section>
|
||||
<section>
|
||||
<h2>Actual error</h2>
|
||||
<p>
|
||||
{{ description }}
|
||||
</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -1,17 +1,18 @@
|
||||
{% extends "bases/base.html" %}
|
||||
|
||||
{% block title %}Home - Alfie's basement{% endblock %}
|
||||
{% block title %}Home - {{ name }}'s basement{% endblock %}
|
||||
{% block description %}server backend survivor{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="/static/css/index.css">
|
||||
{% endblock %}
|
||||
|
||||
{%block content %}
|
||||
{% block content %}
|
||||
<section>
|
||||
<h1>A lil bit abt me</h1>
|
||||
<p>
|
||||
Im not good with writing so dont expect much here. I am a student who is learning c++ and python. I've Done a few projects that i think
|
||||
<span style="color: yellow; font-size: 0.8rem;">This is a "bit" out of date, will update soon ^^</span><br>
|
||||
Im not good with writing so dont expect much here. I was a student learning c++ and python. I've Done a few projects that i think
|
||||
are decent enough to show off, so I have put them on this website. I like to mess around with linux and have a few servers that I run. I've
|
||||
been running a server for a few years now, and I have learned a lot from it. I have also switched to linux on my main computer, which has been
|
||||
slightly annoying at times (mainly because one of my most played games' anticheat doesn't support on linux atm. Also, the lack of photoshop is
|
||||
@@ -37,7 +38,7 @@
|
||||
<img src="https://s1nez.nekoweb.org/img/7dcd20d4.gif" alt="">
|
||||
</section>
|
||||
<div class="flex-row">
|
||||
<a href="" id="spotify-link">
|
||||
<a href="https://www.last.fm/user/acetheking987" id="spotify-link">
|
||||
<div id="spotify">
|
||||
<h1 id="spotify-title"></h1>
|
||||
<h2 id="spotify-artist"></h2>
|
||||
@@ -62,12 +63,17 @@
|
||||
<img src="https://adriansblinkiecollection.neocities.org/stamps/e43.gif" alt="">
|
||||
</section>
|
||||
</div>
|
||||
<section class="smileos">
|
||||
<span style="margin: auto;">
|
||||
Try going to the <span style="color: #ff4343;">Terminal</span> for more information
|
||||
</span>
|
||||
</section>
|
||||
<section>
|
||||
<h1>Projects & stuff</h1>
|
||||
<p>just some projects ive worked on over time</p>
|
||||
<ul>
|
||||
<li>
|
||||
<h2>alfieking.dev</h2>
|
||||
<h2>alfieking.dev/proot.uk</h2>
|
||||
<p>
|
||||
This website is a project that I have been working on for a while now. I have made a few versions of it, but I have
|
||||
never been happy with them. I am quite happy with this version atm since it is more organized and has a design that I
|
||||
@@ -104,6 +110,24 @@
|
||||
<h1>Some News</h1>
|
||||
<h6>(dont expect this to be updated often tho :P)</h6>
|
||||
<ul>
|
||||
<li>
|
||||
<h2>05-03-2026</h2>
|
||||
<p>
|
||||
This is a short post since i plan on reworking these in the next few days, but i had mad a lot of changes to the site. Some are visible and some are not;
|
||||
the bigget change in the new terminal page that needs more features but that is a later me issue. I also redid the backend so making updates are much
|
||||
easier now. I also need to update the main about me and projects sections since they are old and not very good. """hopefully""" i should make a new news post
|
||||
soon when i have reworked the system.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<h2>18-01-2026</h2>
|
||||
<p>
|
||||
:O an update! thats unheard of on this site (aleast its more often than tf2 gets updates). finding motivation to work on things has been painful
|
||||
recently, but im wokring on my mental state a bit so hopefully there will be more updates. I am writing this before i make any major changes but
|
||||
i hope to add a blog or something, or maybe a daily thoughts thing that pings my phone to get me to write something. I also need to rewrite most
|
||||
of the home page as well since its kinda out of date :P
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
<h2>28-06-2025</h2>
|
||||
<p>
|
||||
@@ -111,7 +135,7 @@
|
||||
I didn't want to use a framework at first, mainly because I like the simplicity of a static site, but it allows me to use templatiing and makes
|
||||
adding new features easier and more organized. The site is also more interacive now, with a few secrets on some of the pages. I still plan on adding
|
||||
more secrets and features. I also plan on adding a blog section, that I will move this to, so that I can give updates on the site and other things
|
||||
that I find interesting.
|
||||
that I find interesting.
|
||||
</p>
|
||||
</li>
|
||||
<li>
|
||||
@@ -126,4 +150,5 @@
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
<iframe src="https://john.citrons.xyz/embed?ref=alfieking.dev" style="margin-left:auto;display:block;margin-right:auto;max-width:732px;width:100%;height:94px;border:none;"></iframe>
|
||||
{% endblock %}
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "bases/base.html" %}
|
||||
|
||||
{% block title %}Critters MK - Alfie's basement{% endblock %}
|
||||
{% block title %}Critters MK - {{ name }}'s basement{% endblock %}
|
||||
{% block description %}furry corner{% endblock %}
|
||||
{% block og_image %}/static/content/Toaster_v1.0_Dark.png{% endblock %}
|
||||
{% block keywords %}
|
||||
@@ -34,12 +34,22 @@ protogen v1.0, toaster v1.0
|
||||
<p>
|
||||
Here are some photos from the meets I have attended. I will add more as I attend more meets.
|
||||
</p>
|
||||
<h2 class="gallery-date">26th July 2025</h2>
|
||||
<div class="gallery">
|
||||
<img src="/static/content/fur_meets/26-07-2025_critters_mk/PXL_20250726_152110445.jpg" alt="Critters MK">
|
||||
<img src="/static/content/fur_meets/26-07-2025_critters_mk/PXL_20250726_155134418.jpg" alt="Critters MK">
|
||||
<img src="/static/content/fur_meets/26-07-2025_critters_mk/PXL_20250726_155226274.jpg" alt="Critters MK">
|
||||
<img src="/static/content/fur_meets/26-07-2025_critters_mk/PXL_20250726_155434701.jpg" alt="Critters MK">
|
||||
<h2 class="gallery-date">26th July 2025</h2>
|
||||
<div class="gallery-images">
|
||||
<img src="/static/content/photos/fur_meets/26-07-2025_critters_mk/PXL_20250726_152110445.jpg" alt="Critters MK">
|
||||
<img src="/static/content/photos/fur_meets/26-07-2025_critters_mk/PXL_20250726_155134418.jpg" alt="Critters MK">
|
||||
<img src="/static/content/photos/fur_meets/26-07-2025_critters_mk/PXL_20250726_155226274.jpg" alt="Critters MK">
|
||||
<img src="/static/content/photos/fur_meets/26-07-2025_critters_mk/PXL_20250726_155434701.jpg" alt="Critters MK">
|
||||
</div>
|
||||
</div>
|
||||
<div class="gallery">
|
||||
<h2 class="gallery-date">23rd Aug 2025</h2>
|
||||
<div class="gallery-images">
|
||||
<img src="/static/content/photos/fur_meets/23-08-2025_critters_mk/PXL_20250823_130640362.jpg" alt="Critters MK">
|
||||
<img src="/static/content/photos/fur_meets/23-08-2025_critters_mk/PXL_20250823_130648109.jpg" alt="Critters MK">
|
||||
<img src="/static/content/photos/fur_meets/23-08-2025_critters_mk/PXL_20250823_130659800.jpg" alt="Critters MK">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "bases/base.html" %}
|
||||
|
||||
{% block title %}Paws'N'Pistons - Alfie's basement{% endblock %}
|
||||
{% block title %}Paws'N'Pistons - {{ name }}'s basement{% endblock %}
|
||||
{% block description %}furry corner{% endblock %}
|
||||
{% block og_image %}/static/content/Toaster_v1.0_Dark.png{% endblock %}
|
||||
{% block keywords %}
|
||||
@@ -42,23 +42,25 @@ protogen v1.0, toaster v1.0
|
||||
ALSO, one of them offered to let me try their fursuit!!! Im now going to speedrun going broke trying to get a fursuit of my own cus of this :3 (I was
|
||||
already planning on getting one, but this just made me want one more).
|
||||
<br>
|
||||
<img src="/static/content/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_200906329.jpg" alt="me in a fursuit" id="woooooo">
|
||||
<img src="/static/content/photos/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_200906329.jpg" alt="me in a fursuit" id="woooooo">
|
||||
<br>
|
||||
The fursuit belongs to <a href="https://www.tiktok.com/@trickythefox" target="_blank">Tricky the Fox</a>, they are a very chill person and I had a great time talking to them ^w^.
|
||||
<h1>Photos :3</h1>
|
||||
<p>
|
||||
Here are some photos from the meets I have attended. I will add more as I attend more meets.
|
||||
</p>
|
||||
<h2 class="gallery-date">3rd Aug 2025</h2>
|
||||
<div class="gallery">
|
||||
<img src="/static/content/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_141943558.jpg" alt="Paws'N'Pistons">
|
||||
<img src="/static/content/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_150138054.jpg" alt="Paws'N'Pistons">
|
||||
<img src="/static/content/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_150249916.jpg" alt="Paws'N'Pistons">
|
||||
<img src="/static/content/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_183614897.jpg" alt="Paws'N'Pistons">
|
||||
<img src="/static/content/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_140629639.jpg" alt="Paws'N'Pistons">
|
||||
<img src="/static/content/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_141242090.jpg" alt="Paws'N'Pistons">
|
||||
<img src="/static/content/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_182023562.jpg" alt="Paws'N'Pistons">
|
||||
<img src="/static/content/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_184321576.jpg" alt="Paws'N'Pistons">
|
||||
<h2 class="gallery-date">3rd Aug 2025</h2>
|
||||
<div class="gallery-images">
|
||||
<img src="/static/content/photos/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_141943558.jpg" alt="Paws'N'Pistons">
|
||||
<img src="/static/content/photos/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_150138054.jpg" alt="Paws'N'Pistons">
|
||||
<img src="/static/content/photos/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_150249916.jpg" alt="Paws'N'Pistons">
|
||||
<img src="/static/content/photos/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_183614897.jpg" alt="Paws'N'Pistons">
|
||||
<img src="/static/content/photos/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_140629639.jpg" alt="Paws'N'Pistons">
|
||||
<img src="/static/content/photos/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_141242090.jpg" alt="Paws'N'Pistons">
|
||||
<img src="/static/content/photos/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_182023562.jpg" alt="Paws'N'Pistons">
|
||||
<img src="/static/content/photos/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_184321576.jpg" alt="Paws'N'Pistons">
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
63
templates/terminal.html
Normal file
@@ -0,0 +1,63 @@
|
||||
{% extends "bases/base.html" %}
|
||||
|
||||
{% block title %}SmileOS 2.0{% endblock %}
|
||||
{% block description %}SmileOS 2.0{% endblock %}
|
||||
{% block og_image %}static/content/smileos/SmileOS_2_icon_smile.webp{% endblock %}
|
||||
{% block icon %}static/content/smileos/SmileOS_2_icon_smile.webp{% endblock %}
|
||||
|
||||
{% block head %}
|
||||
<link rel="stylesheet" href="/static/css/terminal.css">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<section id="terminal">
|
||||
<div class="smileos-header">
|
||||
<img src="/static/content/smileos/SmileOS_2_icon_smile.webp" alt="smile">
|
||||
SmileOS 2.0
|
||||
<img src="/static/content/smileos/SmileOS_2_top_button_5.png" alt="minimize" style="margin-left: auto;">
|
||||
<img src="/static/content/smileos/SmileOS_2_top_button_4.png" alt="maximize">
|
||||
<img src="/static/content/smileos/SmileOS_2_top_button_3.png" alt="close">
|
||||
</div>
|
||||
<div id="terminal-container">
|
||||
<img src="/static/content/smileos/SmileOS2.webp" alt="logo" id="smileos-logo">
|
||||
<div id="terminal-buttons">
|
||||
<button class="terminal-button" id="smileos-tip">
|
||||
Tip of the Day
|
||||
</button>
|
||||
<button class="terminal-button" id="smileos-snake">
|
||||
Snake
|
||||
</button>
|
||||
<button class="terminal-button" id="smileos-leaderboard">
|
||||
Leaderboard
|
||||
</button>
|
||||
<button class="terminal-button" id="smileos-about">
|
||||
About
|
||||
</button>
|
||||
</div>
|
||||
<div id="terminal-window">
|
||||
<div class="smileos-header">
|
||||
<img src="/static/content/smileos/SmileOS_2_icon_tip.webp" alt="tip">
|
||||
<span id="smileos-window-title">Tip of the Day</span>
|
||||
<img src="/static/content/smileos/SmileOS_2_top_button_5.png" alt="minimize" style="margin-left: auto;">
|
||||
<img src="/static/content/smileos/SmileOS_2_top_button_4.png" alt="maximize">
|
||||
<img src="/static/content/smileos/SmileOS_2_top_button_3.png" alt="close">
|
||||
</div>
|
||||
<div id="window-container">
|
||||
<div id="window-content">
|
||||
<span id="smileos-window-content"><span class="red">SLAM BOUNCING</span>: Jump immediately after landing from a <span class="red">ground slam</span> to jump higher. The longer the ground slam fall, the higher the bounce.</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<section>
|
||||
<p>
|
||||
Note: this page has background music, you may have to allow the music in the browser and refesh to let it play, or you can try
|
||||
press <a id="smileos-restart-music">this</a> to restart it
|
||||
</p>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
<script src="/static/js/smileos.js"></script>
|
||||
{% endblock %}
|
||||
@@ -1,6 +1,6 @@
|
||||
{% extends "bases/base.html" %}
|
||||
|
||||
{% block title %}Toaster - Alfie's basement{% endblock %}
|
||||
{% block title %}Toaster - {{ name }}'s basement{% endblock %}
|
||||
{% block description %}furry corner{% endblock %}
|
||||
{% block og_image %}/static/content/Toaster_v1.0_Dark.png{% endblock %}
|
||||
{% block keywords %}
|
||||
@@ -86,19 +86,19 @@ protogen v1.0, toaster v1.0
|
||||
<li>
|
||||
<a href="/events/crittersmk"><b>Critters MK</b></a> - A furmeet in Milton Keynes.
|
||||
<div class="fur-meet-gallery-small">
|
||||
<img src="/static/content/fur_meets/26-07-2025_critters_mk/PXL_20250726_152110445.jpg" alt="Critters MK">
|
||||
<img src="/static/content/fur_meets/26-07-2025_critters_mk/PXL_20250726_155134418.jpg" alt="Critters MK">
|
||||
<img src="/static/content/fur_meets/26-07-2025_critters_mk/PXL_20250726_155226274.jpg" alt="Critters MK">
|
||||
<img src="/static/content/fur_meets/26-07-2025_critters_mk/PXL_20250726_155434701.jpg" alt="Critters MK">
|
||||
<img src="/static/content/photos/fur_meets/26-07-2025_critters_mk/PXL_20250726_152110445.jpg" alt="Critters MK">
|
||||
<img src="/static/content/photos/fur_meets/26-07-2025_critters_mk/PXL_20250726_155134418.jpg" alt="Critters MK">
|
||||
<img src="/static/content/photos/fur_meets/26-07-2025_critters_mk/PXL_20250726_155226274.jpg" alt="Critters MK">
|
||||
<img src="/static/content/photos/fur_meets/26-07-2025_critters_mk/PXL_20250726_155434701.jpg" alt="Critters MK">
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/events/paws-n-pistons"><b>Paws'N'Pistons</b></a> - A furry car meet around the UK.
|
||||
<div class="fur-meet-gallery-small">
|
||||
<img src="/static/content/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_141943558.jpg" alt="Paws'N'Pistons">
|
||||
<img src="/static/content/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_150138054.jpg" alt="Paws'N'Pistons">
|
||||
<img src="/static/content/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_150249916.jpg" alt="Paws'N'Pistons">
|
||||
<img src="/static/content/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_183614897.jpg" alt="Paws'N'Pistons">
|
||||
<img src="/static/content/photos/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_141943558.jpg" alt="Paws'N'Pistons">
|
||||
<img src="/static/content/photos/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_150138054.jpg" alt="Paws'N'Pistons">
|
||||
<img src="/static/content/photos/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_150249916.jpg" alt="Paws'N'Pistons">
|
||||
<img src="/static/content/photos/fur_meets/03-08-2025_paws_n_pistons/PXL_20250803_183614897.jpg" alt="Paws'N'Pistons">
|
||||
</div>
|
||||
</li>
|
||||
<p>Click on the links to view more photos from each event :3</p>
|
||||