mirror of
https://codeberg.org/catask-org/catask.git
synced 2025-04-20 05:43:41 -05:00
add remaining files (moved/deleted)
This commit is contained in:
parent
643f9e4efe
commit
ea1d27cf81
10 changed files with 471 additions and 336 deletions
|
@ -1,336 +0,0 @@
|
||||||
{% extends 'base.html' %}
|
|
||||||
{% block title %}Admin{% endblock %}
|
|
||||||
{% set adminLink = 'active' %}
|
|
||||||
{% block additionalHeadItems %}
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/toastify.css') }}">
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/coloris.min.css') }}">
|
|
||||||
{% endblock %}
|
|
||||||
{% block content %}
|
|
||||||
<h1 class="mb-3">Admin panel</h1>
|
|
||||||
<!-- this is actually not used anymore, but htmx requires a valid target so we have to use it -->
|
|
||||||
<div id="response-container"></div>
|
|
||||||
<a class="btn d-lg-none mb-2" href="#preview">Skip to preview</a>
|
|
||||||
<a class="visually-hidden-focusable" href="#preview">Skip to preview</a>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col mw-100">
|
|
||||||
<form hx-post="{{ url_for('api.updateConfig') }}" hx-target="#response-container" hx-swap="none">
|
|
||||||
<h2 id="instance" class="mb-3 fw-normal d-flex align-items-center justify-content-between">
|
|
||||||
Instance
|
|
||||||
<button type="button" class="btn btn-outline-secondary btn-sm" data-bs-toggle="collapse" data-bs-target="#preview-collapse" aria-expanded="true" aria-controls="preview-collapse"><i class="bi bi-arrows-expand-vertical me-1"></i> Toggle preview</button>
|
|
||||||
</h2>
|
|
||||||
<div class="form-group mb-3">
|
|
||||||
<label class="form-label" for="instance.title">Title <small class="text-body-secondary">(e.g. My question box)</small></label>
|
|
||||||
<input type="text" id="instance.title" name="instance.title" value="{{ cfg.instance.title }}" oninput="updateText('instance.title', 'title')" class="form-control">
|
|
||||||
<p class="form-text">Title of this CatAsk instance</p>
|
|
||||||
</div>
|
|
||||||
<div class="form-group mb-3">
|
|
||||||
<label class="form-label d-flex flex-column flex-lg-row align-items-lg-center justify-content-between" for="instance.description">
|
|
||||||
<span>Description <small class="text-body-secondary">(e.g. Ask me a question!)</small></span>
|
|
||||||
<small class="text-body-secondary"><i class="bi bi-markdown me-1"></i> Markdown supported</small>
|
|
||||||
</label>
|
|
||||||
<textarea id="instance.description" name="instance.description" oninput="updateText('instance.description', 'desc')" class="form-control" style="height: 200px; resize: vertical;">{{ cfg.instance.description }}</textarea>
|
|
||||||
<p class="form-text">Description of this CatAsk instance</p>
|
|
||||||
</div>
|
|
||||||
<div class="form-group mb-3">
|
|
||||||
<label class="form-label d-flex flex-column flex-lg-row align-items-lg-center justify-content-between" for="instance.rules">
|
|
||||||
<span>Rules</span>
|
|
||||||
<small class="text-body-secondary"><i class="bi bi-markdown me-1"></i> Markdown supported</small>
|
|
||||||
</label>
|
|
||||||
<textarea id="instance.rules" name="instance.rules" class="form-control" style="height: 200px; resize: vertical;" oninput="updateText('instance.rules', 'rules-content')">{{ cfg.instance.rules }}</textarea>
|
|
||||||
<p class="form-text">Rules of this CatAsk instance</p>
|
|
||||||
</div>
|
|
||||||
<div class="form-group mb-3">
|
|
||||||
<label class="form-label" for="instance.image">Relative image path <small class="text-body-secondary">(default: /static/img/ca_screenshot.png)</small></label>
|
|
||||||
<input type="text" id="instance.image" name="instance.image" value="{{ cfg.instance.image }}" class="form-control">
|
|
||||||
<p class="form-text">Image that's going to be used in a link preview</p>
|
|
||||||
</div>
|
|
||||||
<div class="form-group mb-2">
|
|
||||||
<label class="form-label" for="instance.fullBaseUrl">Base URL <small class="text-body-secondary">(e.g. https://ask.example.com)</small></label>
|
|
||||||
<input type="text" id="instance.fullBaseUrl" name="instance.fullBaseUrl" value="{{ cfg.instance.fullBaseUrl }}" class="form-control">
|
|
||||||
<p class="form-text">Full URL to homepage of this CatAsk instance without a trailing slash</p>
|
|
||||||
</div>
|
|
||||||
<div class="form-group mb-4">
|
|
||||||
<button type="submit" class="btn btn-primary" id="saveConfig">
|
|
||||||
<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>
|
|
||||||
<h2 id="customize" class="mb-3 fw-normal">Customize</h2>
|
|
||||||
<h3 class="fw-light">Favicon</h3>
|
|
||||||
<form hx-post="{{ url_for('api.uploadFavicon') }}" hx-target="#response-container" hx-swap="none" hx-encoding="multipart/form-data">
|
|
||||||
<p class="m-0">Current favicon: <img src="{{ url_for('static', filename='icons/favicon/apple-touch-icon.png') }}" width="32" height="32" alt="{{ cfg.instance.title }}'s icon" class="rounded"></p>
|
|
||||||
<div>
|
|
||||||
<label for="favicon" class="form-label">Upload favicon</label>
|
|
||||||
<input class="form-control" type="file" id="favicon" name="favicon">
|
|
||||||
</div>
|
|
||||||
<div class="mb-4">
|
|
||||||
<button type="submit" class="btn btn-primary mt-3" id="saveFavicon">
|
|
||||||
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
|
|
||||||
<span class="visually-hidden" role="status">Loading...</span>
|
|
||||||
Upload
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<form hx-post="{{ url_for('api.updateConfig') }}" hx-target="#response-container" hx-swap="none">
|
|
||||||
<h3 class="fw-light">Accent color</h3>
|
|
||||||
{# <h4 class="fw-light">Color</h4> #}
|
|
||||||
<div class="form-group d-flex flex-column">
|
|
||||||
<label class="form-label" for="style.accentLight">Light theme</label>
|
|
||||||
<input type="text" name="style.accentLight" id="style.accentLight" value="{{ cfg.style.accentLight }}" class="form-control" data-coloris>
|
|
||||||
<p class="form-text">Default: <b>#6345d9</b></p>
|
|
||||||
</div>
|
|
||||||
<div class="form-group d-flex flex-column">
|
|
||||||
<label class="form-label" for="style.accentDark">Dark theme</label>
|
|
||||||
<input type="text" name="style.accentDark" id="style.accentDark" value="{{ cfg.style.accentDark }}" class="form-control" data-coloris>
|
|
||||||
<p class="form-text">Default: <b>#7259d9</b></p>
|
|
||||||
</div>
|
|
||||||
{# brain doesn't feel like implementing this rn (9/27/24)
|
|
||||||
<h4 class="fw-light mt-2">Background</h4>
|
|
||||||
<div class="form-group d-flex flex-column">
|
|
||||||
<label class="form-label" for="style.bgLight">Light theme</label>
|
|
||||||
<input type="text" name="style.bgLight" id="style.bgLight" value="{{ cfg.style.bgLight }}" class="form-control" data-coloris>
|
|
||||||
<p class="form-text">Default: <b>#ffffff</b></p>
|
|
||||||
</div>
|
|
||||||
<div class="form-group d-flex flex-column">
|
|
||||||
<label class="form-label" for="style.bgDark">Dark theme</label>
|
|
||||||
<input type="text" name="style.bgDark" id="style.bgDark" value="{{ cfg.style.bgDark }}" class="form-control" data-coloris>
|
|
||||||
<p class="form-text">Default: <b>#202020</b></p>
|
|
||||||
</div>
|
|
||||||
#}
|
|
||||||
<div class="form-check mb-3">
|
|
||||||
<input
|
|
||||||
class="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
name="_style.tintColors"
|
|
||||||
id="_style.tintColors"
|
|
||||||
value="{{ cfg.style.tintColors }}"
|
|
||||||
{% if cfg.style.tintColors == true %}checked{% endif %}>
|
|
||||||
<input type="hidden" id="style.tintColors" name="style.tintColors" value="{{ cfg.style.tintColors }}">
|
|
||||||
<label for="_style.tintColors" class="form-check-label">Tint all colors with accent color</label>
|
|
||||||
</div>
|
|
||||||
{#
|
|
||||||
<div class="form-check mb-3">
|
|
||||||
<input
|
|
||||||
class="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
name="_style.tintBackground"
|
|
||||||
id="_style.tintBackground"
|
|
||||||
value="{{ cfg.style.tintBackground }}"
|
|
||||||
{% if cfg.style.tintBackground == true %}checked{% endif %}>
|
|
||||||
<input type="hidden" id="style.tintBackground" name="style.tintBackground" value="{{ cfg.style.tintBackground }}">
|
|
||||||
<label for="_style.tintBackground" class="form-check-label">Tint background with accent color</label>
|
|
||||||
</div>
|
|
||||||
#}
|
|
||||||
<h3 class="fw-light">Navbar link style</h3>
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="radio" name="style.navStyle" id="style.navStyle.underline" value="underline" {% if cfg.style.navStyle == 'underline' %}checked{% endif %}>
|
|
||||||
<label class="form-check-label" for="style.navStyle.underline">
|
|
||||||
Underline <span class="text-body-secondary">(default)</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check mb-4">
|
|
||||||
<input class="form-check-input" type="radio" name="style.navStyle" id="style.navStyle.pills" value="pills" {% if cfg.style.navStyle == 'pills' %}checked{% endif %}>
|
|
||||||
<label class="form-check-label" for="style.navStyle.pills">
|
|
||||||
Pills
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<h3 class="fw-light">Info box layout</h3>
|
|
||||||
<div class="form-check">
|
|
||||||
<input class="form-check-input" type="radio" name="style.infoBoxLayout" id="style.infoBoxLayout.column" value="column" {% if cfg.style.infoBoxLayout == 'column' %}checked{% endif %}>
|
|
||||||
<label class="form-check-label" for="style.infoBoxLayout.column">
|
|
||||||
Column <span class="text-body-secondary">(default)</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-check mb-4">
|
|
||||||
<input class="form-check-input" type="radio" name="style.infoBoxLayout" id="style.infoBoxLayout.row" value="row" {% if cfg.style.infoBoxLayout == 'row' %}checked{% endif %}>
|
|
||||||
<label class="form-check-label" for="style.infoBoxLayout.row">
|
|
||||||
Row
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<h3 class="fw-light">Trimmed content</h3>
|
|
||||||
<div class="form-group mb-3">
|
|
||||||
<label class="form-label" for="trimContentAfter">Trim content after (characters)</label>
|
|
||||||
<input type="number" id="trimContentAfter" name="trimContentAfter" value="{{ cfg.trimContentAfter }}" class="form-control">
|
|
||||||
<p class="form-text">Maximum length of content before it gets trimmed (used in sharing options); set to 0 to disable</p>
|
|
||||||
</div>
|
|
||||||
<h2 id="general" class="mb-3 fw-normal">General</h2>
|
|
||||||
<div class="form-group mb-3">
|
|
||||||
<label class="form-label" for="charLimit">Question character limit</label>
|
|
||||||
<input type="number" id="charLimit" name="charLimit" value="{{ cfg.charLimit }}" class="form-control">
|
|
||||||
<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>
|
|
||||||
<div class="form-group mb-4">
|
|
||||||
<label class="form-label" for="anonName">Name for anonymous users</label>
|
|
||||||
<input type="text" id="anonName" name="anonName" value="{{ cfg.anonName }}" class="form-control">
|
|
||||||
<p class="form-text">This name will be used for questions asked to you by anonymous users</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-check mb-2">
|
|
||||||
<input
|
|
||||||
class="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
name="_lockInbox"
|
|
||||||
id="_lockInbox"
|
|
||||||
value="{{ cfg.lockInbox }}"
|
|
||||||
{% if cfg.lockInbox == true %}checked{% endif %}>
|
|
||||||
<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-2">
|
|
||||||
<input
|
|
||||||
class="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
name="_allowAnonQuestions"
|
|
||||||
id="_allowAnonQuestions"
|
|
||||||
value="{{ cfg.allowAnonQuestions }}"
|
|
||||||
{% if cfg.allowAnonQuestions == true %}checked{% endif %}>
|
|
||||||
<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-2">
|
|
||||||
<input
|
|
||||||
class="form-check-input"
|
|
||||||
type="checkbox"
|
|
||||||
name="_noDeleteConfirm"
|
|
||||||
id="_noDeleteConfirm"
|
|
||||||
value="{{ cfg.noDeleteConfirm }}"
|
|
||||||
{% if cfg.noDeleteConfirm == true %}checked{% endif %}>
|
|
||||||
<input type="hidden" id="noDeleteConfirm" name="noDeleteConfirm" value="{{ cfg.noDeleteConfirm }}">
|
|
||||||
<label for="_noDeleteConfirm" class="form-check-label">Disable confirmation when deleting 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" id="saveConfig">
|
|
||||||
<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>
|
|
||||||
<hr class="mt-4 mb-4">
|
|
||||||
<form hx-post="{{ url_for('admin.index') }}" hx-target="#response-container" hx-swap="none">
|
|
||||||
<input type="hidden" name="action" value="update_word_blacklist">
|
|
||||||
<div class="form-group mb-3">
|
|
||||||
<label class="form-label" for="blacklist_cat"><h2 id="blacklist" class="fw-normal">Word blacklist</h2></label>
|
|
||||||
<p class="text-body-secondary">Blacklisted words for questions; one word per line</p>
|
|
||||||
<textarea id="blacklist_cat" name="blacklist" style="height: 300px; resize: vertical;" class="form-control">{{ blacklist }}</textarea>
|
|
||||||
<button type="submit" class="btn btn-primary mt-3" id="save-blacklist">
|
|
||||||
<span class="me-1 spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
|
|
||||||
<span class="visually-hidden" role="status">Loading...</span>
|
|
||||||
Save
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="collapse show collapse-horizontal col" id="preview-collapse">
|
|
||||||
<div>
|
|
||||||
<div id="preview">
|
|
||||||
<h2 class="mb-3 fw-normal">Preview</h2>
|
|
||||||
<button class="text-warning-emphasis d-inline-flex align-items-center btn btn-sm btn-outline-secondary mw-100" type="button" data-bs-toggle="collapse" data-bs-target="#preview-warning"><i class="bi bi-exclamation-triangle me-2"></i> The preview might not be accurate <i class="bi bi-chevron-down ms-1 small"></i></button>
|
|
||||||
<div class="collapse" id="preview-warning">
|
|
||||||
<small class="mt-2 text-body-secondary">The preview is not guaranteed to render the same as on other pages, because it uses a different renderer (<b>marked (js)</b> while everything else uses <b>mistune (python)</b>)<br>The reason for this is live markdown rendering support</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h3 class="h1 text-center fw-bold mt-4" id="title">{{ cfg.instance.title }}</h3>
|
|
||||||
{% autoescape off %}
|
|
||||||
<h4 class="h5 text-center fw-light" id="desc">{{ cfg.instance.description | render_markdown }}</h4>
|
|
||||||
{% endautoescape %}
|
|
||||||
<div class="m-auto col-sm-10">
|
|
||||||
<div class="accordion" id="rules-accordion">
|
|
||||||
<div class="accordion-item">
|
|
||||||
<h2 class="accordion-header">
|
|
||||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#rules" aria-expanded="false" aria-controls="collapseTwo">
|
|
||||||
<i class="bi bi-exclamation-triangle me-2 fs-4"></i> Rules
|
|
||||||
</button>
|
|
||||||
</h2>
|
|
||||||
<div id="rules" class="accordion-collapse collapse" data-bs-parent="#rules-accordion">
|
|
||||||
<div class="accordion-body">
|
|
||||||
<div class="markdown-content" id="rules-content">{{ cfg.instance.rules | render_markdown }}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
{% block scripts %}
|
|
||||||
<script src="{{ url_for('static', filename='js/toastify.min.js') }}"></script>
|
|
||||||
<script src="{{ url_for('static', filename='js/marked.min.js') }}"></script>
|
|
||||||
<script src="{{ url_for('static', filename='js/coloris.min.js') }}"></script>
|
|
||||||
<script>
|
|
||||||
Coloris({
|
|
||||||
theme: 'square',
|
|
||||||
themeMode: 'auto',
|
|
||||||
formatToggle: true,
|
|
||||||
alpha: false,
|
|
||||||
swatches: [
|
|
||||||
'#c70f0f', // Red
|
|
||||||
'#db5d0e', // Orange
|
|
||||||
'#968829', // Yellow
|
|
||||||
'#217d1a', // Green
|
|
||||||
'#28b59b', // Turquoise
|
|
||||||
'#338FFF', // Blue
|
|
||||||
'#3358ff',
|
|
||||||
'#6345d9', // Indigo
|
|
||||||
'#A833FF', // Purple
|
|
||||||
'#d1298b' // Pink
|
|
||||||
],
|
|
||||||
});
|
|
||||||
marked.use({
|
|
||||||
breaks: true,
|
|
||||||
gfm: true,
|
|
||||||
});
|
|
||||||
function updateText(input, element) {
|
|
||||||
const inputEl = document.getElementById(input);
|
|
||||||
const replaceEl = document.getElementById(element);
|
|
||||||
replaceEl.innerHTML = marked.parse(inputEl.value);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script>
|
|
||||||
// fix handling checkboxes
|
|
||||||
document.querySelectorAll('.form-check-input[type=checkbox]').forEach(function(checkbox) {
|
|
||||||
checkbox.addEventListener('change', function() {
|
|
||||||
checkbox.nextElementSibling.value = this.checked ? 'True' : 'False';
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
<script>
|
|
||||||
const forms = document.querySelectorAll('form');
|
|
||||||
forms.forEach((form) => {
|
|
||||||
form.addEventListener('htmx:afterRequest', function(event) {
|
|
||||||
const jsonResponse = event.detail.xhr.response;
|
|
||||||
if (jsonResponse) {
|
|
||||||
const parsed = JSON.parse(jsonResponse);
|
|
||||||
const alertType = event.detail.successful ? 'success' : 'danger';
|
|
||||||
message = event.detail.successful ? parsed.message : parsed.error;
|
|
||||||
if (event.detail.target.id != "question-count") {
|
|
||||||
Toastify({
|
|
||||||
text: message,
|
|
||||||
duration: 3000,
|
|
||||||
gravity: "top",
|
|
||||||
position: "right",
|
|
||||||
stopOnFocus: true,
|
|
||||||
className: `alert alert-${alertType} shadow alert-dismissible`,
|
|
||||||
close: true
|
|
||||||
}).showToast();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock %}
|
|
10
templates/snippets/admin/emojiRow.html
Normal file
10
templates/snippets/admin/emojiRow.html
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
<tr id="emoji-{{emoji.name}}" class="emoji-row">
|
||||||
|
<td><img src="/{{ emoji.image }}" width="28" height="28" class="emoji"></td>
|
||||||
|
<td>{{ emoji.name.capitalize() }}</td>
|
||||||
|
<td><code>{{ emoji.relative_path }}</code></td>
|
||||||
|
<td>
|
||||||
|
<div id="actions">
|
||||||
|
<button class="btn btn-sm btn-outline-danger" hx-target="#emoji-{{emoji.name}}" hx-delete="{{ url_for('api.deleteEmoji', emoji_name=emoji.relative_path) }}">Delete</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
14
templates/snippets/admin/packRow.html
Normal file
14
templates/snippets/admin/packRow.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<tr id="pack-{{pack.name}}" class="emoji-pack-row">
|
||||||
|
<td><img src="/{{ pack.preview_image }}" width="28" height="28" class="emoji"></td>
|
||||||
|
<td>{{ pack.name }}</td>
|
||||||
|
{% if json_pack %}
|
||||||
|
<td>{{ pack.website }}</td>
|
||||||
|
<td>{{ formatRelativeTime(pack.exportedAt) }}</td>
|
||||||
|
{% endif %}
|
||||||
|
<td><code>{{ pack.relative_path }}</code></td>
|
||||||
|
<td>
|
||||||
|
<div id="actions">
|
||||||
|
<button class="btn btn-sm btn-outline-danger" hx-target="#pack-{{pack.name}}" hx-delete="{{ url_for('api.deleteEmojiPack', pack_name=pack.name) }}">Delete</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
26
templates/snippets/layout/description/normal.html
Normal file
26
templates/snippets/layout/description/normal.html
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<div class="mt-5 mb-sm-2 mb-md-5 col-sm-{% if class %}{{ class }}{% elif cfg.style.infoBoxLayout == 'row' %}10{% else %}8{% endif %} m-auto{% if not class and cfg.style.infoBoxLayout == 'row' %} d-lg-flex justify-content-between gap-2{% endif %}" id="description">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-center fw-bold" id="title">{{ cfg.instance.title }}</h1>
|
||||||
|
{% autoescape off %}
|
||||||
|
<h2 class="h5 text-center fw-light" id="desc">{{ cfg.instance.description | render_markdown }}</h2>
|
||||||
|
{% endautoescape %}
|
||||||
|
</div>
|
||||||
|
{% if len(cfg.instance.rules) > 0 %}
|
||||||
|
<div class="m-auto col-sm-{% if rules_class %}{{ rules_class }}{% elif cfg.style.infoBoxLayout == 'row' %}6{% else %}8{% endif %}">
|
||||||
|
<div class="accordion" id="rules-accordion">
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#rules" aria-expanded="false" aria-controls="collapseTwo">
|
||||||
|
<i class="bi bi-exclamation-triangle me-2 fs-4"></i> Rules
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="rules" class="accordion-collapse collapse" data-bs-parent="#rules-accordion">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<div class="markdown-content" id="rules-content">{{ cfg.instance.rules | render_markdown }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
29
templates/snippets/layout/description/retrospring.html
Normal file
29
templates/snippets/layout/description/retrospring.html
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
<div class="col-sm-{% if combined %}3{% else %}6{% endif %}{% if not combined %} m-auto{% endif %} {{ class }}" id="description">
|
||||||
|
<div class="sticky-md-top pt-3">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="markdown-content fw-buttons" id="desc">
|
||||||
|
{{ cfg.instance.description | render_markdown }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if len(cfg.instance.rules) > 0 %}
|
||||||
|
<div class="mt-3 mb-3">
|
||||||
|
<div class="accordion" id="rules-accordion">
|
||||||
|
<div class="accordion-item">
|
||||||
|
<h2 class="accordion-header">
|
||||||
|
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#rules" aria-expanded="false" aria-controls="collapseTwo">
|
||||||
|
<i class="bi bi-exclamation-triangle me-2 fs-4"></i> Rules
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<div id="rules" class="accordion-collapse collapse">
|
||||||
|
<div class="accordion-body">
|
||||||
|
<div class="markdown-content" id="rules-content">{{ cfg.instance.rules | render_markdown }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
70
templates/snippets/layout/homepage/normal.html
Normal file
70
templates/snippets/layout/homepage/normal.html
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
{% include 'snippets/layout/description/normal.html' %}
|
||||||
|
|
||||||
|
<div class="col-sm-{% if combined %}4{% else %}6{% endif %}{% if not combined %} m-auto{% endif %}">
|
||||||
|
<div class="mb-5 sticky-md-top">
|
||||||
|
{% if cfg.lockInbox == false %}
|
||||||
|
<br>
|
||||||
|
<h2 class="d-flex justify-content-between align-items-center">
|
||||||
|
<span>Ask a question</span>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#cw-collapse" aria-expanded="false" aria-controls="cw-collapse">
|
||||||
|
<i class="bi bi-plus-lg me-1"></i> Add CW
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
<form class="d-lg-block" hx-post="{{ url_for('api.addQuestion') }}" id="question-form" hx-target="#response-container" hx-swap="none">
|
||||||
|
<div class="collapse" id="cw-collapse">
|
||||||
|
<div class="form-floating mb-2">
|
||||||
|
<input class="form-control" type="text" name="cw" id="cw" placeholder="Content warning">
|
||||||
|
<label for="cw">Content warning</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-floating mb-2">
|
||||||
|
<input maxlength="{{ cfg.charLimit }}" {% 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 class="form-floating mb-2">
|
||||||
|
<textarea maxlength="{{ cfg.charLimit }}" class="form-control" style="height: 100px;" required name="question" id="question" placeholder="Write your question..."></textarea>
|
||||||
|
<label for="question">Write your question...</label>
|
||||||
|
<p class="text-end mt-1 small d-flex align-itemcenter justify-content-between"><span class="text-body-secondary"><i class="bi bi-markdown me-1"></i> Markdown supported</span> <span id="charCount">{{ cfg.charLimit }}</span></p>
|
||||||
|
</div>
|
||||||
|
<div class="form-group mb-2">
|
||||||
|
<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">
|
||||||
|
</div>
|
||||||
|
<div class="form-group d-grid d-lg-flex align-items-center justify-content-lg-end mt-3">
|
||||||
|
{#
|
||||||
|
<div class="form-check mb-0 w-100">
|
||||||
|
reserved for version 1.6.0 or later
|
||||||
|
<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" id="ask-btn">
|
||||||
|
<span class="me-1 spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
|
||||||
|
<span class="visually-hidden" role="status">Loading...</span>
|
||||||
|
Ask
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div id="response-container"></div>
|
||||||
|
{% else %}
|
||||||
|
<br>
|
||||||
|
<h2 class="text-center">New questions cannot be asked right now.</h2>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
<div id="questions-container">
|
||||||
|
{% include 'snippets/layout/questions_list.html' %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
62
templates/snippets/layout/homepage/retrospring.html
Normal file
62
templates/snippets/layout/homepage/retrospring.html
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
{% include 'snippets/layout/description/retrospring.html' %}
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<div class="pt-3">
|
||||||
|
<div class="mb-4 card">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<span>Ask a question</span>
|
||||||
|
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#cw-collapse" aria-expanded="false" aria-controls="cw-collapse">
|
||||||
|
<i class="bi bi-plus-lg me-1"></i> Add CW
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if cfg.lockInbox == false %}
|
||||||
|
<form class="d-lg-block" hx-post="{{ url_for('api.addQuestion') }}" id="question-form" hx-target="#response-container" hx-swap="none">
|
||||||
|
<div class="collapse" id="cw-collapse">
|
||||||
|
<div class="form-group mb-2">
|
||||||
|
<input class="form-control" type="text" name="cw" id="cw" placeholder="Content warning">
|
||||||
|
<label for="cw" class="visually-hidden">Content warning</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group mb-2">
|
||||||
|
<input maxlength="{{ cfg.charLimit }}" {% 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" class="visually-hidden">Name {% if cfg.allowAnonQuestions == true %}(optional){% else %}(required){% endif %}</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group mb-2">
|
||||||
|
<textarea maxlength="{{ cfg.charLimit }}" class="form-control" required name="question" id="question" placeholder="Type your question here..."></textarea>
|
||||||
|
<label for="question" class="visually-hidden">Write your question here...</label>
|
||||||
|
<p class="text-end mt-1 small d-flex align-itemcenter justify-content-between"><span class="text-body-secondary"><i class="bi bi-markdown me-1"></i> Markdown supported</span> <span id="charCount">{{ cfg.charLimit }}</span></p>
|
||||||
|
</div>
|
||||||
|
<div class="form-group mb-2">
|
||||||
|
<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">
|
||||||
|
</div>
|
||||||
|
<div class="form-group d-grid d-lg-flex align-items-center justify-content-lg-end mt-3">
|
||||||
|
{#
|
||||||
|
<div class="form-check mb-0 w-100">
|
||||||
|
reserved for version 1.6.0 or later
|
||||||
|
<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-2" id="ask-btn">
|
||||||
|
<span class="me-1 spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
|
||||||
|
<span class="visually-hidden" role="status">Loading...</span>
|
||||||
|
Ask
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<div id="response-container"></div>
|
||||||
|
{% else %}
|
||||||
|
<br>
|
||||||
|
<h2 class="text-center">New questions cannot be asked right now.</h2>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% include 'snippets/layout/questions_list.html' %}
|
||||||
|
</div>
|
217
templates/snippets/layout/question_card.html
Normal file
217
templates/snippets/layout/question_card.html
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
{% if multipleAnswers %}
|
||||||
|
{% for answer in answers %}
|
||||||
|
<div class="card mt-3 mb-3{% if question.pinned %} border-2{% endif %}" id="question-{{ question.id }}">
|
||||||
|
<div class="position-relative">
|
||||||
|
<div class="card-header{% if question.pinned %} border-2{% endif %}">
|
||||||
|
<div class="d-flex justify-content-between align-items-center flex-wrap text-break">
|
||||||
|
<h3 class="h5 card-title mt-1 mb-1 markdown-content">
|
||||||
|
{% if question.from_who %}
|
||||||
|
{{ question.from_who | render_markdown }}
|
||||||
|
{% 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>
|
||||||
|
{% with question=question %}
|
||||||
|
{% include 'snippets/q-card-text.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if answer.cw %}
|
||||||
|
<div class="text-center mb-2 fw-bold cw-text markdown-content">{{ answer.cw | render_markdown }}</div>
|
||||||
|
<div class="collapse question-cw markdown-content" id="answer-cw-{{ answer.id }}">
|
||||||
|
{{ answer.content | render_markdown }}
|
||||||
|
</div>
|
||||||
|
<button class="z-0 cw-btn btn btn-sm btn-secondary shadow text-center w-100 sticky-bottom" type="button" data-bs-toggle="collapse" data-bs-target="#answer-cw-{{ answer.id }}" aria-expanded="false" aria-controls="answer-cw-{{ answer.id }}">
|
||||||
|
<span class="fw-medium cw-btn-text">Show content</span>
|
||||||
|
<span class="text-body-secondary cw-btn-chars">({{ len(answer.content) }} characters)</span>
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<div class="markdown-content">{{ answer.content | render_markdown }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card-footer pt-0 pb-0 ps-3 pe-1 text-body-secondary d-flex justify-content-between align-items-center{% if question.pinned %} border-2{% endif %}">
|
||||||
|
<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.pinned %}
|
||||||
|
<span class="ms-1"><i class="bi bi-pin"></i> <span class="fw-medium">Pinned</span></span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
{% if showViewQuestionBtn %}
|
||||||
|
<a href="{{ url_for('viewQuestion', question_id=question.id) }}" class="btn btn-basic 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>
|
||||||
|
{% endif %}
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-basic 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">
|
||||||
|
<li><button class="dropdown-item" onclick="copyFull(`{{ trimContent(question.content, cfg.trimContentAfter) + ' — ' + trimContent(answer.content, cfg.trimContentAfter) }} {{ 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="dropdown-item d-flex align-items-center gap-1" data-bs-toggle="modal" data-bs-target="#question-{{ 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(question.content, cfg.trimContentAfter) + ' — ' + trimContent(answer.content, cfg.trimContentAfter),safe='') }}%20{{ urllib.parse.quote(cfg.instance.fullBaseUrl + url_for('viewQuestion',question_id=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(question.content, cfg.trimContentAfter) + ' — ' + trimContent(answer.content, cfg.trimContentAfter),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" target="_blank" href="https://www.tumblr.com/widgets/share/tool?shareSource=legacy&posttype=text&title={{ urllib.parse.quote(trimContent(question.content, cfg.trimContentAfter), safe='') }}&url={{ urllib.parse.quote(cfg.instance.fullBaseUrl + url_for('viewQuestion',question_id=question.id), safe='') }}&caption=&content={{ urllib.parse.quote(trimContent(answer.content, cfg.trimContentAfter),safe='') }}%20{{ urllib.parse.quote(cfg.instance.fullBaseUrl + url_for('viewQuestion',question_id=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>
|
||||||
|
<li class="d-block d-lg-none"><button class="dropdown-item nativeShareBtn" onclick="nativeShare(`{{ trimContent(question.content, cfg.trimContentAfter) }}`, `{{ trimContent(answer.content, cfg.trimContentAfter) }}`, `{{ cfg.instance.fullBaseUrl + url_for('viewQuestion',question_id=question.id) }}`)"><i class="bi bi-share me-1"></i> Share on other apps...</button>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-basic 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({{ question.id }})"><i class="bi bi-copy me-1"></i> Copy link</button></li>
|
||||||
|
{% if logged_in %}
|
||||||
|
{% if not question.pinned %}
|
||||||
|
<li><button class="dropdown-item" hx-post="{{ url_for('api.pinQuestion', question_id=question.id) }}" hx-target="#top-response-container" hx-swap="none"><i class="bi bi-pin me-1"></i> Pin</button></li>
|
||||||
|
{% else %}
|
||||||
|
<li><button class="dropdown-item" hx-post="{{ url_for('api.unpinQuestion', question_id=question.id) }}" hx-target="#top-response-container" hx-swap="none"><i class="bi bi-pin-angle me-1"></i> Unpin</button></li>
|
||||||
|
{% endif %}
|
||||||
|
<li><button class="bg-hover-danger text-danger dropdown-item" hx-post="{{ url_for('api.returnToInbox', question_id=question.id) }}" hx-target="#top-response-container" hx-swap="none" data-deletetarget data-target="question-{{ question.id }}"><i class="bi bi-arrow-return-left me-1"></i> Return to inbox</button></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
<div class="card mt-3 mb-3{% if question.pinned %} border-2{% endif %}" id="question-{{ question.id }}">
|
||||||
|
<div class="position-relative">
|
||||||
|
<div class="card-header{% if question.pinned %} border-2{% endif %}">
|
||||||
|
<div class="d-flex justify-content-between align-items-center flex-wrap text-break">
|
||||||
|
<h3 class="h5 card-title mt-1 mb-1 markdown-content">
|
||||||
|
{% if question.from_who %}
|
||||||
|
{{ question.from_who | render_markdown }}
|
||||||
|
{% 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>
|
||||||
|
{% with question=question %}
|
||||||
|
{% include 'snippets/q-card-text.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
{% if answer.cw %}
|
||||||
|
<div class="text-center mb-2 fw-bold cw-text markdown-content">{{ answer.cw | render_markdown }}</div>
|
||||||
|
<div class="collapse question-cw markdown-content" id="answer-cw-{{ answer.id }}">
|
||||||
|
{{ answer.content | render_markdown }}
|
||||||
|
</div>
|
||||||
|
<button class="z-0 cw-btn btn btn-sm btn-secondary shadow text-center w-100 sticky-bottom" type="button" data-bs-toggle="collapse" data-bs-target="#answer-cw-{{ answer.id }}" aria-expanded="false" aria-controls="answer-cw-{{ answer.id }}">
|
||||||
|
<span class="fw-medium cw-btn-text">Show content</span>
|
||||||
|
<span class="text-body-secondary cw-btn-chars">({{ len(answer.content) }} characters)</span>
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<div class="markdown-content">{{ answer.content | render_markdown }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card-footer pt-0 pb-0 ps-3 pe-1 text-body-secondary d-flex justify-content-between align-items-center{% if question.pinned %} border-2{% endif %}">
|
||||||
|
<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.pinned %}
|
||||||
|
<span class="ms-1"><i class="bi bi-pin"></i> <span class="fw-medium">Pinned</span></span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
{% if showViewQuestionBtn %}
|
||||||
|
<a href="{{ url_for('viewQuestion', question_id=question.id) }}" class="btn btn-basic 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>
|
||||||
|
{% endif %}
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-basic 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">
|
||||||
|
<li><button class="dropdown-item" onclick="copyFull(`{{ trimContent(question.content, cfg.trimContentAfter) + ' — ' + trimContent(answer.content, cfg.trimContentAfter) }} {{ 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="dropdown-item d-flex align-items-center gap-1" data-bs-toggle="modal" data-bs-target="#question-{{ 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(question.content, cfg.trimContentAfter) + ' — ' + trimContent(answer.content, cfg.trimContentAfter),safe='') }}%20{{ urllib.parse.quote(cfg.instance.fullBaseUrl + url_for('viewQuestion',question_id=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(question.content, cfg.trimContentAfter) + ' — ' + trimContent(answer.content, cfg.trimContentAfter),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" target="_blank" href="https://www.tumblr.com/widgets/share/tool?shareSource=legacy&posttype=text&title={{ urllib.parse.quote(trimContent(question.content, cfg.trimContentAfter), safe='') }}&url={{ urllib.parse.quote(cfg.instance.fullBaseUrl + url_for('viewQuestion',question_id=question.id), safe='') }}&caption=&content={{ urllib.parse.quote(trimContent(answer.content, cfg.trimContentAfter),safe='') }}%20{{ urllib.parse.quote(cfg.instance.fullBaseUrl + url_for('viewQuestion',question_id=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>
|
||||||
|
<li class="d-block d-lg-none"><button class="dropdown-item nativeShareBtn" onclick="nativeShare(`{{ trimContent(question.content, cfg.trimContentAfter) }}`, `{{ trimContent(answer.content, cfg.trimContentAfter) }}`, `{{ cfg.instance.fullBaseUrl + url_for('viewQuestion',question_id=question.id) }}`)"><i class="bi bi-share me-1"></i> Share on other apps...</button>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-basic 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({{ question.id }})"><i class="bi bi-copy me-1"></i> Copy link</button></li>
|
||||||
|
{% if logged_in %}
|
||||||
|
{% if not question.pinned %}
|
||||||
|
<li><button class="dropdown-item" hx-post="{{ url_for('api.pinQuestion', question_id=question.id) }}" hx-target="#top-response-container" hx-swap="none"><i class="bi bi-pin me-1"></i> Pin</button></li>
|
||||||
|
{% else %}
|
||||||
|
<li><button class="dropdown-item" hx-post="{{ url_for('api.unpinQuestion', question_id=question.id) }}" hx-target="#top-response-container" hx-swap="none"><i class="bi bi-pin-angle me-1"></i> Unpin</button></li>
|
||||||
|
{% endif %}
|
||||||
|
<li><button class="bg-hover-danger text-danger dropdown-item" hx-post="{{ url_for('api.returnToInbox', question_id=question.id) }}" hx-target="#top-response-container" hx-swap="none" data-deletetarget data-target="question-{{ question.id }}"><i class="bi bi-arrow-return-left me-1"></i> Return to inbox</button></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
29
templates/snippets/layout/questions_list.html
Normal file
29
templates/snippets/layout/questions_list.html
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
{% for item in combined %}
|
||||||
|
{% with question=item.question, answers=item.answers, showViewQuestionBtn=True, multipleAnswers=True %}
|
||||||
|
{% include 'snippets/layout/question_card.html' %}
|
||||||
|
{% endwith %}
|
||||||
|
{% 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 btn-outline-secondary" 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, cfg.trimContentAfter) + ' — ' + trimContent(answer.content, cfg.trimContentAfter),safe='') }}%20{{ urllib.parse.quote(cfg.instance.fullBaseUrl + '/q/' + str(item.question.id),safe='') }}`)">Share</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}
|
14
templates/snippets/q-card-text.html
Normal file
14
templates/snippets/q-card-text.html
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
<div class="card-text markdown-content">
|
||||||
|
{% if question.cw %}
|
||||||
|
<p class="text-center mb-2 fw-bold cw-text">{{ question.cw }}</p>
|
||||||
|
<div class="collapse question-cw" id="question-cw-{{ question.id }}">
|
||||||
|
{{ question.content | render_markdown }}
|
||||||
|
</div>
|
||||||
|
<button class="z-0 cw-btn btn btn-sm btn-secondary shadow text-center w-100 sticky-bottom" type="button" data-bs-toggle="collapse" data-bs-target="#question-cw-{{ question.id }}" aria-expanded="false" aria-controls="question-cw-{{ question.id }}">
|
||||||
|
<span class="fw-medium cw-btn-text">Show content</span>
|
||||||
|
<span class="text-body-secondary cw-btn-chars">({{ len(question.content) }} characters)</span>
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
{{ question.content | render_markdown }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
Loading…
Add table
Reference in a new issue