Updates to web page (add bootstrap), initial DB setup
This commit is contained in:
parent
a9a418f301
commit
fa8f24638e
|
|
@ -176,4 +176,4 @@ cython_debug/
|
||||||
|
|
||||||
# ---> StarfallBot
|
# ---> StarfallBot
|
||||||
config.toml
|
config.toml
|
||||||
web/static/style/*.scss
|
web/static/css/
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"basedpyright.analysis.diagnosticMode": "workspace",
|
||||||
|
"basedpyright.analysis.diagnosticSeverityOverrides": {
|
||||||
|
"reportAny": false,
|
||||||
|
"reportExplicitAny": false,
|
||||||
|
"reportMissingTypeStubs": false,
|
||||||
|
"reportUnknownMemberType": false,
|
||||||
|
"reportUnknownVariableType": false
|
||||||
|
},
|
||||||
|
"basedpyright.analysis.extraPaths": [
|
||||||
|
"starfallbot/.venv/lib/**/site-packages"
|
||||||
|
]
|
||||||
|
}
|
||||||
12
app.py
12
app.py
|
|
@ -11,7 +11,7 @@ from starfall.log import Log
|
||||||
from starfall.types import SnapshotQueue
|
from starfall.types import SnapshotQueue
|
||||||
from starfall.web import WebUI
|
from starfall.web import WebUI
|
||||||
|
|
||||||
CURRENT_VERSION = "0.1.0-alpha.1"
|
CURRENT_VERSION = "0.1.0-alpha.2"
|
||||||
|
|
||||||
|
|
||||||
@final
|
@final
|
||||||
|
|
@ -23,6 +23,7 @@ class Application:
|
||||||
threads: dict[str, Thread]
|
threads: dict[str, Thread]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
"""Initializes the application and prepares all threads for launch."""
|
||||||
self.log = Log()
|
self.log = Log()
|
||||||
|
|
||||||
logging.getLogger("app").debug(
|
logging.getLogger("app").debug(
|
||||||
|
|
@ -48,7 +49,11 @@ class Application:
|
||||||
|
|
||||||
self.app = WebUI()
|
self.app = WebUI()
|
||||||
|
|
||||||
def start(self):
|
def run(self):
|
||||||
|
"""
|
||||||
|
Starts all threads and monitors their active state.
|
||||||
|
This function runs indefinitely.
|
||||||
|
"""
|
||||||
self.threads["flask"] = Thread(
|
self.threads["flask"] = Thread(
|
||||||
target=self.app.run,
|
target=self.app.run,
|
||||||
args=(
|
args=(
|
||||||
|
|
@ -64,9 +69,10 @@ class Application:
|
||||||
self.monitor_queue()
|
self.monitor_queue()
|
||||||
|
|
||||||
def monitor_queue(self):
|
def monitor_queue(self):
|
||||||
|
"""Monitors and logs all incoming messages in `self.queue`."""
|
||||||
for entry in self.queue.get_all_for_receiver("app"):
|
for entry in self.queue.get_all_for_receiver("app"):
|
||||||
logging.getLogger("app").info("Received queue message: %r", entry)
|
logging.getLogger("app").info("Received queue message: %r", entry)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
Application().start()
|
Application().run()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,8 @@
|
||||||
discord-py-interactions
|
discord-py-interactions
|
||||||
flask
|
flask
|
||||||
|
flask_assets
|
||||||
|
libsass
|
||||||
|
livereload
|
||||||
flask-sqlalchemy
|
flask-sqlalchemy
|
||||||
rich
|
rich
|
||||||
twitchio
|
twitchio
|
||||||
|
|
@ -39,7 +39,7 @@ class Config:
|
||||||
error,
|
error,
|
||||||
)
|
)
|
||||||
|
|
||||||
def get(self, key: str, default: Any = None):
|
def get(self, key: str, default: Any = None) -> Any:
|
||||||
"""Recursively calls `get(key, default)` to search nested dictionary.
|
"""Recursively calls `get(key, default)` to search nested dictionary.
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,30 @@
|
||||||
from flask_sqlalchemy import SQLAlchemy
|
import importlib
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
from inspect import isclass
|
||||||
|
from pkgutil import iter_modules
|
||||||
|
from typing import final
|
||||||
|
|
||||||
db = SQLAlchemy()
|
from flask_sqlalchemy import SQLAlchemy
|
||||||
|
from sqlalchemy.orm import DeclarativeBase
|
||||||
|
|
||||||
|
|
||||||
|
@final
|
||||||
|
class BaseModel(DeclarativeBase):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
db = SQLAlchemy(model_class=BaseModel)
|
||||||
|
|
||||||
|
|
||||||
|
def load_schema():
|
||||||
|
path = os.path.realpath(os.path.dirname(__file__) + os.sep + "schema")
|
||||||
|
for _, module_name, _ in iter_modules([path], f"{__name__}.schema."):
|
||||||
|
logging.getLogger("db").debug("Parsing module: %s" % module_name)
|
||||||
|
module = importlib.import_module(module_name)
|
||||||
|
|
||||||
|
for attribute_name in dir(module):
|
||||||
|
attribute = getattr(module, attribute_name)
|
||||||
|
|
||||||
|
if isclass(attribute) and issubclass(attribute, db.Table):
|
||||||
|
globals()[attribute_name] = attribute
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
from sqlalchemy.orm import Mapped, mapped_column
|
||||||
|
|
||||||
|
from starfall.db import db
|
||||||
|
|
||||||
|
|
||||||
|
class Users(db.Table):
|
||||||
|
id: Mapped[int] = mapped_column(primary_key=True)
|
||||||
|
email: Mapped[str] = mapped_column(unique=True)
|
||||||
|
username: Mapped[str] = mapped_column()
|
||||||
|
password: Mapped[str] = mapped_column()
|
||||||
|
|
@ -6,7 +6,10 @@ from rich.logging import RichHandler
|
||||||
|
|
||||||
class Log:
|
class Log:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
handler = RichHandler(level=logging.DEBUG, rich_tracebacks=True)
|
handler: logging.Handler = RichHandler(
|
||||||
|
level=logging.DEBUG, rich_tracebacks=True
|
||||||
|
)
|
||||||
|
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=logging.DEBUG,
|
level=logging.DEBUG,
|
||||||
format="%(message)s [%(name)s]",
|
format="%(message)s [%(name)s]",
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,15 @@ import importlib
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
from inspect import isclass
|
from inspect import isclass
|
||||||
from pathlib import Path
|
|
||||||
from pkgutil import iter_modules
|
from pkgutil import iter_modules
|
||||||
from typing import final
|
from typing import final
|
||||||
|
|
||||||
from flask import Blueprint, Flask
|
from flask import Blueprint, Flask
|
||||||
|
from flask_assets import Bundle, Environment
|
||||||
|
from livereload import Server
|
||||||
|
|
||||||
from starfall.config import Config
|
from starfall.config import Config
|
||||||
from starfall.db import db
|
from starfall.db import db, load_schema
|
||||||
from starfall.types import SnapshotQueue
|
from starfall.types import SnapshotQueue
|
||||||
from starfall.web.blueprints.base import BaseBlueprint
|
from starfall.web.blueprints.base import BaseBlueprint
|
||||||
|
|
||||||
|
|
@ -21,6 +22,8 @@ class WebUI:
|
||||||
self.queue: SnapshotQueue | None = None
|
self.queue: SnapshotQueue | None = None
|
||||||
self.app: Flask | None = None
|
self.app: Flask | None = None
|
||||||
self.blueprint: Blueprint = Blueprint("main", __name__)
|
self.blueprint: Blueprint = Blueprint("main", __name__)
|
||||||
|
self.assets: Environment | None = None
|
||||||
|
self.server: Server | None = None
|
||||||
|
|
||||||
def run(self, config: Config, queue: SnapshotQueue):
|
def run(self, config: Config, queue: SnapshotQueue):
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
@ -28,20 +31,58 @@ class WebUI:
|
||||||
|
|
||||||
logging.getLogger("web").debug("Hello from %r", type(self))
|
logging.getLogger("web").debug("Hello from %r", type(self))
|
||||||
|
|
||||||
self.app = Flask(__name__)
|
self.app = Flask(
|
||||||
|
import_name=__name__,
|
||||||
|
root_path=os.path.realpath("."),
|
||||||
|
static_folder=os.path.realpath("./web/static"),
|
||||||
|
template_folder="web/templates",
|
||||||
|
)
|
||||||
|
|
||||||
self.app.config.update(
|
self.app.config.update(
|
||||||
SECRET_KEY=str(self.config.get("web.secret_key")),
|
SECRET_KEY=str(self.config.get("web.secret_key")),
|
||||||
SQLALCHEMY_DATABASE_URI=str(self.config.get("web.database_url")),
|
SQLALCHEMY_DATABASE_URI=str(self.config.get("web.database_url")),
|
||||||
|
TEMPLATES_AUTO_RELOAD=True,
|
||||||
)
|
)
|
||||||
|
self.app.jinja_env.auto_reload = True
|
||||||
|
|
||||||
self.app.root_path = os.path.realpath(".")
|
self.assets = Environment(self.app)
|
||||||
self.app.static_folder = os.path.realpath("./web/static")
|
self.assets.load_path = ["web/static/scss", "web/static/css"]
|
||||||
self.app.template_folder = "web"
|
scss = Bundle(
|
||||||
|
"main.scss",
|
||||||
|
"fonts.scss",
|
||||||
|
"bootstrap.min.scss",
|
||||||
|
filters="libsass",
|
||||||
|
output="css/main.css",
|
||||||
|
depends=["**/*.scss"],
|
||||||
|
)
|
||||||
|
_ = self.assets.register("scss", scss)
|
||||||
|
|
||||||
db.init_app(self.app)
|
db.init_app(self.app)
|
||||||
|
load_schema()
|
||||||
|
|
||||||
|
with self.app.app_context():
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
self.import_blueprints()
|
self.import_blueprints()
|
||||||
self.app.register_blueprint(self.blueprint, options={"queue": self.queue})
|
self.app.register_blueprint(
|
||||||
self.app.run(host=self.config.get("web.host"), port=self.config.get("web.port"))
|
self.blueprint,
|
||||||
|
options={"queue": self.queue},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.server = Server(self.app.wsgi_app)
|
||||||
|
self.server.watch(
|
||||||
|
filepath=os.path.join(str(self.app.template_folder), "**/*.html"),
|
||||||
|
)
|
||||||
|
self.server.watch(
|
||||||
|
filepath=os.path.join(str(self.app.static_folder), "**/*.js"),
|
||||||
|
)
|
||||||
|
self.server.watch(
|
||||||
|
filepath=os.path.join(str(self.app.static_folder), "**/*.scss"),
|
||||||
|
)
|
||||||
|
self.server.serve(
|
||||||
|
host=self.config.get("web.host"),
|
||||||
|
port=self.config.get("web.port"),
|
||||||
|
)
|
||||||
|
|
||||||
def import_blueprints(self):
|
def import_blueprints(self):
|
||||||
path = os.path.realpath(os.path.dirname(__file__) + os.sep + "blueprints")
|
path = os.path.realpath(os.path.dirname(__file__) + os.sep + "blueprints")
|
||||||
|
|
@ -49,6 +90,7 @@ class WebUI:
|
||||||
if module_name.endswith("base"):
|
if module_name.endswith("base"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
logging.getLogger("web").debug("Parsing module: %s" % module_name)
|
||||||
module = importlib.import_module(module_name)
|
module = importlib.import_module(module_name)
|
||||||
|
|
||||||
for attribute_name in dir(module):
|
for attribute_name in dir(module):
|
||||||
|
|
@ -56,4 +98,4 @@ class WebUI:
|
||||||
|
|
||||||
if isclass(attribute) and issubclass(attribute, BaseBlueprint):
|
if isclass(attribute) and issubclass(attribute, BaseBlueprint):
|
||||||
globals()[attribute_name] = attribute
|
globals()[attribute_name] = attribute
|
||||||
globals()[attribute_name](self.blueprint)
|
globals()[attribute_name](self.blueprint, self.assets)
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,42 @@
|
||||||
import inspect
|
import inspect
|
||||||
import logging
|
import logging
|
||||||
from abc import ABC
|
from abc import ABC
|
||||||
|
from types import FrameType
|
||||||
|
|
||||||
from flask import Blueprint
|
from flask import Blueprint
|
||||||
from flask.blueprints import BlueprintSetupState
|
from flask.blueprints import BlueprintSetupState
|
||||||
|
from flask_assets import Environment
|
||||||
|
|
||||||
from starfall.types import SnapshotQueue
|
from starfall.types import SnapshotQueue
|
||||||
|
|
||||||
|
|
||||||
class BaseBlueprint(ABC):
|
class BaseBlueprint(ABC):
|
||||||
def __init__(self, blueprint: Blueprint):
|
def __init__(self, blueprint: Blueprint, assets: Environment):
|
||||||
|
self.queue: SnapshotQueue | None = None
|
||||||
|
self.assets: Environment = assets
|
||||||
blueprint.record(self.on_blueprint_setup)
|
blueprint.record(self.on_blueprint_setup)
|
||||||
|
|
||||||
if type(self) != BaseBlueprint: # noqa: E721
|
if type(self) is not BaseBlueprint:
|
||||||
self._debug("Attaching blueprint of type %r to web service", type(self))
|
self._debug("Attaching blueprint of type %r to web service", type(self))
|
||||||
|
|
||||||
def on_blueprint_setup(self, state: BlueprintSetupState):
|
def on_blueprint_setup(self, state: BlueprintSetupState):
|
||||||
self.queue: SnapshotQueue = state.options["options"]["queue"]
|
self.queue = state.options["options"]["queue"]
|
||||||
|
|
||||||
if type(self) != BaseBlueprint: # noqa: E721
|
if type(self) is not BaseBlueprint:
|
||||||
self._info("Blueprint of type %r successfully set up", type(self))
|
self._info("Blueprint of type %r successfully set up", type(self))
|
||||||
|
|
||||||
def _log_access(self):
|
def _log_access(self):
|
||||||
# There is always a previous frame - This function is called when a
|
# There is always a previous frame - This function is called when a
|
||||||
# route is accessed by a user through a method unknown to this one.
|
# route is accessed by a user through a method unknown to this one.
|
||||||
self._info("Route access: %r", inspect.currentframe().f_back.f_code.co_name)
|
frame: FrameType | None = inspect.currentframe()
|
||||||
|
if not isinstance(frame, FrameType):
|
||||||
|
return
|
||||||
|
|
||||||
|
f_back: FrameType | None = frame.f_back
|
||||||
|
if not isinstance(f_back, FrameType):
|
||||||
|
return
|
||||||
|
|
||||||
|
self._info("Route access: %r", f_back.f_code.co_name)
|
||||||
|
|
||||||
def _debug(self, msg: str, *args: object):
|
def _debug(self, msg: str, *args: object):
|
||||||
logging.getLogger("web").debug(msg, *args)
|
logging.getLogger("web").debug(msg, *args)
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,14 @@
|
||||||
from flask import Blueprint, render_template
|
from flask import Blueprint, render_template
|
||||||
|
from flask_assets import Environment
|
||||||
|
|
||||||
from starfall.web.blueprints.base import BaseBlueprint
|
from starfall.web.blueprints.base import BaseBlueprint
|
||||||
|
|
||||||
|
|
||||||
class HomeBlueprint(BaseBlueprint):
|
class HomeBlueprint(BaseBlueprint):
|
||||||
def __init__(self, blueprint: Blueprint) -> None:
|
def __init__(self, blueprint: Blueprint, assets: Environment) -> None:
|
||||||
super().__init__(blueprint)
|
super().__init__(blueprint, assets)
|
||||||
blueprint.add_url_rule("/", view_func=self.index)
|
blueprint.add_url_rule("/", view_func=self.index)
|
||||||
|
|
||||||
def index(self):
|
def index(self):
|
||||||
self._log_access()
|
self._log_access()
|
||||||
return render_template("home.html")
|
return render_template("home.html", assets=self.assets, route="/")
|
||||||
|
|
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
{% extends "index.html" %}
|
|
||||||
|
|
||||||
{% block titleSuffix %}Overview{% endblock %}
|
|
||||||
|
|
||||||
{% block body %}
|
|
||||||
|
|
||||||
<div class="flex center">
|
|
||||||
<h1>StarfallBot - Overview</h1>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
|
|
@ -1,61 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<title>StarfallBot - {% block titleSuffix %}Unassigned{% endblock %}</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
|
||||||
<meta name="description" content="StarfallBot administation interface" />
|
|
||||||
|
|
||||||
<link rel="preload" href="{{ url_for('static', filename='img/logo.png') }}" as="image" />
|
|
||||||
<link rel="preload" href="{{ url_for('static', filename='style/fonts.css') }}" as="style" />
|
|
||||||
<link rel="preload" href="{{ url_for('static', filename='style/main.css') }}" as="style" />
|
|
||||||
|
|
||||||
<link rel="icon" href="{{ url_for('static', filename='favicon.png') }}" />
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='style/fonts.css') }}" />
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='style/main.css') }}" />
|
|
||||||
|
|
||||||
{% block head %}
|
|
||||||
{% endblock %}
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
{% block beforeMain %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
<header>
|
|
||||||
<div class="container">
|
|
||||||
<nav>
|
|
||||||
{% block beforeNav %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
<a href="/">Home</a>
|
|
||||||
|
|
||||||
{% block afterNav %}
|
|
||||||
{% endblock %}
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main>
|
|
||||||
{% block beforeMainContainer %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
{% block body %}
|
|
||||||
|
|
||||||
<p>No content has been defined for this page.</p>
|
|
||||||
|
|
||||||
{% endblock %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% block afterMainContainer %}
|
|
||||||
{% endblock %}
|
|
||||||
</main>
|
|
||||||
|
|
||||||
{% block afterMain %}
|
|
||||||
{% endblock %}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
</html>
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,80 @@
|
||||||
|
/*!
|
||||||
|
* Color mode toggler for Bootstrap's docs (https://getbootstrap.com/)
|
||||||
|
* Copyright 2011-2025 The Bootstrap Authors
|
||||||
|
* Licensed under the Creative Commons Attribution 3.0 Unported License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
(() => {
|
||||||
|
'use strict'
|
||||||
|
|
||||||
|
const getStoredTheme = () => localStorage.getItem('theme')
|
||||||
|
const setStoredTheme = theme => localStorage.setItem('theme', theme)
|
||||||
|
|
||||||
|
const getPreferredTheme = () => {
|
||||||
|
const storedTheme = getStoredTheme()
|
||||||
|
if (storedTheme) {
|
||||||
|
return storedTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
|
||||||
|
}
|
||||||
|
|
||||||
|
const setTheme = theme => {
|
||||||
|
if (theme === 'auto') {
|
||||||
|
document.documentElement.setAttribute('data-bs-theme', (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'))
|
||||||
|
} else {
|
||||||
|
document.documentElement.setAttribute('data-bs-theme', theme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTheme(getPreferredTheme())
|
||||||
|
|
||||||
|
const showActiveTheme = (theme, focus = false) => {
|
||||||
|
const themeSwitcher = document.querySelector('#bd-theme')
|
||||||
|
|
||||||
|
if (!themeSwitcher) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const themeSwitcherText = document.querySelector('#bd-theme-text')
|
||||||
|
const activeThemeIcon = document.querySelector('.theme-icon-active use')
|
||||||
|
const btnToActive = document.querySelector(`[data-bs-theme-value="${theme}"]`)
|
||||||
|
const svgOfActiveBtn = btnToActive.querySelector('svg use').getAttribute('href')
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-bs-theme-value]').forEach(element => {
|
||||||
|
element.classList.remove('active')
|
||||||
|
element.setAttribute('aria-pressed', 'false')
|
||||||
|
})
|
||||||
|
|
||||||
|
btnToActive.classList.add('active')
|
||||||
|
btnToActive.setAttribute('aria-pressed', 'true')
|
||||||
|
activeThemeIcon.setAttribute('href', svgOfActiveBtn)
|
||||||
|
const themeSwitcherLabel = `${themeSwitcherText.textContent} (${btnToActive.dataset.bsThemeValue})`
|
||||||
|
themeSwitcher.setAttribute('aria-label', themeSwitcherLabel)
|
||||||
|
|
||||||
|
if (focus) {
|
||||||
|
themeSwitcher.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
|
||||||
|
const storedTheme = getStoredTheme()
|
||||||
|
if (storedTheme !== 'light' && storedTheme !== 'dark') {
|
||||||
|
setTheme(getPreferredTheme())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
window.addEventListener('DOMContentLoaded', () => {
|
||||||
|
showActiveTheme(getPreferredTheme())
|
||||||
|
|
||||||
|
document.querySelectorAll('[data-bs-theme-value]')
|
||||||
|
.forEach(toggle => {
|
||||||
|
toggle.addEventListener('click', () => {
|
||||||
|
const theme = toggle.getAttribute('data-bs-theme-value')
|
||||||
|
setStoredTheme(theme)
|
||||||
|
setTheme(theme)
|
||||||
|
showActiveTheme(theme, true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})()
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 42 KiB |
File diff suppressed because it is too large
Load Diff
|
|
@ -0,0 +1,43 @@
|
||||||
|
/* exo-2-regular - latin */
|
||||||
|
@font-face {
|
||||||
|
font-display: swap;
|
||||||
|
/* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||||
|
font-family: 'Exo 2';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url('../fonts/exo-2-v26-latin-regular.woff2') format('woff2');
|
||||||
|
/* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* exo-2-700 - latin */
|
||||||
|
@font-face {
|
||||||
|
font-display: swap;
|
||||||
|
/* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||||
|
font-family: 'Exo 2';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: url('../fonts/exo-2-v26-latin-700.woff2') format('woff2');
|
||||||
|
/* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* overpass-regular - latin */
|
||||||
|
@font-face {
|
||||||
|
font-display: swap;
|
||||||
|
/* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||||
|
font-family: 'Overpass';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url('../fonts/overpass-v19-latin-regular.woff2') format('woff2');
|
||||||
|
/* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* overpass-700 - latin */
|
||||||
|
@font-face {
|
||||||
|
font-display: swap;
|
||||||
|
/* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
||||||
|
font-family: 'Overpass';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: url('../fonts/overpass-v19-latin-700.woff2') format('woff2');
|
||||||
|
/* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
$siteHeadingFontFamily: 'Exo 2', sans-serif;
|
||||||
|
$siteFontFamily: 'Overpass', sans-serif;
|
||||||
|
$siteFontSize: 18px;
|
||||||
|
|
||||||
|
$mainBorderRadius: 16px;
|
||||||
|
|
||||||
|
$contentWidth: 1400px;
|
||||||
|
$contentPadding: 8px;
|
||||||
|
|
||||||
|
$fontSize: (
|
||||||
|
h1: 4rem,
|
||||||
|
h2: 2rem,
|
||||||
|
h3: 1.5rem,
|
||||||
|
h4: 1rem,
|
||||||
|
h5: 0.875rem,
|
||||||
|
h6: 0.75rem
|
||||||
|
);
|
||||||
|
|
||||||
|
[data-bs-theme="light"] {
|
||||||
|
--siteBackground: linear-gradient(to bottom right, rgb(136, 200, 255), rgb(135, 165, 255));
|
||||||
|
--siteTextColor: #000;
|
||||||
|
--mainBackground: #001030;
|
||||||
|
--mainTextColor: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-bs-theme="dark"] {
|
||||||
|
--siteBackground: linear-gradient(to bottom right, rgb(70, 81, 110), rgb(16, 20, 60));
|
||||||
|
--siteTextColor: #000;
|
||||||
|
--mainBackground: #001030;
|
||||||
|
--mainTextColor: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-brand img {
|
||||||
|
max-width: 40px;
|
||||||
|
transform: rotate(340deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
position: relative;
|
||||||
|
min-height: 100%;
|
||||||
|
color: var(--siteTextColor);
|
||||||
|
|
||||||
|
background: var(--siteBackground);
|
||||||
|
|
||||||
|
background: {
|
||||||
|
repeat: no-repeat;
|
||||||
|
attachment: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
font: {
|
||||||
|
family: $siteFontFamily;
|
||||||
|
size: $siteFontSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// main {
|
||||||
|
// height: 100%;
|
||||||
|
// display: flex;
|
||||||
|
// justify-content: center;
|
||||||
|
|
||||||
|
// &>.container {
|
||||||
|
// width: 100%;
|
||||||
|
// height: 100%;
|
||||||
|
// max-width: $contentWidth;
|
||||||
|
// padding: $contentPadding;
|
||||||
|
|
||||||
|
// background: $mainBackground;
|
||||||
|
// border-radius: $mainBorderRadius;
|
||||||
|
// color: $mainTextColor;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// .flex {
|
||||||
|
// display: flex;
|
||||||
|
// flex-flow: row wrap;
|
||||||
|
|
||||||
|
// &.center {
|
||||||
|
// justify-content: center;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
@each $tag, $size in $fontSize {
|
||||||
|
#{$tag} {
|
||||||
|
font: {
|
||||||
|
family: $siteHeadingFontFamily;
|
||||||
|
size: $size;
|
||||||
|
weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
/* exo-2-regular - latin */
|
|
||||||
@font-face {
|
|
||||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
|
||||||
font-family: 'Exo 2';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
src: url('../fonts/exo-2-v26-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* exo-2-700 - latin */
|
|
||||||
@font-face {
|
|
||||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
|
||||||
font-family: 'Exo 2';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
src: url('../fonts/exo-2-v26-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* overpass-regular - latin */
|
|
||||||
@font-face {
|
|
||||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
|
||||||
font-family: 'Overpass';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 400;
|
|
||||||
src: url('../fonts/overpass-v19-latin-regular.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* overpass-700 - latin */
|
|
||||||
@font-face {
|
|
||||||
font-display: swap; /* Check https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display for other options. */
|
|
||||||
font-family: 'Overpass';
|
|
||||||
font-style: normal;
|
|
||||||
font-weight: 700;
|
|
||||||
src: url('../fonts/overpass-v19-latin-700.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
body{position:relative;min-height:100%;color:#000;background-image:linear-gradient(to bottom, #205080, #4070a0);background-repeat:no-repeat;background-attachment:fixed;font-family:"Overpass",sans-serif;font-size:18px}main{height:100%;display:flex;justify-content:center}main>.container{width:100%;height:100%;max-width:1400px;padding:8px;background:#001030;border-radius:16px;color:#fff}main .flex{display:flex;flex-flow:row wrap}main .flex.center{justify-content:center}h1{font-family:"Exo 2",sans-serif;font-size:4rem;font-weight:bold;padding:0;margin:0}h2{font-family:"Exo 2",sans-serif;font-size:2rem;font-weight:bold;padding:0;margin:0}h3{font-family:"Exo 2",sans-serif;font-size:1.5rem;font-weight:bold;padding:0;margin:0}h4{font-family:"Exo 2",sans-serif;font-size:1rem;font-weight:bold;padding:0;margin:0}h5{font-family:"Exo 2",sans-serif;font-size:.875rem;font-weight:bold;padding:0;margin:0}h6{font-family:"Exo 2",sans-serif;font-size:.75rem;font-weight:bold;padding:0;margin:0}
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link {% if route == '/' %}active{% endif %}" href="/">Home</a>
|
||||||
|
</li>
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
{% extends 'index.html' %}
|
||||||
|
|
||||||
|
{% block navLinks %}
|
||||||
|
{% include 'components/menu_public.html' %}
|
||||||
|
{% endblock %}
|
||||||
|
|
@ -0,0 +1,87 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html data-bs-theme="dark">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<title>StarfallBot - {% block titleSuffix %}Unassigned{% endblock %}</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
|
||||||
|
<meta name="description" content="StarfallBot administation interface" />
|
||||||
|
|
||||||
|
<link rel="icon" href="{{ url_for('static', filename='favicon.png') }}" />
|
||||||
|
<link rel="stylesheet" type="text/css" href="{{ assets['scss'].urls()|first }}" />
|
||||||
|
|
||||||
|
{% block head %}{% endblock %}
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<a class="visually-hidden-focusable" href="#content">Skip to main content</a>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<nav class="navbar navbar-expand-md">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<a class="navbar-brand" href="/">
|
||||||
|
<img src="{{ url_for('static', filename='logo.png') }}" alt="Starfall" />
|
||||||
|
</a>
|
||||||
|
<button class="navbar-toggler" type="button" data-bs-toggle="collapse"
|
||||||
|
data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent"
|
||||||
|
aria-expanded="false" aria-label="Toggle navigation">
|
||||||
|
<span class="navbar-toggler-icon"></span>
|
||||||
|
</button>
|
||||||
|
<div class="collapse navbar-collapse" id="navbarSupportedContent">
|
||||||
|
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
||||||
|
{% block navLinks %}{% endblock %}
|
||||||
|
<li class="nav-menu dropdown">
|
||||||
|
<button id="theme" class="btn btn-link nav-link dropdown-toggle" type="button"
|
||||||
|
data-bs-toggle="dropdown" aria-expanded="false">Theme</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<button class="dropdown-item" type="button" data-bs-theme-value="light">
|
||||||
|
Light
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item" type="button" data-bs-theme-value="dark">
|
||||||
|
Dark
|
||||||
|
</button>
|
||||||
|
<button class="dropdown-item" type="button" data-bs-theme-value="auto">
|
||||||
|
Auto
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{% block beforeMain %}{% endblock %}
|
||||||
|
<main>
|
||||||
|
{% block beforeMainContainer %}{% endblock %}
|
||||||
|
|
||||||
|
<div class="container" id="content">
|
||||||
|
{% block body %}
|
||||||
|
{# If there's no body on this page, treat it as a 404. #}
|
||||||
|
<div class="d-flex flex-column justify-content-center align-items-center">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-danger-emphasis m-2">404</h1>
|
||||||
|
</div>
|
||||||
|
<div class="mb-4">
|
||||||
|
<h2 class="flex-col text-danger-emphasis">That's an error.</h2>
|
||||||
|
</div>
|
||||||
|
<div class="flex-row">
|
||||||
|
<p>Want to go back to the <a href="/">start</a> and try again?</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% block afterMainContainer %}{% endblock %}
|
||||||
|
</main>
|
||||||
|
{% block afterMain %}{% endblock %}
|
||||||
|
|
||||||
|
<script type="text/javascript" src="{{ url_for('static', filename='js/bootstrap.bundle.min.js') }}"></script>
|
||||||
|
<script type="text/javascript" src="{{ url_for('static', filename='js/color-switcher.js') }}"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Loading…
Reference in New Issue