From fd639567dd055879500ef5b5c629a06b816572bd Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Wed, 7 Feb 2024 00:56:44 +0000 Subject: [PATCH] Adds progress stats, for each priority --- web/package.json | 1 + web/src/components/psc/progress.tsx | 165 +++++++++++++++++++++++++--- web/src/types/progressbar.d.ts | 1 + web/yarn.lock | 17 ++- 4 files changed, 166 insertions(+), 18 deletions(-) create mode 100644 web/src/types/progressbar.d.ts 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}

-
+
+
+
+

Your Progress

+

+ You've completed {totalProgress.value.completed} out of {totalProgress.value.outOf} items +

+ + +
+
+ +
); }); diff --git a/web/src/types/progressbar.d.ts b/web/src/types/progressbar.d.ts new file mode 100644 index 0000000..6ab354b --- /dev/null +++ b/web/src/types/progressbar.d.ts @@ -0,0 +1 @@ +declare module 'progressbar.js'; diff --git a/web/yarn.lock b/web/yarn.lock index 9094743..39e2ae8 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -1697,7 +1697,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== -fsevents@~2.3.2, fsevents@~2.3.3: +fsevents@^2.3.2, fsevents@~2.3.2, fsevents@~2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== @@ -2975,6 +2975,14 @@ prelude-ls@^1.2.1: resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +progressbar.js@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/progressbar.js/-/progressbar.js-1.1.1.tgz#8c7dc52ce4cc8845c4f3055da75afc366a0543e9" + integrity sha512-FBsw3BKsUbb+hNeYfiP3xzvAAQrPi4DnGDw66bCmfuRCDLcslxyxv2GyYUdBSKFGSIBa73CUP5WMcl6F8AAXlw== + dependencies: + lodash.merge "^4.6.2" + shifty "^2.8.3" + property-information@^6.0.0: version "6.4.1" resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.4.1.tgz#de8b79a7415fd2107dfbe65758bb2cc9dfcf60ac" @@ -3196,6 +3204,13 @@ shebang-regex@^3.0.0: resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== +shifty@^2.8.3: + version "2.20.4" + resolved "https://registry.yarnpkg.com/shifty/-/shifty-2.20.4.tgz#fb2ec81697b808b250024fa9548b5b93fadd78cf" + integrity sha512-4Y0qRkg8ME5XN8yGNAwmFOmsIURGFKT9UQfNL6DDJQErYtN5HsjyoBuJn41ZQfTkuu2rIbRMn9qazjKsDpO2TA== + optionalDependencies: + fsevents "^2.3.2" + side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"