From b130ec37a713e60c72eb0efac985ec3038a9d2c9 Mon Sep 17 00:00:00 2001 From: Bauke Date: Sun, 2 Jul 2023 11:28:41 +0200 Subject: [PATCH] Consolidate all the tour data into a single TourData type. --- source/content-scripts/setup.ts | 50 ++++--- source/options/components/tour.tsx | 72 ++------- source/options/components/tours.tsx | 24 +-- source/tours/exports.ts | 59 ++------ source/tours/interface/account-settings.tsx | 21 ++- source/tours/interface/exports.ts | 10 +- source/tours/interface/homepage.tsx | 158 +++++++++++--------- source/tours/introduction.tsx | 22 ++- source/tours/shared/tour-error.tsx | 1 - source/tours/types.ts | 30 ++++ source/types.d.ts | 7 +- 11 files changed, 211 insertions(+), 243 deletions(-) create mode 100644 source/tours/types.ts diff --git a/source/content-scripts/setup.ts b/source/content-scripts/setup.ts index d43d69b..a07e6fc 100644 --- a/source/content-scripts/setup.ts +++ b/source/content-scripts/setup.ts @@ -3,8 +3,12 @@ import { addCompletedTour, createIntroductionUnderstood, } from "../storage/common.js"; -import {introductionSteps} from "../tours/introduction.js"; -import {TourId, showTourError, tourIdsAndSteps} from "../tours/exports.js"; +import { + allTours, + introductionTour, + showTourError, + type TourData, +} from "../tours/exports.js"; /** The main entry point for the content script. */ async function main(): Promise { @@ -26,9 +30,7 @@ async function main(): Promise { // If a different tour is selected but the introduction hasn't happened yet, // then the main function will be rerun once this tour finishes. startTour( - TourId.Introduction, - introductionSteps, - [], + introductionTour, startsWithPrefix && anchorTourId !== "introduction", ); return; @@ -43,24 +45,24 @@ async function main(): Promise { // Then run through all of the tours we have and start the first match for the // ID. - for (const [id, steps, eventHandlers, requirements] of tourIdsAndSteps) { - if (anchorTourId === id) { - if (requirements.mustBeLoggedIn && !userIsLoggedIn) { + for (const tour of allTours) { + if (anchorTourId === tour.id) { + if (tour.requirements.mustBeLoggedIn && !userIsLoggedIn) { showTourError( - `The ${id} tour can only be shown with a logged in account.`, + `The ${tour.id} tour can only be shown with a logged in account.`, ); return; } - if (requirements.path !== window.location.pathname) { + if (tour.requirements.path !== window.location.pathname) { // This tour's path requirement does not match. showTourError( - `The ${id} tour can only be start on the ${requirements.path} page.`, + `The ${tour.id} tour can only be start on the ${tour.requirements.path} page.`, ); return; } - startTour(id, steps, eventHandlers, false); + startTour(tour, false); return; } } @@ -76,12 +78,7 @@ async function main(): Promise { * @param runMainAgainAfterComplete Should the `main` function be run after this * tour is completed? */ -function startTour( - tourId: TourId, - steps: TourStepOptions[], - eventHandlers: TourStepEventHandler[], - runMainAgainAfterComplete: boolean, -): void { +function startTour(data: TourData, runMainAgainAfterComplete: boolean): void { const defaultButtons: Shepherd.Step.StepOptionsButton[] = [ { classes: "btn", @@ -124,7 +121,7 @@ function startTour( } // Mark the tour as completed. - await addCompletedTour(tourId); + await addCompletedTour(data.id); if (runMainAgainAfterComplete) { await main(); @@ -133,19 +130,24 @@ function startTour( // For every step we have, add it to the tour and subsequently add all the // event handlers to that step. - for (const [stepNumber, stepOptions] of steps.entries()) { + for (const [stepNumber, stepOptions] of data.steps.entries()) { // If the final step doesn't have buttons defined, set the "Continue" button // text to "Finish". - if (stepOptions.buttons === undefined && stepNumber + 1 === steps.length) { + if ( + stepOptions.buttons === undefined && + stepNumber + 1 === data.steps.length + ) { stepOptions.buttons = [...defaultButtons]; stepOptions.buttons[0].text = "Finish"; } const step = tour.addStep(stepOptions); - for (const [targetStepId, [eventName, eventHandler]] of eventHandlers) { - if (targetStepId === step.id) { - step.on(eventName, eventHandler); + for (const {stepId, eventHandlers} of data.eventHandlers) { + if (stepId === step.id) { + for (const {event, handler} of eventHandlers) { + step.on(event, handler); + } } } } diff --git a/source/options/components/tour.tsx b/source/options/components/tour.tsx index acb42b4..22e5354 100644 --- a/source/options/components/tour.tsx +++ b/source/options/components/tour.tsx @@ -1,74 +1,22 @@ -import {Component, type JSX} from "preact"; -import {TourId} from "../../tours/exports.js"; +import {Component} from "preact"; +import {type TourData} from "../../tours/exports.js"; type Props = { hasBeenCompleted: boolean; - name: string; - tourId: TourId; + tour: TourData; }; -function tourDescription(tourId: Props["tourId"]): JSX.Element { - if (tourId === TourId.Introduction) { - return ( -

- A short introduction to Tildes Shepherd and how the tours work. Normally - this is automatically shown when you first installed the extension. -

- ); - } - - if (tourId === TourId.InterfaceHomepage) { - return ( -

- Let's take a look at the home page and all we can do there. -

- ); - } - - if (tourId === TourId.InterfaceAccountSettings) { - return ( -

- View your account settings and all that you can customize. -

- ); - } - - return ( -

- Tour ID "{tourId}" does not have a description, this should probably be - fixed! -

- ); -} - -function tourLink(tourId: Props["tourId"]): string { - const anchor = `#tildes-shepherd-tour=${tourId}`; +function tourLink(tour: TourData): string { + const anchor = `#tildes-shepherd-tour=${tour.id}`; const baseUrl = "https://tildes.net"; - let path = ""; - - switch (tourId) { - case TourId.InterfaceHomepage: - case TourId.Introduction: { - path = "/"; - break; - } - - case TourId.InterfaceAccountSettings: { - path = "/settings"; - break; - } - - default: { - throw new Error(`Unswitched tour ID: ${tourId as string}`); - } - } + const path = tour.requirements.path; return `${baseUrl}${path}${anchor}`; } export class Tour extends Component { render() { - const {hasBeenCompleted, name, tourId} = this.props; + const {hasBeenCompleted, tour} = this.props; const classes = ["tour", hasBeenCompleted ? "completed" : ""].join(" "); const completed = hasBeenCompleted ? (

@@ -78,11 +26,11 @@ export class Tour extends Component { return (

-

{name}

+

{tour.title}

{completed} - {tourDescription(tourId)} + {tour.description}
); diff --git a/source/options/components/tours.tsx b/source/options/components/tours.tsx index 7bcd29b..cc184e9 100644 --- a/source/options/components/tours.tsx +++ b/source/options/components/tours.tsx @@ -1,6 +1,6 @@ import {Component, type JSX} from "preact"; import {createToursCompleted} from "../../storage/common.js"; -import {TourId} from "../../tours/exports.js"; +import {allTours} from "../../tours/exports.js"; import {Tour} from "./tour.js"; type Props = Record; @@ -26,28 +26,14 @@ export class Tours extends Component { render(): JSX.Element { const {toursCompleted} = this.state; - const createTour = (tourId: TourId, name: string): Tour["props"] => { - return { - hasBeenCompleted: toursCompleted.has(tourId), - name, - tourId, - }; - }; - - const tourProps: Array = [ - createTour(TourId.Introduction, "Introduction"), - createTour(TourId.InterfaceHomepage, "The Homepage"), - createTour(TourId.InterfaceAccountSettings, "Your Account Settings"), - ]; + const tours = allTours.map((tour) => ( + + )); return (

Tours

-
- {tourProps.map((props) => ( - - ))} -
+
{tours}
); } diff --git a/source/tours/exports.ts b/source/tours/exports.ts index ba5b823..9c25b24 100644 --- a/source/tours/exports.ts +++ b/source/tours/exports.ts @@ -1,54 +1,13 @@ -import { - accountSettingsEventHandlers, - accountSettingsSteps, - homepageEventHandlers, - homepageSteps, -} from "./interface/exports.js"; -import {introductionSteps} from "./introduction.js"; +import {accountSettingsTour, homepageTour} from "./interface/exports.js"; +import {introductionTour} from "./introduction.js"; +import {type TourData} from "./types.js"; +export * from "./introduction.js"; export * from "./shared/exports.js"; +export * from "./types.js"; -export enum TourId { - InterfaceAccountSettings = "interface-account-settings", - InterfaceHomepage = "interface-homepage", - Introduction = "introduction", -} - -export type TourRequirement = { - mustBeLoggedIn: boolean; - path: string; -}; - -export type TourIdsAndSteps = Array< - [TourId, TourStepOptions[], TourStepEventHandler[], TourRequirement] ->; - -export const tourIdsAndSteps: TourIdsAndSteps = [ - [ - TourId.Introduction, - introductionSteps, - [], - { - mustBeLoggedIn: false, - path: "/", - }, - ], - [ - TourId.InterfaceAccountSettings, - accountSettingsSteps, - accountSettingsEventHandlers, - { - mustBeLoggedIn: true, - path: "/settings", - }, - ], - [ - TourId.InterfaceHomepage, - homepageSteps, - homepageEventHandlers, - { - mustBeLoggedIn: false, - path: "/", - }, - ], +export const allTours: TourData[] = [ + introductionTour, + accountSettingsTour, + homepageTour, ]; diff --git a/source/tours/interface/account-settings.tsx b/source/tours/interface/account-settings.tsx index 1170168..4408e6a 100644 --- a/source/tours/interface/account-settings.tsx +++ b/source/tours/interface/account-settings.tsx @@ -1,4 +1,4 @@ -import type Shepherd from "shepherd.js"; +import {type TourData, TourId} from "../types.js"; import {renderInContainer} from "../utilities.js"; const step01 = renderInContainer( @@ -7,11 +7,26 @@ const step01 = renderInContainer( , ); -export const steps: Shepherd.Step.StepOptions[] = [ +const steps: TourData["steps"] = [ { id: "account-settings-01", text: step01, }, ]; -export const eventHandlers: TourStepEventHandler[] = []; +const eventHandlers: TourData["eventHandlers"] = []; + +const requirements: TourData["requirements"] = { + mustBeLoggedIn: true, + path: "/settings", +}; + +export const accountSettingsTour: TourData = { + id: TourId.InterfaceAccountSettings, + title: "Your Account Settings", + description: "View your account settings and all that you can customize.", + displayInOptionsPage: true, + eventHandlers, + requirements, + steps, +}; diff --git a/source/tours/interface/exports.ts b/source/tours/interface/exports.ts index a2b2518..f6a49b1 100644 --- a/source/tours/interface/exports.ts +++ b/source/tours/interface/exports.ts @@ -1,8 +1,2 @@ -export { - eventHandlers as accountSettingsEventHandlers, - steps as accountSettingsSteps, -} from "./account-settings.js"; -export { - eventHandlers as homepageEventHandlers, - steps as homepageSteps, -} from "./homepage.js"; +export * from "./account-settings.js"; +export * from "./homepage.js"; diff --git a/source/tours/interface/homepage.tsx b/source/tours/interface/homepage.tsx index 1b490cd..12a30b9 100644 --- a/source/tours/interface/homepage.tsx +++ b/source/tours/interface/homepage.tsx @@ -1,11 +1,11 @@ -import type Shepherd from "shepherd.js"; +import {LoggedOutWarning} from "../shared/logged-out-warning.js"; +import {type TourData, TourId} from "../types.js"; import { addDatasetCounter, encapsulateElements, removeAllDatasetCounters, renderInContainer, } from "../utilities.js"; -import {LoggedOutWarning} from "../shared/logged-out-warning.js"; const step01 = renderInContainer( <> @@ -345,7 +345,7 @@ const step10 = renderInContainer( , ); -export const steps: Shepherd.Step.StepOptions[] = [ +const steps: TourData["steps"] = [ { id: "homepage-01", text: step01, @@ -433,79 +433,101 @@ export const steps: Shepherd.Step.StepOptions[] = [ }, ]; -export const eventHandlers: TourStepEventHandler[] = [ - [ - "homepage-04", - [ - "show", - () => { - const topic = ".topic-listing > li:first-child"; - const counters = [ - ".topic-title", - ".topic-metadata", - ".topic-info-comments", - ".topic-info-source", - "time", - ".topic-voting", - ".topic-actions", - ]; +const eventHandlers: TourData["eventHandlers"] = [ + { + stepId: "homepage-04", + eventHandlers: [ + { + event: "show", + handler() { + const topic = ".topic-listing > li:first-child"; + const counters = [ + ".topic-title", + ".topic-metadata", + ".topic-info-comments", + ".topic-info-source", + "time", + ".topic-voting", + ".topic-actions", + ]; - for (const [count, selector] of counters.entries()) { - addDatasetCounter(`${topic} ${selector}`, count + 1); - } + for (const [count, selector] of counters.entries()) { + addDatasetCounter(`${topic} ${selector}`, count + 1); + } + }, }, ], - ], - [ - "homepage-05", - [ - "destroy", - () => { - removeAllDatasetCounters(); + }, + { + stepId: "homepage-05", + eventHandlers: [ + { + event: "destroy", + handler() { + removeAllDatasetCounters(); + }, + }, + { + event: "show", + handler() { + encapsulateElements( + "homepage-06", + "#sidebar .sidebar-controls", + "afterend", + ["#sidebar .form-search", "#sidebar h2", "#sidebar p"], + ); + }, }, ], - ], - [ - "homepage-05", - [ - "show", - () => { - encapsulateElements( - "homepage-06", - "#sidebar .sidebar-controls", - "afterend", - ["#sidebar .form-search", "#sidebar h2", "#sidebar p"], - ); + }, + { + stepId: "homepage-06", + eventHandlers: [ + { + event: "show", + handler() { + encapsulateElements( + "homepage-07", + "#sidebar .divider", + "beforebegin", + ["#sidebar .nav", '#sidebar [href="/groups"'], + ); + }, }, ], - ], - [ - "homepage-06", - [ - "show", - () => { - encapsulateElements("homepage-07", "#sidebar .divider", "beforebegin", [ - "#sidebar .nav", - '#sidebar [href="/groups"', - ]); - }, - ], - ], - [ - "homepage-08", - [ - "show", - () => { - const filteredTags = - document.querySelector("#sidebar details") ?? - undefined; - if (filteredTags === undefined) { - console.warn("Element is unexpectedly undefined"); - return; - } + }, + { + stepId: "homepage-08", + eventHandlers: [ + { + event: "show", + handler() { + const filteredTags = + document.querySelector("#sidebar details") ?? + undefined; + if (filteredTags === undefined) { + console.warn("Element is unexpectedly undefined"); + return; + } - filteredTags.open = true; + filteredTags.open = true; + }, }, ], - ], + }, ]; + +const requirements: TourData["requirements"] = { + mustBeLoggedIn: false, + path: "/", +}; + +export const homepageTour: TourData = { + id: TourId.InterfaceHomepage, + title: "The Tildes Homepage", + description: "Let's take a look at the home page and all we can do there.", + displayInOptionsPage: true, + eventHandlers, + requirements, + steps, +}; diff --git a/source/tours/introduction.tsx b/source/tours/introduction.tsx index 3986632..2c23d11 100644 --- a/source/tours/introduction.tsx +++ b/source/tours/introduction.tsx @@ -1,5 +1,5 @@ -import type Shepherd from "shepherd.js"; import {createIntroductionUnderstood} from "../storage/common.js"; +import {TourId, type TourData} from "./types.js"; import {openOptionsPageFromBackground, renderInContainer} from "./utilities.js"; const step01 = renderInContainer( @@ -82,7 +82,7 @@ const step03 = renderInContainer( , ); -export const introductionSteps: Shepherd.Step.StepOptions[] = [ +const steps: TourData["steps"] = [ { canClickTarget: false, id: "introduction-01", @@ -122,3 +122,21 @@ export const introductionSteps: Shepherd.Step.StepOptions[] = [ text: step03, }, ]; + +const eventHandlers: TourData["eventHandlers"] = []; + +const requirements: TourData["requirements"] = { + mustBeLoggedIn: false, + path: "/", +}; + +export const introductionTour: TourData = { + id: TourId.Introduction, + title: "Tildes Shepherd Introduction", + description: + "A short introduction to Tildes Shepherd and how the tours work.", + displayInOptionsPage: true, + eventHandlers, + requirements, + steps, +}; diff --git a/source/tours/shared/tour-error.tsx b/source/tours/shared/tour-error.tsx index b374bce..0f9bcb6 100644 --- a/source/tours/shared/tour-error.tsx +++ b/source/tours/shared/tour-error.tsx @@ -1,5 +1,4 @@ import Shepherd from "shepherd.js"; -import {type TourId} from "../exports.js"; import {renderInContainer} from "../utilities.js"; export function showTourError(text: string) { diff --git a/source/tours/types.ts b/source/tours/types.ts new file mode 100644 index 0000000..2702204 --- /dev/null +++ b/source/tours/types.ts @@ -0,0 +1,30 @@ +import type Shepherd from "shepherd.js"; + +export enum TourId { + InterfaceAccountSettings = "interface-account-settings", + InterfaceHomepage = "interface-homepage", + Introduction = "introduction", +} + +export type TourRequirement = { + mustBeLoggedIn: boolean; + path: string; +}; + +export type TourStepEvent = { + event: "show" | "destroy"; + handler: Parameters[1]; +}; + +export type TourData = { + description: string; + displayInOptionsPage: boolean; + eventHandlers: Array<{ + eventHandlers: TourStepEvent[]; + stepId: string; + }>; + id: TourId; + requirements: TourRequirement; + steps: Shepherd.Step.StepOptions[]; + title: string; +}; diff --git a/source/types.d.ts b/source/types.d.ts index a6dbe03..af25fc5 100644 --- a/source/types.d.ts +++ b/source/types.d.ts @@ -1,12 +1,7 @@ -import type Shepherd from "shepherd.js"; +export {}; declare global { const $browser: "chromium" | "firefox"; const $dev: boolean; const $test: boolean; - - type TourStepEvent = "show" | "destroy"; - type TourStepEventFunction = Parameters[1]; - type TourStepEventHandler = [string, [TourStepEvent, TourStepEventFunction]]; - type TourStepOptions = Shepherd.Step.StepOptions; }