/home/smartbloks/.trash/extendify/src/Assist/state/Tours.js
import { create } from 'zustand'
import { devtools, persist, createJSONStorage } from 'zustand/middleware'
import { getTourData, saveTourData } from '../api/Data'
const key = 'extendify-assist-tour-progress'
const startingState = {
currentTour: null,
currentStep: undefined,
preparingStep: undefined,
progress: [],
// Optimistically update from local storage - see storage.setItem below
...(JSON.parse(localStorage.getItem(key) || '{}')?.state ?? {}),
}
const state = (set, get) => ({
...startingState,
startTour: async (tourData) => {
const { trackTourProgress, updateProgress, getStepData, onTourPage } =
get()
if (onTourPage(tourData?.settings?.startFrom)) {
await tourData?.onStart?.(tourData)
tourData.steps =
tourData.steps?.filter(
// Filter out steps that define a condition
(s) => s?.showOnlyIf?.() || s?.showOnlyIf?.() === undefined,
) || []
await getStepData(0, tourData)?.events?.beforeAttach?.(tourData)
}
set({ currentTour: tourData, currentStep: 0, preparingStep: undefined })
// Increment the opened count
const tour = trackTourProgress(tourData.id)
updateProgress(tour.id, {
openedCount: tour.openedCount + 1,
lastAction: 'started',
})
},
onTourPage(startFrom = null) {
const url = window.location.href
if (startFrom?.includes(url)) return true
const { currentTour } = get()
return currentTour?.settings?.startFrom?.includes(url)
},
completeCurrentTour: async () => {
const { currentTour, finishedTour, findTourProgress, updateProgress } =
get()
const tour = findTourProgress(currentTour?.id)
if (!tour?.id) return
// if already completed, don't update the completedAt
if (!finishedTour(tour.id)) {
updateProgress(tour.id, {
completedAt: new Date().toISOString(),
lastAction: 'completed',
})
}
// Track how many times it was completed
updateProgress(tour.id, {
completedCount: tour.completedCount + 1,
lastAction: 'completed',
})
await currentTour?.onDetach?.()
await currentTour?.onFinish?.()
set({ currentTour: null, currentStep: undefined })
},
closeCurrentTour: async (lastAction) => {
const { currentTour, findTourProgress, updateProgress } = get()
const tour = findTourProgress(currentTour?.id)
if (!tour?.id) return
const additional = {}
if (['redirected'].includes(lastAction)) {
return updateProgress(tour?.id, { lastAction })
}
if (['closed-by-caught-error'].includes(lastAction)) {
return updateProgress(tour?.id, { lastAction, errored: true })
}
if (lastAction === 'closed-manually') {
additional.closedManuallyCount = tour.closedManuallyCount + 1
}
await currentTour?.onDetach?.()
await currentTour?.onFinish?.()
updateProgress(tour?.id, { lastAction, ...additional })
set({
currentTour: null,
currentStep: undefined,
preparingStep: undefined,
})
},
findTourProgress(tourId) {
return get().progress.find((tour) => tour.id === tourId)
},
finishedTour(tourId) {
return get().findTourProgress(tourId)?.completedAt
},
wasOpened(tourId) {
return get().findTourProgress(tourId)?.openedCount > 0
},
isSeen(tourId) {
return get().findTourProgress(tourId)?.firstSeenAt
},
trackTourProgress(tourId) {
const { findTourProgress } = get()
// If we are already tracking it, return that
if (findTourProgress(tourId)) {
return findTourProgress(tourId)
}
set((state) => ({
progress: [
...state.progress,
{
id: tourId,
firstSeenAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
completedAt: null,
lastAction: 'init',
currentStep: 0,
openedCount: 0,
closedManuallyCount: 0,
completedCount: 0,
errored: false,
},
],
}))
return findTourProgress(tourId)
},
updateProgress(tourId, update) {
const lastAction = update?.lastAction ?? 'unknown'
set((state) => {
const progress = state.progress.map((tour) => {
if (tour.id === tourId) {
return {
...tour,
...update,
lastAction,
updatedAt: new Date().toISOString(),
}
}
return tour
})
return { progress }
})
},
getStepData(step, tour = get().currentTour) {
return tour?.steps?.[step] ?? {}
},
hasNextStep() {
if (!get().currentTour) return false
return get().currentStep < get().currentTour.steps.length - 1
},
nextStep: async () => {
const { currentTour, goToStep, updateProgress, currentStep } = get()
const step = currentStep + 1
await goToStep(step)
updateProgress(currentTour.id, {
currentStep: step,
lastAction: 'next',
})
},
hasPreviousStep() {
if (!get().currentTour) return false
return get().currentStep > 0
},
prevStep: async () => {
const { currentTour, goToStep, updateProgress, currentStep } = get()
const step = currentStep - 1
await goToStep(step)
updateProgress(currentTour.id, {
currentStep: step,
lastAction: 'prev',
})
},
goToStep: async (step) => {
const { currentTour, updateProgress, closeCurrentTour, getStepData } =
get()
const tour = currentTour
// Check that the step is valid
if (step < 0 || step > tour.steps.length - 1) {
closeCurrentTour('closed-by-caught-error')
return
}
updateProgress(tour.id, {
currentStep: step,
lastAction: `go-to-step-${step}`,
})
const events = getStepData(step)?.events
if (events?.beforeAttach) {
set(() => ({ preparingStep: step }))
// Make sure the preparing animation runs at least 300ms
await Promise.allSettled([
events.beforeAttach?.(tour),
new Promise((resolve) => setTimeout(resolve, 300)),
])
set(() => ({ preparingStep: undefined }))
}
set(() => ({ currentStep: step }))
},
})
const storage = {
getItem: async () => JSON.stringify(await getTourData()),
setItem: async (k, value) => {
// Stash here so we can use it on reload optimistically
localStorage.setItem(k, value)
await saveTourData(value)
},
removeItem: () => undefined,
}
export const useTourStore = create(
persist(devtools(state, { name: 'Extendify Assist Tour Progress' }), {
name: key,
storage: createJSONStorage(() => storage),
partialize: (state) => {
// return without currentTour or currentStep
// eslint-disable-next-line no-unused-vars
const { currentTour, currentStep, preparingStep, ...newState } =
state
return newState
},
}),
state,
)