feat: add and change pages after questionnaire path

This commit is contained in:
Денис Катаев 2024-01-30 17:59:31 +00:00 committed by Victor Ershov
parent 69abb8277b
commit 415d9c4d7b
66 changed files with 2541 additions and 150 deletions

5
public/arrow.svg Executable file
View File

@ -0,0 +1,5 @@
<svg width="26" height="33" viewBox="0 0 26 33" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M0.954633 31.5021C0.67963 31.5271 0.477007 31.7704 0.502062 32.0454C0.527118 32.3204 0.770364 32.523 1.04537 32.4979L0.954633 31.5021ZM22.9227 0L20.1047 5.03907L25.8777 4.95999L22.9227 0ZM1.04537 32.4979C11.9046 31.5085 17.6137 27.369 20.5443 21.9577C23.4464 16.5992 23.5645 10.087 23.4843 4.49295L22.4844 4.50728C22.5649 10.1217 22.4271 16.3813 19.665 21.4815C16.9314 26.5289 11.568 30.5351 0.954633 31.5021L1.04537 32.4979Z"
fill="#343434"></path>
</svg>

After

Width:  |  Height:  |  Size: 581 B

6
public/check-mark-purple.svg Executable file
View File

@ -0,0 +1,6 @@
<svg width="11" height="8" viewBox="0 0 60 46" fill="#8e8cf0" xmlns="http://www.w3.org/2000/svg"
class="sc-87f4bcb5-2 jCrhhs">
<path
d="M19.5009 35.9989L5.5009 21.9989L0.834229 26.6655L19.5009 45.3322L59.5009 5.33219L54.8342 0.665527L19.5009 35.9989Z">
</path>
</svg>

After

Width:  |  Height:  |  Size: 287 B

11
public/darts-purple.svg Executable file
View File

@ -0,0 +1,11 @@
<svg width="38" height="38" viewBox="0 0 38 38" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="38" height="38" rx="8" fill="#9974F6"/>
<g clip-path="url(#clip0_649_4604)">
<path d="M19.0009 33.0628C15.2719 33.0609 11.6961 31.5789 9.05908 28.9422C6.42209 26.3055 4.93957 22.7299 4.93723 19.0009C4.9391 15.2716 6.42141 11.6955 9.05845 9.05848C11.6955 6.42144 15.2715 4.93913 19.0009 4.93725C21.3724 4.93975 23.7049 5.54162 25.7815 6.68695C27.8581 7.83228 29.6116 9.48391 30.879 11.4884C32.1464 13.4929 32.8866 15.7852 33.0308 18.1523C33.175 20.5195 32.7186 22.8847 31.7039 25.0282L30.3096 23.6339C30.1789 23.5022 30.0235 23.3977 29.8524 23.3262C29.6812 23.2548 29.4976 23.2178 29.3121 23.2175H28.9223C29.4769 21.9224 29.7834 20.4962 29.7834 19.0009C29.7815 16.1418 28.6449 13.4003 26.6232 11.3786C24.6015 9.35691 21.86 8.2203 19.0009 8.21842C13.0516 8.21842 8.21839 13.0516 8.21839 19.0009C8.22043 20.7818 8.66324 22.5345 9.50732 24.1026C10.3514 25.6707 11.5705 27.0055 13.0559 27.9879C14.5413 28.9703 16.2467 29.5698 18.0201 29.7329C19.7935 29.8959 21.5797 29.6175 23.2193 28.9224V29.3121C23.2193 29.686 23.3681 30.0456 23.6339 30.3096L25.0299 31.7057C23.1463 32.601 21.0865 33.0659 19.0009 33.0628ZM27.8948 33.0628C27.7747 33.0592 27.6606 33.0098 27.5759 32.9246L24.2947 29.6452C24.2508 29.6015 24.2161 29.5496 24.1923 29.4925C24.1686 29.4353 24.1564 29.374 24.1565 29.3121V26.2241L17.9662 20.0338C17.6936 19.7585 17.5406 19.3866 17.5406 18.9991C17.5406 18.6117 17.6936 18.2398 17.9662 17.9645C18.5332 17.3975 19.4668 17.3993 20.0338 17.968L26.2223 24.1565H29.3121C29.374 24.1565 29.4353 24.1687 29.4924 24.1924C29.5496 24.2161 29.6015 24.2509 29.6452 24.2947L32.9246 27.5759C32.9888 27.6417 33.0322 27.7249 33.0495 27.8152C33.0669 27.9055 33.0573 27.9989 33.022 28.0838C32.9867 28.1687 32.9273 28.2414 32.8511 28.2929C32.7749 28.3443 32.6852 28.3722 32.5933 28.3731H30.4407L30.8145 28.7487C31.087 29.0232 31.2402 29.3941 31.2408 29.7809C31.2415 30.1677 31.0896 30.5391 30.818 30.8145C30.5346 31.098 30.1572 31.2397 29.7816 31.2397C29.406 31.2397 29.0304 31.098 28.7469 30.8145L28.3749 30.4425V32.5969C28.3747 32.6593 28.362 32.721 28.3376 32.7785C28.3133 32.8359 28.2777 32.8879 28.233 32.9315C28.1883 32.975 28.1354 33.0092 28.0773 33.0321C28.0192 33.0549 27.9572 33.0642 27.8948 33.0628ZM19.0009 26.5022C14.864 26.5022 11.4978 23.136 11.4978 19.0009C11.4978 14.8658 14.864 11.5014 19.0009 11.5014C20.9892 11.5032 22.8954 12.294 24.3012 13.7001C25.7069 15.1062 26.4973 17.0126 26.4987 19.0009C26.4987 20.1933 26.2117 21.3201 25.7156 22.3228L23.1466 19.7539C23.2559 19.1466 23.2307 18.5228 23.0729 17.9262C22.9152 17.3297 22.6286 16.775 22.2334 16.3011C21.8382 15.8272 21.344 15.4457 20.7856 15.1833C20.2271 14.921 19.6179 14.7842 19.0009 14.7825C17.8828 14.7839 16.811 15.2286 16.0202 16.019C15.2295 16.8094 14.7844 17.8811 14.7825 18.9991C14.7839 20.1172 15.2285 21.1891 16.019 21.9798C16.8094 22.7706 17.8811 23.2157 18.9991 23.2175C19.2578 23.2175 19.5094 23.191 19.7539 23.1467L22.3228 25.7156C21.2914 26.2317 20.1542 26.501 19.0009 26.5022Z" fill="#FCFDFF"/>
</g>
<defs>
<clipPath id="clip0_649_4604">
<rect width="30" height="30" fill="white" transform="matrix(1 0 0 -1 4 34)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.2 KiB

6
public/guard.svg Executable file
View File

@ -0,0 +1,6 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"
class="sc-10a49ee2-30 fDNZxY">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M21.9032 4.36098C22.0287 4.41968 22.1406 4.50347 22.232 4.6071C22.4222 4.8205 22.52 5.09985 22.5041 5.38445C22.0405 14.7267 18.6677 20.5084 12.7479 23.8686C12.593 23.9553 12.4222 24 12.2524 24C12.0825 24 11.9117 23.9553 11.7578 23.8686C5.83795 20.5084 2.46417 14.7267 2.00151 5.38445C1.98647 5.09981 2.08453 4.82067 2.2746 4.6071C2.36629 4.50361 2.47841 4.41994 2.60401 4.36125C2.7296 4.30257 2.86599 4.27014 3.00472 4.26596C4.50945 4.22688 6.29315 3.09865 7.846 2.11642C8.5158 1.69275 9.14266 1.29625 9.68565 1.02604C10.7977 0.473312 11.5325 0.108869 12.0844 0.012864C12.1963 -0.0039698 12.3102 -0.00428381 12.4222 0.011932C12.974 0.107936 13.7088 0.47238 14.8219 1.02511C15.3644 1.29535 15.9906 1.69156 16.6597 2.11493C18.2129 3.09774 19.9975 4.22687 21.5028 4.26596C21.6414 4.26993 21.7777 4.30228 21.9032 4.36098ZM15.1657 8.76305H15.7482C16.0572 8.76305 16.3536 8.89071 16.5721 9.11795C16.7906 9.34519 16.9133 9.65339 16.9133 9.97476V16.0333C16.9133 16.3547 16.7906 16.6629 16.5721 16.8901C16.3536 17.1173 16.0572 17.245 15.7482 17.245H8.7576C8.44859 17.245 8.15224 17.1173 7.93374 16.8901C7.71525 16.6629 7.59249 16.3547 7.59249 16.0333V9.97476C7.59249 9.65339 7.71525 9.34519 7.93374 9.11795C8.15224 8.89071 8.44859 8.76305 8.7576 8.76305H9.34015V8.1572C9.34015 6.48686 10.6468 5.12793 12.2529 5.12793C13.859 5.12793 15.1657 6.48686 15.1657 8.1572V8.76305ZM12.2529 6.33964C11.2894 6.33964 10.5053 7.15512 10.5053 8.1572V8.76305H14.0006V8.1572C14.0006 7.15512 13.2164 6.33964 12.2529 6.33964ZM12.8355 14.8216V13.4421C13.1821 13.2318 13.418 12.8453 13.418 12.3982C13.418 12.0768 13.2953 11.7686 13.0768 11.5414C12.8583 11.3141 12.5619 11.1865 12.2529 11.1865C11.9439 11.1865 11.6476 11.3141 11.4291 11.5414C11.2106 11.7686 11.0878 12.0768 11.0878 12.3982C11.0878 12.8459 11.3237 13.2324 11.6704 13.4421V14.8216H12.8355Z"
fill="#27AE60"></path>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

BIN
public/night_eye.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

6
public/opostrafs.svg Executable file
View File

@ -0,0 +1,6 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg"
class="sc-e2604936-11 eppjeV">
<path
d="M4.583 17.5966C3.553 16.5026 3 15.2756 3 13.2866C3 9.78663 5.457 6.64963 9.03 5.09863L9.923 6.47663C6.588 8.28063 5.936 10.6216 5.676 12.0976C6.213 11.8196 6.916 11.7226 7.605 11.7866C9.409 11.9536 10.831 13.4346 10.831 15.2756C10.831 16.2039 10.4623 17.0941 9.80587 17.7505C9.1495 18.4069 8.25926 18.7756 7.331 18.7756C6.258 18.7756 5.232 18.2856 4.583 17.5966ZM14.583 17.5966C13.553 16.5026 13 15.2756 13 13.2866C13 9.78663 15.457 6.64963 19.03 5.09863L19.923 6.47663C16.588 8.28063 15.936 10.6216 15.676 12.0976C16.213 11.8196 16.916 11.7226 17.605 11.7866C19.409 11.9536 20.831 13.4346 20.831 15.2756C20.831 16.2039 20.4623 17.0941 19.8059 17.7505C19.1495 18.4069 18.2593 18.7756 17.331 18.7756C16.258 18.7756 15.232 18.2856 14.583 17.5966Z"
fill="#2F2E37"></path>
</svg>

After

Width:  |  Height:  |  Size: 934 B

16
public/question.svg Executable file
View File

@ -0,0 +1,16 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg"
class="sc-1200fdd8-6 iAUtsb">
<circle cx="12" cy="12.5" r="12" fill="#FBFBFF"></circle>
<path
d="M10.3168 15.5747V14.829C10.3168 14.1472 10.4197 13.5541 10.6257 13.0498C10.8388 12.5385 11.2223 11.9916 11.7763 11.4092C12.3018 10.8339 12.6357 10.3829 12.7777 10.0562C12.9268 9.72954 13.0014 9.31405 13.0014 8.80979C13.0014 8.34814 12.8949 7.97172 12.6818 7.68053C12.4688 7.38934 12.1314 7.24374 11.6697 7.24374C10.9027 7.24374 10.0966 7.51718 9.25142 8.06405L8.25 5.93337C9.36506 5.17343 10.6009 4.79346 11.9574 4.79346C13.1293 4.79346 14.0526 5.12016 14.7273 5.77357C15.4091 6.41988 15.75 7.32187 15.75 8.47954C15.75 9.2892 15.6328 9.97812 15.3984 10.5463C15.1641 11.1074 14.6776 11.7856 13.9389 12.5811C13.4631 13.0996 13.1577 13.5008 13.0227 13.7849C12.8949 14.0619 12.831 14.4419 12.831 14.9248V15.5747H10.3168ZM9.81605 19.1329C9.81605 18.5719 9.9652 18.1422 10.2635 17.8439C10.5689 17.5385 11.0092 17.3858 11.5845 17.3858C12.1669 17.3858 12.6072 17.542 12.9055 17.8545C13.2038 18.1599 13.353 18.5861 13.353 19.1329C13.353 19.6727 13.2003 20.0989 12.8949 20.4114C12.5895 20.7239 12.1527 20.8801 11.5845 20.8801C11.0163 20.8801 10.5795 20.7274 10.2741 20.422C9.96875 20.1166 9.81605 19.6869 9.81605 19.1329Z"
fill="url(#paint0_linear_340_1679)"></path>
<defs>
<linearGradient id="paint0_linear_340_1679" x1="2.69909" y1="1.10693" x2="12.2347" y2="18.3443"
gradientUnits="userSpaceOnUse">
<stop stop-color="#141333"></stop>
<stop offset="0.443013" stop-color="#202261"></stop>
<stop offset="0.802083" stop-color="#543C97"></stop>
<stop offset="0.973958" stop-color="#6939A2"></stop>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
public/question.webp Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -85,6 +85,11 @@ import BothPage from "../pages/Both";
import RelationshipZodiacInfoPage from "../pages/RelationshipZodiacInfo";
import Satisfied from "../pages/Satisfied";
import AboutUsPage from "../pages/AboutUs";
import LoadingProfilePage from "../pages/LoadingProfile";
import EmailConfirmPage from "../pages/EmailConfirm";
import OnboardingPage from "../pages/Onboarding";
import TrialChoicePage from "../pages/TrialChoice";
import TrialPaymentPage from "../pages/TrialPayment";
function App(): JSX.Element {
const [isSpecialOfferOpen, setIsSpecialOfferOpen] = useState<boolean>(false);
@ -227,6 +232,26 @@ function App(): JSX.Element {
/>
<Route path={routes.client.satisfiedResult()} element={<Satisfied />} />
<Route path={routes.client.aboutUs()} element={<AboutUsPage />} />
<Route
path={routes.client.loadingProfile()}
element={<LoadingProfilePage />}
/>
<Route
path={routes.client.emailConfirm()}
element={<EmailConfirmPage />}
/>
<Route
path={routes.client.onboarding()}
element={<OnboardingPage />}
/>
<Route
path={routes.client.trialChoice()}
element={<TrialChoicePage />}
/>
<Route
path={routes.client.trialPayment()}
element={<TrialPaymentPage />}
/>
{/* Test Routes End */}
<Route

49
src/components/EmailEnterPage/EmailInput.tsx Normal file → Executable file
View File

@ -1,39 +1,48 @@
import { useEffect, useState } from 'react'
import { FormField } from '@/types'
import './styles.css'
import { useEffect, useState } from "react";
import { FormField } from "@/types";
import styles from "./styles.module.css";
const isValidEmail = (email: string) => {
const re = /^(([^<>()[\]\\.,:\s@"]+(\.[^<>()[\]\\.,:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
return re.test(String(email).toLowerCase().trim())
}
const re =
/^(([^<>()[\]\\.,:\s@"]+(\.[^<>()[\]\\.,:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
return re.test(String(email).toLowerCase().trim());
};
function EmailInput(props: FormField<string>): JSX.Element {
const { name, value, placeholder, onValid, onInvalid } = props
const [email, setEmail] = useState(value)
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setEmail(event.target.value)
}
const { name, value, placeholder, onValid, onInvalid } = props;
const [email, setEmail] = useState(value);
const handleChangeEmail = (event: React.ChangeEvent<HTMLInputElement>) => {
const email = event.target.value;
if (!isValidEmail(email)) {
onInvalid();
} else {
onValid(email);
}
setEmail(email);
};
useEffect(() => {
if (isValidEmail(email)) {
onValid(email)
onValid(email);
} else {
onInvalid()
onInvalid();
}
}, [email, onInvalid, onValid])
}, [email, onInvalid, onValid]);
return (
<div className="email-input">
<div className={styles["input-container"]}>
<input
name={name}
type="email"
name={name}
id="email"
value={email}
onChange={handleChange}
onChange={handleChangeEmail}
placeholder=" "
/>
<span className="email-input__placeholder">{placeholder}</span>
<span className={styles["input__placeholder"]}>{placeholder}</span>
</div>
)
);
}
export default EmailInput
export default EmailInput;

View File

@ -0,0 +1,48 @@
import { useState } from "react";
import styles from "./styles.module.css";
interface INameInputProps {
value: string;
placeholder: string;
onValid: (value: string) => void;
onInvalid: () => void;
}
const isValidName = (name: string) => {
return !!(name.length > 0 && name.length < 30);
};
function NameInput({
value,
placeholder,
onValid,
onInvalid,
}: INameInputProps) {
const [name, setName] = useState(value);
const handleChangeName = (event: React.ChangeEvent<HTMLInputElement>) => {
const name = event.target.value;
if (!isValidName(name)) {
onInvalid();
} else {
onValid(name);
}
setName(name);
};
return (
<div className={styles["input-container"]}>
<input
type="name"
name="name"
id="name"
value={name}
onChange={handleChangeName}
placeholder=" "
/>
<span className={styles["input__placeholder"]}>{placeholder}</span>
</div>
);
}
export default NameInput;

285
src/components/EmailEnterPage/index.tsx Normal file → Executable file
View File

@ -1,87 +1,228 @@
import { useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import { useDispatch, useSelector } from 'react-redux'
import { actions, selectors } from '@/store'
import { getClientTimezone } from '@/locales'
import { useAuth } from '@/auth'
import { useApi, ApiError, extractErrorMessage } from '@/api'
import Title from '../Title'
import Policy from '../Policy'
import EmailInput from './EmailInput'
import MainButton from '../MainButton'
import Loader, { LoaderColor } from '../Loader'
import ErrorText from '../ErrorText'
import routes from '@/routes'
import styles from "./styles.module.css";
import { useEffect, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useDispatch, useSelector } from "react-redux";
import { actions, selectors } from "@/store";
import { getClientTimezone } from "@/locales";
import { useAuth } from "@/auth";
import { useApi, ApiError, extractErrorMessage } from "@/api";
import Title from "../Title";
import Policy from "../Policy";
import EmailInput from "./EmailInput";
import MainButton from "../MainButton";
import Loader, { LoaderColor } from "../Loader";
import ErrorText from "../ErrorText";
import routes from "@/routes";
import NameInput from "./NameInput";
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
function EmailEnterPage(): JSX.Element {
const api = useApi()
const { user, signUp } = useAuth()
const { t, i18n } = useTranslation()
const dispatch = useDispatch()
const navigate = useNavigate()
const email = useSelector(selectors.selectEmail)
const birthday = useSelector(selectors.selectBirthday)
const [isDisabled, setIsDisabled] = useState(true)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<ApiError | null>(null)
const timezone = getClientTimezone()
const locale = i18n.language
const handleValidEmail = (email: string) => {
dispatch(actions.form.addEmail(email))
setIsDisabled(false)
}
const handleClick = () => {
// TODO: fix backend error 422 auth with email
return navigate(routes.client.priceList())
if (user) {
return
}
setError(null)
setIsLoading(true)
api.auth({ email, timezone, locale })
.then(({ auth: { token, user } }) => signUp(token, user))
.then((token) => {
const payload = { user: { profile_attributes: { birthday } }, token }
return Promise.all([
api.updateUser(payload),
api.getSubscriptionItems({ locale, token })
])
})
.then(([{ user }, { item_prices }]) => {
dispatch(actions.user.update(user))
dispatch(actions.status.update('registred'))
dispatch(actions.subscriptionPlan.setAll(item_prices))
})
.then(() => navigate(routes.client.subscription()))
.catch((error: ApiError) => setError(error))
.finally(() => setIsLoading(false))
}
const api = useApi();
const { signUp } = useAuth();
const { t, i18n } = useTranslation();
const dispatch = useDispatch();
const navigate = useNavigate();
const [email, setEmail] = useState("");
const [name, setName] = useState("");
const birthday = useSelector(selectors.selectBirthday);
const [isDisabled, setIsDisabled] = useState(true);
const [isValidEmail, setIsValidEmail] = useState(false);
const [isValidName, setIsValidName] = useState(true);
const [isLoading, setIsLoading] = useState(false);
const [isAuth, setIsAuth] = useState(false);
const [apiError, setApiError] = useState<ApiError | null>(null);
const [error, setError] = useState<boolean>(false);
const [subPlans, setSubPlans] = useState<ISubscriptionPlan[]>([]);
const activeSubPlanFromStore = useSelector(selectors.selectActiveSubPlan);
const [activeSubPlan, setActiveSubPlan] = useState<ISubscriptionPlan | null>(
activeSubPlanFromStore
);
const timezone = getClientTimezone();
const locale = i18n.language;
const { subPlan } = useParams();
useEffect(() => {
if (subPlan) {
const targetSubPlan = subPlans.find(
(sub_plan) =>
String(
sub_plan?.trial?.price_cents
? Math.floor((sub_plan?.trial?.price_cents + 1) / 100)
: sub_plan.id.replace(".", "")
) === subPlan
);
if (targetSubPlan) {
setActiveSubPlan(targetSubPlan);
}
}
}, [subPlan, subPlans]);
useEffect(() => {
(async () => {
const { sub_plans } = await api.getSubscriptionPlans({ locale });
const plans = sub_plans
.filter((plan: ISubscriptionPlan) => plan.provider === "stripe")
.sort((a, b) => {
if (!a.trial || !b.trial) {
return 0;
}
if (a?.trial?.price_cents < b?.trial?.price_cents) {
return -1;
}
if (a?.trial?.price_cents > b?.trial?.price_cents) {
return 1;
}
return 0;
});
setSubPlans(plans);
})();
}, [api, locale]);
const handleValidEmail = (email: string) => {
dispatch(actions.form.addEmail(email));
setEmail(email);
setIsValidEmail(true);
};
const handleValidName = (name: string) => {
setName(name);
setIsValidName(true);
};
useEffect(() => {
if (isValidName && isValidEmail) {
setIsDisabled(false);
} else {
setIsDisabled(true);
}
}, [isValidEmail, isValidName, email, name]);
const handleClick = () => {
authorization();
};
const authorization = async () => {
try {
setIsLoading(true);
const auth = await api.auth({ email, timezone, locale });
const {
auth: { token, user },
} = auth;
signUp(token, user);
const payload = {
user: { profile_attributes: { birthday } },
token,
};
const updatedUser = await api.updateUser(payload).catch((error) => {
console.log("Error: ", error);
});
if (updatedUser?.user) {
dispatch(actions.user.update(updatedUser.user));
}
if (name) {
dispatch(
actions.user.update({
username: name,
})
);
}
dispatch(actions.status.update("registred"));
dispatch(
actions.payment.update({
activeSubPlan,
})
);
setIsLoading(false);
setIsAuth(true);
setTimeout(() => {
navigate(routes.client.emailConfirm());
}, 1000);
} catch (error) {
console.error(error);
if (error instanceof ApiError) {
setApiError(error as ApiError);
} else {
setError(true);
}
setIsLoading(false);
}
};
return (
<section className='page'>
<Title variant='h2' className='mt-24'>{t('aura.web.email_title')}</Title>
<section className="page">
<Title variant="h2" className={styles.title}>
Enter your email to see how you can find your perfect partner
</Title>
<p className={styles["not-share"]}>{t("we_dont_share")}</p>
<NameInput
value={name}
placeholder="Your name"
onValid={handleValidName}
onInvalid={() => setIsValidName(true)}
/>
<EmailInput
name="email"
value={email}
placeholder={t('your_email')}
placeholder={t("your_email")}
onValid={handleValidEmail}
onInvalid={() => setIsDisabled(true)}
onInvalid={() => setIsValidEmail(false)}
/>
<p style={{ marginBottom: '8px' }}>{t('we_dont_share')}</p>
<MainButton onClick={handleClick} disabled={isDisabled}>
{isLoading ? <Loader color={LoaderColor.White} /> : t('_continue')}
</MainButton>
<Policy sizing='medium'>
{t('_continue_agree', {
eulaLink: <a href='https://aura.wit.life/terms' target='_blank' rel='noopener noreferrer'>{t('eula')}</a>,
privacyLink: <a href='https://aura.wit.life/privacy' target='_blank' rel='noopener noreferrer'>{t('privacy_policy')}</a>,
<Policy sizing="medium" className={styles.policy}>
{t("_continue_agree", {
eulaLink: (
<a
className={styles.link}
href="https://aura.wit.life/terms"
target="_blank"
rel="noopener noreferrer"
>
{t("eula")}
</a>
),
privacyLink: (
<a
className={styles.link}
href="https://aura.wit.life/privacy"
target="_blank"
rel="noopener noreferrer"
>
{t("privacy_policy")}
</a>
),
})}
</Policy>
<ErrorText size='medium' isShown={Boolean(error)} message={error ? extractErrorMessage(error) : null} />
<MainButton
className={styles.button}
onClick={handleClick}
disabled={isDisabled}
>
{isLoading && <Loader color={LoaderColor.White} />}
{!isLoading &&
!(!apiError && !error && !isLoading && isAuth) &&
t("_continue")}
{!apiError && !error && !isLoading && isAuth && (
<img
className={styles["success-icon"]}
src="/SuccessIcon.png"
alt="Success Icon"
/>
)}
</MainButton>
{(error || apiError) && (
<Title variant="h3" style={{ color: "red", margin: 0 }}>
Something went wrong
</Title>
)}
{apiError && (
<ErrorText
size="medium"
isShown={Boolean(apiError)}
message={apiError ? extractErrorMessage(apiError) : null}
/>
)}
</section>
)
);
}
export default EmailEnterPage
export default EmailEnterPage;

View File

@ -0,0 +1,102 @@
.title {
font-size: 18px;
line-height: 28px;
font-weight: 700;
color: #333333;
}
.not-share {
font-size: 12px;
line-height: 16px;
text-align: center;
max-width: 330px;
margin-left: auto;
margin-right: auto;
margin-bottom: 32px;
color: #333333;
}
.button {
border-radius: 12px;
margin-top: 24px;
box-shadow: rgba(0, 0, 0, 0.25) 0px 4px 4px 0px;
height: 50px;
min-height: 0;
background: linear-gradient(
165.54deg,
rgb(20, 19, 51) -33.39%,
rgb(32, 34, 97) 15.89%,
rgb(84, 60, 151) 55.84%,
rgb(105, 57, 162) 74.96%
);
font-size: 18px;
line-height: 21px;
}
.policy {
max-width: 240px;
}
.policy > p {
font-size: 12px;
}
.link {
font-size: 12px !important;
color: #9974f6 !important;
}
.success-icon {
height: 100%;
width: auto;
}
.input-container {
width: 100%;
position: relative;
text-align: center;
margin-bottom: 20px;
max-width: 400px;
min-width: 250px;
}
.input-container > input {
appearance: none;
border: 1px solid #c7c7c7;
border-radius: 25px;
color: #121620;
font-size: 16px;
height: 48px;
line-height: 18px;
outline: none;
padding: 12px 24px 5px;
transition: border-color 0.3s ease;
width: 100%;
}
.input-container > input:focus {
border-color: #000;
transition-delay: 0.1s;
}
.input-container > input:focus + .input__placeholder,
.input-container > input:not(:placeholder-shown) + .input__placeholder {
font-size: 12px;
top: 12px;
width: auto;
}
.input__placeholder {
color: #8e8e93;
font-size: 16px;
left: 24px;
overflow: hidden;
text-overflow: ellipsis;
transition: top 0.3s ease, color 0.3s ease, font-size 0.3s ease;
white-space: nowrap;
position: absolute;
top: 50%;
transform: translateY(-50%);
user-select: none;
pointer-events: none;
}

47
src/components/EmailItem/index.tsx Normal file → Executable file
View File

@ -1,34 +1,39 @@
import { Currency, Locale, Price } from '../PaymentTable'
import styles from './styles.module.css'
import { Currency, Locale, Price } from "../PaymentTable";
import styles from "./styles.module.css";
export interface IEmailItem {
email: string
price: number
export interface IEmailItemProps {
email: string;
price: number;
className?: string;
}
const currency = Currency.USD
const locale = Locale.EN
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)
}
value = Number(value);
if (value % Math.floor(value) !== 0) {
return value;
}
return Math.floor(value);
};
const formatEmail = (email: string): string => {
return `${email.slice(0, 4)}****`
}
return `${email.slice(0, 4)}****`;
};
function EmailItem({email, price}: IEmailItem): JSX.Element {
const _price = new Price(roundToWhole(price), currency, locale)
function EmailItem({
email,
price,
className = "",
}: IEmailItemProps): JSX.Element {
const _price = new Price(roundToWhole(price), currency, locale);
return (
<span className={styles.container}>
{formatEmail(email)} <strong> chose {_price.format()} </strong>
<span className={`${styles.container} ${className}`}>
{formatEmail(email)} <strong> chose {_price.format()} </strong>
</span>
)
);
}
export default EmailItem
export default EmailItem;

45
src/components/EmailsList/index.tsx Normal file → Executable file
View File

@ -1,11 +1,11 @@
import { getRandomArbitrary, getRandomName } from "@/services/random-value";
import EmailItem, { IEmailItem } from "../EmailItem";
import EmailItem, { IEmailItemProps } from "../EmailItem";
import styles from "./styles.module.css";
import { useTranslation } from "react-i18next";
import { useEffect, useRef, useState } from "react";
const getEmails = (): IEmailItem[] => {
const emails: IEmailItem[] = [];
const getEmails = (): IEmailItemProps[] => {
const emails: IEmailItemProps[] = [];
for (let index = 0; index < 5; index++) {
emails.push({
@ -17,7 +17,19 @@ const getEmails = (): IEmailItem[] => {
return emails;
};
function EmailsList(): JSX.Element {
interface IEmailsListProps {
classNameContainer?: string;
classNameTitle?: string;
classNameEmailItem?: string;
direction?: "up-down" | "down-up" | "left-right" | "right-left";
}
function EmailsList({
classNameContainer = "",
classNameTitle = "",
classNameEmailItem = "",
direction = "up-down",
}: IEmailsListProps): JSX.Element {
const { t } = useTranslation();
const [countUsers, setCountUsers] = useState(752);
const [emails, setEmails] = useState(getEmails());
@ -56,21 +68,34 @@ function EmailsList(): JSX.Element {
}, [emails, elementIdx]);
return (
<div className={styles.container}>
<span className={styles["title"]}>
<div className={`${styles.container} ${classNameContainer}`}>
<span className={`${styles["title"]} ${classNameTitle}`}>
{t("people_joined_today", {
countPeoples: <strong>{countUsers}</strong>,
})}
</span>
<div className={styles["emails-container"]}>
<div className={`${styles["emails-container"]} ${styles[direction]}`}>
{emails.map(({ email, price }, idx) => (
<div
className={`${styles["email-item"]} ${elementIdx > idx ? styles["hidden"] : ""}`}
style={{ display: elementIdx - 1 > idx ? "none" : "" }}
className={`${styles["email-item"]} ${
elementIdx > idx ? styles["hidden"] : ""
}`}
style={{
display: elementIdx - 1 > idx ? "none" : "",
marginLeft:
direction === "right-left" && elementIdx > idx
? `-${itemsRef.current[idx]?.offsetWidth + 10}px`
: 0,
}}
ref={(el: HTMLDivElement) => (itemsRef.current[idx] = el)}
data-width={`-${itemsRef.current[idx]?.offsetWidth}px`}
key={idx}
>
<EmailItem email={email} price={price} />
<EmailItem
email={email}
price={price}
className={classNameEmailItem}
/>
</div>
))}
</div>

21
src/components/EmailsList/styles.module.css Normal file → Executable file
View File

@ -21,12 +21,24 @@
height: 108px;
padding: 8px 0;
display: flex;
flex-direction: column-reverse;
align-items: center;
overflow: hidden;
gap: 10px;
}
.emails-container.up-down {
flex-direction: column-reverse;
}
.emails-container.right-left {
flex-direction: row;
height: fit-content;
}
.emails-container.up-down {
flex-direction: column-reverse;
}
.email-item {
margin-top: 0;
opacity: 1;
@ -34,7 +46,10 @@
}
.hidden {
transition: all .5s ease;
margin-bottom: -26px;
transition: all 0.5s ease;
opacity: 0;
}
.up-down .hidden {
margin-bottom: -26px;
}

View File

@ -50,7 +50,8 @@ function Header({
const goBack = () => {
if (
location.pathname.includes("/questionnaire") ||
location.pathname.includes("/about-us")
location.pathname.includes("/about-us") ||
location.pathname.includes("/payment/stripe")
) {
return navigate(-1);
}

View File

@ -0,0 +1,24 @@
import MainButton from "../MainButton";
import Title from "../Title";
import styles from "./styles.module.css";
interface ILoadingProfileModalChildProps {
title: string;
handleClick: () => void;
}
function LoadingProfileModalChild({ title, handleClick }: ILoadingProfileModalChildProps) {
return (
<div className={styles.modal}>
<img className={styles["question-img"]} src="./question.webp" alt="Question" />
<hr className={styles["horizontal-line"]} />
<Title className={styles.title} variant="h4">{title}</Title>
<div className={styles["buttons-container"]}>
<MainButton className={styles.button} onClick={handleClick}>No</MainButton>
<MainButton className={styles.button} onClick={handleClick}>Yes</MainButton>
</div>
</div>
);
}
export default LoadingProfileModalChild;

View File

@ -0,0 +1,48 @@
.modal {
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.title {
font-weight: 700;
}
.question-img {
width: 100%;
max-width: 134px;
}
.horizontal-line {
width: 100%;
height: 1px;
}
.buttons-container {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
}
.button {
background-color: #eaeef7;
color: #000;
width: calc(50% - 8px);
min-width: 0;
height: 40px;
min-height: 0;
border-radius: 12px;
}
.button:nth-child(2) {
background: linear-gradient(
165.54deg,
rgb(20, 19, 51) -33.39%,
rgb(32, 34, 97) 15.89%,
rgb(84, 60, 151) 55.84%,
rgb(105, 57, 162) 74.96%
);
color: #fff;
}

15
src/components/PriceItem/index.tsx Normal file → Executable file
View File

@ -9,10 +9,19 @@ interface PriceItemProps {
id: string;
value: number;
active: boolean;
className?: string;
classNameActive?: string;
click: (id: string) => void;
}
function PriceItem({ id, value, active, click }: PriceItemProps): JSX.Element {
function PriceItem({
id,
value,
active,
className = "",
classNameActive = "",
click,
}: PriceItemProps): JSX.Element {
const _price = new Price(
roundToWhole(value === 1 ? 0.99 : value),
currency,
@ -23,8 +32,8 @@ function PriceItem({ id, value, active, click }: PriceItemProps): JSX.Element {
const isPopular = id === "stripe.7";
const isActive = active;
return `${styles.container} ${isPopular ? styles.popular : ""} ${
isActive ? styles.active : ""
}`;
isActive ? `${styles.active} ${classNameActive}` : ""
} ${className}`;
};
const itemClick = () => {

15
src/components/PriceList/index.tsx Normal file → Executable file
View File

@ -8,6 +8,8 @@ import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
interface PriceListProps {
subPlans: ISubscriptionPlan[];
activeItem: number | null;
classNameItem?: string;
classNameItemActive?: string;
click: () => void;
}
@ -15,7 +17,12 @@ const getPrice = (plan: ISubscriptionPlan) => {
return (plan.trial?.price_cents || 0) / 100;
};
function PriceList({ click, subPlans }: PriceListProps): JSX.Element {
function PriceList({
click,
subPlans,
classNameItem = "",
classNameItemActive = "",
}: PriceListProps): JSX.Element {
const dispatch = useDispatch();
const [activePlanItem, setActivePlanItem] =
useState<ISubscriptionPlan | null>(null);
@ -30,9 +37,7 @@ function PriceList({ click, subPlans }: PriceListProps): JSX.Element {
})
);
}
setTimeout(() => {
click();
}, 1000);
click();
};
return (
@ -43,6 +48,8 @@ function PriceList({ click, subPlans }: PriceListProps): JSX.Element {
key={idx}
value={getPrice(plan)}
id={plan.id}
className={classNameItem}
classNameActive={classNameItemActive}
click={priceItemClick}
/>
))}

9
src/components/PriceList/styles.module.css Normal file → Executable file
View File

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

6
src/components/PriceListPage/index.tsx Normal file → Executable file
View File

@ -43,7 +43,7 @@ function PriceListPage(): JSX.Element {
});
setSubPlans(plans);
})();
}, [api]);
}, [api, locale]);
const handleNext = () => {
dispatch(
@ -51,7 +51,9 @@ function PriceListPage(): JSX.Element {
home: { pathFromHome: homeConfig.pathFromHome, isShowNavbar: false },
})
);
navigate(routes.client.subscription());
setTimeout(() => {
navigate(routes.client.subscription());
}, 1000);
};
return (

View File

@ -17,7 +17,7 @@ function AboutUsPage() {
aboutUs: answer.id,
})
);
navigate(routes.client.priceList());
navigate(routes.client.loadingProfile());
};
return (

View File

@ -0,0 +1,44 @@
import Title from "@/components/Title";
import styles from "./styles.module.css";
import MainButton from "@/components/MainButton";
import { useNavigate } from "react-router-dom";
import routes from "@/routes";
function EmailConfirmPage() {
const navigate = useNavigate();
const handleNext = () => {
navigate(routes.client.onboarding());
};
return (
<section className={`${styles.page} page`}>
<div className={styles["top-section"]}>
<img
className={styles["eye-image"]}
src="/night_eye.webp"
alt="Night eye"
/>
<Title className={styles.title}>
Get access to your{" "}
<span className={styles.purple}>exclusive reading</span>, special
offers, updates, astrology & relationship tips, recipes, and free
gifts.
</Title>
<p className={styles.description}>
Get it all! Confirm receiving emails so you don't miss anything
</p>
</div>
<div className={styles["bottom-section"]}>
<MainButton className={styles.button} onClick={handleNext}>
Confirm
</MainButton>
<p className={styles["text-link"]} onClick={handleNext}>
I know everything about astrology & relationship
</p>
</div>
</section>
);
}
export default EmailConfirmPage;

View File

@ -0,0 +1,75 @@
.page {
position: relative;
height: fit-content;
min-height: calc(100vh - 50px);
min-height: calc(100dvh - 50px);
display: flex;
justify-content: start;
align-items: center;
justify-content: space-between;
flex-direction: column;
padding-top: 16px;
color: #333333;
}
.title {
margin: 0;
text-align: left;
font-size: 24px;
line-height: 1.3;
}
.description {
margin: 0;
text-align: left;
font-size: 18px;
font-weight: 400;
line-height: 1.3;
}
.purple {
color: #6a3aa2;
}
.eye-image {
width: 100%;
max-width: 180px;
}
.top-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 30px;
padding: 0 24px;
}
.bottom-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
padding: 0 24px;
}
.text-link {
color: #828282;
text-align: center;
font-weight: 600;
line-height: 1.2;
text-decoration: underline;
}
.button {
background: linear-gradient(
165.54deg,
rgb(20, 19, 51) -33.39%,
rgb(32, 34, 97) 15.89%,
rgb(84, 60, 151) 55.84%,
rgb(105, 57, 162) 74.96%
);
min-height: 0;
height: 50px;
font-size: 16px;
font-weight: 400;
}

View File

@ -0,0 +1,109 @@
import Title from "@/components/Title";
import styles from "./styles.module.css";
import ProgressBarLine from "@/components/ui/ProgressBarLine";
import {
loadingProfilePoints,
modalTitlesLoadingProfile,
titlesLoadingProfile,
} from "@/data/loadingProfile";
import { useCallback, useEffect, useRef, useState } from "react";
import { useNavigate } from "react-router-dom";
import routes from "@/routes";
import Modal from "@/components/Modal";
import LoadingProfileModalChild from "@/components/LoadingProfileModalChild";
function LoadingProfilePage() {
const navigate = useNavigate();
const [progress, setProgress] = useState(0);
const [isPause, setIsPause] = useState(false);
const interval = useRef<NodeJS.Timeout>();
const onEndLoading = useCallback(() => {
navigate(routes.client.emailEnter());
}, [navigate]);
const getProgressValue = useCallback(
(index: number) => {
const integerDivision = Math.floor(progress / 100);
if (integerDivision > index) {
return 100;
}
if (integerDivision === index) {
return progress % 100;
}
return 0;
},
[progress]
);
useEffect(() => {
if (progress % 98 === 97) {
setIsPause(true);
}
}, [progress]);
useEffect(() => {
if (progress >= loadingProfilePoints.length * 100) {
return onEndLoading();
}
interval.current = setTimeout(() => {
setProgress((prevProgress) => {
if (!isPause) return prevProgress + 1;
return prevProgress;
});
}, 100);
return () => {
clearTimeout(interval.current);
};
}, [progress, onEndLoading, isPause]);
return (
<section className={`${styles.page} page`}>
{isPause && (
<Modal open={!!isPause} onClose={() => setIsPause(false)}>
<LoadingProfileModalChild
title={modalTitlesLoadingProfile[Math.floor(progress / 100)]}
handleClick={() => setIsPause(false)}
/>
</Modal>
)}
<Title variant="h1" className={styles.title}>
{titlesLoadingProfile[Math.floor(progress / 34)]}
</Title>
<div className={styles["points-container"]}>
{loadingProfilePoints.map(({ title, color }, index) => (
<div
className={`${styles["point"]} ${styles[color]}`}
key={`point-${index}`}
>
<div className={styles["point__text-container"]}>
<Title
variant="h2"
className={styles["point__title"]}
style={{ color }}
>
{title}
</Title>
<p className={styles["point__percentage"]} style={{ color }}>
{getProgressValue(index)}%
</p>
</div>
<ProgressBarLine
containerClassName={styles["progress-bar__container"]}
lineClassName={styles["progress-bar__line"]}
lineColor={color}
value={getProgressValue(index)}
delay={50}
/>
</div>
))}
</div>
<p className={styles.description}>
Sit tight! We`re building your perfect guidance plane based on your
unique astrological blueprint and data of millions users.
</p>
</section>
);
}
export default LoadingProfilePage;

View File

@ -0,0 +1,55 @@
.title {
font-size: 20px;
color: #1c1c1c;
min-height: 60px;
}
.points-container,
.point {
width: 100%;
display: flex;
flex-direction: column;
}
.points-container {
gap: 24px;
}
.point {
gap: 12px;
}
.point__text-container {
width: 100%;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
}
.point__title {
font-size: 14px;
margin: 0;
}
.point__percentage {
font-size: 14px;
font-weight: 600;
}
.progress-bar__container {
background-color: #e6e6e6;
height: 8px;
}
.progress-bar__line {
background-color: #908cf2;
}
.description {
margin-top: 24px;
font-size: 14px;
text-align: center;
line-height: 1.5;
width: 90%;
}

View File

@ -0,0 +1,49 @@
import Title from "@/components/Title";
import styles from "./styles.module.css";
import { useCallback, useEffect, useRef, useState } from "react";
import { onboardingTitles } from "@/data/onboarding";
import routes from "@/routes";
import { useNavigate } from "react-router-dom";
function OnboardingPage() {
const navigate = useNavigate();
const [activeIndexTitle, setIndexTitle] = useState(0);
const [periodClassName, setPeriodClassName] = useState("");
const titleInterval = useRef<NodeJS.Timeout>();
const classNameTimeOut = useRef<NodeJS.Timeout>();
const handleNext = useCallback(() => {
navigate(routes.client.trialChoice());
}, [navigate]);
useEffect(() => {
console.log("onboardingTitles", activeIndexTitle);
setPeriodClassName("to-nontransparent");
classNameTimeOut.current = setTimeout(() => {
setPeriodClassName("to-transparent");
}, 4000);
titleInterval.current = setTimeout(() => {
if (activeIndexTitle < onboardingTitles.length - 1) {
setIndexTitle((prev) => prev + 1);
} else {
handleNext();
}
}, 5000);
return () => {
if (titleInterval.current) clearTimeout(titleInterval.current);
if (classNameTimeOut.current) clearTimeout(classNameTimeOut.current);
};
}, [activeIndexTitle, handleNext]);
return (
<section className={`${styles.page} page`}>
{onboardingTitles[activeIndexTitle] && (
<Title className={`${styles.title} ${styles[periodClassName]}`}>
{onboardingTitles[activeIndexTitle]}
</Title>
)}
</section>
);
}
export default OnboardingPage;

View File

@ -0,0 +1,51 @@
.page {
display: flex;
justify-content: center;
align-items: flex-start;
height: fit-content;
min-height: 100vh;
min-height: 100dvh;
background: linear-gradient(
165.54deg,
rgb(20, 19, 51) -33.39%,
rgb(32, 34, 97) 15.89%,
rgb(84, 60, 151) 55.84%,
rgb(105, 57, 162) 74.96%
);
}
.title {
color: #fff;
text-align: left;
max-width: 300px;
font-size: 32px;
opacity: 0;
transition: opacity 1s;
/* animation-name: show-up;
animation-duration: 5s;
animation-iteration-count: infinite;
animation-timing-function: linear; */
}
.to-transparent {
opacity: 0;
}
.to-nontransparent {
opacity: 1;
}
@keyframes show-up {
0% {
opacity: 0;
}
20% {
opacity: 1;
}
80% {
opacity: 1;
}
100% {
opacity: 0;
}
}

View File

@ -0,0 +1,114 @@
import PriceList from "@/components/PriceList";
import styles from "./styles.module.css";
import { useEffect, useState } from "react";
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
import { useTranslation } from "react-i18next";
import { useApi } from "@/api";
import { useDispatch, useSelector } from "react-redux";
import { actions, selectors } from "@/store";
import { useNavigate } from "react-router-dom";
import routes from "@/routes";
import EmailsList from "@/components/EmailsList";
import MainButton from "@/components/MainButton";
function TrialChoicePage() {
const { i18n } = useTranslation();
const locale = i18n.language;
const api = useApi();
const dispatch = useDispatch();
const navigate = useNavigate();
const selectedPrice = useSelector(selectors.selectSelectedPrice);
const homeConfig = useSelector(selectors.selectHome);
const [subPlans, setSubPlans] = useState<ISubscriptionPlan[]>([]);
const [isDisabled, setIsDisabled] = useState(true);
useEffect(() => {
(async () => {
const { sub_plans } = await api.getSubscriptionPlans({ locale });
const plans = sub_plans
.filter((plan: ISubscriptionPlan) => plan.provider === "stripe")
.sort((a, b) => {
if (!a.trial || !b.trial) {
return 0;
}
if (a.trial?.price_cents < b.trial?.price_cents) {
return -1;
}
if (a.trial?.price_cents > b.trial?.price_cents) {
return 1;
}
return 0;
});
setSubPlans(plans);
})();
}, [api, locale]);
const handlePriceItem = () => {
setIsDisabled(false);
};
const handleNext = () => {
dispatch(
actions.siteConfig.update({
home: { pathFromHome: homeConfig.pathFromHome, isShowNavbar: false },
})
);
navigate(routes.client.trialPayment());
};
return (
<section className={`${styles.page} page`}>
<p className={styles.text}>
We've helped millions of people to have happier lives and better
relationships, and we want to help you too.
</p>
<p className={`${styles.text} ${styles.bold}`}>
Money shouldnt stand in the way of finding astrology guidance that
finally works. So, choose an amount that you think is reasonable to try
us out for one week.
</p>
<p className={`${styles.text} ${styles.bold} ${styles.purple}`}>
It costs us $13.67 to offer a 7-day trial, but please choose the amount
you are comfortable with.
</p>
<div className={styles["price-container"]}>
<PriceList
subPlans={subPlans}
activeItem={selectedPrice}
classNameItem={styles["price-item"]}
classNameItemActive={styles["price-item-active"]}
click={handlePriceItem}
/>
<p className={styles["auxiliary-text"]} style={{ maxWidth: "272px" }}>
This option will help us support those who need to select the lowest
trial prices!
</p>
<img
className={styles["arrow-image"]}
src="/arrow.svg"
alt={`Arrow to $${subPlans.at(-1)}`}
/>
</div>
<div className={styles["emails-list-container"]}>
<EmailsList
classNameContainer={styles["emails-container"]}
classNameTitle={styles["emails-title"]}
classNameEmailItem={styles["email-item"]}
direction="right-left"
/>
</div>
<MainButton
className={styles.button}
disabled={isDisabled}
onClick={handleNext}
>
See my plan
</MainButton>
<p className={styles["auxiliary-text"]}>
*Cost of trial as of February 2023
</p>
</section>
);
}
export default TrialChoicePage;

View File

@ -0,0 +1,123 @@
.page {
display: flex;
flex-direction: column;
gap: 10px;
min-height: calc(100vh - 50px);
min-height: calc(100dvh - 50px);
height: fit-content;
background-color: #fff0f0;
padding: 15px 42px 60px;
}
.text {
font-size: 14px;
line-height: 180%;
font-weight: 400;
}
.text.bold {
font-weight: 600;
}
.purple {
color: #6a3aa2;
}
.auxiliary-text {
font-size: 12px;
line-height: 16px;
color: rgb(52, 52, 52);
width: 100%;
}
.price-container {
position: relative;
width: 100%;
margin-top: 10px;
display: flex;
flex-direction: column;
gap: 20px;
}
.price-item {
background: rgb(234, 238, 247);
color: rgb(51, 51, 51);
border: 1px solid rgb(224, 224, 224);
box-shadow: rgba(84, 60, 151, 0.25) 2px 2px 6px;
border-radius: 12px;
display: flex;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: center;
justify-content: center;
font-weight: 600;
width: calc((100% - 30px) / 4);
max-width: 72px;
max-height: 72px;
height: auto;
aspect-ratio: 1 / 1;
position: relative;
z-index: 1;
}
.price-item-active {
background: linear-gradient(
165.54deg,
rgb(20, 19, 51) -33.39%,
rgb(32, 34, 97) 15.89%,
rgb(84, 60, 151) 55.84%,
rgb(105, 57, 162) 74.96%
);
color: rgb(251, 251, 255);
}
.arrow-image {
position: absolute;
width: 26px;
height: 33px;
top: 92px;
right: 32px;
}
.emails-list-container {
width: 100%;
margin-top: 20px;
}
.emails-container {
background-color: #3c38d70d;
}
.emails-title {
font-weight: 600;
line-height: 24px;
font-size: 16px;
color: rgb(69, 72, 149);
margin-bottom: 6px;
text-align: center;
}
.email-item {
background: rgb(251, 251, 255);
border-radius: 4px;
padding: 5px 7px;
font-size: 12px;
line-height: 130%;
display: flex;
width: max-content;
color: rgb(79, 79, 79);
}
.button {
background: linear-gradient(
165.54deg,
rgb(20, 19, 51) -33.39%,
rgb(32, 34, 97) 15.89%,
rgb(84, 60, 151) 55.84%,
rgb(105, 57, 162) 74.96%
);
color: #fbfbff;
border-radius: 12px;
min-height: 0;
height: 50px;
}

View File

@ -0,0 +1,14 @@
import styles from "./styles.module.css";
import MainButton from "@/components/MainButton";
function CustomButton(props: typeof MainButton.arguments) {
const { className } = props;
return (
<MainButton
{...props}
className={`${className} ${styles.button}`}
></MainButton>
);
}
export default CustomButton;

View File

@ -0,0 +1,28 @@
.button {
height: 100%;
min-height: 0;
width: 100%;
min-width: 0;
text-transform: uppercase;
text-align: center;
border-radius: 6px;
background: rgb(153, 116, 246);
box-shadow: rgba(153, 116, 246, 0.5) 0px 0px 0px 0px;
font-size: 14px;
font-weight: 700;
animation: 1.5s ease 0s infinite normal none running pulse;
}
@keyframes pulse {
0% {
transform: scale(0.9);
}
70% {
transform: scale(1);
box-shadow: rgba(153, 116, 246, 0.2) 0px 0px 0px 4px;
}
100% {
transform: scale(0.9);
box-shadow: rgba(153, 116, 246, 0.2) 0px 0px 0px 0px;
}
}

View File

@ -0,0 +1,58 @@
import { useEffect, useMemo, useState } from "react";
import styles from "./styles.module.css";
import Title from "@/components/Title";
function DiscountExpires() {
const [currentDate, setCurrentDate] = useState(new Date());
const endDate = useMemo(
() => new Date().setMinutes(currentDate.getMinutes() + 10),
// eslint-disable-next-line react-hooks/exhaustive-deps
[]
);
useEffect(() => {
const interval = setInterval(() => {
setCurrentDate(new Date());
}, 1000);
return () => clearInterval(interval);
}, [endDate, currentDate]);
const getMinutes = () => {
const diff = endDate - currentDate.getTime();
let minutes = Math.floor(diff / 1000 / 60);
if (minutes < 0) {
minutes = 0;
}
return minutes.toString().padStart(2, "0");
};
const getSeconds = () => {
const diff = endDate - currentDate.getTime();
let seconds = Math.floor(diff / 1000) % 60;
if (seconds < 0) {
seconds = 0;
}
return seconds.toString().padStart(2, "0");
};
return (
<div className={styles["discount-expires"]}>
<Title variant="h6" className={styles.title}>
Discount expires
</Title>
<div className={styles.values}>
<div className={styles["value-container"]}>
<span className={styles.value}>{getMinutes()}</span>
<span className={styles["value-symbol"]}>min</span>
</div>
<p className={styles.colon}>:</p>
<div className={styles["value-container"]}>
<span className={styles.value}>{getSeconds()}</span>
<span className={styles["value-symbol"]}>sec</span>
</div>
</div>
</div>
);
}
export default DiscountExpires;

View File

@ -0,0 +1,48 @@
.discount-expires {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.title {
font-size: 12px;
font-weight: 600;
line-height: 130%;
color: rgb(51, 51, 51);
margin: 0;
}
.values {
display: flex;
flex-direction: row;
}
.value-container {
display: flex;
flex-direction: column;
-webkit-box-align: center;
align-items: center;
}
.value {
font-weight: 700;
font-size: 18px;
line-height: 110%;
color: #333333;
}
.value-symbol {
font-weight: 600;
font-size: 8px;
line-height: 130%;
color: rgb(130, 130, 130);
}
.colon {
margin: 0 4px;
font-weight: 700;
font-size: 18px;
line-height: 110%;
color: rgb(51, 51, 51);
}

View File

@ -0,0 +1,27 @@
import { useSelector } from "react-redux";
import styles from "./styles.module.css";
import { selectors } from "@/store";
import { stepsQuestionary } from "@/data";
function Goal() {
const { goal } = useSelector(selectors.selectQuestionnaire);
const getGoal = () => {
const question = stepsQuestionary[0].questions.find(
(question) => question.id === "goal"
);
return question?.answers?.find((answer) => answer.id === goal)?.answer;
};
return (
<div className={styles.goal}>
<img src="/darts-purple.svg" alt="Darts icon" />
<div className={styles["text-container"]}>
<span>Goal</span>
<p>{getGoal() || ""}</p>
</div>
</div>
);
}
export default Goal;

View File

@ -0,0 +1,23 @@
.goal {
width: 100%;
display: flex;
align-items: center;
flex-direction: row;
gap: 10px;
}
.text-container {
display: flex;
flex-direction: column;
}
.text-container > span {
font-size: 12px;
line-height: 1;
}
.text-container > p {
font-weight: 600;
line-height: 130%;
font-size: 18px;
}

View File

@ -0,0 +1,12 @@
import styles from "./styles.module.css";
function GuardPayments() {
return (
<div className={styles["guard-payments"]}>
<img src="/guard.svg" alt="Guaranteed security" />
<p className={styles.text}>Guaranteed security payments</p>
</div>
);
}
export default GuardPayments;

View File

@ -0,0 +1,16 @@
.guard-payments {
width: 100%;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 6px;
margin-top: 26px;
}
.text {
font-size: 14px;
font-weight: 400;
line-height: 24px;
color: rgb(79, 79, 79);
}

View File

@ -0,0 +1,20 @@
import CustomButton from "../CustomButton";
import DiscountExpires from "../DiscountExpires";
import styles from "./styles.module.css";
interface IHeaderProps {
buttonClick: () => void;
}
function Header({ buttonClick }: IHeaderProps) {
return (
<header className={styles.header}>
<DiscountExpires />
<CustomButton className={styles.button} onClick={buttonClick}>
get my reading
</CustomButton>
</header>
);
}
export default Header;

View File

@ -0,0 +1,19 @@
.header {
position: fixed;
z-index: 10;
top: 0px;
left: 0px;
height: 62px;
width: 100%;
background-color: rgb(251, 251, 255);
display: flex;
-webkit-box-align: center;
align-items: center;
justify-content: space-between;
padding: 8px 15px;
}
.button {
width: 50%;
height: 32px;
}

View File

@ -0,0 +1,25 @@
import Title from "@/components/Title";
import styles from "./styles.module.css";
interface IQuestionProps {
title: string;
text: string;
}
function Question({ title, text }: IQuestionProps) {
return (
<>
<div className={styles.header}>
<div className={styles["image-container"]}>
<img src="/question.svg" alt="Question icon" />
</div>
<Title variant="h5" className={styles["secondary-title"]}>
{title}
</Title>
</div>
<p className={styles.text}>{text}</p>
</>
);
}
export default Question;

View File

@ -0,0 +1,23 @@
import Title from "@/components/Title";
import styles from "./styles.module.css";
import Question from "./Question";
import { questions } from "@/data/oftenAsk";
function OftenAsk() {
return (
<div className={styles["often-ask"]}>
<Title variant="h2" className={styles.title}>
People often ask
</Title>
<ul>
{questions.map((question, index) => (
<li key={index}>
<Question {...question} />
</li>
))}
</ul>
</div>
);
}
export default OftenAsk;

View File

@ -0,0 +1,48 @@
.often-ask {
display: flex;
flex-direction: column;
-webkit-box-align: center;
align-items: center;
}
.often-ask > ul > li {
width: 100%;
margin-top: 20px;
}
.title {
margin-bottom: 20px;
font-size: 24px;
line-height: 145%;
text-align: center;
color: #333333;
font-weight: 700;
}
.header {
display: flex;
}
.image-container {
width: 32px;
align-self: center;
}
.secondary-title {
flex: 1 1 0%;
font-weight: 600;
font-size: 16px;
line-height: 140%;
color: #0f0f0f;
margin: 0;
text-align: left;
}
.text {
font-weight: 400;
font-size: 14px;
line-height: 140%;
margin-top: 8px;
margin-left: 32px;
color: #333333;
}

View File

@ -0,0 +1,53 @@
import Title from "@/components/Title";
import styles from "./styles.module.css";
import { getPriceFromTrial } from "@/services/price";
import { ISubscriptionPlan } from "@/api/resources/SubscriptionPlans";
import CustomButton from "../CustomButton";
import GuardPayments from "../GuardPayments";
interface IPaymentTableProps {
subPlan: ISubscriptionPlan;
buttonClick: () => void;
}
function PaymentTable({ subPlan, buttonClick }: IPaymentTableProps) {
return (
<>
<div className={styles["payment-table"]}>
<div className={styles.header}>
<span>Special offer</span>
</div>
<div className={styles["table-container"]}>
<Title variant="h3" className={styles.title}>
Personalized reading for{" "}
<span className={styles.purple}>
${getPriceFromTrial(subPlan?.trial)}
</span>
</Title>
<div className={styles["table-element"]}>
<p>Total today:</p>
<span>${getPriceFromTrial(subPlan?.trial)}</span>
</div>
<hr />
<div className={styles["table-element"]}>
<p>Your cost per 2 weeks after trial</p>
<span>${subPlan.price_cents / 100}</span>
</div>
</div>
</div>
<CustomButton className={styles.button} onClick={buttonClick}>
get my reading
</CustomButton>
<GuardPayments />
<p className={styles.policy}>
You are enrolling in 2 weeks subscription. By continuing you agree that
if you don't cancel prior to the end of the 7-day trial for the $5 you
will automatically be charged $29 every 2 weeks until you cancel in
settings. Learn more about cancellation and refund policy in{" "}
<a href="#">Subscription policy</a>
</p>
</>
);
}
export default PaymentTable;

View File

@ -0,0 +1,83 @@
.payment-table {
width: 100%;
margin-top: 24px;
}
.header {
width: 100%;
background: #9974f6;
border-radius: 20px 20px 0px 0px;
padding: 4px 0px;
}
.header > span {
display: block;
width: 100%;
font-weight: 700;
font-size: 14px;
line-height: 120%;
text-align: center;
color: #fbfbff;
}
.table-container {
background: #fbfbff;
border-radius: 0px 0px 20px 20px;
padding: 9px 20px 16px;
}
.table-container > hr {
margin: 10px 0;
}
.title {
font-weight: 700;
font-size: 18px;
line-height: 28px;
color: #0f0f0f;
}
.purple {
color: #7270c0;
}
.table-element {
display: flex;
-webkit-box-pack: justify;
justify-content: space-between;
-webkit-box-align: center;
align-items: center;
}
.table-element > p {
font-weight: 400;
font-size: 14px;
line-height: 24px;
color: #0f0f0f;
}
.table-element > span {
font-weight: 600;
font-size: 16px;
line-height: 22px;
margin-left: 4px;
color: rgb(51, 51, 51);
}
.button {
height: 48px;
margin-top: 26px;
}
.policy {
font-size: 12px;
line-height: 20px;
margin-top: 26px;
color: rgb(51, 51, 51);
text-align: center;
}
.policy > a {
text-decoration: underline;
font-weight: 600;
}

View File

@ -0,0 +1,51 @@
import styles from "./styles.module.css";
interface IPersonalInformationProps {
birthdate: string;
zodiacSign: string;
gender: string;
birthPlace: string;
}
function PersonalInformation({
birthdate,
zodiacSign,
gender,
birthPlace,
}: IPersonalInformationProps) {
return (
<div className={styles["personal-information"]}>
<div className={styles["image-container"]}>
<img
src={`/questionnaire/zodiacs/${gender}/${zodiacSign.toLowerCase()}.webp`}
alt={`${gender} ${zodiacSign}`}
/>
</div>
<div className={styles["text-information"]}>
<ul>
<li>
<h6>Zodiac sign</h6>
<p>{zodiacSign}</p>
</li>
<li>
<h6>Gender</h6>
<p>{gender}</p>
</li>
</ul>
<ul>
<li>
<h6>Date of birth</h6>
<p>{birthdate}</p>
</li>
<li>
<h6>Place of birth</h6>
<p>{birthPlace}</p>
</li>
</ul>
</div>
</div>
);
}
export default PersonalInformation;

View File

@ -0,0 +1,64 @@
.personal-information {
position: absolute;
width: 100%;
height: 340px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
background-color: #eaeef7;
padding: 10px 15px;
}
.image-container {
background: #fbfbff;
width: 100%;
min-height: 100px;
border-top-left-radius: 15px;
border-top-right-radius: 15px;
display: flex;
justify-content: space-around;
padding-top: 10px;
padding-bottom: 6px;
}
.image-container > img {
height: 196px;
}
.text-information {
background: #fbfbff;
border-radius: 0px 0px 20px 20px;
width: 100%;
display: flex;
position: relative;
padding: 8px 12px;
}
.text-information > ul {
flex: 1 1 0%;
display: flex;
flex-wrap: wrap;
gap: 8px;
position: relative;
}
.text-information > ul > li {
width: 100%;
}
.text-information > ul > li > h6 {
font-weight: 700;
font-size: 12px;
line-height: 130%;
color: #454895;
}
.text-information > ul > li > p {
font-size: 14px;
line-height: 130%;
margin-top: 4px;
text-transform: capitalize;
color: #0f0f0f;
}

View File

@ -0,0 +1,45 @@
import Title from "@/components/Title";
import styles from "./styles.module.css";
interface IReviewProps {
username: string;
date: string;
text: string;
mark: number;
}
function Review({ username, date, text, mark }: IReviewProps) {
return (
<div className={styles.review}>
<div className={styles.header}>
<div className={styles.avatar}>
<span>{username.slice(0, 2)}</span>
</div>
<div className={styles.info}>
<Title variant="h6" className={styles.title}>
{username}
</Title>
<p>{date}</p>
</div>
<span
className={styles.stars}
style={{
background: `linear-gradient(
90deg,
rgb(255, 204, 0) ${(mark / 5) * 100}%,
rgb(238, 238, 238) 0px
)`,
}}
></span>
</div>
<div className={styles.content}>
<div className={styles.opostrafs}>
<img src="/opostrafs.svg" alt="Opostrafs" />
</div>
<p className={styles.text}>{text}</p>
</div>
</div>
);
}
export default Review;

View File

@ -0,0 +1,23 @@
import Title from "@/components/Title";
import styles from "./styles.module.css";
import { reviews } from "@/data/reviews";
import Review from "./Review";
function Reviews() {
return (
<div className={styles.reviews}>
<Title variant="h2" className={styles.title}>
Users love us
</Title>
<ul>
{reviews.map((review, index) => (
<li key={index}>
<Review {...review} />
</li>
))}
</ul>
</div>
);
}
export default Reviews;

View File

@ -0,0 +1,96 @@
.reviews {
display: flex;
flex-direction: column;
justify-content: center;
margin-top: 88px;
}
.reviews > ul > li {
width: 100%;
padding: 10px 0;
}
.review {
height: 100%;
padding: 16px;
background: rgb(255, 255, 255);
border-radius: 16px;
}
.header {
display: flex;
}
.avatar {
width: 32px;
height: 32px;
background-color: rgb(210, 209, 249);
border-radius: 50%;
display: flex;
align-self: center;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: center;
justify-content: center;
}
.avatar > span {
font-weight: 600;
font-size: 16px;
line-height: 24px;
background: linear-gradient(
rgb(20, 19, 51) 0%,
rgb(32, 34, 97) 70.63%,
rgb(58, 35, 122) 100%
)
text;
-webkit-text-fill-color: transparent;
}
.info {
flex: 1 1 0%;
margin-left: 6px;
}
.info > .title {
font-weight: 600;
font-size: 16px;
line-height: 130%;
color: #6a3aa2;
margin: 0;
text-align: left;
}
.info > p {
font-size: 12px;
line-height: 130%;
margin-top: 2px;
color: #9b9b9b;
}
.stars {
max-width: 5em;
font-size: 1rem;
height: 1em;
width: 100%;
mask-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTciIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNyAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGcgY2xpcC1wYXRoPSJ1cmwoI2NsaXAwKSI+CjxwYXRoIGQ9Ik04LjQ5OTc4IDEyLjE3NEwzLjc5Nzc4IDE0LjgwNkw0Ljg0Nzc4IDkuNTIwNjVMMC44OTExMTMgNS44NjE5OEw2LjI0MjQ1IDUuMjI3MzJMOC40OTk3OCAwLjMzMzk4NEwxMC43NTcxIDUuMjI3MzJMMTYuMTA4NCA1Ljg2MTk4TDEyLjE1MTggOS41MjA2NUwxMy4yMDE4IDE0LjgwNkw4LjQ5OTc4IDEyLjE3NFoiIGZpbGw9IiNGMkM5NEMiLz4KPC9nPgo8ZGVmcz4KPGNsaXBQYXRoIGlkPSJjbGlwMCI+CjxyZWN0IHdpZHRoPSIxNiIgaGVpZ2h0PSIxNiIgZmlsbD0id2hpdGUiIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAuNSkiLz4KPC9jbGlwUGF0aD4KPC9kZWZzPgo8L3N2Zz4K);
mask-position: 0px 0px;
mask-size: 1em 1em;
}
.content {
display: flex;
margin-top: 10px;
}
.opostrafs {
width: 32px;
}
.text {
flex: 1 1 0%;
font-size: 14px;
margin-left: 6px;
line-height: 140%;
color: rg#0f0f0f;
}

View File

@ -0,0 +1,37 @@
import Title from "@/components/Title";
import styles from "./styles.module.css";
function YouGet() {
return (
<div className={styles["you-get"]}>
<Title variant="h2" className={styles.title}>
What you get
</Title>
<ul>
<li>
<img src="/check-mark-purple.svg" alt="Check mark" />
Your personalised plan
</li>
<li>
<img src="/check-mark-purple.svg" alt="Check mark" />
1:1 advice from your own astrologer
</li>
<li>
<img src="/check-mark-purple.svg" alt="Check mark" />
Finding the most compatible partner
</li>
<li>
<img src="/check-mark-purple.svg" alt="Check mark" />
Insights into your relationship patterns, and emotional and sexual
needs
</li>
<li>
<img src="/check-mark-purple.svg" alt="Check mark" />
Better understanding of yourself
</li>
</ul>
</div>
);
}
export default YouGet;

View File

@ -0,0 +1,30 @@
.you-get {
display: flex;
flex-direction: column;
-webkit-box-align: center;
align-items: center;
width: 100%;
box-sizing: border-box;
margin-bottom: 40px;
padding: 0px 15px;
margin-top: 40px;
max-width: unset;
}
.title {
margin-bottom: 20px;
font-size: 24px;
font-weight: 700;
line-height: 145%;
text-align: center;
color: #333333;
}
.you-get > ul > li {
display: flex;
align-items: center;
gap: 6px;
font-size: 15px;
line-height: 140%;
margin-bottom: 10px;
}

View File

@ -0,0 +1,77 @@
import Title from "@/components/Title";
import styles from "./styles.module.css";
import CustomButton from "../CustomButton";
interface IYourReadingProps {
gender: string;
zodiacSign: string;
buttonClick: () => void;
}
function YourReading({ gender, zodiacSign, buttonClick }: IYourReadingProps) {
return (
<div className={styles["your-reading"]}>
<Title variant="h3" className={styles.title}>
Your reading
</Title>
<div className={styles["image-container"]}>
<img
src={`/questionnaire/zodiacs/${gender}/${zodiacSign.toLowerCase()}.webp`}
alt={`${gender} ${zodiacSign}`}
/>
</div>
<div className={styles["text-container"]}>
<Title variant="h5" className={styles["secondary-title"]}>
Content
</Title>
<ol>
<li>
<p>Compatibility with your partner in other areas of your life.</p>
</li>
<li>
<p>
Deep analysis of the relationships with your partner based on a
unique birth chart matching system
</p>
</li>
<li>
<p>
Simple and actionable guide to improving your relationship with
your partner
</p>
</li>
<li>
<p>
Warning about astrological events and practical advice that will
help you get through this period well
</p>
</li>
<li>
<p>Your horoscope and upcoming events for 2023</p>
</li>
<li>
<p>
Your unique strengths and weaknesses and how to get the most out
of them
</p>
</li>
</ol>
<Title variant="h5" className={styles["secondary-title"]}>
Personality
</Title>
<p className={styles["personality-information"]}>
Personality information
</p>
</div>
<div className={styles["cover-container"]}>
<div className={styles["cover"]}></div>
<p>To read the full reading you need get access</p>
<CustomButton className={styles.button} onClick={buttonClick}>
get my reading
</CustomButton>
</div>
</div>
);
}
export default YourReading;

View File

@ -0,0 +1,137 @@
.your-reading {
position: relative;
width: 100%;
background: #fbfbff;
border-radius: 20px;
padding-top: 20px;
padding-left: 6px;
padding-right: 6px;
margin-top: 40px;
}
.title {
font-weight: 700;
font-size: 24px;
line-height: 135%;
width: 100%;
text-align: center;
color: #333333;
}
.image-container {
display: flex;
-webkit-box-align: center;
align-items: center;
justify-content: center;
background: #eaeef7;
border-radius: 20px;
padding: 16px 20px;
margin-top: 20px;
}
.image-container > img {
height: 146px;
}
.text-container {
width: 100%;
padding: 0px 20px 20px;
margin-top: 20px;
}
.secondary-title {
font-weight: 700;
font-size: 18px;
line-height: 140%;
width: 100%;
text-align: center;
color: #333333;
}
.text-container > ol {
list-style: none;
counter-reset: item 0;
margin-top: 12px;
margin-bottom: 20px;
}
.text-container > ol > li::before {
content: counter(item) ". ";
counter-increment: item 1;
font-weight: 600;
font-size: 14px;
color: #333333;
}
.text-container > ol > li > p {
font-weight: 600;
font-size: 14px;
line-height: 150%;
display: inline;
color: #333333;
}
.personality-information {
font-weight: 400;
font-size: 14px;
line-height: 150%;
margin-top: 12px;
color: #333333;
}
.cover-container {
position: absolute;
bottom: 0px;
left: 0px;
width: 100%;
height: 50%;
background: linear-gradient(
rgba(251, 251, 255, 0) 3.34%,
rgba(251, 251, 255, 0.8) 24.99%,
rgba(251, 251, 255, 0.9) 45.63%,
rgba(255, 251, 251, 0.9) 69.74%,
rgb(255, 240, 240) 89.93%
);
border-radius: 20px;
padding-top: 34px;
display: flex;
flex-direction: column;
-webkit-box-pack: end;
justify-content: end;
z-index: 1;
}
.cover {
position: absolute;
width: 100%;
background: linear-gradient(
rgba(251, 251, 255, 0) 0%,
rgba(251, 251, 255, 0.18) 22.4%,
rgba(251, 251, 255, 0.16) 43.75%,
rgba(251, 251, 255, 0.14) 75%,
rgba(251, 251, 255, 0.196) 89.58%
);
backdrop-filter: blur(1.5px);
border-radius: 20px;
height: 100%;
padding-bottom: 48px;
}
.cover-container > p {
position: relative;
font-weight: 600;
line-height: 140%;
width: 240px;
margin: 0px auto 28px;
text-align: center;
color: #6a3aa2;
z-index: 2;
}
.button {
position: relative;
height: 48px;
z-index: 2;
font-size: 18px;
font-weight: 700;
}

View File

@ -0,0 +1,59 @@
import Title from "@/components/Title";
import Header from "./components/Header";
import PersonalInformation from "./components/PersonalInformation";
import styles from "./styles.module.css";
import Goal from "./components/Goal";
import PaymentTable from "./components/PaymentTable";
import { useSelector } from "react-redux";
import { selectors } from "@/store";
import { Navigate, useNavigate } from "react-router-dom";
import routes from "@/routes";
import { getZodiacSignByDate } from "@/services/zodiac-sign";
import YourReading from "./components/YourReading";
import Reviews from "./components/Reviews";
import YouGet from "./components/YouGet";
import OftenAsk from "./components/OftenAsk";
function TrialPaymentPage() {
const navigate = useNavigate();
const subPlan = useSelector(selectors.selectActiveSubPlan);
const birthdate = useSelector(selectors.selectBirthdate);
const zodiacSign = getZodiacSignByDate(birthdate);
const { gender, birthPlace } = useSelector(selectors.selectQuestionnaire);
if (!subPlan) {
return <Navigate to={routes.client.trialChoice()} />;
}
const handleNext = () => {
navigate(routes.client.paymentStripe());
};
return (
<section className={`${styles.page} page`}>
<Header buttonClick={handleNext} />
<PersonalInformation
zodiacSign={zodiacSign}
gender={gender}
birthPlace={birthPlace}
birthdate={birthdate}
/>
<Title variant="h2" className={styles.title}>
Your Personalized Clarity & Love Reading is ready!
</Title>
<Goal />
<PaymentTable subPlan={subPlan} buttonClick={handleNext} />
<YourReading
gender={gender}
zodiacSign={zodiacSign}
buttonClick={handleNext}
/>
<Reviews />
<YouGet />
<OftenAsk />
<PaymentTable subPlan={subPlan} buttonClick={handleNext} />
</section>
);
}
export default TrialPaymentPage;

View File

@ -0,0 +1,16 @@
.page {
background-color: #fff0f0;
height: fit-content;
min-height: 100vh;
min-height: 100dvh;
padding-top: 62px;
padding-bottom: 62px;
}
.title {
margin-top: 360px;
font-size: 24px;
line-height: 145%;
color: #333333;
font-weight: 700;
}

25
src/components/ui/ProgressBarLine/index.tsx Normal file → Executable file
View File

@ -1,15 +1,30 @@
import styles from "./styles.module.css";
interface IProgressBarLineProps {
value: number
delay: number
containerClassName?: string
value: number;
delay: number;
containerClassName?: string;
lineClassName?: string;
lineColor?: string;
}
function ProgressBarLine({ value, delay, containerClassName }: IProgressBarLineProps): JSX.Element {
function ProgressBarLine({
value,
delay,
containerClassName,
lineClassName,
lineColor,
}: IProgressBarLineProps): JSX.Element {
return (
<div className={`${styles.container} ${containerClassName || ""}`}>
<div className={styles.line} style={{ width: `${value}%`, transitionDuration: `${delay}ms` }}></div>
<div
className={`${styles.line} ${lineClassName || ""}`}
style={{
width: `${value}%`,
transitionDuration: `${delay}ms`,
backgroundColor: lineColor,
}}
></div>
</div>
);
}

37
src/data/loadingProfile.ts Executable file
View File

@ -0,0 +1,37 @@
interface IPoint {
title: string;
color: string;
}
export const loadingProfilePoints: IPoint[] = [
{
title: "Your profile",
color: "#908cf2",
},
{
title: "Personality traits",
color: "#55cdf2",
},
{
title: "Relationship Pattern",
color: "#b86ada",
},
];
export const titlesLoadingProfile = [
"Analyzing your profile...",
"Identifying the planetary positions when you were born...",
"Creating your astrological blueprint...",
"Assessing personality profile...",
"Identifying your strengths and weaknesses...",
"Analyzing your compatibility...",
"Analyzing relationship needs...",
"Charting best guidance plan...",
"Predicting future results...",
];
export const modalTitlesLoadingProfile = [
"Do you enjoy time spent alone?",
"Are you adventurous person?",
"Have you ever tried any remedies/rituals?",
];

20
src/data/oftenAsk.ts Executable file
View File

@ -0,0 +1,20 @@
interface IQuestion {
title: string;
text: string;
}
export const questions: IQuestion[] = [
{
title: "How accurate is the astrology reading on this platform?",
text: "The accuracy of an astrology reading can vary and is subjective. Astrology is not an exact science, but many find that it can provide valuable insights and perspectives. Our platform uses advanced algorithms and expert astrologers to provide the most accurate readings possible.",
},
{
title: "Can I get a compatibility reading for relationships?",
text: "Yes, you can get a compatibility reading for relationships. This type of reading includes a comprehensive astrological analysis of two people to assess their compatibility in various areas, including love, communication and shared values.",
},
{
title:
"Are the astrology readings on this platform confidential and private?",
text: "Yes, all readings on our platform are strictly confidential. We respect our users' privacy and ensure that all personal data and readings are securely stored and not shared with third parties without consent.",
},
];

7
src/data/onboarding.ts Executable file
View File

@ -0,0 +1,7 @@
export const onboardingTitles = [
"Based on your answers",
"Weve created your astrological blueprint and guidance plan",
"To help you find your perfect partner",
"And to improve your relationship for good.",
"Lets get started.",
];

27
src/data/reviews.ts Executable file
View File

@ -0,0 +1,27 @@
interface IReview {
username: string;
date: string;
text: string;
mark: number;
}
export const reviews: IReview[] = [
{
username: "ria._.panwar",
date: "02/17/2023",
text: "It was really helpful and had provided me the clarity that I needed for my current relationship situation. It gives me hope that my relationship could still be save. Thank you. Highly recommended!",
mark: 5,
},
{
username: "jp63_",
date: "02/17/2023",
text: "Amazing, absolutely amazing! The affirmations I received and nurturing advice, was worth everything ! Truly, thank you !!",
mark: 5,
},
{
username: "therealslimmazi",
date: "02/17/2023",
text: "It helps me be able to trust my self and my choices for the future by giving me reassurance with the information i get. My goals and dreams are going to happen and and now i trust myself to do as a need and wish",
mark: 4.6,
},
];

6
src/locales/dev.ts Normal file → Executable file
View File

@ -4,7 +4,7 @@ export default {
next: "Next",
date_of_birth: "What's your date of birth?",
privacy_text: "By continuing, you agree to our <eulaLink> and <privacyLink>. Have a question? Reach our support team <clickHere>",
eula: 'EULA',
eula: "Hint's EULA",
here: 'here',
privacy_notice: 'Privacy Notice',
born_time_question: "What time were you born?",
@ -22,9 +22,9 @@ export default {
day: "Day",
we_will_email_you: "We will email you a copy of your wallpaper for easy access.",
your_email: "Your email",
we_dont_share: "We don't share any personal information.",
we_dont_share: "*We don't share any personal information. We'll email you a copy of your program for convenient access.",
continue_agree: 'By clicking "Continue" below, you agree to our <eulaLink> and <privacyLink>.',
_continue_agree: `By clicking "Continue" below you agree to Hint's EULA and Privacy Policy.`,
_continue_agree: `By clicking "Continue" below you agree to <eulaLink> and <privacyLink>.`,
privacy_policy: "Privacy Policy",
continue: 'Continue',
_continue: 'Continue',

View File

@ -1,7 +1,7 @@
import type { UserStatus } from "./types";
const host = "";
export const apiHost = "https://api-web.aura.wit.life";
export const apiHost = "https://api-web-test.aura.wit.life";
const siteHost = "https://aura.wit.life";
const prefix = "api/v1";
@ -69,6 +69,11 @@ const routes = {
relationshipZodiacInfo: () => [host, "relationship-zodiac-info"].join("/"),
satisfiedResult: () => [host, "satisfied-result"].join("/"),
aboutUs: () => [host, "about-us"].join("/"),
loadingProfile: () => [host, "loading-profile"].join("/"),
emailConfirm: () => [host, "email-confirm"].join("/"),
onboarding: () => [host, "onboarding"].join("/"),
trialChoice: () => [host, "trial-choice"].join("/"),
trialPayment: () => [host, "trial-payment"].join("/"),
notFound: () => [host, "404"].join("/"),
},
server: {
@ -141,6 +146,7 @@ export const entrypoints = [
routes.client.home(),
routes.client.breathResult(),
routes.client.magicBall(),
routes.client.trialChoice(),
];
export const isEntrypoint = (path: string) => entrypoints.includes(path);
export const isNotEntrypoint = (path: string) => !isEntrypoint(path);
@ -211,6 +217,10 @@ export const withoutFooterRoutes = [
routes.client.relationshipZodiacInfo(),
routes.client.satisfiedResult(),
routes.client.aboutUs(),
routes.client.emailConfirm(),
routes.client.onboarding(),
routes.client.trialChoice(),
routes.client.trialPayment(),
];
export const withoutFooterPartOfRoutes = [routes.client.questionnaire()];
@ -269,6 +279,8 @@ export const withoutHeaderRoutes = [
routes.client.both(),
routes.client.relationshipZodiacInfo(),
routes.client.satisfiedResult(),
routes.client.onboarding(),
routes.client.trialPayment(),
];
export const hasNoHeader = (path: string) => {
return !withoutHeaderRoutes.includes(`/${path.split("/")[1]}`);

9
src/services/price/index.ts Normal file → Executable file
View File

@ -1,3 +1,5 @@
import { ITrial } from "@/api/resources/SubscriptionPlans";
export const roundToWhole = (value: string | number): number => {
value = Number(value);
if (value % Math.floor(value) !== 0) {
@ -13,3 +15,10 @@ export const removeAfterDot = (value: string): string => {
}
return value.split(".")[0];
};
export const getPriceFromTrial = (trial: ITrial | null) => {
if (!trial) {
return 0;
}
return (trial.price_cents === 100 ? 99 : trial.price_cents || 0) / 100;
};