Complete authentication and profile features
This commit is contained in:
+47
-21
@@ -4,6 +4,26 @@ const userFilter = document.getElementById('user-filter');
|
||||
|
||||
const cookieName = 'linklog-feed-preferences';
|
||||
|
||||
function createAvatar(user) {
|
||||
const avatar = document.createElement('div');
|
||||
avatar.className = 'avatar';
|
||||
const initial = (user?.username || 'U').slice(0, 1).toUpperCase();
|
||||
avatar.textContent = initial;
|
||||
|
||||
if (user?.avatar_url) {
|
||||
const image = document.createElement('img');
|
||||
image.src = user.avatar_url;
|
||||
image.alt = `${user.username || 'User'} avatar`;
|
||||
image.addEventListener('error', () => {
|
||||
image.remove();
|
||||
avatar.textContent = initial;
|
||||
});
|
||||
avatar.textContent = '';
|
||||
avatar.appendChild(image);
|
||||
}
|
||||
return avatar;
|
||||
}
|
||||
|
||||
function readPreferences() {
|
||||
const cookie = document.cookie
|
||||
.split('; ')
|
||||
@@ -25,7 +45,16 @@ function writePreferences(pref) {
|
||||
document.cookie = `${cookieName}=${value}; path=/; max-age=31536000`;
|
||||
}
|
||||
|
||||
function renderFeed(items) {
|
||||
async function loadUsers() {
|
||||
const response = await fetch('/api/public/users');
|
||||
if (!response.ok) throw new Error('Could not load users');
|
||||
const users = await response.json();
|
||||
const selectedUser = readPreferences().user;
|
||||
userFilter.replaceChildren(new Option('All users', ''), ...users.map((username) => new Option(username, username)));
|
||||
userFilter.value = users.includes(selectedUser) ? selectedUser : '';
|
||||
}
|
||||
|
||||
function renderFeed(items, showIdentity = true) {
|
||||
feedEl.innerHTML = '';
|
||||
|
||||
if (!items.length) {
|
||||
@@ -37,20 +66,6 @@ function renderFeed(items) {
|
||||
const article = document.createElement('article');
|
||||
article.className = 'link-item';
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'link-header';
|
||||
|
||||
const avatar = document.createElement('div');
|
||||
avatar.className = 'avatar';
|
||||
avatar.textContent = (item.user?.username || 'U').slice(0, 1).toUpperCase();
|
||||
|
||||
const userName = document.createElement('div');
|
||||
userName.className = 'user-name';
|
||||
userName.textContent = item.user?.username || 'unknown';
|
||||
|
||||
header.appendChild(avatar);
|
||||
header.appendChild(userName);
|
||||
|
||||
const title = document.createElement('h2');
|
||||
const link = document.createElement('a');
|
||||
link.href = item.url;
|
||||
@@ -67,7 +82,17 @@ function renderFeed(items) {
|
||||
meta.className = 'meta';
|
||||
meta.textContent = item.created_at || 'updated recently';
|
||||
|
||||
article.appendChild(header);
|
||||
if (showIdentity) {
|
||||
const header = document.createElement('div');
|
||||
header.className = 'link-header';
|
||||
header.appendChild(createAvatar(item.user));
|
||||
|
||||
const userName = document.createElement('div');
|
||||
userName.className = 'user-name';
|
||||
userName.textContent = item.user?.username || 'unknown';
|
||||
header.appendChild(userName);
|
||||
article.appendChild(header);
|
||||
}
|
||||
article.appendChild(title);
|
||||
article.appendChild(comment);
|
||||
article.appendChild(meta);
|
||||
@@ -94,7 +119,7 @@ async function loadFeed() {
|
||||
items = [...items].reverse();
|
||||
}
|
||||
|
||||
renderFeed(items);
|
||||
renderFeed(items, !routeUser);
|
||||
}
|
||||
|
||||
function syncPreferences() {
|
||||
@@ -108,12 +133,13 @@ function syncPreferences() {
|
||||
loadFeed();
|
||||
});
|
||||
|
||||
userFilter.addEventListener('input', (event) => {
|
||||
const next = { ...readPreferences(), user: event.target.value.trim() };
|
||||
userFilter.addEventListener('change', (event) => {
|
||||
const selectedUser = event.target.value.trim();
|
||||
const next = { ...readPreferences(), user: selectedUser };
|
||||
writePreferences(next);
|
||||
loadFeed();
|
||||
window.location.assign(selectedUser ? `/${encodeURIComponent(selectedUser)}/` : '/');
|
||||
});
|
||||
}
|
||||
|
||||
syncPreferences();
|
||||
loadFeed();
|
||||
loadUsers().then(loadFeed).catch(() => loadFeed());
|
||||
|
||||
@@ -3,7 +3,6 @@ const profileForm = document.querySelector('#profile-form');
|
||||
const mastodonForm = document.querySelector('#mastodon-form');
|
||||
const profileLogoutButton = document.querySelector('#logout-button');
|
||||
const accessToken = localStorage.getItem('linklogAccessToken');
|
||||
const defaultAvatarUrl = 'https://example.com/avatar.png';
|
||||
const defaultMastodonInstance = 'mastodon.social';
|
||||
const defaultPostPrefix = 'From my #LinkLog: "';
|
||||
|
||||
@@ -27,7 +26,11 @@ async function loadProfile() {
|
||||
document.querySelector('#username').textContent = profile.username || '';
|
||||
document.querySelector('#email').value = profile.email || '';
|
||||
document.querySelector('#bio').value = profile.bio || '';
|
||||
document.querySelector('#avatar-url').value = profile.avatar_url || defaultAvatarUrl;
|
||||
const avatarPreview = document.querySelector('#avatar-preview');
|
||||
if (profile.avatar_url) {
|
||||
avatarPreview.src = profile.avatar_url;
|
||||
avatarPreview.classList.remove('hidden');
|
||||
}
|
||||
if (profile.is_admin) {
|
||||
document.querySelector('#admin-link').classList.remove('hidden');
|
||||
}
|
||||
@@ -46,6 +49,7 @@ async function loadMastodonConfig() {
|
||||
profileForm.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(profileForm);
|
||||
formData.delete('avatar');
|
||||
const response = await fetch('/api/user/me', {
|
||||
method: 'PUT',
|
||||
headers: authHeaders(true),
|
||||
@@ -54,6 +58,27 @@ profileForm.addEventListener('submit', async (event) => {
|
||||
setStatus('#profile-status', response.ok ? 'Profile saved.' : 'Could not save profile.', !response.ok);
|
||||
});
|
||||
|
||||
document.querySelector('#avatar-file').addEventListener('change', async (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
const formData = new FormData();
|
||||
formData.append('avatar', file);
|
||||
const response = await fetch('/api/user/avatar', {
|
||||
method: 'POST',
|
||||
headers: authHeaders(),
|
||||
body: formData,
|
||||
});
|
||||
if (!response.ok) {
|
||||
setStatus('#profile-status', 'Could not upload avatar.', true);
|
||||
return;
|
||||
}
|
||||
const data = await response.json();
|
||||
const avatarPreview = document.querySelector('#avatar-preview');
|
||||
avatarPreview.src = data.avatar_url;
|
||||
avatarPreview.classList.remove('hidden');
|
||||
setStatus('#profile-status', 'Avatar uploaded.');
|
||||
});
|
||||
|
||||
mastodonForm.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(mastodonForm);
|
||||
|
||||
@@ -229,6 +229,14 @@ button:focus-visible {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.profile-avatar-preview {
|
||||
width: 96px;
|
||||
height: 96px;
|
||||
object-fit: cover;
|
||||
border: 2px solid var(--mauve);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
button {
|
||||
width: fit-content;
|
||||
border-color: var(--mauve);
|
||||
@@ -363,6 +371,13 @@ button:disabled {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
color: var(--lavender);
|
||||
font-weight: 700;
|
||||
|
||||
Reference in New Issue
Block a user