diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 8afb617..9a1666a 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -186,12 +186,18 @@ function App(): JSX.Element { element={} /> */} + }> + } + /> + }> }> - } - /> + /> */} } diff --git a/src/components/AttentionPage/index.tsx b/src/components/AttentionPage/index.tsx index 783d581..c8895a8 100644 --- a/src/components/AttentionPage/index.tsx +++ b/src/components/AttentionPage/index.tsx @@ -17,7 +17,7 @@ function AttentionPage({ }: AttentionPageProps): JSX.Element { const { t } = useTranslation(); const navigate = useNavigate(); - const handleNext = () => navigate(routes.client.feedback()); + const handleNext = () => navigate(routes.client.priceList()); return (
diff --git a/src/components/Countdown/index.tsx b/src/components/Countdown/index.tsx index ef8f8a7..1d61641 100644 --- a/src/components/Countdown/index.tsx +++ b/src/components/Countdown/index.tsx @@ -1,31 +1,40 @@ -import { useState, useEffect } from 'react' -import { useTranslation } from 'react-i18next' -import './styles.css' +import { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import styles from "./styles.module.css"; type CountdownProps = { - start: number -} + start: number; + className?: string; + timeClassName?: string; +}; -function Countdown({ start }: CountdownProps): JSX.Element { - const { t } = useTranslation() - const [time, setTime] = useState(start * 60 - 1) +function Countdown({ + start, + className, + timeClassName, +}: CountdownProps): JSX.Element { + const { t } = useTranslation(); + const [time, setTime] = useState(start * 60 - 1); const formatTime = (seconds: number) => { - const minutes = Math.floor(seconds / 60) - const remainingSeconds = seconds % 60 - return `${minutes}:${remainingSeconds.toString().padStart(2, '0')}` - } + const minutes = Math.floor(seconds / 60); + const remainingSeconds = seconds % 60; + return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`; + }; useEffect(() => { - if (time === 0) return - const timer = setTimeout(() => setTime(time - 1), 1000) - return () => clearTimeout(timer) - }, [time]) + if (time === 0) return; + const timer = setTimeout(() => setTime(time - 1), 1000); + return () => clearTimeout(timer); + }, [time]); return ( -
-

{t('reserved_for')}{formatTime(time)}

+
+

+ {t("reserved_for")} + {formatTime(time)} +

- ) + ); } -export default Countdown +export default Countdown; diff --git a/src/components/Countdown/styles.css b/src/components/Countdown/styles.module.css similarity index 100% rename from src/components/Countdown/styles.css rename to src/components/Countdown/styles.module.css diff --git a/src/components/DidYouKnowPage/index.tsx b/src/components/DidYouKnowPage/index.tsx index f6da302..abe97f7 100644 --- a/src/components/DidYouKnowPage/index.tsx +++ b/src/components/DidYouKnowPage/index.tsx @@ -11,7 +11,7 @@ import { getZodiacSignByDate } from "@/services/zodiac-sign"; function DidYouKnowPage(): JSX.Element { const { t } = useTranslation(); const navigate = useNavigate(); - const handleNext = () => navigate(routes.client.freePeriodInfo()); + const handleNext = () => navigate(routes.client.feedback()); const birthdate = useSelector(selectors.selectBirthdate); const zodiacSign = getZodiacSignByDate(birthdate); diff --git a/src/components/FeedbackPage/index.tsx b/src/components/FeedbackPage/index.tsx index 3b26183..bdabda9 100644 --- a/src/components/FeedbackPage/index.tsx +++ b/src/components/FeedbackPage/index.tsx @@ -12,7 +12,7 @@ function FeedbackPage(): JSX.Element { const { t } = useTranslation(); const navigate = useNavigate(); const api = useApi(); - const handleNext = () => navigate(routes.client.auth()); + const handleNext = () => navigate(routes.client.freePeriodInfo()); const assetsData = useCallback(async () => { const { assets } = await api.getAssets({ category: String("au") }); diff --git a/src/components/PaymentTable/index.tsx b/src/components/PaymentTable/index.tsx index 2b2b6d6..55ab9b7 100644 --- a/src/components/PaymentTable/index.tsx +++ b/src/components/PaymentTable/index.tsx @@ -1,53 +1,63 @@ -import { useTranslation } from 'react-i18next' -import Price, { Currency, Locale } from './Price' -import './styles.css' +import { useTranslation } from "react-i18next"; +import Price, { Currency, Locale } from "./Price"; +import styles from "./styles.module.css"; +import Countdown from "../Countdown"; type PaymentItem = { - title: string - price: number - description: string -} + title: string; + price: number; + description: string; +}; type PaymentTableProps = { - currency: Currency - locale: Locale - items: PaymentItem[] -} + currency: Currency; + locale: Locale; + items: PaymentItem[]; +}; -function PaymentTable({ currency, locale, items }: PaymentTableProps): JSX.Element { - const { t } = useTranslation() - const total = items.reduce((acc, item) => acc + item.price, 0) - const totalPrice = new Price(total, currency, locale) - const toItem = (item: typeof items[0], idx: number) => { - const price = new Price(item.price, currency, locale) +function PaymentTable({ + currency, + locale, + items, +}: PaymentTableProps): JSX.Element { + const { t } = useTranslation(); + const total = items.reduce((acc, item) => acc + item.price, 0); + const totalPrice = new Price(total, currency, locale); + const toItem = (item: (typeof items)[0], idx: number) => { + const price = new Price(item.price, currency, locale); return ( -
-
-
{item.title}
-
{price.format()}
+
+
+
{item.title}
+
{price.format()}
-
+

{item.description}

{/*

One dollar thirty six cents per day

*/}
- ) - } + ); + }; return ( -
-
-
-
{t('total_today')}
-
{totalPrice.format()}
-
-
- {items.map(toItem)} +
+ +
+
+
+ {t("total_today")} +
+
+ {totalPrice.format()} +
+
{items.map(toItem)}
+
+
+ {t("charged_only", { price: totalPrice.format() })}
-
{t('charged_only', {price: totalPrice.format()})}
- ) + ); } -export default PaymentTable -export { Price, Currency, Locale } +export default PaymentTable; +export { Price, Currency, Locale }; diff --git a/src/components/PaymentTable/styles.css b/src/components/PaymentTable/styles.module.css similarity index 73% rename from src/components/PaymentTable/styles.css rename to src/components/PaymentTable/styles.module.css index 0bfd7f6..9a3cfaa 100644 --- a/src/components/PaymentTable/styles.css +++ b/src/components/PaymentTable/styles.module.css @@ -1,6 +1,22 @@ .payment { position: relative; width: 100%; + margin-top: 16px; +} + +.payment__countdown { + position: absolute; + border-radius: 14px !important; + padding: 10px 16px !important; + width: fit-content; + right: 12px; + top: -18px; + font-size: 14px !important; +} + +.payment__countdown-time { + font-size: 16px; + font-weight: 700; } .payment__table { @@ -11,6 +27,7 @@ padding: 0 25px; width: 100%; margin-bottom: 10px; + padding-top: 6px; } .payment__total, @@ -57,4 +74,4 @@ font-size: 12px; line-height: 1.5; text-align: center; -} \ No newline at end of file +} diff --git a/src/components/SubscriptionPage/index.tsx b/src/components/SubscriptionPage/index.tsx index 11b7161..a016295 100644 --- a/src/components/SubscriptionPage/index.tsx +++ b/src/components/SubscriptionPage/index.tsx @@ -1,7 +1,7 @@ -import { useSelector } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; -import { selectors } from "@/store"; +import { actions, selectors } from "@/store"; import MainButton from "../MainButton"; import Policy from "../Policy"; import Countdown from "../Countdown"; @@ -14,9 +14,15 @@ import Header from "../Header"; import SpecialWelcomeOffer from "../SpecialWelcomeOffer"; import { useState } from "react"; import { ITrial } from "@/api/resources/SubscriptionPlans"; +import { ApiError, extractErrorMessage, useApi } from "@/api"; +import { useAuth } from "@/auth"; +import { getClientLocale, getClientTimezone } from "@/locales"; +import Loader from "../Loader"; +import Title from "../Title"; +import ErrorText from "../ErrorText"; const currency = Currency.USD; -const locale = Locale.EN; +const locale = getClientLocale() as Locale; const getPriceFromTrial = (trial: ITrial | null) => { if (!trial) { @@ -26,9 +32,26 @@ const getPriceFromTrial = (trial: ITrial | null) => { }; function SubscriptionPage(): JSX.Element { - const [isOpenModal, setIsOpenModal] = useState(false); + const api = useApi(); + const timezone = getClientTimezone(); + const { signUp } = useAuth(); const { t } = useTranslation(); + const navigate = useNavigate(); + const dispatch = useDispatch(); + const [isOpenModal, setIsOpenModal] = useState(false); + const [email, setEmail] = useState(""); + const [emailError, setEmailError] = useState( + "Email is invalid" + ); + const [name, setName] = useState(""); + const [nameError, setNameError] = useState("Name is invalid"); + const [isSubmit, setIsSubmit] = useState(false); + const [isAuth, setIsAuth] = useState(false); + const [isLoading, setIsLoading] = useState(false); + const [apiError, setApiError] = useState(null); + const [error, setError] = useState(false); const activeSubPlan = useSelector(selectors.selectActiveSubPlan); + const birthday = useSelector(selectors.selectBirthday); const paymentItems = [ { title: activeSubPlan?.name || "Per 7-Day Trial For", @@ -39,9 +62,52 @@ function SubscriptionPage(): JSX.Element { }, ]; - const navigate = useNavigate(); - const email = useSelector(selectors.selectEmail); - const handleClick = () => navigate(routes.client.paymentMethod()); + 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)); + } + dispatch(actions.status.update("registred")); + setIsLoading(false); + setIsAuth(true); + setTimeout(() => { + navigate(routes.client.paymentMethod()); + }, 1000); + } catch (error) { + console.error(error); + if (error instanceof ApiError) { + setApiError(error as ApiError); + } else { + setError(true); + } + setIsLoading(false); + } + }; + + // const email = useSelector(selectors.selectEmail); + const handleClick = async () => { + setIsSubmit(true); + + if (!isValidEmail(email) || !isValidName(name)) { + return; + } + await authorization(); + console.log("sdvdvsdv"); + // navigate(routes.client.paymentMethod()) + }; const handleCross = () => setIsOpenModal(true); const policyLink = ( @@ -49,21 +115,110 @@ function SubscriptionPage(): JSX.Element { ); + const isValidEmail = (email: string) => { + return /\S+@\S+\.\S+/.test(email); + }; + + const isValidName = (name: string) => { + return !!(name.length > 0 && name.length < 30); + }; + + const handleChangeEmail = (event: React.ChangeEvent) => { + const email = event.target.value; + if (!isValidEmail(email)) { + setEmailError("Email is invalid"); + } else { + setEmailError(null); + } + setEmail(email); + }; + + const handleChangeName = (event: React.ChangeEvent) => { + const name = event.target.value; + if (!isValidName(name)) { + setNameError("Name is invalid"); + } else { + setNameError(null); + } + setName(name); + }; + return ( <>
- + {/* */}
- + {/* */} +
+
+ + {isSubmit && !!nameError && ( + + {nameError} + + )} +
+
+ + {isSubmit && !!emailError && ( + + {emailError} + + )} +
+ {isLoading && + isSubmit && + isValidEmail(email) && + isValidName(name) && } + {(error || apiError) && ( + + Something went wrong + + )} + {apiError && ( + + )} + {!apiError && !error && !isLoading && isAuth && ( + Success Icon + )} +
- {t("get_access")} + + Start ${getPriceFromTrial(activeSubPlan?.trial || null)} +
<> diff --git a/src/components/SubscriptionPage/styles.module.css b/src/components/SubscriptionPage/styles.module.css index 15061b0..d25d4da 100644 --- a/src/components/SubscriptionPage/styles.module.css +++ b/src/components/SubscriptionPage/styles.module.css @@ -15,4 +15,45 @@ .cross { left: 28px; -} \ No newline at end of file +} + +.inputs-container { + width: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; + margin-top: 16px; +} + +.inputs-container__input { + width: 100%; + border: solid #000 2px; + border-radius: 20px; + font-size: 17px; + font-weight: 400; + line-height: 20px; + padding: 16px 25px; +} + +.inputs-container__input-container { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + width: 100%; +} + +.inputs-container__input-error { + border-color: red; +} + +.inputs-container__label-error { + font-size: 12px; + font-weight: 400; + color: red; + width: 100%; + padding-left: 25px; +} diff --git a/src/locales/dev.ts b/src/locales/dev.ts index b428b63..917a9da 100644 --- a/src/locales/dev.ts +++ b/src/locales/dev.ts @@ -11,7 +11,7 @@ export default { nasa_data_using: "We use NASA data to determine the exact position of the planets in the sky at the time of your birth to create wallpapers that are just right for you.", cta_title: "Start your 7-day trial", cta_subtitle: "No pressure. Cancel anytime.", - reserved_for: "Reserved for ", + reserved_for: "RESERVED for ", creating_profile: "Creating your profile", zodiac_analysis: "Zodiac data analysis", drawing_wallpaper: "Drawing Wallpapers",