From 8c87489f70f977ba525ed7dc6e993360644557b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lera=20Elvo=C3=A9?= Date: Sat, 30 May 2026 01:56:25 +0300 Subject: [PATCH] frontend for bookmark menu --- app/__init__.py | 2 + app/lib/babycode.py | 2 +- app/routes/hyperapi.py | 47 +++++++ app/templates/common/macros.html | 8 +- app/templates/hyper/bookmark_dropdown.html | 23 ++++ app/templates/threads/thread.html | 11 +- app/templates/users/log_in.html | 2 +- app/templates/users/settings.html | 4 +- data/static/css/style.css | 49 +++++++- .../static/js/bits/progressive-enhancement.js | 2 +- data/static/js/bits/ui.js | 117 ++++++++++++++++++ 11 files changed, 256 insertions(+), 11 deletions(-) create mode 100644 app/routes/hyperapi.py create mode 100644 app/templates/hyper/bookmark_dropdown.html diff --git a/app/__init__.py b/app/__init__.py index cea12d2..dd91726 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -211,6 +211,7 @@ def create_app(): from app.routes.mod import bp as mod_bp from app.routes.posts import bp as posts_bp from app.routes.api import bp as api_bp + from app.routes.hyperapi import bp as hyperapi_bp app.register_blueprint(topics_bp) app.register_blueprint(threads_bp) app.register_blueprint(users_bp) @@ -218,6 +219,7 @@ def create_app(): app.register_blueprint(mod_bp) app.register_blueprint(posts_bp) app.register_blueprint(api_bp) + app.register_blueprint(hyperapi_bp) with app.app_context(): from .schema import create as create_tables diff --git a/app/lib/babycode.py b/app/lib/babycode.py index dea0228..b578978 100644 --- a/app/lib/babycode.py +++ b/app/lib/babycode.py @@ -357,7 +357,7 @@ def tag_code(children, attr): else: code = input_code - button = f'' + button = f'' block = f'
{language}{button}
{code}
' return block diff --git a/app/routes/hyperapi.py b/app/routes/hyperapi.py new file mode 100644 index 0000000..7ffa74f --- /dev/null +++ b/app/routes/hyperapi.py @@ -0,0 +1,47 @@ +from flask import Blueprint, render_template, request, url_for +from ..auth import get_active_user, is_logged_in, hard_login_required +from ..models import BookmarkCollections +from functools import wraps + +bp = Blueprint('hyperapi', __name__, url_prefix='/hyperapi/') + +def user_required(view_func): + @wraps(view_func) + def wrapper(*args, **kwargs): + if get_active_user().is_guest(): + return 'Your account must be approved by a moderator before you may perform this action.', 403 + return view_func(*args, **kwargs) + return wrapper + +@bp.get('/bookmarks/dropdown/') +@hard_login_required +@user_required +def get_bookmark_dropdown(): + user = get_active_user() + concept_kind = request.args.get('concept_kind', 'thread') + try: + concept_id = int(request.args.get('concept_id', 0)) + except ValueError: + return 'error', 400 + is_thread = concept_kind == 'thread' + collections = BookmarkCollections.findall({'user_id': user.id}) + in_collection = None + for collection in collections: + callable = collection.has_thread if is_thread else collection.has_post + if callable(concept_id): + in_collection = collection.id + break + submit_url = url_for('.bookmark_thread' if is_thread else '.bookmark_post') + return render_template('hyper/bookmark_dropdown.html', collections=collections, in_collection=in_collection, is_thread=is_thread, concept_id=concept_id, submit_url=submit_url) + +@bp.post('/bookmarks/thread/') +@hard_login_required +@user_required +def bookmark_thread(): + return '', 204 + +@bp.post('/bookmarks/post/') +@hard_login_required +@user_required +def bookmark_post(): + return '', 204 diff --git a/app/templates/common/macros.html b/app/templates/common/macros.html index ec69331..f39884e 100644 --- a/app/templates/common/macros.html +++ b/app/templates/common/macros.html @@ -112,7 +112,7 @@ stub: char count - + {%- if banned_tags -%}
Forbidden tags: @@ -186,12 +186,12 @@ Edit {%- endif -%} {%- if can_reply -%} - + {%- endif -%} {%- if can_delete -%} Delete {%- endif -%} - + {%- endif -%}
@@ -219,7 +219,7 @@ {%- endfor -%} - {%- if is_logged_in() and allow_reacting -%}{%- endif -%} + {%- if is_logged_in() and allow_reacting -%}{%- endif -%} {%- elif is_editing -%} Cancel diff --git a/app/templates/hyper/bookmark_dropdown.html b/app/templates/hyper/bookmark_dropdown.html new file mode 100644 index 0000000..0285858 --- /dev/null +++ b/app/templates/hyper/bookmark_dropdown.html @@ -0,0 +1,23 @@ +
+ +
+ + +
+ {%- for collection in collections -%} +
+ + {%- set tc = collection.get_threads_count() -%} + {%- set pc = collection.get_posts_count() -%} + +
+ {%- endfor -%} + + +
diff --git a/app/templates/threads/thread.html b/app/templates/threads/thread.html index 9fb556f..d3192ac 100644 --- a/app/templates/threads/thread.html +++ b/app/templates/threads/thread.html @@ -29,7 +29,7 @@ - + {%- endif -%} Subscribe via RSS @@ -82,6 +82,15 @@ +{%- if is_logged_in() -%} +
+
+ Bookmark collections + View bookmarks +
+
Loading…
+
+{%- endif -%} {%- if is_logged_in() and get_active_user().can_post_to_thread_or_topic(thread) -%}

Reply to "{{thread.title}}"

diff --git a/app/templates/users/log_in.html b/app/templates/users/log_in.html index 2f6c159..5e1af21 100644 --- a/app/templates/users/log_in.html +++ b/app/templates/users/log_in.html @@ -15,7 +15,7 @@ Welcome back! No account yet? Sign up - +
{%- endblock -%} diff --git a/app/templates/users/settings.html b/app/templates/users/settings.html index 07e5e1c..b4f65a8 100644 --- a/app/templates/users/settings.html +++ b/app/templates/users/settings.html @@ -48,10 +48,10 @@ - +
- +
diff --git a/data/static/css/style.css b/data/static/css/style.css index 595bd7e..f2c0ca9 100644 --- a/data/static/css/style.css +++ b/data/static/css/style.css @@ -629,9 +629,20 @@ form.full-width { display: flex; flex-direction: column; align-items: start; - &> textarea, &> select, &> input[type="text"], &> input[type="password"] { + gap: var(--small-padding); + &> textarea, &> select, &> input[type="text"], &> input[type="password"], &> .inline-group { width: 100%; } + + &> .inline-group { + display: flex; + flex-direction: row; + gap: var(--base-padding); + + &> label { + width: 100%; + } + } } .context-explain { @@ -678,6 +689,42 @@ details.separated { justify-content: center; } +#bookmark-popover { + position: absolute; + min-width: 400px; + max-width: 400px; + max-height: 500px; + margin-block: var(--small-padding); + margin-inline: 0; + padding-inline: var(--medium-padding); + + overflow: scroll; + + .bookmark-menu-header { + display: flex; + justify-content: space-between; + } + + .bookmark-menu-inner .errors.hidden { + display: none; + } +} + +.bookmark-menu-item { + padding-block: var(--medium-padding); + padding-inline: var(--base-padding); + + &:has(.bookmark-menu-label:hover, input:hover) { + background-color: #0001; + } + .bookmark-menu-label { + display: flex; + flex-direction: column; + + } +} + + /* babycode tags */ .inline-code { background-color: var(--code-bg-color); diff --git a/data/static/js/bits/progressive-enhancement.js b/data/static/js/bits/progressive-enhancement.js index 1ad7822..dc2716e 100644 --- a/data/static/js/bits/progressive-enhancement.js +++ b/data/static/js/bits/progressive-enhancement.js @@ -14,7 +14,7 @@ export function enhance(_, __, el) { if (el.disabled) { el.disabled = false; if (el.title.search('JavaScript') !== -1) { - el.title = ''; + el.removeAttribute('title'); } } } diff --git a/data/static/js/bits/ui.js b/data/static/js/bits/ui.js index a94f86a..175c9f8 100644 --- a/data/static/js/bits/ui.js +++ b/data/static/js/bits/ui.js @@ -1,6 +1,9 @@ export const b = { babycodePreviewEndpoint: '/api/babycode-preview/', + bookmarksCollectionEndpoint: '/hyperapi/bookmarks/dropdown/', init: 'babycodeEditorCharCountInit localizeTimestamps', + + bookmarkState: {}, } const getThreadId = () => { @@ -11,6 +14,22 @@ const getThreadId = () => { return parseInt(scheme[2]); } +async function getHTML(endpoint, options = {}) { + let query = {}; + if (options._query !== undefined) { + query = options._query; + delete options._query; + } + + const params = new URLSearchParams(query); + const res = await fetch(`${endpoint}?${params}`, options); + if (!res.ok) { + console.error(res); + } + + return { body: await res.text(), status: res.status }; +} + export function setTab(_, sender, el) { if (sender.ariaSelected === 'true') { return; @@ -223,3 +242,101 @@ export function localizeTimestamps(_, __, el) { const d = new Date(el.dateTime); el.innerText = d.toLocaleString(); } + +export async function showBookmarkMenu(ev, sender, el) { + if (b.bookmarkState.state === undefined) { + el.addEventListener('toggle', e => { + if (e.newState === 'closed') { + b.bookmarkState.state = 'closed'; + } + }); + } + + // dismiss if open and last invoker is the same button that opened it + if (b.bookmarkState.state === 'open' && b.bookmarkState.invoker === sender) { + el.hidePopover(); + return; + } + + b.bookmarkState.invoker = sender; + b.bookmarkState.state = 'open'; + b.send({ 'plain': 'Loading…' }, 'fillBookmarkMenu'); + el.showPopover(); + const bRect = sender.getBoundingClientRect(); + const menuRect = el.getBoundingClientRect(); + const preferredLeft = bRect.right - menuRect.width; + const enoughSpace = preferredLeft >= 0; + + const scrollY = window.scrollY; + + if (enoughSpace) { + el.style.left = `${preferredLeft}px`; + } else { + el.style.left = `${bRect.left}px`; + } + el.style.top = `${bRect.bottom + scrollY}px`; + + const conceptKind = sender.prop('conceptKind'); + const conceptId = sender.prop('conceptId'); + + const bookmarkCollections = await getHTML(b.bookmarksCollectionEndpoint, { + _query: { + concept_kind: conceptKind, + concept_id: conceptId, + } + }); + b.send({ 'html': bookmarkCollections.body }, 'fillBookmarkMenu'); +} + + +export function fillBookmarkMenu(payload, __, el) { + if (payload.plain) { + el.innerText = payload.plain; + return; + } + + el.innerHTML = payload.html; +} + +export async function bookmarkMenuSubmit(ev, _, el) { + ev.preventDefault(); + const url = el.action; + const body = new URLSearchParams(new FormData(el)); + const options = { body: body, method: 'POST' }; + const status = (await getHTML(url, options)).status; + + if (status !== 204) { + b.trigger('bookmarkMenuShowError'); + return; + } + b.trigger('bookmarkMenuShowSavedButton'); +} + +export function bookmarkMenuShowSavedButton(_, __, el) { + el.value = 'Saved!'; +} + +export function bookmarkMenuResetSavedButton(_, __, el) { + el.value = 'Save'; +} + +export function bookmarkMenuShowError(_, __, el) { + if (el === undefined) { + return; + } + + if (el.classList.contains('hidden')) { + el.classList.remove('hidden'); + setTimeout(() => { b.trigger('bookmarkMenuHideError') }, 4000); + } +} + +export function bookmarkMenuHideError(_, __, el) { + if (el === undefined) { + return; + } + + if (!el.classList.contains('hidden')) { + el.classList.add('hidden'); + } +}