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 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 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) => {
|
||||
const { repo } = req.query;
|
||||
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') };
|
||||
})();
|
||||
|
||||
// ── Styles ────────────────────────────────────────────────────────────────
|
||||
var style = document.createElement('style');
|
||||
style.textContent = \`
|
||||
#_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-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; }
|
||||
/* 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);
|
||||
|
||||
// ── FAB ───────────────────────────────────────────────────────────────────
|
||||
var fab = document.createElement('button');
|
||||
fab.id = '_fb-fab'; fab.innerHTML = '💬'; fab.title = 'Feedback';
|
||||
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');
|
||||
overlay.id = '_fb-overlay';
|
||||
overlay.innerHTML = \`
|
||||
@ -161,6 +206,13 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
|
||||
<div style="display:flex;flex-direction:column;gap:.8rem;">
|
||||
<input class="fb-input" id="_fb-title" type="text" placeholder="Title (required)" />
|
||||
<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>
|
||||
<div class="fb-status" id="_fb-status"></div>
|
||||
</div>
|
||||
@ -173,6 +225,7 @@ app.get(['/widget.js', BASE + '/widget.js'], (req, res) => {
|
||||
\`;
|
||||
document.body.appendChild(overlay);
|
||||
|
||||
// ── Open / close ──────────────────────────────────────────────────────────
|
||||
function openOverlay() { overlay.classList.add('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(); });
|
||||
document.getElementById('_fb-hclose').addEventListener('click', closeOverlay);
|
||||
|
||||
// ── Tabs ──────────────────────────────────────────────────────────────────
|
||||
var currentTab = 'new';
|
||||
document.querySelectorAll('.fb-tab').forEach(function(btn) {
|
||||
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() {
|
||||
var title = document.getElementById('_fb-title').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!';
|
||||
document.getElementById('_fb-title').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;
|
||||
setTimeout(function() { status.textContent=''; }, 3000);
|
||||
} 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; });
|
||||
});
|
||||
|
||||
// ── Open issues tab ───────────────────────────────────────────────────────
|
||||
function loadIssues() {
|
||||
var list = document.getElementById('_fb-issues-list');
|
||||
list.innerHTML = '<div class="fb-loading">Loading…</div>';
|
||||
|
||||
Loading…
Reference in New Issue
Block a user