New Entry page improvements on link detection
This commit is contained in:
@@ -11,8 +11,9 @@
|
||||
const existingTagsEl = document.getElementById('existing-tags');
|
||||
const mastodonPublishing = document.getElementById('mastodon-publishing');
|
||||
const mastodonEnabledCheckbox = document.getElementById('mastodon-enabled');
|
||||
const scrapeButton = document.getElementById('scrape-button');
|
||||
const scrapeStatus = document.getElementById('scrape-status');
|
||||
const duplicateStatus = document.getElementById('duplicate-status');
|
||||
const refetchTitleButton = document.getElementById('refetch-title-button');
|
||||
const submitButton = document.getElementById('submit-button');
|
||||
const submitStatus = document.getElementById('submit-status');
|
||||
|
||||
@@ -21,9 +22,14 @@
|
||||
let selectedTags = new Set();
|
||||
let currentUser = null;
|
||||
|
||||
// Utility function to add status messages
|
||||
// Utility function to add status messages; splits on \n into real <br> line breaks without using innerHTML.
|
||||
function setStatus(statusEl, message, isError = false) {
|
||||
statusEl.textContent = message;
|
||||
const lines = message.split('\n');
|
||||
statusEl.replaceChildren(
|
||||
...lines.flatMap((line, index) => (
|
||||
index === 0 ? [document.createTextNode(line)] : [document.createElement('br'), document.createTextNode(line)]
|
||||
)),
|
||||
);
|
||||
statusEl.className = `status ${isError ? 'error' : 'success'}`;
|
||||
statusEl.classList.remove('hidden');
|
||||
if (!isError) {
|
||||
@@ -106,17 +112,29 @@
|
||||
});
|
||||
}
|
||||
|
||||
// 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;
|
||||
// Strip known tracking parameters so duplicate detection and scraping ignore them, mirroring the browser extension.
|
||||
function removeKnownTrackingParams(urlString) {
|
||||
try {
|
||||
const url = new URL(urlString);
|
||||
const known = new Set([
|
||||
'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',
|
||||
'utm_id', 'utm_name', 'gclid', 'fbclid', 'dclid', 'msclkid',
|
||||
]);
|
||||
for (const key of known) {
|
||||
url.searchParams.delete(key);
|
||||
}
|
||||
return url.toString();
|
||||
} catch (error) {
|
||||
return urlString;
|
||||
}
|
||||
}
|
||||
|
||||
scrapeButton.disabled = true;
|
||||
setStatus(scrapeStatus, 'Scraping...', false);
|
||||
// Fetch the page title for the given URL and fill it in, unless the user already typed one.
|
||||
let lastScrapedUrl = null;
|
||||
async function fetchTitle(url, { force = false } = {}) {
|
||||
if (!url || (!force && (titleInput.value.trim() || url === lastScrapedUrl))) return;
|
||||
|
||||
setStatus(scrapeStatus, 'Looking up title...', false);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/scrape?url=${encodeURIComponent(url)}`, {
|
||||
@@ -132,6 +150,7 @@
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
lastScrapedUrl = url;
|
||||
const data = await response.json();
|
||||
if (data.title) {
|
||||
titleInput.value = data.title;
|
||||
@@ -142,16 +161,64 @@
|
||||
} catch (error) {
|
||||
console.error('Scrape error:', error);
|
||||
setStatus(scrapeStatus, `Error: ${error.message}`, true);
|
||||
} finally {
|
||||
scrapeButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
urlInput.addEventListener('blur', async () => {
|
||||
await fetchTitle(removeKnownTrackingParams(urlInput.value.trim()));
|
||||
checkDuplicate();
|
||||
});
|
||||
|
||||
refetchTitleButton.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
const url = removeKnownTrackingParams(urlInput.value.trim());
|
||||
if (!url) {
|
||||
setStatus(scrapeStatus, 'Please enter a URL', true);
|
||||
return;
|
||||
}
|
||||
titleInput.value = '';
|
||||
fetchTitle(url, { force: true });
|
||||
});
|
||||
|
||||
// Warn when the URL/title combination already exists for this user, mirroring the browser extension.
|
||||
titleInput.addEventListener('blur', checkDuplicate);
|
||||
|
||||
urlInput.addEventListener('input', () => duplicateStatus.classList.add('hidden'));
|
||||
titleInput.addEventListener('input', () => duplicateStatus.classList.add('hidden'));
|
||||
|
||||
async function checkDuplicate() {
|
||||
const url = removeKnownTrackingParams(urlInput.value.trim());
|
||||
const title = titleInput.value.trim();
|
||||
if (!url || !title) return;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/links/check?${new URLSearchParams({ title, url })}`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (!response.ok) return;
|
||||
const data = await response.json();
|
||||
if (data.exists) {
|
||||
let message = 'This link already exists. ';
|
||||
if (!data.url_matches) {
|
||||
message += 'But, the stored link has a different URL (missing or different parameters).';
|
||||
message += '\nWhen you save the entry you risk a duplicate entry.';
|
||||
} else {
|
||||
message += '\nYou can still save the entry, which will update the existing entry\'s comment and or tags.';
|
||||
}
|
||||
setStatus(duplicateStatus, message, true);
|
||||
} else {
|
||||
duplicateStatus.classList.add('hidden');
|
||||
}
|
||||
} catch (error) {
|
||||
// Duplicate checking is advisory; submission remains available.
|
||||
}
|
||||
}
|
||||
|
||||
// Handle form submission
|
||||
entryForm.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const url = urlInput.value.trim();
|
||||
const url = removeKnownTrackingParams(urlInput.value.trim());
|
||||
const title = titleInput.value.trim();
|
||||
const comment = commentInput.value.trim();
|
||||
const newTags = newTagsInput.value.trim();
|
||||
|
||||
Reference in New Issue
Block a user