Tag functionality added

This commit is contained in:
Olaf
2026-08-24 20:42:15 +02:00
parent f96a4ab3ad
commit 2810fd914d
16 changed files with 448 additions and 18 deletions
+77 -3
View File
@@ -1,9 +1,11 @@
const feedEl = document.getElementById('feed');
const sortSelect = document.getElementById('sort-select');
const userFilter = document.getElementById('user-filter');
const tagFilter = document.getElementById('tag-filter');
const cookieName = 'linklog-feed-preferences';
const accessToken = localStorage.getItem('linklogAccessToken');
let availableTags = [];
function createAvatar(user) {
const avatar = document.createElement('div');
@@ -46,6 +48,18 @@ function writePreferences(pref) {
document.cookie = `${cookieName}=${value}; path=/; max-age=31536000`;
}
function formatDate(value) {
const date = new Date(value);
if (Number.isNaN(date.getTime())) return value || 'updated recently';
const months = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December',
];
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${date.getFullYear()} ${months[date.getMonth()]} ${date.getDate()} - ${hours}:${minutes}`;
}
async function loadUsers() {
const response = await fetch('/api/public/users');
if (!response.ok) throw new Error('Could not load users');
@@ -55,6 +69,16 @@ async function loadUsers() {
userFilter.value = users.includes(selectedUser) ? selectedUser : '';
}
async function loadTags() {
const response = await fetch('/api/tags');
if (!response.ok) throw new Error('Could not load tags');
const tags = await response.json();
availableTags = tags;
const selectedTag = readPreferences().tag || '';
tagFilter.replaceChildren(new Option('All tags', ''), ...tags.map((tag) => new Option(tag, tag)));
tagFilter.value = tags.includes(selectedTag) ? selectedTag : '';
}
function renderFeed(items, showIdentity = true) {
feedEl.innerHTML = '';
@@ -79,9 +103,16 @@ function renderFeed(items, showIdentity = true) {
comment.className = 'comment';
comment.textContent = item.comment || 'No comment provided';
if (item.tags?.length) {
const tags = document.createElement('div');
tags.className = 'entry-tags';
tags.textContent = item.tags.join(' ');
article.appendChild(tags);
}
const meta = document.createElement('div');
meta.className = 'meta';
meta.textContent = item.created_at || 'updated recently';
meta.textContent = formatDate(item.created_at);
if (item.is_owner) {
const editButton = document.createElement('button');
@@ -118,17 +149,49 @@ function showEditForm(article, item) {
<label>Title <input name="title" value=""></label>
<label>URL <input name="url" type="url" value=""></label>
<label>Comment <textarea name="comment"></textarea></label>
<fieldset class="edit-tags">
<legend>Tags</legend>
<div class="edit-tag-options"></div>
<input name="new_tags" type="text" placeholder="New tags, separated by commas">
<p class="edit-tag-status" role="alert"></p>
</fieldset>
<button type="submit">Save changes</button>
`;
form.elements.title.value = item.title || '';
form.elements.url.value = item.url || '';
form.elements.comment.value = item.comment || '';
const tagOptions = form.querySelector('.edit-tag-options');
tagOptions.replaceChildren(...availableTags.map((tag) => {
const label = document.createElement('label');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.value = tag;
checkbox.checked = item.tags?.includes(tag) || false;
label.append(checkbox, document.createTextNode(` ${tag}`));
return label;
}));
form.addEventListener('submit', async (event) => {
event.preventDefault();
const selectedTags = [...tagOptions.querySelectorAll('input:checked')].map((input) => input.value);
const newTags = form.elements.new_tags.value
.split(',')
.map((tag) => tag.trim())
.filter(Boolean)
.map((tag) => tag.startsWith('#') ? tag : `#${tag}`);
const tags = [...new Set([...selectedTags, ...newTags])];
if (tags.length > 10) {
form.querySelector('.edit-tag-status').textContent = 'A link can have at most 10 tags.';
return;
}
const response = await fetch(`/api/links/${encodeURIComponent(item.id)}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json', Authorization: `Bearer ${accessToken}`},
body: JSON.stringify(Object.fromEntries(new FormData(form))),
body: JSON.stringify({
title: form.elements.title.value,
url: form.elements.url.value,
comment: form.elements.comment.value,
tags,
}),
});
if (response.ok) loadFeed();
});
@@ -152,6 +215,10 @@ async function loadFeed() {
items = items.filter((item) => (item.user?.username || '').toLowerCase() === pref.user.toLowerCase());
}
if (pref.tag) {
items = items.filter((item) => item.tags?.some((tag) => tag.toLowerCase() === pref.tag.toLowerCase()));
}
if (pref.sort === 'oldest') {
items = [...items].reverse();
}
@@ -163,6 +230,7 @@ function syncPreferences() {
const pref = readPreferences();
sortSelect.value = pref.sort;
userFilter.value = pref.user;
tagFilter.value = pref.tag || '';
sortSelect.addEventListener('change', (event) => {
const next = { ...readPreferences(), sort: event.target.value };
@@ -176,7 +244,13 @@ function syncPreferences() {
writePreferences(next);
window.location.assign(selectedUser ? `/${encodeURIComponent(selectedUser)}/` : '/');
});
tagFilter.addEventListener('change', (event) => {
const next = { ...readPreferences(), tag: event.target.value };
writePreferences(next);
loadFeed();
});
}
syncPreferences();
loadUsers().then(loadFeed).catch(() => loadFeed());
Promise.all([loadUsers(), loadTags()]).then(loadFeed).catch(() => loadFeed());
+42
View File
@@ -398,6 +398,41 @@ button:disabled {
border-top: 1px solid var(--border);
}
.edit-tags {
display: grid;
gap: 8px;
margin: 0;
padding: 10px;
border: 1px solid var(--border);
border-radius: 8px;
}
.edit-tag-options {
display: flex;
flex-wrap: wrap;
gap: 6px 10px;
}
.edit-tag-options label {
display: flex;
align-items: center;
gap: 4px;
color: var(--subtext);
font-size: 0.85rem;
}
.edit-tag-options input {
width: auto;
min-width: 0;
}
.edit-tag-status {
min-height: 1.25em;
margin: 0;
color: var(--red);
font-size: 0.82rem;
}
.feed {
padding-bottom: 40px;
}
@@ -474,6 +509,13 @@ button:disabled {
font-size: 0.82rem;
}
.entry-tags {
margin-top: 12px;
color: var(--mauve);
font-size: 0.85rem;
font-weight: 600;
}
@media (max-width: 600px) {
.container {
padding: 0 14px;
+7 -1
View File
@@ -61,6 +61,12 @@
<option value="">All users</option>
</select>
</label>
<label>
Tag filter
<select id="tag-filter">
<option value="">All tags</option>
</select>
</label>
</section>
<section id="feed" class="feed" aria-live="polite"></section>
@@ -68,6 +74,6 @@
<script src="/static/auth-header.js?v=3"></script>
<script src="/static/logout.js?v=3"></script>
<script src="/static/feed.js?v=5"></script>
<script src="/static/feed.js?v=7"></script>
</body>
</html>