feat: implement kList webapp with Flask framework
Adds complete Flask-based web application for server listing service, including routing, database integration, Discord OAuth, templates, static assets, and configuration. Handles user authentication, server browsing, and admin dashboard functionality. BROKEN
This commit is contained in:
41
src/ver/0.0.3/ecosystem.config.js
Normal file
41
src/ver/0.0.3/ecosystem.config.js
Normal file
@@ -0,0 +1,41 @@
|
||||
module.exports = {
|
||||
apps : [{
|
||||
name : "kList",
|
||||
namespace: "kServers",
|
||||
version: "0.0.3",
|
||||
interpreter: "/opt/klist/bin/python",
|
||||
script: "run.py",
|
||||
cwd: "/src/kList-v2/",
|
||||
interpreter_args: "-u",
|
||||
watch : false,
|
||||
error_file: "~/.config/kList/log/kList.error",
|
||||
out_file: "~/.config/kList/log/kList.output",
|
||||
log_file: "~/.config/kList/log/kList.log",
|
||||
time: true,
|
||||
log_date_format : "[ MMM|DD|YYYY - hh:mm a ]",
|
||||
min_uptime : 300,
|
||||
listen_timeout : 8000,
|
||||
kill_timeout : 1600,
|
||||
wait_ready : true,
|
||||
post_update: ["echo kList is lanuching!"],
|
||||
env_production: {
|
||||
APP_ENV: "production",
|
||||
POSTGRES_URI: "postgres://USER:PASSWORD@HOST:POST/DATABASE",
|
||||
BOT_TOKEN: "",
|
||||
CLIENT_ID: "",
|
||||
CLIENT_SECRET: "",
|
||||
SITE_NAME: "kServers",
|
||||
SECRET_KEY: "",
|
||||
ADMIN_USER_IDS: []
|
||||
}, env_development: {
|
||||
APP_ENV: "development",
|
||||
POSTGRES_URI: "postgres://USER:PASSWORD@HOST:POST/DATABASE",
|
||||
BOT_TOKEN: "",
|
||||
CLIENT_ID: "",
|
||||
CLIENT_SECRET: "",
|
||||
SITE_NAME: "kServers",
|
||||
SECRET_KEY: "",
|
||||
ADMIN_USER_IDS: []
|
||||
}
|
||||
}]
|
||||
}
|
||||
11
src/ver/0.0.3/requirements.txt
Normal file
11
src/ver/0.0.3/requirements.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
humanfriendly
|
||||
Python.js
|
||||
discord
|
||||
discord.py
|
||||
colorama
|
||||
psycopg2-binary
|
||||
dnspython
|
||||
PyYAML
|
||||
requests
|
||||
flask
|
||||
flask_dance
|
||||
6
src/ver/0.0.3/run.py
Normal file
6
src/ver/0.0.3/run.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from webapp import create_app
|
||||
|
||||
app = create_app()
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run()
|
||||
41
src/ver/0.0.3/webapp/__init__.py
Normal file
41
src/ver/0.0.3/webapp/__init__.py
Normal file
@@ -0,0 +1,41 @@
|
||||
# kList-v2/webapp/__init__.py
|
||||
import os
|
||||
import logging
|
||||
from rich.logging import RichHandler
|
||||
from flask import Flask
|
||||
from flask_dance.contrib.discord import make_discord_blueprint
|
||||
from werkzeug.middleware.proxy_fix import ProxyFix
|
||||
|
||||
def create_app():
|
||||
"""Create and configure an instance of the Flask application."""
|
||||
app = Flask('kList')
|
||||
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1)
|
||||
|
||||
from . import config
|
||||
app.config.from_object(config)
|
||||
app.secret_key = app.config.get("SECRET_KEY")
|
||||
|
||||
logging.basicConfig(level="INFO", format="%(message)s", datefmt="[%X]", handlers=[RichHandler(rich_tracebacks=True, show_path=False)])
|
||||
log = logging.getLogger("werkzeug")
|
||||
log.setLevel(logging.ERROR)
|
||||
|
||||
if app.config.get("DEBUG_MODE"):
|
||||
os.environ["OAUTHLIB_INSECURE_TRANSPORT"] = "1"
|
||||
|
||||
# --- SIMPLIFIED: Import and register the single routes blueprint ---
|
||||
from . import routes
|
||||
app.register_blueprint(routes.bp)
|
||||
|
||||
discord_bp = make_discord_blueprint(
|
||||
client_id=app.config.get("CLIENT_ID"),
|
||||
client_secret=app.config.get("CLIENT_SECRET"),
|
||||
redirect_url="/login/discord/authorized",
|
||||
scope=["identify"]
|
||||
)
|
||||
app.register_blueprint(discord_bp, url_prefix="/login")
|
||||
|
||||
from . import database
|
||||
with app.app_context():
|
||||
database.init_db()
|
||||
|
||||
return app
|
||||
15
src/ver/0.0.3/webapp/config.py
Normal file
15
src/ver/0.0.3/webapp/config.py
Normal file
@@ -0,0 +1,15 @@
|
||||
# kList-v2/webapp/config.py
|
||||
import os
|
||||
import logging
|
||||
|
||||
APP_ENV = os.environ.get('production', 'development')
|
||||
DEBUG_MODE = (APP_ENV == 'development')
|
||||
POSTGRES_URI = os.environ.get("POSTGRES_URI")
|
||||
SECRET_KEY = os.environ.get("SECRET_KEY")
|
||||
CLIENT_ID = os.environ.get("CLIENT_ID")
|
||||
CLIENT_SECRET = os.environ.get("CLIENT_SECRET")
|
||||
BOT_TOKEN = os.environ.get("BOT_TOKEN")
|
||||
SITE_NAME = os.environ.get("SITE_NAME", "Server List")
|
||||
ADMIN_USER_IDS_STR = os.environ.get("ADMIN_USER_IDS", "")
|
||||
ADMIN_USER_IDS = [int(uid.strip()) for uid in ADMIN_USER_IDS_STR.split(',') if uid.strip()]
|
||||
RED_CONFIG_TABLE = '"kBump.663348082717122527510486617053803107995"."GUILD"'
|
||||
48
src/ver/0.0.3/webapp/database.py
Normal file
48
src/ver/0.0.3/webapp/database.py
Normal file
@@ -0,0 +1,48 @@
|
||||
# kList-v2/webapp/database.py
|
||||
|
||||
import os
|
||||
import psycopg2
|
||||
import psycopg2.extras
|
||||
from psycopg2 import pool
|
||||
import logging
|
||||
|
||||
# Get a logger for this module
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# --- CONFIGURATION ---
|
||||
POSTGRES_URI = os.environ.get("POSTGRES_URI")
|
||||
if not POSTGRES_URI:
|
||||
raise ValueError("POSTGRES_URI environment variable not set.")
|
||||
|
||||
db_pool = None
|
||||
|
||||
def init_db():
|
||||
"""Initializes the database connection pool."""
|
||||
global db_pool
|
||||
# This check prevents re-initializing if it's already done
|
||||
if db_pool is None:
|
||||
try:
|
||||
db_pool = psycopg2.pool.SimpleConnectionPool(minconn=1, maxconn=10, dsn=POSTGRES_URI)
|
||||
log.info("Successfully initialized PostgreSQL connection pool.")
|
||||
except Exception as e:
|
||||
log.error(f"FATAL: Could not initialize database pool. Error: {e}")
|
||||
raise e
|
||||
|
||||
def query_db(query, params=None, fetch="all"):
|
||||
"""A helper function to manage connections and cursors."""
|
||||
if db_pool is None:
|
||||
raise ConnectionError("Database pool is not initialized. Check startup logs for errors.")
|
||||
|
||||
conn = None
|
||||
try:
|
||||
conn = db_pool.getconn()
|
||||
with conn.cursor(cursor_factory=psycopg2.extras.DictCursor) as cursor:
|
||||
cursor.execute(query, params)
|
||||
if fetch == "one":
|
||||
return cursor.fetchone()
|
||||
if fetch == "all":
|
||||
return cursor.fetchall()
|
||||
conn.commit()
|
||||
finally:
|
||||
if conn:
|
||||
db_pool.putconn(conn)
|
||||
0
src/ver/0.0.3/webapp/helper.py
Normal file
0
src/ver/0.0.3/webapp/helper.py
Normal file
177
src/ver/0.0.3/webapp/routes.py
Normal file
177
src/ver/0.0.3/webapp/routes.py
Normal file
@@ -0,0 +1,177 @@
|
||||
# kList-v2/webapp/routes.py
|
||||
import json
|
||||
import requests
|
||||
import logging
|
||||
from flask import Blueprint, render_template, abort, session, redirect, url_for, request, jsonify
|
||||
from flask_dance.contrib.discord import discord
|
||||
|
||||
# Use relative imports because this file is part of the 'webapp' package
|
||||
from .database import query_db
|
||||
from .config import RED_CONFIG_TABLE, ADMIN_USER_IDS, BOT_TOKEN
|
||||
|
||||
# A single Blueprint for all application routes
|
||||
bp = Blueprint('main', __name__)
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# --- HELPER FUNCTION ---
|
||||
DISCORD_API_URL = "https://discord.com/api/v10"
|
||||
|
||||
def get_discord_guild_data(server_id):
|
||||
"""Fetches guild (server) information from the Discord API."""
|
||||
headers = {"Authorization": f"Bot {BOT_TOKEN}"}
|
||||
url = f"{DISCORD_API_URL}/guilds/{server_id}?with_counts=true"
|
||||
try:
|
||||
response = requests.get(url, headers=headers)
|
||||
response.raise_for_status()
|
||||
response.json()
|
||||
json_data = response.json()
|
||||
log.info(f"OUT: {response}")
|
||||
return json_data
|
||||
except requests.exceptions.RequestException:
|
||||
log.error(f"Discord API request failed for server ID {server_id}. Error: {e}")
|
||||
return None
|
||||
|
||||
# --- API ROUTE ---
|
||||
@bp.route("/api/v1/server/<int:server_id>")
|
||||
def get_server_info(server_id):
|
||||
sql_query = f"SELECT json_data FROM {RED_CONFIG_TABLE} WHERE primary_key_1 = %s;"
|
||||
log.info(f"QUERY: {sql_query}")
|
||||
raw_data = query_db(sql_query, (server_id,), fetch="one")
|
||||
if not raw_data:
|
||||
log.info(f"RAW DATA: {raw_data}")
|
||||
return jsonify({"error": "Server not found"}), 404
|
||||
return jsonify(raw_data['json_data'])
|
||||
|
||||
# --- ERROR HANDLER ROUTES ---
|
||||
@bp.errorhandler(404)
|
||||
def page_not_found(err):
|
||||
return render_template('404.html'), 404
|
||||
|
||||
@bp.errorhandler(500)
|
||||
def internal_server_error(err):
|
||||
return render_template('500.html'), 500
|
||||
|
||||
@bp.errorhandler(502)
|
||||
def service_error(err):
|
||||
return render_template('502.html'), 502
|
||||
|
||||
@bp.errorhandler(503)
|
||||
def service_unavailable(err):
|
||||
return render_template('503.html'), 503
|
||||
|
||||
# --- WEB PAGE ROUTES ---
|
||||
|
||||
@bp.route("/")
|
||||
def index():
|
||||
sql_query = f"SELECT primary_key_1, json_data FROM {RED_CONFIG_TABLE} WHERE (json_data->>'status') = 'complete';"
|
||||
servers_from_db = query_db(sql_query)
|
||||
log.info(f"QUERY: {sql_query}")
|
||||
servers_for_template = []
|
||||
for row in servers_from_db:
|
||||
server_info = row['json_data']
|
||||
server_info['_id'] = row['primary_key_1']
|
||||
servers_for_template.append(server_info)
|
||||
log.info(f"INFO: {server_info}")
|
||||
return render_template('index.html', servers=servers_for_template)
|
||||
|
||||
@bp.route("/server/<string:server_name>")
|
||||
def view_server(server_name):
|
||||
original_server_name = server_name.replace('-', ' ')
|
||||
log.info(f"ORIGINAL NAME: {original_server_name}")
|
||||
sql_query = f"SELECT primary_key_1, json_data FROM {RED_CONFIG_TABLE} WHERE (json_data->>'server_name') = %s;"
|
||||
log.info(f"QUERY: {sql_query}")
|
||||
raw_data = query_db(sql_query, (original_server_name,), fetch="one")
|
||||
log.info(f"QUERY: {raw_data}")
|
||||
if not raw_data:
|
||||
abort(404)
|
||||
server_db_data = raw_data['json_data']
|
||||
log.info(f"SERVER DATA: {server_db_data}")
|
||||
server_id = raw_data['primary_key_1']
|
||||
server_db_data['server_id'] = server_id
|
||||
log.info(f"SERVER ID: {server_id}")
|
||||
discord_guild_data = get_discord_guild_data(server_id)
|
||||
log.info(f"GUILD DATA: {get_discord_guild_data}")
|
||||
if not discord_guild_data:
|
||||
log.info(f"GUILD DATA: {discord_guild_data}")
|
||||
abort(503)
|
||||
if server_db_data.get('vanity_code'):
|
||||
discord_guild_data["invite"] = f"https://discord.gg/{server_db_data.get('vanity_code')}"
|
||||
log.info(f"GUILD DATA: {discord_guild_data}")
|
||||
elif server_db_data.get('invite_code'):
|
||||
discord_guild_data["invite"] = f"https://discord.gg/{server_db_data.get('invite_code')}"
|
||||
log.info(f"GUILD DATA: {discord_guild_data}")
|
||||
else:
|
||||
discord_guild_data["invite"] = None
|
||||
log.info(f"TEMPLATE: {render_template}")
|
||||
return render_template('view_server.html', server=server_db_data, discord=discord_guild_data)
|
||||
|
||||
@bp.route('/browse/', defaults={'tag_name': None})
|
||||
@bp.route('/browse/<string:tag_name>')
|
||||
def browse(tag_name):
|
||||
"""Renders a page for browsing servers by tag."""
|
||||
log.info(f"Browse page requested for tag: {tag_name or 'All'}")
|
||||
|
||||
# 1. Get all unique tags from the database
|
||||
tag_query = f"SELECT json_data->>'tags' AS tags FROM {RED_CONFIG_TABLE} WHERE (json_data->>'status') = 'complete';"
|
||||
all_servers_tags = query_db(tag_query)
|
||||
all_tags = set()
|
||||
for server in all_servers_tags:
|
||||
tags_string = server.get('tags')
|
||||
if isinstance(tags_string, str):
|
||||
tags_list = [tag.strip() for tag in tags_string.split(',') if tag.strip()]
|
||||
all_tags.update(tags_list)
|
||||
|
||||
sorted_tags = sorted(list(all_tags), key=str.lower)
|
||||
log.info(f"Found {len(sorted_tags)} unique tags to display.")
|
||||
|
||||
# 2. Filter the servers based on the selected tag
|
||||
if tag_name:
|
||||
log.info(f"Filtering servers by tag: '{tag_name}'")
|
||||
sql_query = f"SELECT primary_key_1, json_data FROM {RED_CONFIG_TABLE} WHERE (json_data->>'status') = 'complete' AND (json_data->>'tags') ILIKE %s;"
|
||||
filtered_servers_raw = query_db(sql_query, (f'%{tag_name}%',))
|
||||
else:
|
||||
log.info("No tag selected, fetching all servers.")
|
||||
sql_query = f"SELECT primary_key_1, json_data FROM {RED_CONFIG_TABLE} WHERE (json_data->>'status') = 'complete';"
|
||||
filtered_servers_raw = query_db(sql_query)
|
||||
|
||||
filtered_servers = []
|
||||
for row in filtered_servers_raw:
|
||||
server_info = row['json_data']
|
||||
server_info['_id'] = row['primary_key_1']
|
||||
filtered_servers.append(server_info)
|
||||
|
||||
log.info(f"Found {len(filtered_servers)} servers to display.")
|
||||
return render_template('browse.html', all_tags=sorted_tags, servers=filtered_servers, current_tag=tag_name)
|
||||
|
||||
# --- AUTH & ADMIN ROUTES ---
|
||||
@bp.route("/auth")
|
||||
def auth():
|
||||
if not discord.authorized:
|
||||
return redirect(url_for(".index")) # Use dot prefix for Blueprints
|
||||
user_info = discord.get("/api/users/@me").json()
|
||||
user_id = int(user_info.get("id"))
|
||||
if user_id in ADMIN_USER_IDS:
|
||||
session["is_admin"] = True
|
||||
session["user_id"] = user_id
|
||||
session["username"] = user_info.get("username")
|
||||
else:
|
||||
session.clear()
|
||||
return "<h1>Access Denied</h1><p>You are not authorized to view this page.</p>", 403
|
||||
return redirect(url_for(".dashboard"))
|
||||
|
||||
@bp.route('/admin')
|
||||
def admin():
|
||||
if session.get("is_admin"):
|
||||
return redirect(url_for(".dashboard"))
|
||||
return render_template('admin.html')
|
||||
|
||||
@bp.route('/dashboard')
|
||||
def dashboard():
|
||||
if not session.get("is_admin"):
|
||||
return redirect(url_for(".admin"))
|
||||
return render_template('dashboard.html')
|
||||
|
||||
@bp.route('/logout')
|
||||
def logout():
|
||||
session.clear()
|
||||
return redirect(url_for('.index'))
|
||||
BIN
src/ver/0.0.3/webapp/static/favicon.ico
Normal file
BIN
src/ver/0.0.3/webapp/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 17 KiB |
133
src/ver/0.0.3/webapp/static/js/app.js
Normal file
133
src/ver/0.0.3/webapp/static/js/app.js
Normal file
@@ -0,0 +1,133 @@
|
||||
/* -----------------------------------------------
|
||||
/* How to use? : Check the GitHub README
|
||||
/* ----------------------------------------------- */
|
||||
|
||||
/* To load a config file (particles.json) you need to host this demo (MAMP/WAMP/local)... */
|
||||
/*
|
||||
particlesJS.load('particles-js', 'particles.json', function() {
|
||||
console.log('particles.js loaded - callback');
|
||||
});
|
||||
*/
|
||||
|
||||
/* Otherwise just put the config content (json): */
|
||||
|
||||
particlesJS('particles-js',
|
||||
|
||||
{
|
||||
"particles": {
|
||||
"number": {
|
||||
"value": 80,
|
||||
"density": {
|
||||
"enable": true,
|
||||
"value_area": 2000
|
||||
}
|
||||
},
|
||||
"color": {
|
||||
"value": "random"
|
||||
},
|
||||
"shape": {
|
||||
"type": "circle",
|
||||
"stroke": {
|
||||
"width": 0,
|
||||
"color": "#000000"
|
||||
},
|
||||
"polygon": {
|
||||
"nb_sides": 5
|
||||
},
|
||||
"image": {
|
||||
"src": "img/github.svg",
|
||||
"width": 100,
|
||||
"height": 100
|
||||
}
|
||||
},
|
||||
"opacity": {
|
||||
"value": 0.5,
|
||||
"random": false,
|
||||
"anim": {
|
||||
"enable": false,
|
||||
"speed": 1,
|
||||
"opacity_min": 0.1,
|
||||
"sync": false
|
||||
}
|
||||
},
|
||||
"size": {
|
||||
"value": 5,
|
||||
"random": true,
|
||||
"anim": {
|
||||
"enable": false,
|
||||
"speed": 40,
|
||||
"size_min": 0.1,
|
||||
"sync": false
|
||||
}
|
||||
},
|
||||
"line_linked": {
|
||||
"enable": true,
|
||||
"distance": 150,
|
||||
"color": "#ffffff",
|
||||
"opacity": 0.4,
|
||||
"width": 1
|
||||
},
|
||||
"move": {
|
||||
"enable": true,
|
||||
"speed": 6,
|
||||
"direction": "none",
|
||||
"random": false,
|
||||
"straight": false,
|
||||
"out_mode": "out",
|
||||
"attract": {
|
||||
"enable": false,
|
||||
"rotateX": 600,
|
||||
"rotateY": 1200
|
||||
}
|
||||
}
|
||||
},
|
||||
"interactivity": {
|
||||
"detect_on": "canvas",
|
||||
"events": {
|
||||
"onhover": {
|
||||
"enable": true,
|
||||
"mode": "grab"
|
||||
},
|
||||
"onclick": {
|
||||
"enable": true,
|
||||
"mode": "push"
|
||||
},
|
||||
"resize": true
|
||||
},
|
||||
"modes": {
|
||||
"grab": {
|
||||
"distance": 400,
|
||||
"line_linked": {
|
||||
"opacity": 1
|
||||
}
|
||||
},
|
||||
"bubble": {
|
||||
"distance": 400,
|
||||
"size": 40,
|
||||
"duration": 2,
|
||||
"opacity": 8,
|
||||
"speed": 3
|
||||
},
|
||||
"repulse": {
|
||||
"distance": 200
|
||||
},
|
||||
"push": {
|
||||
"particles_nb": 4
|
||||
},
|
||||
"remove": {
|
||||
"particles_nb": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
"retina_detect": true,
|
||||
"config_demo": {
|
||||
"hide_card": false,
|
||||
"background_color": "#b61924",
|
||||
"background_image": "",
|
||||
"background_position": "50% 50%",
|
||||
"background_repeat": "no-repeat",
|
||||
"background_size": "cover"
|
||||
}
|
||||
}
|
||||
|
||||
);
|
||||
1541
src/ver/0.0.3/webapp/static/js/particles.js
Normal file
1541
src/ver/0.0.3/webapp/static/js/particles.js
Normal file
File diff suppressed because it is too large
Load Diff
96
src/ver/0.0.3/webapp/static/styles/base.css
Normal file
96
src/ver/0.0.3/webapp/static/styles/base.css
Normal file
@@ -0,0 +1,96 @@
|
||||
html, body {
|
||||
overflow-y: scroll;
|
||||
margin: 0; /* This removes the extra space */
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: #000;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
code {
|
||||
margin: 0; /* This removes the extra space */
|
||||
}
|
||||
|
||||
/* This is the updated rule */
|
||||
pre {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
#particles-js {
|
||||
z-index: -1;
|
||||
position: fixed;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.invite-button {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.list-group-item.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.list-group-item.active::after {
|
||||
content: ' «';
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
@media (max-width: 767px) {
|
||||
.col-md-3 {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
#hidden-admin-button {
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
right: 15px;
|
||||
z-index: 9999;
|
||||
font-size: 18px; /* Adjust size as needed */
|
||||
opacity: 0.0; /* Make it semi-transparent */
|
||||
text-decoration: none;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
#hidden-admin-button:hover {
|
||||
opacity: 1.0; /* Make it fully visible on hover */
|
||||
color: rgb(255, 125, 125);
|
||||
}
|
||||
|
||||
.access-toast {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
background-color: #28a745; /* Bootstrap success green */
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
z-index: 10000;
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s ease-in-out, bottom 0.4s ease-in-out;
|
||||
}
|
||||
|
||||
.access-toast.show {
|
||||
opacity: 1;
|
||||
bottom: 40px;
|
||||
}
|
||||
|
||||
.center-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 80vh;
|
||||
text-align: center;
|
||||
}
|
||||
15
src/ver/0.0.3/webapp/static/styles/browse.css
Normal file
15
src/ver/0.0.3/webapp/static/styles/browse.css
Normal file
@@ -0,0 +1,15 @@
|
||||
.list-group-item.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.list-group-item.active::after {
|
||||
content: ' «';
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
/* Example: Add a bit more space above the tag list when on mobile devices */
|
||||
@media (max-width: 767px) {
|
||||
.col-md-3 {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
8
src/ver/0.0.3/webapp/static/styles/error.css
Normal file
8
src/ver/0.0.3/webapp/static/styles/error.css
Normal file
@@ -0,0 +1,8 @@
|
||||
{% extends "base.css" %}
|
||||
.center-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 80vh;
|
||||
text-align: center;
|
||||
}
|
||||
35
src/ver/0.0.3/webapp/static/styles/index.css
Normal file
35
src/ver/0.0.3/webapp/static/styles/index.css
Normal file
@@ -0,0 +1,35 @@
|
||||
.servers {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center; /* This keeps the cards centered */
|
||||
opacity: 0%;
|
||||
padding: 10px; /* This adds some nice spacing around the cards */
|
||||
}
|
||||
|
||||
.card {
|
||||
background-color: rgba(211, 211, 211, 0.55) !important;
|
||||
}
|
||||
|
||||
.invite-button {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.list-group-item.active {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.list-group-item.active::after {
|
||||
content: ' «';
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
/* Example: Add a bit more space above the tag list when on mobile devices */
|
||||
@media (max-width: 767px) {
|
||||
.col-md-3 {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
}
|
||||
65
src/ver/0.0.3/webapp/static/styles/navbar.css
Normal file
65
src/ver/0.0.3/webapp/static/styles/navbar.css
Normal file
@@ -0,0 +1,65 @@
|
||||
.center-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 80vh;
|
||||
text-align: center;
|
||||
}
|
||||
body {
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
|
||||
.navbar {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.bd-placeholder-img {
|
||||
font-size: 1.125rem;
|
||||
text-anchor: middle;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.bd-placeholder-img-lg {
|
||||
font-size: 3.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.b-example-divider {
|
||||
height: 3rem;
|
||||
background-color: rgba(0, 0, 0, .1);
|
||||
border: solid rgba(0, 0, 0, .15);
|
||||
border-width: 1px 0;
|
||||
box-shadow: inset 0 .5em 1.5em rgba(0, 0, 0, .1), inset 0 .125em .5em rgba(0, 0, 0, .15);
|
||||
}
|
||||
|
||||
.b-example-vr {
|
||||
flex-shrink: 0;
|
||||
width: 1.5rem;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.bi {
|
||||
vertical-align: -.125em;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.nav-scroller {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
height: 2.75rem;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.nav-scroller .nav {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
padding-bottom: 1rem;
|
||||
margin-top: -1px;
|
||||
overflow-x: auto;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
48
src/ver/0.0.3/webapp/static/styles/view_server.css
Normal file
48
src/ver/0.0.3/webapp/static/styles/view_server.css
Normal file
@@ -0,0 +1,48 @@
|
||||
.container {
|
||||
margin-top : 10px;
|
||||
background: rgba(211, 211, 211, 0.55);
|
||||
}
|
||||
|
||||
#serverName {
|
||||
text-align: center;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.info-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: wrap;
|
||||
height: 100%;
|
||||
margin-left: 30px;
|
||||
width: 80%;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.info > img {
|
||||
height: 200px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.info-column > a {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.info-column > .card-body {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
visibility: 55%;
|
||||
}
|
||||
|
||||
.card-body > a {
|
||||
margin-right: 10px;
|
||||
}
|
||||
23
src/ver/0.0.3/webapp/templates/3xx.html
Normal file
23
src/ver/0.0.3/webapp/templates/3xx.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="{{url_for('static', filename='styles/base.css')}}">
|
||||
<title>{{ error.code }} | {{site_name}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="center-container">
|
||||
<div class="card shadow-lg p-4 text-light" style="background-color: rgba(44, 47, 51, 0.8);">
|
||||
<h1 class="display-1">{{ error.code }}</h1>
|
||||
<a href="{{ url_for('index') }}" class="btn btn-primary mt-3">Go Home</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/markdown-it/12.0.4/markdown-it.js"></script>
|
||||
</html>
|
||||
31
src/ver/0.0.3/webapp/templates/4xx.html
Normal file
31
src/ver/0.0.3/webapp/templates/4xx.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="{{url_for('static', filename='styles/base.css')}}">
|
||||
<title>{{ error.code }} | {{site_name}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="center-container">
|
||||
<div class="card shadow-lg p-4 text-light" style="background-color: rgba(44, 47, 51, 0.8);">
|
||||
<h1 class="display-1">{{ error.code }}</h1>
|
||||
<h2>Page Not Found</h2>
|
||||
<p class="lead">Sorry, the page you are looking for does not exist.</p>
|
||||
<a href="{{ url_for('index') }}" class="btn btn-primary mt-3">Go Home</a>
|
||||
|
||||
<hr class="my-4">
|
||||
<p class="text-muted small">
|
||||
<strong>Note for server owners:</strong> If you're seeing this page for your server, its name may have recently changed on Discord. You can fix the link by running the `{p}sync` command in your server.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/markdown-it/12.0.4/markdown-it.js"></script>
|
||||
</html>
|
||||
31
src/ver/0.0.3/webapp/templates/5xx.html
Normal file
31
src/ver/0.0.3/webapp/templates/5xx.html
Normal file
@@ -0,0 +1,31 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="{{url_for('static', filename='styles/base.css')}}">
|
||||
<title>{{ error.code }} | {{site_name}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="center-container">
|
||||
<div class="card shadow-lg p-4 text-light" style="background-color: rgba(44, 47, 51, 0.8);">
|
||||
<h1 class="display-1">{{ error.code }}</h1>
|
||||
<h2>Page Not Found</h2>
|
||||
<p class="lead">Sorry, the page you are looking for does not exist.</p>
|
||||
<a href="{{ url_for('index') }}" class="btn btn-primary mt-3">Go Home</a>
|
||||
|
||||
<hr class="my-4">
|
||||
<p class="text-muted small">
|
||||
<strong>Note for server owners:</strong> If you're seeing this page for your server, its name may have recently changed on Discord. You can fix the link by running the `{p}sync` command in your server.
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/markdown-it/12.0.4/markdown-it.js"></script>
|
||||
</html>
|
||||
28
src/ver/0.0.3/webapp/templates/admin.html
Normal file
28
src/ver/0.0.3/webapp/templates/admin.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ site_name }} | Admin Login{% endblock %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
|
||||
{% block content %}
|
||||
<div class="container my-4 text-light">
|
||||
<div class="row justify-content-center text-center">
|
||||
<div class="col-md-6">
|
||||
<div class="card bg-dark p-4">
|
||||
<h2>Admin Access Required</h2>
|
||||
<p class="lead">Please log in with an authorized Discord account to continue.</p>
|
||||
<hr>
|
||||
<a href="{{ url_for('discord.login') }}" class="btn btn-primary btn-lg">
|
||||
Login with Discord
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/markdown-it/12.0.4/markdown-it.js"></script>
|
||||
{% endblock %}
|
||||
81
src/ver/0.0.3/webapp/templates/base.html
Normal file
81
src/ver/0.0.3/webapp/templates/base.html
Normal file
@@ -0,0 +1,81 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="{{url_for('static', filename='styles/base.css')}}">
|
||||
|
||||
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
|
||||
{% block links %}{% endblock %}
|
||||
<title>{% block title %}{{ site_name }}{% endblock %}</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
<div id="particles-js"></div>
|
||||
<script src="{{url_for('static', filename='js/particles.js')}}"></script>
|
||||
<script src="{{url_for('static', filename='js/app.js')}}"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/markdown-it/12.0.4/markdown-it.js"></script>
|
||||
<script>
|
||||
$(document).ready(() => {
|
||||
const md = window.markdownit();
|
||||
const elements = document.getElementsByClassName("server-description");
|
||||
for (var i = 0; i < elements.length; i++) {
|
||||
elements[i].innerHTML = md.render(elements[i].innerHTML);
|
||||
}
|
||||
$(".servers").animate({
|
||||
opacity: "100%"
|
||||
}, 600)
|
||||
})
|
||||
</script>
|
||||
{% block scripts %}
|
||||
<a id="hidden-admin-button" href="{{ url_for('main.admin') }}" title="">👀</a>
|
||||
|
||||
<script>
|
||||
const adminButton = document.getElementById('hidden-admin-button');
|
||||
|
||||
if (adminButton) {
|
||||
adminButton.addEventListener('click', function(event) {
|
||||
// Always prevent the default link action first
|
||||
event.preventDefault();
|
||||
|
||||
if (!event.ctrlKey && !event.metaKey) {
|
||||
// Denied case
|
||||
alert("❌ Access Denied! ❌|⚠️Only authorized personnel! ⚠️");
|
||||
} else {
|
||||
// --- GRANTED CASE ---
|
||||
// 1. Create the notification element
|
||||
const toast = document.createElement('div');
|
||||
toast.textContent = '✅ Access Granted! Redirecting...';
|
||||
toast.className = 'access-toast';
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// 2. Animate it into view
|
||||
// A tiny delay is needed for the CSS transition to work
|
||||
setTimeout(() => {
|
||||
toast.classList.add('show');
|
||||
}, 10);
|
||||
|
||||
// 3. Redirect to the admin page after a short delay
|
||||
setTimeout(() => {
|
||||
window.location.href = adminButton.href;
|
||||
}, 1000); // Redirect after 1 second
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
</body>
|
||||
<footer>
|
||||
<nav class="navbar sticky-bottom bg-light">
|
||||
<div class="container-fluid">
|
||||
<a class="navbar-brand" href="#">Sticky bottom</a>
|
||||
</div>
|
||||
</nav>
|
||||
</footer>
|
||||
</html>
|
||||
91
src/ver/0.0.3/webapp/templates/browse.html
Normal file
91
src/ver/0.0.3/webapp/templates/browse.html
Normal file
@@ -0,0 +1,91 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Browse Servers | {{ site_name }}{% endblock %}
|
||||
|
||||
{% block links %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="{{url_for('static', filename='styles/index.css')}}">
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container my-4">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<a href="{{ url_for('.index') }}" class="btn btn-outline-light">← Back to Home</a>
|
||||
<h1 class="text-light">Browse Servers</h1>
|
||||
</div>
|
||||
<hr class="text-light">
|
||||
</div>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-3">
|
||||
<h4 class="text-light">Tags</h4>
|
||||
<div class="list-group">
|
||||
<a href="{{ url_for('.browse') }}" class="list-group-item list-group-item-action {% if not current_tag %}active{% endif %}">
|
||||
All Servers
|
||||
</a>
|
||||
{% for tag in all_tags %}
|
||||
<a href="{{ url_for('.browse', tag_name=tag) }}" class="list-group-item list-group-item-action {% if tag == current_tag %}active{% endif %}">
|
||||
{{ tag }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-9">
|
||||
<div class="servers">
|
||||
{% if servers %}
|
||||
{% for server in servers %}
|
||||
<div class="card shadow-lg" style="width: 18rem; margin:5px;">
|
||||
<img class="card-img-top" src="{{server['icon_url']}}">
|
||||
<div class="card-body">
|
||||
<div class="card-header bg-danger bg-gradient text-light" style="margin-bottom:5px;">
|
||||
<strong>{{server['server_name']}}</strong>
|
||||
</div>
|
||||
<p class="card-text server-description">{{ server['description'] }}</p>
|
||||
<p class="card-text">
|
||||
<strong>Tags:</strong> {{ server['tags'] }}
|
||||
</p>
|
||||
<a class="btn btn-outline-success d-block mx-auto" href="/server/{{ server['server_name']|replace(' ', '-') }}">View Server</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="text-light text-center mt-5">
|
||||
<h4>No servers found with the tag "{{ current_tag }}".</h4>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
{% block scripts %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/markdown-it/12.0.4/markdown-it.js"></script>
|
||||
<script>
|
||||
$(document).ready(() => {
|
||||
// This is the part that renders markdown
|
||||
const md = window.markdownit();
|
||||
const elements = document.getElementsByClassName("server-description");
|
||||
for (var i = 0; i < elements.length; i++) {
|
||||
elements[i].innerHTML = md.render(elements[i].innerHTML);
|
||||
}
|
||||
|
||||
// --- ADD THIS ANIMATION ---
|
||||
// This fades in the server list, making it visible
|
||||
$(".servers").animate({
|
||||
opacity: "100%"
|
||||
}, 600)
|
||||
})
|
||||
</script>
|
||||
{% endblock %}
|
||||
41
src/ver/0.0.3/webapp/templates/dashboard.html
Normal file
41
src/ver/0.0.3/webapp/templates/dashboard.html
Normal file
@@ -0,0 +1,41 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Dashboard | {{ site_name }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<body>
|
||||
<div class="container my-4 text-light">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h1>Admin Dashboard</h1>
|
||||
<div>
|
||||
<span class="me-3">Welcome, {{ session.username }}!</span>
|
||||
<a href="{{ url_for('main.logout') }}" class="btn btn-secondary">Logout</a>
|
||||
</div>
|
||||
</div>
|
||||
<hr>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-9">
|
||||
<h2>Overview</h2>
|
||||
<p>You are successfully logged in. This is the main content area for your dashboard.</p>
|
||||
</div>
|
||||
|
||||
<div class="col-md-3">
|
||||
<div class="card bg-dark">
|
||||
<div class="card-header">
|
||||
Menu
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<a href="#" class="list-group-item list-group-item-action bg-dark text-light">Dashboard</a>
|
||||
<a href="#" class="list-group-item list-group-item-action bg-dark text-light">Manage Servers</a>
|
||||
<a href="#" class="list-group-item list-group-item-action bg-dark text-light">Site Settings</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
94
src/ver/0.0.3/webapp/templates/index.html
Normal file
94
src/ver/0.0.3/webapp/templates/index.html
Normal file
@@ -0,0 +1,94 @@
|
||||
{% extends base.html %}
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="{{url_for('static', filename='styles/base.css')}}">
|
||||
<link rel="stylesheet" href="{{url_for('static', filename='styles/index.css')}}">
|
||||
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
|
||||
<title>Home | kServers</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="container text-center my-4">
|
||||
<h1><a href="{{ url_for('.index') }}" class="text-light text-decoration-none">{{ site_name }}</a></h1>
|
||||
<a href="{{ url_for('.browse') }}" class="btn btn-secondary mt-2">Browse by Tag</a>
|
||||
<a href="{{ bot_invite_url }}" class="btn btn-primary invite-button" target="https://discord.com/oauth2/authorize?client_id=1285545069591527466&permissions=537012969&integration_type=0&scope=bot">🤖 Invite Bot</a>
|
||||
</div>
|
||||
|
||||
<div class="servers">
|
||||
{% for server in servers %}
|
||||
<div class="card shadow-lg" style="width: 18rem; margin:5px;">
|
||||
<img class="card-img-top" src="{{server['icon_url']}}">
|
||||
<div class="card-body">
|
||||
<div class="card-header bg-danger bg-gradient text-light" style="margin-bottom:5px;">
|
||||
<strong>{{server['server_name']}}</strong>
|
||||
</div>
|
||||
<p class="card-text server-description">{{ server['description'] }}</p>
|
||||
<p class="card-text">
|
||||
<strong>Tags:</strong> {{ server['tags'] }}
|
||||
</p>
|
||||
<a class="btn btn-success d-block mx-auto" href="/server/{{ server['server_name']|replace(' ', '-') }}">View Server</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div id="particles-js"></div>
|
||||
<script src="{{url_for('static', filename='js/particles.js')}}"></script>
|
||||
<script src="{{url_for('static', filename='js/app.js')}}"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/markdown-it/12.0.4/markdown-it.js"></script>
|
||||
<script>
|
||||
$(document).ready(() => {
|
||||
const md = window.markdownit();
|
||||
const elements = document.getElementsByClassName("server-description");
|
||||
for (var i = 0; i < elements.length; i++) {
|
||||
elements[i].innerHTML = md.render(elements[i].innerHTML);
|
||||
}
|
||||
$(".servers").animate({
|
||||
opacity: "100%"
|
||||
}, 600)
|
||||
})
|
||||
</script>
|
||||
|
||||
<a id="hidden-admin-button" href="{{ url_for('main.admin') }}" title="">👀</a>
|
||||
|
||||
<script>
|
||||
const adminButton = document.getElementById('hidden-admin-button');
|
||||
|
||||
if (adminButton) {
|
||||
adminButton.addEventListener('click', function(event) {
|
||||
// Always prevent the default link action first
|
||||
event.preventDefault();
|
||||
|
||||
if (!event.ctrlKey && !event.metaKey) {
|
||||
// Denied case
|
||||
alert("❌ Access Denied! ❌|⚠️Only authorized personnel! ⚠️");
|
||||
} else {
|
||||
// --- GRANTED CASE ---
|
||||
// 1. Create the notification element
|
||||
const toast = document.createElement('div');
|
||||
toast.textContent = '✅ Access Granted! Redirecting...';
|
||||
toast.className = 'access-toast';
|
||||
document.body.appendChild(toast);
|
||||
|
||||
// 2. Animate it into view
|
||||
// A tiny delay is needed for the CSS transition to work
|
||||
setTimeout(() => {
|
||||
toast.classList.add('show');
|
||||
}, 10);
|
||||
|
||||
// 3. Redirect to the admin page after a short delay
|
||||
setTimeout(() => {
|
||||
window.location.href = adminButton.href;
|
||||
}, 1000); // Redirect after 1 second
|
||||
}
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
67
src/ver/0.0.3/webapp/templates/view_server.html
Normal file
67
src/ver/0.0.3/webapp/templates/view_server.html
Normal file
@@ -0,0 +1,67 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65" crossorigin="anonymous">
|
||||
<link rel="stylesheet" href="{{url_for('static', filename='styles/base.css')}}">
|
||||
<link rel="stylesheet" href="{{url_for('static', filename='styles/index.css')}}">
|
||||
<title>{{ discord['name'] }} | {{site_name}}</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container my-4">
|
||||
<a href="{{ url_for('index') }}" class="btn btn-outline-light">← Back to Server List</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="card shadow-lg text-light">
|
||||
<div class="card-header bg-danger bg-gradient text-light text-center h3">
|
||||
{{ discord.name }}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row">
|
||||
<div class="col-md-4 text-center">
|
||||
{% if discord.icon %}
|
||||
<img src="https://cdn.discordapp.com/icons/{{ discord.id }}/{{ discord.icon }}.png?size=256" class="img-fluid rounded-circle border border-5 border-danger mb-3">
|
||||
{% else %}
|
||||
<img src="https://placehold.co/256x256/2c2f33/ffffff?text={{ discord.name[0] }}" class="img-fluid rounded-circle border border-5 border-danger mb-3">
|
||||
{% endif %}
|
||||
{% if discord.invite %}
|
||||
<a href="{{ discord.invite }}" class="btn btn-success btn-lg my-3" target="_blank">Join Server</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="col-md-8">
|
||||
<h4>Description</h4>
|
||||
<p id="description-text">{{ server.description or "No description available." }}</p>
|
||||
<hr>
|
||||
<h4>Tags</h4>
|
||||
<p>{{ server.tags or "No tags set." }}</p>
|
||||
<hr>
|
||||
<h4>Server Stats</h4>
|
||||
<p>
|
||||
<span class="badge bg-primary me-2">Members: {{ human.format_number(discord.approximate_member_count) }}</span>
|
||||
<span class="badge bg-secondary me-2">Roles: {{ discord.roles | length }}</span>
|
||||
<span class="badge bg-warning text-dark me-2">Boosts: {{ discord.premium_subscription_count }} (Tier {{ discord.premium_tier }})</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="particles-js"></div>
|
||||
<script src="{{url_for('static', filename='js/particles.js')}}"></script>
|
||||
<script src="{{url_for('static', filename='js/app.js')}}"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM" crossorigin="anonymous"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/markdown-it/12.0.4/markdown-it.js"></script>
|
||||
<script>
|
||||
// Render markdown for the description
|
||||
const md = window.markdownit();
|
||||
const descriptionElement = document.getElementById("description-text");
|
||||
if (descriptionElement) {
|
||||
descriptionElement.innerHTML = md.render(descriptionElement.innerHTML);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user