Merge pull request 'feat: implement tutorial modal with step-by-step guidance for new users' (#4) from sz/tut-modal into dev

Reviewed-on: #4
This commit is contained in:
Seazn 2025-10-22 16:52:31 +00:00
commit 86c7be381b
2 changed files with 348 additions and 8 deletions

View 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
}
]

View File

@ -3,6 +3,7 @@
import { useState, useCallback, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import PageLayout from '../components/PageLayout'
import TutorialModal, { createTutorialSteps } from '../components/TutorialModal'
import useAuthStore from '../store/authStore'
import { useUserStatus } from '../hooks/useUserStatus'
import {
@ -14,7 +15,8 @@ import {
DocumentCheckIcon,
ArrowUpOnSquareIcon,
PencilSquareIcon,
ClipboardDocumentCheckIcon
ClipboardDocumentCheckIcon,
AcademicCapIcon
} from '@heroicons/react/24/outline'
interface StatusItem {
@ -31,8 +33,16 @@ export default function QuickActionDashboardPage() {
const { userStatus, loading, error, refreshStatus } = useUserStatus()
const [isClient, setIsClient] = useState(false)
// Tutorial state
const [isTutorialOpen, setIsTutorialOpen] = useState(false)
const [currentTutorialStep, setCurrentTutorialStep] = useState(1)
const [hasSeenTutorial, setHasSeenTutorial] = useState(false)
useEffect(() => {
setIsClient(true)
// Check if user has seen tutorial before
const tutorialSeen = localStorage.getItem('tutorial_seen')
setHasSeenTutorial(!!tutorialSeen)
}, [])
// Derive status from real backend data
@ -92,6 +102,50 @@ export default function QuickActionDashboardPage() {
router.push(`/quickaction-dashboard/register-sign-contract/${userType}`)
}, [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
// NEW: resend cooldown tracking (10 minutes like verify page)
@ -204,13 +258,25 @@ export default function QuickActionDashboardPage() {
{/* Quick Actions */}
<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">
<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
</span>
<h2 className="text-sm sm:text-base font-semibold text-gray-900">
Quick Actions
</h2>
<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">
i
</span>
<h2 className="text-sm sm:text-base font-semibold text-gray-900">
Quick Actions
</h2>
</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">
{/* Email Verification */}
@ -306,6 +372,16 @@ export default function QuickActionDashboardPage() {
</div>
</main>
</div>
{/* Tutorial Modal */}
<TutorialModal
isOpen={isTutorialOpen}
onClose={closeTutorial}
currentStep={currentTutorialStep}
steps={tutorialSteps}
onNext={nextTutorialStep}
onPrevious={previousTutorialStep}
/>
</PageLayout>
)
}