feat: add price list page

This commit is contained in:
gofnnp 2023-08-25 03:57:54 +04:00
parent 8d99442111
commit 062ba47790
15 changed files with 261 additions and 9 deletions

16
package-lock.json generated
View File

@ -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",

View File

@ -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

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -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}

View 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

View 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;
}

View 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

View File

@ -0,0 +1,5 @@
.container {
display: flex;
flex-direction: row;
justify-content: space-between;
}

View 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

View 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;
}

View File

@ -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;
}

View File

@ -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",
},
}

View File

@ -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':

View 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);
};

View File

@ -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
}