Adds progress stats, for each priority

This commit is contained in:
Alicia Sykes 2024-02-07 00:56:44 +00:00
parent 3cf85cdde0
commit fd639567dd
4 changed files with 166 additions and 18 deletions

View File

@ -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": {

View File

@ -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 (
<div>
<p>{totalProgress}</p>
</div>
<div class="flex justify-center flex-col w-full items-center">
<div class="mb-4">
<div class="rounded-box bg-neutral-content bg-opacity-5 w-96 p-4">
<h3 class="text-primary text-2xl">Your Progress</h3>
<p class="text-lg">
You've completed <b>{totalProgress.value.completed} out of {totalProgress.value.outOf}</b> items
</p>
<progress
class="progress w-80"
value={totalProgress.value.completed}
max={totalProgress.value.outOf}>
</progress>
</div>
</div>
<div class="carousel rounded-box mb-8">
{items.map((item) => (
<div
key={item.id}
class="flex flex-col justify-items-center carousel-item w-20 p-4
bg-neutral-content bg-opacity-5 mx-2.5 rounded-box">
<div class="relative" id={item.id}></div>
<p class="text-center">{item.label}</p>
</div>
))}
</div>
</div>
);
});

1
web/src/types/progressbar.d.ts vendored Normal file
View File

@ -0,0 +1 @@
declare module 'progressbar.js';

View File

@ -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"