feat: add price list page
This commit is contained in:
parent
8d99442111
commit
062ba47790
16
package-lock.json
generated
16
package-lock.json
generated
@ -18,7 +18,8 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^12.3.1",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-router-dom": "^6.11.2"
|
||||
"react-router-dom": "^6.11.2",
|
||||
"unique-names-generator": "^4.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chargebee/chargebee-js-types": "^1.0.1",
|
||||
@ -3242,6 +3243,14 @@
|
||||
"node": ">=12.20"
|
||||
}
|
||||
},
|
||||
"node_modules/unique-names-generator": {
|
||||
"version": "4.7.1",
|
||||
"resolved": "https://registry.npmjs.org/unique-names-generator/-/unique-names-generator-4.7.1.tgz",
|
||||
"integrity": "sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow==",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
|
||||
@ -5566,6 +5575,11 @@
|
||||
"integrity": "sha512-cW9T5W9xY37cc+jfEnaUvX91foxtHkza3Nw3wkoF4sSlKn0MONdkdEndig/qPBWXNkmplh3NzayQzCiHM4/hqw==",
|
||||
"dev": true
|
||||
},
|
||||
"unique-names-generator": {
|
||||
"version": "4.7.1",
|
||||
"resolved": "https://registry.npmjs.org/unique-names-generator/-/unique-names-generator-4.7.1.tgz",
|
||||
"integrity": "sha512-lMx9dX+KRmG8sq6gulYYpKWZc9RlGsgBR6aoO8Qsm3qvkSJ+3rAymr+TnV8EDMrIrwuFJ4kruzMWM/OpYzPoow=="
|
||||
},
|
||||
"update-browserslist-db": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz",
|
||||
|
||||
@ -20,7 +20,8 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^12.3.1",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-router-dom": "^6.11.2"
|
||||
"react-router-dom": "^6.11.2",
|
||||
"unique-names-generator": "^4.7.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@chargebee/chargebee-js-types": "^1.0.1",
|
||||
|
||||
BIN
public/check-mark-1.png
Normal file
BIN
public/check-mark-1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.6 KiB |
@ -5,7 +5,7 @@ import {
|
||||
import { useAuth } from '@/auth'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { selectors } from '@/store'
|
||||
import routes, { hasNavigation, getRouteBy, hasNoFooter } from '@/routes'
|
||||
import routes, { hasNavigation, getRouteBy, hasNoFooter, hasNoHeader } from '@/routes'
|
||||
import BirthdayPage from '../BirthdayPage'
|
||||
import BirthtimePage from '../BirthtimePage'
|
||||
import CreateProfilePage from '../CreateProfilePage'
|
||||
@ -23,6 +23,9 @@ import DidYouKnowPage from '../DidYouKnowPage'
|
||||
import FreePeriodInfoPage from '../FreePeriodInfoPage'
|
||||
import AttentionPage from '../AttentionPage'
|
||||
import FeedbackPage from '../FeedbackPage'
|
||||
import CompatibilityPage from '../Compatibility'
|
||||
import BreathPage from '../BreathPage'
|
||||
import PriceListPage from '../PriceListPage'
|
||||
|
||||
function App(): JSX.Element {
|
||||
const [isSpecialOfferOpen, setIsSpecialOfferOpen] = useState<boolean>(false)
|
||||
@ -46,6 +49,9 @@ function App(): JSX.Element {
|
||||
<Route path={routes.client.createProfile()} element={<SkipStep />} />
|
||||
<Route path={routes.client.emailEnter()} element={<EmailEnterPage />} />
|
||||
<Route path={routes.client.static()} element={<StaticPage />} />
|
||||
<Route path={routes.client.compatibility()} element={<CompatibilityPage />} />
|
||||
<Route path={routes.client.breath()} element={<BreathPage />} />
|
||||
<Route path={routes.client.priceList()} element={<PriceListPage />} />
|
||||
<Route element={<PrivateOutlet />}>
|
||||
<Route path={routes.client.subscription()} element={<SubscriptionPage />} />
|
||||
<Route path={routes.client.paymentMethod()} element={<PaymentPage />} />
|
||||
@ -65,11 +71,12 @@ function Layout({ setIsSpecialOfferOpen }: LayoutProps): JSX.Element {
|
||||
const location = useLocation()
|
||||
const showNavbar = hasNavigation(location.pathname)
|
||||
const showFooter = hasNoFooter(location.pathname)
|
||||
const showHeader = hasNoHeader(location.pathname)
|
||||
const [isMenuOpen, setIsMenuOpen] = useState<boolean>(false)
|
||||
const changeIsSpecialOfferOpen = () => setIsSpecialOfferOpen(true)
|
||||
return (
|
||||
<div className='container'>
|
||||
<Header openMenu={() => setIsMenuOpen(true)} clickCross={changeIsSpecialOfferOpen}/>
|
||||
{ showHeader ? <Header openMenu={() => setIsMenuOpen(true)} clickCross={changeIsSpecialOfferOpen}/> : null }
|
||||
<main className='content'><Outlet /></main>
|
||||
{ showFooter ? <Footer color={showNavbar ? 'black' : 'white'}/> : null }
|
||||
{ showNavbar ? <Navbar isOpen={isMenuOpen} closeMenu={() => setIsMenuOpen(false)} /> : null}
|
||||
|
||||
39
src/components/PriceItem/index.tsx
Normal file
39
src/components/PriceItem/index.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import { Currency, Locale, Price } from '../PaymentTable'
|
||||
import { IPrice } from '../PriceList'
|
||||
import styles from './styles.module.css'
|
||||
|
||||
const currency = Currency.USD
|
||||
const locale = Locale.EN
|
||||
|
||||
const roundToWhole = (value: string | number): number => {
|
||||
value = Number(value)
|
||||
if (value % Math.floor(value) !== 0) {
|
||||
return value
|
||||
}
|
||||
return Math.floor(value)
|
||||
}
|
||||
|
||||
const removeAfterDot = (value: string): string => {
|
||||
const _value = Number(value.split('$')[1])
|
||||
if (_value % Math.floor(_value) !== 0 && _value !== 0) {
|
||||
return value
|
||||
}
|
||||
return value.split('.')[0]
|
||||
}
|
||||
|
||||
interface PriceItemProps {
|
||||
click: () => void
|
||||
}
|
||||
|
||||
function PriceItem({ id, value, click }: IPrice & PriceItemProps): JSX.Element {
|
||||
console.log(id);
|
||||
const _price = new Price(roundToWhole(value), currency, locale)
|
||||
|
||||
return (
|
||||
<div onClick={click} className={`${styles.container} ${value === 9 ? styles.popular : ''}`}>
|
||||
{removeAfterDot(_price.format())}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PriceItem
|
||||
33
src/components/PriceItem/styles.module.css
Normal file
33
src/components/PriceItem/styles.module.css
Normal file
@ -0,0 +1,33 @@
|
||||
.container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 10px;
|
||||
border: solid #d6d2d2 2px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.popular {
|
||||
border-color: #30bf52;
|
||||
}
|
||||
|
||||
.popular:after {
|
||||
content: 'Most Popular';
|
||||
position: absolute;
|
||||
width: 80px;
|
||||
height: 20px;
|
||||
left: -10px;
|
||||
bottom: -25px;
|
||||
background-color: #30bf52;
|
||||
font-size: 9px;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 10px;
|
||||
}
|
||||
44
src/components/PriceList/index.tsx
Normal file
44
src/components/PriceList/index.tsx
Normal file
@ -0,0 +1,44 @@
|
||||
import PriceItem from '../PriceItem'
|
||||
import styles from './styles.module.css'
|
||||
|
||||
export interface IPrice {
|
||||
id: number
|
||||
value: number
|
||||
}
|
||||
|
||||
const prices: IPrice[] = [
|
||||
{
|
||||
id: 1,
|
||||
value: 0
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
value: 5
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
value: 9
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
value: 13.67
|
||||
},
|
||||
]
|
||||
|
||||
interface PriceListProps {
|
||||
click: () => void
|
||||
}
|
||||
|
||||
function PriceList({click}: PriceListProps): JSX.Element {
|
||||
|
||||
|
||||
return (
|
||||
<div className={`${styles.container}`}>
|
||||
{prices.map((price, idx) => (
|
||||
<PriceItem key={idx} value={price.value} id={price.id} click={click} />
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default PriceList
|
||||
5
src/components/PriceList/styles.module.css
Normal file
5
src/components/PriceList/styles.module.css
Normal file
@ -0,0 +1,5 @@
|
||||
.container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
38
src/components/PriceListPage/index.tsx
Normal file
38
src/components/PriceListPage/index.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import routes from '@/routes'
|
||||
import styles from './styles.module.css'
|
||||
import UserHeader from '../UserHeader'
|
||||
import { useSelector } from 'react-redux'
|
||||
import { selectors } from '@/store'
|
||||
import Title from '../Title'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import EmailsList from '../EmailsList'
|
||||
import PriceList from '../PriceList'
|
||||
|
||||
function PriceListPage(): JSX.Element {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
const email = useSelector(selectors.selectEmail)
|
||||
const handleNext = () => {
|
||||
navigate(routes.client.breath())
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<UserHeader email={email} />
|
||||
<section className={`${styles.page} page`}>
|
||||
<Title className={styles.title} variant='h2'>{t('choose_your_own_fee')}</Title>
|
||||
<p className={styles.slogan}>{t('should_not_get', { strongText: <strong>{t('money')}</strong> })}</p>
|
||||
<div className={styles['emails-list-container']}>
|
||||
<EmailsList />
|
||||
</div>
|
||||
<div className={styles['price-list-container']}>
|
||||
<PriceList click={handleNext} />
|
||||
</div>
|
||||
</section>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default PriceListPage
|
||||
31
src/components/PriceListPage/styles.module.css
Normal file
31
src/components/PriceListPage/styles.module.css
Normal file
@ -0,0 +1,31 @@
|
||||
.page {
|
||||
position: relative;
|
||||
height: calc(100vh - 81px);
|
||||
max-height: -webkit-fill-available;
|
||||
flex: auto;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.slogan {
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
max-width: 250px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.emails-list-container {
|
||||
width: 40%;
|
||||
min-width: 200px;
|
||||
height: 130px;
|
||||
margin-top: 48px;
|
||||
}
|
||||
|
||||
.price-list-container {
|
||||
width: 100%;
|
||||
margin-top: 32px;
|
||||
}
|
||||
@ -28,6 +28,12 @@ button,h4 {
|
||||
|
||||
button {
|
||||
line-height: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
a,abbr,acronym,address,applet,article,aside,audio,b,big,blockquote,body,canvas,caption,center,cite,code,dd,del,details,dfn,div,dl,dt,em,embed,fieldset,figcaption,figure,footer,form,h1,h2,h3,h4,h5,h6,header,hgroup,html,i,iframe,img,ins,kbd,label,legend,li,mark,menu,nav,object,ol,output,p,pre,q,ruby,s,samp,section,small,span,strike,strong,sub,summary,sup,table,tbody,td,tfoot,th,thead,time,tr,tt,u,ul,var,video {
|
||||
@ -122,7 +128,3 @@ a,button,div,input,select,textarea {
|
||||
.CircularProgressbar {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
@ -65,5 +65,16 @@ export default {
|
||||
get_100_off: "Get 100% off - Today only",
|
||||
special_welcome_offer: "Special welcome offer!",
|
||||
get_discount: "Get discount",
|
||||
compatibility: "Compatibility",
|
||||
iAm: "I am",
|
||||
name: "Name",
|
||||
check: "Check",
|
||||
breathIn: "Breathe in",
|
||||
breathOut: "Breathe out",
|
||||
hold: "Hold",
|
||||
choose_your_own_fee: "Choose your own fee",
|
||||
should_not_get: "<strongText> shouldn't get in the way of finding something for you",
|
||||
money: "Money",
|
||||
people_joined_today: "<countPeoples> people joined today",
|
||||
},
|
||||
}
|
||||
|
||||
@ -20,6 +20,9 @@ const routes = {
|
||||
wallpaper: () => [host, 'wallpaper'].join('/'),
|
||||
static: () => [host, 'static', ':typeId'].join('/'),
|
||||
legal: (type: string) => [host, 'static', type].join('/'),
|
||||
compatibility: () => [host, 'compatibility'].join('/'),
|
||||
breath: () => [host, 'breath'].join('/'),
|
||||
priceList: () => [host, 'price-list'].join('/'),
|
||||
},
|
||||
server: {
|
||||
user: () => [apiHost, prefix, 'user.json'].join('/'),
|
||||
@ -37,6 +40,7 @@ const routes = {
|
||||
subscriptionStatus: () => [apiHost, prefix, 'user', 'subscription_receipts', 'status.json'].join('/'),
|
||||
subscriptionReceipts: () => [apiHost, prefix, 'user', 'subscription_receipts.json'].join('/'),
|
||||
subscriptionReceipt: (id: string) => [apiHost, prefix, 'user', 'subscription_receipts', `${id}.json`].join('/'),
|
||||
compatCategories: () => [apiHost, prefix, 'ai', 'compat_categories.json'].join('/'),
|
||||
},
|
||||
}
|
||||
|
||||
@ -48,6 +52,7 @@ export const entrypoints = [
|
||||
routes.client.didYouKnow(),
|
||||
routes.client.attention(),
|
||||
routes.client.feedback(),
|
||||
routes.client.breath(),
|
||||
]
|
||||
export const isEntrypoint = (path: string) => entrypoints.includes(path)
|
||||
export const isNotEntrypoint = (path: string) => !isEntrypoint(path)
|
||||
@ -64,9 +69,17 @@ export const withoutFooterRoutes = [
|
||||
routes.client.createProfile(),
|
||||
routes.client.attention(),
|
||||
routes.client.feedback(),
|
||||
routes.client.compatibility(),
|
||||
routes.client.breath(),
|
||||
routes.client.priceList(),
|
||||
]
|
||||
export const hasNoFooter = (path: string) => !withoutFooterRoutes.includes(path)
|
||||
|
||||
export const withoutHeaderRoutes = [
|
||||
routes.client.compatibility(),
|
||||
]
|
||||
export const hasNoHeader = (path: string) => !withoutHeaderRoutes.includes(path)
|
||||
|
||||
export const getRouteBy = (status: UserStatus): string => {
|
||||
switch (status) {
|
||||
case 'lead':
|
||||
|
||||
13
src/services/random-value/index.ts
Normal file
13
src/services/random-value/index.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { uniqueNamesGenerator, Config, names } from "unique-names-generator";
|
||||
|
||||
export const getRandomName = (): string => {
|
||||
const config: Config = {
|
||||
dictionaries: [names],
|
||||
};
|
||||
return uniqueNamesGenerator(config);
|
||||
};
|
||||
|
||||
// The maximum is exclusive and the minimum is inclusive
|
||||
export const getRandomArbitrary = (min: number, max: number) => {
|
||||
return Math.floor(Math.random() * (max - min) + min);
|
||||
};
|
||||
@ -11,7 +11,8 @@ export interface FormField<T> {
|
||||
value: T
|
||||
label?: string | null
|
||||
placeholder?: string | null
|
||||
onValid: (value: T) => void
|
||||
inputClassName?: string
|
||||
onValid: (value: string) => void
|
||||
onInvalid: () => void
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user