/* Admin QA Tour — guided walkthrough of recent fixes
Activates with ?admin=1&tour=1 (sets cookies for persistence).
Loads tour stops from /api/qa-tour or inline data.
Each stop: element selector, feedback quote, fix description.
*/
(function() {
var params = new URLSearchParams(window.location.search);
// Set cookies if params present
if (params.has('admin')) document.cookie = 'qa_admin=1;path=/;max-age=86400';
// Clear any stale qa_tour cookie — tour only runs when ?tour=1 is in the URL
document.cookie = 'qa_tour=;path=/;max-age=0';
// tour cookie intentionally not persisted — tour only runs when ?tour=1 is in the URL
// Check URL params OR cookies (admin persists, tour does not)
var isAdmin = params.has('admin') || document.cookie.split(';').some(function(c) { return c.trim().startsWith('qa_admin='); });
var isTour = params.has('tour');
if (!isAdmin || !isTour) return;
// Tour stops — each one highlights an element and shows the feedback
var stops = [];
// Load stops, then filter out already-accepted ones
var allStops = [];
function loadAndFilter() {
// Get inline stops first
allStops = getInlineStops();
if (!allStops.length) return;
// Fetch existing verdicts from DB to filter out accepted
fetch('/api/qa-tour-verdicts?page=' + encodeURIComponent(window.location.pathname))
.then(function(r) { return r.ok ? r.json() : null; })
.then(function(data) {
if (data && data.accepted && data.accepted.length) {
// Filter out stops whose feedback text matches an accepted verdict
stops = allStops.filter(function(stop) {
return !data.accepted.some(function(accepted) {
return accepted.indexOf(stop.feedback.slice(0, 40)) !== -1;
});
});
} else {
stops = allStops;
}
if (stops.length) {
initTour();
} else {
// All stops accepted — show a brief "all clear" message
showAllClear();
}
})
.catch(function() {
stops = allStops;
if (stops.length) initTour();
});
}
function showAllClear() {
var msg = document.createElement('div');
msg.style.cssText = 'position:fixed;bottom:80px;right:24px;z-index:10001;background:#12121a;border:1px solid rgba(52,211,153,0.25);border-radius:12px;padding:16px 24px;font-family:"IBM Plex Sans",sans-serif;font-size:13px;color:rgba(255,255,255,0.75);box-shadow:0 8px 32px rgba(0,0,0,0.4);';
msg.innerHTML = '<span style="color:#34d399;">✓</span> All tour stops accepted — no open items on this page.';
document.body.appendChild(msg);
setTimeout(function() { msg.style.opacity = '0'; msg.style.transition = 'opacity 0.5s'; setTimeout(function() { msg.remove(); }, 600); }, 3000);
}
loadAndFilter();
function getInlineStops() {
// Hardcoded stops for landing page — generated from QA feedback
if (window.location.pathname !== '/') return [];
return [
{
selector: '.scroll-cue',
feedback: '"down button needs to be brighter" / "down arrow needs to be more visible"',
fix: 'Changed to accent teal color. Now clickable — scrolls to first capability section.',
action: 'Look at the bottom of the hero — the arrow and text should be teal.'
},
{
selector: '.below-search__watched',
feedback: '"return+watch clicks on properties come back to this page" / "beta+watch properties just come back to this page"',
fix: 'Property links now navigate to /search?q={address}. "view all" and "N watching" go to /portfolio.',
action: 'Switch to "Returning + Watching" or "Beta + Watching" persona and click a property name.'
},
{
selector: '.below-search__context',
feedback: '"we basically have two Do I need a permit for links — redundant"',
fix: 'Renamed context row link from "do I need a permit for..." to "common permit questions" to differentiate from the sub row anchor link.',
action: 'Compare the sub row (scrolls to section) vs context row (hover dropdown).'
},
{
selector: '.state-toggle',
feedback: '"BETA should be BETA Tester — more personal and descriptive"',
fix: 'Renamed all toggle labels: New Visitor, Beta Tester, Beta + Watching, Returning, Returning + Watching, Power User.',
action: 'Check the toggle buttons in the bottom-right corner.'
}
];
}
var currentStop = 0;
var overlay, spotlight, tooltip;
function initTour() {
// Inject styles
var style = document.createElement('style');
style.textContent = [
'#tour-overlay { position: fixed; inset: 0; z-index: 10000; pointer-events: none; }',
'#tour-spotlight {',
' position: absolute; border-radius: 8px;',
' box-shadow: 0 0 0 9999px rgba(0,0,0,0.7);',
' transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);',
' pointer-events: none;',
'}',
'#tour-tooltip {',
' position: absolute; z-index: 10001; width: 360px; max-width: calc(100vw - 48px);',
' background: #12121a; border: 1px solid rgba(94,234,212,0.25);',
' border-radius: 12px; padding: 20px; pointer-events: auto;',
' box-shadow: 0 8px 32px rgba(0,0,0,0.5);',
' font-family: "IBM Plex Sans", sans-serif;',
'}',
'#tour-tooltip .tour-quote {',
' font-family: "JetBrains Mono", monospace; font-size: 12px; font-weight: 300;',
' color: #fbbf24; font-style: italic; line-height: 1.5;',
' padding: 10px 14px; margin-bottom: 12px;',
' background: rgba(251,191,36,0.06); border-left: 2px solid rgba(251,191,36,0.3);',
' border-radius: 0 6px 6px 0;',
'}',
'#tour-tooltip .tour-fix {',
' font-size: 13px; font-weight: 300; color: rgba(255,255,255,0.75);',
' line-height: 1.5; margin-bottom: 8px;',
'}',
'#tour-tooltip .tour-action {',
' font-family: "JetBrains Mono", monospace; font-size: 11px;',
' color: #5eead4; margin-bottom: 16px;',
'}',
'#tour-tooltip .tour-nav {',
' display: flex; justify-content: space-between; align-items: center;',
'}',
'#tour-tooltip .tour-counter {',
' font-family: "JetBrains Mono", monospace; font-size: 10px; color: rgba(255,255,255,0.25);',
'}',
'#tour-tooltip .tour-btns { display: flex; gap: 8px; }',
'.tour-btn {',
' font-family: "JetBrains Mono", monospace; font-size: 11px; font-weight: 300;',
' padding: 6px 14px; border-radius: 6px; cursor: pointer;',
' transition: all 0.2s; border: 1px solid rgba(255,255,255,0.06);',
' background: rgba(255,255,255,0.04); color: rgba(255,255,255,0.55);',
'}',
'.tour-btn:hover { border-color: rgba(94,234,212,0.3); color: #5eead4; }',
'.tour-btn--primary {',
' background: rgba(94,234,212,0.08); border-color: rgba(94,234,212,0.25);',
' color: #5eead4;',
'}',
'.tour-btn--primary:hover { background: rgba(94,234,212,0.15); }',
'.tour-verdict { display: flex; gap: 8px; align-items: center; margin-bottom: 12px; }',
'.tour-btn--accept {',
' background: rgba(52,211,153,0.08); border-color: rgba(52,211,153,0.25); color: #34d399;',
'}',
'.tour-btn--accept:hover { background: rgba(52,211,153,0.15); border-color: #34d399; }',
'.tour-btn--reject {',
' background: rgba(248,113,113,0.08); border-color: rgba(248,113,113,0.25); color: #f87171;',
'}',
'.tour-btn--reject:hover { background: rgba(248,113,113,0.15); border-color: #f87171; }',
'.tour-comment {',
' flex: 1; padding: 6px 10px;',
' font-family: "JetBrains Mono", monospace; font-size: 11px; font-weight: 300;',
' color: rgba(255,255,255,0.75); background: rgba(255,255,255,0.04);',
' border: 1px solid rgba(255,255,255,0.06); border-radius: 4px; outline: none;',
' transition: border-color 0.2s;',
'}',
'.tour-comment:focus { border-color: rgba(94,234,212,0.3); }',
'.tour-comment::placeholder { color: rgba(255,255,255,0.2); }',
].join('\n');
document.head.appendChild(style);
// Create elements
overlay = document.createElement('div');
overlay.id = 'tour-overlay';
spotlight = document.createElement('div');
spotlight.id = 'tour-spotlight';
overlay.appendChild(spotlight);
document.body.appendChild(overlay);
tooltip = document.createElement('div');
tooltip.id = 'tour-tooltip';
document.body.appendChild(tooltip);
showStop(0);
// Keyboard nav
document.addEventListener('keydown', function(e) {
// Don't hijack Enter when typing in comment
if (e.target.classList && e.target.classList.contains('tour-comment')) {
if (e.key === 'Enter') {
e.preventDefault();
verdictStop('accept'); // Enter in comment = accept with comment
}
return;
}
if (e.key === 'ArrowRight') nextStop();
if (e.key === 'ArrowLeft') prevStop();
if (e.key === 'Escape') endTour();
});
}
function showStop(idx) {
if (idx < 0 || idx >= stops.length) return;
currentStop = idx;
var stop = stops[idx];
// Find element
var el = document.querySelector(stop.selector);
if (el) {
// Scroll into view if needed
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(function() {
var rect = el.getBoundingClientRect();
var pad = 8;
spotlight.style.left = (rect.left - pad + window.scrollX) + 'px';
spotlight.style.top = (rect.top - pad + window.scrollY) + 'px';
spotlight.style.width = (rect.width + pad * 2) + 'px';
spotlight.style.height = (rect.height + pad * 2) + 'px';
// Position tooltip below or above element
var tooltipTop = rect.bottom + window.scrollY + 16;
var tooltipLeft = Math.max(24, Math.min(rect.left + window.scrollX, window.innerWidth - 384));
if (tooltipTop + 250 > window.scrollY + window.innerHeight) {
tooltipTop = rect.top + window.scrollY - 250;
}
tooltip.style.left = tooltipLeft + 'px';
tooltip.style.top = tooltipTop + 'px';
}, 400);
} else {
// Element not found — position tooltip centered
spotlight.style.width = '0';
spotlight.style.height = '0';
tooltip.style.left = '50%';
tooltip.style.top = '40%';
tooltip.style.transform = 'translate(-50%, -50%)';
}
tooltip.innerHTML =
'<div class="tour-quote">' + stop.feedback + '</div>' +
'<div class="tour-fix">' + stop.fix + '</div>' +
'<div class="tour-action">' + stop.action + '</div>' +
'<div class="tour-verdict">' +
'<button class="tour-btn tour-btn--accept" onclick="verdictStop(\'accept\')">Accept ✓</button>' +
'<button class="tour-btn tour-btn--reject" onclick="verdictStop(\'reject\')">Reject ✗</button>' +
'<input type="text" class="tour-comment" id="tour-comment-' + idx + '" placeholder="Comment (optional, Enter to save)">' +
'</div>' +
'<div class="tour-nav">' +
'<span class="tour-counter">' + (idx + 1) + ' / ' + stops.length + '</span>' +
'<div class="tour-btns">' +
(idx > 0 ? '<button class="tour-btn" onclick="document.dispatchEvent(new Event(\'tour-prev\'))">← Back</button>' : '') +
(idx < stops.length - 1
? '<button class="tour-btn tour-btn--primary" onclick="document.dispatchEvent(new Event(\'tour-next\'))">Next →</button>'
: '<button class="tour-btn tour-btn--primary" onclick="document.dispatchEvent(new Event(\'tour-end\'))">Done ✓</button>') +
'</div>' +
'</div>';
}
window.verdictStop = function(verdict) {
var stop = stops[currentStop];
var commentEl = document.getElementById('tour-comment-' + currentStop);
var comment = commentEl ? commentEl.value.trim() : '';
stop.verdict = verdict;
stop.comment = comment;
// Save to DB
fetch('/api/qa-feedback', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': document.querySelector('meta[name="csrf-token"]')?.content || ''
},
body: JSON.stringify({
text: '[TOUR ' + verdict.toUpperCase() + '] ' + stop.feedback + (comment ? ' — ' + comment : ''),
url: window.location.href,
page: window.location.pathname,
viewport: window.innerWidth + 'x' + window.innerHeight,
scrollY: Math.round(window.scrollY),
tourStop: currentStop,
verdict: verdict,
selector: stop.selector
})
}).catch(function() {});
// Visual feedback on the button
var btns = tooltip.querySelectorAll('.tour-btn--accept, .tour-btn--reject');
btns.forEach(function(b) { b.style.opacity = '0.3'; });
var activeBtn = verdict === 'accept'
? tooltip.querySelector('.tour-btn--accept')
: tooltip.querySelector('.tour-btn--reject');
if (activeBtn) {
activeBtn.style.opacity = '1';
activeBtn.style.borderColor = verdict === 'accept' ? '#34d399' : '#f87171';
}
// Auto-advance after brief pause
setTimeout(function() { nextStop(); }, 600);
};
function nextStop() { if (currentStop < stops.length - 1) showStop(currentStop + 1); else endTour(); }
function prevStop() { if (currentStop > 0) showStop(currentStop - 1); }
function endTour() {
if (overlay) overlay.remove();
if (tooltip) tooltip.remove();
}
document.addEventListener('tour-next', nextStop);
document.addEventListener('tour-prev', prevStop);
document.addEventListener('tour-end', endTour);
})();