Files
p2ns/plugin-sites/global.profile/www/js/app.js
T
2026-05-28 18:19:32 -04:00

1046 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Global Profile App
* Handles profile management UI with new SDK
*/
const API_BASE = '/api';
// State
let currentProfile = null;
let peerId = null;
// Profile list state management
let allProfiles = [];
let filteredProfiles = [];
let displayedCount = 0;
let searchQuery = '';
let scrollObserver = null;
const BATCH_SIZE = 25; // Number of profiles to load per batch
// Connection status elements
let statusIndicator = null;
let statusText = null;
let profilesCountValue = null;
// WebSocket connection
let ws = null;
let reconnectTimeout = null;
let reconnectDelay = 1000;
const MAX_RECONNECT_DELAY = 30000;
/**
* Get avatar URL for a peer
*/
function getAvatarUrl(peerId, size = 64) {
if (!peerId) return '';
return `${API_BASE}/profile/avatar/${encodeURIComponent(peerId)}/${size}`;
}
/**
* Initialize the app
*/
async function init() {
try {
// Get connection status elements
statusIndicator = document.getElementById('statusIndicator');
statusText = document.getElementById('statusText');
profilesCountValue = document.getElementById('profilesCountValue');
// Load current profile
await loadMyProfile();
// Load all profiles
await loadProfiles();
// Set up event listeners
setupEventListeners();
// Initialize tag editor
initTagEditor();
// Connect to WebSocket for real-time updates
connectWebSocket();
} catch (err) {
console.error('Error initializing app:', err);
showToast('Failed to initialize app', 'error');
}
}
/**
* Connect to WebSocket server
*/
function connectWebSocket() {
// Don't connect if already connected or connecting
if (ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING)) {
return;
}
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/ws`;
try {
ws = new WebSocket(wsUrl);
ws.onopen = () => {
console.log('WebSocket connected');
reconnectDelay = 1000; // Reset delay on successful connection
updateStatus(true);
// WebSocket is now only used for real-time updates
// Initial profile data is loaded via REST API
};
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
handleWebSocketMessage(data);
} catch (err) {
console.error('Error parsing WebSocket message:', err);
}
};
ws.onclose = () => {
console.log('WebSocket disconnected');
updateStatus(false);
ws = null;
// Attempt to reconnect with exponential backoff
reconnectTimeout = setTimeout(() => {
reconnectDelay = Math.min(reconnectDelay * 2, MAX_RECONNECT_DELAY);
connectWebSocket();
}, reconnectDelay);
};
ws.onerror = (err) => {
console.error('WebSocket error:', err);
};
} catch (err) {
console.error('Error creating WebSocket:', err);
}
}
/**
* Handle WebSocket messages
*/
function handleWebSocketMessage(data) {
console.log('Received WebSocket message:', data.type, data);
switch (data.type) {
case 'profile-update':
// Profile was updated
if (data.profile) {
const index = allProfiles.findIndex(p => p.peerId === data.profile.peerId);
if (index >= 0) {
// Update existing profile
allProfiles[index] = data.profile;
} else {
// Add new profile
allProfiles.push(data.profile);
}
// If it's the current user's profile, reload it
if (data.profile.peerId === peerId) {
loadMyProfile();
}
// Refresh the display
updateProfilesCount();
applyFilter();
}
break;
case 'profile-deleted':
// Profile was deleted
if (data.peerId) {
allProfiles = allProfiles.filter(p => p.peerId !== data.peerId);
// If it's the current user's profile, reload it
if (data.peerId === peerId) {
loadMyProfile();
}
// Refresh the display
updateProfilesCount();
applyFilter();
}
break;
default:
console.log('Unknown WebSocket message type:', data.type);
}
}
/**
* Load current user's profile
*/
async function loadMyProfile() {
try {
const response = await fetch(`${API_BASE}/profile`);
if (!response.ok) {
throw new Error('Failed to load profile');
}
currentProfile = await response.json();
peerId = currentProfile.peerId;
// Populate form
document.getElementById('display-name').value = currentProfile.displayName || '';
document.getElementById('bio').value = currentProfile.bio || '';
document.getElementById('peer-id').value = currentProfile.peerId || '';
// Populate additional details with toggle state
// Store enabled state in customFields if not present
const emailEnabled = currentProfile.customFields?.emailEnabled ?? (currentProfile.email ? true : false);
const websiteEnabled = currentProfile.customFields?.websiteEnabled ?? (currentProfile.website ? true : false);
const xHandleEnabled = currentProfile.customFields?.xHandleEnabled ?? (currentProfile.xUsername ? true : false);
populateField('email', currentProfile.email || '', emailEnabled);
populateField('website', currentProfile.website || '', websiteEnabled);
populateField('xUsername', currentProfile.xUsername || '', xHandleEnabled);
// Handle tags - populate tag editor
let tagsArray = [];
if (Array.isArray(currentProfile.tags)) {
tagsArray = currentProfile.tags;
} else if (typeof currentProfile.tags === 'string') {
try {
const parsed = JSON.parse(currentProfile.tags);
tagsArray = Array.isArray(parsed) ? parsed : [];
} catch {
// If it's a comma-separated string, split it
if (currentProfile.tags.trim()) {
tagsArray = currentProfile.tags.split(',').map(t => t.trim()).filter(t => t);
}
}
}
updateTagEditor(tagsArray);
// Update avatar (use 128x128 for preview)
if (peerId) {
const avatarImg = document.getElementById('avatar-preview');
avatarImg.src = getAvatarUrl(peerId, 128) + '?t=' + Date.now();
avatarImg.style.display = 'block';
document.getElementById('avatar-placeholder').style.display = 'none';
} else {
document.getElementById('avatar-preview').style.display = 'none';
document.getElementById('avatar-placeholder').style.display = 'block';
}
} catch (err) {
console.error('Error loading profile:', err);
showToast('Failed to load profile', 'error');
}
}
/**
* Load all profiles from network
*/
async function loadProfiles() {
try {
const response = await fetch(`${API_BASE}/profiles`);
if (!response.ok) {
const errorText = await response.text();
let errorMessage = 'Failed to load profiles';
try {
const errorJson = JSON.parse(errorText);
errorMessage = errorJson.error || errorJson.message || errorMessage;
} catch (e) {
errorMessage = errorText || errorMessage;
}
console.error('Error loading profiles:', errorMessage, 'Status:', response.status);
throw new Error(errorMessage);
}
const data = await response.json();
console.log('Profiles response:', data);
// Handle both old format (data.profiles) and new format (data.profiles with pagination)
if (data.profiles && Array.isArray(data.profiles)) {
allProfiles = data.profiles;
} else if (Array.isArray(data)) {
// Fallback: if response is directly an array
allProfiles = data;
} else {
console.warn('Unexpected response format:', data);
allProfiles = [];
}
// Update profiles count
updateProfilesCount();
// Initial filter with empty query (show all)
searchQuery = '';
applyFilter();
} catch (err) {
console.error('Error loading profiles:', err);
const profilesList = document.getElementById('profiles-list');
profilesList.innerHTML = `<div class="loading">Failed to load profiles: ${err.message || 'Unknown error'}</div>`;
}
}
/**
* Update the profiles count display
*/
function updateProfilesCount() {
if (profilesCountValue) {
profilesCountValue.textContent = allProfiles.length;
}
}
/**
* Update connection status indicator
* @param {boolean} connected - Whether connected
*/
function updateStatus(connected) {
if (!statusIndicator || !statusText) return;
if (connected) {
statusIndicator.classList.remove('disconnected');
statusText.textContent = 'Connected';
} else {
statusIndicator.classList.add('disconnected');
statusText.textContent = 'Disconnected - Reconnecting...';
}
}
/**
* Filter profiles based on search query
* @param {string} query - Search query string
* @param {Array} profiles - Array of profiles to filter
* @returns {Array} Filtered profiles
*/
function filterProfiles(query, profiles) {
if (!query || query.trim() === '') {
return profiles;
}
const searchTerm = query.toLowerCase().trim();
return profiles.filter(profile => {
// Search in displayName
if (profile.displayName && profile.displayName.toLowerCase().includes(searchTerm)) {
return true;
}
// Search in bio
if (profile.bio && profile.bio.toLowerCase().includes(searchTerm)) {
return true;
}
// Search in email
if (profile.email && profile.email.toLowerCase().includes(searchTerm)) {
return true;
}
// Search in website
if (profile.website && profile.website.toLowerCase().includes(searchTerm)) {
return true;
}
// Search in xUsername
if (profile.xUsername && profile.xUsername.toLowerCase().includes(searchTerm)) {
return true;
}
// Search in tags
if (profile.tags) {
const tagsArray = Array.isArray(profile.tags) ? profile.tags :
(typeof profile.tags === 'string' ? JSON.parse(profile.tags || '[]') : []);
if (tagsArray.some(tag => tag.toLowerCase().includes(searchTerm))) {
return true;
}
}
// Search in peerId
if (profile.peerId && profile.peerId.toLowerCase().includes(searchTerm)) {
return true;
}
return false;
});
}
/**
* Apply filter and reset display
*/
function applyFilter() {
filteredProfiles = filterProfiles(searchQuery, allProfiles);
displayedCount = 0;
// Clear the list and reset scroll
const profilesList = document.getElementById('profiles-list');
profilesList.innerHTML = '';
profilesList.scrollTop = 0;
// Clean up existing observer
if (scrollObserver) {
scrollObserver.disconnect();
scrollObserver = null;
}
// Display first batch
loadMoreProfiles();
}
/**
* Load more profiles into the display (incremental loading)
*/
function loadMoreProfiles() {
const profilesList = document.getElementById('profiles-list');
// Check if we've displayed all filtered profiles
if (displayedCount >= filteredProfiles.length) {
// Remove sentinel if it exists
const sentinel = document.getElementById('scroll-sentinel');
if (sentinel) {
sentinel.remove();
}
// Show message if no profiles match
if (filteredProfiles.length === 0) {
const noResultsMsg = searchQuery.trim() !== ''
? '<div class="loading">No profiles match your search</div>'
: '<div class="loading">No profiles found</div>';
if (profilesList.children.length === 0) {
profilesList.innerHTML = noResultsMsg;
}
}
return;
}
// Calculate how many profiles to display in this batch
const nextBatch = filteredProfiles.slice(displayedCount, displayedCount + BATCH_SIZE);
const renderTimestamp = Date.now();
// Generate HTML for this batch
const html = nextBatch.map((profile, index) => {
const displayName = profile.displayName || 'Anonymous';
const isBot = profile.customFields && (profile.customFields.isBot === true || profile.customFields.botTag === 'BOT');
const botTag = isBot ? '<span class="bot-badge">BOT</span>' : '';
const bio = profile.bio || 'No bio';
// Use 64x64 for profile list items
const avatarUrl = getAvatarUrl(profile.peerId, 64);
const cacheBuster = profile.lastUpdated || (renderTimestamp + index);
// Build additional details (email, website, xUsername)
const details = [];
if (profile.email) {
details.push(`📧 ${escapeHtml(profile.email)}`);
}
if (profile.website) {
details.push(`🌐 <a href="${escapeHtml(profile.website)}" target="_blank" rel="noopener noreferrer">${escapeHtml(profile.website)}</a>`);
}
if (profile.xUsername) {
const xUrl = `https://x.com/${profile.xUsername.replace('@', '')}`;
details.push(`🐦 <a href="${xUrl}" target="_blank" rel="noopener noreferrer">${escapeHtml(profile.xUsername)}</a>`);
}
// Handle tags - render as actual tag badges
let tagsArray = [];
if (profile.tags) {
if (Array.isArray(profile.tags)) {
tagsArray = profile.tags;
} else if (typeof profile.tags === 'string') {
try {
tagsArray = JSON.parse(profile.tags || '[]');
} catch {
tagsArray = [];
}
}
}
const tagsHtml = tagsArray.length > 0
? `<div class="profile-item-tags">${tagsArray.map(tag => `<span class="tag-badge">${escapeHtml(tag)}</span>`).join('')}</div>`
: '';
return `
<div class="profile-item">
<img src="${avatarUrl}?t=${cacheBuster}" alt="${displayName}" class="profile-item-avatar" onerror="this.style.display='none'" onload="this.style.display='block'">
<div class="profile-item-info">
<div class="profile-item-name">${escapeHtml(displayName)} ${botTag}</div>
<div class="profile-item-bio">${escapeHtml(bio)}</div>
${details.length > 0 ? `<div class="profile-item-details">${details.join(' • ')}</div>` : ''}
${tagsHtml}
<div class="profile-item-peerid">${profile.peerId}</div>
</div>
</div>
`;
}).join('');
// Remove any existing sentinel
const existingSentinel = document.getElementById('scroll-sentinel');
if (existingSentinel) {
existingSentinel.remove();
}
// Append new profiles
profilesList.insertAdjacentHTML('beforeend', html);
// Update displayed count
displayedCount += nextBatch.length;
// Add sentinel if there are more profiles to load
if (displayedCount < filteredProfiles.length) {
profilesList.insertAdjacentHTML('beforeend', '<div id="scroll-sentinel"></div>');
setupInfiniteScroll();
}
}
/**
* Set up infinite scroll using Intersection Observer
*/
function setupInfiniteScroll() {
// Clean up existing observer
if (scrollObserver) {
scrollObserver.disconnect();
}
const sentinel = document.getElementById('scroll-sentinel');
if (!sentinel) {
return;
}
// Create new observer
scrollObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadMoreProfiles();
}
});
}, {
root: null,
rootMargin: '100px', // Start loading 100px before reaching the bottom
threshold: 0.1
});
scrollObserver.observe(sentinel);
}
/**
* Populate a field with value and enabled state
* @param {string} dataField - Data field name (email, website, xUsername)
* @param {string} value - Field value
* @param {boolean} enabled - Whether field is enabled
*/
function populateField(dataField, value, enabled) {
// Map data field names to HTML IDs
const fieldMap = {
'email': 'email',
'website': 'website',
'xUsername': 'x-handle'
};
const htmlId = fieldMap[dataField] || dataField;
const input = document.getElementById(htmlId);
const toggle = document.getElementById(`${htmlId}-toggle`);
if (input && toggle) {
input.value = value || '';
setFieldEnabled(dataField, enabled === true);
}
}
/**
* Set field enabled/disabled state
* @param {string} dataField - Data field name (email, website, xUsername)
* @param {boolean} enabled - Whether field should be enabled
*/
function setFieldEnabled(dataField, enabled) {
// Map data field names to HTML IDs
const fieldMap = {
'email': 'email',
'website': 'website',
'xUsername': 'x-handle'
};
const htmlId = fieldMap[dataField] || dataField;
const input = document.getElementById(htmlId);
const toggle = document.getElementById(`${htmlId}-toggle`);
if (input && toggle) {
input.disabled = !enabled;
if (enabled) {
toggle.classList.add('enabled');
toggle.innerHTML = '<span class="toggle-icon">✓</span><span class="toggle-label">Disable</span>';
} else {
toggle.classList.remove('enabled');
toggle.innerHTML = '<span class="toggle-icon">+</span><span class="toggle-label">Enable</span>';
}
}
}
/**
* Debounce function to limit how often a function is called
* @param {Function} func - Function to debounce
* @param {number} wait - Wait time in milliseconds
* @returns {Function} Debounced function
*/
function debounce(func, wait) {
let timeout;
return function executedFunction(...args) {
const later = () => {
clearTimeout(timeout);
func(...args);
};
clearTimeout(timeout);
timeout = setTimeout(later, wait);
};
}
/**
* Handle search input changes
*/
const handleSearchInput = debounce((e) => {
searchQuery = e.target.value;
applyFilter();
}, 300);
/**
* Set up event listeners
*/
function setupEventListeners() {
// Profile form submission
const profileForm = document.getElementById('profile-form');
profileForm.addEventListener('submit', async (e) => {
e.preventDefault();
await saveProfile();
});
// Avatar upload
const avatarUpload = document.getElementById('avatar-upload');
avatarUpload.addEventListener('change', async (e) => {
const file = e.target.files[0];
if (file) {
await uploadAvatar(file);
}
});
// Search input
const searchInput = document.getElementById('profile-search');
if (searchInput) {
searchInput.addEventListener('input', handleSearchInput);
}
// Toggle buttons for additional fields
const toggleButtons = [
{ htmlId: 'email', dataField: 'email' },
{ htmlId: 'website', dataField: 'website' },
{ htmlId: 'x-handle', dataField: 'xUsername' }
];
toggleButtons.forEach(({ htmlId, dataField }) => {
const toggle = document.getElementById(`${htmlId}-toggle`);
if (toggle) {
toggle.addEventListener('click', () => {
const input = document.getElementById(htmlId);
const isCurrentlyEnabled = !input.disabled;
setFieldEnabled(dataField, !isCurrentlyEnabled);
});
}
});
// Delete profile button
const deleteBtn = document.getElementById('delete-btn');
if (deleteBtn) {
deleteBtn.addEventListener('click', deleteProfile);
}
}
/**
* Cleanup WebSocket connection
*/
function cleanupWebSocket() {
if (reconnectTimeout) {
clearTimeout(reconnectTimeout);
reconnectTimeout = null;
}
if (ws) {
ws.close();
ws = null;
}
}
// Cleanup on page unload
window.addEventListener('beforeunload', cleanupWebSocket);
/**
* Save profile
*/
async function saveProfile() {
const saveBtn = document.getElementById('save-btn');
const btnText = saveBtn.querySelector('.btn-text');
const btnLoader = saveBtn.querySelector('.btn-loader');
console.log('[SaveProfile] Starting save operation...');
const startTime = Date.now();
try {
saveBtn.disabled = true;
btnText.style.display = 'none';
btnLoader.style.display = 'inline';
const displayName = document.getElementById('display-name').value;
const bio = document.getElementById('bio').value;
// Get additional details with enabled state
const emailInput = document.getElementById('email');
const websiteInput = document.getElementById('website');
const xHandleInput = document.getElementById('x-handle');
const emailEnabled = !emailInput.disabled;
const websiteEnabled = !websiteInput.disabled;
const xHandleEnabled = !xHandleInput.disabled;
// Get tags from tag editor
const tags = getTagsFromEditor();
console.log('[SaveProfile] Tags to save:', tags);
// Build customFields to store enabled states
const customFields = currentProfile?.customFields || {};
customFields.emailEnabled = emailEnabled;
customFields.websiteEnabled = websiteEnabled;
customFields.xHandleEnabled = xHandleEnabled;
const requestBody = {
displayName,
bio,
email: emailEnabled ? emailInput.value.trim() : '',
website: websiteEnabled ? websiteInput.value.trim() : '',
xUsername: xHandleEnabled ? xHandleInput.value.trim() : '',
tags,
customFields
};
console.log('[SaveProfile] Request body:', JSON.stringify(requestBody, null, 2));
console.log('[SaveProfile] Sending PUT request to', `${API_BASE}/profile`);
const fetchStartTime = Date.now();
const response = await authenticatedFetch(`${API_BASE}/profile`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestBody)
});
console.log('[SaveProfile] Response received after', Date.now() - fetchStartTime, 'ms, status:', response.status);
if (!response.ok) {
const errorText = await response.text();
console.error('[SaveProfile] Error response:', errorText);
throw new Error('Failed to save profile: ' + errorText);
}
console.log('[SaveProfile] Parsing response JSON...');
const updatedProfile = await response.json();
console.log('[SaveProfile] Updated profile received:', updatedProfile);
currentProfile = updatedProfile;
console.log('[SaveProfile] Save completed in', Date.now() - startTime, 'ms');
showToast('Profile saved successfully', 'success');
// Profile update will be handled via WebSocket broadcast
} catch (err) {
console.error('[SaveProfile] Error saving profile after', Date.now() - startTime, 'ms:', err);
showToast('Failed to save profile', 'error');
} finally {
saveBtn.disabled = false;
btnText.style.display = 'inline';
btnLoader.style.display = 'none';
}
}
/**
* Upload avatar
*/
async function uploadAvatar(file) {
const avatarUpload = document.getElementById('avatar-upload');
const UPLOAD_TIMEOUT_MS = 120000;
try {
// Validate file type
if (!file.type.startsWith('image/')) {
showToast('Please select an image file', 'error');
return;
}
// Validate file size (max 5MB)
if (file.size > 5 * 1024 * 1024) {
showToast('Image size must be less than 5MB', 'error');
return;
}
showToast('Uploading avatar...', 'success');
if (avatarUpload) {
avatarUpload.disabled = true;
}
const formData = new FormData();
formData.append('avatar', file);
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), UPLOAD_TIMEOUT_MS);
let response;
try {
response = await authenticatedFetch(`${API_BASE}/profile/avatar`, {
method: 'POST',
body: formData,
signal: controller.signal
});
} finally {
clearTimeout(timeoutId);
}
if (!response.ok) {
const errorText = await response.text();
let errorMessage = 'Failed to upload avatar';
try {
const errorJson = JSON.parse(errorText);
errorMessage = errorJson.error || errorJson.message || errorMessage;
} catch (e) {
errorMessage = errorText || errorMessage;
}
throw new Error(errorMessage);
}
const result = await response.json();
// Update avatar preview (use 128x128 for preview)
const avatarImg = document.getElementById('avatar-preview');
if (peerId) {
avatarImg.src = getAvatarUrl(peerId, 128) + '?t=' + Date.now();
avatarImg.style.display = 'block';
document.getElementById('avatar-placeholder').style.display = 'none';
}
// Update current profile
if (currentProfile) {
currentProfile.avatarHash = result.avatarHash;
}
showToast('Avatar uploaded successfully', 'success');
// Avatar update will be handled via WebSocket broadcast
} catch (err) {
console.error('Error uploading avatar:', err);
if (err.name === 'AbortError') {
showToast('Avatar upload timed out. Try a smaller image or retry in a moment.', 'error');
} else {
showToast(err.message || 'Failed to upload avatar', 'error');
}
} finally {
if (avatarUpload) {
avatarUpload.disabled = false;
avatarUpload.value = '';
}
}
}
/**
* Show toast notification
*/
function showToast(message, type = 'success') {
const toast = document.getElementById('toast');
toast.textContent = message;
toast.className = `toast ${type} show`;
setTimeout(() => {
toast.classList.remove('show');
}, 3000);
}
/**
* Escape HTML to prevent XSS
*/
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
/**
* Tag Editor Functions
*/
let currentTags = [];
/**
* Update the tag editor display
*/
function updateTagEditor(tags) {
currentTags = [...tags];
const tagList = document.getElementById('tag-list');
if (!tagList) return;
tagList.innerHTML = currentTags.map(tag => `
<span class="tag-editor-badge">
${escapeHtml(tag)}
<button type="button" class="tag-remove" data-tag="${escapeHtml(tag)}" aria-label="Remove tag">×</button>
</span>
`).join('');
// Add event listeners to remove buttons
tagList.querySelectorAll('.tag-remove').forEach(btn => {
btn.addEventListener('click', () => {
const tagToRemove = btn.getAttribute('data-tag');
removeTag(tagToRemove);
});
});
}
/**
* Add a tag to the editor
*/
function addTag(tag) {
const trimmedTag = tag.trim();
if (!trimmedTag) return false;
// Check if tag already exists (case-insensitive)
if (currentTags.some(t => t.toLowerCase() === trimmedTag.toLowerCase())) {
return false;
}
// Limit tag length
if (trimmedTag.length > 30) {
showToast('Tag is too long (max 30 characters)', 'error');
return false;
}
// Limit number of tags
if (currentTags.length >= 10) {
showToast('Maximum 10 tags allowed', 'error');
return false;
}
currentTags.push(trimmedTag);
updateTagEditor(currentTags);
return true;
}
/**
* Remove a tag from the editor
*/
function removeTag(tag) {
currentTags = currentTags.filter(t => t !== tag);
updateTagEditor(currentTags);
}
/**
* Get tags array from editor
*/
function getTagsFromEditor() {
return [...currentTags];
}
/**
* Initialize tag editor
*/
function initTagEditor() {
const tagsInput = document.getElementById('tags-input');
if (!tagsInput) return;
// Handle Enter key to add tag
tagsInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
const tag = tagsInput.value.trim();
if (tag) {
if (addTag(tag)) {
tagsInput.value = '';
} else {
showToast('Tag already exists', 'error');
}
}
} else if (e.key === 'Backspace' && tagsInput.value === '' && currentTags.length > 0) {
// Remove last tag if input is empty and backspace is pressed
removeTag(currentTags[currentTags.length - 1]);
}
});
// Handle paste to add multiple tags
tagsInput.addEventListener('paste', (e) => {
e.preventDefault();
const pastedText = (e.clipboardData || window.clipboardData).getData('text');
const tags = pastedText.split(/[,\n]/).map(t => t.trim()).filter(t => t);
let added = 0;
tags.forEach(tag => {
if (addTag(tag)) {
added++;
}
});
if (added > 0) {
tagsInput.value = '';
if (added < tags.length) {
showToast(`Added ${added} tag(s), some were duplicates`, 'success');
}
}
});
}
/**
* Delete profile
*/
async function deleteProfile() {
// Confirm deletion
const confirmed = confirm('Are you sure you want to delete your profile? This action cannot be undone. Your display name, avatar, and all profile information will be permanently removed.');
if (!confirmed) {
return;
}
const deleteBtn = document.getElementById('delete-btn');
const originalText = deleteBtn.textContent;
try {
deleteBtn.disabled = true;
deleteBtn.textContent = 'Deleting...';
const response = await authenticatedFetch(`${API_BASE}/profile`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error('Failed to delete profile');
}
const result = await response.json();
showToast('Profile deleted successfully', 'success');
// Reset form to default state
currentProfile = null;
document.getElementById('display-name').value = '';
document.getElementById('bio').value = '';
document.getElementById('peer-id').value = '';
// Reset additional fields
populateField('email', null, false);
populateField('website', null, false);
populateField('xUsername', null, false);
// Reset tags
updateTagEditor([]);
// Reset avatar
document.getElementById('avatar-preview').style.display = 'none';
document.getElementById('avatar-placeholder').style.display = 'block';
// Profile deletion will be handled via WebSocket broadcast
} catch (err) {
console.error('Error deleting profile:', err);
showToast('Failed to delete profile', 'error');
} finally {
deleteBtn.disabled = false;
deleteBtn.textContent = originalText;
}
}
// Initialize app when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}