Add picker mode — click any element to capture as code snippet in issue
All checks were successful
Deploy to dev.bl.pixeldev.eu / deploy (push) Successful in 2s
All checks were successful
Deploy to dev.bl.pixeldev.eu / deploy (push) Successful in 2s
This commit is contained in:
parent
39cb25112b
commit
838e8dbcb7
155
server.js
155
server.js
@ -3,7 +3,21 @@ const fs = require('fs');
|
|||||||
const http = require('http');
|
const http = require('http');
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
|
||||||
|
// Capture raw body for webhook signature verification before JSON parsing
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (req.path === '/webhook/gitea') {
|
||||||
|
let raw = '';
|
||||||
|
req.on('data', chunk => { raw += chunk; });
|
||||||
|
req.on('end', () => {
|
||||||
|
req.rawBody = raw;
|
||||||
|
try { req.body = JSON.parse(raw); } catch { req.body = {}; }
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
express.json()(req, res, next);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const PORT = parseInt(process.env.PORT || '3060');
|
const PORT = parseInt(process.env.PORT || '3060');
|
||||||
const BASE = (process.env.BASE_PATH || '/feedback-tool').replace(/\/$/, '');
|
const BASE = (process.env.BASE_PATH || '/feedback-tool').replace(/\/$/, '');
|
||||||
@ -41,7 +55,7 @@ function giteaRequest(method, path, body) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET /api/issues?repo=owner/repo — list open issues
|
// GET /api/issues?repo=owner/repo
|
||||||
app.get(['/api/issues', BASE + '/api/issues'], async (req, res) => {
|
app.get(['/api/issues', BASE + '/api/issues'], async (req, res) => {
|
||||||
const { repo } = req.query;
|
const { repo } = req.query;
|
||||||
if (!repo) return res.status(400).json({ error: 'repo required' });
|
if (!repo) return res.status(400).json({ error: 'repo required' });
|
||||||
@ -93,6 +107,7 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
|
|||||||
return { repo: s && s.getAttribute('data-repo') };
|
return { repo: s && s.getAttribute('data-repo') };
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// ── Styles ────────────────────────────────────────────────────────────────
|
||||||
var style = document.createElement('style');
|
var style = document.createElement('style');
|
||||||
style.textContent = \`
|
style.textContent = \`
|
||||||
#_fb-fab { position:fixed; bottom:1.5rem; right:1.5rem; width:54px; height:54px; border-radius:50%;
|
#_fb-fab { position:fixed; bottom:1.5rem; right:1.5rem; width:54px; height:54px; border-radius:50%;
|
||||||
@ -137,13 +152,43 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
|
|||||||
.fb-comment-btn:hover { background:#333; opacity:1; }
|
.fb-comment-btn:hover { background:#333; opacity:1; }
|
||||||
.fb-empty { color:#555; font-size:.85rem; text-align:center; padding:1.5rem 0; }
|
.fb-empty { color:#555; font-size:.85rem; text-align:center; padding:1.5rem 0; }
|
||||||
.fb-loading { color:#555; font-size:.85rem; text-align:center; padding:1rem 0; }
|
.fb-loading { color:#555; font-size:.85rem; text-align:center; padding:1rem 0; }
|
||||||
|
/* Picker row inside new-issue form */
|
||||||
|
.fb-picker-row { display:flex; align-items:center; gap:.6rem; }
|
||||||
|
.fb-picker-btn { background:#1a2a1a; border:1px solid #2a4a2a; color:#6fcf6f; font-size:.78rem;
|
||||||
|
padding:.4rem .7rem; border-radius:6px; cursor:pointer; white-space:nowrap; flex-shrink:0; }
|
||||||
|
.fb-picker-btn:hover { background:#223322; }
|
||||||
|
.fb-picker-hint { color:#444; font-size:.75rem; }
|
||||||
|
.fb-snippet-preview { background:#0a0a0a; border:1px solid #2a2a2a; border-radius:6px;
|
||||||
|
padding:.5rem .7rem; font-size:.72rem; color:#7c9ef5; font-family:monospace;
|
||||||
|
white-space:pre-wrap; word-break:break-all; max-height:80px; overflow-y:auto; }
|
||||||
|
/* Picker overlay */
|
||||||
|
#_fb-picker-shield { display:none; position:fixed; inset:0; z-index:199999; cursor:crosshair; }
|
||||||
|
#_fb-picker-shield.active { display:block; }
|
||||||
|
#_fb-picker-toast { position:fixed; top:1rem; left:50%; transform:translateX(-50%);
|
||||||
|
background:#1a1a1a; border:1px solid #7c9ef5; border-radius:8px;
|
||||||
|
padding:.6rem 1.1rem; color:#e0e0e0; font-size:.85rem; z-index:200000;
|
||||||
|
font-family:'Segoe UI',system-ui,sans-serif; display:none; box-shadow:0 4px 16px rgba(0,0,0,.5); }
|
||||||
|
#_fb-picker-toast.active { display:block; }
|
||||||
|
._fb-hover-highlight { outline:2px solid #7c9ef5 !important; outline-offset:2px !important; }
|
||||||
\`;
|
\`;
|
||||||
document.head.appendChild(style);
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
// ── FAB ───────────────────────────────────────────────────────────────────
|
||||||
var fab = document.createElement('button');
|
var fab = document.createElement('button');
|
||||||
fab.id = '_fb-fab'; fab.innerHTML = '💬'; fab.title = 'Feedback';
|
fab.id = '_fb-fab'; fab.innerHTML = '💬'; fab.title = 'Feedback';
|
||||||
document.body.appendChild(fab);
|
document.body.appendChild(fab);
|
||||||
|
|
||||||
|
// ── Picker shield (invisible overlay during pick mode) ────────────────────
|
||||||
|
var shield = document.createElement('div');
|
||||||
|
shield.id = '_fb-picker-shield';
|
||||||
|
document.body.appendChild(shield);
|
||||||
|
|
||||||
|
var toast = document.createElement('div');
|
||||||
|
toast.id = '_fb-picker-toast';
|
||||||
|
toast.textContent = '🎯 Click any element to capture it — Esc to cancel';
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
|
||||||
|
// ── Main overlay ──────────────────────────────────────────────────────────
|
||||||
var overlay = document.createElement('div');
|
var overlay = document.createElement('div');
|
||||||
overlay.id = '_fb-overlay';
|
overlay.id = '_fb-overlay';
|
||||||
overlay.innerHTML = \`
|
overlay.innerHTML = \`
|
||||||
@ -161,6 +206,13 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
|
|||||||
<div style="display:flex;flex-direction:column;gap:.8rem;">
|
<div style="display:flex;flex-direction:column;gap:.8rem;">
|
||||||
<input class="fb-input" id="_fb-title" type="text" placeholder="Title (required)" />
|
<input class="fb-input" id="_fb-title" type="text" placeholder="Title (required)" />
|
||||||
<textarea class="fb-textarea" id="_fb-desc" placeholder="Describe your feedback…"></textarea>
|
<textarea class="fb-textarea" id="_fb-desc" placeholder="Describe your feedback…"></textarea>
|
||||||
|
<div class="fb-picker-row">
|
||||||
|
<button class="fb-picker-btn" id="_fb-pick-btn">🎯 Pick element</button>
|
||||||
|
<span class="fb-picker-hint" id="_fb-pick-hint">Click to reference a page element</span>
|
||||||
|
</div>
|
||||||
|
<div id="_fb-snippet-wrap" style="display:none;">
|
||||||
|
<div class="fb-snippet-preview" id="_fb-snippet"></div>
|
||||||
|
</div>
|
||||||
<button class="fb-btn" id="_fb-submit">Submit Issue</button>
|
<button class="fb-btn" id="_fb-submit">Submit Issue</button>
|
||||||
<div class="fb-status" id="_fb-status"></div>
|
<div class="fb-status" id="_fb-status"></div>
|
||||||
</div>
|
</div>
|
||||||
@ -173,6 +225,7 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
|
|||||||
\`;
|
\`;
|
||||||
document.body.appendChild(overlay);
|
document.body.appendChild(overlay);
|
||||||
|
|
||||||
|
// ── Open / close ──────────────────────────────────────────────────────────
|
||||||
function openOverlay() { overlay.classList.add('open'); }
|
function openOverlay() { overlay.classList.add('open'); }
|
||||||
function closeOverlay() { overlay.classList.remove('open'); }
|
function closeOverlay() { overlay.classList.remove('open'); }
|
||||||
|
|
||||||
@ -183,6 +236,7 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
|
|||||||
overlay.addEventListener('click', function(e) { if (e.target === overlay) closeOverlay(); });
|
overlay.addEventListener('click', function(e) { if (e.target === overlay) closeOverlay(); });
|
||||||
document.getElementById('_fb-hclose').addEventListener('click', closeOverlay);
|
document.getElementById('_fb-hclose').addEventListener('click', closeOverlay);
|
||||||
|
|
||||||
|
// ── Tabs ──────────────────────────────────────────────────────────────────
|
||||||
var currentTab = 'new';
|
var currentTab = 'new';
|
||||||
document.querySelectorAll('.fb-tab').forEach(function(btn) {
|
document.querySelectorAll('.fb-tab').forEach(function(btn) {
|
||||||
btn.addEventListener('click', function() {
|
btn.addEventListener('click', function() {
|
||||||
@ -195,6 +249,99 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Picker mode ───────────────────────────────────────────────────────────
|
||||||
|
var pickerActive = false;
|
||||||
|
var pickerHighlighted = null;
|
||||||
|
var capturedSnippet = '';
|
||||||
|
|
||||||
|
function getSelector(el) {
|
||||||
|
if (!el || el === document.body) return 'body';
|
||||||
|
var tag = el.tagName.toLowerCase();
|
||||||
|
var id = el.id ? '#' + el.id : '';
|
||||||
|
var cls = el.className && typeof el.className === 'string'
|
||||||
|
? '.' + el.className.trim().split(/\\s+/).slice(0,2).join('.') : '';
|
||||||
|
return tag + id + cls;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSnippet(el) {
|
||||||
|
// Outer HTML truncated sensibly
|
||||||
|
var html = el.outerHTML || '';
|
||||||
|
if (html.length > 500) {
|
||||||
|
// Just show tag + attrs + content preview
|
||||||
|
var inner = (el.textContent || '').trim().slice(0, 120);
|
||||||
|
var m = html.match(/^(<[^>]+>)/);
|
||||||
|
html = (m ? m[1] : '<' + el.tagName.toLowerCase() + '>') + (inner ? '\\n ' + inner + '…' : '') + '\\n</' + el.tagName.toLowerCase() + '>';
|
||||||
|
}
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
function activatePicker() {
|
||||||
|
pickerActive = true;
|
||||||
|
overlay.classList.remove('open');
|
||||||
|
shield.classList.add('active');
|
||||||
|
toast.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function deactivatePicker(snippet) {
|
||||||
|
pickerActive = false;
|
||||||
|
shield.classList.remove('active');
|
||||||
|
toast.classList.remove('active');
|
||||||
|
if (pickerHighlighted) {
|
||||||
|
pickerHighlighted.classList.remove('_fb-hover-highlight');
|
||||||
|
pickerHighlighted = null;
|
||||||
|
}
|
||||||
|
overlay.classList.add('open');
|
||||||
|
if (snippet) {
|
||||||
|
capturedSnippet = snippet;
|
||||||
|
document.getElementById('_fb-snippet').textContent = snippet;
|
||||||
|
document.getElementById('_fb-snippet-wrap').style.display = '';
|
||||||
|
document.getElementById('_fb-pick-hint').textContent = '✓ Element captured — you can pick again';
|
||||||
|
// Append to description
|
||||||
|
var desc = document.getElementById('_fb-desc');
|
||||||
|
var tag = '\\n\\n**Picked element:**\\n\`\`\`html\\n' + snippet + '\\n\`\`\`';
|
||||||
|
if (!desc.value.includes(tag)) desc.value += tag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('_fb-pick-btn').addEventListener('click', function() {
|
||||||
|
activatePicker();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mousemove on page during picker: highlight hovered element
|
||||||
|
document.addEventListener('mousemove', function(e) {
|
||||||
|
if (!pickerActive) return;
|
||||||
|
var el = document.elementFromPoint(e.clientX, e.clientY);
|
||||||
|
// Skip our own UI elements
|
||||||
|
if (!el || el.closest('#_fb-picker-shield') || el.closest('#_fb-picker-toast')) return;
|
||||||
|
if (pickerHighlighted && pickerHighlighted !== el) {
|
||||||
|
pickerHighlighted.classList.remove('_fb-hover-highlight');
|
||||||
|
}
|
||||||
|
pickerHighlighted = el;
|
||||||
|
el.classList.add('_fb-hover-highlight');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Click during picker: capture element
|
||||||
|
shield.addEventListener('click', function(e) {
|
||||||
|
if (!pickerActive) return;
|
||||||
|
e.preventDefault(); e.stopPropagation();
|
||||||
|
// Find element underneath the shield
|
||||||
|
shield.style.display = 'none';
|
||||||
|
var el = document.elementFromPoint(e.clientX, e.clientY);
|
||||||
|
shield.style.display = '';
|
||||||
|
if (el && !el.closest('#_fb-picker-toast')) {
|
||||||
|
el.classList.remove('_fb-hover-highlight');
|
||||||
|
deactivatePicker(getSnippet(el));
|
||||||
|
} else {
|
||||||
|
deactivatePicker(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Esc cancels picker
|
||||||
|
document.addEventListener('keydown', function(e) {
|
||||||
|
if (e.key === 'Escape' && pickerActive) deactivatePicker(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// ── Submit ────────────────────────────────────────────────────────────────
|
||||||
document.getElementById('_fb-submit').addEventListener('click', function() {
|
document.getElementById('_fb-submit').addEventListener('click', function() {
|
||||||
var title = document.getElementById('_fb-title').value.trim();
|
var title = document.getElementById('_fb-title').value.trim();
|
||||||
var body = document.getElementById('_fb-desc').value.trim();
|
var body = document.getElementById('_fb-desc').value.trim();
|
||||||
@ -211,6 +358,9 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
|
|||||||
status.textContent='✓ Issue #' + d.issue_number + ' created!';
|
status.textContent='✓ Issue #' + d.issue_number + ' created!';
|
||||||
document.getElementById('_fb-title').value = '';
|
document.getElementById('_fb-title').value = '';
|
||||||
document.getElementById('_fb-desc').value = '';
|
document.getElementById('_fb-desc').value = '';
|
||||||
|
document.getElementById('_fb-snippet-wrap').style.display = 'none';
|
||||||
|
document.getElementById('_fb-pick-hint').textContent = 'Click to reference a page element';
|
||||||
|
capturedSnippet = '';
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
setTimeout(function() { status.textContent=''; }, 3000);
|
setTimeout(function() { status.textContent=''; }, 3000);
|
||||||
} else {
|
} else {
|
||||||
@ -219,6 +369,7 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
|
|||||||
}).catch(function() { status.style.color='#f57c7c'; status.textContent='Network error.'; btn.disabled=false; });
|
}).catch(function() { status.style.color='#f57c7c'; status.textContent='Network error.'; btn.disabled=false; });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── Open issues tab ───────────────────────────────────────────────────────
|
||||||
function loadIssues() {
|
function loadIssues() {
|
||||||
var list = document.getElementById('_fb-issues-list');
|
var list = document.getElementById('_fb-issues-list');
|
||||||
list.innerHTML = '<div class="fb-loading">Loading…</div>';
|
list.innerHTML = '<div class="fb-loading">Loading…</div>';
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user