Add filter bar (personnage, statut, type, search)
Filters hide non-matching cards via CSS class. Filter options are populated dynamically from NocoDB data and column options. Clear button resets all filters. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -86,6 +86,16 @@
|
|||||||
.avatar.e { background: #5c3d2e; }
|
.avatar.e { background: #5c3d2e; }
|
||||||
.avatar.m { background: #b85c3a; }
|
.avatar.m { background: #b85c3a; }
|
||||||
|
|
||||||
|
/* ── Filter bar ── */
|
||||||
|
.filter-bar { background: var(--bg-alt); border-bottom: 1px solid var(--border); padding: 10px 28px; display: flex; align-items: center; gap: 12px; flex-shrink: 0; flex-wrap: wrap; }
|
||||||
|
.filter-bar select, .filter-bar input { padding: 7px 12px; border: 1px solid var(--border); border-radius: 8px; font-size: 13px; font-family: 'Lora', serif; color: var(--text); background: var(--bg-input); outline: none; }
|
||||||
|
.filter-bar select:focus, .filter-bar input:focus { border-color: var(--accent); }
|
||||||
|
.filter-bar label { font-size: 12px; color: var(--text-hint); text-transform: uppercase; letter-spacing: 0.5px; font-weight: 600; }
|
||||||
|
.filter-group { display: flex; align-items: center; gap: 6px; }
|
||||||
|
.btn-clear-filters { padding: 6px 14px; background: transparent; border: 1px solid var(--border); border-radius: 8px; font-size: 12px; font-family: 'Lora', serif; color: var(--text-muted); cursor: pointer; }
|
||||||
|
.btn-clear-filters:hover { background: var(--border); color: var(--text); }
|
||||||
|
.card.filtered-out { display: none; }
|
||||||
|
|
||||||
/* ── Frise container ── */
|
/* ── Frise container ── */
|
||||||
.frise-wrapper { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
.frise-wrapper { flex: 1; display: flex; flex-direction: column; overflow: hidden; }
|
||||||
|
|
||||||
@@ -206,6 +216,25 @@
|
|||||||
<div class="avatar m">M</div>
|
<div class="avatar m">M</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="filter-bar" id="filter-bar">
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>Personnage</label>
|
||||||
|
<select id="filter-perso" onchange="applyFilters()"><option value="">Tous</option></select>
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>Statut</label>
|
||||||
|
<select id="filter-statut" onchange="applyFilters()"><option value="">Tous</option></select>
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>Type</label>
|
||||||
|
<select id="filter-type" onchange="applyFilters()"><option value="">Tous</option></select>
|
||||||
|
</div>
|
||||||
|
<div class="filter-group">
|
||||||
|
<label>Recherche</label>
|
||||||
|
<input type="text" id="filter-search" placeholder="Mot-cle..." oninput="applyFilters()">
|
||||||
|
</div>
|
||||||
|
<button class="btn-clear-filters" onclick="clearFilters()">Effacer</button>
|
||||||
|
</div>
|
||||||
<div class="frise-wrapper">
|
<div class="frise-wrapper">
|
||||||
<div class="timeline-area" id="timeline-area">
|
<div class="timeline-area" id="timeline-area">
|
||||||
<div class="loading" id="loading">Chargement...</div>
|
<div class="loading" id="loading">Chargement...</div>
|
||||||
@@ -290,6 +319,71 @@ function toggleDark() {
|
|||||||
}
|
}
|
||||||
if (localStorage.getItem('tdb-dark') === '1') document.body.classList.add('dark');
|
if (localStorage.getItem('tdb-dark') === '1') document.body.classList.add('dark');
|
||||||
|
|
||||||
|
// ── Filters ──
|
||||||
|
function populateFilterOptions() {
|
||||||
|
// Collect unique personnages from data
|
||||||
|
const persoSet = new Set();
|
||||||
|
records.forEach(r => {
|
||||||
|
if (r.Personnage) r.Personnage.split(',').forEach(p => persoSet.add(p.trim()));
|
||||||
|
});
|
||||||
|
const persoSel = document.getElementById('filter-perso');
|
||||||
|
const curPerso = persoSel.value;
|
||||||
|
persoSel.innerHTML = '<option value="">Tous</option>';
|
||||||
|
[...persoSet].sort().forEach(p => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = p; opt.textContent = p;
|
||||||
|
persoSel.appendChild(opt);
|
||||||
|
});
|
||||||
|
persoSel.value = curPerso;
|
||||||
|
|
||||||
|
// Statut and Type from column options
|
||||||
|
['Statut', 'Type'].forEach(name => {
|
||||||
|
const sel = document.getElementById(`filter-${name.toLowerCase()}`);
|
||||||
|
const cur = sel.value;
|
||||||
|
sel.innerHTML = '<option value="">Tous</option>';
|
||||||
|
if (columns[name]) {
|
||||||
|
columns[name].options.forEach(o => {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = o; opt.textContent = o;
|
||||||
|
sel.appendChild(opt);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
sel.value = cur;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyFilters() {
|
||||||
|
const perso = document.getElementById('filter-perso').value;
|
||||||
|
const statut = document.getElementById('filter-statut').value;
|
||||||
|
const type = document.getElementById('filter-type').value;
|
||||||
|
const search = document.getElementById('filter-search').value.toLowerCase().trim();
|
||||||
|
|
||||||
|
document.querySelectorAll('.card').forEach(card => {
|
||||||
|
const id = Number(card.dataset.id);
|
||||||
|
const r = records.find(rec => rec.Id === id);
|
||||||
|
if (!r) return;
|
||||||
|
|
||||||
|
let visible = true;
|
||||||
|
if (perso && !(r.Personnage || '').split(',').map(s => s.trim()).includes(perso)) visible = false;
|
||||||
|
if (statut && r.Statut !== statut) visible = false;
|
||||||
|
if (type && r.Type !== type) visible = false;
|
||||||
|
if (search) {
|
||||||
|
const haystack = [r.Title, r['Résumé'], r.Personnage, r['Intérêt narratif'], r.Implications, r.Origine].filter(Boolean).join(' ').toLowerCase();
|
||||||
|
if (!haystack.includes(search)) visible = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
card.classList.toggle('filtered-out', !visible);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFilters() {
|
||||||
|
document.getElementById('filter-perso').value = '';
|
||||||
|
document.getElementById('filter-statut').value = '';
|
||||||
|
document.getElementById('filter-type').value = '';
|
||||||
|
document.getElementById('filter-search').value = '';
|
||||||
|
applyFilters();
|
||||||
|
}
|
||||||
|
|
||||||
const statusClass = s => {
|
const statusClass = s => {
|
||||||
if (!s) return '';
|
if (!s) return '';
|
||||||
const map = { 'Germe': 'badge-germe', 'À étudier': 'badge-etudier', 'Posée': 'badge-posee', 'Abandonnée': 'badge-abandonnee', 'En conflit': 'badge-conflit' };
|
const map = { 'Germe': 'badge-germe', 'À étudier': 'badge-etudier', 'Posée': 'badge-posee', 'Abandonnée': 'badge-abandonnee', 'En conflit': 'badge-conflit' };
|
||||||
@@ -544,7 +638,9 @@ async function loadData() {
|
|||||||
records = recData.list || [];
|
records = recData.list || [];
|
||||||
columns = await colRes.json();
|
columns = await colRes.json();
|
||||||
populateSelects();
|
populateSelects();
|
||||||
|
populateFilterOptions();
|
||||||
renderFrise();
|
renderFrise();
|
||||||
|
applyFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
function esc(s) {
|
function esc(s) {
|
||||||
|
|||||||
Reference in New Issue
Block a user