From 5d665df3d4eec523471bed9c21de6b65fdcc22e9 Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Thu, 8 Feb 2024 18:07:29 +0000 Subject: [PATCH] Finishes progress component, improves styling for homepage, adds theme support, adds ignore option for table, starts working on filters for table, and more --- web/package.json | 1 + web/src/components/core/icon.tsx | 5 + web/src/components/furniture/hero.tsx | 2 +- web/src/components/psc/checklist-table.tsx | 149 ++++++++++++++---- web/src/components/psc/progress.tsx | 154 +++++++++++++++++-- web/src/components/psc/psc.module.css | 3 +- web/src/components/psc/section-link-grid.tsx | 10 +- web/src/routes/about/index.tsx | 4 +- web/src/routes/checklist/[title]/index.tsx | 2 +- web/tailwind.config.js | 81 ++++------ web/yarn.lock | 12 ++ 11 files changed, 319 insertions(+), 104 deletions(-) diff --git a/web/package.json b/web/package.json index c854ae2..acdb04a 100644 --- a/web/package.json +++ b/web/package.json @@ -35,6 +35,7 @@ }, "dependencies": { "@builder.io/qwik": "^1.1.4", + "chart.js": "^4.4.1", "marked": "^12.0.0", "progressbar.js": "^1.1.1", "sharp": "^0.33.2" diff --git a/web/src/components/core/icon.tsx b/web/src/components/core/icon.tsx index db1b1d1..db66ba7 100644 --- a/web/src/components/core/icon.tsx +++ b/web/src/components/core/icon.tsx @@ -109,6 +109,11 @@ const getSvgPath = (icon: string) => { vb: "0 0 512 512", path: "M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336h24V272H216c-13.3 0-24-10.7-24-24s10.7-24 24-24h48c13.3 0 24 10.7 24 24v88h8c13.3 0 24 10.7 24 24s-10.7 24-24 24H216c-13.3 0-24-10.7-24-24s10.7-24 24-24zm40-208a32 32 0 1 1 0 64 32 32 0 1 1 0-64z", }; + case 'filters': + return { + vb: "0 0 512 512", + path: "M3.9 54.9C10.5 40.9 24.5 32 40 32H472c15.5 0 29.5 8.9 36.1 22.9s4.6 30.5-5.2 42.5L320 320.9V448c0 12.1-6.8 23.2-17.7 28.6s-23.8 4.3-33.5-3l-64-48c-8.1-6-12.8-15.5-12.8-25.6V320.9L9 97.3C-.7 85.4-2.8 68.8 3.9 54.9z", + }; default: return { vb: "", path: "" }; // Default path or a placeholder icon } diff --git a/web/src/components/furniture/hero.tsx b/web/src/components/furniture/hero.tsx index d2eda3e..db895ea 100644 --- a/web/src/components/furniture/hero.tsx +++ b/web/src/components/furniture/hero.tsx @@ -4,7 +4,7 @@ import Icon from "../core/icon"; export default component$(() => { return ( -
+

The Ultimate

diff --git a/web/src/components/psc/checklist-table.tsx b/web/src/components/psc/checklist-table.tsx index 9094d5a..729bb0f 100644 --- a/web/src/components/psc/checklist-table.tsx +++ b/web/src/components/psc/checklist-table.tsx @@ -1,13 +1,14 @@ import { component$ } from "@builder.io/qwik"; +import Icon from "~/components/core/icon"; import type { Priority, Section } from '../../types/PSC'; import { marked } from "marked"; import { useLocalStorage } from "~/hooks/useLocalStorage"; export default component$((props: { section: Section }) => { - const STORAGE_KEY = 'PSC_PROGRESS'; - const [value, setValue] = useLocalStorage(STORAGE_KEY, {}); + const [completed, setCompleted] = useLocalStorage('PSC_PROGRESS', {}); + const [ignored, setIgnored] = useLocalStorage('PSC_IGNORED', {}); const getBadgeClass = (priority: Priority, precedeClass: string = '') => { switch (priority.toLocaleLowerCase()) { @@ -30,12 +31,62 @@ export default component$((props: { section: Section }) => { return marked.parse(text || '', { async: false }) as string || ''; }; - const isChecked = (point: string) => { - const pointId = generateId(point); - return value.value[pointId] || false; + const isIgnored = (pointId: string) => { + return ignored.value[pointId] || false; }; + + const isChecked = (pointId: string) => { + if (isIgnored(pointId)) return false; + return completed.value[pointId] || false; + }; + return ( + <> + +
+ +
+ +
+
+ {/* Filter by completion */} +
+

Show

+ + + +
+ {/* Filter by level */} +
+

Filter

+ + + +
+
+
+ + + @@ -46,36 +97,66 @@ export default component$((props: { section: Section }) => { - {props.section?.checklist.map((item, index) => ( - - - - - - - ))} + {props.section.checklist.map((item, index) => { + const badgeColor = getBadgeClass(item.priority); + const itemId = generateId(item.point); + const isItemCompleted = isChecked(itemId); + const isItemIgnored = isIgnored(itemId); + return ( + + + + + + + )} + )}
- { - const id = item.point.toLowerCase().replace(/ /g, '-'); - const data = value.value; - data[id] = !data[id]; - setValue(data); - }} - /> - - - -
- {item.priority} -
-
+ { + const data = completed.value; + data[itemId] = !data[itemId]; + setCompleted(data); + }} + /> + + { + const ignoredData = ignored.value; + ignoredData[itemId] = !ignoredData[itemId]; + setIgnored(ignoredData); + + const completedData = completed.value; + completedData[itemId] = false; + setCompleted(completedData); + }} + /> + + + +
+ {item.priority} +
+
+ ); }); diff --git a/web/src/components/psc/progress.tsx b/web/src/components/psc/progress.tsx index 21c7deb..6a8363a 100644 --- a/web/src/components/psc/progress.tsx +++ b/web/src/components/psc/progress.tsx @@ -1,4 +1,5 @@ import { $, component$, useSignal, useOnWindow, useContext } from "@builder.io/qwik"; +import { Chart, registerables } from 'chart.js'; import { useLocalStorage } from "~/hooks/useLocalStorage"; import { ChecklistContext } from "~/store/checklist-context"; @@ -18,6 +19,8 @@ export default component$(() => { const [checkedItems] = useLocalStorage('PSC_PROGRESS', {}); // Store to hold calculated progress results const totalProgress = useSignal({ completed: 0, outOf: 0 }); + // Ref to the radar chart canvas + const radarChart = useSignal(); /** * Calculates the users progress over specified sections. @@ -142,6 +145,111 @@ export default component$(() => { makeDataAndDrawChart('advanced', 'hsl(var(--er, 0 91% 71%))'); })); + + interface RadarChartData { + labels: string[]; + datasets: { + label: string; + data: number[]; + [key: string]: any; // Anything else goes! + }[]; + } + + /** + * Builds the multi-dimensional data used for the radar chart + * based on each section, each level of priority, and the progress + * @param sections - The sections to build data from + */ + const makeRadarData = $((sections: Sections): Promise => { + // The labels for the corners of the chart, based on sections + const labels = sections.map((section: Section) => section.title); + // Items applied to every dataset + const datasetTemplate = { + borderWidth: 1, + }; + // Helper function to asynchronously calculate percentage + const calculatePercentage = async (section: Section, priority: Priority) => { + const filteredSections = await filterByPriority([section], priority); + const progress = await calculateProgress(filteredSections); + return progress.outOf > 0 ? (progress.completed / progress.outOf) * 100 : 0; + }; + + // Asynchronously build data for each priority level + const buildDataForPriority = (priority: Priority, color: string) => { + return Promise.all(sections.map(section => calculatePercentage(section, priority))) + .then(data => ({ + ...datasetTemplate, + label: priority.charAt(0).toUpperCase() + priority.slice(1), + data: data, + backgroundColor: color, + })); + }; + + // Wait on each set to resolve, and return the final data object + return Promise.all([ + buildDataForPriority('recommended', 'hsl(158 64% 52%/75%)'), + buildDataForPriority('optional', 'hsl(43 96% 56%/75%)'), + buildDataForPriority('advanced', 'hsl(0 91% 71%/75%)'), + ]).then(datasets => ({ + labels, + datasets, + })); + }); + + + + useOnWindow('load', $(() => { + Chart.register(...registerables); + + makeRadarData(checklists.value).then((data) => { + if (radarChart.value) { + new Chart(radarChart.value, { + type: 'radar', + data, + options: { + responsive: true, + scales: { + r: { + angleLines: { + display: true, + color: '#7d7d7da1', + }, + suggestedMin: 0, + suggestedMax: 100, + ticks: { + stepSize: 25, + callback: (value) => `${value}%`, + color: '#ffffffbf', + backdropColor: '#ffffff3b', + }, + grid: { + display: true, + color: '#7d7d7dd4', + }, + }, + }, + plugins: { + legend: { + position: 'bottom', + labels: { + font: { + size: 10, + }, + }, + }, + tooltip: { + callbacks: { + label: (ctx) => `Completed ${ctx.parsed.r}% of ${ctx.dataset.label || ''} items`, + } + } + }, + } + }); + + } + }); + })); + const items = [ { id: 'recommended-container', label: 'Essential' }, { id: 'optional-container', label: 'Optional' }, @@ -150,9 +258,11 @@ export default component$(() => { // Beware, some god-awful markup ahead (thank Tailwind for that!) return ( -
-
-
+
+ +
+ {/* Progress Percent */} +

Your Progress

You've completed {totalProgress.value.completed} out of {totalProgress.value.outOf} items @@ -163,17 +273,35 @@ export default component$(() => { max={totalProgress.value.outOf}>

-
- ); diff --git a/web/src/components/psc/psc.module.css b/web/src/components/psc/psc.module.css index 4e899de..2e3fe27 100644 --- a/web/src/components/psc/psc.module.css +++ b/web/src/components/psc/psc.module.css @@ -1,6 +1,5 @@ .container { - display: grid; - gap: 2rem; + /* I couldn't figure out how to do this with Tailwind.... */ grid-template-columns: repeat(auto-fit, minmax(350px, 1fr)); } diff --git a/web/src/components/psc/section-link-grid.tsx b/web/src/components/psc/section-link-grid.tsx index aca6874..28204ae 100644 --- a/web/src/components/psc/section-link-grid.tsx +++ b/web/src/components/psc/section-link-grid.tsx @@ -7,13 +7,15 @@ import styles from './psc.module.css'; export default component$((props: { sections: Section[] }) => { return ( -
+
{props.sections.map((section: Section) => (
diff --git a/web/src/routes/about/index.tsx b/web/src/routes/about/index.tsx index e371f3d..2449266 100644 --- a/web/src/routes/about/index.tsx +++ b/web/src/routes/about/index.tsx @@ -5,11 +5,11 @@ import type { DocumentHead } from "@builder.io/qwik-city"; export default component$(() => { return (
-
+

About the Security Checklist

-
+

About the Author

This project was originally started by me, Alicia Sykes- with a lot of help from the community. diff --git a/web/src/routes/checklist/[title]/index.tsx b/web/src/routes/checklist/[title]/index.tsx index 963da4e..9eb9da5 100644 --- a/web/src/routes/checklist/[title]/index.tsx +++ b/web/src/routes/checklist/[title]/index.tsx @@ -22,7 +22,7 @@ export default component$(() => { return (

-
+

{section?.title} diff --git a/web/tailwind.config.js b/web/tailwind.config.js index 0e4445d..94bbe09 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -1,41 +1,41 @@ + +const applyCustomColors = (theme, front, back) => { + return { + ...require("daisyui/src/theming/themes")[`[data-theme=${theme}]`], + "--front":front, + "--back": back || `${front} /0.75`, + }; +}; + module.exports = { content: ['./src/**/*.{js,ts,jsx,tsx,mdx}'], plugins: [require('daisyui')], + theme: { + extend: { + colors: { + "front": "hsl(var(--front, 0deg 0% 60% / 10%))", + "back": "hsl(var(--back, 212 14% 10% / 1))", + }, + }, + }, daisyui: { themes: [ - "light", - "dark", - "cupcake", - "bumblebee", - "emerald", - "corporate", - "synthwave", - "retro", - "cyberpunk", - "valentine", - "halloween", - "garden", - "forest", - "aqua", - "lofi", - "pastel", - "fantasy", - "wireframe", - "black", - "luxury", - "dracula", - "cmyk", - "autumn", - "business", - "acid", - "lemonade", - "night", - "coffee", - "winter", - "dim", - "nord", - "sunset", + { light: applyCustomColors("light", "237 9% 86% / 0.75", "237 9% 86% / 1") }, + { dark: applyCustomColors("dark", "217 14% 17%", "212 14% 10%") }, + { night: applyCustomColors("night", "220deg 44.68% 9.22%", "219.2, 38.2%, 13.3%") }, + + { cupcake: applyCustomColors("cupcake", "297deg 77% 90%", "303.33deg 60% 94.12%") }, + { bumblebee: applyCustomColors("bumblebee", "75.5deg 40% 87%", "60deg 23.08% 92.35%") }, + { corporate: applyCustomColors("corporate", "211.67deg 43.9% 83.92%", "212.3, 25.5%, 90%") }, + { synthwave: applyCustomColors("synthwave", "253.3, 58.1%, 12.2%", "253.5, 47.6%, 16.5%") }, + { retro: applyCustomColors("retro", "41.9, 37.1%, 72%", "42.5, 36.4%, 87.1%") }, + { valentine: applyCustomColors("valentine", "320.4, 70.7%, 85.3%", "322.1, 61.3%, 93.9%") }, + { halloween: applyCustomColors("halloween", "0, 0%, 9%", "0, 0%, 16.9%") }, + { aqua: applyCustomColors("aqua", "230.5, 41%, 27.3%", "230.8, 33.9%, 22.5%") }, + { lofi: applyCustomColors("lofi", "228, 11.6%, 91.6%") }, + { fantasy: applyCustomColors("fantasy", "230.8, 33.9%, 22.5%", "210, 2.3%, 83.1%") }, + { dracula: applyCustomColors("dracula", "210, 2.3%, 83.1%, 0.03", "228, 20%, 14.7%") }, ], }, safelist: [ @@ -44,21 +44,8 @@ module.exports = { variants: ['light', 'dark', 'hover', 'focus'], }, { - pattern: /(badge|bg)-(success|warning|error|info|neutral)/, - variants: ['light', 'dark', 'hover', 'focus'], + pattern: /(badge|bg|checkbox|toggle)-(success|warning|error|info|neutral)/, + variants: ['light', 'dark', 'hover', 'focus', 'checked'], } ], - theme: { - extend: { - colors: { - 'custom-blue': '#5b6d5b', - 'custom-pink': { - 100: '#ffecef', - 400: '#ff69b4', - 800: '#ff1493', - }, - // Add more custom colors or extend existing ones here - }, - }, - }, }; diff --git a/web/yarn.lock b/web/yarn.lock index 39e2ae8..e467b94 100644 --- a/web/yarn.lock +++ b/web/yarn.lock @@ -480,6 +480,11 @@ "@jridgewell/resolve-uri" "^3.1.0" "@jridgewell/sourcemap-codec" "^1.4.14" +"@kurkle/color@^0.3.0": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@kurkle/color/-/color-0.3.2.tgz#5acd38242e8bde4f9986e7913c8fdf49d3aa199f" + integrity sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw== + "@mdx-js/mdx@2.3.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-2.3.0.tgz#d65d8c3c28f3f46bb0e7cb3bf7613b39980671a9" @@ -1019,6 +1024,13 @@ character-reference-invalid@^2.0.0: resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9" integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== +chart.js@^4.4.1: + version "4.4.1" + resolved "https://registry.yarnpkg.com/chart.js/-/chart.js-4.4.1.tgz#ac5dc0e69a7758909158a96fe80ce43b3bb96a9f" + integrity sha512-C74QN1bxwV1v2PEujhmKjOZ7iUM4w6BWs23Md/6aOZZSlwMzeCIDGuZay++rBgChYru7/+QFeoQW0fQoP534Dg== + dependencies: + "@kurkle/color" "^0.3.0" + chokidar@^3.5.3: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"