Collapse to learn, review, callible into one column

This commit is contained in:
Ian Keane 2026-06-11 12:54:08 -04:00
parent cccfba0712
commit 58c4dc08f3
3 changed files with 51 additions and 34 deletions

46
app.js
View file

@ -241,9 +241,7 @@ const filters = {
modal: null, modal: null,
instrument: null, instrument: null,
tuning: null, tuning: null,
callable: null, status: null,
review: null,
to_learn: null,
difficulty: null, difficulty: null,
}; };
@ -251,8 +249,8 @@ const popover = $('filter-popover');
const popoverInner = $('filter-popover-inner'); const popoverInner = $('filter-popover-inner');
let activeFilterKey = null; let activeFilterKey = null;
const BOOL_FILTERS = new Set(['modal', 'callable', 'review', 'to_learn']); const BOOL_FILTERS = new Set(['modal']);
const DROPDOWN_FILTERS = new Set(['source', 'instrument', 'tuning', 'difficulty']); const DROPDOWN_FILTERS = new Set(['source', 'instrument', 'tuning', 'difficulty', 'status']);
function openFilterPopover(key, thEl) { function openFilterPopover(key, thEl) {
if (activeFilterKey === key) { closeFilterPopover(); return; } if (activeFilterKey === key) { closeFilterPopover(); return; }
@ -267,7 +265,7 @@ function openFilterPopover(key, thEl) {
if (filters[key] === true) { cb.checked = true; cb.indeterminate = false; } if (filters[key] === true) { cb.checked = true; cb.indeterminate = false; }
else if (filters[key] === false) { cb.checked = false; cb.indeterminate = true; } else if (filters[key] === false) { cb.checked = false; cb.indeterminate = true; }
else { cb.checked = false; cb.indeterminate = false; } else { cb.checked = false; cb.indeterminate = false; }
const names = { modal: 'Modal', callable: 'Callable', review: 'Needs review', to_learn: 'To learn' }; const names = { modal: 'Modal' };
label.appendChild(cb); label.appendChild(cb);
label.appendChild(document.createTextNode(names[key] ?? key)); label.appendChild(document.createTextNode(names[key] ?? key));
popoverInner.appendChild(label); popoverInner.appendChild(label);
@ -328,6 +326,7 @@ function buildDropdownOptions(key) {
if (key === 'instrument') return [...none, ...allInstruments.map(i => [String(i.id), i.name])]; if (key === 'instrument') return [...none, ...allInstruments.map(i => [String(i.id), i.name])];
if (key === 'tuning') return [...none, ...allTunings.map(t => [String(t.id), t.name])]; if (key === 'tuning') return [...none, ...allTunings.map(t => [String(t.id), t.name])];
if (key === 'difficulty') return [...none, ['easy', 'Easy'], ['hard', 'Hard']]; if (key === 'difficulty') return [...none, ['easy', 'Easy'], ['hard', 'Hard']];
if (key === 'status') return [...none, ['callable', 'Callable'], ['review', 'Needs Review'], ['to_learn', 'To Learn']];
return none; return none;
} }
@ -367,9 +366,7 @@ function rowMatchesFilters(tune, entry) {
if (filters.instrument && (!entry || String(entry.instrument_id) !== filters.instrument)) return false; if (filters.instrument && (!entry || String(entry.instrument_id) !== filters.instrument)) return false;
if (entry) { if (entry) {
if (filters.tuning && String(entry.tuning_id) !== filters.tuning) return false; if (filters.tuning && String(entry.tuning_id) !== filters.tuning) return false;
if (filters.callable !== null && entry.callable !== filters.callable) return false; if (filters.status && entry.status !== filters.status) return false;
if (filters.review !== null && entry.review !== filters.review) return false;
if (filters.to_learn !== null && entry.to_learn !== filters.to_learn) return false;
if (filters.difficulty && entry.difficulty?.toLowerCase() !== filters.difficulty) return false; if (filters.difficulty && entry.difficulty?.toLowerCase() !== filters.difficulty) return false;
} }
return true; return true;
@ -420,11 +417,7 @@ function renderTable() {
? (tune.modal ? `${tune.key} modal` : tune.key) ? (tune.modal ? `${tune.key} modal` : tune.key)
: (tune.modal ? 'modal' : ''); : (tune.modal ? 'modal' : '');
const metaRaw = [instStr, keyStr].filter(Boolean).join(' · '); const metaRaw = [instStr, keyStr].filter(Boolean).join(' · ');
const mobPills = entry ? [ const mobPills = entry ? pillStatus(entry.status, 'mob-pill') : '';
`<span class="pill mob-pill ${entry.callable ? 'pill-yes' : 'pill-dim'}">Call</span>`,
`<span class="pill mob-pill ${entry.review ? 'pill-warn' : 'pill-dim'}">Rev</span>`,
`<span class="pill mob-pill ${entry.to_learn ? 'pill-warn' : 'pill-dim'}">Lrn</span>`,
].join('') : '';
const mobActions = [ const mobActions = [
hasNotes ? `<button class="action-btn btn-notes" data-tune-id="${tune.id}" data-entry-id="${entry?.id ?? ''}">📝</button>` : '', hasNotes ? `<button class="action-btn btn-notes" data-tune-id="${tune.id}" data-entry-id="${entry?.id ?? ''}">📝</button>` : '',
tune.references?.length > 0 ? `<button class="action-btn btn-show-refs" data-tune-id="${tune.id}">🔗</button>` : '', tune.references?.length > 0 ? `<button class="action-btn btn-show-refs" data-tune-id="${tune.id}">🔗</button>` : '',
@ -439,9 +432,7 @@ function renderTable() {
<td>${tune.modal ? '<span class="pill pill-yes">Yes</span>' : '<span class="pill" style="background:#222">—</span>'}</td> <td>${tune.modal ? '<span class="pill pill-yes">Yes</span>' : '<span class="pill" style="background:#222">—</span>'}</td>
<td>${esc(entry?.instrument_name ?? '')}</td> <td>${esc(entry?.instrument_name ?? '')}</td>
<td>${esc(entry?.tuning ?? '')}</td> <td>${esc(entry?.tuning ?? '')}</td>
<td>${pillBool(entry?.callable)}</td> <td>${pillStatus(entry?.status)}</td>
<td>${pillBool(entry?.review, true)}</td>
<td>${pillBool(entry?.to_learn, false, true)}</td>
<td>${hasNotes <td>${hasNotes
? `<button class="action-btn btn-notes" data-tune-id="${tune.id}" data-entry-id="${entry?.id ?? ''}">📝</button>` ? `<button class="action-btn btn-notes" data-tune-id="${tune.id}" data-entry-id="${entry?.id ?? ''}">📝</button>`
: ''}</td> : ''}</td>
@ -486,6 +477,14 @@ function pillBool(val, warnIfTrue = false, neutralIfTrue = false) {
return '<span class="pill pill-no">No</span>'; return '<span class="pill pill-no">No</span>';
} }
function pillStatus(status, extraClass = '') {
const c = extraClass ? ' ' + extraClass : '';
if (status === 'callable') return `<span class="pill pill-yes${c}">Callable</span>`;
if (status === 'review') return `<span class="pill pill-warn${c}">Needs Review</span>`;
if (status === 'to_learn') return `<span class="pill pill-to-learn${c}">To Learn</span>`;
return `<span class="pill pill-dim${c}">—</span>`;
}
// ── Add / Edit modal ────────────────────────────────────────── // ── Add / Edit modal ──────────────────────────────────────────
btnAddTune.addEventListener('click', openAdd); btnAddTune.addEventListener('click', openAdd);
@ -524,9 +523,7 @@ function openEdit(tuneId, entryId) {
tuneForm.elements.inst_tuning.value = entry.tuning ?? ''; tuneForm.elements.inst_tuning.value = entry.tuning ?? '';
tuneForm.elements.inst_learned_from.value = entry.learned_from ?? ''; tuneForm.elements.inst_learned_from.value = entry.learned_from ?? '';
tuneForm.elements.inst_date_learned.value = entry.date_learned ?? ''; tuneForm.elements.inst_date_learned.value = entry.date_learned ?? '';
tuneForm.elements.inst_callable.checked = !!entry.callable; tuneForm.elements.inst_status.value = entry.status ?? '';
tuneForm.elements.inst_review.checked = !!entry.review;
tuneForm.elements.inst_to_learn.checked = !!entry.to_learn;
tuneForm.elements.inst_difficulty.value = entry.difficulty ?? ''; tuneForm.elements.inst_difficulty.value = entry.difficulty ?? '';
} }
@ -657,9 +654,7 @@ async function saveTune() {
tuning_id: resolveTuningId(f.inst_tuning.value), tuning_id: resolveTuningId(f.inst_tuning.value),
learned_from_id: resolveMusicianId(f.inst_learned_from.value), learned_from_id: resolveMusicianId(f.inst_learned_from.value),
date_learned: f.inst_date_learned.value || null, date_learned: f.inst_date_learned.value || null,
callable: f.inst_callable.checked, status: f.inst_status.value || null,
review: f.inst_review.checked,
to_learn: f.inst_to_learn.checked,
difficulty: f.inst_difficulty.value || null, difficulty: f.inst_difficulty.value || null,
} : null; } : null;
@ -891,7 +886,7 @@ function callGetEntries() {
const result = []; const result = [];
for (const tune of allTunes) for (const tune of allTunes)
for (const entry of tune.instruments) for (const entry of tune.instruments)
if (entry[callMode]) result.push({ tune, entry }); if (entry.status === callMode) result.push({ tune, entry });
return result; return result;
} }
@ -986,8 +981,7 @@ function callSelect(value, modal) {
callStep = 2; callStep = 2;
renderCallStep(); renderCallStep();
} else { } else {
filters.callable = callMode === 'callable' ? true : null; filters.status = callMode;
filters.to_learn = callMode === 'to_learn' ? true : null;
filters.instrument = callInstrumentId !== null ? String(callInstrumentId) : null; filters.instrument = callInstrumentId !== null ? String(callInstrumentId) : null;
filters.tuning = callTuningId !== null ? String(callTuningId) : null; filters.tuning = callTuningId !== null ? String(callTuningId) : null;
filters.key = value; filters.key = value;

View file

@ -33,9 +33,7 @@
<th data-filter="modal"><span class="col-full">Modal</span><span class="col-short">Mod.</span></th> <th data-filter="modal"><span class="col-full">Modal</span><span class="col-short">Mod.</span></th>
<th data-filter="instrument">Instrument</th> <th data-filter="instrument">Instrument</th>
<th data-filter="tuning">Tuning</th> <th data-filter="tuning">Tuning</th>
<th data-filter="callable"><span class="col-full">Callable</span><span class="col-short">Call.</span></th> <th data-filter="status">Status</th>
<th data-filter="review"><span class="col-full">Review</span><span class="col-short">Rev.</span></th>
<th data-filter="to_learn"><span class="col-full">To learn</span><span class="col-short">Learn</span></th>
<th>Notes</th> <th>Notes</th>
<th>Refs</th> <th>Refs</th>
<th class="edit-col" hidden></th> <th class="edit-col" hidden></th>
@ -129,9 +127,14 @@
</span> </span>
</label> </label>
<label>Date learned <input name="inst_date_learned" type="date" /></label> <label>Date learned <input name="inst_date_learned" type="date" /></label>
<label>Callable <input name="inst_callable" type="checkbox" /></label> <label>Status
<label>Review <input name="inst_review" type="checkbox" /></label> <span class="status-radios">
<label>To learn <input name="inst_to_learn" type="checkbox" /></label> <label class="radio-row"><input type="radio" name="inst_status" value="" /> None</label>
<label class="radio-row"><input type="radio" name="inst_status" value="callable" /> Callable</label>
<label class="radio-row"><input type="radio" name="inst_status" value="review" /> Needs Review</label>
<label class="radio-row"><input type="radio" name="inst_status" value="to_learn" /> To Learn</label>
</span>
</label>
<label>Difficulty <label>Difficulty
<select name="inst_difficulty"> <select name="inst_difficulty">
<option value=""></option> <option value=""></option>

View file

@ -234,6 +234,7 @@ th.sort-desc .sort-indicator::after { content: '▼'; }
.pill-yes { background: #1e4d2b; color: #6fcf97; } .pill-yes { background: #1e4d2b; color: #6fcf97; }
.pill-no { background: #4d1e1e; color: #eb5757; } .pill-no { background: #4d1e1e; color: #eb5757; }
.pill-warn { background: #4d3a1e; color: #f2c94c; } .pill-warn { background: #4d3a1e; color: #f2c94c; }
.pill-to-learn { background: #2a1a4e; color: #c4b5fd; }
.action-btn { .action-btn {
background: transparent; background: transparent;
@ -361,6 +362,25 @@ fieldset.active .fs-body { display: contents; }
justify-content: flex-end; justify-content: flex-end;
margin-top: 1rem; margin-top: 1rem;
} }
.status-radios {
display: flex;
flex-direction: column;
gap: .3rem;
}
.radio-row {
display: flex;
align-items: center;
gap: .5rem;
font-size: .85rem;
font-weight: normal;
cursor: pointer;
}
.radio-row input[type="radio"] {
width: 1rem;
height: 1rem;
accent-color: var(--accent);
flex-shrink: 0;
}
#btn-save-tune { background: var(--accent); } #btn-save-tune { background: var(--accent); }
#btn-cancel { background: transparent; border: 1px solid var(--border); } #btn-cancel { background: transparent; border: 1px solid var(--border); }
@ -426,7 +446,7 @@ fieldset.active .fs-body { display: contents; }
min-width: 1.5rem; min-width: 1.5rem;
} }
.mob-status { display: flex; gap: .2rem; align-items: center; flex-shrink: 0; } .mob-status { display: flex; gap: .2rem; align-items: center; flex-shrink: 0; }
.mob-pill { font-size: .62rem; padding: .1rem .3rem; } .mob-pill { font-size: .7rem; padding: .1rem .4rem; }
/* Line 2: Instrument (Tuning) · Key [actions] */ /* Line 2: Instrument (Tuning) · Key [actions] */
.mob-line2 { .mob-line2 {