feat: implement tutorial flow with URL parameter handling for navigation

This commit is contained in:
seaznCode 2025-10-22 21:43:29 +02:00
parent 9f5da2c43d
commit 4024085dc4
8 changed files with 284 additions and 141 deletions

BIN
public/images/misc/cow.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 KiB

View File

@ -10,7 +10,10 @@ import {
DocumentTextIcon,
ClockIcon,
ArrowRightIcon,
CheckCircleIcon
CheckCircleIcon,
HandRaisedIcon,
HeartIcon,
CheckIcon
} from '@heroicons/react/24/outline'
interface TutorialStep {
@ -47,6 +50,23 @@ export default function TutorialModal({
const isLastStep = currentStep === steps.length
const isFirstStep = currentStep === 1
// Helper function to check if step is completed
const isStepCompleted = (stepId: number) => {
if (stepId === 2) return step.buttonText.includes("✅") // Email verified
if (stepId === 3) return step.buttonText.includes("✅") // ID uploaded
if (stepId === 4) return step.buttonText.includes("✅") // Profile completed
if (stepId === 5) return step.buttonText.includes("✅") // Agreement signed
return false
}
// Get clean button text without emoji
const getCleanButtonText = (text: string) => {
return text.replace(/✅/g, '').trim().replace(/!$/, '')
}
const stepCompleted = isStepCompleted(step.id)
const buttonText = stepCompleted ? getCleanButtonText(step.buttonText) : step.buttonText
return (
<Transition.Root show={isOpen} as={Fragment}>
@ -60,11 +80,11 @@ export default function TutorialModal({
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-opacity-30 backdrop-blur-sm transition-opacity" />
<div className="fixed inset-0 bg-blue-900/60 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">
<div className="fixed inset-0 z-10">
<div className="flex min-h-full items-center justify-center p-4">
<Transition.Child
as={Fragment}
enter="ease-out duration-300"
@ -74,91 +94,130 @@ export default function TutorialModal({
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">
<Dialog.Panel className="relative w-full max-w-5xl h-[60vh]">
<div className="relative isolate overflow-hidden bg-slate-50 h-full after:pointer-events-none after:absolute after:inset-0 after:inset-ring after:inset-ring-gray-200/50 sm:rounded-3xl after:sm:rounded-3xl lg:flex lg:gap-x-12 lg:px-8 w-full">
{/* Background Gradient */}
<svg
viewBox="0 0 1024 1024"
aria-hidden="true"
className="absolute -top-48 -left-48 -z-10 size-96 mask-[radial-gradient(closest-side,white,transparent)]"
>
<circle r={512} cx={512} cy={512} fill="url(#tutorial-gradient)" fillOpacity="0.7" />
<defs>
<radialGradient id="tutorial-gradient">
<stop stopColor="#3B82F6" />
<stop offset={1} stopColor="#8B5CF6" />
</radialGradient>
</defs>
</svg>
{/* Close Button */}
<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"
className="absolute right-4 top-4 z-10 rounded-md bg-gray-200/70 p-2 text-gray-600 hover:text-gray-800 hover:bg-gray-300/70 focus:outline-none focus:ring-2 focus:ring-blue-500 backdrop-blur-sm"
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>
{/* Content Section - Left Half */}
<div className="lg:flex-1 lg:max-w-md text-center lg:text-left py-8 px-6 flex flex-col justify-center">
{/* Icon */}
<div className="mx-auto lg:mx-0 flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-blue-100 mb-4 ring-2 ring-blue-200">
<step.icon className="h-6 w-6 text-blue-600" aria-hidden="true" />
</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>
{/* Title */}
<h2 className="text-xl font-semibold tracking-tight whitespace-nowrap text-gray-800 sm:text-2xl">
{step.title}
</h2>
{/* Description */}
<p className="mt-3 text-sm text-gray-600 leading-relaxed h-12 overflow-hidden">
{step.description}
</p>
{/* Details */}
<ul className="mt-4 text-left text-gray-600 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-1.5 flex-shrink-0" />
<span className="text-xs leading-5">{detail}</span>
</li>
))}
</ul>
{/* 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-1.5">
<div
className="bg-gradient-to-r from-blue-500 to-purple-500 h-1.5 rounded-full transition-all duration-500"
style={{ width: `${(currentStep / steps.length) * 100}%` }}
/>
</div>
</div>
{/* Action Buttons */}
<div className="mt-6 flex items-center justify-center gap-x-3 lg:justify-start">
<button
type="button"
onClick={step.buttonAction}
disabled={(!step.canProceed && currentStep !== steps.length) || buttonText.includes("Waiting for admin review") || stepCompleted}
className={`rounded-md px-3 py-2 text-sm font-semibold inset-ring inset-ring-gray-200/50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 transition-all flex items-center gap-2 ${
stepCompleted
? 'bg-green-600 text-white'
: (step.canProceed || currentStep === steps.length) && !buttonText.includes("Waiting for admin review")
? 'bg-blue-600 text-white hover:bg-blue-500 shadow-lg hover:shadow-xl'
: 'bg-gray-300 text-gray-500'
}`}
>
{stepCompleted && <CheckIcon className="h-4 w-4" />}
{buttonText}
</button>
</div>
{/* Navigation Buttons */}
{(!isFirstStep || !isLastStep) && (
<div className="mt-4 flex items-center justify-center gap-x-4 lg:justify-start">
<button
type="button"
onClick={onPrevious}
disabled={isFirstStep}
className={`text-xs font-semibold transition-colors ${
isFirstStep
? 'text-slate-50 cursor-default'
: 'text-gray-500 hover:text-gray-700'
}`}
>
Go back
</button>
{!isLastStep && (
<button
type="button"
onClick={onNext}
className="text-xs font-semibold text-blue-600 hover:text-blue-700 transition-colors"
>
Continue
</button>
)}
</div>
)}
</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}%` }}
{/* Visual Section - Right Half */}
<div className="relative lg:flex-1 mt-4 lg:mt-0 h-32 lg:h-full lg:min-h-[150px] flex items-end justify-end">
<img
src="/images/misc/cow.png"
alt="Profit Planet Mascot"
className="max-h-full max-w-full object-contain opacity-90 pl-30"
/>
</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>
@ -179,86 +238,102 @@ export const createTutorialSteps = (
onUploadId: () => void,
onCompleteInfo: () => void,
onSignContract: () => void,
onCloseTutorial: () => void
onCloseTutorial: () => void,
onNext: () => 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.",
title: "Hello there! 👋 Welcome to Profit Planet",
description: "We're so happy you've decided to join us! This quick tutorial will guide you through setting up your account in just a few simple steps. Let's make this journey together - it'll only take a few minutes!",
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"
"We'll walk you through each step personally",
"Everything is designed to be simple and clear",
"You can skip steps if you want to come back later",
"Our team is here to help if you need anything"
],
icon: EnvelopeIcon,
buttonText: emailVerified ? "Email Verified ✓" : "Verify Email",
buttonAction: onVerifyEmail,
icon: HandRaisedIcon,
buttonText: "Let's get started! 🚀",
buttonAction: onNext,
canProceed: true
},
{
id: 2,
title: "Step 2: Upload ID Documents",
description: "Next, we need to verify your identity by uploading official ID documents.",
title: "Let's verify your email address 📧",
description: "First things first - we'd love to make sure we can reach you! Please check your email inbox for a friendly message from us and the verification code.",
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"
"Check your email inbox for our welcome message",
"Copy & paste the verification code into the field",
"Don't see it? Check your spam folder - sometimes it hides there",
"Need a new email? Just click below and we'll send another"
],
icon: IdentificationIcon,
buttonText: idUploaded ? "ID Uploaded ✓" : "Upload ID",
buttonAction: onUploadId,
canProceed: emailVerified
icon: EnvelopeIcon,
buttonText: emailVerified ? "Email verified! ✅" : "Verify my email",
buttonAction: emailVerified ? onNext : onVerifyEmail,
canProceed: true
},
{
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"
title: "Time to upload your ID 📋",
description: "Now we need to get to know you better! Please upload a clear photo of your official ID. Don't worry - this information is completely secure and helps us keep everyone safe.",
details: [
"Take a clear, well-lit photo of your ID document",
"Make sure all text is easily readable",
"Passport, driver's license, or national ID all work perfectly",
"We protect your privacy - this is just for verification"
],
icon: UserIcon,
buttonText: additionalInfo ? "Profile Complete ✓" : "Complete Profile",
buttonAction: onCompleteInfo,
canProceed: emailVerified && idUploaded
icon: IdentificationIcon,
buttonText: idUploaded ? "ID uploaded! ✅" : "Upload my ID",
buttonAction: idUploaded ? onNext : onUploadId,
canProceed: true
},
{
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"
title: "Complete your profile 👤",
description: `Almost there! Now let's fill out your ${userType === 'personal' ? 'personal' : 'company'} profile. This helps us customize your experience and ensure everything runs smoothly.`,
details: userType === 'personal' ? [
"Share your full name and date of birth with us",
"Add your current address (we keep this private)",
"Include a phone number so we can reach you if needed",
"All information is required for account security"
] : [
"Tell us about your company and business details",
"Add your business address and contact information",
"Upload any business documents we might need",
"Make sure everything matches your official records"
],
icon: DocumentTextIcon,
buttonText: contractSigned ? "Contract Signed ✓" : "Sign Contract",
icon: UserIcon,
buttonText: additionalInfo ? "Profile completed! ✅" : "Complete my profile",
buttonAction: additionalInfo ? onNext : onCompleteInfo,
canProceed: true
},
{
id: 5,
title: "Our team is preparing your contract ⏳",
description: "Excellent! Now our team needs to manually review your information and prepare your personalized contract. This ensures everything is perfectly tailored to your needs.",
details: [
"Our admin team is currently reviewing your submitted documents",
"We're preparing a personalized contract just for you",
"This process typically takes 1-2 business days",
"You'll receive an email notification once your contract is ready to sign"
],
icon: ClockIcon,
buttonText: contractSigned ? "Contract signed! ✅" : "Waiting for admin review",
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.",
id: 6,
title: "You're all set! 🎉 Welcome to the community",
description: "Congratulations! You've completed all the steps perfectly. Our friendly team will now review your information - we'll have you approved and ready to go very soon!",
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"
"Our team will carefully review everything you've submitted",
"This usually takes just 1-2 business days",
"We'll send you a celebratory email once you're approved",
"In the meantime, feel free to explore"
],
icon: ClockIcon,
buttonText: "I Understand",
icon: HeartIcon,
buttonText: "I understand!",
buttonAction: onCloseTutorial,
canProceed: true // Always enable "I Understand" button
canProceed: true
}
]

View File

@ -51,6 +51,33 @@ export default function QuickActionDashboardPage() {
const additionalInfo = userStatus?.profile_completed || false
const contractSigned = userStatus?.contract_signed || false
// Check if we should open tutorial (from URL parameter) - separate useEffect after status is loaded
useEffect(() => {
if (!isClient || loading || !userStatus) return
const urlParams = new URLSearchParams(window.location.search)
if (urlParams.get('tutorial') === 'true') {
// Remove the parameter from URL
const newUrl = window.location.pathname
window.history.replaceState({}, '', newUrl)
// Open tutorial and go to next step
setTimeout(() => {
setIsTutorialOpen(true)
// Determine next step based on completion status
if (!emailVerified) {
setCurrentTutorialStep(2)
} else if (!idUploaded) {
setCurrentTutorialStep(3)
} else if (!additionalInfo) {
setCurrentTutorialStep(4)
} else {
setCurrentTutorialStep(5)
}
}, 500)
}
}, [isClient, loading, userStatus, emailVerified, idUploaded, additionalInfo])
const statusItems: StatusItem[] = [
{
key: 'email',
@ -82,19 +109,19 @@ export default function QuickActionDashboardPage() {
}
]
// Action handlers - navigate to proper QuickAction pages
// Action handlers - navigate to proper QuickAction pages with tutorial callback
const handleVerifyEmail = useCallback(() => {
router.push('/quickaction-dashboard/register-email-verify')
router.push('/quickaction-dashboard/register-email-verify?tutorial=true')
}, [router])
const handleUploadId = useCallback(() => {
const userType = user?.userType || 'personal'
router.push(`/quickaction-dashboard/register-upload-id/${userType}`)
router.push(`/quickaction-dashboard/register-upload-id/${userType}?tutorial=true`)
}, [router, user])
const handleCompleteInfo = useCallback(() => {
const userType = user?.userType || 'personal'
router.push(`/quickaction-dashboard/register-additional-information/${userType}`)
router.push(`/quickaction-dashboard/register-additional-information/${userType}?tutorial=true`)
}, [router, user])
const handleSignContract = useCallback(() => {
@ -143,7 +170,8 @@ export default function QuickActionDashboardPage() {
handleUploadId,
handleCompleteInfo,
handleSignContract,
closeTutorial
closeTutorial,
() => setCurrentTutorialStep(prev => prev + 1) // onNext function
)
const canSignContract = emailVerified && idUploaded && additionalInfo

View File

@ -128,7 +128,15 @@ export default function CompanyAdditionalInformationPage() {
// Redirect to next step after short delay
setTimeout(() => {
router.push('/quickaction-dashboard/register-sign-contract/company')
// Check if we came from tutorial
const urlParams = new URLSearchParams(window.location.search)
const fromTutorial = urlParams.get('tutorial') === 'true'
if (fromTutorial) {
router.push('/quickaction-dashboard?tutorial=true')
} else {
router.push('/quickaction-dashboard/register-sign-contract/company')
}
}, 1500)
} catch (error: any) {

View File

@ -170,7 +170,15 @@ export default function PersonalAdditionalInformationPage() {
// Redirect to next step after short delay
setTimeout(() => {
router.push('/quickaction-dashboard/register-sign-contract/personal')
// Check if we came from tutorial
const urlParams = new URLSearchParams(window.location.search)
const fromTutorial = urlParams.get('tutorial') === 'true'
if (fromTutorial) {
router.push('/quickaction-dashboard?tutorial=true')
} else {
router.push('/quickaction-dashboard/register-sign-contract/personal')
}
}, 1500)
} catch (error: any) {

View File

@ -199,7 +199,15 @@ export default function EmailVerifyPage() {
await refreshStatus() // Refresh user status
// Redirect after 2 seconds
setTimeout(() => {
window.location.href = '/quickaction-dashboard'
// Check if we came from tutorial
const urlParams = new URLSearchParams(window.location.search)
const fromTutorial = urlParams.get('tutorial') === 'true'
if (fromTutorial) {
window.location.href = '/quickaction-dashboard?tutorial=true'
} else {
window.location.href = '/quickaction-dashboard'
}
}, 2000)
} else {
setError(data.error || 'Verification failed. Please try again.')

View File

@ -123,8 +123,16 @@ export function useCompanyUploadId() {
await refreshStatus()
setTimeout(() => {
// keep same redirect as page used before
window.location.href = '/quickaction-dashboard/register-additional-information'
// Check if we came from tutorial
const urlParams = new URLSearchParams(window.location.search)
const fromTutorial = urlParams.get('tutorial') === 'true'
if (fromTutorial) {
window.location.href = '/quickaction-dashboard?tutorial=true'
} else {
// keep same redirect as page used before
window.location.href = '/quickaction-dashboard/register-additional-information'
}
}, 1500)
} catch (err: any) {
console.error('Company ID upload error:', err)

View File

@ -128,7 +128,15 @@ export function usePersonalUploadId() {
setSuccess(true)
await refreshStatus()
setTimeout(() => {
window.location.href = '/quickaction-dashboard'
// Check if we came from tutorial
const urlParams = new URLSearchParams(window.location.search)
const fromTutorial = urlParams.get('tutorial') === 'true'
if (fromTutorial) {
window.location.href = '/quickaction-dashboard?tutorial=true'
} else {
window.location.href = '/quickaction-dashboard'
}
}, 2000)
} else {
setError(data.message || 'Upload failed. Please try again.')