OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / lib / join-ui.js

157 lines · javascript ref:
/**
 * Join UI — page for joining a group from another server via invite
 */

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

const PAGE_CSS = `
    h1 { font-size: 28px; margin-bottom: 10px; }
    .subtitle { color: #8b949e; margin-bottom: 30px; }
    .card h2 { font-size: 18px; margin-bottom: 16px; }
    .form-input, .form-textarea {
      width: 100%; padding: 10px 14px; background: #0d1117;
      border: 1px solid #30363d; border-radius: 6px;
      color: #c9d1d9; font-size: 14px; font-family: monospace;
    }
    .form-input:focus, .form-textarea:focus { outline: none; border-color: #58a6ff; }
    .form-textarea { min-height: 80px; resize: vertical; }
    .step { display: none; }
    .step.active { display: block; }
    .group-preview {
      background: #0d1117; border: 1px solid #30363d;
      border-radius: 6px; padding: 16px; margin-bottom: 16px;
    }
    .group-preview h3 { font-size: 16px; margin-bottom: 8px; color: #58a6ff; }
    .group-preview p { font-size: 14px; color: #8b949e; }
    .repo-checkbox {
      display: flex; align-items: center; gap: 10px;
      padding: 10px 14px; background: #0d1117; border: 1px solid #30363d;
      border-radius: 6px; margin-bottom: 8px; cursor: pointer;
    }
    .repo-checkbox:hover { border-color: #58a6ff; }
    .repo-checkbox input[type="checkbox"] { width: 16px; height: 16px; accent-color: #238636; }
    .repo-checkbox label { flex: 1; cursor: pointer; font-size: 14px; }
    .success-box {
      background: #0d1117; border: 1px solid #238636;
      border-radius: 6px; padding: 20px; text-align: center;
    }
    .success-box h3 { color: #238636; font-size: 20px; margin-bottom: 10px; }
    .toast {
      position: fixed; bottom: 20px; right: 20px;
      background: #238636; color: white; padding: 12px 20px;
      border-radius: 6px; font-size: 14px; z-index: 2000;
      opacity: 0; transform: translateY(10px);
      transition: opacity 0.3s, transform 0.3s;
    }
    .toast.visible { opacity: 1; transform: translateY(0); }
    .toast.error { background: #da3633; }
`

export function renderJoinPage (pathPrefix = '') {
  const content = `
    <h1>Join Group</h1>
    <div class="subtitle">Connect to a group on another server using an invite code</div>

    <div class="step active" id="step-invite">
      <div class="card">
        <h2>Step 1: Enter Invite Code</h2>
        <div class="form-group">
          <label for="invite-hex">Invite Hex</label>
          <textarea class="form-textarea" id="invite-hex" placeholder="Paste the invite hex from the other server..."></textarea>
        </div>
        <button onclick="connectWithInvite()" id="connect-btn">Connect</button>
      </div>
    </div>

    <div class="step" id="step-preview">
      <div class="card">
        <h2>Step 2: Select Repos to Sync</h2>
        <div id="group-preview"></div>
        <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
          <label style="font-size:14px;">Repositories</label>
          <label style="font-size:12px;color:#8b949e;cursor:pointer;" onclick="toggleAllRepos()">
            <input type="checkbox" id="select-all" checked> Select All
          </label>
        </div>
        <div id="repo-list"></div>
        <div style="display:flex;gap:10px;margin-top:16px;">
          <button onclick="confirmJoin()" id="confirm-btn">Confirm Join</button>
          <button class="secondary" onclick="goBack()">Back</button>
        </div>
      </div>
    </div>

    <div class="step" id="step-success">
      <div class="card">
        <div class="success-box">
          <h3>Joined!</h3>
          <p style="color:#8b949e;margin-bottom:16px;" id="success-msg">You have joined the group.</p>
          <a href="${pathPrefix}/groups" style="text-decoration:none;"><button>View Groups</button></a>
        </div>
      </div>
    </div>

    <div id="toast" class="toast"></div>
  `

  const scripts = `
    function getBasePath() { return '${pathPrefix}'; }
    var joinResult = null;

    async function connectWithInvite() {
      var invite = document.getElementById('invite-hex').value.trim();
      if (!invite) { showToast('Paste an invite code first', true); return; }
      var btn = document.getElementById('connect-btn');
      btn.disabled = true; btn.textContent = 'Connecting...';
      try {
        var res = await window.fetch(getBasePath() + '/api/groups/join', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ invite: invite }) });
        var data = await res.json();
        if (!res.ok) throw new Error(data.error || 'Failed to connect');
        joinResult = data;
        var preview = document.getElementById('group-preview');
        if (data.group) {
          preview.innerHTML = '<div class="group-preview"><h3>' + esc(data.group.displayName || data.group.name || data.groupName) + '</h3>' + (data.group.description ? '<p>' + esc(data.group.description) + '</p>' : '') + '<p style="margin-top:8px;">' + (data.group.repos ? data.group.repos.length : 0) + ' repos, ' + (data.group.members ? data.group.members.length : 0) + ' members</p></div>';
          var repoList = document.getElementById('repo-list');
          if (data.group.repos && data.group.repos.length > 0) {
            repoList.innerHTML = data.group.repos.map(function(name) { return '<div class="repo-checkbox"><input type="checkbox" id="repo-' + name + '" value="' + esc(name) + '" checked><label for="repo-' + name + '">' + esc(name) + '</label></div>'; }).join('');
          } else { repoList.innerHTML = '<p style="color:#8b949e;">No repos in this group yet</p>'; }
        } else {
          preview.innerHTML = '<div class="group-preview"><h3>Registry Connected</h3><p>Connected to shared registry. Group data will sync shortly.</p></div>';
          document.getElementById('repo-list').innerHTML = '<p style="color:#8b949e;">Syncing...</p>';
        }
        document.getElementById('step-invite').classList.remove('active');
        document.getElementById('step-preview').classList.add('active');
      } catch (err) { showToast(err.message, true); }
      finally { btn.disabled = false; btn.textContent = 'Connect'; }
    }

    function toggleAllRepos() {
      var checked = document.getElementById('select-all').checked;
      document.querySelectorAll('#repo-list input[type="checkbox"]').forEach(function(cb) { cb.checked = checked; });
    }

    async function confirmJoin() {
      var selectedRepos = [];
      document.querySelectorAll('#repo-list input[type="checkbox"]:checked').forEach(function(cb) { selectedRepos.push(cb.value); });
      var groupName = joinResult && joinResult.groupName;
      if (groupName && selectedRepos.length > 0) {
        var btn = document.getElementById('confirm-btn');
        btn.disabled = true; btn.textContent = 'Saving...';
        try {
          var res = await window.fetch(getBasePath() + '/api/groups/' + encodeURIComponent(groupName) + '/sync', { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ repos: selectedRepos }) });
          if (!res.ok) { var err = await res.json(); throw new Error(err.error || 'Failed to save sync config'); }
        } catch (err) { showToast(err.message, true); btn.disabled = false; btn.textContent = 'Confirm Join'; return; }
      }
      document.getElementById('success-msg').textContent = groupName ? 'Joined group "' + groupName + '" — syncing ' + selectedRepos.length + ' repo(s)' : 'Connected to shared registry';
      document.getElementById('step-preview').classList.remove('active');
      document.getElementById('step-success').classList.add('active');
    }

    function goBack() { document.getElementById('step-preview').classList.remove('active'); document.getElementById('step-invite').classList.add('active'); }
    function esc(t) { if (!t) return ''; return String(t).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
    function showToast(msg, isError) { var t = document.getElementById('toast'); t.textContent = msg; t.className = 'toast visible' + (isError ? ' error' : ''); setTimeout(function() { t.className = 'toast'; }, 3000); }
  `

  return renderPage('Join Group', content, { pathPrefix, activePage: 'groups', css: PAGE_CSS, scripts, maxWidth: 700 })
}