/**
* 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 = `
Failed to load profiles: ${err.message || 'Unknown error'}
`;
}
}
/**
* 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() !== ''
? 'No profiles match your search
'
: 'No profiles found
';
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 ? 'BOT' : '';
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(`🌐 ${escapeHtml(profile.website)}`);
}
if (profile.xUsername) {
const xUrl = `https://x.com/${profile.xUsername.replace('@', '')}`;
details.push(`🐦 ${escapeHtml(profile.xUsername)}`);
}
// 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
? `${tagsArray.map(tag => `${escapeHtml(tag)}`).join('')}
`
: '';
return `
${escapeHtml(displayName)} ${botTag}
${escapeHtml(bio)}
${details.length > 0 ? `
${details.join(' • ')}
` : ''}
${tagsHtml}
${profile.peerId}
`;
}).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', '');
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 = '✓Disable';
} else {
toggle.classList.remove('enabled');
toggle.innerHTML = '+Enable';
}
}
}
/**
* 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) {
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');
// Create form data
const formData = new FormData();
formData.append('avatar', file);
const response = await authenticatedFetch(`${API_BASE}/profile/avatar`, {
method: 'POST',
body: formData
});
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);
showToast(err.message || 'Failed to upload avatar', 'error');
}
}
/**
* 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 => `
${escapeHtml(tag)}
`).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();
}