OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / lib / user-profile-ui.js

783 lines · javascript ref:
/**
 * User Profile UI
 *
 * Shows user identity, permissions, and settings
 */

import { renderPage } from './page-layout.js'

const PAGE_CSS = `
    /* User Status Card */
    .user-status {
      background: #161b22;
      border: 2px solid #238636;
      border-radius: 6px;
      padding: 20px;
      margin-bottom: 30px;
    }
    .user-status.read-only { border-color: #58a6ff; }
    .user-status-header {
      display: flex;
      align-items: center;
      gap: 15px;
      margin-bottom: 15px;
    }
    .status-badge {
      padding: 6px 12px;
      border-radius: 12px;
      font-size: 14px;
      font-weight: 600;
    }
    .status-badge.admin {
      background: #238636;
      color: white;
    }
    .status-badge.write {
      background: #58a6ff;
      color: white;
    }
    .status-badge.read {
      background: #6e7681;
      color: white;
    }
    .user-id {
      font-family: monospace;
      color: #8b949e;
      font-size: 12px;
    }

    /* Sections */
    .section {
      background: #161b22;
      border: 1px solid #30363d;
      border-radius: 6px;
      padding: 20px;
      margin-bottom: 20px;
    }
    .section h2 {
      margin-bottom: 15px;
      color: #58a6ff;
      font-size: 20px;
    }
    .section h3 {
      margin: 20px 0 10px 0;
      color: #c9d1d9;
      font-size: 16px;
    }

    /* Identity List */
    .identity-list { display: flex; flex-direction: column; gap: 10px; }
    .identity-item {
      background: #0d1117;
      padding: 15px;
      border-radius: 4px;
      border: 1px solid #30363d;
      display: flex;
      justify-content: space-between;
      align-items: center;
    }
    .identity-info { flex: 1; }
    .identity-type {
      font-weight: bold;
      color: #58a6ff;
      margin-bottom: 5px;
    }
    .identity-id {
      font-family: monospace;
      font-size: 11px;
      color: #8b949e;
      word-break: break-all;
    }
    .identity-date {
      font-size: 12px;
      color: #6e7681;
      margin-top: 5px;
    }
    .identity-actions {
      display: flex;
      gap: 10px;
    }

    /* Permissions Table */
    .permissions-table {
      width: 100%;
      border-collapse: collapse;
    }
    .permissions-table th,
    .permissions-table td {
      padding: 10px;
      text-align: left;
      border-bottom: 1px solid #30363d;
    }
    .permissions-table th {
      color: #8b949e;
      font-weight: 600;
      font-size: 12px;
      text-transform: uppercase;
    }
    .perm-badge {
      display: inline-block;
      padding: 2px 8px;
      border-radius: 12px;
      font-size: 11px;
      font-weight: 600;
      margin-right: 5px;
    }
    .perm-badge.read { background: #1f6feb; color: white; }
    .perm-badge.write { background: #238636; color: white; }
    .perm-badge.admin { background: #da3633; color: white; }

    /* Buttons */
    button {
      background: #238636;
      color: white;
      border: none;
      padding: 10px 20px;
      border-radius: 6px;
      cursor: pointer;
      font-size: 14px;
    }
    button:hover { background: #2ea043; }
    button.secondary {
      background: #21262d;
      border: 1px solid #30363d;
    }
    button.secondary:hover { background: #30363d; }
    button.danger { background: #da3633; }
    button.danger:hover { background: #f85149; }

    /* Loading */
    .loading {
      text-align: center;
      padding: 40px;
      color: #8b949e;
    }

    /* Help Text */
    .help-text {
      background: #161b22;
      border-left: 3px solid #58a6ff;
      padding: 15px;
      margin: 20px 0;
      color: #8b949e;
      font-size: 14px;
      line-height: 1.6;
    }

    /* Profile Edit */
    .profile-avatar {
      width: 80px;
      height: 80px;
      border-radius: 50%;
      object-fit: cover;
      border: 2px solid #30363d;
    }
    .profile-edit-form {
      display: none;
      margin-top: 20px;
      padding: 20px;
      background: #0d1117;
      border-radius: 6px;
      border: 1px solid #30363d;
    }
    .profile-edit-form.visible { display: block; }
    .form-group {
      margin-bottom: 15px;
    }
    .form-group label {
      display: block;
      margin-bottom: 5px;
      color: #8b949e;
      font-size: 14px;
    }
    .form-group input[type="text"] {
      width: 100%;
      padding: 8px 12px;
      background: #0d1117;
      border: 1px solid #30363d;
      border-radius: 6px;
      color: #c9d1d9;
      font-size: 14px;
    }
    .form-group input[type="text"]:focus {
      outline: none;
      border-color: #58a6ff;
    }
    .avatar-preview {
      margin-top: 10px;
    }
    .form-actions {
      display: flex;
      gap: 10px;
      margin-top: 20px;
    }
`

export function renderUserProfile(pathPrefix = '') {
  const content = `
    <div id="user-status" class="user-status">
      <div class="loading">Loading your profile...</div>
    </div>

    <div id="identities-section" class="section" style="display: none;">
      <h2>🔑 Your Identities</h2>
      <p style="color: #8b949e; margin-bottom: 15px;">
        You can link multiple authentication methods to your account. All linked identities share the same permissions.
      </p>
      <div id="identities-list" class="identity-list"></div>

      <h3>Add New Identity</h3>
      <div style="display: flex; gap: 10px; margin-top: 10px;">
        <button onclick="linkSSHKey()">🔐 Link SSH Key</button>
        <button class="secondary" onclick="linkKeetIdentity()">💳 Link Keet Identity</button>
      </div>

      <h3>Merge Another Account</h3>
      <p style="color: #8b949e; margin: 10px 0; font-size: 14px;">
        Have multiple pear-git accounts? Merge them into one. All issues, comments, and permissions will be transferred.
      </p>
      <div id="merge-section">
        <button onclick="showMergeForm()">🔗 Merge Another Account</button>
      </div>
      <div id="merge-form" style="display: none; margin-top: 15px; padding: 20px; background: #0d1117; border-radius: 6px; border: 1px solid #30363d;">
        <div class="form-group">
          <label for="merge-target-pubkey">Public Key of Account to Merge</label>
          <input type="text" id="merge-target-pubkey" placeholder="Paste the 44-character public key of the other account">
          <div style="color: #8b949e; font-size: 12px; margin-top: 5px;">
            Find this in the other account's Profile → Copy ID button on any web-identity
          </div>
        </div>
        <div style="background: #f8514933; border: 1px solid #f85149; border-radius: 6px; padding: 12px; margin: 15px 0; font-size: 13px;">
          <strong style="color: #f85149;">⚠️ Warning:</strong> This will merge the other account INTO this one. The other account's identity will be linked here, and all their data transferred. This cannot be undone.
        </div>
        <div style="display: flex; gap: 10px;">
          <button onclick="initiateMerge()">Create Merge Request</button>
          <button class="secondary" onclick="hideMergeForm()">Cancel</button>
        </div>
        <div id="merge-result" style="display: none; margin-top: 15px; padding: 15px; border-radius: 6px;"></div>
      </div>

      <div id="pending-merges" style="display: none; margin-top: 20px;">
        <h4 style="color: #8b949e; margin-bottom: 10px;">Pending Merge Requests</h4>
        <div id="pending-merges-list"></div>
      </div>
    </div>

    <div id="permissions-section" class="section" style="display: none;">
      <h2>🔒 Your Permissions</h2>
      <table class="permissions-table">
        <thead>
          <tr>
            <th>Repository</th>
            <th>Permissions</th>
          </tr>
        </thead>
        <tbody id="permissions-list"></tbody>
      </table>

      <div class="help-text">
        <strong>Need more access?</strong><br>
        Contact an administrator to upgrade your permissions. Provide them with your User ID shown above.
      </div>
    </div>

    <div id="backup-section" class="section" style="display: none;">
      <h2>💾 Backup Your Identity</h2>
      <p style="color: #8b949e; margin-bottom: 15px;">
        Your web identity is stored in your browser. Save it to restore access from another device.
      </p>
      <div style="display: flex; gap: 10px; flex-wrap: wrap;">
        <button onclick="downloadKeypair()">📥 Download Keypair</button>
        <button class="secondary" onclick="showKeypair()">👁️ Show Keypair</button>
        <button class="secondary" onclick="copyFullKeypair()">📋 Copy Full Keypair (132 chars)</button>
        <button class="secondary" onclick="showImportDialog()">📤 Import Keypair</button>
      </div>
      <div id="keypair-display" style="display: none; margin-top: 15px;">
        <div style="background: #0d1117; padding: 15px; border-radius: 4px; border: 1px solid #30363d;">
          <div style="margin-bottom: 10px;">
            <strong style="color: #58a6ff;">Public Key (44 chars):</strong>
            <div id="pubkey-text" style="font-family: monospace; font-size: 12px; word-break: break-all; margin-top: 5px; padding: 8px; background: #161b22; border-radius: 4px;"></div>
          </div>
          <div style="margin-bottom: 10px;">
            <strong style="color: #f0883e;">Full Keypair (132 chars) - KEEP SECRET:</strong>
            <div id="keypair-text" style="font-family: monospace; font-size: 11px; word-break: break-all; margin-top: 5px; padding: 8px; background: #161b22; border-radius: 4px;"></div>
          </div>
          <div style="display: flex; gap: 10px; margin-top: 10px;">
            <button class="secondary" onclick="copyPubkey()">📋 Copy Public Key</button>
            <button onclick="copyFullKeypair()">📋 Copy Full Keypair</button>
          </div>
        </div>
      </div>
      <div class="help-text" style="margin-top: 15px; padding: 12px; background: #f8514922; border: 1px solid #f85149; border-radius: 6px;">
        <strong style="color: #f85149;">⚠️ Security Warning:</strong> Your full keypair (132 chars) is your private key. Never share it publicly. Only share your public key (44 chars) when needed for account merging.
      </div>
    </div>
  `

  const scripts = `
    const STORAGE_KEY = 'pear-git-keypair';
    let currentUser = null;

    // Use server-injected path prefix from identity script
    function getBasePath() {
      return window.__PATH_PREFIX__ || '';
    }

    async function loadUserProfile() {
      // Wait for identity to be ready (header injection set up)
      if (window.identityReady) {
        await window.identityReady;
      }

      try {
        const response = await window.fetch(getBasePath() + '/api/user/identities');
        if (!response.ok) {
          throw new Error('Not authenticated');
        }

        const data = await response.json();
        currentUser = data;

        renderUserStatus(data);
        renderIdentities(data.identities);
        renderPermissions(data.permissions);

        document.getElementById('identities-section').style.display = 'block';
        document.getElementById('permissions-section').style.display = 'block';
        document.getElementById('backup-section').style.display = 'block';
      } catch (err) {
        console.error('Failed to load profile:', err);
        document.getElementById('user-status').innerHTML = \`
          <div style="text-align: center; padding: 40px;">
            <div style="font-size: 48px; margin-bottom: 20px;">⚠️</div>
            <h2 style="color: #f85149; margin-bottom: 10px;">Not Authenticated</h2>
            <p style="color: #8b949e;">Please visit the home page to generate an identity.</p>
            <a href="./" style="display: inline-block; margin-top: 20px; padding: 10px 20px; background: #238636; color: white; text-decoration: none; border-radius: 6px;">Go to Home</a>
          </div>
        \`;
      }
    }

    function renderUserStatus(data) {
      const hasWrite = data.permissions.global.includes('write') ||
                       data.permissions.repos.some(r => r.permissions.includes('write'));
      const hasAdmin = data.permissions.global.includes('admin') ||
                       data.permissions.repos.some(r => r.permissions.includes('admin'));

      let statusClass = 'read-only';
      let badge = '<span class="status-badge read">Read Only</span>';

      if (hasAdmin) {
        statusClass = 'admin';
        badge = '<span class="status-badge admin">Admin</span>';
      } else if (hasWrite) {
        statusClass = 'write';
        badge = '<span class="status-badge write">Write Access</span>';
      }

      const avatarHtml = data.profilePicture
        ? \`<img src="\${data.profilePicture}" class="profile-avatar" alt="Profile">\`
        : '<div style="font-size: 48px;">👤</div>';

      document.getElementById('user-status').className = \`user-status \${statusClass}\`;
      document.getElementById('user-status').innerHTML = \`
        <div class="user-status-header">
          \${avatarHtml}
          <div style="flex: 1;">
            <h2 style="margin-bottom: 5px;" id="display-name">\${data.name}</h2>
            <div class="user-id">User ID: \${data.userId}</div>
          </div>
          \${badge}
        </div>
        <div style="color: #8b949e; font-size: 14px; margin-bottom: 10px;">
          Registered: \${new Date(data.identities[0]?.createdAt || Date.now()).toLocaleDateString()}
        </div>
        <button class="secondary" onclick="toggleEditProfile()">✏️ Edit Profile</button>

        <div id="profile-edit-form" class="profile-edit-form">
          <div class="form-group">
            <label for="edit-name">Display Name</label>
            <input type="text" id="edit-name" value="\${data.name}" placeholder="Your name">
          </div>
          <div class="form-group">
            <label for="edit-picture">Profile Picture</label>
            <input type="file" id="edit-picture" accept="image/*" onchange="previewAvatar(event)">
            <div id="avatar-preview" class="avatar-preview"></div>
            <div style="color: #8b949e; font-size: 12px; margin-top: 5px;">
              Upload an image or leave empty to use default avatar
            </div>
          </div>
          <div class="form-actions">
            <button onclick="saveProfile()">💾 Save Changes</button>
            <button class="secondary" onclick="toggleEditProfile()">Cancel</button>
          </div>
        </div>
      \`;
    }

    function renderIdentities(identities) {
      const icons = {
        'web-identity': '🌐',
        'ssh-key': '🔐',
        'keet-identity': '💳',
        'bearer-token': '🔑'
      };

      const names = {
        'web-identity': 'Browser Identity',
        'ssh-key': 'SSH Key',
        'keet-identity': 'Keet Identity',
        'bearer-token': 'API Token'
      };

      document.getElementById('identities-list').innerHTML = identities.map(id => \`
        <div class="identity-item">
          <div class="identity-info">
            <div class="identity-type">\${icons[id.method] || '🔑'} \${names[id.method] || id.method}</div>
            <div class="identity-id">\${id.identityId}</div>
            <div class="identity-date">Added: \${new Date(id.createdAt).toLocaleString()}</div>
          </div>
          <div class="identity-actions">
            <button class="secondary" onclick="copyIdentity('\${id.identityId}')">📋 Copy ID</button>
          </div>
        </div>
      \`).join('');
    }

    function renderPermissions(permissions) {
      const allPerms = [...permissions.repos];

      if (permissions.global.length > 0) {
        allPerms.unshift({ name: '* (All Repos)', permissions: permissions.global });
      }

      document.getElementById('permissions-list').innerHTML = allPerms.map(repo => \`
        <tr>
          <td>\${repo.name}</td>
          <td>
            \${repo.permissions.map(p => \`
              <span class="perm-badge \${p}">\${p}</span>
            \`).join('')}
          </td>
        </tr>
      \`).join('');
    }

    function toggleEditProfile() {
      const form = document.getElementById('profile-edit-form');
      form.classList.toggle('visible');
    }

    let profilePictureData = null;

    function previewAvatar(event) {
      const file = event.target.files[0];
      if (!file) return;

      // Check file size (max 2MB)
      if (file.size > 2 * 1024 * 1024) {
        alert('Image too large. Please choose an image under 2MB.');
        event.target.value = '';
        return;
      }

      const reader = new FileReader();
      reader.onload = function(e) {
        profilePictureData = e.target.result;
        document.getElementById('avatar-preview').innerHTML = \`
          <img src="\${profilePictureData}" class="profile-avatar" alt="Preview" style="margin-top: 10px;">
        \`;
      };
      reader.readAsDataURL(file);
    }

    async function saveProfile() {
      const name = document.getElementById('edit-name').value.trim();

      if (!name) {
        alert('Please enter a display name');
        return;
      }

      try {
        const updates = { name };

        // Only include picture if one was selected
        if (profilePictureData) {
          updates.profilePicture = profilePictureData;
        }

        const response = await window.fetch(getBasePath() + '/api/user/profile', {
          method: 'PATCH',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(updates)
        });

        if (!response.ok) {
          const error = await response.json();
          throw new Error(error.error || 'Failed to update profile');
        }

        const result = await response.json();

        // Update displayed name and avatar
        document.getElementById('display-name').textContent = result.user.name;
        if (result.user.profilePicture) {
          currentUser.profilePicture = result.user.profilePicture;
        }
        currentUser.name = result.user.name;

        // Close form and reset
        toggleEditProfile();
        profilePictureData = null;
        document.getElementById('avatar-preview').innerHTML = '';

        alert('✓ Profile updated successfully!');

        // Reload to show updated avatar
        window.location.reload();
      } catch (err) {
        console.error('Profile update error:', err);
        alert('Failed to update profile: ' + err.message);
      }
    }

    function copyIdentity(identityId) {
      navigator.clipboard.writeText(identityId).then(() => {
        alert('Identity ID copied to clipboard!');
      });
    }

    async function linkSSHKey() {
      const sshKey = prompt('Paste your SSH public key (ssh-rsa ... or ssh-ed25519 ...):');
      if (!sshKey) return;

      try {
        const response = await window.fetch(getBasePath() + '/api/user/link-ssh-key', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ sshPublicKey: sshKey })
        });

        if (!response.ok) {
          const error = await response.json();
          alert('Failed to link SSH key: ' + error.error);
          return;
        }

        alert('✓ SSH key linked successfully!');
        loadUserProfile();
      } catch (err) {
        alert('Failed to link SSH key: ' + err.message);
      }
    }

    function linkKeetIdentity() {
      alert('Keet identity linking requires signature verification. This feature will be available in a future update.');
    }

    // ============================================
    // Account Merge Functions
    // ============================================

    function showMergeForm() {
      document.getElementById('merge-form').style.display = 'block';
      document.getElementById('merge-target-pubkey').focus();
    }

    function hideMergeForm() {
      document.getElementById('merge-form').style.display = 'none';
      document.getElementById('merge-target-pubkey').value = '';
      document.getElementById('merge-result').style.display = 'none';
    }

    async function initiateMerge() {
      const targetPubkey = document.getElementById('merge-target-pubkey').value.trim();
      const resultDiv = document.getElementById('merge-result');

      if (!targetPubkey) {
        resultDiv.style.display = 'block';
        resultDiv.style.background = '#f8514933';
        resultDiv.style.border = '1px solid #f85149';
        resultDiv.textContent = 'Please enter the public key of the account to merge.';
        return;
      }

      if (targetPubkey.length !== 44) {
        resultDiv.style.display = 'block';
        resultDiv.style.background = '#f8514933';
        resultDiv.style.border = '1px solid #f85149';
        resultDiv.textContent = 'Invalid public key. Must be exactly 44 characters (base64 encoded).';
        return;
      }

      // Get our keypair for auto-signing
      const keypair = localStorage.getItem(STORAGE_KEY);
      if (!keypair || keypair.length !== 132) {
        resultDiv.style.display = 'block';
        resultDiv.style.background = '#f8514933';
        resultDiv.style.border = '1px solid #f85149';
        resultDiv.textContent = 'No valid keypair in browser storage. Cannot initiate merge.';
        return;
      }

      resultDiv.style.display = 'block';
      resultDiv.style.background = '#21262d';
      resultDiv.style.border = '1px solid #30363d';
      resultDiv.textContent = 'Creating merge request and signing...';

      try {
        // Step 1: Create merge request to get challenge
        const initResponse = await window.fetch(getBasePath() + '/api/user/merge/initiate', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ targetPublicKey: targetPubkey })
        });

        const initData = await initResponse.json();

        if (!initResponse.ok) {
          resultDiv.style.background = '#f8514933';
          resultDiv.style.border = '1px solid #f85149';
          resultDiv.textContent = initData.error || 'Failed to create merge request';
          return;
        }

        // Step 2: Sign the challenge with our keypair
        const publicKey = keypair.substring(0, 44);
        const secretKeyB64 = keypair.substring(44);
        const challenge = initData.challenge;

        // Wait for signing library
        let attempts = 0;
        while (!window.signChallenge && attempts < 50) {
          await new Promise(r => setTimeout(r, 100));
          attempts++;
        }

        if (!window.signChallenge) {
          throw new Error('Signing library not loaded. Please refresh and try again.');
        }

        const signature = await window.signChallenge(challenge, secretKeyB64);

        // Step 3: Submit our signature
        const confirmResponse = await window.fetch(getBasePath() + '/api/user/merge/' + initData.mergeId + '/confirm', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ signature, publicKey })
        });

        const confirmData = await confirmResponse.json();

        if (!confirmResponse.ok) {
          resultDiv.style.background = '#f8514933';
          resultDiv.style.border = '1px solid #f85149';
          resultDiv.textContent = confirmData.error || 'Failed to sign merge request';
          return;
        }

        // Success - show merge link
        const mergeUrl = window.location.origin + getBasePath() + '/merge/' + initData.mergeId;

        resultDiv.style.background = '#23863633';
        resultDiv.style.border = '1px solid #238636';
        resultDiv.innerHTML = \`
          <strong style="color: #238636;">✓ Merge Request Created & Signed!</strong>
          <p style="margin: 10px 0; color: #c9d1d9;">Your signature is recorded. Now open this link in your <strong>other account's browser</strong> to complete the merge:</p>
          <div style="background: #0d1117; padding: 10px; border-radius: 4px; margin: 10px 0; word-break: break-all;">
            <a href="\${mergeUrl}" style="color: #58a6ff;">\${mergeUrl}</a>
          </div>
          <p style="color: #8b949e; font-size: 12px;">Expires: \${new Date(initData.expiresAt).toLocaleString()}</p>
          <div style="margin-top: 10px;">
            <button onclick="copyMergeLink('\${mergeUrl}')" class="secondary" style="padding: 6px 12px; font-size: 12px;">📋 Copy Link</button>
          </div>
          <div style="margin-top: 15px; padding: 12px; background: #1f6feb33; border: 1px solid #1f6feb; border-radius: 6px;">
            <strong>Next step:</strong> Open the link above in the browser where your <em>other</em> ANProto identity is stored. It will auto-detect and offer one-click confirmation.
          </div>
        \`;

      } catch (err) {
        resultDiv.style.background = '#f8514933';
        resultDiv.style.border = '1px solid #f85149';
        resultDiv.textContent = 'Error: ' + err.message;
      }
    }

    function copyMergeLink(url) {
      navigator.clipboard.writeText(url).then(() => {
        alert('Merge link copied to clipboard!');
      });
    }

    function copyFullKeypair() {
      const keypair = localStorage.getItem(STORAGE_KEY);
      if (!keypair) {
        alert('No keypair found in browser storage.');
        return;
      }
      navigator.clipboard.writeText(keypair).then(() => {
        alert('Full keypair (132 chars) copied to clipboard!');
      }).catch(() => {
        prompt('Copy manually:', keypair);
      });
    }

    function copyPubkey() {
      const keypair = localStorage.getItem(STORAGE_KEY);
      if (!keypair) {
        alert('No keypair found in browser storage.');
        return;
      }
      const publicKey = keypair.substring(0, 44);
      navigator.clipboard.writeText(publicKey).then(() => {
        alert('Public key (44 chars) copied to clipboard!');
      }).catch(() => {
        prompt('Copy manually:', publicKey);
      });
    }

    function downloadKeypair() {
      const keypair = localStorage.getItem(STORAGE_KEY);
      if (!keypair) {
        alert('No keypair found in browser storage.');
        return;
      }

      const blob = new Blob([keypair], { type: 'text/plain' });
      const url = URL.createObjectURL(blob);
      const a = document.createElement('a');
      a.href = url;
      a.download = \`pear-git-keypair-\${Date.now()}.txt\`;
      a.click();
      URL.revokeObjectURL(url);
    }

    function showKeypair() {
      const keypair = localStorage.getItem(STORAGE_KEY);
      if (!keypair) {
        alert('No keypair found in browser storage. Your identity may have been created on a different browser or cleared.');
        return;
      }

      const display = document.getElementById('keypair-display');
      const keypairText = document.getElementById('keypair-text');
      const pubkeyText = document.getElementById('pubkey-text');

      if (display.style.display === 'none') {
        const publicKey = keypair.substring(0, 44);
        pubkeyText.textContent = publicKey;
        keypairText.textContent = keypair;
        display.style.display = 'block';
      } else {
        display.style.display = 'none';
      }
    }

    // Load profile on page load
    loadUserProfile();
  `

  return renderPage('User Profile', content, { pathPrefix, activePage: 'profile', css: PAGE_CSS, scripts, maxWidth: 1000 })
}