1.2.0-alpha
45
app.py
|
@ -99,8 +99,15 @@ def render_markdown(text):
|
|||
def index():
|
||||
conn = func.connectToDb()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT * FROM questions WHERE answered=%s ORDER BY creation_date DESC", (True,))
|
||||
questions = cursor.fetchall()
|
||||
|
||||
cursor.execute("SELECT * FROM questions WHERE answered=%s AND pinned=%s ORDER BY creation_date DESC", (True, True))
|
||||
pinned_questions = cursor.fetchall()
|
||||
|
||||
cursor.execute("SELECT * FROM questions WHERE answered=%s AND pinned=%s ORDER BY creation_date DESC", (True, False))
|
||||
non_pinned_questions = cursor.fetchall()
|
||||
|
||||
questions = pinned_questions + non_pinned_questions
|
||||
|
||||
cursor.execute("SELECT * FROM answers ORDER BY creation_date DESC")
|
||||
answers = cursor.fetchall()
|
||||
|
||||
|
@ -123,7 +130,7 @@ def index():
|
|||
def inbox():
|
||||
conn = func.connectToDb()
|
||||
cursor = conn.cursor(dictionary=True)
|
||||
cursor.execute("SELECT * FROM questions WHERE answered=%s", (False,))
|
||||
cursor.execute("SELECT * FROM questions WHERE answered=%s ORDER BY creation_date DESC", (False,))
|
||||
questions = cursor.fetchall()
|
||||
|
||||
cursor.close()
|
||||
|
@ -146,13 +153,13 @@ def login():
|
|||
admin_password = request.form.get('admin_password')
|
||||
if admin_password == os.environ.get('ADMIN_PASSWORD'):
|
||||
session['logged_in'] = True
|
||||
return redirect(url_for('admin.index'))
|
||||
return redirect(url_for('index'))
|
||||
else:
|
||||
flash("Wrong password", 'danger')
|
||||
return redirect(url_for('admin.login'))
|
||||
else:
|
||||
if logged_in:
|
||||
return redirect('admin.index')
|
||||
return redirect('index')
|
||||
else:
|
||||
return render_template('admin/login.html')
|
||||
|
||||
|
@ -276,6 +283,34 @@ def returnToInbox():
|
|||
|
||||
return {'message': 'Successfully returned question to inbox.'}, 200
|
||||
|
||||
@api_bp.route('/pin_question/', methods=['POST'])
|
||||
def pinQuestion():
|
||||
question_id = request.args.get('question_id', '')
|
||||
if not question_id:
|
||||
abort(400, "Missing 'question_id' attribute or 'question_id' is empty")
|
||||
|
||||
conn = func.connectToDb()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("UPDATE questions SET pinned=%s WHERE id=%s", (True, question_id))
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
return {'message': 'Successfully pinned question.'}, 200
|
||||
|
||||
@api_bp.route('/unpin_question/', methods=['POST'])
|
||||
def unpinQuestion():
|
||||
question_id = request.args.get('question_id', '')
|
||||
if not question_id:
|
||||
abort(400, "Missing 'question_id' attribute or 'question_id' is empty")
|
||||
|
||||
conn = func.connectToDb()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("UPDATE questions SET pinned=%s WHERE id=%s", (False, question_id))
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
return {'message': 'Successfully unpinned question.'}, 200
|
||||
|
||||
@api_bp.route('/add_answer/', methods=['POST'])
|
||||
@loginRequired
|
||||
def addAnswer():
|
||||
|
|
|
@ -2,6 +2,6 @@ antiSpamFile = 'wordlist.txt'
|
|||
blacklistFile = 'word_blacklist.txt'
|
||||
configFile = 'config.json'
|
||||
appName = 'CatAsk'
|
||||
version = '1.1.0'
|
||||
version = '1.2.0'
|
||||
# id (identifier) is to be interpreted as described in https://semver.org/#spec-item-9
|
||||
version_id = '-alpha'
|
||||
|
|
11
functions.py
|
@ -110,9 +110,12 @@ def renderMarkdown(text):
|
|||
'i',
|
||||
'br',
|
||||
's',
|
||||
'del'
|
||||
'del',
|
||||
'a'
|
||||
]
|
||||
allowed_attrs = {}
|
||||
allowed_attrs = {
|
||||
'a': 'href'
|
||||
}
|
||||
# hard_wrap=True means that newlines will be
|
||||
# converted into <br> tags
|
||||
#
|
||||
|
@ -141,8 +144,8 @@ def generateMetadata(question=None, answer=None):
|
|||
# if question is specified, generate metadata for that question
|
||||
if question and answer:
|
||||
metadata.update({
|
||||
'title': trimContent(f"{question['content']}", 150) + " | " + cfg['instance']['title'],
|
||||
'description': trimContent(f"{answer['content']}", 150),
|
||||
'title': trimContent(question['content'], 150) + " | " + cfg['instance']['title'],
|
||||
'description': trimContent(answer['content'], 150),
|
||||
'url': cfg['instance']['fullBaseUrl'] + url_for('viewQuestion', question_id=question['id']),
|
||||
'image': cfg['instance']['image']
|
||||
})
|
||||
|
|
|
@ -11,7 +11,8 @@ CREATE TABLE IF NOT EXISTS questions (
|
|||
creation_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
content TEXT NOT NULL,
|
||||
answered BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
answer_id INT
|
||||
answer_id INT,
|
||||
pinned BOOLEAN NOT NULL DEFAULT FALSE
|
||||
) ENGINE=InnoDB;
|
||||
|
||||
ALTER TABLE questions
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
--bs-primary-rgb: 127,98,240;
|
||||
--bs-primary-subtle: color-mix(in srgb, var(--bs-primary) 10%, transparent);
|
||||
--bs-danger-bg-subtle: #2c0b0e;
|
||||
--bs-link-color: color-mix(in srgb, var(--bs-primary) 70%, white);
|
||||
--bs-link-color: color-mix(in srgb, var(--bs-primary) 60%, white);
|
||||
}
|
||||
|
||||
.btn {
|
||||
|
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 686 B After Width: | Height: | Size: 710 B |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
@ -8,6 +8,9 @@
|
|||
id="svg9"
|
||||
sodipodi:docname="catask.svg"
|
||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
|
||||
inkscape:export-filename="catask-16.png"
|
||||
inkscape:export-xdpi="4.0421052"
|
||||
inkscape:export-ydpi="4.0421052"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
|
@ -22,8 +25,8 @@
|
|||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="0.48026316"
|
||||
inkscape:cx="346.68493"
|
||||
inkscape:cy="368.54794"
|
||||
inkscape:cx="316.49315"
|
||||
inkscape:cy="533.04109"
|
||||
inkscape:window-width="1280"
|
||||
inkscape:window-height="962"
|
||||
inkscape:window-x="0"
|
||||
|
@ -42,7 +45,7 @@
|
|||
d="M372.86 340.836C361.458 364.034 337.594 380 310 380H69.9999C42.4057 380 18.5407 364.033 7.13956 340.835C0.0540733 303.832 2.43122 265.23 21.5492 228.243C22.3857 226.625 24.2343 225.797 25.9907 226.281C61.5026 236.067 97.5669 264.887 131.247 340.717C131.983 342.374 133.752 343.367 135.546 343.105C160.424 339.463 177.96 339.001 190 339.001C202.04 339.001 219.576 339.463 244.454 343.105C246.248 343.367 248.017 342.375 248.753 340.717C282.433 264.887 318.497 236.067 354.009 226.281C355.765 225.797 357.614 226.625 358.45 228.243C377.568 265.23 379.945 303.833 372.86 340.836Z"
|
||||
fill="#DA75FF"
|
||||
id="path1"
|
||||
style="fill:#e59bff;fill-opacity:1" />
|
||||
style="fill:#caacff;fill-opacity:1" />
|
||||
<mask
|
||||
id="path-3-inside-1_603_9"
|
||||
fill="white">
|
||||
|
@ -77,12 +80,14 @@
|
|||
gradientUnits="userSpaceOnUse">
|
||||
<stop
|
||||
stop-color="#CE95FF"
|
||||
id="stop4" />
|
||||
id="stop4"
|
||||
offset="0"
|
||||
style="stop-color:#9a69ce;stop-opacity:1;" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#8B52BC"
|
||||
id="stop5"
|
||||
style="stop-color:#6a3a93;stop-opacity:1;" />
|
||||
style="stop-color:#6f42c1;stop-opacity:1;" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint1_linear_603_9"
|
||||
|
@ -95,13 +100,13 @@
|
|||
<stop
|
||||
stop-color="#CE5AFF"
|
||||
id="stop6"
|
||||
offset="0"
|
||||
offset="0.2899459"
|
||||
style="stop-color:#ffffff;stop-opacity:1;" />
|
||||
<stop
|
||||
offset="1"
|
||||
stop-color="#C12FFF"
|
||||
id="stop7"
|
||||
style="stop-color:#d973ff;stop-opacity:1;" />
|
||||
style="stop-color:#caacff;stop-opacity:1;" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id="paint2_linear_603_9"
|
||||
|
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.7 KiB |
Before Width: | Height: | Size: 686 B After Width: | Height: | Size: 710 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
@ -42,7 +42,7 @@
|
|||
<input type="hidden" id="lockInbox" name="lockInbox" value="{{ cfg.lockInbox }}">
|
||||
<label for="_lockInbox" class="form-check-label">Lock inbox and don't allow new questions</label>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<div class="form-check mb-2">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
|
@ -53,6 +53,17 @@
|
|||
<input type="hidden" id="allowAnonQuestions" name="allowAnonQuestions" value="{{ cfg.allowAnonQuestions }}">
|
||||
<label for="_allowAnonQuestions" class="form-check-label">Allow anonymous questions</label>
|
||||
</div>
|
||||
<div class="form-check mb-3">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
name="_showQuestionCount"
|
||||
id="_showQuestionCount"
|
||||
value="{{ cfg.showQuestionCount }}"
|
||||
{% if cfg.showQuestionCount == true %}checked{% endif %}>
|
||||
<input type="hidden" id="showQuestionCount" name="showQuestionCount" value="{{ cfg.showQuestionCount }}">
|
||||
<label for="_showQuestionCount" class="form-check-label">Show question count in homepage</label>
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<button type="submit" class="btn btn-primary mt-3">
|
||||
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
|
||||
|
|
|
@ -41,21 +41,21 @@
|
|||
<body class="ms-2 me-2 mb-2">
|
||||
<a class="visually-hidden-focusable" href="#main-content">Skip to content</a>
|
||||
<div class="container-fluid">
|
||||
{% if logged_in %}
|
||||
<div class="d-flex justify-content-between 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 %}">
|
||||
<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"><a class="nav-link {{ homeLink }}" id="home-link" href="{{ url_for('index') }}">Home</a>
|
||||
{% 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 {{ adminLink }}" id="admin-link" href="{{ url_for('admin.index') }}">Admin</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% if logged_in %}
|
||||
<ul class="nav nav-underline m-0">
|
||||
<li><a class="nav-link" href="{{ url_for('admin.logout') }}">Logout</a></li>
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mt-3 mb-3" aria-hidden="true"></div>
|
||||
{% endif %}
|
||||
{% with messages = get_flashed_messages(with_categories=True) %}
|
||||
{% if messages %}
|
||||
{% for category, message in messages %}
|
||||
|
|
|
@ -3,49 +3,60 @@
|
|||
{% set inboxLink = 'active' %}
|
||||
{% block content %}
|
||||
{% if questions != [] %}
|
||||
{% for question in questions %}
|
||||
<div class="card mb-3 mt-3 alert-placeholder" id="question-{{ question.id }}">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h5 class="card-title mt-1 mb-1">{% if question.from_who %}{{ question.from_who }}{% else %}Anonymous{% endif %}</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>
|
||||
<h3 class="fs-4">{{ len(questions) }} <span class="fw-light">question(s)</span></h3>
|
||||
<div class="row">
|
||||
{% for question in questions %}
|
||||
<div class="col-sm-8 m-auto">
|
||||
<div class="card mb-3 mt-3 alert-placeholder" id="question-{{ question.id }}">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<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" data-bs-placement="top"></i> {{ cfg.anonName }}
|
||||
{% endif %}
|
||||
</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>
|
||||
</div>
|
||||
<div class="card-text markdown-content">{{ question.content | render_markdown }}</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<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">
|
||||
<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">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
|
||||
<span class="visually-hidden" role="status">Loading...</span>
|
||||
Answer
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#question-{{ question.id }}-modal">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-text markdown-content">{{ question.content | render_markdown }}</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<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">
|
||||
<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">
|
||||
<button type="submit" class="btn btn-primary">
|
||||
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
|
||||
<span class="visually-hidden" role="status">Loading...</span>
|
||||
Answer
|
||||
</button>
|
||||
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#question-{{ question.id }}-modal">Delete</button>
|
||||
<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">Confirmation</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete this question?</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" data-bs-dismiss="modal" hx-delete="{{ url_for('api.deleteQuestion', question_id=question.id) }}" hx-target="#question-{{ question.id }}" hx-swap="none">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</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">Confirmation</h1>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Are you sure you want to delete this question?</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger" data-bs-dismiss="modal" hx-delete="{{ url_for('api.deleteQuestion', question_id=question.id) }}" hx-target="#question-{{ question.id }}" hx-swap="none">Confirm</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<h2 class="text-center mt-5">Inbox is currently empty.</h2>
|
||||
{% endif %}
|
||||
|
|
|
@ -40,6 +40,10 @@
|
|||
</div>
|
||||
{% if combined %}
|
||||
<div class="col-sm-8">
|
||||
{% if cfg.showQuestionCount == true %}
|
||||
<h3 class="fs-4">{{ len(combined) }} <span class="fw-light">question(s)</span></h3>
|
||||
{% endif %}
|
||||
<div id="top-response-container"></div>
|
||||
{% for item in combined %}
|
||||
<div class="card mt-3 mb-3" id="question-{{ item.question.id }}">
|
||||
<div class="card-header">
|
||||
|
@ -51,28 +55,39 @@
|
|||
<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 %}
|
||||
</h5>
|
||||
<h6 class="card-subtitle fw-light text-body-secondary" data-bs-toggle="tooltip" data-bs-title="{{ item.question.creation_date }}" data-bs-placement="top">{{ formatRelativeTime(str(item.question.creation_date)) }}</h6>
|
||||
<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>
|
||||
</div>
|
||||
<div class="card-text markdown-content">{{ item.question.content | render_markdown }}</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<a href="{{ url_for('viewQuestion', question_id=item.question.id) }}" class="text-decoration-none text-reset">
|
||||
{% for answer in item.answers %}
|
||||
<div class="markdown-content">{{ answer.content }}</div>
|
||||
</div>
|
||||
</a>
|
||||
{% 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-2 text-body-secondary d-flex justify-content-between align-items-center">
|
||||
<span class="fs-6">answered {{ formatRelativeTime(str(answer.creation_date)) }}</span>
|
||||
<div>
|
||||
<span class="fs-6" data-bs-toggle="tooltip" data-bs-title="{{ answer.creation_date.strftime("%B %d, %Y %H:%M") }}">answered {{ formatRelativeTime(str(answer.creation_date)) }}</span>
|
||||
{% if item.question.pinned %}
|
||||
<span class="ms-1"><i class="bi bi-pin"></i> <span class="fw-medium">Pinned</span></span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<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>
|
||||
<ul class="dropdown-menu">
|
||||
<li><button class="dropdown-item" onclick="copy({{ item.question.id }})">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=item.question.id) }}" hx-target="#question-{{ item.question.id }}" hx-swap="none">Return to inbox</button></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
<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>
|
||||
<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>
|
||||
<ul class="dropdown-menu">
|
||||
<li><button class="dropdown-item" onclick="copy({{ item.question.id }})">Copy link</button></li>
|
||||
{% if logged_in %}
|
||||
{% 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>
|
||||
{% 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>
|
||||
{% 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>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
{% block title %}"{{ trimContent(question.content, 15) }}" - "{{ trimContent(answer.content, 15) }}"{% endblock %}
|
||||
{% block content %}
|
||||
<div class="row">
|
||||
<div class="col-sm-6 m-auto">
|
||||
<div class="col-sm-8 m-auto">
|
||||
<div class="card mt-2 mb-2" id="question-{{ question.id }}">
|
||||
<div class="card-header">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
|
@ -12,7 +12,7 @@
|
|||
<div class="card-text markdown-content">{{ question.content | render_markdown }}</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="mb-0">{{ answer.content }}</p>
|
||||
<div class="markdown-content">{{ answer.content | render_markdown }}</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">
|
||||
<span class="fs-6">answered {{ formatRelativeTime(str(answer.creation_date)) }}</span>
|
||||
|
|