diff --git a/web/package.json b/web/package.json index b39d011..c854ae2 100644 --- a/web/package.json +++ b/web/package.json @@ -36,6 +36,7 @@ "dependencies": { "@builder.io/qwik": "^1.1.4", "marked": "^12.0.0", + "progressbar.js": "^1.1.1", "sharp": "^0.33.2" }, "resolutions": { diff --git a/web/src/components/psc/progress.tsx b/web/src/components/psc/progress.tsx index ba565ea..21c7deb 100644 --- a/web/src/components/psc/progress.tsx +++ b/web/src/components/psc/progress.tsx @@ -1,24 +1,34 @@ -import { $, component$, useTask$, useSignal, useOnWindow, useContext } from "@builder.io/qwik"; +import { $, component$, useSignal, useOnWindow, useContext } from "@builder.io/qwik"; -import type { Priority, Sections, Section } from '../../types/PSC'; import { useLocalStorage } from "~/hooks/useLocalStorage"; import { ChecklistContext } from "~/store/checklist-context"; +import type { Priority, Sections, Section } from '~/types/PSC'; +/** + * Component for client-side user progress metrics. + * Combines checklist data with progress from local storage, + * calculates percentage completion for each priority level, + * and renders some pretty pie charts to visualize results + */ export default component$(() => { + // All checklist data, from store const checklists = useContext(ChecklistContext); - - const totalProgress = useSignal(0); - - const STORAGE_KEY = 'PSC_PROGRESS'; - const [checkedItems] = useLocalStorage(STORAGE_KEY, {}); + // Completed items, from local storage + const [checkedItems] = useLocalStorage('PSC_PROGRESS', {}); + // Store to hold calculated progress results + const totalProgress = useSignal({ completed: 0, outOf: 0 }); /** - * Given an array of sections, returns the percentage completion of all included checklists. + * Calculates the users progress over specified sections. + * Given an array of sections, reads checklists in each, + * counts total number of checklist items + * counts the number of completed items from local storage + * and returns the percentage of completion */ - const calculateProgress = $((sections: Sections): number => { + const calculateProgress = $((sections: Sections): { completed: number, outOf: number } => { if (!checkedItems.value || !sections.length) { - return 0; + return { completed: 0, outOf: 0 }; } const totalItems = sections.reduce((total: number, section: Section) => total + section.checklist.length, 0); let totalComplete = 0; @@ -31,20 +41,141 @@ export default component$(() => { } }); }); - return Math.round((totalComplete / totalItems) * 100); + return { completed: totalComplete, outOf: totalItems }; + // return Math.round((totalComplete / totalItems) * 100); }); + /** + * Filters the checklist items in a given array of sections, + * so only the ones of a given priority are returned + * @param sections - Array of sections to filter + * @param priority - The priority to filter by + */ + const filterByPriority = $((sections: Sections, priority: Priority): Sections => { + const normalize = (pri: string) => pri.toLowerCase().replace(/ /g, '-'); + return sections.map(section => ({ + ...section, + checklist: section.checklist.filter(item => normalize(item.priority) === normalize(priority)) + })); + }); + + /** + * Draws a completion chart using ProgressBar.js + * Illustrating a given percent rendered to a given target element + * @param percentage - The percentage of completion (0-100) + * @param target - The ID of the element to draw the chart in + * @param color - The color of the progress chart, defaults to Tailwind primary + */ + const drawProgress = $((percentage: number, target: string, color?: string) => { + // Get a given color value from Tailwind CSS variable + const getCssVariableValue = (variableName: string, fallback = '') => { + return getComputedStyle(document.documentElement) + .getPropertyValue(variableName) + .trim() + || fallback; + } + // Define colors and styles for progress chart + const primaryColor = color || 'hsl(var(--pf, 220, 13%, 69%))'; + const foregroundColor = 'hsl(var(--nc, 220, 13%, 69%))'; + const red = `hsl(${getCssVariableValue('--er', '0 91% 71%')})`; + const green = `hsl(${getCssVariableValue('--su', '158 64% 52%')})`; + const labelStyles = { + color: foregroundColor, position: 'absolute', right: '0.5rem', top: '2rem' + }; + // Animations to occur on each step of the progress bar + const stepFunction = (state: any, bar: any) => { + const value = Math.round(bar.value() * 100); + bar.path.setAttribute('stroke', state.color); + bar.setText(value ? `${value}%` : ''); + if (value >= percentage) { + bar.path.setAttribute('stroke', primaryColor); + } + }; + // Define config settings for progress chart + const progressConfig = { + strokeWidth: 6, + trailWidth: 3, + color: primaryColor, + trailColor: foregroundColor, + text: { style: labelStyles }, + from: { color: red }, + to: { color: green }, + step: stepFunction, + }; + // Initiate ProgressBar.js passing in config, to draw the progress chart + import('progressbar.js').then((ProgressBar) => { + const line = new ProgressBar.SemiCircle(target, progressConfig); + line.animate(percentage / 100); + }); + }); + + /** + * Given a priority, filters the checklist, calculates data, renders chart + * @param priority - The priority to filter by + * @param color - The color override for the chart + */ + const makeDataAndDrawChart = $((priority: Priority, color?: string) => { + filterByPriority(checklists.value, priority) + .then((sections: Sections) => { + calculateProgress(sections) + .then((progress) => { + const { completed, outOf } = progress; + const percent = Math.round((completed / outOf) * 100) + drawProgress(percent, `#${priority}-container`, color) + }) + }); + }); + + /** + * When the window has loaded (client-side only) + * Initiate the filtering, calculation and rendering of progress charts + */ useOnWindow('load', $(() => { + calculateProgress(checklists.value) - .then(percentage => { - totalProgress.value = percentage; - }); + .then((progress) => { + totalProgress.value = progress; + }) + + makeDataAndDrawChart('recommended', 'hsl(var(--su, 158 64% 52%))'); + makeDataAndDrawChart('optional', 'hsl(var(--wa, 43 96% 56%))'); + makeDataAndDrawChart('advanced', 'hsl(var(--er, 0 91% 71%))'); })); + const items = [ + { id: 'recommended-container', label: 'Essential' }, + { id: 'optional-container', label: 'Optional' }, + { id: 'advanced-container', label: 'Advanced' }, + ]; + + // Beware, some god-awful markup ahead (thank Tailwind for that!) return ( -
{totalProgress}
-+ You've completed {totalProgress.value.completed} out of {totalProgress.value.outOf} items +
+ +{item.label}
+