Merge pull request #7 from richardnixondev/refactor/htmx-all-config-forms

refactor(frontend): migrate remaining 13 config forms to HTMX
This commit is contained in:
authentik Default Admin 2026-05-17 21:13:58 +01:00 committed by GitHub
commit 3802707c3d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 182 additions and 335 deletions

View file

@ -7,7 +7,7 @@ packages = ["src"]
[project]
name = "reddit-media-collector"
version = "1.2.0"
version = "1.2.1"
description = "Self-hosted media collector for Reddit with Immich integration"
readme = "README.md"
requires-python = ">=3.11"

View file

@ -2,7 +2,7 @@
from pathlib import Path
from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi import APIRouter, Depends, HTTPException, Request, Response
from fastapi.templating import Jinja2Templates
from pydantic import BaseModel
@ -19,6 +19,23 @@ def _wants_html(request: Request) -> bool:
return "text/html" in request.headers.get("accept", "")
def _empty_html_or_json(request: Request, payload: dict) -> Response | dict:
"""DELETE response: empty HTML body for HTMX (swap removes the element);
JSON for legacy callers."""
if _wants_html(request):
return Response(status_code=200)
return payload
def _tag_fragment(request: Request, prefix: str, value: str, delete_url: str, confirm: str):
"""Render the generic blacklist tag chip."""
return _templates.TemplateResponse(
request,
"partials/_tag.html",
{"prefix": prefix, "value": value, "delete_url": delete_url, "confirm": confirm},
)
# 60 mutations/minute per (IP, path) — protects config.yaml from runaway
# loops or naive scripted spam. Shared dep instance; rate_limit's bucket key
# already includes request.url.path, so each route gets its own counter.
@ -78,13 +95,13 @@ async def add_subreddit(data: SubredditCreate, request: Request):
@router.delete("/api/subreddits/{name}", dependencies=[_mut])
async def delete_subreddit(name: str):
async def delete_subreddit(name: str, request: Request):
"""Remove a subreddit."""
success = config_manager.remove_subreddit(name)
if not success:
raise HTTPException(status_code=404, detail="Subreddit not found")
return {"message": f"Subreddit '{name}' removed successfully"}
return _empty_html_or_json(request, {"message": f"Subreddit '{name}' removed successfully"})
@router.get("/api/users")
@ -94,7 +111,7 @@ async def list_users():
@router.post("/api/users", dependencies=[_mut])
async def add_user(data: UserCreate):
async def add_user(data: UserCreate, request: Request):
"""Add a new user."""
if not data.name:
raise HTTPException(status_code=400, detail="Name is required")
@ -103,17 +120,24 @@ async def add_user(data: UserCreate):
if not success:
raise HTTPException(status_code=409, detail="User already exists")
if _wants_html(request):
return _templates.TemplateResponse(
request,
"partials/_item_user.html",
{"user": {"name": data.name, "limit": data.limit}},
)
return {"message": f"User '{data.name}' added successfully"}
@router.delete("/api/users/{name}", dependencies=[_mut])
async def delete_user(name: str):
async def delete_user(name: str, request: Request):
"""Remove a user."""
success = config_manager.remove_user(name)
if not success:
raise HTTPException(status_code=404, detail="User not found")
return {"message": f"User '{name}' removed successfully"}
return _empty_html_or_json(request, {"message": f"User '{name}' removed successfully"})
# Blacklist endpoints
@ -126,7 +150,7 @@ async def get_blacklist():
@router.post("/api/blacklist/authors", dependencies=[_mut])
async def add_blacklist_author(data: BlacklistItem):
async def add_blacklist_author(data: BlacklistItem, request: Request):
"""Add an author to the blacklist."""
if not data.value:
raise HTTPException(status_code=400, detail="Value is required")
@ -135,21 +159,30 @@ async def add_blacklist_author(data: BlacklistItem):
if not success:
raise HTTPException(status_code=409, detail="Author already blacklisted")
if _wants_html(request):
return _tag_fragment(
request,
prefix="u/",
value=data.value,
delete_url=f"/api/blacklist/authors/{data.value}",
confirm=f"Remover u/{data.value} da blacklist?",
)
return {"message": f"Author '{data.value}' added to blacklist"}
@router.delete("/api/blacklist/authors/{author}", dependencies=[_mut])
async def remove_blacklist_author(author: str):
async def remove_blacklist_author(author: str, request: Request):
"""Remove an author from the blacklist."""
success = config_manager.remove_blacklist_author(author)
if not success:
raise HTTPException(status_code=404, detail="Author not found in blacklist")
return {"message": f"Author '{author}' removed from blacklist"}
return _empty_html_or_json(request, {"message": f"Author '{author}' removed from blacklist"})
@router.post("/api/blacklist/subreddits", dependencies=[_mut])
async def add_blacklist_subreddit(data: BlacklistItem):
async def add_blacklist_subreddit(data: BlacklistItem, request: Request):
"""Add a subreddit to the blacklist."""
if not data.value:
raise HTTPException(status_code=400, detail="Value is required")
@ -158,21 +191,30 @@ async def add_blacklist_subreddit(data: BlacklistItem):
if not success:
raise HTTPException(status_code=409, detail="Subreddit already blacklisted")
if _wants_html(request):
return _tag_fragment(
request,
prefix="r/",
value=data.value,
delete_url=f"/api/blacklist/subreddits/{data.value}",
confirm=f"Remover r/{data.value} da blacklist?",
)
return {"message": f"Subreddit '{data.value}' added to blacklist"}
@router.delete("/api/blacklist/subreddits/{subreddit}", dependencies=[_mut])
async def remove_blacklist_subreddit(subreddit: str):
async def remove_blacklist_subreddit(subreddit: str, request: Request):
"""Remove a subreddit from the blacklist."""
success = config_manager.remove_blacklist_subreddit(subreddit)
if not success:
raise HTTPException(status_code=404, detail="Subreddit not found in blacklist")
return {"message": f"Subreddit '{subreddit}' removed from blacklist"}
return _empty_html_or_json(request, {"message": f"Subreddit '{subreddit}' removed from blacklist"})
@router.post("/api/blacklist/keywords", dependencies=[_mut])
async def add_blacklist_keyword(data: BlacklistItem):
async def add_blacklist_keyword(data: BlacklistItem, request: Request):
"""Add a title keyword to the blacklist."""
if not data.value:
raise HTTPException(status_code=400, detail="Value is required")
@ -181,21 +223,30 @@ async def add_blacklist_keyword(data: BlacklistItem):
if not success:
raise HTTPException(status_code=409, detail="Keyword already blacklisted")
if _wants_html(request):
return _tag_fragment(
request,
prefix="",
value=data.value,
delete_url=f"/api/blacklist/keywords/{data.value}",
confirm=f'Remover "{data.value}" da blacklist?',
)
return {"message": f"Keyword '{data.value}' added to blacklist"}
@router.delete("/api/blacklist/keywords/{keyword:path}", dependencies=[_mut])
async def remove_blacklist_keyword(keyword: str):
async def remove_blacklist_keyword(keyword: str, request: Request):
"""Remove a title keyword from the blacklist."""
success = config_manager.remove_blacklist_keyword(keyword)
if not success:
raise HTTPException(status_code=404, detail="Keyword not found in blacklist")
return {"message": f"Keyword '{keyword}' removed from blacklist"}
return _empty_html_or_json(request, {"message": f"Keyword '{keyword}' removed from blacklist"})
@router.post("/api/blacklist/domains", dependencies=[_mut])
async def add_blacklist_domain(data: BlacklistItem):
async def add_blacklist_domain(data: BlacklistItem, request: Request):
"""Add a domain to the blacklist."""
if not data.value:
raise HTTPException(status_code=400, detail="Value is required")
@ -204,17 +255,26 @@ async def add_blacklist_domain(data: BlacklistItem):
if not success:
raise HTTPException(status_code=409, detail="Domain already blacklisted")
if _wants_html(request):
return _tag_fragment(
request,
prefix="",
value=data.value,
delete_url=f"/api/blacklist/domains/{data.value}",
confirm=f'Remover "{data.value}" da blacklist?',
)
return {"message": f"Domain '{data.value}' added to blacklist"}
@router.delete("/api/blacklist/domains/{domain:path}", dependencies=[_mut])
async def remove_blacklist_domain(domain: str):
async def remove_blacklist_domain(domain: str, request: Request):
"""Remove a domain from the blacklist."""
success = config_manager.remove_blacklist_domain(domain)
if not success:
raise HTTPException(status_code=404, detail="Domain not found in blacklist")
return {"message": f"Domain '{domain}' removed from blacklist"}
return _empty_html_or_json(request, {"message": f"Domain '{domain}' removed from blacklist"})
# Settings endpoints

View file

@ -238,255 +238,9 @@ async function loadRecentDownloads() {
}
}
// Form handlers — POST attaches via addEventListener at DOMContentLoaded
function attachFormHandlers() {
const subredditForm = document.getElementById('subreddit-form');
if (subredditForm) subredditForm.addEventListener('submit', handleSubredditSubmit);
const userForm = document.getElementById('user-form');
if (userForm) userForm.addEventListener('submit', handleUserSubmit);
const authorBlacklistForm = document.getElementById('author-blacklist-form');
if (authorBlacklistForm) authorBlacklistForm.addEventListener('submit', handleAuthorBlacklistSubmit);
const keywordBlacklistForm = document.getElementById('keyword-blacklist-form');
if (keywordBlacklistForm) keywordBlacklistForm.addEventListener('submit', handleKeywordBlacklistSubmit);
const domainBlacklistForm = document.getElementById('domain-blacklist-form');
if (domainBlacklistForm) domainBlacklistForm.addEventListener('submit', handleDomainBlacklistSubmit);
const subredditBlacklistForm = document.getElementById('subreddit-blacklist-form');
if (subredditBlacklistForm) subredditBlacklistForm.addEventListener('submit', handleSubredditBlacklistSubmit);
}
async function handleSubredditSubmit(e) {
e.preventDefault();
const name = document.getElementById('sub-name').value.trim();
const limit = parseInt(document.getElementById('sub-limit').value);
const sort = document.getElementById('sub-sort').value;
try {
const response = await fetch('/api/subreddits', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name, limit, sort})
});
if (response.ok) {
showAlert('Subreddit adicionado!', 'success');
location.reload();
} else {
const data = await response.json();
showAlert(data.detail || 'Erro ao adicionar', 'error');
}
} catch (err) {
showApiError(err, 'Erro de conexao');
}
}
async function handleUserSubmit(e) {
e.preventDefault();
const name = document.getElementById('user-name').value.trim();
const limit = parseInt(document.getElementById('user-limit').value);
try {
const response = await fetch('/api/users', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name, limit})
});
if (response.ok) {
showAlert('User adicionado!', 'success');
location.reload();
} else {
const data = await response.json();
showAlert(data.detail || 'Erro ao adicionar', 'error');
}
} catch (err) {
showApiError(err, 'Erro de conexao');
}
}
async function removeSubreddit(name) {
if (!confirm(`Remover r/${name}?`)) return;
try {
const response = await fetch(`/api/subreddits/${encodeURIComponent(name)}`, {method: 'DELETE'});
if (response.ok) {
showAlert('Subreddit removido!', 'success');
location.reload();
} else {
const data = await response.json();
showAlert(data.detail || 'Erro ao remover', 'error');
}
} catch (err) {
showApiError(err, 'Erro de conexao');
}
}
async function removeUser(name) {
if (!confirm(`Remover u/${name}?`)) return;
try {
const response = await fetch(`/api/users/${encodeURIComponent(name)}`, {method: 'DELETE'});
if (response.ok) {
showAlert('User removido!', 'success');
location.reload();
} else {
const data = await response.json();
showAlert(data.detail || 'Erro ao remover', 'error');
}
} catch (err) {
showApiError(err, 'Erro de conexao');
}
}
async function handleAuthorBlacklistSubmit(e) {
e.preventDefault();
const value = document.getElementById('blacklist-author').value.trim();
if (!value) return;
try {
const response = await fetch('/api/blacklist/authors', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({value})
});
if (response.ok) {
showAlert('Autor adicionado a blacklist!', 'success');
location.reload();
} else {
const data = await response.json();
showAlert(data.detail || 'Erro ao adicionar', 'error');
}
} catch (err) {
showApiError(err, 'Erro de conexao');
}
}
async function handleKeywordBlacklistSubmit(e) {
e.preventDefault();
const value = document.getElementById('blacklist-keyword').value.trim();
if (!value) return;
try {
const response = await fetch('/api/blacklist/keywords', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({value})
});
if (response.ok) {
showAlert('Keyword adicionada a blacklist!', 'success');
location.reload();
} else {
const data = await response.json();
showAlert(data.detail || 'Erro ao adicionar', 'error');
}
} catch (err) {
showApiError(err, 'Erro de conexao');
}
}
async function handleDomainBlacklistSubmit(e) {
e.preventDefault();
const value = document.getElementById('blacklist-domain').value.trim();
if (!value) return;
try {
const response = await fetch('/api/blacklist/domains', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({value})
});
if (response.ok) {
showAlert('Dominio adicionado a blacklist!', 'success');
location.reload();
} else {
const data = await response.json();
showAlert(data.detail || 'Erro ao adicionar', 'error');
}
} catch (err) {
showApiError(err, 'Erro de conexao');
}
}
async function handleSubredditBlacklistSubmit(e) {
e.preventDefault();
const value = document.getElementById('blacklist-subreddit').value.trim();
if (!value) return;
try {
const response = await fetch('/api/blacklist/subreddits', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({value})
});
if (response.ok) {
showAlert('Subreddit adicionado a blacklist!', 'success');
location.reload();
} else {
const data = await response.json();
showAlert(data.detail || 'Erro ao adicionar', 'error');
}
} catch (err) {
showApiError(err, 'Erro de conexao');
}
}
async function removeBlacklistAuthor(author) {
if (!confirm(`Remover u/${author} da blacklist?`)) return;
try {
const response = await fetch(`/api/blacklist/authors/${encodeURIComponent(author)}`, {method: 'DELETE'});
if (response.ok) {
showAlert('Autor removido da blacklist!', 'success');
location.reload();
} else {
const data = await response.json();
showAlert(data.detail || 'Erro ao remover', 'error');
}
} catch (err) {
showApiError(err, 'Erro de conexao');
}
}
async function removeBlacklistSubreddit(subreddit) {
if (!confirm(`Remover r/${subreddit} da blacklist?`)) return;
try {
const response = await fetch(`/api/blacklist/subreddits/${encodeURIComponent(subreddit)}`, {method: 'DELETE'});
if (response.ok) {
showAlert('Subreddit removido da blacklist!', 'success');
location.reload();
} else {
const data = await response.json();
showAlert(data.detail || 'Erro ao remover', 'error');
}
} catch (err) {
showApiError(err, 'Erro de conexao');
}
}
async function removeBlacklistKeyword(keyword) {
if (!confirm(`Remover "${keyword}" da blacklist?`)) return;
try {
const response = await fetch(`/api/blacklist/keywords/${encodeURIComponent(keyword)}`, {method: 'DELETE'});
if (response.ok) {
showAlert('Keyword removida da blacklist!', 'success');
location.reload();
} else {
const data = await response.json();
showAlert(data.detail || 'Erro ao remover', 'error');
}
} catch (err) {
showApiError(err, 'Erro de conexao');
}
}
async function removeBlacklistDomain(domain) {
if (!confirm(`Remover "${domain}" da blacklist?`)) return;
try {
const response = await fetch(`/api/blacklist/domains/${encodeURIComponent(domain)}`, {method: 'DELETE'});
if (response.ok) {
showAlert('Dominio removido da blacklist!', 'success');
location.reload();
} else {
const data = await response.json();
showAlert(data.detail || 'Erro ao remover', 'error');
}
} catch (err) {
showApiError(err, 'Erro de conexao');
}
}
// Form handlers for config (subreddits, users, blacklist) moved to HTMX —
// markup in partials/tab_sources.html. JS keeps only handlers for forms
// that still need it (settings, scheduler, individual collection).
async function cleanupBlacklist() {
const btn = document.getElementById('cleanup-blacklist-btn');
@ -1640,7 +1394,6 @@ document.addEventListener('visibilitychange', () => {
// Boot
document.addEventListener('DOMContentLoaded', () => {
attachFormHandlers();
startStatusPolling();
updateStatus();
loadStats();

View file

@ -3,5 +3,10 @@
<div class="list-item-name">r/{{ sub.name }}</div>
<div class="list-item-details">Limite: {{ sub.limit }} | Ordem: {{ sub.sort or 'new' }}</div>
</div>
<button class="btn-remove" onclick="removeSubreddit('{{ sub.name }}')">Remover</button>
<button class="btn-remove"
hx-delete="/api/subreddits/{{ sub.name }}"
hx-headers='{"Accept": "text/html"}'
hx-target="closest .list-item"
hx-swap="outerHTML"
hx-confirm="Remover r/{{ sub.name }}?">Remover</button>
</div>

View file

@ -0,0 +1,12 @@
<div class="list-item" data-name="{{ user.name }}">
<div class="list-item-info">
<div class="list-item-name">u/{{ user.name }}</div>
<div class="list-item-details">Limite: {{ user.limit }}</div>
</div>
<button class="btn-remove"
hx-delete="/api/users/{{ user.name }}"
hx-headers='{"Accept": "text/html"}'
hx-target="closest .list-item"
hx-swap="outerHTML"
hx-confirm="Remover u/{{ user.name }}?">Remover</button>
</div>

View file

@ -0,0 +1,15 @@
{# Generic blacklist tag chip. Params:
- prefix: "u/", "r/" or "" (for keywords/domains)
- value: tag value (e.g. "spammer", "promo")
- delete_url: full URL of the DELETE endpoint
- confirm: text to show in hx-confirm
#}
<div class="tag">
<span>{{ prefix }}{{ value }}</span>
<button class="tag-remove"
hx-delete="{{ delete_url }}"
hx-headers='{"Accept": "text/html"}'
hx-target="closest .tag"
hx-swap="outerHTML"
hx-confirm="{{ confirm }}">&times;</button>
</div>

View file

@ -31,51 +31,41 @@
</form>
<div class="list" id="subreddit-list">
{% if subreddits %}
{% for sub in subreddits %}
<div class="list-item" data-name="{{ sub.name }}">
<div class="list-item-info">
<div class="list-item-name">r/{{ sub.name }}</div>
<div class="list-item-details">Limite: {{ sub.limit }} | Ordem: {{ sub.sort or 'new' }}</div>
</div>
<button class="btn-remove" onclick="removeSubreddit('{{ sub.name }}')">Remover</button>
</div>
{% endfor %}
{% for sub in subreddits %}
{% include 'partials/_item_subreddit.html' %}
{% else %}
<div class="empty">Nenhum subreddit configurado</div>
{% endif %}
{% endfor %}
</div>
</div>
<div class="section">
<h2>Users ({{ users|length }})</h2>
<form id="user-form" class="inline-form-user">
<form id="user-form" class="inline-form-user"
hx-post="/api/users"
hx-ext="json-enc"
hx-headers='{"Accept": "text/html"}'
hx-target="#user-list"
hx-swap="beforeend"
hx-on::after-request="if(event.detail.successful){this.reset();window.showAlert&&showAlert('User adicionado!','success');}else{window.showApiError&&showApiError(event.detail.xhr,'Erro ao adicionar');}">
<div class="form-group">
<label>Username</label>
<input type="text" id="user-name" placeholder="ex: username" required>
<input type="text" name="name" id="user-name" placeholder="ex: username" required>
</div>
<div class="form-group">
<label>Limite</label>
<input type="number" id="user-limit" value="100" min="1" max="500">
<input type="number" name="limit" id="user-limit" value="100" min="1" max="500">
</div>
<button type="submit" class="btn-add">Adicionar</button>
</form>
<div class="list" id="user-list">
{% if users %}
{% for user in users %}
<div class="list-item" data-name="{{ user.name }}">
<div class="list-item-info">
<div class="list-item-name">u/{{ user.name }}</div>
<div class="list-item-details">Limite: {{ user.limit }}</div>
</div>
<button class="btn-remove" onclick="removeUser('{{ user.name }}')">Remover</button>
</div>
{% endfor %}
{% for user in users %}
{% include 'partials/_item_user.html' %}
{% else %}
<div class="empty">Nenhum user configurado</div>
{% endif %}
{% endfor %}
</div>
</div>
</div>
@ -90,89 +80,101 @@
<div class="blacklist-grid" style="grid-template-columns: repeat(2, 1fr);">
<div class="blacklist-card">
<h3>Autores Bloqueados ({{ blacklist.authors|length }})</h3>
<form id="author-blacklist-form" class="inline-form-simple">
<form id="author-blacklist-form" class="inline-form-simple"
hx-post="/api/blacklist/authors"
hx-ext="json-enc"
hx-headers='{"Accept": "text/html"}'
hx-target="#author-tags"
hx-swap="beforeend"
hx-on::after-request="if(event.detail.successful){this.reset();window.showAlert&&showAlert('Autor adicionado a blacklist!','success');}else{window.showApiError&&showApiError(event.detail.xhr,'Erro ao adicionar');}">
<div class="form-group">
<input type="text" id="blacklist-author" placeholder="username">
<input type="text" name="value" id="blacklist-author" placeholder="username">
</div>
<button type="submit" class="btn-add">Adicionar</button>
</form>
<div class="tag-list" id="author-tags">
{% if blacklist.authors %}
{% for author in blacklist.authors %}
<div class="tag">
<span>u/{{ author }}</span>
<button class="tag-remove" onclick="removeBlacklistAuthor('{{ author }}')">&times;</button>
</div>
{% endfor %}
{% for author in blacklist.authors %}
{% with prefix='u/', value=author, delete_url='/api/blacklist/authors/' ~ author, confirm='Remover u/' ~ author ~ ' da blacklist?' %}
{% include 'partials/_tag.html' %}
{% endwith %}
{% else %}
<span class="empty" style="padding: 10px 0;">Nenhum autor bloqueado</span>
{% endif %}
{% endfor %}
</div>
</div>
<div class="blacklist-card">
<h3>Subreddits Bloqueados ({{ blacklist.subreddits|length if blacklist.subreddits else 0 }})</h3>
<form id="subreddit-blacklist-form" class="inline-form-simple">
<form id="subreddit-blacklist-form" class="inline-form-simple"
hx-post="/api/blacklist/subreddits"
hx-ext="json-enc"
hx-headers='{"Accept": "text/html"}'
hx-target="#subreddit-tags"
hx-swap="beforeend"
hx-on::after-request="if(event.detail.successful){this.reset();window.showAlert&&showAlert('Subreddit adicionado a blacklist!','success');}else{window.showApiError&&showApiError(event.detail.xhr,'Erro ao adicionar');}">
<div class="form-group">
<input type="text" id="blacklist-subreddit" placeholder="ex: pics">
<input type="text" name="value" id="blacklist-subreddit" placeholder="ex: pics">
</div>
<button type="submit" class="btn-add">Adicionar</button>
</form>
<div class="tag-list" id="subreddit-tags">
{% if blacklist.subreddits %}
{% for sub in blacklist.subreddits %}
<div class="tag">
<span>r/{{ sub }}</span>
<button class="tag-remove" onclick="removeBlacklistSubreddit('{{ sub }}')">&times;</button>
</div>
{% endfor %}
{% for sub in blacklist.subreddits or [] %}
{% with prefix='r/', value=sub, delete_url='/api/blacklist/subreddits/' ~ sub, confirm='Remover r/' ~ sub ~ ' da blacklist?' %}
{% include 'partials/_tag.html' %}
{% endwith %}
{% else %}
<span class="empty" style="padding: 10px 0;">Nenhum subreddit bloqueado</span>
{% endif %}
{% endfor %}
</div>
</div>
<div class="blacklist-card">
<h3>Palavras no Titulo ({{ blacklist.title_keywords|length }})</h3>
<form id="keyword-blacklist-form" class="inline-form-simple">
<form id="keyword-blacklist-form" class="inline-form-simple"
hx-post="/api/blacklist/keywords"
hx-ext="json-enc"
hx-headers='{"Accept": "text/html"}'
hx-target="#keyword-tags"
hx-swap="beforeend"
hx-on::after-request="if(event.detail.successful){this.reset();window.showAlert&&showAlert('Keyword adicionada a blacklist!','success');}else{window.showApiError&&showApiError(event.detail.xhr,'Erro ao adicionar');}">
<div class="form-group">
<input type="text" id="blacklist-keyword" placeholder="ex: selling, promo">
<input type="text" name="value" id="blacklist-keyword" placeholder="ex: selling, promo">
</div>
<button type="submit" class="btn-add">Adicionar</button>
</form>
<div class="tag-list" id="keyword-tags">
{% if blacklist.title_keywords %}
{% for keyword in blacklist.title_keywords %}
<div class="tag">
<span>{{ keyword }}</span>
<button class="tag-remove" onclick="removeBlacklistKeyword('{{ keyword }}')">&times;</button>
</div>
{% endfor %}
{% for keyword in blacklist.title_keywords %}
{% with prefix='', value=keyword, delete_url='/api/blacklist/keywords/' ~ keyword, confirm='Remover "' ~ keyword ~ '" da blacklist?' %}
{% include 'partials/_tag.html' %}
{% endwith %}
{% else %}
<span class="empty" style="padding: 10px 0;">Nenhuma palavra bloqueada</span>
{% endif %}
{% endfor %}
</div>
</div>
<div class="blacklist-card">
<h3>Dominios Bloqueados ({{ blacklist.domains|length }})</h3>
<form id="domain-blacklist-form" class="inline-form-simple">
<form id="domain-blacklist-form" class="inline-form-simple"
hx-post="/api/blacklist/domains"
hx-ext="json-enc"
hx-headers='{"Accept": "text/html"}'
hx-target="#domain-tags"
hx-swap="beforeend"
hx-on::after-request="if(event.detail.successful){this.reset();window.showAlert&&showAlert('Dominio adicionado a blacklist!','success');}else{window.showApiError&&showApiError(event.detail.xhr,'Erro ao adicionar');}">
<div class="form-group">
<input type="text" id="blacklist-domain" placeholder="ex: linktr.ee">
<input type="text" name="value" id="blacklist-domain" placeholder="ex: linktr.ee">
</div>
<button type="submit" class="btn-add">Adicionar</button>
</form>
<div class="tag-list" id="domain-tags">
{% if blacklist.domains %}
{% for domain in blacklist.domains %}
<div class="tag">
<span>{{ domain }}</span>
<button class="tag-remove" onclick="removeBlacklistDomain('{{ domain }}')">&times;</button>
</div>
{% endfor %}
{% for domain in blacklist.domains %}
{% with prefix='', value=domain, delete_url='/api/blacklist/domains/' ~ domain, confirm='Remover "' ~ domain ~ '" da blacklist?' %}
{% include 'partials/_tag.html' %}
{% endwith %}
{% else %}
<span class="empty" style="padding: 10px 0;">Nenhum dominio bloqueado</span>
{% endif %}
{% endfor %}
</div>
</div>
</div>