Add all pages except profile creating page

This commit is contained in:
Aidar Shaikhutdin @makeweb.space 2023-05-04 20:56:34 +06:00
parent 3b52ed4989
commit 5519769c73
36 changed files with 818 additions and 136 deletions

View File

@ -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() {
<div className='container'>
<Header />
<main className='content'>
<section className='page'>
<Routes>
<Route path={routes.client.root()} element={
<Navigate to={routes.client.birthday()} />
} />
<Route path={routes.client.birthday()} element={<BirthdayPage />} />
<Route path={routes.client.birthtime()} element={<BirthtimePage />} />
<Route path={routes.client.createProfile()} element={<CreateProfilePage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</section>
<Routes>
<Route path={routes.client.root()} element={
<Navigate to={routes.client.birthday()} />
} />
<Route path={routes.client.birthday()} element={<BirthdayPage />} />
<Route path={routes.client.birthtime()} element={<BirthtimePage />} />
<Route path={routes.client.createProfile()} element={<CreateProfilePage />} />
<Route path={routes.client.emailEnter()} element={<EmailEnterPage />} />
<Route path={routes.client.subscription()} element={<SubscriptionPage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</main>
</div>
)

View File

@ -8,10 +8,9 @@
}
.content {
display: flex;
width: 100%;
height: 100vh;
position: relative;
width: 100%;
}
.page {

View File

@ -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 (
<>
<section className='page'>
<Title variant='h3' className='mt-24'>Let's start!</Title>
<Title variant='h2'>What's your date of birth?</Title>
<DateControl />
<MainButton label='Next' onClick={handleNext} />
<DatePicker
name='birthdate'
value={birthdate}
onValid={handleValid}
onInvalid={() => setIsDisabled(true)}
/>
<MainButton label='Next' onClick={handleNext} disabled={isDisabled}/>
<footer className='footer'>
<Policy />
<Policy links={links}>
By continuing, you agree to our EULA and Privacy Notice. Have a question? Reach our support team here
</Policy>
<Purposes />
</footer>
</>
</section>
)
}

View File

@ -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 (
<>
<section className='page'>
<Title variant="h2" className="mt-24">What time were you born?</Title>
<p className="description">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.</p>
<TimeControl />
<MainButton label='Next' onClick={handleNext} />
</>
<p className="description">
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.
</p>
<TimePicker onChange={(value: string) => setBirhtime(value)}/>
<MainButton label='Next' onClick={handleNext} disabled={isDisabled}/>
</section>
)
}

View File

@ -0,0 +1,12 @@
import './styles.css'
function CallToAction(): JSX.Element {
return (
<div className='call-to-action mb-24'>
<h1>Start your 7-day trial</h1>
<p>No pressure. Cancel anytime.</p>
</div>
)
}
export default CallToAction

View File

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

View File

@ -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 (
<div className="countdown mb-24">
<p>Reserved for {formatTime(time)}</p>
</div>
)
}
export default Countdown

View File

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

View File

@ -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 (
<>
<section className='page'>
<Title variant="h2" className="mt-24">Creating your profile</Title>
</>
</section>
)
}

View File

@ -1,33 +0,0 @@
import './styles.css'
function DateControl(): JSX.Element {
return (
<div className='date-control'>
<div className='date-control__container'>
<div className="date-control__field">
<h3 className='date-control__field-label'>Year</h3>
<label className="date-control__input">
<input type="number" placeholder=" " max="4" />
<p className="date-control__input-placeholder">YYYY</p>
</label>
</div>
<div className="date-control__field">
<h3 className='date-control__field-label'>Month</h3>
<label className="date-control__input">
<input type="text" placeholder=" " />
<p className="date-control__input-placeholder">MM</p>
</label>
</div>
<div className="date-control__field">
<h3 className='date-control__field-label'>Day</h3>
<label className="date-control__input">
<input type="text" placeholder=" " />
<p className="date-control__input-placeholder">DD</p>
</label>
</div>
</div>
</div>
)
}
export default DateControl

View File

@ -0,0 +1,40 @@
import { FormField } from '../../types'
import { normalize, calculateMaxValue } from './utils'
type DatePartValue = number | undefined
type DateInputProps = Omit<FormField<DatePartValue>, '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<HTMLInputElement>) => {
const datePart = parseInt(e.target.value, 10)
if (isNaN(datePart)) return
if (datePart > calculateMaxValue(maxLength)) return
if (!validate(datePart)) return
onChange(datePart)
}
return (
<div className="date-picker__field">
<h3 className='date-picker__field-label'>{label}</h3>
<label className="date-picker__input">
<input
name={name}
type="number"
placeholder=" "
maxLength={maxLength}
value={value ? normalize(value, maxLength) : ''}
onChange={handleChange}
/>
<p className="date-picker__input-placeholder">{placeholder}</p>
</label>
</div>
)
}
export default DateInput

View File

@ -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<string>): 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 (
<form name={name} className='date-picker'>
<div className='date-picker__container'>
<DateInput
name='year'
value={year}
max={getMaxYear()}
maxLength={4}
label='Year'
placeholder='YYYY'
onChange={(year: number) => setYear(year)}
/>
<DateInput
name='month'
value={month}
max={12}
maxLength={2}
label='Month'
placeholder='MM'
onChange={(month: number) => setMonth(month)}
/>
<DateInput
name='day'
value={day}
max={getDaysInMonth(year, month)}
maxLength={2}
label='Day'
placeholder='DD'
onChange={(day: number) => setDay(day)}
/>
</div>
<ErrorText
isShown={hasError}
message='Date not found. Please check your details and try again.'
/>
</form>
)
}

View File

@ -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 (
<p className={`date-picker__error ${className}`}>
{message}
</p>
)
}
export default ErrorText

View File

@ -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 (
<div className='date-picker'>
<div className='date-picker__container'>
<div className="date-picker__field">
<select
className="date-picker__field-select"
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setHour(e.target.value)}>
{Array.from(Array(12).keys()).map((hour) => (
<option key={hour} value={hour + 1}>{hour + 1}</option>
))}
</select>
</div>
<div className="date-picker__field">
<select
className="date-picker__field-select"
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setMinute(e.target.value)}>
{Array.from(Array(60).keys()).map((minute) => {
return (
<option key={minute} value={minute}>{normalize(minute, 2)}</option>
);
})}
</select>
</div>
<div className="date-picker__field">
<select
className="date-picker__field-select"
onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setPeriod(e.target.value)}>
<option value="AM">AM</option>
<option value="PM">PM</option>
</select>
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,5 @@
import './styles.css'
export * from './DatePicker'
export * from './TimePicker'
export * from './utils'

View File

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

View File

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

View File

@ -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 (
<section className='page'>
<Title variant='h2' className='mt-24'>
We will email you a copy of your wallpaper for easy access.
</Title>
<EmailInput
name="email"
value={email}
placeholder="Your Email"
onValid={handleValidEmail}
onInvalid={() => setIsDisabled(true)}
/>
<p>We don't share any personal information.</p>
<Policy links={links} sizing='medium'>
By clicking "Continue" below, you agree to our EULA and Privacy Policy.
</Policy>
<MainButton label='Continue' onClick={handleClick} disabled={isDisabled} />
</section>
)
}
export default EmailEnterPage

View File

@ -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<string>): JSX.Element {
const { name, value, placeholder, onValid, onInvalid } = props
const [email, setEmail] = useState(value)
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const inputEmail = event.target.value
setEmail(inputEmail)
if (isValidEmail(inputEmail)) {
onValid(inputEmail)
} else {
onInvalid()
}
}
return (
<div className="email-input">
<input
name={name}
type="email"
value={email}
onChange={handleChange}
placeholder=" "
/>
<span className="email-input__placeholder">{placeholder}</span>
</div>
)
}
export default EmailInput

View File

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

View File

@ -33,6 +33,7 @@ function Header(): JSX.Element {
<header className="header">
{ showBackButton ? <BackButton className="pa" onClick={goBack} /> : null }
<img src={iconUrl} alt="logo" width="40" height="40" />
<span className="header__title">Aura</span>
</header>
)
}

View File

@ -8,3 +8,10 @@
position: relative;
width: 100%;
}
.header__title {
font-size: 24px;
font-weight: 600;
margin-left: 10px;
text-transform: uppercase;
}

View File

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

View File

@ -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 (
<div key={idx} className='payment__item'>
<div className='payment__item-summary'>
<div className='payment__item-title'>{item.title}</div>
<div className='payment__item-price'>{price.format()}</div>
</div>
<div className='payment__item-description'>
<p>{item.description}</p>
<p>One dollar thirty six cents per day</p>
</div>
</div>
)
}
return (
<div className='payment'>
<div className='payment__table'>
<div className='payment__total'>
<div className='payment__total-title'>Total today</div>
<div className='payment__total-price'>{totalPrice.format()}</div>
</div>
<div className='payment__items'>
{items.map(toItem)}
</div>
</div>
<div className='payment__information'>
You will be charged only $1 for your 7-day trial. We'll email you a reminder before your trial period ends. Cancel anytime.
</div>
</div>
)
}
export default Payment
export { Price, Currency, Locale }

View File

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

View File

@ -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 <a key={idx} href={item.href} target="_blank" rel="noreferrer nofollow">{item.content}</a>
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 (
<a key={`${link.text}-${idx}`} href={link.href} target="_blank" rel="noreferrer nofollow">
{link.text}
</a>
);
});
}
const content = text.map<ReactNode>(toElement)
return <div className="policy"><p>{ content }</p></div>
return (
<div className={`policy ${sizes[sizing]}`}>
<p>{createLinkedContent(children)}</p>
</div>
)
}
export default Policy

View File

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

View File

@ -2,5 +2,4 @@
color: #8e8e93;
font-size: 12px;
line-height: 18px;
margin-top: 6px;
}

View File

@ -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 (
<>
<UserHeader email={userEmail} />
<section className='page'>
<Title variant='h3'>
Your personalized Aries Wallpaper has been created! Find your happiness now and get an additional individual horoscope based on your energies.
</Title>
<Countdown start={10}/>
<CallToAction />
<Payment items={paymentItems} currency={currency} locale={locale}/>
<MainButton label='Get access' onClick={handleClick} />
<Policy links={links}>
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
</Policy>
</section>
</>
)
}
export default SubscriptionPage

View File

@ -1,33 +0,0 @@
function TimeControl(): JSX.Element {
return (
<div className='date-control'>
<div className='date-control__container'>
<div className="date-control__field">
<select className="date-control__field-select">
{Array.from(Array(12).keys()).map((hour) => (
<option key={hour} value={hour + 1}>{hour + 1}</option>
))}
</select>
</div>
<div className="date-control__field">
<select className="date-control__field-select">
{Array.from(Array(60).keys()).map((minute) => {
const formattedMinute = String(minute).padStart(2, '0');
return (
<option key={minute} value={minute}>{formattedMinute}</option>
);
})}
</select>
</div>
<div className="date-control__field">
<select className="date-control__field-select">
<option value="AM">AM</option>
<option value="PM">PM</option>
</select>
</div>
</div>
</div>
)
}
export default TimeControl

View File

@ -1,5 +1,4 @@
.title {
letter-spacing: .2px;
line-height: 150%;
margin-bottom: 24px;
text-align: center;

View File

@ -0,0 +1,16 @@
import './styles.css'
type UserHeaderProps = {
email: string
}
function UserHeader({ email }: UserHeaderProps): JSX.Element {
return (
<section className='user-header'>
<div className='user-header__content'>{email}</div>
<div className='user-header__icon'>{email.at(0)?.toUpperCase()}</div>
</section>
)
}
export default UserHeader

View File

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

View File

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

View File

@ -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('/'),
},

8
src/types.ts Normal file
View File

@ -0,0 +1,8 @@
export interface FormField<T> {
name: string
value: T
label?: string
placeholder?: string
onValid: (value: T) => void
onInvalid: () => void
}