mirror of
https://github.com/Lissy93/personal-security-checklist.git
synced 2024-12-27 00:09:34 -05:00
Adds filters, sorting and more to checklist page
This commit is contained in:
parent
5d665df3d4
commit
d29d5e2664
@ -38,6 +38,7 @@
|
|||||||
"chart.js": "^4.4.1",
|
"chart.js": "^4.4.1",
|
||||||
"marked": "^12.0.0",
|
"marked": "^12.0.0",
|
||||||
"progressbar.js": "^1.1.1",
|
"progressbar.js": "^1.1.1",
|
||||||
|
"qwik-transition": "^0.0.7",
|
||||||
"sharp": "^0.33.2"
|
"sharp": "^0.33.2"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
@ -114,6 +114,16 @@ const getSvgPath = (icon: string) => {
|
|||||||
vb: "0 0 512 512",
|
vb: "0 0 512 512",
|
||||||
path: "M3.9 54.9C10.5 40.9 24.5 32 40 32H472c15.5 0 29.5 8.9 36.1 22.9s4.6 30.5-5.2 42.5L320 320.9V448c0 12.1-6.8 23.2-17.7 28.6s-23.8 4.3-33.5-3l-64-48c-8.1-6-12.8-15.5-12.8-25.6V320.9L9 97.3C-.7 85.4-2.8 68.8 3.9 54.9z",
|
path: "M3.9 54.9C10.5 40.9 24.5 32 40 32H472c15.5 0 29.5 8.9 36.1 22.9s4.6 30.5-5.2 42.5L320 320.9V448c0 12.1-6.8 23.2-17.7 28.6s-23.8 4.3-33.5-3l-64-48c-8.1-6-12.8-15.5-12.8-25.6V320.9L9 97.3C-.7 85.4-2.8 68.8 3.9 54.9z",
|
||||||
};
|
};
|
||||||
|
case 'sort':
|
||||||
|
return {
|
||||||
|
vb: "0 0 512 512",
|
||||||
|
path: "M137.4 41.4c12.5-12.5 32.8-12.5 45.3 0l128 128c9.2 9.2 11.9 22.9 6.9 34.9s-16.6 19.8-29.6 19.8H32c-12.9 0-24.6-7.8-29.6-19.8s-2.2-25.7 6.9-34.9l128-128zm0 429.3l-128-128c-9.2-9.2-11.9-22.9-6.9-34.9s16.6-19.8 29.6-19.8H288c12.9 0 24.6 7.8 29.6 19.8s2.2 25.7-6.9 34.9l-128 128c-12.5 12.5-32.8 12.5-45.3 0z",
|
||||||
|
};
|
||||||
|
case 'clear':
|
||||||
|
return {
|
||||||
|
vb: "0 0 512 512",
|
||||||
|
path: "M0 128C0 92.7 28.7 64 64 64H370.7c17 0 33.3 6.7 45.3 18.7L566.6 233.4c6 6 9.4 14.1 9.4 22.6s-3.4 16.6-9.4 22.6L416 429.3c-12 12-28.3 18.7-45.3 18.7H64c-35.3 0-64-28.7-64-64V128zm143 47c-9.4 9.4-9.4 24.6 0 33.9l47 47-47 47c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l47-47 47 47c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-47-47 47-47c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-47 47-47-47c-9.4-9.4-24.6-9.4-33.9 0z",
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
return { vb: "", path: "" }; // Default path or a placeholder icon
|
return { vb: "", path: "" }; // Default path or a placeholder icon
|
||||||
}
|
}
|
||||||
@ -150,7 +160,7 @@ const IconComponent = component$((props: IconProps) => {
|
|||||||
const { vb, path } = getSvgPath(props.icon);
|
const { vb, path } = getSvgPath(props.icon);
|
||||||
const svgClass = props.class || '';
|
const svgClass = props.class || '';
|
||||||
const width = props.width || 80;
|
const width = props.width || 80;
|
||||||
const height = props.height || 50;
|
const height = props.height || props.width || 50;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<svg class={svgClass} style={svgStyle} xmlns="http://www.w3.org/2000/svg" viewBox={vb} width={width} height={height}>
|
<svg class={svgClass} style={svgStyle} xmlns="http://www.w3.org/2000/svg" viewBox={vb} width={width} height={height}>
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import { component$ } from "@builder.io/qwik";
|
import { $, component$, useStore, useSignal } from "@builder.io/qwik";
|
||||||
|
import { useCSSTransition } from "qwik-transition";
|
||||||
|
|
||||||
import Icon from "~/components/core/icon";
|
import Icon from "~/components/core/icon";
|
||||||
import type { Priority, Section } from '../../types/PSC';
|
import type { Priority, Section, Checklist } from '../../types/PSC';
|
||||||
import { marked } from "marked";
|
import { marked } from "marked";
|
||||||
import { useLocalStorage } from "~/hooks/useLocalStorage";
|
import { useLocalStorage } from "~/hooks/useLocalStorage";
|
||||||
|
|
||||||
@ -10,6 +11,24 @@ export default component$((props: { section: Section }) => {
|
|||||||
const [completed, setCompleted] = useLocalStorage('PSC_PROGRESS', {});
|
const [completed, setCompleted] = useLocalStorage('PSC_PROGRESS', {});
|
||||||
const [ignored, setIgnored] = useLocalStorage('PSC_IGNORED', {});
|
const [ignored, setIgnored] = useLocalStorage('PSC_IGNORED', {});
|
||||||
|
|
||||||
|
const showFilters = useSignal(false);
|
||||||
|
const { stage } = useCSSTransition(showFilters, { timeout: 300 });
|
||||||
|
|
||||||
|
const sortState = useStore({ column: '', ascending: true });
|
||||||
|
|
||||||
|
const checklist = useSignal<Checklist[]>(props.section.checklist);
|
||||||
|
|
||||||
|
const originalFilters = {
|
||||||
|
show: 'all', // 'all', 'remaining', 'completed'
|
||||||
|
levels: {
|
||||||
|
recommended: true,
|
||||||
|
optional: true,
|
||||||
|
advanced: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterState = useStore(originalFilters);
|
||||||
|
|
||||||
const getBadgeClass = (priority: Priority, precedeClass: string = '') => {
|
const getBadgeClass = (priority: Priority, precedeClass: string = '') => {
|
||||||
switch (priority.toLocaleLowerCase()) {
|
switch (priority.toLocaleLowerCase()) {
|
||||||
case 'recommended':
|
case 'recommended':
|
||||||
@ -41,29 +60,102 @@ export default component$((props: { section: Section }) => {
|
|||||||
return completed.value[pointId] || false;
|
return completed.value[pointId] || false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const filteredChecklist = checklist.value.filter((item) => {
|
||||||
|
const itemId = generateId(item.point);
|
||||||
|
const itemCompleted = isChecked(itemId);
|
||||||
|
const itemIgnored = isIgnored(itemId);
|
||||||
|
const itemLevel = item.priority;
|
||||||
|
|
||||||
|
// Filter by completion status
|
||||||
|
if (filterState.show === 'remaining' && (itemCompleted || itemIgnored)) return false;
|
||||||
|
if (filterState.show === 'completed' && !itemCompleted) return false;
|
||||||
|
|
||||||
|
// Filter by level
|
||||||
|
return filterState.levels[itemLevel.toLocaleLowerCase() as Priority];
|
||||||
|
});
|
||||||
|
|
||||||
|
const sortChecklist = (a: Checklist, b: Checklist) => {
|
||||||
|
const getValue = (item: Checklist) => {
|
||||||
|
switch (sortState.column) {
|
||||||
|
case 'done':
|
||||||
|
if (isIgnored(generateId(item.point))) {
|
||||||
|
return 2;
|
||||||
|
}
|
||||||
|
return isChecked(generateId(item.point)) ? 0 : 1;
|
||||||
|
case 'advice':
|
||||||
|
return item.point;
|
||||||
|
case 'level':
|
||||||
|
return ['recommended', 'optional', 'advanced'].indexOf(item.priority.toLowerCase());
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const valueA = getValue(a);
|
||||||
|
const valueB = getValue(b);
|
||||||
|
|
||||||
|
if (valueA === valueB) {
|
||||||
|
return 0;
|
||||||
|
} else if (sortState.ascending) {
|
||||||
|
return valueA < valueB ? -1 : 1;
|
||||||
|
} else {
|
||||||
|
return valueA > valueB ? -1 : 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSort = $((column: string) => {
|
||||||
|
if (sortState.column === column) { // Reverse direction if same column
|
||||||
|
sortState.ascending = !sortState.ascending;
|
||||||
|
} else { // Sort table by column
|
||||||
|
sortState.column = column;
|
||||||
|
sortState.ascending = true; // Default to ascending
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const resetFilters = $(() => {
|
||||||
|
checklist.value = props.section.checklist;
|
||||||
|
sortState.column = '';
|
||||||
|
sortState.ascending = true;
|
||||||
|
filterState.levels = originalFilters.levels;
|
||||||
|
filterState.show = originalFilters.show;
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
||||||
<div class="collapse rounded-none">
|
|
||||||
<input type="checkbox" />
|
<div class="flex flex-wrap gap-2 justify-end my-4">
|
||||||
<div class="collapse-title flex justify-end font-bold">
|
{(sortState.column || JSON.stringify(filterState) !== JSON.stringify(originalFilters)) && (
|
||||||
<button class="btn btn-sm hover:bg-primary"><Icon width={16} height={16} icon="filters"/>Filters</button>
|
<button class="btn btn-sm hover:btn-primary" onClick$={resetFilters}>
|
||||||
</div>
|
<Icon width={18} height={16} icon="clear"/>
|
||||||
<div class="collapse-content flex flex-wrap justify-between bg-base-100 rounded px-4 pt-1 !pb-1">
|
Reset Filters
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button class="btn btn-sm hover:btn-primary" onClick$={() => { showFilters.value = !showFilters.value; }}>
|
||||||
|
<Icon width={18} height={16} icon="filters"/>
|
||||||
|
{showFilters.value ? 'Hide' : 'Show'} Filters
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{showFilters.value && (
|
||||||
|
<div class="flex flex-wrap justify-between bg-base-100 rounded px-4 py-1 transition-all"
|
||||||
|
style={{ opacity: stage.value === "enterTo" ? 1 : 0, height: stage.value === "enterTo" ? 'auto' : 0 }}>
|
||||||
{/* Filter by completion */}
|
{/* Filter by completion */}
|
||||||
<div class="flex justify-end items-center gap-1">
|
<div class="flex justify-end items-center gap-1">
|
||||||
<p class="font-bold text-sm">Show</p>
|
<p class="font-bold text-sm">Show</p>
|
||||||
<label class="p-2 rounded hover:bg-front transition-all cursor-pointer flex gap-2">
|
<label onClick$={() => (filterState.show = 'all')}
|
||||||
|
class="p-2 rounded hover:bg-front transition-all cursor-pointer flex gap-2">
|
||||||
<span class="text-sm">All</span>
|
<span class="text-sm">All</span>
|
||||||
<input type="radio" name="show-all" class="radio radio-sm checked:radio-info" checked />
|
<input type="radio" name="show" class="radio radio-sm checked:radio-info" checked />
|
||||||
</label>
|
</label>
|
||||||
<label class="p-2 rounded hover:bg-front transition-all cursor-pointer flex gap-2">
|
<label onClick$={() => (filterState.show = 'remaining')}
|
||||||
|
class="p-2 rounded hover:bg-front transition-all cursor-pointer flex gap-2">
|
||||||
<span class="text-sm">Remaining</span>
|
<span class="text-sm">Remaining</span>
|
||||||
<input type="radio" name="show-remaining" class="radio radio-sm checked:radio-error" />
|
<input type="radio" name="show" class="radio radio-sm checked:radio-error" />
|
||||||
</label>
|
</label>
|
||||||
<label class="p-2 rounded hover:bg-front transition-all cursor-pointer flex gap-2">
|
<label onClick$={() => (filterState.show = 'completed')}
|
||||||
|
class="p-2 rounded hover:bg-front transition-all cursor-pointer flex gap-2">
|
||||||
<span class="text-sm">Completed</span>
|
<span class="text-sm">Completed</span>
|
||||||
<input type="radio" name="show-completed" class="radio radio-sm checked:radio-success" />
|
<input type="radio" name="show" class="radio radio-sm checked:radio-success" />
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{/* Filter by level */}
|
{/* Filter by level */}
|
||||||
@ -71,33 +163,60 @@ export default component$((props: { section: Section }) => {
|
|||||||
<p class="font-bold text-sm">Filter</p>
|
<p class="font-bold text-sm">Filter</p>
|
||||||
<label class="p-2 rounded hover:bg-front transition-all cursor-pointer flex gap-2">
|
<label class="p-2 rounded hover:bg-front transition-all cursor-pointer flex gap-2">
|
||||||
<span class="text-sm">Basic</span>
|
<span class="text-sm">Basic</span>
|
||||||
<input type="checkbox" checked class="checkbox checkbox-sm checked:checkbox-success" />
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={filterState.levels.recommended}
|
||||||
|
onChange$={() => (filterState.levels.recommended = !filterState.levels.recommended)}
|
||||||
|
class="checkbox checkbox-sm checked:checkbox-success"
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="p-2 rounded hover:bg-front transition-all cursor-pointer flex gap-2">
|
<label class="p-2 rounded hover:bg-front transition-all cursor-pointer flex gap-2">
|
||||||
<span class="text-sm">Optional</span>
|
<span class="text-sm">Optional</span>
|
||||||
<input type="checkbox" checked class="checkbox checkbox-sm checked:checkbox-warning" />
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={filterState.levels.optional}
|
||||||
|
onChange$={() => (filterState.levels.optional = !filterState.levels.optional)}
|
||||||
|
class="checkbox checkbox-sm checked:checkbox-warning"
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label class="p-2 rounded hover:bg-front transition-all cursor-pointer flex gap-2">
|
<label
|
||||||
|
class="p-2 rounded hover:bg-front transition-all cursor-pointer flex gap-2">
|
||||||
<span class="text-sm">Advanced</span>
|
<span class="text-sm">Advanced</span>
|
||||||
<input type="checkbox" checked class="checkbox checkbox-sm checked:checkbox-error" />
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={filterState.levels.advanced}
|
||||||
|
class="checkbox checkbox-sm checked:checkbox-error"
|
||||||
|
onChange$={() => (filterState.levels.advanced = !filterState.levels.advanced)}
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Done?</th>
|
{ [
|
||||||
<th>Advice</th>
|
{ id: 'done', text: 'Done?'},
|
||||||
<th>Level</th>
|
{ id: 'advice', text: 'Advice' },
|
||||||
|
{ id: 'level', text: 'Level' }
|
||||||
|
].map((item) => (
|
||||||
|
<th
|
||||||
|
key={item.id}
|
||||||
|
class="cursor-pointer"
|
||||||
|
onClick$={() => handleSort(item.id)}
|
||||||
|
>
|
||||||
|
<span class="flex items-center gap-0.5 hover:text-primary transition">
|
||||||
|
<Icon width={12} height={14} icon="sort" />
|
||||||
|
{item.text}
|
||||||
|
</span>
|
||||||
|
</th>
|
||||||
|
))}
|
||||||
<th>Details</th>
|
<th>Details</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{props.section.checklist.map((item, index) => {
|
{filteredChecklist.sort(sortChecklist).map((item, index) => {
|
||||||
const badgeColor = getBadgeClass(item.priority);
|
const badgeColor = getBadgeClass(item.priority);
|
||||||
const itemId = generateId(item.point);
|
const itemId = generateId(item.point);
|
||||||
const isItemCompleted = isChecked(itemId);
|
const isItemCompleted = isChecked(itemId);
|
||||||
|
@ -39,7 +39,7 @@ module.exports = {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
safelist: [
|
safelist: [
|
||||||
{
|
{ // TODO: This adds a lot of overhead. Go through code, and remove any un-needed variants.
|
||||||
pattern: /(bg|outline|text|tw-color|border)-(yellow|lime|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|red)-(200|300|400|500|600)/,
|
pattern: /(bg|outline|text|tw-color|border)-(yellow|lime|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|red)-(200|300|400|500|600)/,
|
||||||
variants: ['light', 'dark', 'hover', 'focus'],
|
variants: ['light', 'dark', 'hover', 'focus'],
|
||||||
},
|
},
|
||||||
|
@ -3010,6 +3010,11 @@ queue-microtask@^1.2.2:
|
|||||||
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243"
|
||||||
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
|
||||||
|
|
||||||
|
qwik-transition@^0.0.7:
|
||||||
|
version "0.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/qwik-transition/-/qwik-transition-0.0.7.tgz#57c3d6db5fd0d0bca47a6ddef76a77b39a0b3c8a"
|
||||||
|
integrity sha512-U1wEJ/RWYuwfuWl8cWToTJASkRogoBGlBKbr/lKhzQYGeLNXk2K7IUXCMtELcUNHEI7J1MyNbjYc5lA6fMbQ8w==
|
||||||
|
|
||||||
read-cache@^1.0.0:
|
read-cache@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"
|
resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774"
|
||||||
|
Loading…
Reference in New Issue
Block a user