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 (
-
- )
-}
-
-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}
+
+
+ {placeholder}
+
+
+ )
+}
+
+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 (
+
+ )
+}
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 (
+
+
+
+ ) => setHour(e.target.value)}>
+ {Array.from(Array(12).keys()).map((hour) => (
+ {hour + 1}
+ ))}
+
+
+
+ ) => setMinute(e.target.value)}>
+ {Array.from(Array(60).keys()).map((minute) => {
+ return (
+ {normalize(minute, 2)}
+ );
+ })}
+
+
+
+ ) => setPeriod(e.target.value)}>
+ AM
+ PM
+
+
+
+
+ )
+}
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 }
+ 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
+ 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 (
-
-
-
-
- {Array.from(Array(12).keys()).map((hour) => (
- {hour + 1}
- ))}
-
-
-
-
- {Array.from(Array(60).keys()).map((minute) => {
- const formattedMinute = String(minute).padStart(2, '0');
- return (
- {formattedMinute}
- );
- })}
-
-
-
-
- AM
- PM
-
-
-
-
- )
-}
-
-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
+}