mirror of
https://codeberg.org/catask-org/catask.git
synced 2025-04-20 13:53:42 -05:00
1.3.0-alpha
This commit is contained in:
parent
6b1588d5cf
commit
d4c2c7df88
17 changed files with 525 additions and 142 deletions
24
app.py
24
app.py
|
@ -1,8 +1,10 @@
|
||||||
from flask import Flask, Blueprint, jsonify, request, abort, render_template, flash, session, redirect, url_for
|
from flask import Flask, Blueprint, jsonify, request, abort, render_template, flash, session, redirect, url_for
|
||||||
|
from flask_compress import Compress
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from mysql.connector import errorcode
|
from mysql.connector import errorcode
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
import mysql.connector
|
import mysql.connector
|
||||||
|
import urllib
|
||||||
import functions as func
|
import functions as func
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
@ -18,6 +20,8 @@ app.secret_key = os.environ.get("APP_SECRET")
|
||||||
cfg = func.loadJSON(const.configFile)
|
cfg = func.loadJSON(const.configFile)
|
||||||
app.config.from_mapping(cfg)
|
app.config.from_mapping(cfg)
|
||||||
app.config.update(cfg)
|
app.config.update(cfg)
|
||||||
|
# compress to improve page load speed
|
||||||
|
Compress(app)
|
||||||
|
|
||||||
# -- blueprints --
|
# -- blueprints --
|
||||||
api_bp = Blueprint('api', const.appName)
|
api_bp = Blueprint('api', const.appName)
|
||||||
|
@ -123,7 +127,7 @@ def index():
|
||||||
|
|
||||||
cursor.close()
|
cursor.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
return render_template('index.html', combined=combined, metadata=metadata, getRandomWord=func.getRandomWord, formatRelativeTime=func.formatRelativeTime)
|
return render_template('index.html', combined=combined, urllib=urllib, trimContent=func.trimContent, metadata=metadata, getRandomWord=func.getRandomWord, formatRelativeTime=func.formatRelativeTime)
|
||||||
|
|
||||||
@app.route('/inbox/', methods=['GET'])
|
@app.route('/inbox/', methods=['GET'])
|
||||||
@loginRequired
|
@loginRequired
|
||||||
|
@ -142,7 +146,21 @@ def viewQuestion(question_id):
|
||||||
question = func.getQuestion(question_id)
|
question = func.getQuestion(question_id)
|
||||||
answer = func.getAnswer(question_id)
|
answer = func.getAnswer(question_id)
|
||||||
metadata = func.generateMetadata(question, answer)
|
metadata = func.generateMetadata(question, answer)
|
||||||
return render_template('view_question.html', question=question, answer=answer, metadata=metadata, formatRelativeTime=func.formatRelativeTime, trimContent=func.trimContent)
|
return render_template('view_question.html', question=question, urllib=urllib, answer=answer, metadata=metadata, formatRelativeTime=func.formatRelativeTime, trimContent=func.trimContent)
|
||||||
|
|
||||||
|
# TODO: implement this and private questions should be here too
|
||||||
|
"""
|
||||||
|
@app.route('/questions/', methods=['GET'])
|
||||||
|
def seeAskedQuestions():
|
||||||
|
conn = func.connectToDb()
|
||||||
|
cursor = conn.cursor(dictionary=True)
|
||||||
|
cursor.execute("SELECT * FROM questions WHERE asker_id=%s ORDER BY creation_date DESC", (asker_id,))
|
||||||
|
questions = cursor.fetchall()
|
||||||
|
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
return render_template('asked_questions.html', questions=questions, formatRelativeTime=func.formatRelativeTime)
|
||||||
|
"""
|
||||||
|
|
||||||
# -- admin client routes --
|
# -- admin client routes --
|
||||||
|
|
||||||
|
@ -217,6 +235,8 @@ def addQuestion():
|
||||||
from_who = request.form.get('from_who', cfg['anonName'])
|
from_who = request.form.get('from_who', cfg['anonName'])
|
||||||
question = request.form.get('question', '')
|
question = request.form.get('question', '')
|
||||||
antispam = request.form.get('antispam', '')
|
antispam = request.form.get('antispam', '')
|
||||||
|
# reserved for version 1.4.0
|
||||||
|
# private = request.form.get('private')
|
||||||
|
|
||||||
if not question:
|
if not question:
|
||||||
abort(400, "Question field must not be empty")
|
abort(400, "Question field must not be empty")
|
||||||
|
|
|
@ -8,5 +8,6 @@
|
||||||
"charLimit": "512",
|
"charLimit": "512",
|
||||||
"anonName": "Anonymous",
|
"anonName": "Anonymous",
|
||||||
"lockInbox": false,
|
"lockInbox": false,
|
||||||
"allowAnonQuestions": true
|
"allowAnonQuestions": true,
|
||||||
|
"showQuestionCount": false
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,6 @@ antiSpamFile = 'wordlist.txt'
|
||||||
blacklistFile = 'word_blacklist.txt'
|
blacklistFile = 'word_blacklist.txt'
|
||||||
configFile = 'config.json'
|
configFile = 'config.json'
|
||||||
appName = 'CatAsk'
|
appName = 'CatAsk'
|
||||||
version = '1.2.1'
|
version = '1.3.0'
|
||||||
# id (identifier) is to be interpreted as described in https://semver.org/#spec-item-9
|
# id (identifier) is to be interpreted as described in https://semver.org/#spec-item-9
|
||||||
version_id = '-alpha'
|
version_id = '-alpha'
|
||||||
|
|
23
functions.py
23
functions.py
|
@ -3,6 +3,7 @@ from markupsafe import Markup
|
||||||
from bleach.sanitizer import Cleaner
|
from bleach.sanitizer import Cleaner
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from mistune import HTMLRenderer, escape
|
||||||
import mistune
|
import mistune
|
||||||
import humanize
|
import humanize
|
||||||
import mysql.connector
|
import mysql.connector
|
||||||
|
@ -96,7 +97,16 @@ def getRandomWord():
|
||||||
return random.choice(items)
|
return random.choice(items)
|
||||||
|
|
||||||
def trimContent(var, trim):
|
def trimContent(var, trim):
|
||||||
return var[:trim] + '...' if len(var) > trim else var
|
trimmed = var[:trim] + '…' if len(var) >= trim else var
|
||||||
|
trimmed = trimmed.rstrip()
|
||||||
|
return trimmed
|
||||||
|
|
||||||
|
class FixAsciiEmojis(HTMLRenderer):
|
||||||
|
def text(self, text):
|
||||||
|
if text.startswith('>') and text.endswith('<'):
|
||||||
|
return text
|
||||||
|
else:
|
||||||
|
return escape(text)
|
||||||
|
|
||||||
def renderMarkdown(text):
|
def renderMarkdown(text):
|
||||||
plugins = [
|
plugins = [
|
||||||
|
@ -124,14 +134,15 @@ def renderMarkdown(text):
|
||||||
# sanity of whoever will use this software
|
# sanity of whoever will use this software
|
||||||
# (after all, not everyone knows markdown syntax)
|
# (after all, not everyone knows markdown syntax)
|
||||||
md = mistune.create_markdown(
|
md = mistune.create_markdown(
|
||||||
escape=True,
|
escape=False,
|
||||||
plugins=plugins,
|
plugins=plugins,
|
||||||
hard_wrap=True
|
hard_wrap=True,
|
||||||
|
renderer=FixAsciiEmojis()
|
||||||
)
|
)
|
||||||
html = md(text)
|
html = md(text)
|
||||||
cleaner = Cleaner(tags=allowed_tags, attributes=allowed_attrs)
|
# cleaner = Cleaner(tags=allowed_tags, attributes=allowed_attrs)
|
||||||
clean_html = cleaner.clean(html)
|
# clean_html = cleaner.clean(html)
|
||||||
return Markup(clean_html)
|
return Markup(html)
|
||||||
|
|
||||||
def generateMetadata(question=None, answer=None):
|
def generateMetadata(question=None, answer=None):
|
||||||
metadata = {
|
metadata = {
|
||||||
|
|
|
@ -5,3 +5,4 @@ humanize
|
||||||
mistune
|
mistune
|
||||||
bleach
|
bleach
|
||||||
pathlib
|
pathlib
|
||||||
|
Flask-Compress
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
# CatAsk stable roadmap
|
# CatAsk stable roadmap
|
||||||
|
|
||||||
|
* [x] make stuff more accessible
|
||||||
|
* [ ] implement private questions
|
||||||
* [x] deleting answered questions OR returning them to inbox like retrospring does
|
* [x] deleting answered questions OR returning them to inbox like retrospring does
|
||||||
* [ ] bulk deleting questions from inbox
|
* [ ] bulk deleting questions from inbox
|
||||||
* [ ] blocking askers by ip
|
* [ ] blocking askers by ip
|
||||||
|
|
|
@ -13,6 +13,8 @@ CREATE TABLE IF NOT EXISTS questions (
|
||||||
answered BOOLEAN NOT NULL DEFAULT FALSE,
|
answered BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
answer_id INT,
|
answer_id INT,
|
||||||
pinned BOOLEAN NOT NULL DEFAULT FALSE
|
pinned BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
|
-- below is reserved for version 1.4.0
|
||||||
|
-- private BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
) ENGINE=InnoDB;
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
ALTER TABLE questions
|
ALTER TABLE questions
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
--bs-font-sans-serif: "Rubik", sans-serif;
|
--bs-font-sans-serif: "Rubik", sans-serif;
|
||||||
--bs-link-color-rgb: var(--bs-primary-rgb);
|
--bs-link-color-rgb: var(--bs-primary-rgb);
|
||||||
--bs-nav-link-color: var(--bs-primary);
|
--bs-nav-link-color: var(--bs-primary);
|
||||||
|
--bs-border-radius: .5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-bs-theme=light] {
|
[data-bs-theme=light] {
|
||||||
|
@ -18,15 +19,32 @@
|
||||||
--bs-link-hover-color: color-mix(in srgb, var(--bs-primary) 80%, white);
|
--bs-link-hover-color: color-mix(in srgb, var(--bs-primary) 80%, white);
|
||||||
--bs-danger: #dc3545;
|
--bs-danger: #dc3545;
|
||||||
--bs-danger-bg-subtle: #f8e6e8;
|
--bs-danger-bg-subtle: #f8e6e8;
|
||||||
|
--bs-link-color: var(--bs-primary);
|
||||||
|
--bs-link-hover-color: color-mix(in srgb, var(--bs-primary) 80%, black);
|
||||||
|
--bs-basic-btn-hover-bg: color-mix(in srgb, var(--bs-body-bg) 95%, black);
|
||||||
|
--bs-basic-btn-hover-bg-strong: color-mix(in srgb, var(--bs-body-bg) 90%, black);
|
||||||
|
--bs-basic-btn-active-bg: color-mix(in srgb, var(--bs-body-bg) 92%, black);
|
||||||
|
--bs-basic-btn-active-bg-strong: color-mix(in srgb, var(--bs-body-bg) 87%, black);
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-bs-theme=dark] {
|
[data-bs-theme=dark] {
|
||||||
|
--bs-body-bg: #202020;
|
||||||
|
--bs-body-bg-rgb: 32, 32, 32;
|
||||||
--bs-primary: #7f62f0;
|
--bs-primary: #7f62f0;
|
||||||
--bs-primary-rgb: 127,98,240;
|
--bs-primary-rgb: 127,98,240;
|
||||||
--bs-primary-subtle: color-mix(in srgb, var(--bs-primary) 10%, transparent);
|
--bs-primary-subtle: color-mix(in srgb, var(--bs-primary) 10%, transparent);
|
||||||
--bs-danger-bg-subtle: #2c0b0e;
|
--bs-danger-bg-subtle: #2c0b0e;
|
||||||
--bs-link-color: color-mix(in srgb, var(--bs-primary) 60%, white);
|
--bs-link-color: color-mix(in srgb, var(--bs-primary) 65%, white);
|
||||||
--bs-link-hover-color: color-mix(in srgb, var(--bs-primary) 80%, white);
|
--bs-link-hover-color: color-mix(in srgb, var(--bs-primary) 80%, white);
|
||||||
|
--bs-basic-btn-hover-bg:color-mix(in srgb, var(--bs-body-bg) 95%, white);
|
||||||
|
--bs-basic-btn-hover-bg-strong: color-mix(in srgb, var(--bs-body-bg) 90%, white);
|
||||||
|
--bs-basic-btn-active-bg: color-mix(in srgb, var(--bs-body-bg) 92%, white);
|
||||||
|
--bs-basic-btn-active-bg-strong: color-mix(in srgb, var(--bs-body-bg) 87%, white);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-bs-theme=light] .light-invert,
|
||||||
|
[data-bs-theme=dark] .dark-invert {
|
||||||
|
filter: invert();
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
|
@ -37,7 +55,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-bs-theme=light] .btn-primary {
|
[data-bs-theme=light] .btn-primary {
|
||||||
--bs-btn-bg: color-mix(in srgb, var(--bs-primary) 90%, white);
|
--bs-btn-bg: var(--bs-primary);
|
||||||
--bs-btn-border-color: var(--bs-btn-bg);
|
--bs-btn-border-color: var(--bs-btn-bg);
|
||||||
--bs-btn-hover-bg: color-mix(in srgb, var(--bs-btn-bg) 90%, black);
|
--bs-btn-hover-bg: color-mix(in srgb, var(--bs-btn-bg) 90%, black);
|
||||||
--bs-btn-hover-border-color: var(--bs-btn-hover-bg);
|
--bs-btn-hover-border-color: var(--bs-btn-hover-bg);
|
||||||
|
@ -61,14 +79,35 @@
|
||||||
padding: 7px 13px;
|
padding: 7px 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-bs-theme=light] .nav-link {
|
.btn:not(.btn-primary, .btn-success, .btn-danger,.btn-outline-danger):hover, .btn:not(.btn-primary, .btn-success, .btn-danger,.btn-outline-danger):focus {
|
||||||
color: color-mix(in srgb, var(--bs-primary) 90%, black);
|
background-color: var(--bs-basic-btn-hover-bg);
|
||||||
|
}
|
||||||
|
.btn:not(.btn-primary, .btn-success, .btn-danger,.btn-outline-danger):active {
|
||||||
|
background-color: var(--bs-basic-btn-active-bg);
|
||||||
|
}
|
||||||
|
.card-footer .btn:not(.btn-primary, .btn-success, .btn-danger,.btn-outline-danger):hover, .card-footer .btn:not(.btn-primary, .btn-success, .btn-danger,.btn-outline-danger):focus {
|
||||||
|
background-color: var(--bs-basic-btn-hover-bg-strong) !important;
|
||||||
|
color: var(--bs-body-color) !important;
|
||||||
|
}
|
||||||
|
.card-footer .btn:not(.btn-primary, .btn-success, .btn-danger,.btn-outline-danger):active {
|
||||||
|
background-color: var(--bs-basic-btn-active-bg-strong) !important;
|
||||||
|
}
|
||||||
|
.btn-check:checked + .btn, .btn.active, .btn.show, .btn:first-child:active, :not(.btn-check) + .btn:active {
|
||||||
|
border-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
[data-bs-theme=dark] .nav-link, [data-bs-theme=dark] a {
|
[data-bs-theme=light] .nav-link, a {
|
||||||
color: var(--bs-link-color);
|
color: var(--bs-link-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-underline .nav-link:not(.active):hover {
|
||||||
|
color: var(--bs-nav-link-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* [data-bs-theme=dark] .nav-link, [data-bs-theme=dark] a { */
|
||||||
|
/* color: var(--bs-link-color); */
|
||||||
|
/* } */
|
||||||
|
|
||||||
a:hover {
|
a:hover {
|
||||||
color: var(--bs-link-hover-color);
|
color: var(--bs-link-hover-color);
|
||||||
}
|
}
|
||||||
|
@ -120,6 +159,15 @@ a:hover {
|
||||||
border-color: color-mix(in srgb, var(--bs-primary), transparent);
|
border-color: color-mix(in srgb, var(--bs-primary), transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.list-group-item.active {
|
||||||
|
--bs-list-group-active-bg: var(--bs-primary);
|
||||||
|
--bs-list-group-active-border-color: var(--bs-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.card, .dropdown-menu {
|
||||||
|
box-shadow: var(--bs-box-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
.no-arrow.dropdown-toggle::after {
|
.no-arrow.dropdown-toggle::after {
|
||||||
border: none;
|
border: none;
|
||||||
display: none;
|
display: none;
|
||||||
|
@ -141,3 +189,4 @@ a:hover {
|
||||||
background-color: var(--bs-primary);
|
background-color: var(--bs-primary);
|
||||||
border-color: var(--bs-primary);
|
border-color: var(--bs-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
17
static/icons/fediverse.svg
Normal file
17
static/icons/fediverse.svg
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||||
|
<path fill="#a730b8" d="M12.088 23.868a6.734 6.732 0 0 1-2.88 2.866L25.02 42.602l3.812-1.93Zm20.857 20.93-3.812 1.932 8.012 8.04a6.734 6.732 0 0 1 2.88-2.866z"/>
|
||||||
|
<path fill="#5496be" d="m51.24 30.147-8.952 4.535.66 4.22 10.128-5.131a6.734 6.732 0 0 1-1.837-3.624Zm-14.15 7.168L15.926 48.038a6.734 6.732 0 0 1 1.837 3.624l19.989-10.127z"/>
|
||||||
|
<path fill="#ce3d1a" d="M30.284 10.9 20.071 30.833l3.016 3.027L33.9 12.755a6.734 6.732 0 0 1-3.616-1.854zm-12.87 25.117-5.172 10.095a6.734 6.732 0 0 1 3.615 1.855l4.573-8.925z"/>
|
||||||
|
<path fill="#d0188f" d="M9.12 26.778a6.734 6.732 0 0 1-3.364.703 6.734 6.732 0 0 1-.65-.068l3.02 19.316a6.734 6.732 0 0 1 3.365-.703 6.734 6.732 0 0 1 .65.068z"/>
|
||||||
|
<path fill="#5b36e9" d="M17.779 51.758a6.734 6.732 0 0 1 .07 1.356 6.734 6.732 0 0 1-.71 2.656l19.318 3.099a6.734 6.732 0 0 1-.07-1.356 6.734 6.732 0 0 1 .71-2.656Z"/>
|
||||||
|
<path fill="#30b873" d="m53.144 33.841-8.917 17.402a6.734 6.732 0 0 1 3.617 1.855l8.916-17.402a6.734 6.732 0 0 1-3.616-1.855z"/>
|
||||||
|
<path fill="#ebe305" d="M40.983 9.229a6.734 6.732 0 0 1-2.88 2.866L51.91 25.953a6.734 6.732 0 0 1 2.88-2.867z"/>
|
||||||
|
<path fill="#f47601" d="M28.38 7.206 10.922 16.05a6.734 6.732 0 0 1 1.837 3.624l17.456-8.844a6.734 6.732 0 0 1-1.837-3.624Z"/>
|
||||||
|
<path fill="#57c115" d="M38.07 12.111a6.734 6.732 0 0 1-3.42.731 6.734 6.732 0 0 1-.589-.062l1.546 9.898 4.22.677zm-1.564 16.322 3.656 23.402a6.734 6.732 0 0 1 3.315-.678 6.734 6.732 0 0 1 .705.077L40.726 29.11Z"/>
|
||||||
|
<path fill="#dbb210" d="M12.772 19.748a6.734 6.732 0 0 1 .075 1.377 6.734 6.732 0 0 1-.7 2.637l9.909 1.59 1.947-3.801zm16.984 2.726-1.948 3.803 23.413 3.759a6.734 6.732 0 0 1-.068-1.341 6.734 6.732 0 0 1 .718-2.67z"/>
|
||||||
|
<circle fill="#ffca00" cx="35.017" cy="6.12" r="6.12"/>
|
||||||
|
<circle fill="#64ff00" cx="57.878" cy="29.062" r="6.12"/>
|
||||||
|
<circle fill="#00a3ff" cx="43.111" cy="57.88" r="6.12"/>
|
||||||
|
<circle fill="#9500ff" cx="11.124" cy="52.749" r="6.12"/>
|
||||||
|
<circle fill="red" cx="6.122" cy="20.759" r="6.12"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 2 KiB |
1
static/icons/tumblr-light.svg
Normal file
1
static/icons/tumblr-light.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#ffffff"><path d="M6.27051 7.62976C8.86829 7.07312 10.816 4.76401 10.816 2H13.8463V7.15152H17.4826V10.7879H13.8463V16.2424C13.8463 16.7566 14.044 17.4493 14.7554 17.9091C15.2296 18.2156 16.2397 18.3671 17.7857 18.3636V22H13.5432C11.0329 22 8.99778 19.9649 8.99778 17.4545V10.7879H6.27051V7.62976Z"></path></svg>
|
After Width: | Height: | Size: 378 B |
3
static/icons/tumblr.svg
Normal file
3
static/icons/tumblr.svg
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M6.27051 7.62976C8.86829 7.07312 10.816 4.76401 10.816 2H13.8463V7.15152H17.4826V10.7879H13.8463V16.2424C13.8463 16.7566 14.044 17.4493 14.7554 17.9091C15.2296 18.2156 16.2397 18.3671 17.7857 18.3636V22H13.5432C11.0329 22 8.99778 19.9649 8.99778 17.4545V10.7879H6.27051V7.62976Z"></path>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 386 B |
|
@ -5,87 +5,93 @@
|
||||||
<h1 class="mb-3">Admin panel</h1>
|
<h1 class="mb-3">Admin panel</h1>
|
||||||
<div id="response-container"></div>
|
<div id="response-container"></div>
|
||||||
<form hx-post="{{ url_for('api.updateConfig') }}" hx-target="#response-container" hx-swap="none">
|
<form hx-post="{{ url_for('api.updateConfig') }}" hx-target="#response-container" hx-swap="none">
|
||||||
<h2>Instance</h2>
|
<h2 id="instance">Instance</h2>
|
||||||
<div class="form-group mb-3">
|
<div class="form-group mb-3">
|
||||||
<label for="instance.title">Title <small class="text-secondary">(e.g. My question box)</small></label>
|
<label class="form-label" for="instance.title">Title <small class="text-secondary">(e.g. My question box)</small></label>
|
||||||
<input type="text" id="instance.title" name="instance.title" value="{{ cfg.instance.title }}" class="form-control">
|
<input type="text" id="instance.title" name="instance.title" value="{{ cfg.instance.title }}" class="form-control">
|
||||||
</div>
|
<p class="form-text">Title of this CatAsk instance</p>
|
||||||
<div class="form-group mb-3">
|
</div>
|
||||||
<label for="instance.description">Description <small class="text-secondary">(e.g. Ask me a question!)</small></label>
|
<div class="form-group mb-3">
|
||||||
<input type="text" id="instance.description" name="instance.description" value="{{ cfg.instance.description }}" class="form-control">
|
<label class="form-label" for="instance.description">Description <small class="text-secondary">(e.g. Ask me a question!)</small></label>
|
||||||
</div>
|
<input type="text" id="instance.description" name="instance.description" value="{{ cfg.instance.description }}" class="form-control">
|
||||||
<div class="form-group mb-3">
|
<p class="form-text">Description of this CatAsk instance</p>
|
||||||
<label for="instance.image">Relative image path <small class="text-secondary">(default: /static/img/ca_screenshot.png)</small></label>
|
</div>
|
||||||
<input type="text" id="instance.image" name="instance.image" value="{{ cfg.instance.image }}" class="form-control">
|
<div class="form-group mb-3">
|
||||||
</div>
|
<label class="form-label" for="instance.image">Relative image path <small class="text-secondary">(default: /static/img/ca_screenshot.png)</small></label>
|
||||||
<div class="form-group mb-3">
|
<input type="text" id="instance.image" name="instance.image" value="{{ cfg.instance.image }}" class="form-control">
|
||||||
<label for="instance.fullBaseUrl">Base URL <small class="text-secondary">(e.g. https://example.com)</small></label>
|
<p class="form-text">Image that's going to be used in a link preview</p>
|
||||||
<input type="text" id="instance.fullBaseUrl" name="instance.fullBaseUrl" value="{{ cfg.instance.fullBaseUrl }}" class="form-control">
|
</div>
|
||||||
</div>
|
<div class="form-group mb-4">
|
||||||
<h2>General</h2>
|
<label class="form-label" for="instance.fullBaseUrl">Base URL <small class="text-secondary">(e.g. https://ask.example.com)</small></label>
|
||||||
<div class="form-group mb-3">
|
<input type="text" id="instance.fullBaseUrl" name="instance.fullBaseUrl" value="{{ cfg.instance.fullBaseUrl }}" class="form-control">
|
||||||
<label for="charLimit">Question character limit</label>
|
<p class="form-text">Full URL to homepage of this CatAsk instance without a trailing slash</p>
|
||||||
<input type="number" id="charLimit" name="charLimit" value="{{ cfg.charLimit }}" class="form-control">
|
</div>
|
||||||
</div>
|
<h2 id="general">General</h2>
|
||||||
<div class="form-group mb-3">
|
<div class="form-group mb-3">
|
||||||
<label for="anonName">Name for anonymous users</label>
|
<label class="form-label" for="charLimit">Question character limit</label>
|
||||||
<input type="text" id="anonName" name="anonName" value="{{ cfg.anonName }}" class="form-control">
|
<input type="number" id="charLimit" name="charLimit" value="{{ cfg.charLimit }}" class="form-control">
|
||||||
</div>
|
<p class="form-text">Max length of a question in characters; questions extending this character limit will not be added to the database</p>
|
||||||
<div class="form-check mb-2">
|
</div>
|
||||||
<input
|
<div class="form-group mb-4">
|
||||||
class="form-check-input"
|
<label class="form-label" for="anonName">Name for anonymous users</label>
|
||||||
type="checkbox"
|
<input type="text" id="anonName" name="anonName" value="{{ cfg.anonName }}" class="form-control">
|
||||||
name="_lockInbox"
|
<p class="form-text">This name will be used for questions asked to you by anonymous users</p>
|
||||||
id="_lockInbox"
|
</div>
|
||||||
value="{{ cfg.lockInbox }}"
|
<div class="form-check mb-2">
|
||||||
{% if cfg.lockInbox == true %}checked{% endif %}>
|
<input
|
||||||
<input type="hidden" id="lockInbox" name="lockInbox" value="{{ cfg.lockInbox }}">
|
class="form-check-input"
|
||||||
<label for="_lockInbox" class="form-check-label">Lock inbox and don't allow new questions</label>
|
type="checkbox"
|
||||||
</div>
|
name="_lockInbox"
|
||||||
<div class="form-check mb-2">
|
id="_lockInbox"
|
||||||
<input
|
value="{{ cfg.lockInbox }}"
|
||||||
class="form-check-input"
|
{% if cfg.lockInbox == true %}checked{% endif %}>
|
||||||
type="checkbox"
|
<input type="hidden" id="lockInbox" name="lockInbox" value="{{ cfg.lockInbox }}">
|
||||||
name="_allowAnonQuestions"
|
<label for="_lockInbox" class="form-check-label">Lock inbox and don't allow new questions</label>
|
||||||
id="_allowAnonQuestions"
|
</div>
|
||||||
value="{{ cfg.allowAnonQuestions }}"
|
<div class="form-check mb-2">
|
||||||
{% if cfg.allowAnonQuestions == true %}checked{% endif %}>
|
<input
|
||||||
<input type="hidden" id="allowAnonQuestions" name="allowAnonQuestions" value="{{ cfg.allowAnonQuestions }}">
|
class="form-check-input"
|
||||||
<label for="_allowAnonQuestions" class="form-check-label">Allow anonymous questions</label>
|
type="checkbox"
|
||||||
</div>
|
name="_allowAnonQuestions"
|
||||||
<div class="form-check mb-3">
|
id="_allowAnonQuestions"
|
||||||
<input
|
value="{{ cfg.allowAnonQuestions }}"
|
||||||
class="form-check-input"
|
{% if cfg.allowAnonQuestions == true %}checked{% endif %}>
|
||||||
type="checkbox"
|
<input type="hidden" id="allowAnonQuestions" name="allowAnonQuestions" value="{{ cfg.allowAnonQuestions }}">
|
||||||
name="_showQuestionCount"
|
<label for="_allowAnonQuestions" class="form-check-label">Allow anonymous questions</label>
|
||||||
id="_showQuestionCount"
|
</div>
|
||||||
value="{{ cfg.showQuestionCount }}"
|
<div class="form-check mb-3">
|
||||||
{% if cfg.showQuestionCount == true %}checked{% endif %}>
|
<input
|
||||||
<input type="hidden" id="showQuestionCount" name="showQuestionCount" value="{{ cfg.showQuestionCount }}">
|
class="form-check-input"
|
||||||
<label for="_showQuestionCount" class="form-check-label">Show question count in homepage</label>
|
type="checkbox"
|
||||||
</div>
|
name="_showQuestionCount"
|
||||||
<div class="form-group mb-3">
|
id="_showQuestionCount"
|
||||||
<button type="submit" class="btn btn-primary mt-3">
|
value="{{ cfg.showQuestionCount }}"
|
||||||
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
|
{% if cfg.showQuestionCount == true %}checked{% endif %}>
|
||||||
<span class="visually-hidden" role="status">Loading...</span>
|
<input type="hidden" id="showQuestionCount" name="showQuestionCount" value="{{ cfg.showQuestionCount }}">
|
||||||
Save
|
<label for="_showQuestionCount" class="form-check-label">Show question count in homepage</label>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
<div class="form-group mb-3">
|
||||||
</form>
|
<button type="submit" class="btn btn-primary mt-3">
|
||||||
<hr class="mt-4 mb-4">
|
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
|
||||||
<form hx-post="{{ url_for('admin.index') }}" hx-target="#response-container" hx-swap="none">
|
<span class="visually-hidden" role="status">Loading...</span>
|
||||||
<input type="hidden" name="action" value="update_word_blacklist">
|
Save
|
||||||
<div class="form-group mb-3">
|
</button>
|
||||||
<label for="blacklist"><h2>Word blacklist</h2></label>
|
</div>
|
||||||
<p class="text-secondary">Blacklisted words for questions; one word per line</p>
|
</form>
|
||||||
<textarea id="blacklist" name="blacklist" style="height: 300px; resize: vertical;" class="form-control">{{ blacklist }}</textarea>
|
<hr class="mt-4 mb-4">
|
||||||
<button type="submit" class="btn btn-primary mt-3">
|
<form hx-post="{{ url_for('admin.index') }}" hx-target="#response-container" hx-swap="none">
|
||||||
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
|
<input type="hidden" name="action" value="update_word_blacklist">
|
||||||
<span class="visually-hidden" role="status">Loading...</span>
|
<div class="form-group mb-3">
|
||||||
Save
|
<label class="form-label" for="blacklist_cat"><h2 id="blacklist">Word blacklist</h2></label>
|
||||||
</button>
|
<p class="text-secondary">Blacklisted words for questions; one word per line</p>
|
||||||
</div>
|
<textarea id="blacklist_cat" name="blacklist" style="height: 300px; resize: vertical;" class="form-control">{{ blacklist }}</textarea>
|
||||||
</form>
|
<button type="submit" class="btn btn-primary mt-3">
|
||||||
|
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
|
||||||
|
<span class="visually-hidden" role="status">Loading...</span>
|
||||||
|
Save
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
|
|
74
templates/asked_questions.html
Normal file
74
templates/asked_questions.html
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
{% extends 'base.html' %}
|
||||||
|
{% block title %}Asked questions{% endblock %}
|
||||||
|
{# set inboxLink = 'active' #}
|
||||||
|
{% block content %}
|
||||||
|
{% if questions != [] %}
|
||||||
|
<h3 class="fs-4">{{ len(questions) }} <span class="fw-light">question(s)</span></h3>
|
||||||
|
<div class="row">
|
||||||
|
{% for question in questions %}
|
||||||
|
<div class="card mt-3 mb-3" id="question-{{ question.id }}">
|
||||||
|
<div class="position-relative">
|
||||||
|
<div class="card-header">
|
||||||
|
<div class="d-flex justify-content-between align-items-center flex-wrap text-break">
|
||||||
|
<h3 class="h5 card-title mt-1 mb-1">
|
||||||
|
{% if question.from_who %}
|
||||||
|
{{ question.from_who }}
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-incognito" data-bs-toggle="tooltip" data-bs-title="This question was asked anonymously" data-bs-placement="top"></i> {{ cfg.anonName }}
|
||||||
|
{% endif %}
|
||||||
|
</h3>
|
||||||
|
<h3 class="h6 card-subtitle fw-light text-body-secondary" data-bs-toggle="tooltip" data-bs-title="{{ question.creation_date.strftime("%B %d, %Y %H:%M") }}" data-bs-placement="top">{{ formatRelativeTime(str(question.creation_date)) }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-text markdown-content">{{ question.content | render_markdown }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% for answer in item.answers %}
|
||||||
|
<div class="markdown-content">{{ answer.content | render_markdown }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-footer pt-0 pb-0 ps-3 pe-1 text-body-secondary d-flex justify-content-between align-items-center">
|
||||||
|
<div>
|
||||||
|
<span class="fs-6" data-bs-toggle="tooltip" data-bs-title="{{ answer.creation_date.strftime("%B %d, %Y %H:%M") }}">{{ formatRelativeTime(str(answer.creation_date)) }}</span>
|
||||||
|
{% if question.private %}
|
||||||
|
<span class="ms-1"><i class="bi bi-lock"></i> <span class="fw-medium">Private</span></span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<a href="{{ url_for('viewQuestion', question_id=question.id) }}" class="btn pt-2 pb-2 text-body-secondary" data-bs-toggle="tooltip" data-bs-title="View question" aria-label="View question"><i class="bi bi-box-arrow-up-right"></i></a>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<h2 class="text-center mt-5">Inbox is currently empty.</h2>
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||||
|
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
|
||||||
|
const appendAlert = (elementId, message, type) => {
|
||||||
|
const alertPlaceholder = document.getElementById(elementId);
|
||||||
|
const alertHtml = `
|
||||||
|
<div class="alert alert-${type} alert-dismissible" role="alert">
|
||||||
|
<div>${message}</div>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
alertPlaceholder.outerHTML = alertHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('htmx:afterRequest', function(event) {
|
||||||
|
const jsonResponse = event.detail.xhr.response;
|
||||||
|
if (jsonResponse) {
|
||||||
|
const parsed = JSON.parse(jsonResponse);
|
||||||
|
const msgType = event.detail.successful ? 'success' : 'error';
|
||||||
|
const targetElementId = event.detail.target.id;
|
||||||
|
appendAlert(targetElementId, parsed.message, msgType);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
|
@ -42,8 +42,8 @@
|
||||||
<a class="visually-hidden-focusable" href="#main-content">Skip to content</a>
|
<a class="visually-hidden-focusable" href="#main-content">Skip to content</a>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
<div class="d-flex {% if logged_in %}justify-content-between {% endif %}align-items-center mt-3 {% if logged_in %}mb-3{% endif %}">
|
<div class="d-flex {% if logged_in %}justify-content-between {% endif %}align-items-center mt-3 {% if logged_in %}mb-3{% endif %}">
|
||||||
<ul class="nav nav-underline position-relative {% if not logged_in %}mb-3{% endif %}">
|
<ul class="nav nav-underline position-relative">
|
||||||
<li class="nav-item d-flex align-items-center"><a href="{{ url_for('index') }}"><img src="{{ url_for('static', filename='icons/catask.svg') }}" width="32" height="32"></a></li>
|
<li class="nav-item d-flex align-items-center"><a href="{{ url_for('index') }}" aria-label="{{ cfg.instance.title }}'s icon"><img src="{{ url_for('static', filename='icons/catask.svg') }}" width="32" height="32" alt="{{ cfg.instance.title }}'s icon"></a></li>
|
||||||
<li class="nav-item"><a class="nav-link {{ homeLink }}" id="home-link" href="{{ url_for('index') }}">Home</a>
|
<li class="nav-item"><a class="nav-link {{ homeLink }}" id="home-link" href="{{ url_for('index') }}">Home</a>
|
||||||
{% if logged_in %}
|
{% if logged_in %}
|
||||||
<li class="nav-item"><a class="nav-link {{ inboxLink }}" id="inbox-link" href="{{ url_for('inbox') }}">Inbox</a>
|
<li class="nav-item"><a class="nav-link {{ inboxLink }}" id="inbox-link" href="{{ url_for('inbox') }}">Inbox</a>
|
||||||
|
@ -109,7 +109,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="d-inline-block">
|
<div class="d-inline-block">
|
||||||
<a href="https://git.gay/mst/catask" target="_blank" class="text-body-secondary text-decoration-none">{{ const.appName }} v{{ version }}{{ version_id }}</a>
|
<a href="https://git.gay/mst/catask" target="_blank" class="text-body-secondary text-decoration-none">{{ const.appName }} {{ version }}{{ version_id }}</a>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -17,15 +17,25 @@
|
||||||
<i class="bi bi-incognito" data-bs-toggle="tooltip" data-bs-title="This question was asked anonymously" data-bs-placement="top"></i> {{ cfg.anonName }}
|
<i class="bi bi-incognito" data-bs-toggle="tooltip" data-bs-title="This question was asked anonymously" data-bs-placement="top"></i> {{ cfg.anonName }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h5>
|
</h5>
|
||||||
<h6 class="card-subtitle fw-light text-body-secondary" data-bs-toggle="tooltip" data-bs-title="{{ question.creation_date }}" data-bs-placement="top">{{ formatRelativeTime(str(question.creation_date)) }}</h6>
|
<h6 class="card-subtitle fw-light text-body-secondary">
|
||||||
|
{#
|
||||||
|
reserved for version 1.4.0 or later
|
||||||
|
|
||||||
|
{% if question.private %}
|
||||||
|
<span class="me-2"><i class="bi bi-lock"></i> <span class="fw-medium" data-bs-toggle="tooltip" data-bs-title="This question was asked privately">Private</span></span>
|
||||||
|
{% endif %}
|
||||||
|
#}
|
||||||
|
<span data-bs-toggle="tooltip" data-bs-title="{{ question.creation_date.strftime("%B %d, %Y %H:%M") }}">{{ formatRelativeTime(str(question.creation_date)) }}</span>
|
||||||
|
</h6>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-text markdown-content">{{ question.content | render_markdown }}</div>
|
<div class="card-text markdown-content">{{ question.content | render_markdown }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form hx-post="{{ url_for('api.addAnswer', question_id=question.id) }}" hx-target="#question-{{ question.id }}" hx-swap="none">
|
<form hx-post="{{ url_for('api.addAnswer', question_id=question.id) }}" hx-target="#question-{{ question.id }}" hx-swap="none">
|
||||||
<div class="form-group d-sm-grid d-md-block gap-2">
|
<div class="form-group d-sm-grid d-md-block gap-2">
|
||||||
|
<label for="answer-{{ question.id }}" class="visually-hidden-focusable">Write your answer...</label>
|
||||||
<textarea class="form-control mb-2" required name="answer" id="answer-{{ question.id }}" placeholder="Write your answer..."></textarea>
|
<textarea class="form-control mb-2" required name="answer" id="answer-{{ question.id }}" placeholder="Write your answer..."></textarea>
|
||||||
<div class="d-flex flex-sm-column flex-md-row-reverse gap-2">
|
<div class="d-flex flex-column flex-md-row-reverse gap-2">
|
||||||
<button type="submit" class="btn btn-primary">
|
<button type="submit" class="btn btn-primary">
|
||||||
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
|
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
|
||||||
<span class="visually-hidden" role="status">Loading...</span>
|
<span class="visually-hidden" role="status">Loading...</span>
|
||||||
|
|
|
@ -4,7 +4,9 @@
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="mt-5 mb-sm-2 mb-md-5">
|
<div class="mt-5 mb-sm-2 mb-md-5">
|
||||||
<h1 class="text-center fw-bold">{{ cfg.instance.title }}</h1>
|
<h1 class="text-center fw-bold">{{ cfg.instance.title }}</h1>
|
||||||
<h5 class="text-center fw-light">{{ cfg.instance.description }}</h5>
|
{% autoescape off %}
|
||||||
|
<h2 class="h5 text-center fw-light">{{ cfg.instance.description }}</h2>
|
||||||
|
{% endautoescape %}
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-{% if combined %}4{% else %}8{% endif %}{% if not combined %} m-auto{% endif %}">
|
<div class="col-sm-{% if combined %}4{% else %}8{% endif %}{% if not combined %} m-auto{% endif %}">
|
||||||
|
@ -13,18 +15,32 @@
|
||||||
<br>
|
<br>
|
||||||
<h2>Ask a question</h2>
|
<h2>Ask a question</h2>
|
||||||
<form class="d-lg-block" hx-post="{{ url_for('api.addQuestion') }}" id="question-form" hx-target="#response-container" hx-swap="none">
|
<form class="d-lg-block" hx-post="{{ url_for('api.addQuestion') }}" id="question-form" hx-target="#response-container" hx-swap="none">
|
||||||
<div class="form-group mb-2">
|
<div class="form-floating mb-2">
|
||||||
<input {% if cfg.allowAnonQuestions == false %}required{% endif %} class="form-control" type="text" name="from_who" id="from_who" placeholder="Name {% if cfg.allowAnonQuestions == true %}(optional){% endif %}">
|
<input {% if cfg.allowAnonQuestions == false %}required{% endif %} class="form-control" type="text" name="from_who" id="from_who" placeholder="Name {% if cfg.allowAnonQuestions == true %}(optional){% else %}(required){% endif %}">
|
||||||
|
<label for="from_who">Name {% if cfg.allowAnonQuestions == true %}(optional){% else %}(required){% endif %}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group mb-2">
|
<div class="form-floating mb-2">
|
||||||
<textarea class="form-control" required name="question" id="question" placeholder="Write your question..."></textarea>
|
<textarea class="form-control" style="height: 100px;" required name="question" id="question" placeholder="Write your question..."></textarea>
|
||||||
|
<label for="question">Write your question...</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group mb-2">
|
<div class="form-group mb-2">
|
||||||
<label for="antispam">Anti-spam: please enter the word <code class="text-uppercase">{{ getRandomWord() }}</code> in lowercase</label>
|
<label for="antispam">Anti-spam: please enter the word <code class="text-uppercase">{{ getRandomWord() }}</code> in lowercase</label>
|
||||||
<input class="form-control" type="text" required name="antispam" id="antispam" autocomplete="off">
|
<input class="form-control" type="text" required name="antispam" id="antispam" autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group d-grid d-lg-flex justify-content-lg-end mt-3">
|
<div class="form-group d-grid d-lg-flex align-items-center justify-content-lg-end mt-3">
|
||||||
<button type="submit" class="btn btn-primary">
|
{#
|
||||||
|
<div class="form-check mb-0 w-100">
|
||||||
|
reserved for version 1.4.0
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
name="_private"
|
||||||
|
id="_private">
|
||||||
|
<input type="hidden" id="private" name="private">
|
||||||
|
<label for="_private" class="form-check-label">Ask privately</label>
|
||||||
|
</div>
|
||||||
|
#}
|
||||||
|
<button type="submit" class="btn btn-primary col-sm-4">
|
||||||
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
|
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
|
||||||
<span class="visually-hidden" role="status">Loading...</span>
|
<span class="visually-hidden" role="status">Loading...</span>
|
||||||
Ask
|
Ask
|
||||||
|
@ -45,61 +61,151 @@
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div id="top-response-container"></div>
|
<div id="top-response-container"></div>
|
||||||
{% for item in combined %}
|
{% for item in combined %}
|
||||||
<div class="card mt-3 mb-3" id="question-{{ item.question.id }}">
|
<div class="card mt-3 mb-3{% if item.question.pinned %} border-2{% endif %}" id="question-{{ item.question.id }}">
|
||||||
<div class="card-header">
|
<div class="position-relative">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="card-header{% if item.question.pinned %} border-2{% endif %}">
|
||||||
<h5 class="card-title mt-1 mb-1">
|
<div class="d-flex justify-content-between align-items-center flex-wrap text-break">
|
||||||
{% if item.question.from_who %}
|
<h3 class="h5 card-title mt-1 mb-1">
|
||||||
{{ item.question.from_who }}
|
{% if item.question.from_who %}
|
||||||
{% else %}
|
{{ item.question.from_who }}
|
||||||
<i class="bi bi-incognito" data-bs-toggle="tooltip" data-bs-title="This question was asked anonymously" data-bs-placement="top"></i> {{ cfg.anonName }}
|
{% else %}
|
||||||
{% endif %}
|
<i class="bi bi-incognito" data-bs-toggle="tooltip" data-bs-title="This question was asked anonymously" data-bs-placement="top"></i> {{ cfg.anonName }}
|
||||||
</h5>
|
{% endif %}
|
||||||
<h6 class="card-subtitle fw-light text-body-secondary" data-bs-toggle="tooltip" data-bs-title="{{ item.question.creation_date.strftime("%B %d, %Y %H:%M") }}" data-bs-placement="top">{{ formatRelativeTime(str(item.question.creation_date)) }}</h6>
|
</h3>
|
||||||
|
<h3 class="h6 card-subtitle fw-light text-body-secondary" data-bs-toggle="tooltip" data-bs-title="{{ item.question.creation_date.strftime("%B %d, %Y %H:%M") }}" data-bs-placement="top">{{ formatRelativeTime(str(item.question.creation_date)) }}</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-text markdown-content">{{ item.question.content | render_markdown }}</div>
|
||||||
|
<!-- <h4 class="position-absolute bottom-0 end-0 fw-light mb-0 me-2 fs-2">#{{item.question.id}}</h4> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="card-text markdown-content">{{ item.question.content | render_markdown }}</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% for answer in item.answers %}
|
{% for answer in item.answers %}
|
||||||
<div class="markdown-content">{{ answer.content | render_markdown }}</div>
|
<div class="markdown-content">{{ answer.content | render_markdown }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer pt-0 pb-0 ps-3 pe-2 text-body-secondary d-flex justify-content-between align-items-center">
|
<div class="card-footer pt-0 pb-0 ps-3 pe-1 text-body-secondary d-flex justify-content-between align-items-center{% if item.question.pinned %} border-2{% endif %}">
|
||||||
<div>
|
<div>
|
||||||
<span class="fs-6" data-bs-toggle="tooltip" data-bs-title="{{ answer.creation_date.strftime("%B %d, %Y %H:%M") }}">{{ formatRelativeTime(str(answer.creation_date)) }}</span>
|
<span class="fs-6" data-bs-toggle="tooltip" data-bs-title="{{ answer.creation_date.strftime("%B %d, %Y %H:%M") }}">{{ formatRelativeTime(str(answer.creation_date)) }}</span>
|
||||||
{% if item.question.pinned %}
|
{% if item.question.pinned %}
|
||||||
<span class="ms-1"><i class="bi bi-pin"></i> <span class="fw-medium">Pinned</span></span>
|
<span class="ms-1"><i class="bi bi-pin"></i> <span class="fw-medium">Pinned</span></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<a href="{{ url_for('viewQuestion', question_id=item.question.id) }}" class="btn pt-2 pb-2 text-body-secondary" data-bs-toggle="tooltip" data-bs-title="View question"><i class="bi bi-box-arrow-up-right"></i></a>
|
<a href="{{ url_for('viewQuestion', question_id=item.question.id) }}" class="btn pt-2 pb-2 text-body-secondary" data-bs-toggle="tooltip" data-bs-title="View question" aria-label="View question"><i class="bi bi-box-arrow-up-right"></i></a>
|
||||||
<div class="dropdown">
|
<div class="dropdown">
|
||||||
<button class="btn btn-sm pt-2 pb-2 no-arrow text-body-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"><i style="font-size: 1.2rem;" class="bi bi-three-dots"></i></button>
|
<button class="btn pt-2 pb-2 no-arrow text-body-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false" aria-label="Share question"><i class="bi bi-share"></i></button>
|
||||||
<ul class="dropdown-menu">
|
<ul class="dropdown-menu">
|
||||||
<li><button class="dropdown-item" onclick="copy({{ item.question.id }})">Copy link</button></li>
|
<li><button class="dropdown-item" onclick="copyFull(`{{ trimContent(item.question.content, 30) + ' — ' + trimContent(answer.content, 30) }} {{ cfg.instance.fullBaseUrl + url_for('viewQuestion',question_id=item.question.id) }}`)"><i class="bi bi-copy me-1"></i> Copy to clipboard</button></li>
|
||||||
|
<li>
|
||||||
|
<button class="dropdown-item d-flex align-items-center gap-1" data-bs-toggle="modal" data-bs-target="#question-{{ item.question.id }}-modal">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 64 64" class="me-1">
|
||||||
|
<path fill="currentColor" d="M12.088 23.868a6.734 6.732 0 0 1-2.88 2.866L25.02 42.602l3.812-1.93Zm20.857 20.93-3.812 1.932 8.012 8.04a6.734 6.732 0 0 1 2.88-2.866z"/>
|
||||||
|
<path fill="currentColor" d="m51.24 30.147-8.952 4.535.66 4.22 10.128-5.131a6.734 6.732 0 0 1-1.837-3.624Zm-14.15 7.168L15.926 48.038a6.734 6.732 0 0 1 1.837 3.624l19.989-10.127z"/>
|
||||||
|
<path fill="currentColor" d="M30.284 10.9 20.071 30.833l3.016 3.027L33.9 12.755a6.734 6.732 0 0 1-3.616-1.854zm-12.87 25.117-5.172 10.095a6.734 6.732 0 0 1 3.615 1.855l4.573-8.925z"/>
|
||||||
|
<path fill="currentColor" d="M9.12 26.778a6.734 6.732 0 0 1-3.364.703 6.734 6.732 0 0 1-.65-.068l3.02 19.316a6.734 6.732 0 0 1 3.365-.703 6.734 6.732 0 0 1 .65.068z"/>
|
||||||
|
<path fill="currentColor" d="M17.779 51.758a6.734 6.732 0 0 1 .07 1.356 6.734 6.732 0 0 1-.71 2.656l19.318 3.099a6.734 6.732 0 0 1-.07-1.356 6.734 6.732 0 0 1 .71-2.656Z"/>
|
||||||
|
<path fill="currentColor" d="m53.144 33.841-8.917 17.402a6.734 6.732 0 0 1 3.617 1.855l8.916-17.402a6.734 6.732 0 0 1-3.616-1.855z"/>
|
||||||
|
<path fill="currentColor" d="M40.983 9.229a6.734 6.732 0 0 1-2.88 2.866L51.91 25.953a6.734 6.732 0 0 1 2.88-2.867z"/>
|
||||||
|
<path fill="currentColor" d="M28.38 7.206 10.922 16.05a6.734 6.732 0 0 1 1.837 3.624l17.456-8.844a6.734 6.732 0 0 1-1.837-3.624Z"/>
|
||||||
|
<path fill="currentColor" d="M38.07 12.111a6.734 6.732 0 0 1-3.42.731 6.734 6.732 0 0 1-.589-.062l1.546 9.898 4.22.677zm-1.564 16.322 3.656 23.402a6.734 6.732 0 0 1 3.315-.678 6.734 6.732 0 0 1 .705.077L40.726 29.11Z"/>
|
||||||
|
<path fill="currentColor" d="M12.772 19.748a6.734 6.732 0 0 1 .075 1.377 6.734 6.732 0 0 1-.7 2.637l9.909 1.59 1.947-3.801zm16.984 2.726-1.948 3.803 23.413 3.759a6.734 6.732 0 0 1-.068-1.341 6.734 6.732 0 0 1 .718-2.67z"/>
|
||||||
|
<circle fill="currentColor" cx="35.017" cy="6.12" r="6.12"/>
|
||||||
|
<circle fill="currentColor" cx="57.878" cy="29.062" r="6.12"/>
|
||||||
|
<circle fill="currentColor" cx="43.111" cy="57.88" r="6.12"/>
|
||||||
|
<circle fill="currentColor" cx="11.124" cy="52.749" r="6.12"/>
|
||||||
|
<circle fill="currentColor" cx="6.122" cy="20.759" r="6.12"/>
|
||||||
|
</svg>
|
||||||
|
Share on Fediverse
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li><a class="dropdown-item" target="_blank" href="https://twitter.com/intent/tweet?text={{ urllib.parse.quote(trimContent(item.question.content, 30) + ' — ' + trimContent(answer.content, 30),safe='') }}%20{{ urllib.parse.quote(cfg.instance.fullBaseUrl + url_for('viewQuestion',question_id=item.question.id), safe='') }}"><i class="bi bi-twitter me-1"></i> Share on Twitter</a></li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item d-flex align-items-center gap-1" target="_blank" href="https://bsky.app/intent/compose?text={{ urllib.parse.quote(trimContent(item.question.content, 30) + ' — ' + trimContent(answer.content, 30),safe='') }}%20{{ urllib.parse.quote(cfg.instance.fullBaseUrl + url_for('viewQuestion',question_id=item.question.id), safe='') }}">
|
||||||
|
<svg width="16" height="16" class="me-1" viewBox="0 0 568 501" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill="currentColor" d="M123.121 33.6637C188.241 82.5526 258.281 181.681 284 234.873C309.719 181.681 379.759 82.5526 444.879 33.6637C491.866 -1.61183 568 -28.9064 568 57.9464C568 75.2916 558.055 203.659 552.222 224.501C531.947 296.954 458.067 315.434 392.347 304.249C507.222 323.8 536.444 388.56 473.333 453.32C353.473 576.312 301.061 422.461 287.631 383.039C285.169 375.812 284.017 372.431 284 375.306C283.983 372.431 282.831 375.812 280.369 383.039C266.939 422.461 214.527 576.312 94.6667 453.32C31.5556 388.56 60.7778 323.8 175.653 304.249C109.933 315.434 36.0535 296.954 15.7778 224.501C9.94525 203.659 0 75.2916 0 57.9464C0 -28.9064 76.1345 -1.61183 123.121 33.6637Z" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
Share on Bluesky
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item d-flex align-items-center gap-1" target="_blank" href="https://www.tumblr.com/widgets/share/tool?shareSource=legacy&posttype=text&title={{ urllib.parse.quote(trimContent(item.question.content, 30), safe='') }}&url={{ urllib.parse.quote(cfg.instance.fullBaseUrl + url_for('viewQuestion',question_id=item.question.id), safe='') }}&caption=&content={{ urllib.parse.quote(trimContent(answer.content, 30),safe='') }}%20{{ urllib.parse.quote(cfg.instance.fullBaseUrl + url_for('viewQuestion',question_id=item.question.id), safe='') }}">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16" class="me-1">
|
||||||
|
<path d="M6.27051 7.62976C8.86829 7.07312 10.816 4.76401 10.816 2H13.8463V7.15152H17.4826V10.7879H13.8463V16.2424C13.8463 16.7566 14.044 17.4493 14.7554 17.9091C15.2296 18.2156 16.2397 18.3671 17.7857 18.3636V22H13.5432C11.0329 22 8.99778 19.9649 8.99778 17.4545V10.7879H6.27051V7.62976Z"></path>
|
||||||
|
</svg>
|
||||||
|
Share on Tumblr
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn pt-2 pb-2 no-arrow text-body-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false" aria-label="Miscellaneous menu"><i class="bi bi-three-dots"></i></button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><button class="dropdown-item" onclick="copy({{ item.question.id }})"><i class="bi bi-copy me-1"></i> Copy link</button></li>
|
||||||
{% if logged_in %}
|
{% if logged_in %}
|
||||||
{% if not item.question.pinned %}
|
{% if not item.question.pinned %}
|
||||||
<li><button class="dropdown-item" hx-post="{{ url_for('api.pinQuestion', question_id=item.question.id) }}" hx-target="#top-response-container" hx-swap="none">Pin</button></li>
|
<li><button class="dropdown-item" hx-post="{{ url_for('api.pinQuestion', question_id=item.question.id) }}" hx-target="#top-response-container" hx-swap="none"><i class="bi bi-pin me-1"></i> Pin</button></li>
|
||||||
{% else %}
|
{% else %}
|
||||||
<li><button class="dropdown-item" hx-post="{{ url_for('api.unpinQuestion', question_id=item.question.id) }}" hx-target="#top-response-container" hx-swap="none">Unpin</button></li>
|
<li><button class="dropdown-item" hx-post="{{ url_for('api.unpinQuestion', question_id=item.question.id) }}" hx-target="#top-response-container" hx-swap="none"><i class="bi bi-pin-angle me-1"></i> Unpin</button></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li><button class="bg-hover-danger text-danger dropdown-item" hx-post="{{ url_for('api.returnToInbox', question_id=item.question.id) }}" hx-target="#question-{{ item.question.id }}" hx-swap="none">Return to inbox</button></li>
|
<li><button class="bg-hover-danger text-danger dropdown-item" hx-post="{{ url_for('api.returnToInbox', question_id=item.question.id) }}" hx-target="#question-{{ item.question.id }}" hx-swap="none"><i class="bi bi-arrow-return-left me-1"></i> Return to inbox</button></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% for item in combined %}
|
||||||
|
{% for answer in item.answers %}
|
||||||
|
<div class="modal fade" id="question-{{ item.question.id }}-modal" tabindex="-1" aria-labelledby="q-{{ item.question.id }}-modal-label" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h1 class="modal-title fs-5" id="q-{{ item.question.id }}-modal-label">Share on Fediverse</h1>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group mb-3">
|
||||||
|
<label for="fediInstance">Fediverse instance domain:</label>
|
||||||
|
<input type="text" id="fediInstance-{{item.question.id}}" name="fediInstance" class="form-control">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="shareOnFediverse('{{ item.question.id }}', `{{ urllib.parse.quote(trimContent(item.question.content, 30) + ' — ' + trimContent(answer.content, 30),safe='') }}%20{{ urllib.parse.quote(cfg.instance.fullBaseUrl + '/q/' + str(item.question.id),safe='') }}`)">Share</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
|
<script>
|
||||||
|
// fix handling checkboxes
|
||||||
|
document.querySelectorAll('.form-check-input').forEach(function(checkbox) {
|
||||||
|
checkbox.addEventListener('change', function() {
|
||||||
|
checkbox.nextElementSibling.value = this.checked ? '1' : '0';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
<script>
|
<script>
|
||||||
function copy(questionId) {
|
function copy(questionId) {
|
||||||
navigator.clipboard.writeText("{{ cfg.instance.fullBaseUrl }}/q/" + questionId + "/")
|
navigator.clipboard.writeText("{{ cfg.instance.fullBaseUrl }}/q/" + questionId + "/")
|
||||||
}
|
}
|
||||||
|
function copyFull(text) {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
}
|
||||||
|
function shareOnFediverse(questionId, contentToShare) {
|
||||||
|
const instanceDomain = document.getElementById(`fediInstance-${questionId}`).value.trim();
|
||||||
|
const shareUrl = `https://${instanceDomain}/share?text=${contentToShare}`;
|
||||||
|
|
||||||
|
window.open(shareUrl, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
document.getElementById('question-form').reset();
|
document.getElementById('question-form').reset();
|
||||||
|
|
|
@ -5,38 +5,118 @@
|
||||||
<div class="col-sm-8 m-auto">
|
<div class="col-sm-8 m-auto">
|
||||||
<div class="card mt-2 mb-2" id="question-{{ question.id }}">
|
<div class="card mt-2 mb-2" id="question-{{ question.id }}">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center flex-wrap text-break">
|
||||||
<h5 class="card-title mt-1 mb-1">{% if question.from_who %}{{ question.from_who }}{% else %}<i class="bi bi-incognito"></i> {{ cfg.anonName }}{% endif %}</h5>
|
<h5 class="card-title mt-1 mb-1">{% if question.from_who %}{{ question.from_who }}{% else %}<i class="bi bi-incognito"data-bs-toggle="tooltip" data-bs-title="This question was asked anonymously"></i> {{ cfg.anonName }}{% endif %}</h5>
|
||||||
<h6 class="card-subtitle fw-light text-body-secondary">{{ formatRelativeTime(str(question.creation_date)) }}</h6>
|
<h6 class="card-subtitle fw-light text-body-secondary"data-bs-toggle="tooltip" data-bs-title="{{ question.creation_date.strftime("%B %d, %Y %H:%M") }}">{{ formatRelativeTime(str(question.creation_date)) }}</h6>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-text markdown-content">{{ question.content | render_markdown }}</div>
|
<div class="card-text markdown-content">{{ question.content | render_markdown }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="markdown-content">{{ answer.content | render_markdown }}</div>
|
<div class="markdown-content">{{ answer.content | render_markdown }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer pt-0 pb-0 ps-3 pe-2 text-body-secondary d-flex justify-content-between align-items-center">
|
<div class="card-footer pt-0 pb-0 ps-3 pe-1 text-body-secondary d-flex justify-content-between align-items-center">
|
||||||
<span class="fs-6">answered {{ formatRelativeTime(str(answer.creation_date)) }}</span>
|
<span class="fs-6" data-bs-toggle="tooltip" data-bs-title="{{ answer.creation_date.strftime("%B %d, %Y %H:%M") }}">{{ formatRelativeTime(str(answer.creation_date)) }}</span>
|
||||||
<div class="dropdown">
|
<div class="d-flex align-items-center">
|
||||||
<button class="btn btn-sm pt-2 pb-2 no-arrow text-body-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"><i style="font-size: 1.2rem;" class="bi bi-three-dots"></i></button>
|
<div class="dropdown">
|
||||||
<ul class="dropdown-menu">
|
<button class="btn pt-2 pb-2 no-arrow text-body-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"><i class="bi bi-share"></i></button>
|
||||||
<li><button class="dropdown-item" onclick="copy({{ question.id }})">Copy link</button></li>
|
<ul class="dropdown-menu">
|
||||||
{% if logged_in %}
|
<li><button class="dropdown-item" onclick="copyFull(`{{ trimContent(question.content, 30) + ' — ' + trimContent(answer.content, 30) }} {{ cfg.instance.fullBaseUrl + url_for('viewQuestion',question_id=question.id) }}`)"><i class="bi bi-copy me-1"></i> Copy to clipboard</button></li>
|
||||||
<li><button class="bg-hover-danger text-danger dropdown-item" hx-post="{{ url_for('api.returnToInbox', question_id=question.id) }}" hx-target="#question-{{ question.id }}" hx-swap="none">Return to inbox</button></li>
|
<li>
|
||||||
{% endif %}
|
<button class="dropdown-item d-flex align-items-center gap-1" data-bs-toggle="modal" data-bs-target="#question-{{ question.id }}-modal">
|
||||||
</ul>
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 64 64" class="me-1">
|
||||||
|
<path fill="currentColor" d="M12.088 23.868a6.734 6.732 0 0 1-2.88 2.866L25.02 42.602l3.812-1.93Zm20.857 20.93-3.812 1.932 8.012 8.04a6.734 6.732 0 0 1 2.88-2.866z"/>
|
||||||
|
<path fill="currentColor" d="m51.24 30.147-8.952 4.535.66 4.22 10.128-5.131a6.734 6.732 0 0 1-1.837-3.624Zm-14.15 7.168L15.926 48.038a6.734 6.732 0 0 1 1.837 3.624l19.989-10.127z"/>
|
||||||
|
<path fill="currentColor" d="M30.284 10.9 20.071 30.833l3.016 3.027L33.9 12.755a6.734 6.732 0 0 1-3.616-1.854zm-12.87 25.117-5.172 10.095a6.734 6.732 0 0 1 3.615 1.855l4.573-8.925z"/>
|
||||||
|
<path fill="currentColor" d="M9.12 26.778a6.734 6.732 0 0 1-3.364.703 6.734 6.732 0 0 1-.65-.068l3.02 19.316a6.734 6.732 0 0 1 3.365-.703 6.734 6.732 0 0 1 .65.068z"/>
|
||||||
|
<path fill="currentColor" d="M17.779 51.758a6.734 6.732 0 0 1 .07 1.356 6.734 6.732 0 0 1-.71 2.656l19.318 3.099a6.734 6.732 0 0 1-.07-1.356 6.734 6.732 0 0 1 .71-2.656Z"/>
|
||||||
|
<path fill="currentColor" d="m53.144 33.841-8.917 17.402a6.734 6.732 0 0 1 3.617 1.855l8.916-17.402a6.734 6.732 0 0 1-3.616-1.855z"/>
|
||||||
|
<path fill="currentColor" d="M40.983 9.229a6.734 6.732 0 0 1-2.88 2.866L51.91 25.953a6.734 6.732 0 0 1 2.88-2.867z"/>
|
||||||
|
<path fill="currentColor" d="M28.38 7.206 10.922 16.05a6.734 6.732 0 0 1 1.837 3.624l17.456-8.844a6.734 6.732 0 0 1-1.837-3.624Z"/>
|
||||||
|
<path fill="currentColor" d="M38.07 12.111a6.734 6.732 0 0 1-3.42.731 6.734 6.732 0 0 1-.589-.062l1.546 9.898 4.22.677zm-1.564 16.322 3.656 23.402a6.734 6.732 0 0 1 3.315-.678 6.734 6.732 0 0 1 .705.077L40.726 29.11Z"/>
|
||||||
|
<path fill="currentColor" d="M12.772 19.748a6.734 6.732 0 0 1 .075 1.377 6.734 6.732 0 0 1-.7 2.637l9.909 1.59 1.947-3.801zm16.984 2.726-1.948 3.803 23.413 3.759a6.734 6.732 0 0 1-.068-1.341 6.734 6.732 0 0 1 .718-2.67z"/>
|
||||||
|
<circle fill="currentColor" cx="35.017" cy="6.12" r="6.12"/>
|
||||||
|
<circle fill="currentColor" cx="57.878" cy="29.062" r="6.12"/>
|
||||||
|
<circle fill="currentColor" cx="43.111" cy="57.88" r="6.12"/>
|
||||||
|
<circle fill="currentColor" cx="11.124" cy="52.749" r="6.12"/>
|
||||||
|
<circle fill="currentColor" cx="6.122" cy="20.759" r="6.12"/>
|
||||||
|
</svg>
|
||||||
|
Share on Fediverse
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li><a class="dropdown-item" href="https://twitter.com/intent/tweet?text={{ urllib.parse.quote(trimContent(question.content, 30) + ' — ' + trimContent(answer.content, 30),safe='') }}%20{{ urllib.parse.quote(cfg.instance.fullBaseUrl + '/q/' + str(question.id),safe='') }}"><i class="bi bi-twitter me-1"></i> Share on Twitter</a></li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item d-flex align-items-center gap-1" href="https://bsky.app/intent/compose?text={{ urllib.parse.quote(trimContent(question.content, 30) + ' — ' + trimContent(answer.content, 30),safe='') }}%20{{ urllib.parse.quote(cfg.instance.fullBaseUrl + url_for('viewQuestion',question_id=question.id), safe='') }}">
|
||||||
|
<svg width="16" height="16" class="me-1" viewBox="0 0 568 501" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill="currentColor" d="M123.121 33.6637C188.241 82.5526 258.281 181.681 284 234.873C309.719 181.681 379.759 82.5526 444.879 33.6637C491.866 -1.61183 568 -28.9064 568 57.9464C568 75.2916 558.055 203.659 552.222 224.501C531.947 296.954 458.067 315.434 392.347 304.249C507.222 323.8 536.444 388.56 473.333 453.32C353.473 576.312 301.061 422.461 287.631 383.039C285.169 375.812 284.017 372.431 284 375.306C283.983 372.431 282.831 375.812 280.369 383.039C266.939 422.461 214.527 576.312 94.6667 453.32C31.5556 388.56 60.7778 323.8 175.653 304.249C109.933 315.434 36.0535 296.954 15.7778 224.501C9.94525 203.659 0 75.2916 0 57.9464C0 -28.9064 76.1345 -1.61183 123.121 33.6637Z" fill="black"/>
|
||||||
|
</svg>
|
||||||
|
Share on Bluesky
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item d-flex align-items-center gap-1" href="https://www.tumblr.com/widgets/share/tool?shareSource=legacy&posttype=text&title={{ urllib.parse.quote(trimContent(question.content, 30),safe='') }}&url={{ urllib.parse.quote(cfg.instance.fullBaseUrl + '/q/' + str(question.id),safe='') }}&caption=&content={{ urllib.parse.quote(trimContent(answer.content, 30),safe='') }}%20{{ urllib.parse.quote(cfg.instance.fullBaseUrl + '/q/' + str(question.id),safe='') }}">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" width="16" height="16" class="me-1">
|
||||||
|
<path d="M6.27051 7.62976C8.86829 7.07312 10.816 4.76401 10.816 2H13.8463V7.15152H17.4826V10.7879H13.8463V16.2424C13.8463 16.7566 14.044 17.4493 14.7554 17.9091C15.2296 18.2156 16.2397 18.3671 17.7857 18.3636V22H13.5432C11.0329 22 8.99778 19.9649 8.99778 17.4545V10.7879H6.27051V7.62976Z"></path>
|
||||||
|
</svg>
|
||||||
|
Share on Tumblr
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn pt-2 pb-2 no-arrow text-body-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"><i class="bi bi-three-dots"></i></button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li><button class="dropdown-item" onclick="copy({{ question.id }})"><i class="bi bi-copy me-1"></i> Copy link</button></li>
|
||||||
|
{% if logged_in %}
|
||||||
|
<li><button class="bg-hover-danger text-danger dropdown-item" hx-post="{{ url_for('api.returnToInbox', question_id=question.id) }}" hx-target="#question-{{ question.id }}" hx-swap="none"><i class="bi bi-arrow-return-left me-1"></i> Return to inbox</button></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal fade" id="question-{{ question.id }}-modal" tabindex="-1" aria-labelledby="q-{{ question.id }}-modal-label" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h1 class="modal-title fs-5" id="q-{{ question.id }}-modal-label">Share on Fediverse</h1>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="form-group mb-3">
|
||||||
|
<label for="fediInstance">Fediverse instance domain:</label>
|
||||||
|
<input type="text" id="fediInstance-{{question.id}}" name="fediInstance" class="form-control">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="button" class="btn btn-primary" data-bs-dismiss="modal" onclick="shareOnFediverse('{{ question.id }}', `{{ urllib.parse.quote(trimContent(question.content, 30) + ' — ' + trimContent(answer.content, 30),safe='') }}%20{{ urllib.parse.quote(cfg.instance.fullBaseUrl + '/q/' + str(question.id) + '/',safe='') }}`)">Share</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script>
|
<script>
|
||||||
function copy(questionId) {
|
function copy(questionId) {
|
||||||
navigator.clipboard.writeText("{{ cfg.instance.fullBaseUrl }}/q/" + questionId + "/")
|
navigator.clipboard.writeText("{{ cfg.instance.fullBaseUrl }}/q/" + questionId + "/")
|
||||||
}
|
}
|
||||||
|
function copyFull(text) {
|
||||||
|
navigator.clipboard.writeText(text);
|
||||||
|
}
|
||||||
|
function shareOnFediverse(questionId, contentToShare) {
|
||||||
|
const instanceDomain = document.getElementById(`fediInstance-${questionId}`).value.trim();
|
||||||
|
const shareUrl = `https://${instanceDomain}/share?text=${contentToShare}`;
|
||||||
|
|
||||||
|
window.open(shareUrl, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
|
const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]');
|
||||||
|
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
|
||||||
|
|
||||||
const appendAlert = (elementId, message, type) => {
|
const appendAlert = (elementId, message, type) => {
|
||||||
const alertPlaceholder = document.getElementById(elementId);
|
const alertPlaceholder = document.getElementById(elementId);
|
||||||
const alertHtml = `
|
const alertHtml = `
|
||||||
|
|
Loading…
Add table
Reference in a new issue