Skip to content

Instantly share code, notes, and snippets.

@glorat
Created February 25, 2025 15:06
Show Gist options
  • Save glorat/1ced5ecc887d8ee006b90055210a05e0 to your computer and use it in GitHub Desktop.
Save glorat/1ced5ecc887d8ee006b90055210a05e0 to your computer and use it in GitHub Desktop.
Shepherd.js
import * as ShepherdNamespace from 'shepherd.js'
import 'shepherd.js/dist/css/shepherd.css'
// Create a singleton instance of Shepherd.Tour
class TourService {
private static instance: TourService
private tour: ShepherdNamespace.Tour
private matterIdStep?: ShepherdNamespace.Step
private constructor() {
const Shepherd = (ShepherdNamespace as any).default
this.tour = new Shepherd.Tour({
useModalOverlay: true,
defaultStepOptions: {
classes: 'shepherd-theme-default',
scrollTo: true,
cancelIcon: {
enabled: true,
},
},
})
}
public static getInstance(): TourService {
if (!TourService.instance) {
TourService.instance = new TourService()
}
return TourService.instance
}
private resetTour(): void {
this.tour.complete()
this.tour.steps.length = 0
this.matterIdStep = undefined
}
public advanceMatterIdStep(value: string): void {
if (this.matterIdStep?.isOpen() && value.toLowerCase() === 'sample') {
this.tour.next()
}
}
public async startOnboardingTour(): Promise<void> {
this.resetTour()
this.tour.addStep({
id: 'welcome',
text: "Welcome to BriefTech! Let's create your first workspace to get started.",
buttons: [
{
text: 'Next',
action: () => this.tour.next(),
},
],
})
this.matterIdStep = this.tour.addStep({
id: 'enter-matter-id',
text: 'Enter "sample" as your Matter Reference to create a sample workspace.',
attachTo: {
element: '[data-testid="matter-reference-input"]',
on: 'bottom',
},
buttons: [],
}) as ShepherdNamespace.Step
this.tour.addStep({
id: 'create-workspace',
text: 'Great! Now click "Create Matter" to set up your workspace.',
attachTo: {
element: 'button[type="submit"]',
on: 'bottom',
},
advanceOn: {
selector: 'button[type="submit"]',
event: 'click',
},
buttons: [],
})
this.tour.addStep({
id: 'workspace-initializing',
beforeShowPromise: () =>
waitForElement('[data-testid="multi-file-empty-manager"]'),
text: 'Your workspace is setup',
buttons: [
{
text: 'Next',
action: () => this.tour.next(),
},
],
})
this.tour.addStep({
id: 'load-samples',
text: "To get started quickly, let's load some sample files into your workspace.",
attachTo: {
element: '[data-testid="load-samples-btn"]',
on: 'bottom',
},
advanceOn: {
selector: '[data-testid="load-samples-btn"]',
event: 'click',
},
buttons: [],
})
this.tour.addStep({
id: 'samples-loading',
beforeShowPromise: () => waitForElement('[data-testid="file-table"]'),
text: 'Your samples are loading here',
buttons: [
{
text: 'Finish',
action: () => this.tour.complete(),
},
],
})
console.log('Starting shepherding of user')
await this.tour.start()
}
public advanceToInitializingStep(): void {
const currentStep = this.tour.steps.find((step) => step.isOpen())
if (currentStep?.id === 'create-workspace') {
this.tour.next()
}
}
}
function waitForElement(selector: string, timeout = 5000): Promise<void> {
return new Promise<void>((resolve, reject) => {
const startTime = Date.now()
const checkExist = setInterval(() => {
if (document.querySelector(selector)) {
clearInterval(checkExist)
resolve()
} else if (Date.now() - startTime > timeout) {
clearInterval(checkExist)
reject(`Element ${selector} not found within timeout`)
}
}, 200)
})
}
// Export a singleton instance
export const tourService = TourService.getInstance()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment