Tag functionality added
This commit is contained in:
+77
-3
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user