/**
* 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()">×</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">
</>
</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})">×</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})">×</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>
`;
}