Skip to content

Commit bc0e002

Browse files
authored
Merge pull request #9 from codeid9/feature/multi-label-filter-logic
feat: multi-select lable chips with AND/OR logic
2 parents de9de75 + 89c05ce commit bc0e002

2 files changed

Lines changed: 129 additions & 29 deletions

File tree

src/App.jsx

Lines changed: 59 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const DEMO_NOTES = [
2323
title: "Welcome 👋",
2424
text: "This is the *web demo* of Keep Sticky Board.\n\nDrag me by my title bar.",
2525
color: "yellow",
26-
labels: ["demo", "welcome"]
26+
labels: ["demo", "welcome","ss","s","e","t"]
2727
},
2828
{
2929
id: "demo-2",
@@ -258,9 +258,11 @@ export default function App() {
258258
const [notes, setNotes] = useState([]);
259259
const [positions, setPositions] = useState({});
260260
const [query, setQuery] = useState("");
261-
const [activeLabel, setActiveLabel] = useState("ALL");
262261
const saveTimer = useRef(null);
263262
const [zoom, setZoom] = useState(1);
263+
const [openFilterBox,setOpenFilterBox] = useState(false);
264+
const [selectedLabels, setSelectedLabels] = useState([]);
265+
const [filterLogic, setFilterLogic] = useState("OR");
264266

265267
// Initial load:
266268
// - Electron: load from keepAPI
@@ -367,26 +369,42 @@ export default function App() {
367369
setActiveLabel("ALL");
368370
}
369371

372+
const toggleLabel = (label) => {
373+
if (label === "ALL") {
374+
setSelectedLabels([]);
375+
return;
376+
}
377+
setSelectedLabels(prev =>
378+
prev.includes(label)
379+
? prev.filter(l => l !== label)
380+
: [...prev, label]
381+
);
382+
};
383+
370384
const allLabels = useMemo(() => {
371385
const s = new Set();
372386
for (const n of notes) for (const l of n.labels || []) s.add(l);
373387
return ["ALL", ...Array.from(s).sort((a, b) => a.localeCompare(b))];
374388
}, [notes]);
375389

376390
const filtered = useMemo(() => {
377-
const q = query.trim().toLowerCase();
378-
return notes.filter((n) => {
379-
if (activeLabel !== "ALL" && !(n.labels || []).includes(activeLabel)) return false;
380-
if (!q) return true;
381-
382-
const imageTerms = getNoteImages(n)
383-
.map((img) => `${img.alt || ""} ${img.src || ""}`)
384-
.join("\n");
391+
const q = query.trim().toLowerCase();
392+
return notes.filter((n) => {
393+
// Label Filter Logic
394+
if (selectedLabels.length > 0) {
395+
const noteLabels = n.labels || [];
396+
if (filterLogic === "OR") {
397+
if (!selectedLabels.some(l => noteLabels.includes(l))) return false;
398+
} else {
399+
if (!selectedLabels.every(l => noteLabels.includes(l))) return false;
400+
}
401+
}
385402

386-
const hay = `${n.title || ""}\n${n.text || ""}\n${imageTerms}`.toLowerCase();
387-
return hay.includes(q);
388-
});
389-
}, [notes, query, activeLabel]);
403+
if (!q) return true;
404+
const hay = `${n.title || ""}\n${n.text || ""}`.toLowerCase();
405+
return hay.includes(q);
406+
});
407+
}, [notes, query, selectedLabels, filterLogic]);
390408

391409
function noteColor(n) {
392410
const c = String(n.color || "").toLowerCase();
@@ -436,11 +454,33 @@ export default function App() {
436454
value={query}
437455
onChange={(e) => setQuery(e.target.value)}
438456
/>
439-
<select className="select" value={activeLabel} onChange={(e) => setActiveLabel(e.target.value)}>
440-
{allLabels.map((l) => (
441-
<option key={l} value={l}>{l}</option>
442-
))}
443-
</select>
457+
<button className={`filter btn ${openFilterBox ? "active" : ""}`} onClick={()=>setOpenFilterBox(prev=>!prev)}>Filter Notes</button>
458+
<div className={`filter-section ${openFilterBox? "active" : ""}`}>
459+
{/* Logic Toggle: AND vs OR */}
460+
<div className="logic-toggle">
461+
<button
462+
className={`btn-toggle ${filterLogic === 'OR' ? 'active' : ''}`}
463+
onClick={() => setFilterLogic('OR')}
464+
> OR </button>
465+
<button
466+
className={`btn-toggle ${filterLogic === 'AND' ? 'active' : ''}`}
467+
onClick={() => setFilterLogic('AND')}
468+
> AND </button>
469+
</div>
470+
471+
{/* Multi-select Chips */}
472+
<div className="chips-container">
473+
{allLabels.map((l) => (
474+
<button
475+
key={l}
476+
className={`chip-filter ${selectedLabels.includes(l) || (l === "ALL" && selectedLabels.length === 0) ? "selected" : ""}`}
477+
onClick={() => toggleLabel(l)}
478+
>
479+
{l}
480+
</button>
481+
))}
482+
</div>
483+
</div>
444484
</div>
445485
</header>
446486
<div className="board" style={{

src/app.css

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ body{
2121
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
2222
background: var(--bg);
2323
color: var(--text);
24+
overflow: hidden;
2425
}
2526

2627
.app{ height:100%; display:flex; flex-direction:column; }
@@ -61,16 +62,8 @@ body{
6162
color: var(--text);
6263
outline:none;
6364
}
64-
.search:focus{ box-shadow: 0 0 0 3px var(--ring); }
65-
66-
.select{
67-
padding:10px 12px;
68-
border-radius:10px;
69-
border:1px solid rgba(255,255,255,.18);
70-
background: rgba(0,0,0,.20);
71-
color: var(--text);
72-
outline:none;
73-
}
65+
.search:focus,
66+
.filter.active{ box-shadow: 0 0 0 3px var(--ring); }
7467

7568
.board{
7669
position:relative;
@@ -218,6 +211,73 @@ body{
218211
background: rgba(255,255,255,0.3);
219212
}
220213

214+
.filter-section {
215+
position: absolute;
216+
right:0;
217+
transform: translateX(100%);
218+
top:4.5rem;
219+
display: flex;
220+
flex-direction: column;
221+
align-items: center;
222+
gap: 15px;
223+
flex-wrap: wrap;
224+
background: var(--ring);
225+
width: 250px;
226+
padding: 0.5rem;
227+
border-radius: 8px;
228+
box-shadow: 0 0 8px 0 rgba(0,0,0,.8);
229+
transition: transform 0.3s ease-out;
230+
}
231+
.filter-section.active{
232+
right:1.5rem;
233+
transform: translateX(0);
234+
}
235+
236+
.chips-container {
237+
display: flex;
238+
gap: 8px;
239+
justify-content: center;
240+
flex-wrap: wrap;
241+
padding-bottom: 5px;
242+
max-height: 200px;
243+
overflow-y: auto;
244+
}
245+
246+
.chip-filter {
247+
padding: 5px 12px;
248+
border-radius: 15px;
249+
border: 1px solid #ddd;
250+
background: white;
251+
cursor: pointer;
252+
white-space: nowrap;
253+
transition: 0.2s;
254+
}
255+
256+
.chip-filter.selected {
257+
background: #202124;
258+
color: white;
259+
border-color: #202124;
260+
}
261+
262+
.logic-toggle {
263+
display: flex;
264+
border: 1px solid #ccc;
265+
border-radius: 5px;
266+
overflow: hidden;
267+
}
268+
269+
.btn-toggle {
270+
padding: 5px 10px;
271+
border: none;
272+
background: #f1f3f4;
273+
cursor: pointer;
274+
}
275+
276+
.btn-toggle.active {
277+
background: #4285f4;
278+
color: white;
279+
}
280+
221281
@media (max-width: 900px){
222282
.topbar{
223283
align-items:flex-start;

0 commit comments

Comments
 (0)