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
|
||||
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.web import WebUI
|
||||
|
||||
CURRENT_VERSION = "0.1.0-alpha.1"
|
||||
CURRENT_VERSION = "0.1.0-alpha.2"
|
||||
|
||||
|
||||
@final
|
||||
|
|
@ -23,6 +23,7 @@ class Application:
|
|||
threads: dict[str, Thread]
|
||||
|
||||
def __init__(self):
|
||||
"""Initializes the application and prepares all threads for launch."""
|
||||
self.log = Log()
|
||||
|
||||
logging.getLogger("app").debug(
|
||||
|
|
@ -48,7 +49,11 @@ class Application:
|
|||
|
||||
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(
|
||||
target=self.app.run,
|
||||
args=(
|
||||
|
|
@ -64,9 +69,10 @@ class Application:
|
|||
self.monitor_queue()
|
||||
|
||||
def monitor_queue(self):
|
||||
"""Monitors and logs all incoming messages in `self.queue`."""
|
||||
for entry in self.queue.get_all_for_receiver("app"):
|
||||
logging.getLogger("app").info("Received queue message: %r", entry)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
Application().start()
|
||||
Application().run()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
discord-py-interactions
|
||||
flask
|
||||
flask_assets
|
||||
libsass
|
||||
livereload
|
||||
flask-sqlalchemy
|
||||
rich
|
||||
twitchio
|
||||
|
|
@ -39,7 +39,7 @@ class Config:
|
|||
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.
|
||||
|
||||
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:
|
||||
def __init__(self) -> None:
|
||||
handler = RichHandler(level=logging.DEBUG, rich_tracebacks=True)
|
||||
handler: logging.Handler = RichHandler(
|
||||
level=logging.DEBUG, rich_tracebacks=True
|
||||
)
|
||||
|
||||
logging.basicConfig(
|
||||
level=logging.DEBUG,
|
||||
format="%(message)s [%(name)s]",
|
||||
|
|
|
|||
|
|
@ -2,14 +2,15 @@ import importlib
|
|||
import logging
|
||||
import os
|
||||
from inspect import isclass
|
||||
from pathlib import Path
|
||||
from pkgutil import iter_modules
|
||||
from typing import final
|
||||
|
||||
from flask import Blueprint, Flask
|
||||
from flask_assets import Bundle, Environment
|
||||
from livereload import Server
|
||||
|
||||
from starfall.config import Config
|
||||
from starfall.db import db
|
||||
from starfall.db import db, load_schema
|
||||
from starfall.types import SnapshotQueue
|
||||
from starfall.web.blueprints.base import BaseBlueprint
|
||||
|
||||
|
|
@ -21,6 +22,8 @@ class WebUI:
|
|||
self.queue: SnapshotQueue | None = None
|
||||
self.app: Flask | None = None
|
||||
self.blueprint: Blueprint = Blueprint("main", __name__)
|
||||
self.assets: Environment | None = None
|
||||
self.server: Server | None = None
|
||||
|
||||
def run(self, config: Config, queue: SnapshotQueue):
|
||||
self.config = config
|
||||
|
|
@ -28,20 +31,58 @@ class WebUI:
|
|||
|
||||
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(
|
||||
SECRET_KEY=str(self.config.get("web.secret_key")),
|
||||
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.app.static_folder = os.path.realpath("./web/static")
|
||||
self.app.template_folder = "web"
|
||||
self.assets = Environment(self.app)
|
||||
self.assets.load_path = ["web/static/scss", "web/static/css"]
|
||||
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)
|
||||
load_schema()
|
||||
|
||||
with self.app.app_context():
|
||||
db.create_all()
|
||||
|
||||
self.import_blueprints()
|
||||
self.app.register_blueprint(self.blueprint, options={"queue": self.queue})
|
||||
self.app.run(host=self.config.get("web.host"), port=self.config.get("web.port"))
|
||||
self.app.register_blueprint(
|
||||
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):
|
||||
path = os.path.realpath(os.path.dirname(__file__) + os.sep + "blueprints")
|
||||
|
|
@ -49,6 +90,7 @@ class WebUI:
|
|||
if module_name.endswith("base"):
|
||||
continue
|
||||
|
||||
logging.getLogger("web").debug("Parsing module: %s" % module_name)
|
||||
module = importlib.import_module(module_name)
|
||||
|
||||
for attribute_name in dir(module):
|
||||
|
|
@ -56,4 +98,4 @@ class WebUI:
|
|||
|
||||
if isclass(attribute) and issubclass(attribute, BaseBlueprint):
|
||||
globals()[attribute_name] = attribute
|
||||
globals()[attribute_name](self.blueprint)
|
||||
globals()[attribute_name](self.blueprint, self.assets)
|
||||
|
|
|
|||
|
|
@ -1,30 +1,42 @@
|
|||
import inspect
|
||||
import logging
|
||||
from abc import ABC
|
||||
from types import FrameType
|
||||
|
||||
from flask import Blueprint
|
||||
from flask.blueprints import BlueprintSetupState
|
||||
from flask_assets import Environment
|
||||
|
||||
from starfall.types import SnapshotQueue
|
||||
|
||||
|
||||
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)
|
||||
|
||||
if type(self) != BaseBlueprint: # noqa: E721
|
||||
if type(self) is not BaseBlueprint:
|
||||
self._debug("Attaching blueprint of type %r to web service", type(self))
|
||||
|
||||
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))
|
||||
|
||||
def _log_access(self):
|
||||
# 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.
|
||||
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):
|
||||
logging.getLogger("web").debug(msg, *args)
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
from flask import Blueprint, render_template
|
||||
from flask_assets import Environment
|
||||
|
||||
from starfall.web.blueprints.base import BaseBlueprint
|
||||
|
||||
|
||||
class HomeBlueprint(BaseBlueprint):
|
||||
def __init__(self, blueprint: Blueprint) -> None:
|
||||
super().__init__(blueprint)
|
||||
def __init__(self, blueprint: Blueprint, assets: Environment) -> None:
|
||||
super().__init__(blueprint, assets)
|
||||
blueprint.add_url_rule("/", view_func=self.index)
|
||||
|
||||
def index(self):
|
||||
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