feat: add special welcome offer page
This commit is contained in:
parent
9ce9a90031
commit
1832036710
@ -1,6 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Routes, Route, Navigate, Outlet, useLocation
|
||||
Routes, Route, Navigate, Outlet, useLocation, useNavigate
|
||||
} from 'react-router-dom'
|
||||
import { useAuth } from '../../auth'
|
||||
import { useSelector } from 'react-redux'
|
||||
@ -25,14 +25,22 @@ import AttentionPage from '../AttentionPage'
|
||||
import FeedbackPage from '../FeedbackPage'
|
||||
|
||||
function App(): JSX.Element {
|
||||
const [isSpecialOfferOpen, setIsSpecialOfferOpen] = useState<boolean>(false)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const closeSpecialOffer = () => {
|
||||
setIsSpecialOfferOpen(false)
|
||||
navigate(routes.client.emailEnter())
|
||||
}
|
||||
|
||||
return (
|
||||
<Routes>
|
||||
<Route element={<Layout />}>
|
||||
<Route element={<Layout setIsSpecialOfferOpen={setIsSpecialOfferOpen} />}>
|
||||
<Route path={routes.client.root()} element={<MainPage />} />
|
||||
<Route path={routes.client.birthday()} element={<BirthdayPage />} />
|
||||
<Route path={routes.client.didYouKnow()} element={<DidYouKnowPage />} />
|
||||
<Route path={routes.client.freePeriodInfo()} element={<FreePeriodInfoPage />} />
|
||||
<Route path={routes.client.attention()} element={<AttentionPage />} />
|
||||
<Route path={routes.client.attention()} element={<AttentionPage isOpenModal={isSpecialOfferOpen} onCloseSpecialOffer={closeSpecialOffer} />} />
|
||||
<Route path={routes.client.feedback()} element={<FeedbackPage />} />
|
||||
<Route path={routes.client.birthtime()} element={<BirthtimePage />} />
|
||||
<Route path={routes.client.createProfile()} element={<SkipStep />} />
|
||||
@ -49,14 +57,19 @@ function App(): JSX.Element {
|
||||
)
|
||||
}
|
||||
|
||||
function Layout(): JSX.Element {
|
||||
interface LayoutProps {
|
||||
setIsSpecialOfferOpen: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}
|
||||
|
||||
function Layout({ setIsSpecialOfferOpen }: LayoutProps): JSX.Element {
|
||||
const location = useLocation()
|
||||
const showNavbar = hasNavigation(location.pathname)
|
||||
const showFooter = hasNoFooter(location.pathname)
|
||||
const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false)
|
||||
const changeIsSpecialOfferOpen = () => setIsSpecialOfferOpen(true)
|
||||
return (
|
||||
<div className='container'>
|
||||
<Header openMenu={() => setIsMenuOpen(true)}/>
|
||||
<Header openMenu={() => setIsMenuOpen(true)} clickCross={changeIsSpecialOfferOpen}/>
|
||||
<main className='content'><Outlet /></main>
|
||||
{ showFooter ? <Footer color={showNavbar ? 'black' : 'white'}/> : null }
|
||||
{ showNavbar ? <Navbar isOpen={isMenuOpen} closeMenu={() => setIsMenuOpen(false)} /> : null}
|
||||
|
||||
@ -4,8 +4,14 @@ import Title from '../Title'
|
||||
import routes from '../../routes'
|
||||
import styles from './styles.module.css'
|
||||
import CheckboxWithText from '../CheckboxWithText'
|
||||
import SpecialWelcomeOffer from '../SpecialWelcomeOffer'
|
||||
|
||||
function AttentionPage(): JSX.Element {
|
||||
interface AttentionPageProps {
|
||||
isOpenModal: boolean
|
||||
onCloseSpecialOffer?: () => void
|
||||
}
|
||||
|
||||
function AttentionPage({ isOpenModal, onCloseSpecialOffer }: AttentionPageProps): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const handleNext = () => navigate(routes.client.feedback())
|
||||
@ -18,6 +24,7 @@ function AttentionPage(): JSX.Element {
|
||||
|
||||
return (
|
||||
<section className={`${styles.page} page`}>
|
||||
<SpecialWelcomeOffer open={isOpenModal} onClose={onCloseSpecialOffer} />
|
||||
<img className={styles.icon} src="/stop-icon.png" alt="stop" />
|
||||
<Title variant='h2'>{t('attention')}</Title>
|
||||
<p className={styles.text}>{t('attention_page_text')}</p>
|
||||
|
||||
@ -7,17 +7,24 @@ import styles from './styles.module.css'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { selectors } from '../../store'
|
||||
import { getZodiacSignByDate } from '../../zodiac-sign'
|
||||
import SpecialWelcomeOffer from '../SpecialWelcomeOffer'
|
||||
import { useState } from 'react'
|
||||
|
||||
function DidYouKnowPage(): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const handleNext = () => navigate(routes.client.freePeriodInfo())
|
||||
const [isOpenModal, setIsOpenModal] = useState(false)
|
||||
const handleSpecialOffer = () => {
|
||||
setIsOpenModal(true)
|
||||
}
|
||||
const birthdate = useSelector(selectors.selectBirthdate)
|
||||
const zodiacSign = getZodiacSignByDate(birthdate)
|
||||
|
||||
|
||||
return (
|
||||
<section className={`${styles.page} page`}>
|
||||
<SpecialWelcomeOffer open={isOpenModal} />
|
||||
<div className={styles.content}>
|
||||
<Title variant='h1'>{t('did_you_know')}</Title>
|
||||
<p className={styles.zodiacInfo}>
|
||||
@ -28,7 +35,7 @@ function DidYouKnowPage(): JSX.Element {
|
||||
<MainButton onClick={handleNext}>
|
||||
{t('learn_about_my_energy')}
|
||||
</MainButton>
|
||||
<span className={styles.skip}>{t('skip_for_now')}</span>
|
||||
<span className={styles.skip} onClick={handleSpecialOffer}>{t('skip_for_now')}</span>
|
||||
</footer>
|
||||
</section>
|
||||
)
|
||||
|
||||
@ -1,17 +1,20 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useNavigate, useLocation } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import routes, { hasNavigation, isNotEntrypoint } from '../../routes'
|
||||
import routes, { hasCrossButton, hasNavigation, isNotEntrypoint } from '../../routes'
|
||||
import BackButton from './BackButton'
|
||||
import iconUrl from './icon.png'
|
||||
import menuUrl from './menu.png'
|
||||
import './styles.css'
|
||||
import styles from './styles.module.css'
|
||||
|
||||
type HeaderProps = {
|
||||
openMenu: () => void
|
||||
openMenu?: () => void
|
||||
showBack?: boolean
|
||||
showCross?: boolean
|
||||
clickCross?: () => void
|
||||
}
|
||||
|
||||
function Header({ openMenu }: HeaderProps): JSX.Element {
|
||||
function Header({ openMenu, showBack, showCross, clickCross = () => {undefined}, ...props }: HeaderProps & React.HTMLAttributes<HTMLDivElement>): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
@ -19,6 +22,7 @@ function Header({ openMenu }: HeaderProps): JSX.Element {
|
||||
const [isNavigated, setIsNavigated] = useState<boolean>(false);
|
||||
const showBackButton = isNotEntrypoint(location.pathname)
|
||||
const showMenuButton = hasNavigation(location.pathname)
|
||||
const showCrossButton = hasCrossButton(location.pathname)
|
||||
|
||||
useEffect(() => {
|
||||
if (!initialPath) {
|
||||
@ -38,11 +42,12 @@ function Header({ openMenu }: HeaderProps): JSX.Element {
|
||||
}
|
||||
|
||||
return (
|
||||
<header className="header">
|
||||
{ showBackButton ? <BackButton className="pa" onClick={goBack} /> : null }
|
||||
<header className={styles.header} {...props}>
|
||||
{ (showBackButton || showBack) ? <BackButton className='pa' onClick={goBack} /> : null }
|
||||
<img src={iconUrl} alt="logo" width="40" height="40" />
|
||||
<span className="header__title">{t('app_name')}</span>
|
||||
{showMenuButton ? <div className="header__menu-btn" onClick={openMenu}>
|
||||
<span className={styles["header__title"]}>{t('app_name')}</span>
|
||||
{(showCrossButton || showCross) ? <img className={styles.cross} src="/cross.png" alt="Cross" onClick={clickCross} /> : null}
|
||||
{showMenuButton ? <div className={styles["header__menu-btn"]} onClick={openMenu}>
|
||||
<img src={menuUrl} alt="menu" width="40" height="40" />
|
||||
</div> : null}
|
||||
</header>
|
||||
|
||||
@ -23,11 +23,16 @@
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.header__menu-btn {
|
||||
.header__menu-btn, .cross {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
top: 33%;
|
||||
right: 28px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.cross {
|
||||
height: 33%;
|
||||
width: auto;
|
||||
}
|
||||
26
src/components/ModalTop/index.tsx
Normal file
26
src/components/ModalTop/index.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { ReactNode } from 'react'
|
||||
import styles from './styles.module.css'
|
||||
import Header from '../Header'
|
||||
|
||||
interface ModalProps {
|
||||
children: ReactNode
|
||||
open?: boolean
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
function ModalTop({ open, children, onClose }: ModalProps): JSX.Element {
|
||||
const handleClose = (event: React.MouseEvent) => {
|
||||
if (event.target !== event.currentTarget) return
|
||||
onClose?.()
|
||||
}
|
||||
if (!open) return <></>
|
||||
return (
|
||||
<div className={styles.modal} onClick={handleClose}>
|
||||
<div className={styles['modal-content']}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ModalTop
|
||||
44
src/components/ModalTop/styles.module.css
Normal file
44
src/components/ModalTop/styles.module.css
Normal file
@ -0,0 +1,44 @@
|
||||
.modal {
|
||||
background: rgb(14 14 14 / 80%);
|
||||
backdrop-filter: blur(6px);
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
width: 100vw;
|
||||
z-index: 2000;
|
||||
animation: appearanceBackground .6s ease-in;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border-radius: 0 0 16px 16px;
|
||||
background-color: #fff;
|
||||
animation: appearance 0.6s ease-in;
|
||||
}
|
||||
|
||||
header {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
@keyframes appearance {
|
||||
0% {
|
||||
margin-top: -100%;
|
||||
}
|
||||
|
||||
100% {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes appearanceBackground {
|
||||
0% {
|
||||
background: transparent;
|
||||
backdrop-filter: blur(0px);
|
||||
}
|
||||
|
||||
100% {
|
||||
background: rgb(14 14 14 / 80%);
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
}
|
||||
42
src/components/SpecialWelcomeOffer/index.tsx
Normal file
42
src/components/SpecialWelcomeOffer/index.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Title from '../Title'
|
||||
import routes from '../../routes'
|
||||
import styles from './styles.module.css'
|
||||
import ModalTop from '../ModalTop'
|
||||
import Header from '../Header'
|
||||
|
||||
interface ModalTopProps {
|
||||
open: boolean
|
||||
onClose?: () => void
|
||||
}
|
||||
|
||||
function SpecialWelcomeOffer({ open, onClose }: ModalTopProps): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const handleNext = () => {
|
||||
navigate(routes.client.emailEnter())
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
{open ? (
|
||||
<ModalTop open={open} onClose={onClose || handleNext}>
|
||||
<Header showBack={true} showCross={true} clickCross={onClose || handleNext} />
|
||||
<div className={styles.content}>
|
||||
<span className={styles['welcome-offer']}>{t('special_welcome_offer')}</span>
|
||||
<Title variant='h1'>{t('get_100_off')}</Title>
|
||||
<div className={styles['discount-container']}>
|
||||
<span className={styles['red-price']}>$9.99</span>
|
||||
<span className={styles['price']}>$0.00</span>
|
||||
</div>
|
||||
<button className={styles.button} onClick={onClose || handleNext}>{t('get_discount')}</button>
|
||||
</div>
|
||||
</ModalTop>
|
||||
): null}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SpecialWelcomeOffer
|
||||
40
src/components/SpecialWelcomeOffer/styles.module.css
Normal file
40
src/components/SpecialWelcomeOffer/styles.module.css
Normal file
@ -0,0 +1,40 @@
|
||||
|
||||
|
||||
.content {
|
||||
padding: 0 24px 64px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding-top: 28px;
|
||||
}
|
||||
|
||||
.welcome-offer {
|
||||
color: #717171;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.discount-container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 16px;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.red-price {
|
||||
color: red;
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: #78d180;
|
||||
color: #fff;
|
||||
border-radius: 26px;
|
||||
width: 100%;
|
||||
max-width: 300px;
|
||||
padding: 12px 0;
|
||||
border: none;
|
||||
font-size: 14px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
@ -62,5 +62,8 @@ export default {
|
||||
attention_page_text: "There are significant deviations in your energy, this is a fairly rare occurrence and few people have encountered something like this. Therefore, we cannot provide you with a full report as you may not be prepared for this.",
|
||||
not_ready_for_information: "I understand that I may not be ready for this information.",
|
||||
feedback: "I couldn`t believe me eyes when I saw what was revealed about my energy; it was truly shocking. Everything that was mentioned really happened in my life, and I understood the reasons and especially who was behind it. Everything was very beneficial and spot on, without any excess or unnecessary details. Thanks for the assistance; this analysis greatly helped me in my life. I recommend it to everyone. Special thanks to Aura!",
|
||||
get_100_off: "Get 100% off - Today only",
|
||||
special_welcome_offer: "Special welcome offer!",
|
||||
get_discount: "Get discount",
|
||||
},
|
||||
}
|
||||
|
||||
@ -55,6 +55,9 @@ export const withNavigationRoutes = [routes.client.wallpaper()]
|
||||
export const hasNavigation = (path: string) => withNavigationRoutes.includes(path)
|
||||
export const hasNoNavigation = (path: string) => !hasNavigation(path)
|
||||
|
||||
export const withCrossButtonRoutes = [routes.client.attention()]
|
||||
export const hasCrossButton = (path: string) => withCrossButtonRoutes.includes(path)
|
||||
|
||||
export const withoutFooterRoutes = [
|
||||
routes.client.didYouKnow(),
|
||||
routes.client.freePeriodInfo(),
|
||||
|
||||
@ -2,18 +2,21 @@ import { combineReducers, configureStore, createAction } from '@reduxjs/toolkit'
|
||||
import token, { actions as tokenActions, selectToken } from './token'
|
||||
import user, { actions as userActions, selectUser } from './user'
|
||||
import form, { actions as formActions, selectors as formSelectors } from './form'
|
||||
import aura, { actions as auraActions } from './aura'
|
||||
import subscriptionPlans, { actions as subscriptionPlasActions, selectPlanById } from './subscriptionPlan'
|
||||
import status, { actions as userStatusActions, selectStatus } from './status'
|
||||
import { loadStore, backupStore } from './storageHelper'
|
||||
import { selectAuraCoordinates } from './aura'
|
||||
|
||||
const preloadedState = loadStore()
|
||||
export const reducer = combineReducers({ token, user, form, status, subscriptionPlans })
|
||||
export const reducer = combineReducers({ token, user, form, status, subscriptionPlans, aura })
|
||||
export const actions = {
|
||||
token: tokenActions,
|
||||
user: userActions,
|
||||
form: formActions,
|
||||
status: userStatusActions,
|
||||
subscriptionPlan: subscriptionPlasActions,
|
||||
aura: auraActions,
|
||||
reset: createAction('reset'),
|
||||
}
|
||||
export const selectors = {
|
||||
@ -21,6 +24,7 @@ export const selectors = {
|
||||
selectUser,
|
||||
selectStatus,
|
||||
selectPlanById,
|
||||
selectAuraCoordinates,
|
||||
...formSelectors,
|
||||
}
|
||||
export type RootState = ReturnType<typeof reducer>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user