Redesign pool fix match modal: fixed height, no layout shift (#186)

Modal now uses a fixed 600px height from open — results scroll within
a dedicated area instead of growing the modal and pushing inputs up.
This eliminates the layout shift that caused accidental result clicks.

Other fixes:
- Input fields now have labels (Track, Artist)
- Overlay dismiss uses mousedown with stopPropagation to prevent
  accidental close when clicking near inputs
- Reduced results from 50 to 20 for faster response
- Clean minimal design matching app style
- Mobile: full-screen modal, stacked inputs with 44px touch targets
This commit is contained in:
Broque Thomas 2026-03-24 11:16:37 -07:00
parent 434ac417fd
commit e28aeccfd1
2 changed files with 372 additions and 46 deletions

View file

@ -57806,39 +57806,52 @@ function openPoolFixModal(trackId, trackName, artistName) {
fixOverlay = document.createElement('div');
fixOverlay.className = 'pool-fix-overlay';
fixOverlay.id = 'pool-fix-overlay';
fixOverlay.onclick = (e) => { if (e.target === fixOverlay) closePoolFixModal(); };
// Only close on click to the overlay itself — use a dedicated close zone
// to prevent accidental dismissal when clicking near inputs
fixOverlay.addEventListener('mousedown', (e) => {
if (e.target === fixOverlay) {
e.preventDefault(); // Prevent stealing focus from inputs
closePoolFixModal();
}
});
fixOverlay.innerHTML = `
<div class="pool-fix-modal">
<div class="discovery-fix-modal-header">
<div class="pool-fix-modal" onmousedown="event.stopPropagation()">
<div class="pool-fix-header">
<h2>Fix Track Match</h2>
<button class="modal-close-btn" onclick="closePoolFixModal()"></button>
<button class="pool-fix-close" onclick="closePoolFixModal()" title="Close"></button>
</div>
<div class="discovery-fix-modal-content">
<div class="source-track-info">
<h3>Source Track</h3>
<div class="source-track-display">
<div class="source-field"><label>Track:</label><span>${_esc(trackName)}</span></div>
<div class="source-field"><label>Artist:</label><span>${_esc(artistName)}</span></div>
<div class="pool-fix-body">
<div class="pool-fix-source">
<div class="pool-fix-source-label">Original Track</div>
<div class="pool-fix-source-row">
<span class="pool-fix-source-title">${_esc(trackName)}</span>
<span class="pool-fix-source-sep"></span>
<span class="pool-fix-source-artist">${_esc(artistName)}</span>
</div>
</div>
<div class="search-inputs-section">
<h3>Search for Match</h3>
<div class="search-input-group">
<input type="text" id="pool-fix-track-input" placeholder="Track name" class="fix-modal-input" value="${_escAttr(trackName)}">
<input type="text" id="pool-fix-artist-input" placeholder="Artist name" class="fix-modal-input" value="${_escAttr(artistName)}">
<button class="search-btn" onclick="searchPoolFix()">Search</button>
<div class="pool-fix-search">
<div class="pool-fix-input-row">
<div class="pool-fix-input-wrap">
<label for="pool-fix-track-input">Track</label>
<input type="text" id="pool-fix-track-input" placeholder="Track name" value="${_escAttr(trackName)}">
</div>
<div class="pool-fix-input-wrap">
<label for="pool-fix-artist-input">Artist</label>
<input type="text" id="pool-fix-artist-input" placeholder="Artist name" value="${_escAttr(artistName)}">
</div>
<button class="pool-fix-search-btn" onclick="searchPoolFix()">Search</button>
</div>
</div>
<div class="search-results-section">
<h3>Results</h3>
<div id="pool-fix-results" class="fix-modal-results">
<div class="pool-empty">Searching...</div>
<div class="pool-fix-results-area">
<div id="pool-fix-results" class="pool-fix-results-list">
<div class="pool-fix-empty">Searching...</div>
</div>
</div>
</div>
<div class="discovery-fix-modal-footer">
<button class="modal-btn secondary" onclick="closePoolFixModal()">Cancel</button>
<div class="pool-fix-footer">
<button class="pool-fix-cancel" onclick="closePoolFixModal()">Cancel</button>
</div>
</div>
`;
@ -57853,7 +57866,11 @@ function openPoolFixModal(trackId, trackName, artistName) {
trackInput.addEventListener('keypress', enterHandler);
artistInput.addEventListener('keypress', enterHandler);
// Auto-search (delay allows modal layout to settle and prevents accidental clicks)
// Focus the track input
trackInput.focus();
trackInput.select();
// Auto-search after a delay
setTimeout(() => searchPoolFix(), 500);
}
@ -57871,42 +57888,42 @@ async function searchPoolFix() {
const trackVal = trackInput.value.trim();
const artistVal = artistInput.value.trim();
if (!trackVal && !artistVal) {
resultsContainer.innerHTML = '<div class="pool-empty">Enter a search term</div>';
resultsContainer.innerHTML = '<div class="pool-fix-empty">Enter a search term</div>';
return;
}
resultsContainer.innerHTML = '<div class="pool-empty">Searching...</div>';
resultsContainer.innerHTML = '<div class="pool-fix-empty"><div class="pool-fix-spinner"></div>Searching...</div>';
try {
const params = new URLSearchParams();
if (trackVal) params.set('track', trackVal);
if (artistVal) params.set('artist', artistVal);
params.set('limit', '50');
params.set('limit', '20');
const res = await fetch(`/api/spotify/search_tracks?${params.toString()}`);
const data = await res.json();
const tracks = data.tracks || [];
if (tracks.length === 0) {
resultsContainer.innerHTML = '<div class="pool-empty">No results found</div>';
resultsContainer.innerHTML = '<div class="pool-fix-empty">No results found</div>';
return;
}
resultsContainer.innerHTML = tracks.map((track, i) => {
resultsContainer.innerHTML = tracks.map((track) => {
const artists = (track.artists || []).join(', ');
const duration = track.duration_ms ? formatDuration(track.duration_ms) : '';
const albumText = track.album ? ` · ${_esc(track.album)}` : '';
return `
<div class="fix-result-card" onclick='selectPoolFixTrack(${JSON.stringify(track).replace(/'/g, "&#39;")})'>
<div class="fix-result-card-content">
<div class="fix-result-title">${_esc(track.name || 'Unknown')}</div>
<div class="fix-result-artist">${_esc(artists)}</div>
<div class="fix-result-album">${_esc(track.album || '')}</div>
${duration ? `<div class="fix-result-duration">${duration}</div>` : ''}
<div class="pool-fix-result" onclick='selectPoolFixTrack(${JSON.stringify(track).replace(/'/g, "&#39;")})'>
<div class="pool-fix-result-main">
<div class="pool-fix-result-title">${_esc(track.name || 'Unknown')}</div>
<div class="pool-fix-result-meta">${_esc(artists)}${albumText}</div>
</div>
${duration ? `<div class="pool-fix-result-dur">${duration}</div>` : ''}
</div>
`;
}).join('');
} catch (err) {
resultsContainer.innerHTML = `<div class="pool-empty">Search failed: ${_esc(err.message)}</div>`;
resultsContainer.innerHTML = `<div class="pool-fix-empty">Search failed: ${_esc(err.message)}</div>`;
}
}

View file

@ -8673,12 +8673,12 @@ body {
border-color: rgba(239, 68, 68, 0.3);
}
/* ===== Fix Sub-Modal (kept) ===== */
/* ===== Pool Fix Match Modal ===== */
.pool-fix-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.6);
backdrop-filter: blur(4px);
background: rgba(0, 0, 0, 0.7);
backdrop-filter: blur(8px);
z-index: 10001;
display: flex;
align-items: center;
@ -8687,16 +8687,325 @@ body {
}
.pool-fix-modal {
background: linear-gradient(135deg, rgba(26, 26, 26, 0.98) 0%, rgba(18, 18, 18, 0.99) 100%);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 16px;
width: 90%;
max-width: 700px;
max-height: 80vh;
background: rgba(18, 18, 18, 0.98);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 14px;
width: 560px;
max-width: 92vw;
/* FIXED HEIGHT — prevents layout shift when results load */
height: 600px;
max-height: 85vh;
display: flex;
flex-direction: column;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);
animation: slideUp 0.25s ease;
box-shadow: 0 24px 64px rgba(0, 0, 0, 0.6);
animation: slideUp 0.2s ease;
overflow: hidden;
}
.pool-fix-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 18px 22px;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
flex-shrink: 0;
}
.pool-fix-header h2 {
font-size: 17px;
font-weight: 700;
color: #fff;
margin: 0;
letter-spacing: -0.2px;
}
.pool-fix-close {
width: 32px;
height: 32px;
border-radius: 8px;
border: none;
background: rgba(255, 255, 255, 0.06);
color: rgba(255, 255, 255, 0.5);
font-size: 14px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s ease;
}
.pool-fix-close:hover {
background: rgba(255, 255, 255, 0.12);
color: #fff;
}
.pool-fix-body {
flex: 1;
display: flex;
flex-direction: column;
min-height: 0;
padding: 18px 22px 0;
overflow: hidden;
}
/* Source track display — compact */
.pool-fix-source {
padding: 12px 14px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 10px;
margin-bottom: 16px;
flex-shrink: 0;
}
.pool-fix-source-label {
font-size: 10px;
font-weight: 700;
color: rgba(255, 255, 255, 0.35);
text-transform: uppercase;
letter-spacing: 0.06em;
margin-bottom: 6px;
}
.pool-fix-source-row {
display: flex;
align-items: baseline;
gap: 8px;
flex-wrap: wrap;
}
.pool-fix-source-title {
font-size: 14px;
font-weight: 600;
color: rgba(255, 255, 255, 0.9);
}
.pool-fix-source-sep {
color: rgba(255, 255, 255, 0.2);
font-size: 13px;
}
.pool-fix-source-artist {
font-size: 13px;
color: rgba(255, 255, 255, 0.5);
}
/* Search inputs */
.pool-fix-search {
margin-bottom: 14px;
flex-shrink: 0;
}
.pool-fix-input-row {
display: flex;
gap: 10px;
align-items: flex-end;
}
.pool-fix-input-wrap {
flex: 1;
display: flex;
flex-direction: column;
gap: 4px;
}
.pool-fix-input-wrap label {
font-size: 11px;
font-weight: 600;
color: rgba(255, 255, 255, 0.4);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.pool-fix-input-wrap input {
width: 100%;
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
padding: 9px 12px;
color: #fff;
font-size: 14px;
font-family: inherit;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
box-sizing: border-box;
}
.pool-fix-input-wrap input:focus {
outline: none;
border-color: rgba(var(--accent-rgb), 0.5);
box-shadow: 0 0 0 2px rgba(var(--accent-rgb), 0.1);
}
.pool-fix-input-wrap input::placeholder {
color: rgba(255, 255, 255, 0.25);
}
.pool-fix-search-btn {
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 9px 18px;
color: rgba(255, 255, 255, 0.8);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s ease;
white-space: nowrap;
flex-shrink: 0;
height: 38px;
}
.pool-fix-search-btn:hover {
background: rgba(255, 255, 255, 0.12);
color: #fff;
}
/* Results area — FIXED, scrolls internally, no layout shift */
.pool-fix-results-area {
flex: 1;
min-height: 0;
overflow: hidden;
display: flex;
flex-direction: column;
}
.pool-fix-results-list {
flex: 1;
min-height: 0;
overflow-y: auto;
display: flex;
flex-direction: column;
gap: 4px;
padding-bottom: 8px;
}
.pool-fix-results-list::-webkit-scrollbar {
width: 5px;
}
.pool-fix-results-list::-webkit-scrollbar-track {
background: transparent;
}
.pool-fix-results-list::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}
.pool-fix-results-list::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
/* Result rows */
.pool-fix-result {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 12px;
border-radius: 8px;
cursor: pointer;
transition: background 0.12s ease;
flex-shrink: 0;
}
.pool-fix-result:hover {
background: rgba(255, 255, 255, 0.06);
}
.pool-fix-result-main {
flex: 1;
min-width: 0;
}
.pool-fix-result-title {
font-size: 14px;
font-weight: 500;
color: rgba(255, 255, 255, 0.9);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pool-fix-result-meta {
font-size: 12px;
color: rgba(255, 255, 255, 0.4);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-top: 2px;
}
.pool-fix-result-dur {
font-size: 12px;
color: rgba(255, 255, 255, 0.3);
flex-shrink: 0;
font-variant-numeric: tabular-nums;
}
/* Empty / loading states */
.pool-fix-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
padding: 40px 20px;
color: rgba(255, 255, 255, 0.3);
font-size: 13px;
flex: 1;
}
.pool-fix-spinner {
width: 24px;
height: 24px;
border: 2px solid rgba(255, 255, 255, 0.08);
border-top-color: rgba(255, 255, 255, 0.4);
border-radius: 50%;
animation: spin 0.7s linear infinite;
}
/* Footer */
.pool-fix-footer {
padding: 14px 22px;
border-top: 1px solid rgba(255, 255, 255, 0.06);
display: flex;
justify-content: flex-end;
flex-shrink: 0;
}
.pool-fix-cancel {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
padding: 8px 18px;
color: rgba(255, 255, 255, 0.6);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
}
.pool-fix-cancel:hover {
background: rgba(255, 255, 255, 0.1);
color: rgba(255, 255, 255, 0.85);
}
/* Mobile */
@media (max-width: 600px) {
.pool-fix-modal {
width: 100%;
max-width: 100%;
height: 100vh;
max-height: 100vh;
border-radius: 0;
}
.pool-fix-input-row {
flex-direction: column;
gap: 8px;
}
.pool-fix-search-btn {
width: 100%;
height: 44px;
}
}
/* Mirrored playlist track modal */