234 lines
5.5 KiB
TypeScript
234 lines
5.5 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { Button, Modal } from './ui'
|
|
import { t } from '@/lib/i18n'
|
|
import { isOnboardingCompleted, markOnboardingCompleted } from '@/lib/onboardingPreferences'
|
|
|
|
interface OnboardingStep {
|
|
id: string
|
|
title: string
|
|
content: string
|
|
targetSelector?: string
|
|
}
|
|
|
|
const ONBOARDING_STEPS: OnboardingStep[] = [
|
|
{
|
|
id: 'welcome',
|
|
title: t('onboarding.steps.welcome.title'),
|
|
content: t('onboarding.steps.welcome.content'),
|
|
},
|
|
{
|
|
id: 'search',
|
|
title: t('onboarding.steps.search.title'),
|
|
content: t('onboarding.steps.search.content'),
|
|
targetSelector: '[role="search"]',
|
|
},
|
|
{
|
|
id: 'filters',
|
|
title: t('onboarding.steps.filters.title'),
|
|
content: t('onboarding.steps.filters.content'),
|
|
targetSelector: '#filters-section',
|
|
},
|
|
{
|
|
id: 'articles',
|
|
title: t('onboarding.steps.articles.title'),
|
|
content: t('onboarding.steps.articles.content'),
|
|
targetSelector: '#articles-section',
|
|
},
|
|
{
|
|
id: 'payment',
|
|
title: t('onboarding.steps.payment.title'),
|
|
content: t('onboarding.steps.payment.content'),
|
|
},
|
|
]
|
|
|
|
interface OnboardingTourProps {
|
|
onComplete?: () => void
|
|
}
|
|
|
|
interface OnboardingHandlersParams {
|
|
isLastStep: boolean
|
|
currentStep: number
|
|
setCurrentStep: (value: number) => void
|
|
setIsActive: (value: boolean) => void
|
|
onComplete?: () => void
|
|
}
|
|
|
|
function useOnboardingState(): {
|
|
isActive: boolean
|
|
setIsActive: (value: boolean) => void
|
|
currentStep: number
|
|
setCurrentStep: (value: number) => void
|
|
isLoading: boolean
|
|
} {
|
|
const [isActive, setIsActive] = useState(false)
|
|
const [currentStep, setCurrentStep] = useState(0)
|
|
const [isLoading, setIsLoading] = useState(true)
|
|
|
|
useEffect(() => {
|
|
const checkOnboarding = async (): Promise<void> => {
|
|
try {
|
|
const completed = await isOnboardingCompleted()
|
|
if (!completed) {
|
|
setIsActive(true)
|
|
}
|
|
} catch (error) {
|
|
console.error('Error checking onboarding status:', error)
|
|
} finally {
|
|
setIsLoading(false)
|
|
}
|
|
}
|
|
void checkOnboarding()
|
|
}, [])
|
|
|
|
return { isActive, setIsActive, currentStep, setCurrentStep, isLoading }
|
|
}
|
|
|
|
function useStepScrolling(isActive: boolean, currentStep: number): void {
|
|
useEffect(() => {
|
|
if (isActive && currentStep < ONBOARDING_STEPS.length) {
|
|
const step = ONBOARDING_STEPS[currentStep]
|
|
if (step?.targetSelector) {
|
|
const element = document.querySelector(step.targetSelector)
|
|
if (element) {
|
|
element.scrollIntoView({ behavior: 'smooth', block: 'center' })
|
|
}
|
|
}
|
|
}
|
|
}, [isActive, currentStep])
|
|
}
|
|
|
|
function OnboardingTourActions({
|
|
currentStep,
|
|
isLastStep,
|
|
onNext,
|
|
onSkip,
|
|
}: {
|
|
currentStep: number
|
|
isLastStep: boolean
|
|
onNext: () => void
|
|
onSkip: () => void
|
|
}): React.ReactElement {
|
|
const stepText = t('onboarding.step', { current: currentStep + 1, total: ONBOARDING_STEPS.length })
|
|
|
|
return (
|
|
<div className="flex justify-between items-center">
|
|
<div className="text-xs text-cyber-accent/70">
|
|
{stepText}
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="small"
|
|
onClick={onSkip}
|
|
>
|
|
{t('onboarding.skip')}
|
|
</Button>
|
|
<Button
|
|
type="button"
|
|
variant="primary"
|
|
size="small"
|
|
onClick={onNext}
|
|
>
|
|
{isLastStep ? t('onboarding.finish') : t('onboarding.next')}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function OnboardingTourContent({
|
|
step,
|
|
currentStep,
|
|
isLastStep,
|
|
onNext,
|
|
onSkip,
|
|
}: {
|
|
step: OnboardingStep
|
|
currentStep: number
|
|
isLastStep: boolean
|
|
onNext: () => void
|
|
onSkip: () => void
|
|
}): React.ReactElement {
|
|
return (
|
|
<div className="space-y-4">
|
|
<p className="text-cyber-accent">{step.content}</p>
|
|
<OnboardingTourActions
|
|
currentStep={currentStep}
|
|
isLastStep={isLastStep}
|
|
onNext={onNext}
|
|
onSkip={onSkip}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function useOnboardingHandlers(params: OnboardingHandlersParams): {
|
|
handleNext: () => void
|
|
handleSkip: () => void
|
|
} {
|
|
const handleComplete = async (): Promise<void> => {
|
|
try {
|
|
await markOnboardingCompleted()
|
|
params.setIsActive(false)
|
|
params.onComplete?.()
|
|
} catch (error) {
|
|
console.error('Error marking onboarding as completed:', error)
|
|
}
|
|
}
|
|
|
|
const handleNext = (): void => {
|
|
if (params.isLastStep) {
|
|
void handleComplete()
|
|
} else {
|
|
params.setCurrentStep(params.currentStep + 1)
|
|
}
|
|
}
|
|
|
|
const handleSkip = (): void => {
|
|
void handleComplete()
|
|
}
|
|
|
|
return { handleNext, handleSkip }
|
|
}
|
|
|
|
export function OnboardingTour({ onComplete }: OnboardingTourProps): React.ReactElement | null {
|
|
const { isActive, setIsActive, currentStep, setCurrentStep, isLoading } = useOnboardingState()
|
|
useStepScrolling(isActive, currentStep)
|
|
|
|
if (isLoading || !isActive) {
|
|
return null
|
|
}
|
|
|
|
const step = ONBOARDING_STEPS[currentStep]
|
|
if (!step) {
|
|
return null
|
|
}
|
|
|
|
const isLastStep = currentStep === ONBOARDING_STEPS.length - 1
|
|
const { handleNext, handleSkip } = useOnboardingHandlers({
|
|
isLastStep,
|
|
currentStep,
|
|
setCurrentStep,
|
|
setIsActive,
|
|
onComplete,
|
|
})
|
|
|
|
return (
|
|
<Modal
|
|
isOpen={isActive}
|
|
onClose={handleSkip}
|
|
title={step.title}
|
|
size="medium"
|
|
>
|
|
<OnboardingTourContent
|
|
step={step}
|
|
currentStep={currentStep}
|
|
isLastStep={isLastStep}
|
|
onNext={handleNext}
|
|
onSkip={handleSkip}
|
|
/>
|
|
</Modal>
|
|
)
|
|
}
|