supermemory/apps/web/app/onboarding/onboarding-context.tsx
MaheshtheDev b148296f50 feat: layout design with theme improvements (#443)
feat: layout design with theme improvements

new improvements on light mode layout

chore: settings page with graph header and memories list
2025-10-01 21:59:54 +00:00

232 lines
5.7 KiB
TypeScript

"use client"
import {
createContext,
useContext,
useState,
useEffect,
type ReactNode,
useMemo,
} from "react"
import { useQueryState } from "nuqs"
import { useIsMobile } from "@hooks/use-mobile"
// Define the context interface
interface OnboardingContextType {
currentStep: OnboardingStep
setStep: (step: OnboardingStep) => void
nextStep: () => void
previousStep: () => void
totalSteps: number
currentStepIndex: number
// Visible-step aware helpers
visibleSteps: OnboardingStep[]
currentVisibleStepIndex: number
currentVisibleStepNumber: number
getStepNumberFor: (step: OnboardingStep) => number
introTriggers: {
first: boolean
second: boolean
third: boolean
fourth: boolean
}
orbsRevealed: boolean
resetIntroTriggers: () => void
}
// Create the context
const OnboardingContext = createContext<OnboardingContextType | undefined>(
undefined,
)
// Define the base step order
const BASE_STEP_ORDER = [
"intro",
"name",
"bio",
"mcp",
"extension",
"welcome",
] as const
export type OnboardingStep = (typeof BASE_STEP_ORDER)[number]
interface OnboardingProviderProps {
children: ReactNode
initialStep?: OnboardingStep
}
export function OnboardingProvider({
children,
initialStep = "intro",
}: OnboardingProviderProps) {
// Helper function to validate if a step is valid
const isValidStep = (step: string): step is OnboardingStep => {
return BASE_STEP_ORDER.includes(step as OnboardingStep)
}
const [currentStep, setCurrentStep] = useQueryState("step", {
defaultValue: initialStep,
parse: (value: string) => {
// Validate the step from URL - if invalid, use the initial step
return isValidStep(value) ? value : initialStep
},
serialize: (value: OnboardingStep) => value,
})
const [orbsRevealed, setOrbsRevealed] = useState(false)
const [introTriggers, setIntroTriggers] = useState({
first: false,
second: false,
third: false,
fourth: false,
})
const isMobile = useIsMobile()
// Compute visible steps based on device
const visibleSteps = useMemo(() => {
if (isMobile) {
// On mobile, hide MCP and Extension steps
return BASE_STEP_ORDER.filter((s) => s !== "mcp" && s !== "extension")
}
return [...BASE_STEP_ORDER]
}, [isMobile])
// Setup intro trigger timings when on intro step
useEffect(() => {
if (currentStep !== "intro") return
const cleanups = [
setTimeout(() => {
setIntroTriggers((prev) => ({ ...prev, first: true }))
}, 300),
setTimeout(() => {
setIntroTriggers((prev) => ({ ...prev, second: true }))
}, 300),
setTimeout(() => {
setIntroTriggers((prev) => ({ ...prev, third: true }))
}, 300),
setTimeout(() => {
setIntroTriggers((prev) => ({ ...prev, fourth: true }))
}, 400),
]
return () => cleanups.forEach(clearTimeout)
}, [currentStep])
// Set orbs as revealed once the fourth trigger is activated OR if we're on any non-intro step
useEffect(() => {
if (currentStep !== "intro") {
// If we're not on the intro step, orbs should always be visible
// (user has either completed intro or navigated directly to another step)
if (!orbsRevealed) {
setOrbsRevealed(true)
}
} else if (introTriggers.fourth && !orbsRevealed) {
// On intro step, reveal orbs only after the fourth trigger
setOrbsRevealed(true)
}
}, [introTriggers.fourth, orbsRevealed, currentStep])
// Ensure current step is always part of visible steps; if not, advance to the next visible step
useEffect(() => {
if (!visibleSteps.includes(currentStep)) {
if (visibleSteps.length === 0) return
const baseIndex = BASE_STEP_ORDER.indexOf(currentStep)
// Find the next visible step after the current base index
const nextAfterBase = visibleSteps.find(
(step) => BASE_STEP_ORDER.indexOf(step) > baseIndex,
)
const targetStep = nextAfterBase ?? visibleSteps[visibleSteps.length - 1]!
setCurrentStep(targetStep)
}
}, [visibleSteps, currentStep])
function setStep(step: OnboardingStep) {
setCurrentStep(step)
}
function nextStep() {
const currentIndex = visibleSteps.indexOf(currentStep)
const nextIndex = currentIndex + 1
if (nextIndex < visibleSteps.length) {
setStep(visibleSteps[nextIndex]!)
}
}
function previousStep() {
const currentIndex = visibleSteps.indexOf(currentStep)
const previousIndex = currentIndex - 1
if (previousIndex >= 0) {
setStep(visibleSteps[previousIndex]!)
}
}
function resetIntroTriggers() {
setIntroTriggers({
first: false,
second: false,
third: false,
fourth: false,
})
}
const currentStepIndex = BASE_STEP_ORDER.indexOf(currentStep)
// Visible-step aware helpers
const stepsForNumbering = useMemo(
() => visibleSteps.filter((s) => s !== "intro" && s !== "welcome"),
[visibleSteps],
)
function getStepNumberFor(step: OnboardingStep): number {
if (step === "intro" || step === "welcome") {
return 0
}
const idx = stepsForNumbering.indexOf(step)
return idx === -1 ? 0 : idx + 1
}
const currentVisibleStepIndex = useMemo(
() => visibleSteps.indexOf(currentStep),
[visibleSteps, currentStep],
)
const currentVisibleStepNumber = useMemo(
() => getStepNumberFor(currentStep),
[currentStep, stepsForNumbering],
)
const totalSteps = stepsForNumbering.length
const contextValue: OnboardingContextType = {
currentStep,
setStep,
nextStep,
previousStep,
totalSteps,
currentStepIndex,
visibleSteps,
currentVisibleStepIndex,
currentVisibleStepNumber,
getStepNumberFor,
introTriggers,
orbsRevealed,
resetIntroTriggers,
}
return (
<OnboardingContext.Provider value={contextValue}>
{children}
</OnboardingContext.Provider>
)
}
export function useOnboarding() {
const context = useContext(OnboardingContext)
if (context === undefined) {
throw new Error("useOnboarding must be used within an OnboardingProvider")
}
return context
}