Skip to content

Commit 99d9e00

Browse files
authored
Merge pull request #368 from widgetify-app/feat/add-welcome-wizard
Feat/add welcome wizard
2 parents b487110 + b6f80eb commit 99d9e00

11 files changed

Lines changed: 458 additions & 19 deletions

File tree

src/common/utils/call-event.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ export interface EventName {
4747
openProfile: null
4848
openMarketModal: null
4949
font_change: FontFamily
50+
close_all_modals: null
51+
openWizardModal: null
5052
}
5153

5254
export function callEvent<K extends keyof EventName>(eventName: K, data?: EventName[K]) {

src/components/chip.component.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
interface ChipProps {
2+
selected: boolean
3+
onClick: () => void
4+
children: React.ReactNode
5+
className?: string
6+
}
7+
8+
export const Chip: React.FC<ChipProps> = ({
9+
selected,
10+
onClick,
11+
children,
12+
className = '',
13+
}) => {
14+
return (
15+
<button
16+
onClick={onClick}
17+
className={`px-4 py-2 cursor-pointer rounded-full text-xs font-bold transition-all border-2 ${selected ? 'bg-primary border-primary text-white' : 'bg-base-100 border-base-300/30 text-muted hover:border-primary/30'} ${className}`}
18+
>
19+
{children}
20+
</button>
21+
)
22+
}

src/components/welcome-wizard.tsx

Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,365 @@
1+
import { useState } from 'react'
2+
import Modal from '@/components/modal'
3+
import { Button } from '@/components/button/button'
4+
import {
5+
useGetOccupations,
6+
useGetInterests,
7+
} from '@/services/hooks/profile/getProfileMeta.hook'
8+
import { LuChevronLeft } from 'react-icons/lu'
9+
import { TextInput } from '@/components/text-input'
10+
import { sleep } from '@/common/utils/timeout'
11+
import { Chip } from '@/components/chip.component'
12+
import { ItemSelector } from './item-selector'
13+
import { useSetupWizard } from '@/services/hooks/auth/authService.hook'
14+
import { showToast } from '@/common/toast'
15+
import { safeAwait } from '@/services/api'
16+
import Analytics from '@/analytics'
17+
18+
export enum ReferralSource {
19+
Social = 'social',
20+
Youtube = 'youtube',
21+
Friends = 'friends',
22+
SearchOther = 'search_other',
23+
}
24+
25+
interface WelcomeWizardProps {
26+
isOpen: boolean
27+
onClose: () => void
28+
}
29+
30+
const StepWrapper = ({ children }: { children: React.ReactNode }) => {
31+
return <div className="flex flex-col md:flex-row min-h-125">{children}</div>
32+
}
33+
34+
const StepImage = ({ src, alt }: { src: string; alt: string }) => {
35+
return (
36+
<div className="relative flex items-center justify-center w-full p-2 bg-base-200/50 md:w-1/2">
37+
<img src={src} alt={alt} className="object-cover w-full rounded max-h-124" />
38+
</div>
39+
)
40+
}
41+
export const WelcomeWizard = ({ isOpen, onClose }: WelcomeWizardProps) => {
42+
const [currentStep, setCurrentStep] = useState(1)
43+
const [fetching, setFetching] = useState(false)
44+
const [selectedOccupation, setSelectedOccupation] = useState<string | null>(null)
45+
const [selectedInterests, setSelectedInterests] = useState<string[]>([])
46+
const [selectedReferralSource, setSelectedReferralSource] =
47+
useState<ReferralSource | null>(null)
48+
const [referralCode, setReferralCode] = useState<string>('')
49+
const totalSteps = 5
50+
51+
const { mutateAsync, isPending } = useSetupWizard()
52+
53+
const { data: occupations, isLoading: occupationsLoading } =
54+
useGetOccupations(fetching)
55+
const { data: interests, isLoading: interestsLoading } = useGetInterests(fetching)
56+
57+
const nextStep = () => {
58+
Analytics.event(`welcome_wizard_step_${currentStep}_completed`)
59+
if (currentStep < totalSteps) setCurrentStep(currentStep + 1)
60+
else onClose()
61+
}
62+
63+
const prevStep = () => {
64+
if (currentStep > 1) setCurrentStep(currentStep - 1)
65+
Analytics.event('welcome_wizard_step_back_clicked')
66+
}
67+
68+
useEffect(() => {
69+
const load = async () => {
70+
await sleep(300)
71+
setFetching(true)
72+
}
73+
load()
74+
Analytics.event('welcome_wizard_opened')
75+
}, [])
76+
77+
const save = async () => {
78+
if (
79+
!selectedOccupation ||
80+
selectedInterests.length === 0 ||
81+
!selectedReferralSource
82+
) {
83+
showToast('لطفاً تمام مراحل را تکمیل کنید.', 'error')
84+
return
85+
}
86+
87+
const [err, _] = await safeAwait(
88+
mutateAsync({
89+
occupationId: selectedOccupation,
90+
interestsIds: selectedInterests,
91+
referralSource: selectedReferralSource,
92+
referralCode: referralCode || undefined,
93+
})
94+
)
95+
if (err) {
96+
showToast('خطا در ثبت اطلاعات. لطفاً دوباره تلاش کنید.', 'error')
97+
Analytics.event('welcome_wizard_completion_failed')
98+
return
99+
}
100+
101+
setCurrentStep(currentStep + 1)
102+
Analytics.event('welcome_wizard_completed')
103+
}
104+
105+
const renderStepContent = () => {
106+
switch (currentStep) {
107+
case 1:
108+
return (
109+
<StepWrapper>
110+
<div className="flex flex-col items-center justify-center w-full p-8 text-center md:w-1/2 md:p-12">
111+
<div className="mb-10 space-y-4">
112+
<h2 className="text-2xl font-black text-content">
113+
خوش اومدی!
114+
</h2>
115+
<p className="text-sm font-medium leading-loose opacity-70">
116+
خیلی خوشحالیم که اینجایی. بیا با هم پروفایلت رو کامل
117+
کنیم تا تجربه بهتری داشته باشی.
118+
</p>
119+
</div>
120+
<Button
121+
size="sm"
122+
onClick={nextStep}
123+
className="w-full h-12 text-base font-bold text-white shadow-lg rounded-2xl"
124+
isPrimary
125+
>
126+
بزن بریم
127+
</Button>
128+
</div>
129+
<StepImage src="https://picsum.photos/400/601" alt="Welcome" />
130+
</StepWrapper>
131+
)
132+
133+
case 2:
134+
return (
135+
<StepWrapper>
136+
<div className="flex flex-col justify-between w-full p-4 md:w-1/2 md:p-10">
137+
<div className="w-full">
138+
<div className="mb-6 text-right">
139+
<h2 className="mb-2 text-2xl font-black text-content">
140+
چه کاره‌ای؟
141+
</h2>
142+
<p className="text-sm font-medium opacity-60">
143+
حرفه‌ات رو انتخاب کن
144+
</p>
145+
</div>
146+
<div className="flex flex-wrap gap-2 overflow-y-auto max-h-75 scrollbar-none">
147+
{occupationsLoading ? (
148+
<div className="col-span-2 py-10 text-center animate-pulse">
149+
در حال بارگذاری...
150+
</div>
151+
) : (
152+
occupations?.map((job) => {
153+
const isSelected =
154+
selectedOccupation === job.id
155+
return (
156+
<Chip
157+
key={job.id}
158+
selected={isSelected}
159+
onClick={() =>
160+
setSelectedOccupation(job.id)
161+
}
162+
>
163+
{job.title}
164+
</Chip>
165+
)
166+
})
167+
)}
168+
</div>
169+
</div>
170+
<div className="flex gap-3 mt-6">
171+
<Button
172+
size="sm"
173+
onClick={nextStep}
174+
disabled={!selectedOccupation}
175+
className="flex-1 h-12 font-bold text-white rounded-2xl"
176+
isPrimary
177+
>
178+
تایید و ادامه
179+
</Button>
180+
</div>
181+
</div>
182+
<StepImage src="https://picsum.photos/400/601" alt="Welcome" />
183+
</StepWrapper>
184+
)
185+
186+
case 3:
187+
return (
188+
<StepWrapper>
189+
<div className="flex flex-col justify-between w-full p-8 md:w-1/2 md:p-10">
190+
<div className="w-full">
191+
<div className="mb-6 text-right">
192+
<h2 className="mb-2 text-2xl font-black text-content">
193+
به چی علاقه داری؟
194+
</h2>
195+
<p className="text-sm font-medium opacity-60">
196+
هر تعداد که دوست داری انتخاب کن
197+
</p>
198+
</div>
199+
<div className="flex flex-wrap gap-2 overflow-y-auto max-h-75 scrollbar-none">
200+
{interestsLoading ? (
201+
<div className="w-full py-10 text-center animate-pulse">
202+
در حال بارگذاری...
203+
</div>
204+
) : (
205+
interests?.map((item) => {
206+
const isSelected = selectedInterests.includes(
207+
item.id
208+
)
209+
return (
210+
<Chip
211+
key={item.id}
212+
selected={isSelected}
213+
onClick={() => {
214+
if (isSelected)
215+
setSelectedInterests(
216+
selectedInterests.filter(
217+
(id) => id !== item.id
218+
)
219+
)
220+
else
221+
setSelectedInterests([
222+
...selectedInterests,
223+
item.id,
224+
])
225+
}}
226+
>
227+
{item.title}
228+
</Chip>
229+
)
230+
})
231+
)}
232+
</div>
233+
</div>
234+
<div className="flex gap-3 mt-6">
235+
<Button
236+
size="sm"
237+
onClick={nextStep}
238+
disabled={selectedInterests.length === 0}
239+
className="flex-1 h-12 font-bold text-white rounded-2xl"
240+
isPrimary
241+
>
242+
ادامه
243+
</Button>
244+
</div>
245+
</div>
246+
<StepImage src="https://picsum.photos/400/601" alt="Welcome" />
247+
</StepWrapper>
248+
)
249+
250+
case 4:
251+
return (
252+
<StepWrapper>
253+
<div className="flex flex-col items-center justify-center w-full p-8 text-center md:w-1/2 md:p-12">
254+
<div className="mb-10 space-y-4">
255+
<h2 className="text-2xl font-black text-content">
256+
مرحله ۴: از کجا شنیدی؟
257+
</h2>
258+
<p className="text-sm font-medium leading-loose opacity-70 text-balance">
259+
لطفاً بگو از کجا با ویجتیفای آشنا شدی.
260+
</p>
261+
</div>
262+
263+
<div className="w-full max-w-md space-y-4">
264+
<div className="flex flex-wrap gap-2">
265+
{[
266+
{
267+
value: ReferralSource.Social,
268+
label: 'شبکه‌های اجتماعی',
269+
},
270+
{
271+
value: ReferralSource.Youtube,
272+
label: 'یوتیوب',
273+
},
274+
{
275+
value: ReferralSource.Friends,
276+
label: 'دوستان',
277+
},
278+
{
279+
value: ReferralSource.SearchOther,
280+
label: 'جستجو یا سایر',
281+
},
282+
].map((option) => (
283+
<ItemSelector
284+
isActive={
285+
selectedReferralSource === option.value
286+
}
287+
label={option.label}
288+
key={option.value}
289+
onClick={() =>
290+
setSelectedReferralSource(option.value)
291+
}
292+
/>
293+
))}
294+
</div>
295+
296+
{selectedReferralSource === ReferralSource.Friends && (
297+
<div className="mt-4">
298+
<TextInput
299+
value={referralCode}
300+
onChange={setReferralCode}
301+
placeholder="کد دعوت را وارد کنید"
302+
/>
303+
</div>
304+
)}
305+
</div>
306+
307+
<Button
308+
size="sm"
309+
onClick={() => save()}
310+
disabled={!selectedReferralSource || isPending}
311+
loading={isPending}
312+
className="w-full h-12 mt-4 text-base font-bold text-white shadow-lg rounded-2xl"
313+
isPrimary
314+
>
315+
ادامه
316+
</Button>
317+
</div>
318+
<StepImage src="https://picsum.photos/400/601" alt="Welcome" />
319+
</StepWrapper>
320+
)
321+
322+
case 5:
323+
return (
324+
<StepWrapper>
325+
<div className="flex flex-col items-center justify-center w-full p-8 text-center md:w-1/2 md:p-12">
326+
<div className="mb-10 space-y-4">
327+
<h2 className="text-2xl font-black text-content">
328+
همه چیز آماده‌ست! 🚀
329+
</h2>
330+
<p className="text-sm font-medium leading-loose opacity-70">
331+
تنظیمات پروفایلت با موفقیت انجام شد. حالا می‌تونی از
332+
تمام امکانات استفاده کنی.
333+
</p>
334+
</div>
335+
<Button
336+
size="sm"
337+
onClick={onClose}
338+
className="w-full h-12 text-base font-bold text-white shadow-lg rounded-2xl"
339+
isPrimary
340+
>
341+
شروع استفاده
342+
</Button>
343+
</div>
344+
<StepImage src="https://picsum.photos/400/601" alt="Welcome" />
345+
</StepWrapper>
346+
)
347+
}
348+
}
349+
350+
return (
351+
<Modal isOpen={isOpen} onClose={onClose} size="xl" direction="rtl" title=" ">
352+
<div className="relative overflow-hidden rounded bg-base-100">
353+
{currentStep > 1 && currentStep < totalSteps && (
354+
<button
355+
onClick={prevStep}
356+
className="absolute z-20 p-2 transition-colors rounded-full top-10 right-96 bg-base-200/50 text-content hover:bg-base-300"
357+
>
358+
<LuChevronLeft size={20} />
359+
</button>
360+
)}
361+
{renderStepContent()}
362+
</div>
363+
</Modal>
364+
)
365+
}

0 commit comments

Comments
 (0)