Rudimentary search
Build LinkLog Development Image / development-image (push) Successful in 11s

This commit is contained in:
2026-09-05 08:31:23 +02:00
parent 48c60e00a2
commit 6ff90aae9a
6 changed files with 103 additions and 1 deletions
+74
View File
@@ -5,6 +5,7 @@ const feedEl = document.getElementById('feed');
const sortSelect = document.getElementById('sort-select');
const userFilter = document.getElementById('user-filter');
const tagFilter = document.getElementById('tag-filter');
const searchInput = document.getElementById('search-input');
const cookieName = 'linklog-feed-preferences';
const accessToken = localStorage.getItem('linklogAccessToken');
@@ -51,6 +52,70 @@ function writePreferences(pref) {
document.cookie = `${cookieName}=${value}; path=/; max-age=31536000`;
}
function tokenizeSearch(value) {
const tokens = [];
const pattern = /"([^"]+)"|(\S+)/g;
let match;
while ((match = pattern.exec(value)) !== null) {
tokens.push(match[1] || match[2]);
}
return tokens;
}
function parseSearchQuery(value) {
const groups = [[]];
const excluded = [];
const sites = [];
tokenizeSearch(value).forEach((token) => {
if (token.toUpperCase() === 'OR') {
groups.push([]);
return;
}
const isExcluded = token.startsWith('-') && token.length > 1;
const term = isExcluded ? token.slice(1) : token;
const siteMatch = term.match(/^site:(\S+)$/i);
if (siteMatch && !isExcluded) {
sites.push(siteMatch[1].toLowerCase());
return;
}
if (isExcluded) {
excluded.push(term.toLowerCase());
} else {
groups[groups.length - 1].push(term.toLowerCase());
}
});
return {groups: groups.filter((group) => group.length), excluded, sites};
}
function searchableText(item) {
return [
item.title,
item.url,
item.comment,
item.user?.username,
...(item.tags || []),
].filter(Boolean).join(' ').toLowerCase();
}
function matchesSearch(item, query) {
if (!query.trim()) return true;
const parsed = parseSearchQuery(query);
const text = searchableText(item);
const matchesSite = parsed.sites.every((site) => {
try {
const hostname = new URL(item.url).hostname.toLowerCase();
return hostname === site || hostname.endsWith(`.${site}`);
} catch (error) {
return false;
}
});
const matchesGroup = !parsed.groups.length || parsed.groups.some((group) => group.every((term) => text.includes(term)));
const excludesTerm = parsed.excluded.some((term) => text.includes(term));
return matchesSite && matchesGroup && !excludesTerm;
}
function formatDate(value) {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value || 'updated recently';
@@ -278,6 +343,8 @@ async function loadFeed(selectedTag = null) {
items = items.filter((item) => item.tags?.some((tag) => tag.toLowerCase() === activeTag.toLowerCase()));
}
items = items.filter((item) => matchesSearch(item, pref.search || ''));
if (pref.sort === 'oldest') {
items = [...items].reverse();
}
@@ -290,6 +357,7 @@ function syncPreferences() {
sortSelect.value = pref.sort;
userFilter.value = pref.user;
tagFilter.value = pref.tag || '';
searchInput.value = pref.search || '';
sortSelect.addEventListener('change', (event) => {
const next = { ...readPreferences(), sort: event.target.value };
@@ -309,6 +377,12 @@ function syncPreferences() {
writePreferences(next);
loadFeed(event.target.value);
});
searchInput.addEventListener('input', (event) => {
const next = { ...readPreferences(), search: event.target.value };
writePreferences(next);
loadFeed();
});
}
syncPreferences();