fix: refactor, emails list, breath circle

This commit is contained in:
gofnnp 2023-08-29 02:34:59 +04:00
parent 806a5d3050
commit 041112bdaa
14 changed files with 170 additions and 54 deletions

View File

@ -11,6 +11,7 @@ function BreathCircle(): JSX.Element {
const navigate = useNavigate() const navigate = useNavigate()
const [text, setText] = useState(t('')) const [text, setText] = useState(t(''))
const [render, setRender] = useState(false) const [render, setRender] = useState(false)
const [counter, setCounter] = useState(0)
useEffect(() => { useEffect(() => {
@ -29,10 +30,13 @@ function BreathCircle(): JSX.Element {
return sleep(4000) return sleep(4000)
}) })
.then(() => { .then(() => {
handleNext() setCounter((prevState) => prevState + 1)
if (counter === 3) {
handleNext()
}
setRender((prevState) => !prevState) setRender((prevState) => !prevState)
}) })
}, [t, render, navigate]) }, [t, render, navigate, counter])
return ( return (
<div className={styles['outer-circle']}> <div className={styles['outer-circle']}>

View File

@ -10,8 +10,8 @@
.inner-circle { .inner-circle {
position: relative; position: relative;
width: 200px; width: 240px;
height: 200px; height: 240px;
background-color: #69a8e7; background-color: #69a8e7;
border-radius: 100%; border-radius: 100%;
display: flex; display: flex;

View File

@ -10,7 +10,9 @@ import { AICompatCategories, useApi, useApiCall } from "@/api";
function CompatibilityPage(): JSX.Element { function CompatibilityPage(): JSX.Element {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const [isDisabled, setIsDisabled] = useState(false); const [isDisabled, setIsDisabled] = useState(true);
const [isDisabledName, setIsDisabledName] = useState(true);
const [isDisabledDate, setIsDisabledDate] = useState(true);
const [name, setName] = useState<string>(''); const [name, setName] = useState<string>('');
const [date, setDate] = useState<string | IDate>(''); const [date, setDate] = useState<string | IDate>('');
const [compatCategory, setCompatCategory] = useState(2); const [compatCategory, setCompatCategory] = useState(2);
@ -25,13 +27,19 @@ function CompatibilityPage(): JSX.Element {
const { data } = useApiCall<AICompatCategories.CompatCategory[]>(loadData) const { data } = useApiCall<AICompatCategories.CompatCategory[]>(loadData)
const handleValidName = (name: string) => { const handleValidName = (name: string) => {
setIsDisabled(name === ''); setIsDisabledName(!name.length);
setName(name) setName(name)
checkAllDisabled()
} }
const handleValidDate = (date: IDate | string) => { const handleValidDate = (date: IDate | string) => {
setIsDisabled(date === ''); setIsDisabledDate(date === '');
setDate(date) setDate(date)
checkAllDisabled()
}
const checkAllDisabled = () => {
setIsDisabled(isDisabledName || isDisabledDate);
} }
const changeCompatCategory = (event: React.ChangeEvent<HTMLInputElement>) => { const changeCompatCategory = (event: React.ChangeEvent<HTMLInputElement>) => {
@ -58,7 +66,7 @@ function CompatibilityPage(): JSX.Element {
value={name} value={name}
placeholder={t('name')} placeholder={t('name')}
onValid={handleValidName} onValid={handleValidName}
onInvalid={() => setIsDisabled(true)} onInvalid={() => setIsDisabledName(true)}
/> />
</div> </div>
<div className={styles['input-container__date-container']}> <div className={styles['input-container__date-container']}>
@ -67,7 +75,7 @@ function CompatibilityPage(): JSX.Element {
value={getDateAsString(date)} value={getDateAsString(date)}
inputClassName={styles['date-input']} inputClassName={styles['date-input']}
onValid={handleValidDate} onValid={handleValidDate}
onInvalid={() => setIsDisabled(true)} onInvalid={() => setIsDisabledDate(true)}
/> />
</div> </div>
</div> </div>

View File

@ -4,25 +4,45 @@ import styles from './styles.module.css'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useEffect, useRef, useState } from 'react' import { useEffect, useRef, useState } from 'react'
const emails: IEmailItem[] = [] // const getEmails = (): IEmailItem[] => {
// const emails: IEmailItem[] = []
// for (let i = 0; i < 2; i++) {
// const subList = []
// for (let j = 0; j < 4; j++) {
// subList.push({
// email: getRandomName(),
// price: 9
// })
// }
// subList[getRandomArbitrary(0, 4)].price = [5, 13.67][getRandomArbitrary(0, 2)]
// emails.push(...subList)
// }
for (let i = 0; i < 25; i++) { // return emails
const sublist = [] // }
for (let j = 0; j < 4; j++) {
sublist.push({ const getEmails = (): IEmailItem[] => {
const emails: IEmailItem[] = []
for (let index = 0; index < 5; index++) {
emails.push({
email: getRandomName(), email: getRandomName(),
price: 9 price: [9, 9, 9, 9, 9, 5, 13.67][getRandomArbitrary(0, 7)]
}) })
} }
sublist[getRandomArbitrary(0, 4)].price = [5, 13.67][getRandomArbitrary(0, 2)]
emails.push(...sublist) return emails
} }
function EmailsList(): JSX.Element { function EmailsList(): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const [countUsers, setCountUsers] = useState(752) const [countUsers, setCountUsers] = useState(752)
const [emails, setEmails] = useState(getEmails())
const [elementIdx, setElementIdx] = useState(0)
const itemsRef = useRef<HTMLDivElement[]>([]); const itemsRef = useRef<HTMLDivElement[]>([]);
let elementIdx = 0
useEffect(() => { useEffect(() => {
const randomDelay = getRandomArbitrary(3000, 5000) const randomDelay = getRandomArbitrary(3000, 5000)
@ -30,28 +50,39 @@ function EmailsList(): JSX.Element {
setCountUsers((prevState) => prevState + 1) setCountUsers((prevState) => prevState + 1)
}, randomDelay) }, randomDelay)
return () => clearTimeout(countUsersTimeOut) return () => clearTimeout(countUsersTimeOut)
}, [countUsers, elementIdx]) }, [countUsers])
useEffect(() => { useEffect(() => {
const randomDelay = getRandomArbitrary(500, 5000)
const itemsRefInterval = setInterval(() => { const itemsRefInterval = setInterval(() => {
if (itemsRef.current[elementIdx - 1]) {
itemsRef.current[elementIdx - 1].remove()
}
if (itemsRef.current[elementIdx]?.style) { if (itemsRef.current[elementIdx]?.style) {
itemsRef.current[elementIdx].className = styles.hidden itemsRef.current[elementIdx].classList.add(styles.hidden)
} }
elementIdx++ setEmails((prevState) => {
}, 3000) const array = prevState.slice(0)
return () => clearTimeout(itemsRefInterval) array.push({
}, [elementIdx]) email: getRandomName(),
price: [9, 9, 9, 9, 9, 5, 13.67][getRandomArbitrary(0, 7)]
})
return array
})
setElementIdx((prevState) => prevState + 1)
}, randomDelay)
return () => clearInterval(itemsRefInterval)
}, [emails, elementIdx])
return ( return (
<div className={styles.container}> <div className={styles.container}>
<span className={styles['title']}>{t('people_joined_today', { countPeoples: <strong>{countUsers}</strong> })}</span> <span className={styles['title']}>{t('people_joined_today', { countPeoples: <strong>{countUsers}</strong> })}</span>
<div className={styles['emails-container']}> <div className={styles['emails-container']}>
{emails.map(({email, price}, idx) => ( {emails.map(({email, price}, idx) => (
<div ref={(el: HTMLDivElement) => itemsRef.current[idx] = el} key={idx}> <div className={styles['email-item']} ref={(el: HTMLDivElement) => itemsRef.current[idx] = el} key={idx}>
<EmailItem email={email} price={price} /> <EmailItem email={email} price={price} />
</div> </div>
))} ))}
</div> </div>
</div> </div>

View File

@ -13,22 +13,28 @@
font-size: 14px; font-size: 14px;
font-weight: 500; font-weight: 500;
color: #494747; color: #494747;
text-align: center;
} }
.emails-container { .emails-container {
width: 100%; width: 100%;
flex-grow: 3; height: 108px;
padding: 8px 0; padding: 8px 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column-reverse;
align-items: center; align-items: center;
overflow: hidden; overflow: hidden;
gap: 10px; gap: 10px;
} }
.hidden { .email-item {
transition: all 1s ease; margin-top: 0;
margin-top: -26px; opacity: 1;
opacity: 0; will-change: margin-top, opacity;
will-change: margin-top, opacity; }
.hidden {
transition: all .5s ease;
margin-bottom: -26px;
opacity: 0;
} }

View File

@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next'
import Title from '../Title' import Title from '../Title'
import routes from '@/routes' import routes from '@/routes'
import styles from './styles.module.css' import styles from './styles.module.css'
import { useCallback } from 'react' import { MouseEventHandler, TouchEventHandler, useCallback } from 'react'
import { useDispatch } from 'react-redux' import { useDispatch } from 'react-redux'
import { actions } from '@/store' import { actions } from '@/store'
@ -19,15 +19,27 @@ function FreePeriodInfoPage(): JSX.Element {
} }
})) }))
}, [dispatch]) }, [dispatch])
const handleNext = (e: any) => {
const X = e.clientX || Math.round(e.touches[0].clientX) const handleNext = () => {
const Y = e.clientY || Math.round(e.touches[0].clientY)
updateCoordinates(X, Y)
navigate(routes.client.createProfile()) navigate(routes.client.createProfile())
} }
const mouseDown: MouseEventHandler<HTMLElement> = (e) => {
const X = e.clientX
const Y = e.clientY
updateCoordinates(X, Y)
handleNext()
}
const touchStart: TouchEventHandler<HTMLElement> = (e) => {
const X = e.touches[0].clientX
const Y = e.touches[0].clientY
updateCoordinates(X, Y)
handleNext()
}
return ( return (
<section className={`${styles.page} page`} onMouseDown={handleNext} onTouchStart={handleNext}> <section className={`${styles.page} page`} onMouseDown={mouseDown} onTouchStart={touchStart}>
<div className={styles.content}> <div className={styles.content}>
<Title variant='h4' className={styles.title}>{t('touch_screen')}</Title> <Title variant='h4' className={styles.title}>{t('touch_screen')}</Title>
</div> </div>

View File

@ -22,15 +22,21 @@ const removeAfterDot = (value: string): string => {
} }
interface PriceItemProps { interface PriceItemProps {
click: () => void active: boolean
click: () => void
} }
function PriceItem({ id, value, click }: IPrice & PriceItemProps): JSX.Element { function PriceItem({ id, value, active, click }: IPrice & PriceItemProps): JSX.Element {
console.log(id);
const _price = new Price(roundToWhole(value), currency, locale) const _price = new Price(roundToWhole(value), currency, locale)
const compatClassName = () => {
const isPopular = id === 3
const isActive = active
return `${styles.container} ${isPopular ? styles.popular : ''} ${isActive ? styles.active : ''}`
}
return ( return (
<div onClick={click} className={`${styles.container} ${value === 9 ? styles.popular : ''}`}> <div onClick={click} className={compatClassName()}>
{removeAfterDot(_price.format())} {removeAfterDot(_price.format())}
</div> </div>
) )

View File

@ -11,6 +11,12 @@
font-weight: 600; font-weight: 600;
} }
.active {
background-color: #30bf52;
border-color: #30bf52;
color: #fff;
}
.popular { .popular {
border-color: #30bf52; border-color: #30bf52;
} }

View File

@ -26,16 +26,16 @@ const prices: IPrice[] = [
] ]
interface PriceListProps { interface PriceListProps {
click: () => void activeItem: number | null
click: () => void
} }
function PriceList({click}: PriceListProps): JSX.Element { function PriceList({click, activeItem}: PriceListProps): JSX.Element {
return ( return (
<div className={`${styles.container}`}> <div className={`${styles.container}`}>
{prices.map((price, idx) => ( {prices.map((price, idx) => (
<PriceItem key={idx} value={price.value} id={price.id} click={click} /> <PriceItem active={price.id === activeItem} key={idx} value={price.value} id={price.id} click={click} />
))} ))}
</div> </div>
) )

View File

@ -12,11 +12,12 @@ import PriceList from '../PriceList'
function PriceListPage(): JSX.Element { function PriceListPage(): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate() const navigate = useNavigate()
const selectedPrice = useSelector(selectors.selectSelectedPrice)
const email = useSelector(selectors.selectEmail) const email = useSelector(selectors.selectEmail)
const handleNext = () => { const handleNext = () => {
navigate(routes.client.breath()) navigate(routes.client.breath())
} }
return ( return (
<> <>
@ -28,7 +29,7 @@ function PriceListPage(): JSX.Element {
<EmailsList /> <EmailsList />
</div> </div>
<div className={styles['price-list-container']}> <div className={styles['price-list-container']}>
<PriceList click={handleNext} /> <PriceList activeItem={selectedPrice} click={handleNext} />
</div> </div>
</section> </section>
</> </>

View File

@ -21,7 +21,7 @@
.emails-list-container { .emails-list-container {
width: 40%; width: 40%;
min-width: 200px; min-width: 200px;
height: 130px; /* height: 136px; */
margin-top: 48px; margin-top: 48px;
} }

View File

@ -5,6 +5,9 @@ import routes from '@/routes'
import styles from './styles.module.css' import styles from './styles.module.css'
import ModalTop from '../ModalTop' import ModalTop from '../ModalTop'
import Header from '../Header' import Header from '../Header'
import { useCallback } from 'react'
import { useDispatch } from 'react-redux'
import { actions } from '@/store'
interface ModalTopProps { interface ModalTopProps {
open: boolean open: boolean
@ -14,7 +17,14 @@ interface ModalTopProps {
function SpecialWelcomeOffer({ open, onClose }: ModalTopProps): JSX.Element { function SpecialWelcomeOffer({ open, onClose }: ModalTopProps): JSX.Element {
const { t } = useTranslation() const { t } = useTranslation()
const navigate = useNavigate() const navigate = useNavigate()
const dispatch = useDispatch()
const updateSelectedPrice = useCallback((selectedPrice: number | null) => {
dispatch(actions.payment.update({
selectedPrice
}))
}, [dispatch])
const handleNext = () => { const handleNext = () => {
updateSelectedPrice(1)
navigate(routes.client.emailEnter()) navigate(routes.client.emailEnter())
} }

View File

@ -3,13 +3,15 @@ import token, { actions as tokenActions, selectToken } from './token'
import user, { actions as userActions, selectUser } from './user' import user, { actions as userActions, selectUser } from './user'
import form, { actions as formActions, selectors as formSelectors } from './form' import form, { actions as formActions, selectors as formSelectors } from './form'
import aura, { actions as auraActions } from './aura' import aura, { actions as auraActions } from './aura'
import payment, { actions as paymentActions } from './payment'
import subscriptionPlans, { actions as subscriptionPlasActions, selectPlanById } from './subscriptionPlan' import subscriptionPlans, { actions as subscriptionPlasActions, selectPlanById } from './subscriptionPlan'
import status, { actions as userStatusActions, selectStatus } from './status' import status, { actions as userStatusActions, selectStatus } from './status'
import { loadStore, backupStore } from './storageHelper' import { loadStore, backupStore } from './storageHelper'
import { selectAuraCoordinates } from './aura' import { selectAuraCoordinates } from './aura'
import { selectSelectedPrice } from './payment'
const preloadedState = loadStore() const preloadedState = loadStore()
export const reducer = combineReducers({ token, user, form, status, subscriptionPlans, aura }) export const reducer = combineReducers({ token, user, form, status, subscriptionPlans, aura, payment })
export const actions = { export const actions = {
token: tokenActions, token: tokenActions,
user: userActions, user: userActions,
@ -17,6 +19,7 @@ export const actions = {
status: userStatusActions, status: userStatusActions,
subscriptionPlan: subscriptionPlasActions, subscriptionPlan: subscriptionPlasActions,
aura: auraActions, aura: auraActions,
payment: paymentActions,
reset: createAction('reset'), reset: createAction('reset'),
} }
export const selectors = { export const selectors = {
@ -25,6 +28,7 @@ export const selectors = {
selectStatus, selectStatus,
selectPlanById, selectPlanById,
selectAuraCoordinates, selectAuraCoordinates,
selectSelectedPrice,
...formSelectors, ...formSelectors,
} }
export type RootState = ReturnType<typeof reducer> export type RootState = ReturnType<typeof reducer>

28
src/store/payment.ts Normal file
View File

@ -0,0 +1,28 @@
import { createSlice, createSelector } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
interface IPayment {
selectedPrice: number | null
}
const initialState: IPayment = {
selectedPrice: null
}
const paymentSlice = createSlice({
name: 'payment',
initialState,
reducers: {
update(state, action: PayloadAction<Partial<IPayment>>) {
return { ...state, ...action.payload }
},
},
extraReducers: (builder) => builder.addCase('reset', () => initialState),
})
export const { actions } = paymentSlice
export const selectSelectedPrice = createSelector(
(state: { payment: IPayment }) => state.payment.selectedPrice,
(payment) => payment
)
export default paymentSlice.reducer