1
Fork 0

Consolidate all the tour data into a single TourData type.

This commit is contained in:
Bauke 2023-07-02 11:28:41 +02:00
parent 896895c69a
commit b130ec37a7
Signed by: Bauke
GPG Key ID: C1C0F29952BCF558
11 changed files with 211 additions and 243 deletions

View File

@ -3,8 +3,12 @@ import {
addCompletedTour, addCompletedTour,
createIntroductionUnderstood, createIntroductionUnderstood,
} from "../storage/common.js"; } from "../storage/common.js";
import {introductionSteps} from "../tours/introduction.js"; import {
import {TourId, showTourError, tourIdsAndSteps} from "../tours/exports.js"; allTours,
introductionTour,
showTourError,
type TourData,
} from "../tours/exports.js";
/** The main entry point for the content script. */ /** The main entry point for the content script. */
async function main(): Promise<void> { async function main(): Promise<void> {
@ -26,9 +30,7 @@ async function main(): Promise<void> {
// If a different tour is selected but the introduction hasn't happened yet, // If a different tour is selected but the introduction hasn't happened yet,
// then the main function will be rerun once this tour finishes. // then the main function will be rerun once this tour finishes.
startTour( startTour(
TourId.Introduction, introductionTour,
introductionSteps,
[],
startsWithPrefix && anchorTourId !== "introduction", startsWithPrefix && anchorTourId !== "introduction",
); );
return; return;
@ -43,24 +45,24 @@ async function main(): Promise<void> {
// Then run through all of the tours we have and start the first match for the // Then run through all of the tours we have and start the first match for the
// ID. // ID.
for (const [id, steps, eventHandlers, requirements] of tourIdsAndSteps) { for (const tour of allTours) {
if (anchorTourId === id) { if (anchorTourId === tour.id) {
if (requirements.mustBeLoggedIn && !userIsLoggedIn) { if (tour.requirements.mustBeLoggedIn && !userIsLoggedIn) {
showTourError( 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; return;
} }
if (requirements.path !== window.location.pathname) { if (tour.requirements.path !== window.location.pathname) {
// This tour's path requirement does not match. // This tour's path requirement does not match.
showTourError( 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; return;
} }
startTour(id, steps, eventHandlers, false); startTour(tour, false);
return; return;
} }
} }
@ -76,12 +78,7 @@ async function main(): Promise<void> {
* @param runMainAgainAfterComplete Should the `main` function be run after this * @param runMainAgainAfterComplete Should the `main` function be run after this
* tour is completed? * tour is completed?
*/ */
function startTour( function startTour(data: TourData, runMainAgainAfterComplete: boolean): void {
tourId: TourId,
steps: TourStepOptions[],
eventHandlers: TourStepEventHandler[],
runMainAgainAfterComplete: boolean,
): void {
const defaultButtons: Shepherd.Step.StepOptionsButton[] = [ const defaultButtons: Shepherd.Step.StepOptionsButton[] = [
{ {
classes: "btn", classes: "btn",
@ -124,7 +121,7 @@ function startTour(
} }
// Mark the tour as completed. // Mark the tour as completed.
await addCompletedTour(tourId); await addCompletedTour(data.id);
if (runMainAgainAfterComplete) { if (runMainAgainAfterComplete) {
await main(); await main();
@ -133,19 +130,24 @@ function startTour(
// For every step we have, add it to the tour and subsequently add all the // For every step we have, add it to the tour and subsequently add all the
// event handlers to that step. // 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 // If the final step doesn't have buttons defined, set the "Continue" button
// text to "Finish". // 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 = [...defaultButtons];
stepOptions.buttons[0].text = "Finish"; stepOptions.buttons[0].text = "Finish";
} }
const step = tour.addStep(stepOptions); const step = tour.addStep(stepOptions);
for (const [targetStepId, [eventName, eventHandler]] of eventHandlers) { for (const {stepId, eventHandlers} of data.eventHandlers) {
if (targetStepId === step.id) { if (stepId === step.id) {
step.on(eventName, eventHandler); for (const {event, handler} of eventHandlers) {
step.on(event, handler);
}
} }
} }
} }

View File

@ -1,74 +1,22 @@
import {Component, type JSX} from "preact"; import {Component} from "preact";
import {TourId} from "../../tours/exports.js"; import {type TourData} from "../../tours/exports.js";
type Props = { type Props = {
hasBeenCompleted: boolean; hasBeenCompleted: boolean;
name: string; tour: TourData;
tourId: TourId;
}; };
function tourDescription(tourId: Props["tourId"]): JSX.Element { function tourLink(tour: TourData): string {
if (tourId === TourId.Introduction) { const anchor = `#tildes-shepherd-tour=${tour.id}`;
return (
<p class="tour-description">
A short introduction to Tildes Shepherd and how the tours work. Normally
this is automatically shown when you first installed the extension.
</p>
);
}
if (tourId === TourId.InterfaceHomepage) {
return (
<p class="tour-description">
Let's take a look at the home page and all we can do there.
</p>
);
}
if (tourId === TourId.InterfaceAccountSettings) {
return (
<p class="tour-description">
View your account settings and all that you can customize.
</p>
);
}
return (
<p class="tour-description">
Tour ID "{tourId}" does not have a description, this should probably be
fixed!
</p>
);
}
function tourLink(tourId: Props["tourId"]): string {
const anchor = `#tildes-shepherd-tour=${tourId}`;
const baseUrl = "https://tildes.net"; const baseUrl = "https://tildes.net";
let path = ""; const path = tour.requirements.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}`);
}
}
return `${baseUrl}${path}${anchor}`; return `${baseUrl}${path}${anchor}`;
} }
export class Tour extends Component<Props> { export class Tour extends Component<Props> {
render() { render() {
const {hasBeenCompleted, name, tourId} = this.props; const {hasBeenCompleted, tour} = this.props;
const classes = ["tour", hasBeenCompleted ? "completed" : ""].join(" "); const classes = ["tour", hasBeenCompleted ? "completed" : ""].join(" ");
const completed = hasBeenCompleted ? ( const completed = hasBeenCompleted ? (
<p class="tour-completed" title="You've completed this tour before!"> <p class="tour-completed" title="You've completed this tour before!">
@ -78,11 +26,11 @@ export class Tour extends Component<Props> {
return ( return (
<div class={classes.trim()}> <div class={classes.trim()}>
<h3>{name}</h3> <h3>{tour.title}</h3>
{completed} {completed}
{tourDescription(tourId)} {tour.description}
<p class="tour-link"> <p class="tour-link">
<a href={tourLink(tourId)}>Take this tour</a> <a href={tourLink(tour)}>Take this tour</a>
</p> </p>
</div> </div>
); );

View File

@ -1,6 +1,6 @@
import {Component, type JSX} from "preact"; import {Component, type JSX} from "preact";
import {createToursCompleted} from "../../storage/common.js"; import {createToursCompleted} from "../../storage/common.js";
import {TourId} from "../../tours/exports.js"; import {allTours} from "../../tours/exports.js";
import {Tour} from "./tour.js"; import {Tour} from "./tour.js";
type Props = Record<string, unknown>; type Props = Record<string, unknown>;
@ -26,28 +26,14 @@ export class Tours extends Component<Props, State> {
render(): JSX.Element { render(): JSX.Element {
const {toursCompleted} = this.state; const {toursCompleted} = this.state;
const createTour = (tourId: TourId, name: string): Tour["props"] => { const tours = allTours.map((tour) => (
return { <Tour hasBeenCompleted={toursCompleted.has(tour.id)} tour={tour} />
hasBeenCompleted: toursCompleted.has(tourId), ));
name,
tourId,
};
};
const tourProps: Array<Tour["props"]> = [
createTour(TourId.Introduction, "Introduction"),
createTour(TourId.InterfaceHomepage, "The Homepage"),
createTour(TourId.InterfaceAccountSettings, "Your Account Settings"),
];
return ( return (
<main> <main>
<h2>Tours</h2> <h2>Tours</h2>
<div class="tours"> <div class="tours">{tours}</div>
{tourProps.map((props) => (
<Tour {...props} />
))}
</div>
</main> </main>
); );
} }

View File

@ -1,54 +1,13 @@
import { import {accountSettingsTour, homepageTour} from "./interface/exports.js";
accountSettingsEventHandlers, import {introductionTour} from "./introduction.js";
accountSettingsSteps, import {type TourData} from "./types.js";
homepageEventHandlers,
homepageSteps,
} from "./interface/exports.js";
import {introductionSteps} from "./introduction.js";
export * from "./introduction.js";
export * from "./shared/exports.js"; export * from "./shared/exports.js";
export * from "./types.js";
export enum TourId { export const allTours: TourData[] = [
InterfaceAccountSettings = "interface-account-settings", introductionTour,
InterfaceHomepage = "interface-homepage", accountSettingsTour,
Introduction = "introduction", homepageTour,
}
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: "/",
},
],
]; ];

View File

@ -1,4 +1,4 @@
import type Shepherd from "shepherd.js"; import {type TourData, TourId} from "../types.js";
import {renderInContainer} from "../utilities.js"; import {renderInContainer} from "../utilities.js";
const step01 = renderInContainer( const step01 = renderInContainer(
@ -7,11 +7,26 @@ const step01 = renderInContainer(
</>, </>,
); );
export const steps: Shepherd.Step.StepOptions[] = [ const steps: TourData["steps"] = [
{ {
id: "account-settings-01", id: "account-settings-01",
text: step01, 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,
};

View File

@ -1,8 +1,2 @@
export { export * from "./account-settings.js";
eventHandlers as accountSettingsEventHandlers, export * from "./homepage.js";
steps as accountSettingsSteps,
} from "./account-settings.js";
export {
eventHandlers as homepageEventHandlers,
steps as homepageSteps,
} from "./homepage.js";

View File

@ -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 { import {
addDatasetCounter, addDatasetCounter,
encapsulateElements, encapsulateElements,
removeAllDatasetCounters, removeAllDatasetCounters,
renderInContainer, renderInContainer,
} from "../utilities.js"; } from "../utilities.js";
import {LoggedOutWarning} from "../shared/logged-out-warning.js";
const step01 = renderInContainer( const step01 = renderInContainer(
<> <>
@ -345,7 +345,7 @@ const step10 = renderInContainer(
</>, </>,
); );
export const steps: Shepherd.Step.StepOptions[] = [ const steps: TourData["steps"] = [
{ {
id: "homepage-01", id: "homepage-01",
text: step01, text: step01,
@ -433,79 +433,101 @@ export const steps: Shepherd.Step.StepOptions[] = [
}, },
]; ];
export const eventHandlers: TourStepEventHandler[] = [ const eventHandlers: TourData["eventHandlers"] = [
[ {
"homepage-04", stepId: "homepage-04",
[ eventHandlers: [
"show", {
() => { event: "show",
const topic = ".topic-listing > li:first-child"; handler() {
const counters = [ const topic = ".topic-listing > li:first-child";
".topic-title", const counters = [
".topic-metadata", ".topic-title",
".topic-info-comments", ".topic-metadata",
".topic-info-source", ".topic-info-comments",
"time", ".topic-info-source",
".topic-voting", "time",
".topic-actions", ".topic-voting",
]; ".topic-actions",
];
for (const [count, selector] of counters.entries()) { for (const [count, selector] of counters.entries()) {
addDatasetCounter(`${topic} ${selector}`, count + 1); addDatasetCounter(`${topic} ${selector}`, count + 1);
} }
},
}, },
], ],
], },
[ {
"homepage-05", stepId: "homepage-05",
[ eventHandlers: [
"destroy", {
() => { event: "destroy",
removeAllDatasetCounters(); handler() {
removeAllDatasetCounters();
},
},
{
event: "show",
handler() {
encapsulateElements(
"homepage-06",
"#sidebar .sidebar-controls",
"afterend",
["#sidebar .form-search", "#sidebar h2", "#sidebar p"],
);
},
}, },
], ],
], },
[ {
"homepage-05", stepId: "homepage-06",
[ eventHandlers: [
"show", {
() => { event: "show",
encapsulateElements( handler() {
"homepage-06", encapsulateElements(
"#sidebar .sidebar-controls", "homepage-07",
"afterend", "#sidebar .divider",
["#sidebar .form-search", "#sidebar h2", "#sidebar p"], "beforebegin",
); ["#sidebar .nav", '#sidebar [href="/groups"'],
);
},
}, },
], ],
], },
[ {
"homepage-06", stepId: "homepage-08",
[ eventHandlers: [
"show", {
() => { event: "show",
encapsulateElements("homepage-07", "#sidebar .divider", "beforebegin", [ handler() {
"#sidebar .nav", const filteredTags =
'#sidebar [href="/groups"', document.querySelector<HTMLDetailsElement>("#sidebar details") ??
]); undefined;
}, if (filteredTags === undefined) {
], console.warn("Element is unexpectedly undefined");
], return;
[ }
"homepage-08",
[
"show",
() => {
const filteredTags =
document.querySelector<HTMLDetailsElement>("#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,
};

View File

@ -1,5 +1,5 @@
import type Shepherd from "shepherd.js";
import {createIntroductionUnderstood} from "../storage/common.js"; import {createIntroductionUnderstood} from "../storage/common.js";
import {TourId, type TourData} from "./types.js";
import {openOptionsPageFromBackground, renderInContainer} from "./utilities.js"; import {openOptionsPageFromBackground, renderInContainer} from "./utilities.js";
const step01 = renderInContainer( const step01 = renderInContainer(
@ -82,7 +82,7 @@ const step03 = renderInContainer(
</>, </>,
); );
export const introductionSteps: Shepherd.Step.StepOptions[] = [ const steps: TourData["steps"] = [
{ {
canClickTarget: false, canClickTarget: false,
id: "introduction-01", id: "introduction-01",
@ -122,3 +122,21 @@ export const introductionSteps: Shepherd.Step.StepOptions[] = [
text: step03, 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,
};

View File

@ -1,5 +1,4 @@
import Shepherd from "shepherd.js"; import Shepherd from "shepherd.js";
import {type TourId} from "../exports.js";
import {renderInContainer} from "../utilities.js"; import {renderInContainer} from "../utilities.js";
export function showTourError(text: string) { export function showTourError(text: string) {

30
source/tours/types.ts Normal file
View File

@ -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<Shepherd.Step["on"]>[1];
};
export type TourData = {
description: string;
displayInOptionsPage: boolean;
eventHandlers: Array<{
eventHandlers: TourStepEvent[];
stepId: string;
}>;
id: TourId;
requirements: TourRequirement;
steps: Shepherd.Step.StepOptions[];
title: string;
};

7
source/types.d.ts vendored
View File

@ -1,12 +1,7 @@
import type Shepherd from "shepherd.js"; export {};
declare global { declare global {
const $browser: "chromium" | "firefox"; const $browser: "chromium" | "firefox";
const $dev: boolean; const $dev: boolean;
const $test: boolean; const $test: boolean;
type TourStepEvent = "show" | "destroy";
type TourStepEventFunction = Parameters<Shepherd.Step["on"]>[1];
type TourStepEventHandler = [string, [TourStepEvent, TourStepEventFunction]];
type TourStepOptions = Shepherd.Step.StepOptions;
} }