Adds progress saving, theme switching support and more

This commit is contained in:
Alicia Sykes 2024-02-05 20:38:14 +00:00
parent 9610d59a3c
commit 7a1801cc85
14 changed files with 198 additions and 62 deletions

View File

@ -1,5 +1,4 @@
{ {
"license": "MIT", "license": "MIT",
"scripts": { "scripts": {
"dev": "vite --mode ssr", "dev": "vite --mode ssr",
@ -25,6 +24,7 @@
"daisyui": "^3.0.2", "daisyui": "^3.0.2",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-plugin-qwik": "^1.4.3", "eslint-plugin-qwik": "^1.4.3",
"js-yaml": "^4.1.0",
"postcss": "^8.4.21", "postcss": "^8.4.21",
"tailwindcss": "^3.2.4", "tailwindcss": "^3.2.4",
"typescript": "^5.3.3", "typescript": "^5.3.3",

View File

@ -0,0 +1 @@
../../personal-security-checklist.yml

View File

@ -94,6 +94,11 @@ const getSvgPath = (icon: string) => {
vb: "0 0 512 512", vb: "0 0 512 512",
path: "M0 72C0 49.9 17.9 32 40 32H88c22.1 0 40 17.9 40 40v48c0 22.1-17.9 40-40 40H40c-22.1 0-40-17.9-40-40V72zM0 232c0-22.1 17.9-40 40-40H88c22.1 0 40 17.9 40 40v48c0 22.1-17.9 40-40 40H40c-22.1 0-40-17.9-40-40V232zM128 392v48c0 22.1-17.9 40-40 40H40c-22.1 0-40-17.9-40-40V392c0-22.1 17.9-40 40-40H88c22.1 0 40 17.9 40 40zM160 72c0-22.1 17.9-40 40-40h48c22.1 0 40 17.9 40 40v48c0 22.1-17.9 40-40 40H200c-22.1 0-40-17.9-40-40V72zM288 232v48c0 22.1-17.9 40-40 40H200c-22.1 0-40-17.9-40-40V232c0-22.1 17.9-40 40-40h48c22.1 0 40 17.9 40 40zM160 392c0-22.1 17.9-40 40-40h48c22.1 0 40 17.9 40 40v48c0 22.1-17.9 40-40 40H200c-22.1 0-40-17.9-40-40V392zM448 72v48c0 22.1-17.9 40-40 40H360c-22.1 0-40-17.9-40-40V72c0-22.1 17.9-40 40-40h48c22.1 0 40 17.9 40 40zM320 232c0-22.1 17.9-40 40-40h48c22.1 0 40 17.9 40 40v48c0 22.1-17.9 40-40 40H360c-22.1 0-40-17.9-40-40V232zM448 392v48c0 22.1-17.9 40-40 40H360c-22.1 0-40-17.9-40-40V392c0-22.1 17.9-40 40-40h48c22.1 0 40 17.9 40 40z", path: "M0 72C0 49.9 17.9 32 40 32H88c22.1 0 40 17.9 40 40v48c0 22.1-17.9 40-40 40H40c-22.1 0-40-17.9-40-40V72zM0 232c0-22.1 17.9-40 40-40H88c22.1 0 40 17.9 40 40v48c0 22.1-17.9 40-40 40H40c-22.1 0-40-17.9-40-40V232zM128 392v48c0 22.1-17.9 40-40 40H40c-22.1 0-40-17.9-40-40V392c0-22.1 17.9-40 40-40H88c22.1 0 40 17.9 40 40zM160 72c0-22.1 17.9-40 40-40h48c22.1 0 40 17.9 40 40v48c0 22.1-17.9 40-40 40H200c-22.1 0-40-17.9-40-40V72zM288 232v48c0 22.1-17.9 40-40 40H200c-22.1 0-40-17.9-40-40V232c0-22.1 17.9-40 40-40h48c22.1 0 40 17.9 40 40zM160 392c0-22.1 17.9-40 40-40h48c22.1 0 40 17.9 40 40v48c0 22.1-17.9 40-40 40H200c-22.1 0-40-17.9-40-40V392zM448 72v48c0 22.1-17.9 40-40 40H360c-22.1 0-40-17.9-40-40V72c0-22.1 17.9-40 40-40h48c22.1 0 40 17.9 40 40zM320 232c0-22.1 17.9-40 40-40h48c22.1 0 40 17.9 40 40v48c0 22.1-17.9 40-40 40H360c-22.1 0-40-17.9-40-40V232zM448 392v48c0 22.1-17.9 40-40 40H360c-22.1 0-40-17.9-40-40V392c0-22.1 17.9-40 40-40h48c22.1 0 40 17.9 40 40z",
}; };
case 'homepage':
return {
vb: "0 0 512 512",
path: "M575.8 255.5c0 18-15 32.1-32 32.1h-32l.7 160.2c0 2.7-.2 5.4-.5 8.1V472c0 22.1-17.9 40-40 40H456c-1.1 0-2.2 0-3.3-.1c-1.4 .1-2.8 .1-4.2 .1H416 392c-22.1 0-40-17.9-40-40V448 384c0-17.7-14.3-32-32-32H256c-17.7 0-32 14.3-32 32v64 24c0 22.1-17.9 40-40 40H160 128.1c-1.5 0-3-.1-4.5-.2c-1.2 .1-2.4 .2-3.6 .2H104c-22.1 0-40-17.9-40-40V360c0-.9 0-1.9 .1-2.8V287.6H32c-18 0-32-14-32-32.1c0-9 3-17 10-24L266.4 8c7-7 15-8 22-8s15 2 21 7L564.8 231.5c8 7 12 15 11 24z",
};
default: default:
return { vb: "", path: "" }; // Default path or a placeholder icon return { vb: "", path: "" }; // Default path or a placeholder icon
} }

View File

@ -1,13 +1,15 @@
import { component$ } from "@builder.io/qwik"; import { component$ } from "@builder.io/qwik";
import Icon from "~/components/core/icon";
import { data } from '~/mock-data';
import type { Section } from '~/types/PSC';
import { useTheme } from '~/store/theme-store';
import Icon from "../core/icon";
import { data } from '../../mock-data';
import type { Section } from '../../types/PSC';
export default component$(() => { export default component$(() => {
const { theme, setTheme } = useTheme();
return ( return (
<> <>
<input id="my-drawer-3" type="checkbox" class="drawer-toggle" /> <input id="my-drawer-3" type="checkbox" class="drawer-toggle" />
@ -62,7 +64,14 @@ export default component$(() => {
</ul> </ul>
<div class="tooltip tooltip-bottom" data-tip="Theme"> <div class="tooltip tooltip-bottom" data-tip="Theme">
<label class="cursor-pointer grid place-items-center"> <label class="cursor-pointer grid place-items-center">
<input type="checkbox" value="synthwave" class="toggle theme-controller bg-base-content row-start-1 col-start-1 col-span-2"/> <input
type="checkbox"
checked={theme.theme === 'dark'}
onClick$={() => {
setTheme(theme.theme === 'dark' ? 'light' : 'dark');
}}
class="toggle theme-controller bg-base-content row-start-1 col-start-1 col-span-2"
/>
<svg class="col-start-1 row-start-1 stroke-base-100 fill-base-100" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"/></svg> <svg class="col-start-1 row-start-1 stroke-base-100 fill-base-100" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><path d="M12 1v2M12 21v2M4.2 4.2l1.4 1.4M18.4 18.4l1.4 1.4M1 12h2M21 12h2M4.2 19.8l1.4-1.4M18.4 5.6l1.4-1.4"/></svg>
<svg class="col-start-2 row-start-1 stroke-base-100 fill-base-100" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg> <svg class="col-start-2 row-start-1 stroke-base-100 fill-base-100" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"></path></svg>
</label> </label>
@ -77,10 +86,10 @@ export default component$(() => {
<Icon class="mr-2" icon="shield" width={16} height={16} /> <Icon class="mr-2" icon="shield" width={16} height={16} />
Personal Security Checklist Personal Security Checklist
</h2> </h2>
<li><a href="/">Home</a></li> <li><a href="/"><Icon class="mr-2" icon="homepage" width={16} height={16} />Home</a></li>
<li><a href="/">GitHub</a></li> <li><a href="/"><Icon class="mr-2" icon="github" width={16} height={16} />GitHub</a></li>
<li> <li>
<a href="/checklist">Checklists</a> <a href="/checklist"><Icon class="mr-2" icon="all" width={16} height={16} />Checklists</a>
<ul> <ul>
{data.map((item: Section, index: number) => ( {data.map((item: Section, index: number) => (
<li key={`checklist-side-${index}`} class={`hover:bg-${item.color}-600 hover:bg-opacity-15`}> <li key={`checklist-side-${index}`} class={`hover:bg-${item.color}-600 hover:bg-opacity-15`}>
@ -109,10 +118,10 @@ export default component$(() => {
<a href="#">License</a> <a href="#">License</a>
</li> </li>
</ul> </ul>
<ul>
<li> <li>
<a href="#">Author</a> <a href="#">Author</a>
<ul> <ul>
<li><a href="#">About</a></li>
<li><a href="#">Contact</a></li> <li><a href="#">Contact</a></li>
<li> <li>
<a href="#">Socials</a> <a href="#">Socials</a>
@ -137,6 +146,7 @@ export default component$(() => {
</li> </li>
</ul> </ul>
</li> </li>
</ul>
</li> </li>
</ul> </ul>
</div> </div>

View File

@ -0,0 +1,31 @@
import { $, type QRL, useOnWindow, useStore } from "@builder.io/qwik";
export function useLocalStorage(key: string, initialState: any): [any, QRL<(value: any) => void>] {
const store = useStore({ value: initialState });
useOnWindow('load', $(() => {
try {
const item = window.localStorage.getItem(key);
if (!item) {
window.localStorage.setItem(key, JSON.stringify(initialState));
}
store.value = item ? JSON.parse(item) : initialState;
} catch (error) {
console.log(error);
store.value = initialState;
}
}));
const setValue$ = $((value: any) => {
try {
store.value = value;
if (typeof window !== "undefined") {
window.localStorage.setItem(key, JSON.stringify(value));
}
} catch (error) {
console.log(error);
}
});
return [store, setValue$];
}

View File

@ -4,23 +4,29 @@ import type { DocumentHead } from "@builder.io/qwik-city";
export default component$(() => { export default component$(() => {
return ( return (
<div class="m-4 md:mx-16">
<article class="bg-base-200 bg-opacity-25 p-8 mx-auto max-w-[1200px] m-8 rounded-lg shadow-lg"> <article class="bg-base-200 bg-opacity-25 p-8 mx-auto max-w-[1200px] m-8 rounded-lg shadow-lg">
<h3>About the Author</h3> <h2 class="text-2xl">About the Security Checklist</h2>
</article>
<div class="divider"></div>
<article class="bg-base-200 bg-opacity-25 p-8 mx-auto max-w-[1200px] m-8 rounded-lg shadow-lg">
<h2 class="text-2xl">About the Author</h2>
<p> <p>
This project was originally started by me, Alicia Sykes- with a lot of help from the community. This project was originally started by me, Alicia Sykes- with a lot of help from the community.
</p> </p>
<br /> <br />
<img class="ml-6 rounded-lg float-right" width="200" height="141" alt="Alicia Sykes Thames" src="https://i.ibb.co/s5SM1K7/DSC-0031-4.jpg" />
<p> <p>
I write apps which aim to help people escape big tech, secure their data, I write apps which aim to help people escape big tech, secure their data,
and protect their privacy. I have a particular interest in self-hosting, and protect their privacy.
Linux and OSINT.
</p> </p>
<br /> <br />
<p> <p>
If this type of stuff interests you, I maintain several other repositories that you may also like: I have a particular interest in self-hosting, Linux and OSINT.
So if this type of stuff interests you, check out these other projects that I maintain:
</p> </p>
<br /> <br />
<ul> <ul class="list-disc pl-8">
<li>Web-Check - OSINT tool for analysing any website</li> <li>Web-Check - OSINT tool for analysing any website</li>
<li>Dashy - Dashboard app, for organising your self-hosted services</li> <li>Dashy - Dashboard app, for organising your self-hosted services</li>
<li>Portainer-Templates - Compiled repository of 1-click Docker apps for self-hosting</li> <li>Portainer-Templates - Compiled repository of 1-click Docker apps for self-hosting</li>
@ -30,9 +36,11 @@ export default component$(() => {
<li>Email Comparison - Objective comparison of private/secure mail providers</li> <li>Email Comparison - Objective comparison of private/secure mail providers</li>
<li>Git-In - Tools and resources to help beginners get into open source</li> <li>Git-In - Tools and resources to help beginners get into open source</li>
</ul> </ul>
<br />
<p>For a full list of projects I've published, see <a href="https://apps.aliciasykes.com/">apps.aliciasykes.com</a>, or follow me on GitHub (I'm <a href="https://github.com/lissy93">Lissy93</a>).</p> <p>For a full list of projects I've published, see <a href="https://apps.aliciasykes.com/">apps.aliciasykes.com</a>, or follow me on GitHub (I'm <a href="https://github.com/lissy93">Lissy93</a>).</p>
</article> </article>
</div>
); );
}); });

View File

@ -1,23 +1,23 @@
import { component$ } from '@builder.io/qwik'; import { component$, useContext } from '@builder.io/qwik';
import { useLocation } from '@builder.io/qwik-city'; import { useLocation } from '@builder.io/qwik-city';
import { marked } from 'marked'; import { marked } from 'marked';
import Icon from '../../../components/core/icon'; import Icon from '~/components/core/icon';
import { data } from '../../../mock-data'; import { ChecklistContext } from '~/store/checklist-context';
import type { Section, Priority } from '../../../types/PSC'; import { useLocalStorage } from '~/hooks/useLocalStorage';
import type { Section, Priority } from "~/types/PSC";
export default component$(() => { export default component$(() => {
const checklists = useContext(ChecklistContext);
const loc = useLocation(); const loc = useLocation();
// const endpoint = useEndpoint<{ params: { title: string } }>();
const slug = loc.params.title; const slug = loc.params.title;
const section: Section | undefined = data.find((item: Section) => item.slug === slug); const section: Section | undefined = checklists.value.find((item: Section) => item.slug === slug);
// You can now use `title` to fetch data related to this checklist item
// and render it below.
const getBadgeClass = (priority: Priority, precedeClass: string = '') => { const getBadgeClass = (priority: Priority, precedeClass: string = '') => {
switch (priority) { switch (priority.toLocaleLowerCase()) {
case 'recommended': case 'recommended':
return `${precedeClass}success`; return `${precedeClass}success`;
case 'optional': case 'optional':
@ -37,7 +37,16 @@ export default component$(() => {
return marked.parse(text || '', { async: false }) as string || ''; return marked.parse(text || '', { async: false }) as string || '';
}; };
const STORAGE_KEY = 'PSC_PROGRESS';
const [value, setValue] = useLocalStorage(STORAGE_KEY, {});
const isChecked = (point: string) => {
const pointId = generateId(point);
return value.value[pointId] || false;
};
return ( return (
<div class="md:my-8 md:px-16 sm:px-2 rounded-md">
<article class="bg-base-200 bg-opacity-25 p-8 mx-auto w-full max-w-[1200px] rounded-lg shadow-lg"> <article class="bg-base-200 bg-opacity-25 p-8 mx-auto w-full max-w-[1200px] rounded-lg shadow-lg">
<h1 class={['gap-2 text-5xl font-bold capitalize flex']}> <h1 class={['gap-2 text-5xl font-bold capitalize flex']}>
<Icon height={36} width={36} icon={section?.icon || 'star'} /> <Icon height={36} width={36} icon={section?.icon || 'star'} />
@ -59,7 +68,18 @@ export default component$(() => {
{section?.checklist.map((item, index) => ( {section?.checklist.map((item, index) => (
<tr key={index} class={`rounded-sm hover:bg-opacity-5 hover:bg-${getBadgeClass(item.priority)}`}> <tr key={index} class={`rounded-sm hover:bg-opacity-5 hover:bg-${getBadgeClass(item.priority)}`}>
<td> <td>
<input type="checkbox" class="checkbox" id={generateId(item.point)} /> <input
type="checkbox"
class="checkbox"
id={generateId(item.point)}
checked={isChecked(item.point)}
onClick$={() => {
const id = item.point.toLowerCase().replace(/ /g, '-');
const data = value.value;
data[id] = !data[id];
setValue(data);
}}
/>
</td> </td>
<td> <td>
<label class="text-base font-bold" for={generateId(item.point)}> <label class="text-base font-bold" for={generateId(item.point)}>
@ -78,6 +98,7 @@ export default component$(() => {
</table> </table>
</div> </div>
</article> </article>
</div>
); );
}); });

View File

@ -1,13 +1,16 @@
import { component$ } from "@builder.io/qwik"; import { component$, useContext } from "@builder.io/qwik";
import { data } from '../../mock-data'; import { ChecklistContext } from '~/store/checklist-context';
import type { Section } from "~/types/PSC";
export default component$(() => { export default component$(() => {
const checklists = useContext(ChecklistContext);
return ( return (
<main class="p-8"> <main class="p-8">
<div class="join join-vertical w-full"> <div class="join join-vertical w-full">
{data.map((section, index) => ( {checklists.value.map((section: Section, index: number) => (
<div key={index} class="collapse collapse-plus bg-base-200 join-item"> <div key={index} class={['collapse collapse-plus bg-base-200 my-4', `border-double border-2 border-${section.color}-400`]}>
<input type="radio" name="my-accordion-3" /> <input type="radio" name="my-accordion-3" />
<div class={['collapse-title text-xl font-medium', `bg-${section.color}-400`]}> <div class={['collapse-title text-xl font-medium', `bg-${section.color}-400`]}>
<h3 class="text-slate-700">{section.title}</h3> <h3 class="text-slate-700">{section.title}</h3>

View File

@ -1,16 +1,18 @@
import { component$ } from "@builder.io/qwik"; import { component$, useContext } from '@builder.io/qwik';
import type { DocumentHead } from "@builder.io/qwik-city"; import { type DocumentHead } from "@builder.io/qwik-city";
import Hero from "../components/furniture/hero"; import Hero from "../components/furniture/hero";
import SectionLinkGrid from "../components/psc/section-link-grid"; import SectionLinkGrid from "../components/psc/section-link-grid";
import { data } from '../mock-data'; import { ChecklistContext } from '~/store/checklist-context';
export default component$(() => { export default component$(() => {
const checklists = useContext(ChecklistContext);
return ( return (
<> <>
<Hero /> <Hero />
<SectionLinkGrid sections={data} /> <SectionLinkGrid sections={checklists.value} />
</> </>
); );
}); });

View File

@ -1,9 +1,23 @@
import { component$, Slot } from "@builder.io/qwik"; import { component$, useContextProvider, Slot } from "@builder.io/qwik";
import type { RequestHandler } from "@builder.io/qwik-city"; import { routeLoader$, type RequestHandler } from "@builder.io/qwik-city";
import Navbar from "../components/furniture/nav"; import Navbar from "../components/furniture/nav";
import Footer from "../components/furniture/footer"; import Footer from "../components/furniture/footer";
import { ChecklistContext } from "../store/checklist-context";
import { type Sections } from "~/types/PSC";
import jsyaml from "js-yaml";
export const useChecklists = routeLoader$(async () => {
const url = import.meta.env.DEV ? `http://localhost:5173/personal-security-checklist.yml` : '/personal-security-checklist.yml';
return await fetch(url)
.then((res) => res.text())
.then((res) => jsyaml.load(res) as Sections)
.catch(() => []);
});
export const onGet: RequestHandler = async ({ cacheControl }) => { export const onGet: RequestHandler = async ({ cacheControl }) => {
cacheControl({ cacheControl({
staleWhileRevalidate: 60 * 60 * 24 * 7, staleWhileRevalidate: 60 * 60 * 24 * 7,
@ -12,6 +26,10 @@ export const onGet: RequestHandler = async ({ cacheControl }) => {
}; };
export default component$(() => { export default component$(() => {
const checklists = useChecklists();
useContextProvider(ChecklistContext, checklists)
return ( return (
<> <>
{/* <Header /> */} {/* <Header /> */}

View File

@ -0,0 +1,8 @@
import { type Signal } from '@builder.io/qwik';
import { createContextId } from '@builder.io/qwik';
import type { Sections } from '../types/PSC';
export const ChecklistContext = createContextId<Signal<Sections>>(
'psc.ChecklistContext'
);

View File

@ -0,0 +1,27 @@
import { useStore, useOnWindow, $ } from '@builder.io/qwik';
import { useLocalStorage } from '~/hooks/useLocalStorage';
const STORAGE_KEY = 'PSC_THEME';
const defaultTheme = 'dark';
export const useTheme = () => {
const [theme, saveTheme] = useLocalStorage(STORAGE_KEY, defaultTheme);
const state = useStore({ theme: theme.value });
useOnWindow('load', $(() => {
const storedTheme = theme.value || defaultTheme;
state.theme = storedTheme;
document.getElementsByTagName('body')[0].setAttribute('data-theme', storedTheme);
}));
const setTheme = $((newTheme: string) => {
console.log('Updating Theme', newTheme);
saveTheme(newTheme);
state.theme = newTheme;
document.getElementsByTagName('body')[0].setAttribute('data-theme', newTheme);
});
return { theme: state, setTheme };
};

View File

@ -3,6 +3,8 @@ export interface PersonalSecurityChecklist {
sections: Section[], sections: Section[],
} }
export type Sections = Section[];
export interface Section { export interface Section {
title: string, title: string,
slug: string, slug: string,

View File

@ -40,7 +40,7 @@ module.exports = {
}, },
safelist: [ safelist: [
{ {
pattern: /(bg|outline|text|tw-color)-(yellow|lime|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|red)-(200|300|400|500|600)/, pattern: /(bg|outline|text|tw-color|border)-(yellow|lime|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|red)-(200|300|400|500|600)/,
variants: ['light', 'dark', 'hover', 'focus'], variants: ['light', 'dark', 'hover', 'focus'],
}, },
{ {