From b86c41241cd35d39aa68a663d88c8e9ee578f800 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Wed, 24 Dec 2025 15:14:12 +0330 Subject: [PATCH 1/3] feat: add welcome wizard for new user onboarding --- src/common/utils/call-event.ts | 2 + src/components/welcome-wizard.tsx | 397 ++++++++++++++++++ src/layouts/navbar/friends-list/friends.tsx | 14 +- src/layouts/navbar/profile/profile.tsx | 23 +- .../setting/tabs/about-us/about-us.tsx | 4 +- .../tabs/account/auth-form/auth-otp.tsx | 7 + .../components/login-google.button.tsx | 7 + 7 files changed, 438 insertions(+), 16 deletions(-) create mode 100644 src/components/welcome-wizard.tsx diff --git a/src/common/utils/call-event.ts b/src/common/utils/call-event.ts index d5da9496..506d9654 100644 --- a/src/common/utils/call-event.ts +++ b/src/common/utils/call-event.ts @@ -47,6 +47,8 @@ export interface EventName { openProfile: null openMarketModal: null font_change: FontFamily + close_all_modals: null + openWizardModal: null } export function callEvent(eventName: K, data?: EventName[K]) { diff --git a/src/components/welcome-wizard.tsx b/src/components/welcome-wizard.tsx new file mode 100644 index 00000000..008c3f2f --- /dev/null +++ b/src/components/welcome-wizard.tsx @@ -0,0 +1,397 @@ +import { useState } from 'react' +import Modal from '@/components/modal' +import { Button } from '@/components/button/button' +import { + useGetOccupations, + useGetInterests, +} from '@/services/hooks/profile/getProfileMeta.hook' +import { LuChevronLeft, LuSparkles } from 'react-icons/lu' +import { FaCheck } from 'react-icons/fa' +import { TextInput } from '@/components/text-input' +import { sleep } from '@/common/utils/timeout' + +export enum ReferralSource { + Social = 'social', + Youtube = 'youtube', + Friends = 'friends', + SearchOther = 'search_other', +} + +interface WelcomeWizardProps { + isOpen: boolean + onClose: () => void +} + +const StepWrapper = ({ children }: { children: React.ReactNode }) => { + return
{children}
+} + +const StepImage = ({ src, alt }: { src: string; alt: string }) => { + return ( +
+ {alt} +
+ ) +} +export const WelcomeWizard = ({ isOpen, onClose }: WelcomeWizardProps) => { + const [currentStep, setCurrentStep] = useState(1) + const [fetching, setFetching] = useState(false) + const [selectedOccupation, setSelectedOccupation] = useState(null) + const [selectedInterests, setSelectedInterests] = useState([]) + const [selectedReferralSource, setSelectedReferralSource] = + useState(null) + const [referralCode, setReferralCode] = useState('') + const totalSteps = 5 + + const { data: occupations, isLoading: occupationsLoading } = + useGetOccupations(fetching) + const { data: interests, isLoading: interestsLoading } = useGetInterests(fetching) + + const nextStep = () => { + if (currentStep < totalSteps) setCurrentStep(currentStep + 1) + else onClose() + } + + const prevStep = () => { + if (currentStep > 1) setCurrentStep(currentStep - 1) + } + + useEffect(() => { + const load = async () => { + await sleep(300) + setFetching(true) + } + load() + }, []) + + const renderStepContent = () => { + switch (currentStep) { + case 1: + return ( + +
+
+

+ خوش اومدی! +

+

+ خیلی خوشحالیم که اینجایی. بیا با هم پروفایلت رو کامل + کنیم تا تجربه بهتری داشته باشی. +

+
+ +
+ +
+ ) + + case 2: + return ( + +
+
+
+

+ چه کاره‌ای؟ +

+

+ حرفه‌ات رو انتخاب کن +

+
+
+ {occupationsLoading ? ( +
+ در حال بارگذاری... +
+ ) : ( + occupations?.map((job) => { + const isSelected = + selectedOccupation === job.id + return ( + + ) + }) + )} +
+
+
+ +
+
+ +
+ ) + + case 3: + return ( + +
+
+
+

+ به چی علاقه داری؟ +

+

+ تا ۳ مورد رو می‌تونی انتخاب کنی +

+
+
+ {interestsLoading ? ( +
+ در حال بارگذاری... +
+ ) : ( + interests?.map((item) => { + const isSelected = selectedInterests.includes( + item.id + ) + return ( + + ) + }) + )} +
+
+
+ +
+
+ +
+ ) + + case 4: + return ( + +
+
+

+ مرحله ۴: از کجا شنیدی؟ +

+

+ لطفاً بگو از کجا با ویجتیفای آشنا شدی. +

+
+ +
+
+ + + + +
+ + {selectedReferralSource === ReferralSource.Friends && ( +
+ +
+ )} +
+ + +
+ +
+ ) + + case 5: + return ( + +
+
+

+ همه چیز آماده‌ست! 🚀 +

+

+ تنظیمات پروفایلت با موفقیت انجام شد. حالا می‌تونی از + تمام امکانات استفاده کنی. +

+
+ +
+ +
+ ) + } + } + + return ( + +
+ {currentStep > 1 && currentStep < totalSteps && ( + + )} + {renderStepContent()} +
+
+ ) +} diff --git a/src/layouts/navbar/friends-list/friends.tsx b/src/layouts/navbar/friends-list/friends.tsx index d0f6816a..703c889a 100644 --- a/src/layouts/navbar/friends-list/friends.tsx +++ b/src/layouts/navbar/friends-list/friends.tsx @@ -5,7 +5,6 @@ import { AuthRequiredModal } from '@/components/auth/AuthRequiredModal' import { ClickableTooltip } from '@/components/clickableTooltip' import { useAuth } from '@/context/auth.context' import { useGetFriends } from '@/services/hooks/friends/friendService.hook' -import { UserAccountModal } from '../../setting/tabs/account/user-account.modal' import { FriendItem } from './friend.item' import { HiUserGroup } from 'react-icons/hi2' @@ -73,12 +72,11 @@ export function FriendsList() { const { isAuthenticated } = useAuth() const [firstAuth, setFirstAuth] = useState(false) - const [showSettingsModal, setShowSettingsModal] = useState(false) const [activeProfileId, setActiveProfileId] = useState(null) const [isOpen, setIsOpen] = useState(false) const triggerRef = useRef(null) - const { data: friendsData, refetch: refetchFriends } = useGetFriends({ + const { data: friendsData } = useGetFriends({ status: 'ACCEPTED', enabled: isAuthenticated, }) @@ -91,13 +89,8 @@ export function FriendsList() { setFirstAuth(true) return } - setShowSettingsModal(true) } - const handleSettingsModalClose = () => { - setShowSettingsModal(false) - refetchFriends() - } const handleAuthModalClose = () => setFirstAuth(false) if (!isAuthenticated) { @@ -128,11 +121,6 @@ export function FriendsList() { closeOnClickOutside={true} /> - - { if (user?.avatar) { @@ -32,6 +34,7 @@ const getTooltipContent = (user: any) => { export function ProfileNav() { const { user, isAuthenticated } = useAuth() const [showSettingsModal, setShowSettingsModal] = useState(false) + const [openedWizard, setOpenedWizard] = useState(false) const handleProfileClick = () => { setShowSettingsModal(true) @@ -51,8 +54,19 @@ export function ProfileNav() { handleProfileClick() }) + const eventClose = listenEvent('close_all_modals', () => { + modalCloseHandler() + }) + + const openWizardEvent = listenEvent('openWizardModal', () => { + modalCloseHandler() + setOpenedWizard(true) + }) + return () => { event() + eventClose() + openWizardEvent() } }, []) @@ -85,6 +99,13 @@ export function ProfileNav() { selectedTab="profile" onClose={modalCloseHandler} /> + + {openedWizard && ( + setOpenedWizard(false)} + /> + )} ) } diff --git a/src/layouts/setting/tabs/about-us/about-us.tsx b/src/layouts/setting/tabs/about-us/about-us.tsx index 6c689701..ad2ed6fe 100644 --- a/src/layouts/setting/tabs/about-us/about-us.tsx +++ b/src/layouts/setting/tabs/about-us/about-us.tsx @@ -44,7 +44,7 @@ export function AboutUsTab() { 'mb-1 text-3xl font-bold text-transparent bg-clip-text bg-gradient-to-r from-blue-700 to-indigo-600' } > - ویجتی‌فای + ویجتیفای
- ویجتی‌فای یک افزونه متن‌باز برای مرورگر شماست که صفحه جدید را با + ویجتیفای یک افزونه متن‌باز برای مرورگر شماست که صفحه جدید را با ابزارهای کاربردی و سبک زیبا به محیطی کارآمد و شخصی‌سازی شده تبدیل می‌کند.

diff --git a/src/layouts/setting/tabs/account/auth-form/auth-otp.tsx b/src/layouts/setting/tabs/account/auth-form/auth-otp.tsx index 390caedb..554f12b7 100644 --- a/src/layouts/setting/tabs/account/auth-form/auth-otp.tsx +++ b/src/layouts/setting/tabs/account/auth-form/auth-otp.tsx @@ -8,6 +8,8 @@ import { useAuth } from '@/context/auth.context' import { isEmpty, isEmail, isLessThan } from '@/utils/validators' import InputTextError from './components/input-text-error' import OtpInput from './components/otp-input' +import { callEvent } from '@/common/utils/call-event' +import { sleep } from '@/common/utils/timeout' type AuthOtpProps = { step: 'enter-email' | 'enter-otp' @@ -79,6 +81,11 @@ const AuthOtp: React.FC = ({ step, setStep }) => { try { setError({ email: null, otp: null, api: null }) const response = await verifyOtp({ email, code: otp }) + if (response.isNewUser) { + callEvent('openWizardModal') + await sleep(500) + } + login(response.data) } catch (err: any) { const content = translateError(err) diff --git a/src/layouts/setting/tabs/account/auth-form/components/login-google.button.tsx b/src/layouts/setting/tabs/account/auth-form/components/login-google.button.tsx index 0290df16..71726c0d 100644 --- a/src/layouts/setting/tabs/account/auth-form/components/login-google.button.tsx +++ b/src/layouts/setting/tabs/account/auth-form/components/login-google.button.tsx @@ -10,6 +10,8 @@ import type { AxiosError } from 'axios' import { showToast } from '@/common/toast' import { translateError } from '@/utils/translate-error' import Analytics from '@/analytics' +import { callEvent } from '@/common/utils/call-event' +import { sleep } from '@/common/utils/timeout' export default function LoginGoogleButton() { const { login } = useAuth() @@ -67,6 +69,11 @@ export default function LoginGoogleButton() { return showToast(translateError(err) as string, 'error') } + if (response.isNewUser) { + callEvent('openWizardModal') + await sleep(300) + } + login(response.data) } } finally { From 446c4dc06e65d0efbf8e17e601f6f5febf668f04 Mon Sep 17 00:00:00 2001 From: sajjad isvand Date: Wed, 24 Dec 2025 15:26:48 +0330 Subject: [PATCH 2/3] feat: implement Chip component and integrate it into WelcomeWizard for improved user selection --- src/components/chip.component.tsx | 24 ++++ src/components/welcome-wizard.tsx | 164 ++++++++----------------- src/layouts/navbar/profile/profile.tsx | 2 +- 3 files changed, 75 insertions(+), 115 deletions(-) create mode 100644 src/components/chip.component.tsx diff --git a/src/components/chip.component.tsx b/src/components/chip.component.tsx new file mode 100644 index 00000000..f9fb3410 --- /dev/null +++ b/src/components/chip.component.tsx @@ -0,0 +1,24 @@ +import React from 'react' + +interface ChipProps { + selected: boolean + onClick: () => void + children: React.ReactNode + className?: string +} + +export const Chip: React.FC = ({ + selected, + onClick, + children, + className = '', +}) => { + return ( + + ) +} diff --git a/src/components/welcome-wizard.tsx b/src/components/welcome-wizard.tsx index 008c3f2f..36467f91 100644 --- a/src/components/welcome-wizard.tsx +++ b/src/components/welcome-wizard.tsx @@ -5,10 +5,11 @@ import { useGetOccupations, useGetInterests, } from '@/services/hooks/profile/getProfileMeta.hook' -import { LuChevronLeft, LuSparkles } from 'react-icons/lu' -import { FaCheck } from 'react-icons/fa' +import { LuChevronLeft } from 'react-icons/lu' import { TextInput } from '@/components/text-input' import { sleep } from '@/common/utils/timeout' +import { Chip } from '@/components/chip.component' +import { ItemSelector } from './item-selector' export enum ReferralSource { Social = 'social', @@ -64,6 +65,12 @@ export const WelcomeWizard = ({ isOpen, onClose }: WelcomeWizardProps) => { load() }, []) + const save = async () => { + // todo: send data to server + // when done, close the wizard + onClose() + } + const renderStepContent = () => { switch (currentStep) { case 1: @@ -105,7 +112,7 @@ export const WelcomeWizard = ({ isOpen, onClose }: WelcomeWizardProps) => { حرفه‌ات رو انتخاب کن

-
+
{occupationsLoading ? (
در حال بارگذاری... @@ -115,37 +122,15 @@ export const WelcomeWizard = ({ isOpen, onClose }: WelcomeWizardProps) => { const isSelected = selectedOccupation === job.id return ( - + {job.title} + ) }) )} @@ -177,7 +162,7 @@ export const WelcomeWizard = ({ isOpen, onClose }: WelcomeWizardProps) => { به چی علاقه داری؟

- تا ۳ مورد رو می‌تونی انتخاب کنی + هر تعداد که دوست داری انتخاب کن

@@ -191,8 +176,9 @@ export const WelcomeWizard = ({ isOpen, onClose }: WelcomeWizardProps) => { item.id ) return ( - + ) }) )} @@ -251,79 +230,36 @@ export const WelcomeWizard = ({ isOpen, onClose }: WelcomeWizardProps) => {
-
- - - - + ))}
{selectedReferralSource === ReferralSource.Friends && ( @@ -341,10 +277,10 @@ export const WelcomeWizard = ({ isOpen, onClose }: WelcomeWizardProps) => { size="sm" onClick={nextStep} disabled={!selectedReferralSource} - className="w-full h-12 mt-8 text-base font-bold text-white shadow-lg shadow-primary/30 rounded-2xl" - isPrimary={true} + className="w-full h-12 mt-4 text-base font-bold text-white shadow-lg rounded-2xl" + isPrimary > - بعدی + ادامه
@@ -366,7 +302,7 @@ export const WelcomeWizard = ({ isOpen, onClose }: WelcomeWizardProps) => {