Adds progress stats, for each priority
parent
3cf85cdde0
commit
fd639567dd
|
@ -36,6 +36,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@builder.io/qwik": "^1.1.4",
|
"@builder.io/qwik": "^1.1.4",
|
||||||
"marked": "^12.0.0",
|
"marked": "^12.0.0",
|
||||||
|
"progressbar.js": "^1.1.1",
|
||||||
"sharp": "^0.33.2"
|
"sharp": "^0.33.2"
|
||||||
},
|
},
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
|
|
|
@ -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 { useLocalStorage } from "~/hooks/useLocalStorage";
|
||||||
import { ChecklistContext } from "~/store/checklist-context";
|
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$(() => {
|
export default component$(() => {
|
||||||
|
|
||||||
|
// All checklist data, from store
|
||||||
const checklists = useContext(ChecklistContext);
|
const checklists = useContext(ChecklistContext);
|
||||||
|
// Completed items, from local storage
|
||||||
const totalProgress = useSignal(0);
|
const [checkedItems] = useLocalStorage('PSC_PROGRESS', {});
|
||||||
|
// Store to hold calculated progress results
|
||||||
const STORAGE_KEY = 'PSC_PROGRESS';
|
const totalProgress = useSignal({ completed: 0, outOf: 0 });
|
||||||
const [checkedItems] = useLocalStorage(STORAGE_KEY, {});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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) {
|
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);
|
const totalItems = sections.reduce((total: number, section: Section) => total + section.checklist.length, 0);
|
||||||
let totalComplete = 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', $(() => {
|
useOnWindow('load', $(() => {
|
||||||
|
|
||||||
calculateProgress(checklists.value)
|
calculateProgress(checklists.value)
|
||||||
.then(percentage => {
|
.then((progress) => {
|
||||||
totalProgress.value = percentage;
|
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 (
|
return (
|
||||||
<div>
|
<div class="flex justify-center flex-col w-full items-center">
|
||||||
<p>{totalProgress}</p>
|
<div class="mb-4">
|
||||||
</div>
|
<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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
declare module 'progressbar.js';
|
|
@ -1697,7 +1697,7 @@ fs.realpath@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f"
|
||||||
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
|
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"
|
version "2.3.3"
|
||||||
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
|
resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
|
||||||
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
|
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"
|
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
|
||||||
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
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:
|
property-information@^6.0.0:
|
||||||
version "6.4.1"
|
version "6.4.1"
|
||||||
resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.4.1.tgz#de8b79a7415fd2107dfbe65758bb2cc9dfcf60ac"
|
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"
|
resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172"
|
||||||
integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
|
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:
|
side-channel@^1.0.4:
|
||||||
version "1.0.4"
|
version "1.0.4"
|
||||||
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
|
resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"
|
||||||
|
|
Loading…
Reference in New Issue