web ui checkpoint

This commit is contained in:
evilchili 2025-10-12 15:36:38 -07:00
parent 8eb505f09e
commit c1272788f0
8 changed files with 333 additions and 87 deletions

View File

@ -96,15 +96,16 @@ VIEW_URI=/
schema, self.path.database, sort_keys=True, indent=4, separators=(",", ": ") schema, self.path.database, sort_keys=True, indent=4, separators=(",", ": ")
) )
self.theme = Path(__file__).parent / "themes" / "default" self.theme = Path(__file__).parent / "themes" / self.config.THEME
self.web = Flask(self.config.NAME, template_folder=self.theme) self.web = Flask(self.config.NAME, template_folder=self.theme, static_folder=self.theme / 'static')
self.web.config["SECRET_KEY"] = self.config.SECRET_KEY self.web.config["SECRET_KEY"] = self.config.SECRET_KEY
self.web.config["SEND_FILE_MAX_AGE_DEFAULT"] = 0 self.web.config["SEND_FILE_MAX_AGE_DEFAULT"] = 0
self.web.config["DEBUG"] = True self.web.config["DEBUG"] = True
self.web.config["SESSION_TYPE"] = "filesystem" self.web.config["SESSION_TYPE"] = "filesystem"
self.web.config["SESSION_REFRESH_EACH_REQUEST"] = True self.web.config["SESSION_REFRESH_EACH_REQUEST"] = True
self.web.config["SESSION_FILE_DIR"] = self.path.sessions self.web.config["SESSION_FILE_DIR"] = self.path.sessions
Session(self.web) Session(self.web)
self._initialized = True self._initialized = True

71
src/ttfrog/bootstrap.py Normal file
View File

@ -0,0 +1,71 @@
from ttfrog import app, schema
TEMPLATE = """
# Heading 1
## Heading 2
### Heading 3
#### Heading 4
##### Heading 5
###### Heading 6
***
Normal text.
**Bold text.**
*Italic Text.*
[A link](/).
1. a
2. numbered
3. list.
> a block quote
| A | Table | Section |
| --- | ----- | ------- |
| foo | bar | baz |
"""
def bootstrap():
"""
Bootstrap the database entries by populating the first Page, the Admin user and the Admins group.
"""
app.check_state()
# create the top-level pages
root = app.db.save(schema.Page(name=app.config.VIEW_URI, title="Home", body="This is the home page"))
users = root.add_member(schema.Page(name="User", title="Users", body="users go here."))
groups = root.add_member(schema.Page(name="Group", title="Groups", body="groups go here."))
npcs = root.add_member(schema.Page(name="NPC", title="NPCS!", body="NPCS!"))
wiki = root.add_member(schema.Page(name="Wiki", title="Wiki", body=TEMPLATE))
# create the NPCs
npcs.add_member(schema.NPC(name="Sabetha", body=""))
npcs.add_member(schema.NPC(name="John", body=""))
# create the users
guest = users.add_member(schema.User(name="guest"))
admin = users.add_member(
schema.User(name=app.config.ADMIN_USERNAME, password="fnord", email=app.config.ADMIN_EMAIL)
)
# create the admin user and admins group
admins = groups.add_member(schema.Group(name="administrators", members=[admin]))
# admins get full access
root.set_permissions(
admins, permissions=[schema.Permissions.READ, schema.Permissions.WRITE, schema.Permissions.DELETE]
)
# guests get read access by default, except on Groups and Users
groups.set_permissions(guest, permissions=[])
users.set_permissions(guest, permissions=[])
root.set_permissions(guest, permissions=[schema.Permissions.READ])

View File

@ -13,11 +13,12 @@
<input name='title' id="data_form__title" type='text' value="{{ page.title }}"> <input name='title' id="data_form__title" type='text' value="{{ page.title }}">
<textarea name="body" id="data_form__body">{{ page.body }}</textarea> <textarea name="body" id="data_form__body">{{ page.body }}</textarea>
</form> </form>
<main class='page'>
{% block nav %} {% block nav %}
{% include "nav.html" %} {% include "nav.html" %}
{% endblock %} {% endblock %}
<div class='table-wrapper'>
<main>
{% for message in g.messages %} {% for message in g.messages %}
<div class="alert"> <div class="alert">
{{ message }} {{ message }}
@ -25,11 +26,13 @@
{% endfor %} {% endfor %}
{% block content %}{% endblock %} {% block content %}{% endblock %}
</main> </main>
</div>
<footer> <footer>
{% block footer %} {% block footer %}
fnord
{% endblock %} {% endblock %}
</footer> </footer>
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}
</body> </body>
</html> </html>

View File

@ -1,3 +1,7 @@
<div class='breadcrumbs'> <div id='breadcrumbs'>
<a href="{{ url_for('index') }}">Home</a>{% for (uri, name) in breadcrumbs %}.<a href="{{ uri }}">{{ name }}</a>{% endfor %} <a href="{{ url_for('index') }}">Home</a>{% for (uri, name) in breadcrumbs %}.<a href="{{ uri }}">{{ name }}</a>{% endfor %}
<div id='actions'>
<a id='action__edit' data-state='view'>edit</a>
<a id='action__save' style='display: none;'>save</a>
</div>
</div> </div>

View File

@ -5,14 +5,9 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
<form id="editor_form"> {% include "breadcrumbs.html" %}
<div id='viewer'></div>
<h1><input name='title' type='text' value="{{ page.title }}"></h1> <div id='editor'></div>
{% include "breadcrumbs.html" %}
<div id='page_body'></div>
</form>
{% endblock %} {% endblock %}
@ -20,15 +15,46 @@
{% block scripts %} {% block scripts %}
<script src="https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js"></script> <script src="https://uicdn.toast.com/editor/latest/toastui-editor-all.min.js"></script>
<script> <script>
const viewerEl = document.querySelector("#viewer");
const editorEl = document.querySelector("#editor");;
const initialValue = (
"# " + document.getElementById("data_form__title").value +
"\n" +
document.getElementById("data_form__body").value
);
const editor = new toastui.Editor({ const editor = new toastui.Editor({
el: document.querySelector('#page_body'), el: editorEl,
initialEditType: 'wysiwyg', initialEditType: 'wysiwyg',
initialValue: document.getElementById("data_form__body").value, initialValue: initialValue,
minHeight: '500px',
previewStyle: 'tab', previewStyle: 'tab',
usageStatistics: false usageStatistics: false
}); });
editor.getMarkdown();
console.log(editor); const edit_btn = document.querySelector("#action__edit");
const save_btn = document.querySelector("#action__save");
toggleEditor = function() {
if (edit_btn.dataset.state == 'view') {
edit_btn.dataset.state = 'edit';
edit_btn.innerText = "discard draft";
viewerEl.style['display'] = "none";
editorEl.style['display'] = "inline";
save_btn.style['display'] = 'inline-block';
} else {
edit_btn.dataset.state = 'view';
edit_btn.innerText = "edit";
editorEl.style['display'] = "none";
viewerEl.style['display'] = "inline";
save_btn.style['display'] = 'none';
}
};
edit_btn.addEventListener('click', toggleEditor)
viewerEl.innerHTML = editor.getHTML();
</script> </script>
{% endblock %} {% endblock %}

View File

@ -1,22 +1,105 @@
@import "theme.css";
html { html {
height:100%;
padding: 0px; padding: 0px;
margin: 0px; margin: 0px;
height: 100vh;
box-sizing: border-box;
}
body {
font-family: var(--default-font-family);
font-size: var(--default-font-size);
height: inherit;
width: 100%;
margin: 0px;
box-sizing: border-box;
overflow-wrap: break-word;
overflow-y: scroll;
scrollbar-gutter: stable;
} }
body { h1, .toastui-editor-contents h1 {
display: block; font-size: var(--h1-size) !important;
margin: 0px 20px; font-weight: var(--header-weight) !important;
font-family: sans-serif; font-family: var(--header-font) !important;
width: calc(100%; - 20px); text-decoration: inherit !important;
height: 100%; font-variant: inherit !important;
border: none !important;
margin: initial !important;
line-height: initial !important;
padding: initial !important;
} }
h2, .toastui-editor-contents h2 {
font-size: var(--h2-size) !important;
font-weight: var(--header2-weight) !important;
font-family: var(--header-font) !important;
text-decoration: inherit !important;
font-variant: inherit !important;
border: none !important;
margin: initial !important;
line-height: initial !important;
padding: initial !important;
}
h3, .toastui-editor-contents h3 {
font-size: var(--h3-size) !important;
font-weight: var(--header3-weight) !important;
font-family: var(--header-font) !important;
text-decoration: inherit !important;
font-variant: inherit !important;
border: none !important;
margin: initial !important;
line-height: initial !important;
padding: initial !important;
}
h4, .toastui-editor-contents h4 {
font-size: var(--h4-size) !important;
font-weight: var(--header4-weight) !important;
font-family: var(--header-font) !important;
text-decoration: inherit !important;
font-variant: inherit !important;
border: none !important;
margin: initial !important;
line-height: initial !important;
padding: initial !important;
}
h5, .toastui-editor-contents h5 {
font-size: var(--h5-size) !important;
font-weight: var(--header5-weight) !important;
font-family: var(--header-font) !important;
text-decoration: inherit !important;
font-variant: inherit !important;
border: none !important;
margin: initial !important;
line-height: initial !important;
padding: initial !important;
}
h6, .toastui-editor-contents h6 {
font-size: var(--h6-size) !important;
font-weight: var(--header6-weight) !important;
font-family: var(--header-font) !important;
text-decoration: inherit !important;
font-variant: inherit !important;
border: none !important;
margin: initial !important;
line-height: initial !important;
padding: initial !important;
}
a {text-decoration: none;} a {text-decoration: none;}
.container { nav ul.container {
margin: auto; margin: auto;
width: 100%;
} }
ul { ul {
@ -27,10 +110,11 @@ ul {
} }
nav { nav {
display: block;
margin: 0px;
padding: 0px;
height: var(--nav-height);
background: #0ca0d6; background: #0ca0d6;
font-size: 0;
position: relative;
} }
nav > ul > li { nav > ul > li {
@ -91,52 +175,87 @@ nav > ul > li:hover > a {
} }
.breadcrumbs { #breadcrumbs {
font-size: 14px; height: var(--breadcrumbs-height);
margin-bottom: 10px; border-bottom: 1px dotted #DEDEDE;
}
main {
display: table;
width: 100%;
height: 100%;
} }
#data_form { #data_form {
display: none; display: none;
} }
main h1 { #editor {
padding: 0px; display: none;
margin: 0px;
}
main h1 > input {
border: none;
font-size: inherit;
font-weight: inherit;
width: 100%;
border-bottom: 1px dotted #000;
margin-bottom: 0px;
} }
#editor_form { #viewer {
display: table;
width: 100%;
}
#page_body {
display: inline; display: inline;
} }
#actions {
display: flex;
float: right;
}
#actions > a {
display: flex;
border: 1px solid black;
border-radius: 5px;
padding: 5px;
background: #FFF;
color: blue;
cursor: pointer;
margin-left: 5px;
}
#actions a:hover {
color: white;
background: blue;
}
.table-wrapper {
padding: 0px;
padding-top: var(--wrapper-padding);
display: table;
margin: auto;
width: var(--max-width);
max-width: 960px;
}
main {
display: table-row;
height: var(--main-height);
}
footer {
display: block;
position: relative;
bottom: 100vh - var(--footer-height);
height: var(--footer-height);
background-color: #DEDEDE;
}
.toastui-editor-main-container { .toastui-editor-main-container {
display: contents; display: contents;
} }
.toastui-editor,
.toastui-editor-main,
.toastui-editor-md-container,
.toastui-editor-defaultUI, .toastui-editor-defaultUI,
.toastui-editor-contents { .toastui-editor-contents,
.toastui-editor-ww-mode,
.toastui-editor-md-preview,
.toastui-editor-md,
.ProseMirror {
border: 0px !important; border: 0px !important;
padding: 0px !important; padding: 0px !important;
margin: 0px !important; margin: 0px !important;
font-size: var(--default-font-size) !important;
font-family: var(--default-font-family) !important;
} }
menu {
display: none;
}

View File

@ -0,0 +1,21 @@
:root {
/* Typography */
--default-font-family: sans-serif;
--default-font-size: 16px;
--header-font: sans-serif;
--header-weight: 700;
--h1-size: 32px;
--h2-size: 24px;
--h3-size: 18.72px;
--h4-size: 16px;
--h5-size: 13.28px;
--h6-size: 10.72px;
/* Layout */
--nav-height: 50px;
--footer-height: 150px;
--breadcrumbs-height: 50px;
--wrapper-padding: 20px;
--main-height: calc(100vh - var(--nav-height) - var(--footer-height) - var(--breadcrumbs-height) + var(--wrapper-padding));
--max-width: calc(100vw - 20px);
}

View File

@ -3,8 +3,6 @@ from tinydb import where
from ttfrog import app, forms, schema from ttfrog import app, forms, schema
STATIC = ["static"]
def relative_uri(path: str = ""): def relative_uri(path: str = ""):
""" """
@ -23,12 +21,12 @@ def get_parent(table: str, uri: str):
return get_page(parent_uri, table=table if "/" in parent_uri else "Page", create_okay=False) return get_page(parent_uri, table=table if "/" in parent_uri else "Page", create_okay=False)
def get_page(path: str = "", table: str = "Page", create_okay: bool = False): def get_page(path: str, table: str = "Page", create_okay: bool = False):
""" """
Get one page, including its members, but not recursively. Get one page, including its members, but not recursively.
""" """
uri = path.strip("/") or relative_uri(request.path) uri = relative_uri(path)
app.web.logger.debug(f"Need a page in {table} for {uri = }")
if table not in app.db.tables(): if table not in app.db.tables():
return None return None
@ -58,29 +56,31 @@ def get_page(path: str = "", table: str = "Page", create_okay: bool = False):
def rendered(page: schema.Record, template: str = "page.html"): def rendered(page: schema.Record, template: str = "page.html"):
if not page: if not page:
return Response("Page not found", status=404) return Response("Page not found", status=404)
return render_template(template, page=page, app=app, breadcrumbs=breadcrumbs(), user=session["user"], g=g)
return render_template(
def get_static(path): template,
return Response("OK", status=200) page=page,
app=app,
breadcrumbs=breadcrumbs(),
root=g.root,
user=g.user,
g=g
)
def breadcrumbs(): def breadcrumbs():
""" """
Return (uri, name) pairs for the parents leading from the VIEW_URI to the current request. Return (uri, name) pairs for the parents leading from the VIEW_URI to the current request.
""" """
if app.config.VIEW_URI != "/":
root = get_page()
yield (app.config.VIEW_URI, root.name)
uri = "" uri = ""
for name in relative_uri().split("/"): for name in relative_uri().split("/"):
uri = "/".join([uri, name]).lstrip("/") uri = "/".join([uri, name])
yield (uri, name) yield (uri, name)
@app.web.route("/") @app.web.route(app.config.VIEW_URI)
def index(): def index():
return rendered(get_page(create_okay=False)) return rendered(get_page(app.config.VIEW_URI, create_okay=False))
@app.web.route("/login", methods=["GET", "POST"]) @app.web.route("/login", methods=["GET", "POST"])
@ -94,7 +94,6 @@ def login():
g.user = user g.user = user
session["user_id"] = g.user.doc_id session["user_id"] = g.user.doc_id
session["user"] = dict(g.user.serialize()) session["user"] = dict(g.user.serialize())
app.web.logger.debug(f"Session for {user.name} ({user.doc_id}) started.")
return redirect(url_for("index")) return redirect(url_for("index"))
g.messages.append(f"Invalid login for {username}") g.messages.append(f"Invalid login for {username}")
return rendered(schema.Page(name="Login", title="Please enter your login details"), "login.html") return rendered(schema.Page(name="Login", title="Please enter your login details"), "login.html")
@ -112,7 +111,8 @@ def logout():
@app.web.route(f"{app.config.VIEW_URI}/<path:path>", methods=["GET"], defaults={"table": "Page"}) @app.web.route(f"{app.config.VIEW_URI}/<path:path>", methods=["GET"], defaults={"table": "Page"})
def view(table, path): def view(table, path):
parent = get_parent(table, relative_uri()) parent = get_parent(table, relative_uri())
return rendered(get_page(request.path, table=table, create_okay=(parent and parent.doc_id is not None))) page = get_page(request.path, table=table, create_okay=(parent and parent.doc_id is not None))
return rendered(page)
@app.web.route(f"{app.config.VIEW_URI}/<path:table>/<path:path>", methods=["POST"]) @app.web.route(f"{app.config.VIEW_URI}/<path:table>/<path:path>", methods=["POST"])
@ -146,6 +146,7 @@ def before_request():
g.user = app.db.User.get(doc_id=user_id) g.user = app.db.User.get(doc_id=user_id)
session["user_id"] = user_id session["user_id"] = user_id
session["user"] = dict(g.user.serialize()) session["user"] = dict(g.user.serialize())
g.root = get_page(app.config.VIEW_URI)
@app.web.after_request @app.web.after_request