feat: implement tutorial modal with step-by-step guidance for new users
This commit is contained in:
parent
db0a8707d3
commit
5de28f2eaf
264
src/app/components/TutorialModal.tsx
Normal file
264
src/app/components/TutorialModal.tsx
Normal file
@ -0,0 +1,264 @@
|
|||||||
|
'use client'
|
||||||
|
|
||||||
|
import { Fragment } from 'react'
|
||||||
|
import { Dialog, Transition } from '@headlessui/react'
|
||||||
|
import {
|
||||||
|
XMarkIcon,
|
||||||
|
EnvelopeIcon,
|
||||||
|
IdentificationIcon,
|
||||||
|
UserIcon,
|
||||||
|
DocumentTextIcon,
|
||||||
|
ClockIcon,
|
||||||
|
ArrowRightIcon,
|
||||||
|
CheckCircleIcon
|
||||||
|
} from '@heroicons/react/24/outline'
|
||||||
|
|
||||||
|
interface TutorialStep {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
details: string[]
|
||||||
|
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>
|
||||||
|
buttonText: string
|
||||||
|
buttonAction: () => void
|
||||||
|
canProceed: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TutorialModalProps {
|
||||||
|
isOpen: boolean
|
||||||
|
onClose: () => void
|
||||||
|
currentStep: number
|
||||||
|
steps: TutorialStep[]
|
||||||
|
onNext: () => void
|
||||||
|
onPrevious: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TutorialModal({
|
||||||
|
isOpen,
|
||||||
|
onClose,
|
||||||
|
currentStep,
|
||||||
|
steps,
|
||||||
|
onNext,
|
||||||
|
onPrevious
|
||||||
|
}: TutorialModalProps) {
|
||||||
|
const step = steps[currentStep - 1]
|
||||||
|
|
||||||
|
if (!step) return null
|
||||||
|
|
||||||
|
const isLastStep = currentStep === steps.length
|
||||||
|
const isFirstStep = currentStep === 1
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition.Root show={isOpen} as={Fragment}>
|
||||||
|
<Dialog as="div" className="relative z-50" onClose={onClose}>
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0"
|
||||||
|
enterTo="opacity-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100"
|
||||||
|
leaveTo="opacity-0"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-opacity-30 backdrop-blur-sm transition-opacity" />
|
||||||
|
</Transition.Child>
|
||||||
|
|
||||||
|
<div className="fixed inset-0 z-10 overflow-y-auto">
|
||||||
|
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
|
||||||
|
<Transition.Child
|
||||||
|
as={Fragment}
|
||||||
|
enter="ease-out duration-300"
|
||||||
|
enterFrom="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
enterTo="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leave="ease-in duration-200"
|
||||||
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
|
>
|
||||||
|
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-white px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
|
||||||
|
<div className="absolute right-0 top-0 hidden pr-4 pt-4 sm:block">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Close</span>
|
||||||
|
<XMarkIcon className="h-6 w-6" aria-hidden="true" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="sm:flex sm:items-start">
|
||||||
|
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 sm:mx-0 sm:h-10 sm:w-10">
|
||||||
|
<step.icon className="h-6 w-6 text-blue-600" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
|
||||||
|
<Dialog.Title as="h3" className="text-lg font-semibold leading-6 text-gray-900">
|
||||||
|
{step.title}
|
||||||
|
</Dialog.Title>
|
||||||
|
<div className="mt-2">
|
||||||
|
<p className="text-sm text-gray-500 mb-3">
|
||||||
|
{step.description}
|
||||||
|
</p>
|
||||||
|
<ul className="text-sm text-gray-700 space-y-2">
|
||||||
|
{step.details.map((detail, index) => (
|
||||||
|
<li key={index} className="flex items-start gap-2">
|
||||||
|
<div className="h-1.5 w-1.5 rounded-full bg-blue-500 mt-2 flex-shrink-0" />
|
||||||
|
<span>{detail}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Progress indicator */}
|
||||||
|
<div className="mt-6">
|
||||||
|
<div className="flex items-center justify-between text-xs text-gray-500 mb-2">
|
||||||
|
<span>Step {currentStep} of {steps.length}</span>
|
||||||
|
<span>{Math.round((currentStep / steps.length) * 100)}% Complete</span>
|
||||||
|
</div>
|
||||||
|
<div className="w-full bg-gray-200 rounded-full h-2">
|
||||||
|
<div
|
||||||
|
className="bg-blue-600 h-2 rounded-full transition-all duration-300"
|
||||||
|
style={{ width: `${(currentStep / steps.length) * 100}%` }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-5 sm:mt-6 sm:flex sm:flex-row-reverse gap-3">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={step.buttonAction}
|
||||||
|
disabled={!step.canProceed && currentStep !== 5}
|
||||||
|
className={`inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold text-white shadow-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 sm:w-auto ${
|
||||||
|
(step.canProceed || currentStep === 5)
|
||||||
|
? 'bg-blue-600 hover:bg-blue-500 focus-visible:outline-blue-600'
|
||||||
|
: 'bg-gray-400 cursor-not-allowed'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{step.buttonText}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{!isLastStep && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onNext}
|
||||||
|
className="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto"
|
||||||
|
>
|
||||||
|
Skip & Next
|
||||||
|
<ArrowRightIcon className="ml-2 h-4 w-4" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{!isFirstStep && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onPrevious}
|
||||||
|
className="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto"
|
||||||
|
>
|
||||||
|
Previous
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Dialog.Panel>
|
||||||
|
</Transition.Child>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Dialog>
|
||||||
|
</Transition.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tutorial step data
|
||||||
|
export const createTutorialSteps = (
|
||||||
|
emailVerified: boolean,
|
||||||
|
idUploaded: boolean,
|
||||||
|
additionalInfo: boolean,
|
||||||
|
contractSigned: boolean,
|
||||||
|
userType: string,
|
||||||
|
onVerifyEmail: () => void,
|
||||||
|
onUploadId: () => void,
|
||||||
|
onCompleteInfo: () => void,
|
||||||
|
onSignContract: () => void,
|
||||||
|
onCloseTutorial: () => void
|
||||||
|
): TutorialStep[] => [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
title: "Step 1: Verify Your Email",
|
||||||
|
description: "Let's start by verifying your email address. This is essential for account security.",
|
||||||
|
details: [
|
||||||
|
"Check your email inbox for our verification message",
|
||||||
|
"Click the verification link in the email",
|
||||||
|
"If you don't see it, check your spam folder",
|
||||||
|
"You can request a new verification email if needed"
|
||||||
|
],
|
||||||
|
icon: EnvelopeIcon,
|
||||||
|
buttonText: emailVerified ? "Email Verified ✓" : "Verify Email",
|
||||||
|
buttonAction: onVerifyEmail,
|
||||||
|
canProceed: true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
title: "Step 2: Upload ID Documents",
|
||||||
|
description: "Next, we need to verify your identity by uploading official ID documents.",
|
||||||
|
details: [
|
||||||
|
"Prepare a clear photo of your ID (passport, driver's license, or national ID)",
|
||||||
|
"Ensure all text is clearly readable",
|
||||||
|
"Take photos in good lighting conditions",
|
||||||
|
"Both front and back sides may be required"
|
||||||
|
],
|
||||||
|
icon: IdentificationIcon,
|
||||||
|
buttonText: idUploaded ? "ID Uploaded ✓" : "Upload ID",
|
||||||
|
buttonAction: onUploadId,
|
||||||
|
canProceed: emailVerified
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3,
|
||||||
|
title: "Step 3: Complete Your Profile",
|
||||||
|
description: `Fill out your ${userType === 'personal' ? 'personal' : 'company'} profile with additional information.`,
|
||||||
|
details: userType === 'personal' ? [
|
||||||
|
"Enter your full name and date of birth",
|
||||||
|
"Provide your current address",
|
||||||
|
"Add phone number for contact",
|
||||||
|
"All fields are required for verification"
|
||||||
|
] : [
|
||||||
|
"Enter your company details and tax information",
|
||||||
|
"Provide business address and contact info",
|
||||||
|
"Upload any required business documents",
|
||||||
|
"Ensure all information matches your official records"
|
||||||
|
],
|
||||||
|
icon: UserIcon,
|
||||||
|
buttonText: additionalInfo ? "Profile Complete ✓" : "Complete Profile",
|
||||||
|
buttonAction: onCompleteInfo,
|
||||||
|
canProceed: emailVerified && idUploaded
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4,
|
||||||
|
title: "Step 4: Sign the Contract",
|
||||||
|
description: "Almost done! Now you need to review and sign our service agreement.",
|
||||||
|
details: [
|
||||||
|
"Read through the terms and conditions carefully",
|
||||||
|
"Review the service agreement details",
|
||||||
|
"Digitally sign the contract to proceed",
|
||||||
|
"This finalizes your account setup process"
|
||||||
|
],
|
||||||
|
icon: DocumentTextIcon,
|
||||||
|
buttonText: contractSigned ? "Contract Signed ✓" : "Sign Contract",
|
||||||
|
buttonAction: onSignContract,
|
||||||
|
canProceed: emailVerified && idUploaded && additionalInfo
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5,
|
||||||
|
title: "Final Step: Admin Verification",
|
||||||
|
description: "Congratulations! You've completed all required steps. Now wait for admin approval.",
|
||||||
|
details: [
|
||||||
|
"Our team will review your submitted information",
|
||||||
|
"Verification typically takes 1-2 business days",
|
||||||
|
"You'll receive an email notification once approved",
|
||||||
|
"You can continue using limited features while waiting"
|
||||||
|
],
|
||||||
|
icon: ClockIcon,
|
||||||
|
buttonText: "I Understand",
|
||||||
|
buttonAction: onCloseTutorial,
|
||||||
|
canProceed: true // Always enable "I Understand" button
|
||||||
|
}
|
||||||
|
]
|
||||||
@ -3,6 +3,7 @@
|
|||||||
import { useState, useCallback, useEffect } from 'react'
|
import { useState, useCallback, useEffect } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import PageLayout from '../components/PageLayout'
|
import PageLayout from '../components/PageLayout'
|
||||||
|
import TutorialModal, { createTutorialSteps } from '../components/TutorialModal'
|
||||||
import useAuthStore from '../store/authStore'
|
import useAuthStore from '../store/authStore'
|
||||||
import { useUserStatus } from '../hooks/useUserStatus'
|
import { useUserStatus } from '../hooks/useUserStatus'
|
||||||
import {
|
import {
|
||||||
@ -14,7 +15,8 @@ import {
|
|||||||
DocumentCheckIcon,
|
DocumentCheckIcon,
|
||||||
ArrowUpOnSquareIcon,
|
ArrowUpOnSquareIcon,
|
||||||
PencilSquareIcon,
|
PencilSquareIcon,
|
||||||
ClipboardDocumentCheckIcon
|
ClipboardDocumentCheckIcon,
|
||||||
|
AcademicCapIcon
|
||||||
} from '@heroicons/react/24/outline'
|
} from '@heroicons/react/24/outline'
|
||||||
|
|
||||||
interface StatusItem {
|
interface StatusItem {
|
||||||
@ -31,8 +33,16 @@ export default function QuickActionDashboardPage() {
|
|||||||
const { userStatus, loading, error, refreshStatus } = useUserStatus()
|
const { userStatus, loading, error, refreshStatus } = useUserStatus()
|
||||||
const [isClient, setIsClient] = useState(false)
|
const [isClient, setIsClient] = useState(false)
|
||||||
|
|
||||||
|
// Tutorial state
|
||||||
|
const [isTutorialOpen, setIsTutorialOpen] = useState(false)
|
||||||
|
const [currentTutorialStep, setCurrentTutorialStep] = useState(1)
|
||||||
|
const [hasSeenTutorial, setHasSeenTutorial] = useState(false)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsClient(true)
|
setIsClient(true)
|
||||||
|
// Check if user has seen tutorial before
|
||||||
|
const tutorialSeen = localStorage.getItem('tutorial_seen')
|
||||||
|
setHasSeenTutorial(!!tutorialSeen)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Derive status from real backend data
|
// Derive status from real backend data
|
||||||
@ -92,6 +102,50 @@ export default function QuickActionDashboardPage() {
|
|||||||
router.push(`/quickaction-dashboard/register-sign-contract/${userType}`)
|
router.push(`/quickaction-dashboard/register-sign-contract/${userType}`)
|
||||||
}, [router, user])
|
}, [router, user])
|
||||||
|
|
||||||
|
// Tutorial handlers
|
||||||
|
const startTutorial = useCallback(() => {
|
||||||
|
setCurrentTutorialStep(1)
|
||||||
|
setIsTutorialOpen(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const closeTutorial = useCallback(() => {
|
||||||
|
setIsTutorialOpen(false)
|
||||||
|
localStorage.setItem('tutorial_seen', 'true')
|
||||||
|
setHasSeenTutorial(true)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const nextTutorialStep = useCallback(() => {
|
||||||
|
setCurrentTutorialStep(prev => prev + 1)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const previousTutorialStep = useCallback(() => {
|
||||||
|
setCurrentTutorialStep(prev => Math.max(1, prev - 1))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// Auto-start tutorial for new users
|
||||||
|
useEffect(() => {
|
||||||
|
if (isClient && !hasSeenTutorial && !loading && userStatus) {
|
||||||
|
// Auto-start tutorial if user hasn't completed first step
|
||||||
|
if (!emailVerified) {
|
||||||
|
setTimeout(() => setIsTutorialOpen(true), 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [isClient, hasSeenTutorial, loading, userStatus, emailVerified])
|
||||||
|
|
||||||
|
// Create tutorial steps
|
||||||
|
const tutorialSteps = createTutorialSteps(
|
||||||
|
emailVerified,
|
||||||
|
idUploaded,
|
||||||
|
additionalInfo,
|
||||||
|
contractSigned,
|
||||||
|
user?.userType || 'personal',
|
||||||
|
handleVerifyEmail,
|
||||||
|
handleUploadId,
|
||||||
|
handleCompleteInfo,
|
||||||
|
handleSignContract,
|
||||||
|
closeTutorial
|
||||||
|
)
|
||||||
|
|
||||||
const canSignContract = emailVerified && idUploaded && additionalInfo
|
const canSignContract = emailVerified && idUploaded && additionalInfo
|
||||||
|
|
||||||
// NEW: resend cooldown tracking (10 minutes like verify page)
|
// NEW: resend cooldown tracking (10 minutes like verify page)
|
||||||
@ -204,7 +258,8 @@ export default function QuickActionDashboardPage() {
|
|||||||
|
|
||||||
{/* Quick Actions */}
|
{/* Quick Actions */}
|
||||||
<div className="bg-white rounded-xl shadow-sm ring-1 ring-gray-200 p-5 sm:p-8">
|
<div className="bg-white rounded-xl shadow-sm ring-1 ring-gray-200 p-5 sm:p-8">
|
||||||
<div className="flex items-center gap-2 mb-5">
|
<div className="flex items-center justify-between mb-5">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
<span className="inline-flex items-center justify-center h-7 w-7 rounded-full bg-blue-100 text-blue-600 text-sm font-semibold">
|
<span className="inline-flex items-center justify-center h-7 w-7 rounded-full bg-blue-100 text-blue-600 text-sm font-semibold">
|
||||||
i
|
i
|
||||||
</span>
|
</span>
|
||||||
@ -212,6 +267,17 @@ export default function QuickActionDashboardPage() {
|
|||||||
Quick Actions
|
Quick Actions
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={startTutorial}
|
||||||
|
className="relative inline-flex items-center gap-2 rounded-md bg-blue-50 px-3 py-2 text-sm font-medium text-blue-700 hover:bg-blue-100 transition-colors"
|
||||||
|
>
|
||||||
|
<AcademicCapIcon className="h-4 w-4" />
|
||||||
|
Tutorial
|
||||||
|
{!hasSeenTutorial && (
|
||||||
|
<span className="absolute -top-1 -right-1 h-3 w-3 bg-red-500 rounded-full animate-pulse" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div className="grid gap-4 sm:gap-6 grid-cols-1 md:grid-cols-2 xl:grid-cols-4">
|
<div className="grid gap-4 sm:gap-6 grid-cols-1 md:grid-cols-2 xl:grid-cols-4">
|
||||||
{/* Email Verification */}
|
{/* Email Verification */}
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
@ -306,6 +372,16 @@ export default function QuickActionDashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Tutorial Modal */}
|
||||||
|
<TutorialModal
|
||||||
|
isOpen={isTutorialOpen}
|
||||||
|
onClose={closeTutorial}
|
||||||
|
currentStep={currentTutorialStep}
|
||||||
|
steps={tutorialSteps}
|
||||||
|
onNext={nextTutorialStep}
|
||||||
|
onPrevious={previousTutorialStep}
|
||||||
|
/>
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user