New Entry functionality
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
|
||||
(() => {
|
||||
const loginButton = document.querySelector('#auth-login-button');
|
||||
const newEntryButton = document.querySelector('#new-entry-button');
|
||||
const profileLink = document.querySelector('#auth-profile-link');
|
||||
const labelsLink = document.querySelector('#auth-labels-link');
|
||||
const adminLink = document.querySelector('#auth-admin-link');
|
||||
@@ -24,6 +25,7 @@
|
||||
|
||||
function showSignedOut() {
|
||||
loginButton.classList.remove('hidden');
|
||||
if (newEntryButton) newEntryButton.classList.add('hidden');
|
||||
profileLink.classList.add('hidden');
|
||||
labelsLink.classList.add('hidden');
|
||||
adminLink.classList.add('hidden');
|
||||
@@ -33,6 +35,7 @@
|
||||
|
||||
function showSignedIn(user) {
|
||||
loginButton.classList.add('hidden');
|
||||
if (newEntryButton) newEntryButton.classList.remove('hidden');
|
||||
profileLink.classList.remove('hidden');
|
||||
labelsLink.classList.remove('hidden');
|
||||
adminLink.classList.toggle('hidden', !user.is_admin);
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
// Copyright © 2026 Olaf Kolkman
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
(() => {
|
||||
const entryForm = document.getElementById('entry-form');
|
||||
const authRequired = document.getElementById('auth-required');
|
||||
const urlInput = document.getElementById('url-input');
|
||||
const titleInput = document.getElementById('title-input');
|
||||
const commentInput = document.getElementById('comment-input');
|
||||
const newTagsInput = document.getElementById('new-tags-input');
|
||||
const existingTagsEl = document.getElementById('existing-tags');
|
||||
const mastodonEnabledCheckbox = document.getElementById('mastodon-enabled');
|
||||
const scrapeButton = document.getElementById('scrape-button');
|
||||
const scrapeStatus = document.getElementById('scrape-status');
|
||||
const submitButton = document.getElementById('submit-button');
|
||||
const submitStatus = document.getElementById('submit-status');
|
||||
|
||||
const token = localStorage.getItem('linklogAccessToken');
|
||||
let availableTags = [];
|
||||
let selectedTags = new Set();
|
||||
let currentUser = null;
|
||||
|
||||
// Utility function to add status messages
|
||||
function setStatus(statusEl, message, isError = false) {
|
||||
statusEl.textContent = message;
|
||||
statusEl.className = `status ${isError ? 'error' : 'success'}`;
|
||||
statusEl.classList.remove('hidden');
|
||||
if (!isError) {
|
||||
setTimeout(() => statusEl.classList.add('hidden'), 4000);
|
||||
}
|
||||
}
|
||||
|
||||
// Check authentication
|
||||
if (!token) {
|
||||
authRequired.classList.remove('hidden');
|
||||
entryForm.classList.add('hidden');
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify token is still valid
|
||||
fetch('/api/auth/me', { headers: { Authorization: `Bearer ${token}` } })
|
||||
.then((response) => {
|
||||
if (!response.ok) throw new Error('Not authenticated');
|
||||
return response.json();
|
||||
})
|
||||
.then((user) => {
|
||||
currentUser = user;
|
||||
entryForm.classList.remove('hidden');
|
||||
loadTags();
|
||||
})
|
||||
.catch(() => {
|
||||
localStorage.removeItem('linklogAccessToken');
|
||||
authRequired.classList.remove('hidden');
|
||||
entryForm.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Load available tags
|
||||
async function loadTags() {
|
||||
try {
|
||||
const response = await fetch('/api/tags');
|
||||
if (!response.ok) throw new Error('Could not load tags');
|
||||
availableTags = await response.json();
|
||||
renderTags();
|
||||
} catch (error) {
|
||||
console.error('Error loading tags:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Render tag checkboxes
|
||||
function renderTags() {
|
||||
existingTagsEl.innerHTML = '';
|
||||
availableTags.forEach((tag) => {
|
||||
const label = document.createElement('label');
|
||||
label.className = 'tag-checkbox';
|
||||
|
||||
const checkbox = document.createElement('input');
|
||||
checkbox.type = 'checkbox';
|
||||
checkbox.value = tag;
|
||||
checkbox.checked = selectedTags.has(tag);
|
||||
checkbox.addEventListener('change', () => {
|
||||
if (checkbox.checked) {
|
||||
selectedTags.add(tag);
|
||||
} else {
|
||||
selectedTags.delete(tag);
|
||||
}
|
||||
});
|
||||
|
||||
label.appendChild(checkbox);
|
||||
label.append(` ${tag}`);
|
||||
existingTagsEl.appendChild(label);
|
||||
});
|
||||
}
|
||||
|
||||
// Scrape URL for title
|
||||
scrapeButton.addEventListener('click', async (e) => {
|
||||
e.preventDefault();
|
||||
const url = urlInput.value.trim();
|
||||
if (!url) {
|
||||
setStatus(scrapeStatus, 'Please enter a URL', true);
|
||||
return;
|
||||
}
|
||||
|
||||
scrapeButton.disabled = true;
|
||||
setStatus(scrapeStatus, 'Scraping...', false);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/scrape?url=${encodeURIComponent(url)}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
localStorage.removeItem('linklogAccessToken');
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (data.title) {
|
||||
titleInput.value = data.title;
|
||||
setStatus(scrapeStatus, 'Title loaded!', false);
|
||||
} else {
|
||||
setStatus(scrapeStatus, 'No title found', true);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Scrape error:', error);
|
||||
setStatus(scrapeStatus, `Error: ${error.message}`, true);
|
||||
} finally {
|
||||
scrapeButton.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Handle form submission
|
||||
entryForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const url = urlInput.value.trim();
|
||||
const title = titleInput.value.trim();
|
||||
const comment = commentInput.value.trim();
|
||||
const newTags = newTagsInput.value.trim();
|
||||
|
||||
if (!url || !title) {
|
||||
setStatus(submitStatus, 'URL and title are required', true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Combine selected tags and new tags
|
||||
const tags = Array.from(selectedTags);
|
||||
if (newTags) {
|
||||
const newTagsList = newTags
|
||||
.split(',')
|
||||
.map((t) => t.trim())
|
||||
.filter((t) => t);
|
||||
tags.push(...newTagsList);
|
||||
}
|
||||
|
||||
submitButton.disabled = true;
|
||||
setStatus(submitStatus, 'Saving...', false);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/links', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
title,
|
||||
url,
|
||||
comment,
|
||||
tags,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
localStorage.removeItem('linklogAccessToken');
|
||||
location.reload();
|
||||
return;
|
||||
}
|
||||
const error = await response.json().catch(() => ({}));
|
||||
throw new Error(error.detail || `HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
setStatus(submitStatus, `Link saved to LinkLog${data.duplicate ? ' (updated)' : ''}!`, false);
|
||||
|
||||
// Redirect to user page after 1 second
|
||||
if (currentUser) {
|
||||
setTimeout(() => {
|
||||
window.location.href = `/${encodeURIComponent(currentUser.username)}/`;
|
||||
}, 1000);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Submit error:', error);
|
||||
setStatus(submitStatus, `Error: ${error.message}`, true);
|
||||
} finally {
|
||||
submitButton.disabled = false;
|
||||
}
|
||||
});
|
||||
})();
|
||||
@@ -248,6 +248,31 @@ body::selection {
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.new-entry-button {
|
||||
padding: 10px 13px;
|
||||
border: 1px solid var(--mauve);
|
||||
border-radius: 7px;
|
||||
background: var(--mauve);
|
||||
color: var(--crust);
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.new-entry-button:hover {
|
||||
background: var(--lavender);
|
||||
border-color: var(--lavender);
|
||||
}
|
||||
|
||||
.new-entry-button a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.new-entry-button.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.auth-menu {
|
||||
position: absolute;
|
||||
z-index: 30;
|
||||
@@ -768,6 +793,221 @@ button:disabled {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
/* Entry form styling */
|
||||
.entry-form {
|
||||
max-width: 600px;
|
||||
margin: 0 auto;
|
||||
display: grid;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.entry-form.hidden,
|
||||
#auth-required.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#auth-required {
|
||||
max-width: 600px;
|
||||
margin: 40px auto;
|
||||
padding: 16px;
|
||||
background: var(--surface-0);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 12px;
|
||||
border-left: 4px solid var(--red);
|
||||
}
|
||||
|
||||
#auth-required p {
|
||||
margin: 0;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
#auth-required a {
|
||||
color: var(--lavender);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#auth-required a:hover {
|
||||
color: var(--mauve);
|
||||
}
|
||||
|
||||
.form-section {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.form-section label {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.form-section > legend {
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
font-size: 0.95rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.form-section input[type='url'],
|
||||
.form-section input[type='text'],
|
||||
.form-section textarea {
|
||||
padding: 10px 12px;
|
||||
background: var(--mantle);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
font-family: inherit;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.form-section textarea {
|
||||
resize: vertical;
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.form-section input[type='url']:focus,
|
||||
.form-section input[type='text']:focus,
|
||||
.form-section textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--lavender);
|
||||
box-shadow: 0 0 0 3px rgba(180, 190, 254, 0.1);
|
||||
}
|
||||
|
||||
.tag-options {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 10px;
|
||||
padding: 12px;
|
||||
background: var(--mantle);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.tag-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: var(--subtext);
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tag-checkbox input {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-section fieldset {
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.form-section fieldset legend {
|
||||
margin: 0;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.form-section fieldset label {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.form-section fieldset input[type='text'] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-section fieldset input[type='checkbox'] {
|
||||
width: auto;
|
||||
margin-right: 6px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.form-actions button,
|
||||
.form-actions a {
|
||||
padding: 10px 16px;
|
||||
border-radius: 8px;
|
||||
font-weight: 600;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.form-actions button[type='submit'],
|
||||
#scrape-button:not(:disabled) {
|
||||
background: var(--mauve);
|
||||
border: 1px solid var(--mauve);
|
||||
color: var(--crust);
|
||||
}
|
||||
|
||||
.form-actions button[type='submit']:hover:not(:disabled) {
|
||||
background: var(--lavender);
|
||||
border-color: var(--lavender);
|
||||
}
|
||||
|
||||
.form-actions button[type='submit']:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#scrape-button {
|
||||
background: var(--surface-1);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
#scrape-button:not(:disabled):hover {
|
||||
background: var(--surface-2);
|
||||
border-color: var(--text);
|
||||
}
|
||||
|
||||
#scrape-button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.form-actions a {
|
||||
background: var(--surface-1);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.form-actions a:hover {
|
||||
background: var(--surface-2);
|
||||
border-color: var(--text);
|
||||
}
|
||||
|
||||
.status {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.status.success {
|
||||
background: rgba(131, 165, 152, 0.2);
|
||||
border: 1px solid var(--teal);
|
||||
color: var(--teal);
|
||||
}
|
||||
|
||||
.status.error {
|
||||
background: rgba(243, 139, 168, 0.2);
|
||||
border: 1px solid var(--red);
|
||||
color: var(--red);
|
||||
}
|
||||
|
||||
.status.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.container {
|
||||
padding: 0 14px;
|
||||
@@ -815,6 +1055,11 @@ button:disabled {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.new-entry-button {
|
||||
padding: 8px 11px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.logout-button {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<p>A quiet place for the links worth keeping.</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button id="new-entry-button" class="new-entry-button hidden" type="button"><a href="/new-entry">New entry</a></button>
|
||||
<button class="menu-toggle" type="button" aria-expanded="false" aria-controls="auth-menu">Menu</button>
|
||||
<nav id="auth-menu" class="auth-menu hidden" aria-label="Account menu">
|
||||
<a id="auth-home-link" href="/">Home</a>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<p>Manage users and plugin configuration</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button id="new-entry-button" class="new-entry-button hidden" type="button"><a href="/new-entry">New entry</a></button>
|
||||
<button class="menu-toggle" type="button" aria-expanded="false" aria-controls="auth-menu">Menu</button>
|
||||
<nav id="auth-menu" class="auth-menu hidden" aria-label="Account menu">
|
||||
<a id="auth-home-link" href="/">Home</a>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
</div>
|
||||
<div class="header-tools">
|
||||
<div class="header-actions">
|
||||
<button id="new-entry-button" class="new-entry-button hidden" type="button"><a href="/new-entry">New entry</a></button>
|
||||
<button class="menu-toggle" type="button" aria-expanded="false" aria-controls="auth-menu">Menu</button>
|
||||
<nav id="auth-menu" class="auth-menu hidden" aria-label="Account menu">
|
||||
<a id="auth-home-link" href="/">Home</a>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<p>Manage your link labels</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button id="new-entry-button" class="new-entry-button hidden" type="button"><a href="/new-entry">New entry</a></button>
|
||||
<button class="menu-toggle" type="button" aria-expanded="false" aria-controls="auth-menu">Menu</button>
|
||||
<nav id="auth-menu" class="auth-menu hidden" aria-label="Account menu">
|
||||
<a id="auth-home-link" href="/">Home</a>
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
<p>Access your LinkLog settings</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button id="new-entry-button" class="new-entry-button hidden" type="button"><a href="/new-entry">New entry</a></button>
|
||||
<button class="menu-toggle" type="button" aria-expanded="false" aria-controls="auth-menu">Menu</button>
|
||||
<nav id="auth-menu" class="auth-menu hidden" aria-label="Account menu">
|
||||
<a id="auth-home-link" href="/">Home</a>
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
<!DOCTYPE html>
|
||||
<!-- Copyright © 2026 Olaf Kolkman -->
|
||||
<!-- SPDX-License-Identifier: GPL-3.0-or-later -->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>New Entry - LinkLog</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="stylesheet" href="/static/style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="site-header">
|
||||
<div class="container">
|
||||
<div class="header-row">
|
||||
<div>
|
||||
<img class="site-logo" src="/static/logo.svg" alt="LinkLog" />
|
||||
<p>New Entry</p>
|
||||
</div>
|
||||
<div class="header-tools">
|
||||
<div class="header-actions">
|
||||
<button id="new-entry-button" class="new-entry-button hidden" type="button"><a href="/new-entry">New entry</a></button>
|
||||
<button class="menu-toggle" type="button" aria-expanded="false" aria-controls="auth-menu">Menu</button>
|
||||
<nav id="auth-menu" class="auth-menu hidden" aria-label="Account menu">
|
||||
<a id="auth-home-link" href="/">Home</a>
|
||||
<a id="auth-about-link" href="/about">About</a>
|
||||
<a id="auth-login-button" href="/login">Sign in</a>
|
||||
<a id="auth-profile-link" class="hidden" href="/profile">Profile</a>
|
||||
<a id="auth-labels-link" class="hidden" href="/labels">Labels</a>
|
||||
<a id="auth-admin-link" class="hidden" href="/admin">Admin</a>
|
||||
<div id="auth-session" class="auth-session hidden">
|
||||
<a id="auth-username" class="user-name" href="/"></a>
|
||||
</div>
|
||||
<button id="logout-button" class="logout-button hidden" type="button">Sign out</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<div id="auth-required" class="error-message hidden">
|
||||
<p>You must be signed in to create a new entry. <a href="/login">Sign in here</a>.</p>
|
||||
</div>
|
||||
|
||||
<form id="entry-form" class="entry-form hidden">
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<span>URL</span>
|
||||
<input id="url-input" name="url" type="url" required placeholder="https://example.com" />
|
||||
</label>
|
||||
<button id="scrape-button" class="secondary" type="button">Auto-fill Title</button>
|
||||
<div id="scrape-status" class="status hidden" aria-live="polite"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<span>Title</span>
|
||||
<input id="title-input" name="title" type="text" required />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="form-section">
|
||||
<label>
|
||||
<span>Comment</span>
|
||||
<textarea id="comment-input" name="comment" rows="4" placeholder="Optional comment about this link"></textarea>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<fieldset class="form-section">
|
||||
<legend>Tags</legend>
|
||||
<div id="existing-tags" class="tag-options"></div>
|
||||
<label>
|
||||
<span>Add new tags</span>
|
||||
<input id="new-tags-input" type="text" pattern="#[^, ]+(,\s*#[^, ]+)*" placeholder="#tag1, #tag2" />
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<fieldset class="form-section">
|
||||
<legend>Mastodon Publishing</legend>
|
||||
<label>
|
||||
<input id="mastodon-enabled" type="checkbox" />
|
||||
Post to Mastodon (if configured)
|
||||
</label>
|
||||
</fieldset>
|
||||
|
||||
<div class="form-actions">
|
||||
<button id="submit-button" type="submit">Save Entry</button>
|
||||
<a href="/" class="secondary button">Cancel</a>
|
||||
</div>
|
||||
|
||||
<div id="submit-status" class="status hidden" aria-live="polite"></div>
|
||||
</form>
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
<span>Copyright © 2026 Olaf Kolkman</span> · <a href="https://git.kolkman.org/olaf/Link-Log">Repository</a>
|
||||
</footer>
|
||||
|
||||
<script src="/static/auth-header.js"></script>
|
||||
<script src="/static/new-entry.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -19,6 +19,7 @@
|
||||
<h1>Profile</h1>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button id="new-entry-button" class="new-entry-button hidden" type="button"><a href="/new-entry">New entry</a></button>
|
||||
<button class="menu-toggle" type="button" aria-expanded="false" aria-controls="auth-menu">Menu</button>
|
||||
<nav id="auth-menu" class="auth-menu hidden" aria-label="Account menu">
|
||||
<a id="auth-home-link" href="/">Home</a>
|
||||
|
||||
Reference in New Issue
Block a user