2024-02-08 20:58:51 +00:00
|
|
|
import { $, component$, useStore, useSignal } from "@builder.io/qwik";
|
|
|
|
import { useCSSTransition } from "qwik-transition";
|
2024-02-06 17:05:46 +00:00
|
|
|
|
2024-02-08 18:07:29 +00:00
|
|
|
import Icon from "~/components/core/icon";
|
2024-02-08 20:58:51 +00:00
|
|
|
import type { Priority, Section, Checklist } from '../../types/PSC';
|
2024-02-06 17:05:46 +00:00
|
|
|
import { marked } from "marked";
|
|
|
|
import { useLocalStorage } from "~/hooks/useLocalStorage";
|
|
|
|
|
|
|
|
export default component$((props: { section: Section }) => {
|
|
|
|
|
2024-02-08 18:07:29 +00:00
|
|
|
const [completed, setCompleted] = useLocalStorage('PSC_PROGRESS', {});
|
|
|
|
const [ignored, setIgnored] = useLocalStorage('PSC_IGNORED', {});
|
2024-02-06 17:05:46 +00:00
|
|
|
|
2024-02-08 20:58:51 +00:00
|
|
|
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);
|
|
|
|
|
2024-02-06 17:05:46 +00:00
|
|
|
const getBadgeClass = (priority: Priority, precedeClass: string = '') => {
|
|
|
|
switch (priority.toLocaleLowerCase()) {
|
|
|
|
case 'recommended':
|
|
|
|
return `${precedeClass}success`;
|
|
|
|
case 'optional':
|
|
|
|
return `${precedeClass}warning`;
|
|
|
|
case 'advanced':
|
|
|
|
return `${precedeClass}error`;
|
|
|
|
default:
|
|
|
|
return `${precedeClass}neutral`;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const generateId = (title: string) => {
|
|
|
|
return title.toLowerCase().replace(/ /g, '-');
|
|
|
|
};
|
|
|
|
|
|
|
|
const parseMarkdown = (text: string | undefined): string => {
|
|
|
|
return marked.parse(text || '', { async: false }) as string || '';
|
|
|
|
};
|
|
|
|
|
2024-02-08 18:07:29 +00:00
|
|
|
const isIgnored = (pointId: string) => {
|
|
|
|
return ignored.value[pointId] || false;
|
2024-02-06 17:05:46 +00:00
|
|
|
};
|
|
|
|
|
2024-02-08 18:07:29 +00:00
|
|
|
|
|
|
|
const isChecked = (pointId: string) => {
|
|
|
|
if (isIgnored(pointId)) return false;
|
|
|
|
return completed.value[pointId] || false;
|
|
|
|
};
|
|
|
|
|
2024-02-08 20:58:51 +00:00
|
|
|
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;
|
|
|
|
});
|
|
|
|
|
2024-02-06 17:05:46 +00:00
|
|
|
return (
|
2024-02-08 18:07:29 +00:00
|
|
|
<>
|
|
|
|
|
2024-02-08 20:58:51 +00:00
|
|
|
|
|
|
|
<div class="flex flex-wrap gap-2 justify-end my-4">
|
|
|
|
{(sortState.column || JSON.stringify(filterState) !== JSON.stringify(originalFilters)) && (
|
|
|
|
<button class="btn btn-sm hover:btn-primary" onClick$={resetFilters}>
|
|
|
|
<Icon width={18} height={16} icon="clear"/>
|
|
|
|
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 }}>
|
2024-02-08 18:07:29 +00:00
|
|
|
{/* Filter by completion */}
|
|
|
|
<div class="flex justify-end items-center gap-1">
|
|
|
|
<p class="font-bold text-sm">Show</p>
|
2024-02-08 20:58:51 +00:00
|
|
|
<label onClick$={() => (filterState.show = 'all')}
|
|
|
|
class="p-2 rounded hover:bg-front transition-all cursor-pointer flex gap-2">
|
2024-02-08 18:07:29 +00:00
|
|
|
<span class="text-sm">All</span>
|
2024-02-08 20:58:51 +00:00
|
|
|
<input type="radio" name="show" class="radio radio-sm checked:radio-info" checked />
|
2024-02-08 18:07:29 +00:00
|
|
|
</label>
|
2024-02-08 20:58:51 +00:00
|
|
|
<label onClick$={() => (filterState.show = 'remaining')}
|
|
|
|
class="p-2 rounded hover:bg-front transition-all cursor-pointer flex gap-2">
|
2024-02-08 18:07:29 +00:00
|
|
|
<span class="text-sm">Remaining</span>
|
2024-02-08 20:58:51 +00:00
|
|
|
<input type="radio" name="show" class="radio radio-sm checked:radio-error" />
|
2024-02-08 18:07:29 +00:00
|
|
|
</label>
|
2024-02-08 20:58:51 +00:00
|
|
|
<label onClick$={() => (filterState.show = 'completed')}
|
|
|
|
class="p-2 rounded hover:bg-front transition-all cursor-pointer flex gap-2">
|
2024-02-08 18:07:29 +00:00
|
|
|
<span class="text-sm">Completed</span>
|
2024-02-08 20:58:51 +00:00
|
|
|
<input type="radio" name="show" class="radio radio-sm checked:radio-success" />
|
2024-02-08 18:07:29 +00:00
|
|
|
</label>
|
|
|
|
</div>
|
|
|
|
{/* Filter by level */}
|
|
|
|
<div class="flex justify-end items-center gap-1">
|
|
|
|
<p class="font-bold text-sm">Filter</p>
|
|
|
|
<label class="p-2 rounded hover:bg-front transition-all cursor-pointer flex gap-2">
|
|
|
|
<span class="text-sm">Basic</span>
|
2024-02-08 20:58:51 +00:00
|
|
|
<input
|
|
|
|
type="checkbox"
|
|
|
|
checked={filterState.levels.recommended}
|
|
|
|
onChange$={() => (filterState.levels.recommended = !filterState.levels.recommended)}
|
|
|
|
class="checkbox checkbox-sm checked:checkbox-success"
|
|
|
|
/>
|
2024-02-08 18:07:29 +00:00
|
|
|
</label>
|
|
|
|
<label class="p-2 rounded hover:bg-front transition-all cursor-pointer flex gap-2">
|
|
|
|
<span class="text-sm">Optional</span>
|
2024-02-08 20:58:51 +00:00
|
|
|
<input
|
|
|
|
type="checkbox"
|
|
|
|
checked={filterState.levels.optional}
|
|
|
|
onChange$={() => (filterState.levels.optional = !filterState.levels.optional)}
|
|
|
|
class="checkbox checkbox-sm checked:checkbox-warning"
|
|
|
|
/>
|
2024-02-08 18:07:29 +00:00
|
|
|
</label>
|
2024-02-08 20:58:51 +00:00
|
|
|
<label
|
|
|
|
class="p-2 rounded hover:bg-front transition-all cursor-pointer flex gap-2">
|
2024-02-08 18:07:29 +00:00
|
|
|
<span class="text-sm">Advanced</span>
|
2024-02-08 20:58:51 +00:00
|
|
|
<input
|
|
|
|
type="checkbox"
|
|
|
|
checked={filterState.levels.advanced}
|
|
|
|
class="checkbox checkbox-sm checked:checkbox-error"
|
|
|
|
onChange$={() => (filterState.levels.advanced = !filterState.levels.advanced)}
|
|
|
|
/>
|
2024-02-08 18:07:29 +00:00
|
|
|
</label>
|
|
|
|
</div>
|
|
|
|
</div>
|
2024-02-08 20:58:51 +00:00
|
|
|
)}
|
2024-02-08 18:07:29 +00:00
|
|
|
|
2024-02-06 17:05:46 +00:00
|
|
|
<table class="table">
|
|
|
|
<thead>
|
|
|
|
<tr>
|
2024-02-08 20:58:51 +00:00
|
|
|
{ [
|
|
|
|
{ id: 'done', text: 'Done?'},
|
|
|
|
{ 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>
|
|
|
|
))}
|
2024-02-06 17:05:46 +00:00
|
|
|
<th>Details</th>
|
|
|
|
</tr>
|
|
|
|
</thead>
|
|
|
|
<tbody>
|
2024-02-08 20:58:51 +00:00
|
|
|
{filteredChecklist.sort(sortChecklist).map((item, index) => {
|
2024-02-08 18:07:29 +00:00
|
|
|
const badgeColor = getBadgeClass(item.priority);
|
|
|
|
const itemId = generateId(item.point);
|
|
|
|
const isItemCompleted = isChecked(itemId);
|
|
|
|
const isItemIgnored = isIgnored(itemId);
|
|
|
|
return (
|
|
|
|
<tr key={index} class={[
|
|
|
|
'rounded-sm transition-all',
|
|
|
|
isItemCompleted ? `bg-${badgeColor} bg-opacity-10` : '',
|
|
|
|
isItemIgnored? 'bg-neutral bg-opacity-15' : '',
|
|
|
|
!isItemIgnored && !isItemCompleted ? `hover:bg-opacity-5 hover:bg-${badgeColor}` : '',
|
|
|
|
]}>
|
|
|
|
<td class="text-center">
|
|
|
|
<input
|
|
|
|
type="checkbox"
|
|
|
|
class={`checkbox checked:checkbox-${badgeColor} hover:checkbox-${badgeColor}`}
|
|
|
|
id={`done-${itemId}`}
|
|
|
|
checked={isChecked(itemId)}
|
|
|
|
disabled={isIgnored(itemId)}
|
|
|
|
onClick$={() => {
|
|
|
|
const data = completed.value;
|
|
|
|
data[itemId] = !data[itemId];
|
|
|
|
setCompleted(data);
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
<label for={`ignore-${itemId}`} class="text-small block opacity-50 mt-2">Ignore</label>
|
|
|
|
<input
|
|
|
|
type="checkbox"
|
|
|
|
id={`ignore-${itemId}`}
|
|
|
|
class={`toggle toggle-xs toggle-${badgeColor}`}
|
|
|
|
checked={isIgnored(itemId)}
|
|
|
|
onClick$={() => {
|
|
|
|
const ignoredData = ignored.value;
|
|
|
|
ignoredData[itemId] = !ignoredData[itemId];
|
|
|
|
setIgnored(ignoredData);
|
|
|
|
|
|
|
|
const completedData = completed.value;
|
|
|
|
completedData[itemId] = false;
|
|
|
|
setCompleted(completedData);
|
|
|
|
}}
|
|
|
|
/>
|
|
|
|
</td>
|
|
|
|
<td>
|
|
|
|
<label
|
|
|
|
for={`done-${itemId}`}
|
|
|
|
class={`text-base font-bold ${isIgnored(itemId) ? 'line-through' : 'cursor-pointer'}`}>
|
|
|
|
{item.point}
|
|
|
|
</label>
|
|
|
|
</td>
|
|
|
|
<td>
|
|
|
|
<div class={`badge gap-2 badge-${badgeColor}`}>
|
|
|
|
{item.priority}
|
|
|
|
</div>
|
|
|
|
</td>
|
|
|
|
<td dangerouslySetInnerHTML={parseMarkdown(item.details)}></td>
|
|
|
|
</tr>
|
|
|
|
)}
|
|
|
|
)}
|
2024-02-06 17:05:46 +00:00
|
|
|
</tbody>
|
|
|
|
</table>
|
2024-02-08 18:07:29 +00:00
|
|
|
</>
|
2024-02-06 17:05:46 +00:00
|
|
|
);
|
|
|
|
});
|