From 5519769c7306ca9c4dfa0ea877d050c4e3709ba7 Mon Sep 17 00:00:00 2001 From: "Aidar Shaikhutdin @makeweb.space" Date: Thu, 4 May 2023 20:56:34 +0600 Subject: [PATCH] Add all pages except profile creating page --- src/components/App/index.tsx | 24 ++++--- src/components/App/styles.css | 3 +- src/components/BirthdayPage/index.tsx | 36 ++++++++-- src/components/BirthtimePage/index.tsx | 27 ++++++-- src/components/CallToAction/index.tsx | 12 ++++ src/components/CallToAction/styles.css | 12 ++++ src/components/Countdown/index.tsx | 29 ++++++++ src/components/Countdown/styles.css | 10 +++ src/components/CreateProfilePage/index.tsx | 14 +++- src/components/DateControl/index.tsx | 33 --------- src/components/DateTimePicker/DateInput.tsx | 40 +++++++++++ src/components/DateTimePicker/DatePicker.tsx | 67 +++++++++++++++++++ src/components/DateTimePicker/ErrorText.tsx | 15 +++++ src/components/DateTimePicker/TimePicker.tsx | 53 +++++++++++++++ src/components/DateTimePicker/index.ts | 5 ++ .../styles.css | 40 +++++++---- src/components/DateTimePicker/utils.ts | 23 +++++++ src/components/EmailEnterPage/index.tsx | 48 +++++++++++++ src/components/EmailInput/index.tsx | 37 ++++++++++ src/components/EmailInput/styles.css | 47 +++++++++++++ src/components/Header/index.tsx | 1 + src/components/Header/styles.css | 7 ++ src/components/Payment/Price.ts | 44 ++++++++++++ src/components/Payment/index.tsx | 53 +++++++++++++++ src/components/Payment/styles.css | 60 +++++++++++++++++ src/components/Policy/index.tsx | 60 +++++++++-------- src/components/Policy/styles.css | 18 ++++- src/components/Purposes/styles.css | 1 - src/components/SubscriptionPage/index.tsx | 43 ++++++++++++ src/components/TimeControl/index.tsx | 33 --------- src/components/Title/styles.css | 1 - src/components/UserHeader/index.tsx | 16 +++++ src/components/UserHeader/styles.css | 23 +++++++ src/index.css | 9 +++ src/routes.ts | 2 +- src/types.ts | 8 +++ 36 files changed, 818 insertions(+), 136 deletions(-) create mode 100644 src/components/CallToAction/index.tsx create mode 100644 src/components/CallToAction/styles.css create mode 100644 src/components/Countdown/index.tsx create mode 100644 src/components/Countdown/styles.css delete mode 100644 src/components/DateControl/index.tsx create mode 100644 src/components/DateTimePicker/DateInput.tsx create mode 100644 src/components/DateTimePicker/DatePicker.tsx create mode 100644 src/components/DateTimePicker/ErrorText.tsx create mode 100644 src/components/DateTimePicker/TimePicker.tsx create mode 100644 src/components/DateTimePicker/index.ts rename src/components/{DateControl => DateTimePicker}/styles.css (70%) create mode 100644 src/components/DateTimePicker/utils.ts create mode 100644 src/components/EmailEnterPage/index.tsx create mode 100644 src/components/EmailInput/index.tsx create mode 100644 src/components/EmailInput/styles.css create mode 100644 src/components/Payment/Price.ts create mode 100644 src/components/Payment/index.tsx create mode 100644 src/components/Payment/styles.css create mode 100644 src/components/SubscriptionPage/index.tsx delete mode 100644 src/components/TimeControl/index.tsx create mode 100644 src/components/UserHeader/index.tsx create mode 100644 src/components/UserHeader/styles.css create mode 100644 src/types.ts diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index cccec4e..1f77d84 100644 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -2,6 +2,8 @@ import { Routes, Route, Navigate } from 'react-router-dom'; import BirthdayPage from '../BirthdayPage' import BirthtimePage from '../BirthtimePage' import CreateProfilePage from '../CreateProfilePage' +import EmailEnterPage from '../EmailEnterPage' +import SubscriptionPage from '../SubscriptionPage' import NotFoundPage from '../NotFoundPage' import Header from '../Header' import routes from '../../routes' @@ -12,17 +14,17 @@ function App() {
-
- - - } /> - } /> - } /> - } /> - } /> - -
+ + + } /> + } /> + } /> + } /> + } /> + } /> + } /> +
) diff --git a/src/components/App/styles.css b/src/components/App/styles.css index 932079e..9421b69 100644 --- a/src/components/App/styles.css +++ b/src/components/App/styles.css @@ -8,10 +8,9 @@ } .content { - display: flex; + width: 100%; height: 100vh; position: relative; - width: 100%; } .page { diff --git a/src/components/BirthdayPage/index.tsx b/src/components/BirthdayPage/index.tsx index 9364231..1271202 100644 --- a/src/components/BirthdayPage/index.tsx +++ b/src/components/BirthdayPage/index.tsx @@ -1,26 +1,50 @@ +import { useEffect, useState } from 'react' import { useNavigate } from 'react-router-dom' import routes from '../../routes' import Policy from '../Policy' import Purposes from '../Purposes' import Title from '../Title' -import DateControl from '../DateControl' +import { DatePicker } from '../DateTimePicker' import MainButton from '../MainButton' import './styles.css' function BirthdayPage(): JSX.Element { const navigate = useNavigate(); + const [birthdate, setBirthdate] = useState('') + const [isDisabled, setIsDisabled] = useState(true) + const links = [ + { text: 'EULA', href: 'https://aura.wit.life/terms' }, + { text: 'Privacy Notice', href: 'https://aura.wit.life/privacy' }, + { text: 'here', href: 'https://aura.wit.life/' }, + ] const handleNext = () => navigate(routes.client.birthtime()) + const handleValid = (birthdate: string) => { + setBirthdate(birthdate) + setIsDisabled(false) + } + + useEffect(() => { + console.log('birthdate', birthdate) + }, [birthdate]) + return ( - <> +
Let's start! What's your date of birth? - - + setIsDisabled(true)} + /> +
- + + By continuing, you agree to our EULA and Privacy Notice. Have a question? Reach our support team here +
- +
) } diff --git a/src/components/BirthtimePage/index.tsx b/src/components/BirthtimePage/index.tsx index 0aa2da7..2c30f18 100644 --- a/src/components/BirthtimePage/index.tsx +++ b/src/components/BirthtimePage/index.tsx @@ -1,20 +1,35 @@ +import { useState, useEffect } from "react" import { useNavigate } from "react-router-dom" import Title from "../Title" import MainButton from "../MainButton" -import TimeControl from "../TimeControl" +import { TimePicker } from "../DateTimePicker" import routes from "../../routes" import './styles.css' function BirthtimePage(): JSX.Element { const navigate = useNavigate(); + const [birthtime, setBirhtime] = useState('') + const [isDisabled, setIsDisabled] = useState(true) const handleNext = () => navigate(routes.client.createProfile()) + + useEffect(() => { + if (!birthtime) { + setIsDisabled(true) + return + } + setIsDisabled(false) + console.log('birthtime', birthtime) + }, [birthtime]) + return ( - <> +
What time were you born? -

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.

- - - +

+ 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. +

+ setBirhtime(value)}/> + +
) } diff --git a/src/components/CallToAction/index.tsx b/src/components/CallToAction/index.tsx new file mode 100644 index 0000000..375c05f --- /dev/null +++ b/src/components/CallToAction/index.tsx @@ -0,0 +1,12 @@ +import './styles.css' + +function CallToAction(): JSX.Element { + return ( +
+

Start your 7-day trial

+

No pressure. Cancel anytime.

+
+ ) +} + +export default CallToAction diff --git a/src/components/CallToAction/styles.css b/src/components/CallToAction/styles.css new file mode 100644 index 0000000..89edc0b --- /dev/null +++ b/src/components/CallToAction/styles.css @@ -0,0 +1,12 @@ +.call-to-action { + text-align: center; +} + +.call-to-action > h1 { + line-height: 1.5; +} + +.call-to-action > p { + font-size: 18px; + font-weight: 500; +} diff --git a/src/components/Countdown/index.tsx b/src/components/Countdown/index.tsx new file mode 100644 index 0000000..35b0665 --- /dev/null +++ b/src/components/Countdown/index.tsx @@ -0,0 +1,29 @@ +import { useState, useEffect } from 'react' +import './styles.css' + +type CountdownProps = { + start: number +} + +function Countdown({ start }: CountdownProps): JSX.Element { + 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')}` + } + + useEffect(() => { + if (time === 0) return + const timer = setTimeout(() => setTime(time - 1), 1000) + return () => clearTimeout(timer) + }, [time]) + + return ( +
+

Reserved for {formatTime(time)}

+
+ ) +} + +export default Countdown diff --git a/src/components/Countdown/styles.css b/src/components/Countdown/styles.css new file mode 100644 index 0000000..90c3021 --- /dev/null +++ b/src/components/Countdown/styles.css @@ -0,0 +1,10 @@ +.countdown { + background-color: #000; + color: #fff; + text-align: center; + font-size: 18px; + font-weight: 400; + line-height: 1; + padding: 10px 30px; + border-radius: 2px; +} diff --git a/src/components/CreateProfilePage/index.tsx b/src/components/CreateProfilePage/index.tsx index cfb75f7..f9042a6 100644 --- a/src/components/CreateProfilePage/index.tsx +++ b/src/components/CreateProfilePage/index.tsx @@ -1,10 +1,20 @@ +import { useEffect } from "react" +import { useNavigate } from "react-router-dom" import Title from "../Title" +import routes from "../../routes" function CreateProfilePage(): JSX.Element { + const navigate = useNavigate() + + useEffect(() => { + const timerId = setTimeout(() => navigate(routes.client.emailEnter()), 3000) + return () => clearTimeout(timerId) + }, [navigate]) + return ( - <> +
Creating your profile - +
) } diff --git a/src/components/DateControl/index.tsx b/src/components/DateControl/index.tsx deleted file mode 100644 index ba433f5..0000000 --- a/src/components/DateControl/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import './styles.css' - -function DateControl(): JSX.Element { - return ( -
-
-
-

Year

- -
-
-

Month

- -
-
-

Day

- -
-
-
- ) -} - -export default DateControl diff --git a/src/components/DateTimePicker/DateInput.tsx b/src/components/DateTimePicker/DateInput.tsx new file mode 100644 index 0000000..8d7bc26 --- /dev/null +++ b/src/components/DateTimePicker/DateInput.tsx @@ -0,0 +1,40 @@ +import { FormField } from '../../types' +import { normalize, calculateMaxValue } from './utils' + +type DatePartValue = number | undefined + +type DateInputProps = Omit, 'onValid' | 'onInvalid'> & { + max: number + maxLength: number + onChange: (part: number) => void +} + +function DateInput(props: DateInputProps): JSX.Element { + const { label, placeholder, name, value, max, maxLength, onChange } = props + const validate = (value: number): boolean => value >= 0 && value <= max + const handleChange = (e: React.ChangeEvent) => { + const datePart = parseInt(e.target.value, 10) + if (isNaN(datePart)) return + if (datePart > calculateMaxValue(maxLength)) return + if (!validate(datePart)) return + onChange(datePart) + } + return ( +
+

{label}

+ +
+ ) +} + +export default DateInput diff --git a/src/components/DateTimePicker/DatePicker.tsx b/src/components/DateTimePicker/DatePicker.tsx new file mode 100644 index 0000000..668f53a --- /dev/null +++ b/src/components/DateTimePicker/DatePicker.tsx @@ -0,0 +1,67 @@ +import { useEffect, useState } from 'react' +import { FormField } from '../../types' +import DateInput from './DateInput' +import ErrorText from './ErrorText' +import { stringify, getMaxYear, isNotTheDate, getDaysInMonth } from './utils' + +export function DatePicker(props: FormField): JSX.Element { + const { name, value, onValid, onInvalid } = props + + const date = new Date(value) + + const [year, setYear] = useState(date.getFullYear()) + const [month, setMonth] = useState(date.getMonth()) + const [day, setDay] = useState(date.getDate()) + const [hasError, setHasError] = useState(false) + + useEffect(() => { + if (isNaN(year) || isNaN(month) || isNaN(day)) return + const combinedDate = `${year}-${month}-${day}` + const date = new Date(combinedDate) + if (isNotTheDate(date)) { + setHasError(true) + onInvalid() + return + } + setHasError(false) + onValid(stringify(date)) + }, [year, month, day, hasError, onValid, onInvalid]) + + return ( +
+
+ setYear(year)} + /> + setMonth(month)} + /> + setDay(day)} + /> +
+ + + ) +} diff --git a/src/components/DateTimePicker/ErrorText.tsx b/src/components/DateTimePicker/ErrorText.tsx new file mode 100644 index 0000000..789b39f --- /dev/null +++ b/src/components/DateTimePicker/ErrorText.tsx @@ -0,0 +1,15 @@ +type ErrorTextProps = { + isShown: boolean + message: string +} + +function ErrorText({ message, isShown }: ErrorTextProps): JSX.Element { + const className = isShown ? 'date-picker__error--shown' : '' + return ( +

+ {message} +

+ ) +} + +export default ErrorText diff --git a/src/components/DateTimePicker/TimePicker.tsx b/src/components/DateTimePicker/TimePicker.tsx new file mode 100644 index 0000000..90be240 --- /dev/null +++ b/src/components/DateTimePicker/TimePicker.tsx @@ -0,0 +1,53 @@ +import { useEffect, useState } from "react" +import { normalize } from "./utils" + +type TimePickerProps = { + onChange: (value: string) => void +} + +export function TimePicker({ onChange }: TimePickerProps): JSX.Element { + const [hour, setHour] = useState('1') + const [minute, setMinute] = useState('0') + const [period, setPeriod] = useState('AM') + + useEffect(() => { + const formattedHour = period === 'AM' ? normalize(hour, 2) : String(Number(hour) + 12) + const formattedMinute = normalize(minute, 2) + onChange(`${formattedHour}:${formattedMinute}`) + }, [hour, minute, period, onChange]) + + return ( +
+
+
+ +
+
+ +
+
+ +
+
+
+ ) +} diff --git a/src/components/DateTimePicker/index.ts b/src/components/DateTimePicker/index.ts new file mode 100644 index 0000000..ce1faa3 --- /dev/null +++ b/src/components/DateTimePicker/index.ts @@ -0,0 +1,5 @@ +import './styles.css' + +export * from './DatePicker' +export * from './TimePicker' +export * from './utils' diff --git a/src/components/DateControl/styles.css b/src/components/DateTimePicker/styles.css similarity index 70% rename from src/components/DateControl/styles.css rename to src/components/DateTimePicker/styles.css index 5abfaa7..17d426d 100644 --- a/src/components/DateControl/styles.css +++ b/src/components/DateTimePicker/styles.css @@ -1,30 +1,30 @@ -.date-control { +.date-picker { + margin: 0 auto; margin-bottom: 24px; position: relative; width: 100%; + max-width: 400px; } -.date-control__container { +.date-picker__container { grid-gap: 12px; background-color: #fff; display: grid; gap: 12px; grid-template-columns: repeat(3,1fr); - max-width: 400px; position: relative; width: 100%; z-index: 3; - margin: 0 auto; } -.date-control__field-label { +.date-picker__field-label { color: #6b7baa; font-size: 12px; line-height: 16px; margin: 0 0 6px 6px; } -.date-control__input { +.date-picker__input { display: block; font-size: 16px; width: 100%; @@ -33,7 +33,7 @@ margin-bottom: 0; } -.date-control__input > input { +.date-picker__input > input { appearance: none; border-radius: 8px; color: #121620; @@ -51,12 +51,12 @@ padding-top: 5px; } -.date-control__input > input:focus { +.date-picker__input > input:focus { border-color: #066fde; transition-delay: .1s; } -.date-control__input-placeholder { +.date-picker__input-placeholder { color: #6b7baa; font-size: 16px; left: 12px; @@ -69,15 +69,15 @@ transform: translateY(-50%); } -.date-control__input input:focus + .date-control__input-placeholder, -.date-control__input input:not(:placeholder-shown) + .date-control__input-placeholder { +.date-picker__input input:focus + .date-picker__input-placeholder, +.date-picker__input input:not(:placeholder-shown) + .date-picker__input-placeholder { display: none; font-size: 12px; top: 12px; width: auto; } -.date-control__field-select { +.date-picker__field-select { display: block; font-size: 16px; width: 100%; @@ -97,3 +97,19 @@ border: 2px solid #dee5f9; padding-top: 5px; } + +.date-picker__error { + color: #ff5c5d; + font-size: 12px; + left: 12px; + line-height: 16px; + margin-left: 12px; + position: absolute; + transform: translateY(-32px); + transition: all .5s; +} + +.date-picker__error--shown { + position: static; + transform: translateY(6px); +} diff --git a/src/components/DateTimePicker/utils.ts b/src/components/DateTimePicker/utils.ts new file mode 100644 index 0000000..e58eff0 --- /dev/null +++ b/src/components/DateTimePicker/utils.ts @@ -0,0 +1,23 @@ +export const normalize = (value: number | string, max: number): string => { + return String(value).padStart(max, '0') +} + +export const calculateMaxValue = (digits: number): number => { + return Math.pow(10, digits) - 1; +} + +export const getMaxYear = (): number => { + return new Date().getFullYear() +} + +export const stringify = (value: Date): string => { + return value.toISOString().split('T')[0] +} + +export const isNotTheDate = (date: Date) => { + return date.toString() === 'Invalid Date' +} + +export const getDaysInMonth = (year: number, month: number): number => { + return new Date(year, month, 0).getDate(); +} diff --git a/src/components/EmailEnterPage/index.tsx b/src/components/EmailEnterPage/index.tsx new file mode 100644 index 0000000..f328dc2 --- /dev/null +++ b/src/components/EmailEnterPage/index.tsx @@ -0,0 +1,48 @@ +import { useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import Title from '../Title' +import Policy from '../Policy' +import EmailInput from '../EmailInput' +import MainButton from '../MainButton' +import routes from '../../routes' + +function EmailEnterPage(): JSX.Element { + const navigate = useNavigate() + const [email, setEmail] = useState('') + const [isDisabled, setIsDisabled] = useState(true) + const links = [ + { text: 'EULA', href: 'https://aura.wit.life/terms' }, + { text: 'Privacy Policy', href: 'https://aura.wit.life/privacy' }, + ] + const handleValidEmail = (email: string) => { + setEmail(email) + setIsDisabled(false) + } + const handleClick = () => navigate(routes.client.subscription()) + + useEffect(() => { + console.log('email', email) + }, [email]) + + return ( +
+ + We will email you a copy of your wallpaper for easy access. + + setIsDisabled(true)} + /> +

We don't share any personal information.

+ + By clicking "Continue" below, you agree to our EULA and Privacy Policy. + + +
+ ) +} + +export default EmailEnterPage diff --git a/src/components/EmailInput/index.tsx b/src/components/EmailInput/index.tsx new file mode 100644 index 0000000..208cd6f --- /dev/null +++ b/src/components/EmailInput/index.tsx @@ -0,0 +1,37 @@ +import { useState } from 'react' +import { FormField } from '../../types' +import './styles.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()) +} + +function EmailInput(props: FormField): JSX.Element { + const { name, value, placeholder, onValid, onInvalid } = props + const [email, setEmail] = useState(value) + const handleChange = (event: React.ChangeEvent) => { + const inputEmail = event.target.value + setEmail(inputEmail) + if (isValidEmail(inputEmail)) { + onValid(inputEmail) + } else { + onInvalid() + } + } + + return ( +
+ + {placeholder} +
+ ) +} + +export default EmailInput diff --git a/src/components/EmailInput/styles.css b/src/components/EmailInput/styles.css new file mode 100644 index 0000000..48e5d69 --- /dev/null +++ b/src/components/EmailInput/styles.css @@ -0,0 +1,47 @@ +.email-input { + width: 100%; + position: relative; + text-align: center; + margin-bottom: 20px; + max-width: 400px; + min-width: 250px; +} + +.email-input > input { + appearance: none; + border: 1px solid #c7c7c7; + border-radius: 8px; + color: #121620; + font-size: 16px; + height: 48px; + line-height: 18px; + outline: none; + padding: 12px 12px 5px; + transition: border-color .3s ease; + width: 100%; +} + +.email-input > input:focus { + border-color: #000; + transition-delay: .1s; +} + +.email-input > input:focus + .email-input__placeholder, +.email-input > input:not(:placeholder-shown) + .email-input__placeholder { + font-size: 12px; + top: 12px; + width: auto; +} + +.email-input__placeholder { + color: #8e8e93; + font-size: 16px; + left: 12px; + overflow: hidden; + text-overflow: ellipsis; + transition: top .3s ease,color .3s ease,font-size .3s ease; + white-space: nowrap; + position: absolute; + top: 50%; + transform: translateY(-50%); +} diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx index c3ddb5e..e92ca7c 100644 --- a/src/components/Header/index.tsx +++ b/src/components/Header/index.tsx @@ -33,6 +33,7 @@ function Header(): JSX.Element {
{ showBackButton ? : null } logo + Aura
) } diff --git a/src/components/Header/styles.css b/src/components/Header/styles.css index 7474cc4..0c5773b 100644 --- a/src/components/Header/styles.css +++ b/src/components/Header/styles.css @@ -8,3 +8,10 @@ position: relative; width: 100%; } + +.header__title { + font-size: 24px; + font-weight: 600; + margin-left: 10px; + text-transform: uppercase; +} diff --git a/src/components/Payment/Price.ts b/src/components/Payment/Price.ts new file mode 100644 index 0000000..0bbf695 --- /dev/null +++ b/src/components/Payment/Price.ts @@ -0,0 +1,44 @@ +export enum Currency { + USD = 'USD', + EUR = 'EUR', +} + +export enum Locale { + EN = 'en-US', + FR = 'fr-FR', +} + +class Price { + private _value: number + private _currency: Currency + private _locale: Locale + + constructor(value: number, currency: Currency, locale: Locale = Locale.EN) { + this._value = value + this._currency = currency + this._locale = locale + } + + format(): string { + const options: Intl.NumberFormatOptions = { + style: 'currency', + currency: this._currency, + }; + return new Intl.NumberFormat(this._locale, options).format(this._value) + } + + toSentence(): string { + // TODO: implement + // 1.36 => One dollar thirty six cents + // 1.00 => One dollar + // 1.01 => One dollar one cent + // 2.00 => Two dollars + // 2.01 => Two dollars one cent + // 2.02 => Two dollars two cents + // 2.10 => Two dollars ten cents + // and so one... + return '' + } +} + +export default Price diff --git a/src/components/Payment/index.tsx b/src/components/Payment/index.tsx new file mode 100644 index 0000000..bb7e866 --- /dev/null +++ b/src/components/Payment/index.tsx @@ -0,0 +1,53 @@ +import Price, { Currency, Locale } from './Price' +import './styles.css' + +type PaymentItem = { + title: string + price: number + description: string +} + +type PaymentProps = { + currency: Currency + locale: Locale + items: PaymentItem[] +} + +function Payment({ currency, locale, items }: PaymentProps): JSX.Element { + 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.description}

+

One dollar thirty six cents per day

+
+
+ ) + } + return ( +
+
+
+
Total today
+
{totalPrice.format()}
+
+
+ {items.map(toItem)} +
+
+
+ You will be charged only $1 for your 7-day trial. We'll email you a reminder before your trial period ends. Cancel anytime. +
+
+ ) +} + +export default Payment +export { Price, Currency, Locale } diff --git a/src/components/Payment/styles.css b/src/components/Payment/styles.css new file mode 100644 index 0000000..36b38d8 --- /dev/null +++ b/src/components/Payment/styles.css @@ -0,0 +1,60 @@ +.payment { + position: relative; + width: 100%; + margin-bottom: 24px; +} + +.payment__table { + background: #fff; + border: 2px solid #000; + border-radius: 20px; + color: #000; + padding: 0 25px; + width: 100%; + margin-bottom: 10px; +} + +.payment__total, +.payment__item-summary { + display: flex; +} + +.payment__total-price, +.payment__item-price { + margin-left: auto; +} + +.payment__total { + font-size: 20px; + font-weight: 500; + padding: 10px 0; + border-bottom: 1px solid #000; + margin-bottom: 10px; + margin-top: 5px; +} + +.payment__items { + font-size: 16px; + line-height: 1.1; +} + +.payment__item { + margin-bottom: 5px; +} + +.payment__item-description { + font-size: 12px; + font-weight: 400; + color: #8e8e93; +} + +.payment__item-summary { + font-size: 14px; + font-weight: 500; + margin-bottom: 5px; +} + +.payment__information { + font-size: 12px; + line-height: 1.5; +} \ No newline at end of file diff --git a/src/components/Policy/index.tsx b/src/components/Policy/index.tsx index 94d7282..9979f7b 100644 --- a/src/components/Policy/index.tsx +++ b/src/components/Policy/index.tsx @@ -1,39 +1,45 @@ import { ReactNode } from 'react' import './styles.css' -enum PolicyContentType { - Text = 'text', - Link = 'link', +type Link = { + text: string + href: string } -type PolicyContent = { - type: PolicyContentType - content: string - href?: string +interface PolicyProps { + links: Link[] + children: string + sizing?: 'small' | 'medium' | 'large' } -function Policy(): JSX.Element { - const text: PolicyContent[] = [ - { type: PolicyContentType.Text, content: 'By continuing, you agree to our ' }, - { type: PolicyContentType.Link, content: 'EULA', href: 'https://aura.wit.life/terms' }, - { type: PolicyContentType.Text, content: ' and ' }, - { type: PolicyContentType.Link, content: 'Privacy Notice', href: 'https://aura.wit.life/privacy' }, - { type: PolicyContentType.Text, content: '. Have a question? Reach our support team ' }, - { type: PolicyContentType.Link, content: 'here', href: 'https://aura.wit.life/' }, - ] - const toElement = (item: PolicyContent, idx: number) => { - switch (item.type) { - case 'text': - return item.content - case 'link': - return {item.content} - default: - throw new Error(`Unknown type: ${item.type}`) - } +const sizes = { + small: 'policy--small', + medium: 'policy--medium', + large: 'policy--large', +} + +function Policy({ links, children, sizing = 'small' }: PolicyProps): JSX.Element { + const createLinkedContent = (sentence: string): ReactNode[] => { + const pattern = links.map(link => `(${link.text})`).join('|'); + const regex = new RegExp(pattern, 'g'); + return sentence.split(regex).map((part, idx) => { + const link = links.find(({ text }) => text === part); + + if (!link) return part + + return ( + + {link.text} + + ); + }); } - const content = text.map(toElement) - return

{ content }

+ return ( +
+

{createLinkedContent(children)}

+
+ ) } export default Policy diff --git a/src/components/Policy/styles.css b/src/components/Policy/styles.css index d44b2e2..74daa21 100644 --- a/src/components/Policy/styles.css +++ b/src/components/Policy/styles.css @@ -4,13 +4,13 @@ max-width: 400px; width: 100%; margin-top: 20px; + margin-bottom: 6px; text-align: center; } .policy p, .policy a { color: #121620; - font-size: 12px; font-weight: 400; line-height: 18px; } @@ -18,3 +18,19 @@ .policy a { text-decoration: underline; } + +.policy--small p, +.policy--small a { + font-size: 12px; +} + +.policy--medium p, +.policy--medium a { + font-size: 14px; +} + +.policy--large p, +.policy--large a { + font-size: 16px; +} + diff --git a/src/components/Purposes/styles.css b/src/components/Purposes/styles.css index 32f613e..6d83546 100644 --- a/src/components/Purposes/styles.css +++ b/src/components/Purposes/styles.css @@ -2,5 +2,4 @@ color: #8e8e93; font-size: 12px; line-height: 18px; - margin-top: 6px; } diff --git a/src/components/SubscriptionPage/index.tsx b/src/components/SubscriptionPage/index.tsx new file mode 100644 index 0000000..8d64f8d --- /dev/null +++ b/src/components/SubscriptionPage/index.tsx @@ -0,0 +1,43 @@ +import Title from '../Title' +import MainButton from '../MainButton' +import Policy from '../Policy' +import Countdown from '../Countdown' +import Payment, { Currency, Locale } from '../Payment' +import UserHeader from '../UserHeader' +import CallToAction from '../CallToAction' + +function SubscriptionPage(): JSX.Element { + const userEmail = 'some@email.com' + const links = [ + { text: 'Subscription policy', href: 'https://aura.wit.life/' }, + ] + const currency = Currency.USD + const locale = Locale.EN + const paymentItems = [ + { + title: 'Per 7-Day Trial For', + price: 1.00, + description: '2-Week Plan', + }, + ] + const handleClick = () => console.log('What we will do?') + return ( + <> + +
+ + Your personalized Aries Wallpaper has been created! Find your happiness now and get an additional individual horoscope based on your energies. + + + + + + + By proceeding, you agree that if you do not cancel your subscription before the end of the 7-day trial period, you will be automatically charged nineteen US dollars zero cents every 2 weeks until you cancel the subscription in the settings. Learn more about cancellation and refund policy in Subscription policy + +
+ + ) +} + +export default SubscriptionPage diff --git a/src/components/TimeControl/index.tsx b/src/components/TimeControl/index.tsx deleted file mode 100644 index 194ce79..0000000 --- a/src/components/TimeControl/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -function TimeControl(): JSX.Element { - return ( -
-
-
- -
-
- -
-
- -
-
-
- ) -} - -export default TimeControl diff --git a/src/components/Title/styles.css b/src/components/Title/styles.css index e27f9dd..8f773e3 100644 --- a/src/components/Title/styles.css +++ b/src/components/Title/styles.css @@ -1,5 +1,4 @@ .title { - letter-spacing: .2px; line-height: 150%; margin-bottom: 24px; text-align: center; diff --git a/src/components/UserHeader/index.tsx b/src/components/UserHeader/index.tsx new file mode 100644 index 0000000..073c789 --- /dev/null +++ b/src/components/UserHeader/index.tsx @@ -0,0 +1,16 @@ +import './styles.css' + +type UserHeaderProps = { + email: string +} + +function UserHeader({ email }: UserHeaderProps): JSX.Element { + return ( +
+
{email}
+
{email.at(0)?.toUpperCase()}
+
+ ) +} + +export default UserHeader diff --git a/src/components/UserHeader/styles.css b/src/components/UserHeader/styles.css new file mode 100644 index 0000000..c6d405b --- /dev/null +++ b/src/components/UserHeader/styles.css @@ -0,0 +1,23 @@ +.user-header { + display: flex; + align-items: center; + justify-content: flex-end; + padding: 0 32px; + background: #c2ceee; + color: #fff; + width: 100%; + height: 31px; + font-size: 14px; + font-weight: 600; +} + +.user-header__icon { + display: flex; + align-items: center; + justify-content: center; + background: #9babd9; + border-radius: 50%; + width: 27px; + height: 27px; + margin-left: 10px; +} \ No newline at end of file diff --git a/src/index.css b/src/index.css index 0cdb979..e154428 100644 --- a/src/index.css +++ b/src/index.css @@ -3,6 +3,11 @@ font-family: SF Pro Text, sans-serif; } +h1 { + font-size: 26px; + font-weight: 700; +} + h2 { font-size: 24px; font-weight: 600; @@ -89,6 +94,10 @@ a,button,div,input,select,textarea { margin-top: 24px; } +.mb-24 { + margin-bottom: 24px; +} + .pa { position: absolute; } diff --git a/src/routes.ts b/src/routes.ts index 8bd8480..12ab951 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -4,9 +4,9 @@ const prefix = 'api/v1'; const routes = { client: { root: () => [host, ''].join('/'), - email: () => [host, 'email'].join('/'), birthday: () => [host, 'birthday'].join('/'), birthtime: () => [host, 'birthtime'].join('/'), + emailEnter: () => [host, 'email'].join('/'), subscription: () => [host, 'subscription'].join('/'), createProfile: () => [host, 'profile', 'create'].join('/'), }, diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..c9ec3cf --- /dev/null +++ b/src/types.ts @@ -0,0 +1,8 @@ +export interface FormField { + name: string + value: T + label?: string + placeholder?: string + onValid: (value: T) => void + onInvalid: () => void +}