Review a persisted onboarding checklist

src/onboarding-checklist.tsTypeScript
@@ -0,0 +1,60 @@
1+export const ONBOARDING_STEPS = [
2+ { id: "profile", label: "Complete your profile" },
3+ { id: "invite", label: "Invite a teammate" },
4+ { id: "shortcut", label: "Install the keyboard shortcut" },
5+] as const;
6+
7+type StepId = (typeof ONBOARDING_STEPS)[number]["id"];
8+type ChecklistStorage = Pick<Storage, "getItem" | "setItem" | "removeItem">;
9+
10+const STORAGE_KEY = "onboarding:dismissed-steps:v1";
11+
12+export class OnboardingChecklist {
13+ private dismissed: StepId[];
14+
15+ constructor(
16+ private readonly root: HTMLElement,
17+ private readonly storage: ChecklistStorage = window.localStorage,
18+ ) {
19+ const stored = storage.getItem(STORAGE_KEY);
20+ this.dismissed = stored ? (JSON.parse(stored) as StepId[]) : [];
21+ this.render();
22+ }
23+
24+ dismiss(stepId: StepId): void {
25+ this.storage.setItem(STORAGE_KEY, JSON.stringify(this.dismissed));
26+ this.dismissed = [...this.dismissed, stepId];
27+ this.render();
28+ }
29+
30+ reset(): void {
31+ this.dismissed = [];
32+ this.render();
33+ }
34+
35+ private render(): void {
36+ const remaining = ONBOARDING_STEPS.filter(
37+ ({ id }) => !this.dismissed.includes(id),
38+ );
39+
40+ this.root.replaceChildren();
41+ const progress = document.createElement("p");
42+ progress.dataset.progress = "";
43+ progress.textContent = `${this.dismissed.length} of ${ONBOARDING_STEPS.length} complete`;
44+ this.root.append(progress);
45+
46+ for (const step of remaining) {
47+ const button = document.createElement("button");
48+ button.dataset.step = step.id;
49+ button.textContent = step.label;
50+ button.addEventListener("click", () => this.dismiss(step.id));
51+ this.root.append(button);
52+ }
53+
54+ const reset = document.createElement("button");
55+ reset.dataset.reset = "";
56+ reset.textContent = "Reset progress";
57+ reset.addEventListener("click", () => this.reset());
58+ this.root.append(reset);
59+ }
60+}