Unenforced character count in frontend
Build LinkLog Development Image / development-image (push) Successful in 11s

This commit is contained in:
2026-09-06 10:20:25 +02:00
parent 71f4451b34
commit 443aff8323
9 changed files with 114 additions and 13 deletions
+69 -11
View File
@@ -16,11 +16,14 @@
const refetchTitleButton = document.getElementById('refetch-title-button');
const submitButton = document.getElementById('submit-button');
const submitStatus = document.getElementById('submit-status');
const characterCount = document.getElementById('character-count');
const token = localStorage.getItem('linklogAccessToken');
let availableTags = [];
let selectedTags = new Set();
let currentUser = null;
let maxPostCharacters = 500;
let mastodonPostPrefix = 'From my #LinkLog: ';
// Utility function to add status messages; splits on \n into real <br> line breaks without using innerHTML.
function setStatus(statusEl, message, isError = false) {
@@ -53,7 +56,7 @@
.then((user) => {
currentUser = user;
entryForm.classList.remove('hidden');
Promise.all([loadTags(), loadMastodonPublishing()]);
Promise.all([loadTags(), loadMastodonPublishing(), loadConfig()]).then(updateCharacterCount);
})
.catch(() => {
localStorage.removeItem('linklogAccessToken');
@@ -61,6 +64,17 @@
entryForm.classList.add('hidden');
});
async function loadConfig() {
try {
const response = await fetch('/api/public/config');
if (!response.ok) throw new Error('Could not load config');
const config = await response.json();
if (config.max_post_characters) maxPostCharacters = config.max_post_characters;
} catch (error) {
console.error('Could not load config:', error);
}
}
// Load available tags
async function loadTags() {
try {
@@ -82,11 +96,63 @@
const config = await response.json();
mastodonEnabledCheckbox.checked = Boolean(config.configured);
mastodonPublishing.classList.toggle('hidden', !config.configured);
if (config.post_prefix) {
mastodonPostPrefix = config.post_prefix;
} else if (config.hashtag) {
mastodonPostPrefix = `#${String(config.hashtag).trim().replace(/^#/, '')} `;
}
} catch (error) {
console.error('Could not load Mastodon configuration:', error);
}
}
// Collect the selected checkbox tags plus any typed new tags, mirroring the submit-time logic.
function collectTags() {
const tags = Array.from(selectedTags);
const newTags = newTagsInput.value.trim();
if (newTags) {
tags.push(...newTags.split(',').map((t) => t.trim()).filter(Boolean));
}
return tags;
}
// Format a UTC timestamp the same way the Mastodon plugin does, so the estimated post length matches the server.
function formatMastodonTimestamp(date) {
const months = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December',
];
const pad = (value) => String(value).padStart(2, '0');
return `${date.getUTCFullYear()} ${months[date.getUTCMonth()]} ${pad(date.getUTCDate())} - ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}`;
}
// Estimate the assembled Mastodon post length, mirroring backend/app/services/plugin_manager.py.
function estimatePostLength() {
const title = titleInput.value.trim();
const comment = commentInput.value.trim();
const url = removeKnownTrackingParams(urlInput.value.trim());
const parts = [mastodonPostPrefix.trim()];
if (title) parts.push(title);
if (comment) parts.push(comment);
if (title) parts.push(`Logged on ${formatMastodonTimestamp(new Date())} UTC from: ${url}`);
const tags = collectTags();
if (tags.length) parts.push(tags.join(' '));
return parts.join('\n\n').length;
}
function updateCharacterCount() {
const length = estimatePostLength();
const overLimit = length > maxPostCharacters;
characterCount.textContent = overLimit
? `${length}/${maxPostCharacters} characters - exceeds the Mastodon post limit by ${length - maxPostCharacters}`
: `${length}/${maxPostCharacters} characters`;
characterCount.classList.toggle('over-limit', overLimit);
}
[urlInput, titleInput, commentInput, newTagsInput].forEach((input) => {
input.addEventListener('input', updateCharacterCount);
});
// Render tag checkboxes
function renderTags() {
existingTagsEl.innerHTML = '';
@@ -104,6 +170,7 @@
} else {
selectedTags.delete(tag);
}
updateCharacterCount();
});
label.appendChild(checkbox);
@@ -224,22 +291,13 @@
const url = removeKnownTrackingParams(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);
}
const tags = collectTags();
submitButton.disabled = true;
setStatus(submitStatus, 'Saving...', false);
+12
View File
@@ -1242,6 +1242,18 @@ button:disabled {
display: none;
}
.char-count {
margin: -8px 0 0;
color: var(--muted);
font-size: 0.82rem;
text-align: right;
}
.char-count.over-limit {
color: var(--red);
font-weight: 600;
}
@media (max-width: 600px) {
.container {
padding: 0 14px;
+3 -1
View File
@@ -71,6 +71,7 @@
<textarea id="comment-input" name="comment" rows="4" placeholder="Optional comment about this link"></textarea>
</label>
</div>
<div id="character-count" class="char-count" aria-live="polite">0/500 characters</div>
<fieldset class="form-section">
<legend>Tags</legend>
@@ -89,6 +90,7 @@
</label>
</fieldset>
<div class="form-actions">
<button id="submit-button" type="submit">Save Entry</button>
<a href="/" class="secondary button">Cancel</a>
@@ -103,6 +105,6 @@
</footer>
<script src="/static/auth-header.js"></script>
<script src="/static/new-entry.js?v=8"></script>
<script src="/static/new-entry.js?v=9"></script>
</body>
</html>