OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git / lib / issue-modal-ui.js

656 lines · javascript ref:
/**
 * Render Issue Creation Modal
 *
 * Full-featured issue creator with:
 * - Markdown editor with toolbar
 * - Image upload support
 * - Live preview
 * - Formatting buttons
 */
export function renderIssueModal() {
  return `
    <style>
      .issue-modal-overlay {
        display: none;
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        bottom: 0;
        background: rgba(0, 0, 0, 0.8);
        z-index: 1000;
        overflow-y: auto;
      }

      .issue-modal-overlay.active {
        display: flex;
        align-items: flex-start;
        justify-content: center;
        padding: 40px 20px;
      }

      .issue-modal {
        background: #0d1117;
        border: 1px solid #30363d;
        border-radius: 6px;
        max-width: 900px;
        width: 100%;
        margin: auto;
      }

      .issue-modal-header {
        padding: 20px;
        border-bottom: 1px solid #30363d;
        display: flex;
        justify-content: space-between;
        align-items: center;
      }

      .issue-modal-header h2 {
        margin: 0;
        font-size: 20px;
        color: #c9d1d9;
      }

      .issue-modal-close {
        background: transparent;
        border: none;
        color: #8b949e;
        cursor: pointer;
        font-size: 24px;
        padding: 0;
        width: 32px;
        height: 32px;
        border-radius: 6px;
      }

      .issue-modal-close:hover {
        background: #161b22;
        color: #c9d1d9;
      }

      .issue-modal-body {
        padding: 20px;
      }

      .issue-form-group {
        margin-bottom: 20px;
      }

      .issue-form-group label {
        display: block;
        margin-bottom: 8px;
        color: #c9d1d9;
        font-size: 14px;
        font-weight: 600;
      }

      .issue-form-group input[type="text"] {
        width: 100%;
        background: #0d1117;
        border: 1px solid #30363d;
        border-radius: 6px;
        padding: 8px 12px;
        color: #c9d1d9;
        font-size: 14px;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      }

      .issue-form-group input[type="text"]:focus {
        outline: none;
        border-color: #58a6ff;
      }

      .editor-toolbar {
        display: flex;
        gap: 4px;
        padding: 8px;
        background: #161b22;
        border: 1px solid #30363d;
        border-radius: 6px 6px 0 0;
        border-bottom: none;
      }

      .editor-toolbar button {
        background: transparent;
        border: 1px solid transparent;
        color: #8b949e;
        cursor: pointer;
        padding: 6px 10px;
        border-radius: 6px;
        font-size: 13px;
        font-weight: 500;
      }

      .editor-toolbar button:hover {
        background: #0d1117;
        border-color: #30363d;
        color: #c9d1d9;
      }

      .editor-toolbar button:active {
        background: #010409;
      }

      .editor-toolbar .separator {
        width: 1px;
        background: #30363d;
        margin: 0 4px;
      }

      #issue-description {
        width: 100%;
        min-height: 200px;
        background: #0d1117;
        border: 1px solid #30363d;
        border-radius: 0 0 6px 6px;
        padding: 12px;
        color: #c9d1d9;
        font-size: 14px;
        font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, monospace;
        resize: vertical;
      }

      #issue-description:focus {
        outline: none;
        border-color: #58a6ff;
      }

      .editor-tabs {
        display: flex;
        gap: 8px;
        padding: 8px;
        background: #161b22;
        border: 1px solid #30363d;
        border-radius: 6px 6px 0 0;
        border-bottom: none;
      }

      .editor-tab {
        background: transparent;
        border: none;
        color: #8b949e;
        cursor: pointer;
        padding: 6px 12px;
        border-radius: 6px;
        font-size: 13px;
      }

      .editor-tab.active {
        background: #0d1117;
        color: #c9d1d9;
      }

      .editor-tab:hover:not(.active) {
        color: #c9d1d9;
      }

      .editor-content {
        display: none;
      }

      .editor-content.active {
        display: block;
      }

      #issue-preview {
        min-height: 200px;
        background: #0d1117;
        border: 1px solid #30363d;
        border-radius: 0 0 6px 6px;
        padding: 12px;
        color: #c9d1d9;
        font-size: 14px;
        line-height: 1.6;
      }

      #issue-preview img {
        max-width: 100%;
        border-radius: 6px;
        margin: 10px 0;
      }

      #issue-preview code {
        background: #161b22;
        padding: 2px 6px;
        border-radius: 3px;
        font-family: monospace;
        font-size: 12px;
      }

      #issue-preview pre {
        background: #161b22;
        padding: 16px;
        border-radius: 6px;
        overflow-x: auto;
      }

      #issue-preview pre code {
        background: none;
        padding: 0;
      }

      #issue-preview em {
        color: #8b949e;
        font-style: italic;
      }

      .image-upload-area {
        display: flex;
        gap: 10px;
        align-items: center;
        margin-top: 10px;
      }

      .image-upload-btn {
        background: #238636;
        color: white;
        border: none;
        padding: 8px 16px;
        border-radius: 6px;
        cursor: pointer;
        font-size: 13px;
        font-weight: 500;
      }

      .image-upload-btn:hover {
        background: #2ea043;
      }

      .image-preview {
        display: flex;
        gap: 10px;
        flex-wrap: wrap;
        margin-top: 10px;
      }

      .image-preview-item {
        position: relative;
        width: 100px;
        height: 100px;
        border: 1px solid #30363d;
        border-radius: 6px;
        overflow: hidden;
      }

      .image-preview-item img {
        width: 100%;
        height: 100%;
        object-fit: cover;
      }

      .image-preview-remove {
        position: absolute;
        top: 4px;
        right: 4px;
        background: rgba(0, 0, 0, 0.7);
        color: white;
        border: none;
        border-radius: 3px;
        width: 20px;
        height: 20px;
        cursor: pointer;
        font-size: 14px;
        line-height: 1;
        padding: 0;
      }

      .image-preview-remove:hover {
        background: rgba(255, 0, 0, 0.8);
      }

      .issue-modal-footer {
        padding: 20px;
        border-top: 1px solid #30363d;
        display: flex;
        justify-content: flex-end;
        gap: 10px;
      }

      .issue-modal-footer button {
        padding: 8px 20px;
        border-radius: 6px;
        font-size: 14px;
        font-weight: 500;
        cursor: pointer;
      }

      .issue-modal-cancel {
        background: transparent;
        border: 1px solid #30363d;
        color: #c9d1d9;
      }

      .issue-modal-cancel:hover {
        background: #161b22;
      }

      .issue-modal-submit {
        background: #238636;
        border: none;
        color: white;
      }

      .issue-modal-submit:hover {
        background: #2ea043;
      }

      .issue-modal-submit:disabled {
        background: #161b22;
        color: #8b949e;
        cursor: not-allowed;
      }
    </style>

    <div class="issue-modal-overlay" id="issue-modal-overlay">
      <div class="issue-modal">
        <div class="issue-modal-header">
          <h2>New Issue</h2>
          <button class="issue-modal-close" onclick="closeIssueModal()">&times;</button>
        </div>

        <div class="issue-modal-body">
          <form id="issue-form" onsubmit="submitIssue(event)">
            <div class="issue-form-group">
              <label for="issue-title">Title</label>
              <input
                type="text"
                id="issue-title"
                placeholder="Brief description of the issue"
                required
                autofocus
              />
            </div>

            <div class="issue-form-group">
              <label>Description</label>

              <div class="editor-tabs">
                <button type="button" class="editor-tab active" onclick="switchEditorTab('write')">Write</button>
                <button type="button" class="editor-tab" onclick="switchEditorTab('preview')">Preview</button>
              </div>

              <div class="editor-content active" id="write-tab">
                <div class="editor-toolbar">
                  <button type="button" onclick="insertMarkdown('bold')" title="Bold">
                    <strong>B</strong>
                  </button>
                  <button type="button" onclick="insertMarkdown('italic')" title="Italic">
                    <em>I</em>
                  </button>
                  <button type="button" onclick="insertMarkdown('code')" title="Code">
                    &lt;/&gt;
                  </button>
                  <div class="separator"></div>
                  <button type="button" onclick="insertMarkdown('link')" title="Link">
                    🔗
                  </button>
                  <button type="button" onclick="document.getElementById('issue-image-upload').click()" title="Upload Image">
                    📷
                  </button>
                </div>

                <textarea
                  id="issue-description"
                  placeholder="Describe the issue in detail. You can use markdown formatting."
                  oninput="updatePreview()"
                ></textarea>

                <input
                  type="file"
                  id="issue-image-upload"
                  accept="image/*"
                  multiple
                  style="display: none"
                  onchange="handleImageUpload(event)"
                />

                <div class="image-preview" id="image-preview"></div>
              </div>

              <div class="editor-content" id="preview-tab">
                <div id="issue-preview">
                  <em>Nothing to preview</em>
                </div>
              </div>
            </div>
          </form>
        </div>

        <div class="issue-modal-footer">
          <button type="button" class="issue-modal-cancel" onclick="closeIssueModal()">
            Cancel
          </button>
          <button type="submit" form="issue-form" class="issue-modal-submit" id="issue-submit-btn">
            Create Issue
          </button>
        </div>
      </div>
    </div>

    <script>
      let uploadedImages = [];

      function openIssueModal() {
        document.getElementById('issue-modal-overlay').classList.add('active');
        document.getElementById('issue-title').focus();
      }

      function closeIssueModal() {
        document.getElementById('issue-modal-overlay').classList.remove('active');
        document.getElementById('issue-form').reset();
        document.getElementById('image-preview').innerHTML = '';
        uploadedImages = [];
        updatePreview();
      }

      function switchEditorTab(tab) {
        const tabs = document.querySelectorAll('.editor-tab');
        const contents = document.querySelectorAll('.editor-content');

        tabs.forEach(t => t.classList.remove('active'));
        contents.forEach(c => c.classList.remove('active'));

        if (tab === 'write') {
          tabs[0].classList.add('active');
          contents[0].classList.add('active');
        } else {
          tabs[1].classList.add('active');
          contents[1].classList.add('active');
          updatePreview();
        }
      }

      function insertMarkdown(type) {
        const textarea = document.getElementById('issue-description');
        const start = textarea.selectionStart;
        const end = textarea.selectionEnd;
        const text = textarea.value;
        const selectedText = text.substring(start, end) || 'text';

        let before = '', after = '';

        switch (type) {
          case 'bold':
            before = '**';
            after = '**';
            break;
          case 'italic':
            before = '*';
            after = '*';
            break;
          case 'code':
            before = '\`';
            after = '\`';
            break;
          case 'link':
            before = '[';
            after = '](url)';
            break;
        }

        const newText = text.substring(0, start) + before + selectedText + after + text.substring(end);
        textarea.value = newText;

        // Set cursor position
        const cursorPos = start + before.length + selectedText.length;
        textarea.focus();
        textarea.setSelectionRange(cursorPos, cursorPos);

        updatePreview();
      }

      async function handleImageUpload(event) {
        const files = event.target.files;
        const preview = document.getElementById('image-preview');

        for (const file of files) {
          if (!file.type.startsWith('image/')) continue;

          const reader = new FileReader();
          reader.onload = (e) => {
            const dataUrl = e.target.result;
            uploadedImages.push(dataUrl);

            // Add preview
            const item = document.createElement('div');
            item.className = 'image-preview-item';
            item.innerHTML = \`
              <img src="\${dataUrl}" />
              <button class="image-preview-remove" onclick="removeImage(\${uploadedImages.length - 1})">&times;</button>
            \`;
            preview.appendChild(item);

            updatePreview();
          };
          reader.readAsDataURL(file);
        }

        // Reset input
        event.target.value = '';
      }

      function removeImage(index) {
        uploadedImages.splice(index, 1);
        const preview = document.getElementById('image-preview');
        preview.innerHTML = '';

        uploadedImages.forEach((dataUrl, i) => {
          const item = document.createElement('div');
          item.className = 'image-preview-item';
          item.innerHTML = \`
            <img src="\${dataUrl}" />
            <button class="image-preview-remove" onclick="removeImage(\${i})">&times;</button>
          \`;
          preview.appendChild(item);
        });

        updatePreview();
      }

      function updatePreview() {
        const text = document.getElementById('issue-description').value;
        const preview = document.getElementById('issue-preview');

        if (!text && uploadedImages.length === 0) {
          preview.innerHTML = '<em>Nothing to preview</em>';
          return;
        }

        // Parse markdown (simple implementation)
        let html = escapeHtml(text);

        // Bold
        html = html.replace(/\\*\\*([^*]+)\\*\\*/g, '<strong>$1</strong>');
        html = html.replace(/__([^_]+)__/g, '<strong>$1</strong>');

        // Italic
        html = html.replace(/\\*([^*]+)\\*/g, '<em>$1</em>');
        html = html.replace(/_([^_]+)_/g, '<em>$1</em>');

        // Code
        html = html.replace(/\`([^\`]+)\`/g, '<code>$1</code>');

        // Code blocks
        html = html.replace(/\`\`\`([^\`]+)\`\`\`/g, '<pre><code>$1</code></pre>');

        // Line breaks
        html = html.replace(/\\n/g, '<br>');

        // Add images
        if (uploadedImages.length > 0) {
          html += '<br>';
          uploadedImages.forEach(dataUrl => {
            html += \`<img src="\${dataUrl}" />\`;
          });
        }

        preview.innerHTML = html;
      }

      function escapeHtml(text) {
        const div = document.createElement('div');
        div.textContent = text;
        return div.innerHTML;
      }

      async function submitIssue(event) {
        event.preventDefault();

        const title = document.getElementById('issue-title').value.trim();
        const description = document.getElementById('issue-description').value.trim();

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

        const submitBtn = document.getElementById('issue-submit-btn');
        submitBtn.disabled = true;
        submitBtn.textContent = 'Creating...';

        try {
          const response = await window.fetch(window.location.pathname + '/issues', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({
              title,
              description,
              images: uploadedImages
            })
          });

          const result = await response.json();

          if (response.ok) {
            closeIssueModal();
            window.location.reload();
          } else {
            alert('Error: ' + (result.error || 'Failed to create issue'));
            submitBtn.disabled = false;
            submitBtn.textContent = 'Create Issue';
          }
        } catch (err) {
          alert('Error: ' + err.message);
          submitBtn.disabled = false;
          submitBtn.textContent = 'Create Issue';
        }
      }

      // Close modal on overlay click
      document.getElementById('issue-modal-overlay').addEventListener('click', (e) => {
        if (e.target.id === 'issue-modal-overlay') {
          closeIssueModal();
        }
      });

      // Close modal on Escape key
      document.addEventListener('keydown', (e) => {
        if (e.key === 'Escape' && document.getElementById('issue-modal-overlay').classList.contains('active')) {
          closeIssueModal();
        }
      });
    </script>
  `;
}