Add all pages except profile creating page
This commit is contained in:
parent
3b52ed4989
commit
5519769c73
@ -2,6 +2,8 @@ import { Routes, Route, Navigate } from 'react-router-dom';
|
|||||||
import BirthdayPage from '../BirthdayPage'
|
import BirthdayPage from '../BirthdayPage'
|
||||||
import BirthtimePage from '../BirthtimePage'
|
import BirthtimePage from '../BirthtimePage'
|
||||||
import CreateProfilePage from '../CreateProfilePage'
|
import CreateProfilePage from '../CreateProfilePage'
|
||||||
|
import EmailEnterPage from '../EmailEnterPage'
|
||||||
|
import SubscriptionPage from '../SubscriptionPage'
|
||||||
import NotFoundPage from '../NotFoundPage'
|
import NotFoundPage from '../NotFoundPage'
|
||||||
import Header from '../Header'
|
import Header from '../Header'
|
||||||
import routes from '../../routes'
|
import routes from '../../routes'
|
||||||
@ -12,17 +14,17 @@ function App() {
|
|||||||
<div className='container'>
|
<div className='container'>
|
||||||
<Header />
|
<Header />
|
||||||
<main className='content'>
|
<main className='content'>
|
||||||
<section className='page'>
|
<Routes>
|
||||||
<Routes>
|
<Route path={routes.client.root()} element={
|
||||||
<Route path={routes.client.root()} element={
|
<Navigate to={routes.client.birthday()} />
|
||||||
<Navigate to={routes.client.birthday()} />
|
} />
|
||||||
} />
|
<Route path={routes.client.birthday()} element={<BirthdayPage />} />
|
||||||
<Route path={routes.client.birthday()} element={<BirthdayPage />} />
|
<Route path={routes.client.birthtime()} element={<BirthtimePage />} />
|
||||||
<Route path={routes.client.birthtime()} element={<BirthtimePage />} />
|
<Route path={routes.client.createProfile()} element={<CreateProfilePage />} />
|
||||||
<Route path={routes.client.createProfile()} element={<CreateProfilePage />} />
|
<Route path={routes.client.emailEnter()} element={<EmailEnterPage />} />
|
||||||
<Route path="*" element={<NotFoundPage />} />
|
<Route path={routes.client.subscription()} element={<SubscriptionPage />} />
|
||||||
</Routes>
|
<Route path="*" element={<NotFoundPage />} />
|
||||||
</section>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -8,10 +8,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
display: flex;
|
width: 100%;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page {
|
.page {
|
||||||
|
|||||||
@ -1,26 +1,50 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
import { useNavigate } from 'react-router-dom'
|
import { useNavigate } from 'react-router-dom'
|
||||||
import routes from '../../routes'
|
import routes from '../../routes'
|
||||||
import Policy from '../Policy'
|
import Policy from '../Policy'
|
||||||
import Purposes from '../Purposes'
|
import Purposes from '../Purposes'
|
||||||
import Title from '../Title'
|
import Title from '../Title'
|
||||||
import DateControl from '../DateControl'
|
import { DatePicker } from '../DateTimePicker'
|
||||||
import MainButton from '../MainButton'
|
import MainButton from '../MainButton'
|
||||||
import './styles.css'
|
import './styles.css'
|
||||||
|
|
||||||
function BirthdayPage(): JSX.Element {
|
function BirthdayPage(): JSX.Element {
|
||||||
const navigate = useNavigate();
|
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 handleNext = () => navigate(routes.client.birthtime())
|
||||||
|
const handleValid = (birthdate: string) => {
|
||||||
|
setBirthdate(birthdate)
|
||||||
|
setIsDisabled(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('birthdate', birthdate)
|
||||||
|
}, [birthdate])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<section className='page'>
|
||||||
<Title variant='h3' className='mt-24'>Let's start!</Title>
|
<Title variant='h3' className='mt-24'>Let's start!</Title>
|
||||||
<Title variant='h2'>What's your date of birth?</Title>
|
<Title variant='h2'>What's your date of birth?</Title>
|
||||||
<DateControl />
|
<DatePicker
|
||||||
<MainButton label='Next' onClick={handleNext} />
|
name='birthdate'
|
||||||
|
value={birthdate}
|
||||||
|
onValid={handleValid}
|
||||||
|
onInvalid={() => setIsDisabled(true)}
|
||||||
|
/>
|
||||||
|
<MainButton label='Next' onClick={handleNext} disabled={isDisabled}/>
|
||||||
<footer className='footer'>
|
<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 />
|
<Purposes />
|
||||||
</footer>
|
</footer>
|
||||||
</>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,20 +1,35 @@
|
|||||||
|
import { useState, useEffect } from "react"
|
||||||
import { useNavigate } from "react-router-dom"
|
import { useNavigate } from "react-router-dom"
|
||||||
import Title from "../Title"
|
import Title from "../Title"
|
||||||
import MainButton from "../MainButton"
|
import MainButton from "../MainButton"
|
||||||
import TimeControl from "../TimeControl"
|
import { TimePicker } from "../DateTimePicker"
|
||||||
import routes from "../../routes"
|
import routes from "../../routes"
|
||||||
import './styles.css'
|
import './styles.css'
|
||||||
|
|
||||||
function BirthtimePage(): JSX.Element {
|
function BirthtimePage(): JSX.Element {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [birthtime, setBirhtime] = useState('')
|
||||||
|
const [isDisabled, setIsDisabled] = useState(true)
|
||||||
const handleNext = () => navigate(routes.client.createProfile())
|
const handleNext = () => navigate(routes.client.createProfile())
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!birthtime) {
|
||||||
|
setIsDisabled(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
setIsDisabled(false)
|
||||||
|
console.log('birthtime', birthtime)
|
||||||
|
}, [birthtime])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<section className='page'>
|
||||||
<Title variant="h2" className="mt-24">What time were you born?</Title>
|
<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>
|
<p className="description">
|
||||||
<TimeControl />
|
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.
|
||||||
<MainButton label='Next' onClick={handleNext} />
|
</p>
|
||||||
</>
|
<TimePicker onChange={(value: string) => setBirhtime(value)}/>
|
||||||
|
<MainButton label='Next' onClick={handleNext} disabled={isDisabled}/>
|
||||||
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
12
src/components/CallToAction/index.tsx
Normal file
12
src/components/CallToAction/index.tsx
Normal 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
|
||||||
12
src/components/CallToAction/styles.css
Normal file
12
src/components/CallToAction/styles.css
Normal 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;
|
||||||
|
}
|
||||||
29
src/components/Countdown/index.tsx
Normal file
29
src/components/Countdown/index.tsx
Normal 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
|
||||||
10
src/components/Countdown/styles.css
Normal file
10
src/components/Countdown/styles.css
Normal 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;
|
||||||
|
}
|
||||||
@ -1,10 +1,20 @@
|
|||||||
|
import { useEffect } from "react"
|
||||||
|
import { useNavigate } from "react-router-dom"
|
||||||
import Title from "../Title"
|
import Title from "../Title"
|
||||||
|
import routes from "../../routes"
|
||||||
|
|
||||||
function CreateProfilePage(): JSX.Element {
|
function CreateProfilePage(): JSX.Element {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const timerId = setTimeout(() => navigate(routes.client.emailEnter()), 3000)
|
||||||
|
return () => clearTimeout(timerId)
|
||||||
|
}, [navigate])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<section className='page'>
|
||||||
<Title variant="h2" className="mt-24">Creating your profile</Title>
|
<Title variant="h2" className="mt-24">Creating your profile</Title>
|
||||||
</>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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
|
|
||||||
40
src/components/DateTimePicker/DateInput.tsx
Normal file
40
src/components/DateTimePicker/DateInput.tsx
Normal 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
|
||||||
67
src/components/DateTimePicker/DatePicker.tsx
Normal file
67
src/components/DateTimePicker/DatePicker.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
15
src/components/DateTimePicker/ErrorText.tsx
Normal file
15
src/components/DateTimePicker/ErrorText.tsx
Normal 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
|
||||||
53
src/components/DateTimePicker/TimePicker.tsx
Normal file
53
src/components/DateTimePicker/TimePicker.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
5
src/components/DateTimePicker/index.ts
Normal file
5
src/components/DateTimePicker/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
import './styles.css'
|
||||||
|
|
||||||
|
export * from './DatePicker'
|
||||||
|
export * from './TimePicker'
|
||||||
|
export * from './utils'
|
||||||
@ -1,30 +1,30 @@
|
|||||||
.date-control {
|
.date-picker {
|
||||||
|
margin: 0 auto;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-control__container {
|
.date-picker__container {
|
||||||
grid-gap: 12px;
|
grid-gap: 12px;
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
grid-template-columns: repeat(3,1fr);
|
grid-template-columns: repeat(3,1fr);
|
||||||
max-width: 400px;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
margin: 0 auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-control__field-label {
|
.date-picker__field-label {
|
||||||
color: #6b7baa;
|
color: #6b7baa;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 16px;
|
line-height: 16px;
|
||||||
margin: 0 0 6px 6px;
|
margin: 0 0 6px 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-control__input {
|
.date-picker__input {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -33,7 +33,7 @@
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-control__input > input {
|
.date-picker__input > input {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
color: #121620;
|
color: #121620;
|
||||||
@ -51,12 +51,12 @@
|
|||||||
padding-top: 5px;
|
padding-top: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-control__input > input:focus {
|
.date-picker__input > input:focus {
|
||||||
border-color: #066fde;
|
border-color: #066fde;
|
||||||
transition-delay: .1s;
|
transition-delay: .1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-control__input-placeholder {
|
.date-picker__input-placeholder {
|
||||||
color: #6b7baa;
|
color: #6b7baa;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
left: 12px;
|
left: 12px;
|
||||||
@ -69,15 +69,15 @@
|
|||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-control__input input:focus + .date-control__input-placeholder,
|
.date-picker__input input:focus + .date-picker__input-placeholder,
|
||||||
.date-control__input input:not(:placeholder-shown) + .date-control__input-placeholder {
|
.date-picker__input input:not(:placeholder-shown) + .date-picker__input-placeholder {
|
||||||
display: none;
|
display: none;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
top: 12px;
|
top: 12px;
|
||||||
width: auto;
|
width: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-control__field-select {
|
.date-picker__field-select {
|
||||||
display: block;
|
display: block;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@ -97,3 +97,19 @@
|
|||||||
border: 2px solid #dee5f9;
|
border: 2px solid #dee5f9;
|
||||||
padding-top: 5px;
|
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);
|
||||||
|
}
|
||||||
23
src/components/DateTimePicker/utils.ts
Normal file
23
src/components/DateTimePicker/utils.ts
Normal 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();
|
||||||
|
}
|
||||||
48
src/components/EmailEnterPage/index.tsx
Normal file
48
src/components/EmailEnterPage/index.tsx
Normal 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
|
||||||
37
src/components/EmailInput/index.tsx
Normal file
37
src/components/EmailInput/index.tsx
Normal 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
|
||||||
47
src/components/EmailInput/styles.css
Normal file
47
src/components/EmailInput/styles.css
Normal 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%);
|
||||||
|
}
|
||||||
@ -33,6 +33,7 @@ function Header(): JSX.Element {
|
|||||||
<header className="header">
|
<header className="header">
|
||||||
{ showBackButton ? <BackButton className="pa" onClick={goBack} /> : null }
|
{ showBackButton ? <BackButton className="pa" onClick={goBack} /> : null }
|
||||||
<img src={iconUrl} alt="logo" width="40" height="40" />
|
<img src={iconUrl} alt="logo" width="40" height="40" />
|
||||||
|
<span className="header__title">Aura</span>
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,3 +8,10 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.header__title {
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 600;
|
||||||
|
margin-left: 10px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|||||||
44
src/components/Payment/Price.ts
Normal file
44
src/components/Payment/Price.ts
Normal 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
|
||||||
53
src/components/Payment/index.tsx
Normal file
53
src/components/Payment/index.tsx
Normal 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 }
|
||||||
60
src/components/Payment/styles.css
Normal file
60
src/components/Payment/styles.css
Normal 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;
|
||||||
|
}
|
||||||
@ -1,39 +1,45 @@
|
|||||||
import { ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
import './styles.css'
|
import './styles.css'
|
||||||
|
|
||||||
enum PolicyContentType {
|
type Link = {
|
||||||
Text = 'text',
|
text: string
|
||||||
Link = 'link',
|
href: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type PolicyContent = {
|
interface PolicyProps {
|
||||||
type: PolicyContentType
|
links: Link[]
|
||||||
content: string
|
children: string
|
||||||
href?: string
|
sizing?: 'small' | 'medium' | 'large'
|
||||||
}
|
}
|
||||||
|
|
||||||
function Policy(): JSX.Element {
|
const sizes = {
|
||||||
const text: PolicyContent[] = [
|
small: 'policy--small',
|
||||||
{ type: PolicyContentType.Text, content: 'By continuing, you agree to our ' },
|
medium: 'policy--medium',
|
||||||
{ type: PolicyContentType.Link, content: 'EULA', href: 'https://aura.wit.life/terms' },
|
large: 'policy--large',
|
||||||
{ 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 ' },
|
function Policy({ links, children, sizing = 'small' }: PolicyProps): JSX.Element {
|
||||||
{ type: PolicyContentType.Link, content: 'here', href: 'https://aura.wit.life/' },
|
const createLinkedContent = (sentence: string): ReactNode[] => {
|
||||||
]
|
const pattern = links.map(link => `(${link.text})`).join('|');
|
||||||
const toElement = (item: PolicyContent, idx: number) => {
|
const regex = new RegExp(pattern, 'g');
|
||||||
switch (item.type) {
|
return sentence.split(regex).map((part, idx) => {
|
||||||
case 'text':
|
const link = links.find(({ text }) => text === part);
|
||||||
return item.content
|
|
||||||
case 'link':
|
if (!link) return part
|
||||||
return <a key={idx} href={item.href} target="_blank" rel="noreferrer nofollow">{item.content}</a>
|
|
||||||
default:
|
return (
|
||||||
throw new Error(`Unknown type: ${item.type}`)
|
<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
|
export default Policy
|
||||||
|
|||||||
@ -4,13 +4,13 @@
|
|||||||
max-width: 400px;
|
max-width: 400px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
|
margin-bottom: 6px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.policy p,
|
.policy p,
|
||||||
.policy a {
|
.policy a {
|
||||||
color: #121620;
|
color: #121620;
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
}
|
}
|
||||||
@ -18,3 +18,19 @@
|
|||||||
.policy a {
|
.policy a {
|
||||||
text-decoration: underline;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,5 +2,4 @@
|
|||||||
color: #8e8e93;
|
color: #8e8e93;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: 18px;
|
line-height: 18px;
|
||||||
margin-top: 6px;
|
|
||||||
}
|
}
|
||||||
|
|||||||
43
src/components/SubscriptionPage/index.tsx
Normal file
43
src/components/SubscriptionPage/index.tsx
Normal 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
|
||||||
@ -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
|
|
||||||
@ -1,5 +1,4 @@
|
|||||||
.title {
|
.title {
|
||||||
letter-spacing: .2px;
|
|
||||||
line-height: 150%;
|
line-height: 150%;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|||||||
16
src/components/UserHeader/index.tsx
Normal file
16
src/components/UserHeader/index.tsx
Normal 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
|
||||||
23
src/components/UserHeader/styles.css
Normal file
23
src/components/UserHeader/styles.css
Normal 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;
|
||||||
|
}
|
||||||
@ -3,6 +3,11 @@
|
|||||||
font-family: SF Pro Text, sans-serif;
|
font-family: SF Pro Text, sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 24px;
|
font-size: 24px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@ -89,6 +94,10 @@ a,button,div,input,select,textarea {
|
|||||||
margin-top: 24px;
|
margin-top: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mb-24 {
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
.pa {
|
.pa {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,9 +4,9 @@ const prefix = 'api/v1';
|
|||||||
const routes = {
|
const routes = {
|
||||||
client: {
|
client: {
|
||||||
root: () => [host, ''].join('/'),
|
root: () => [host, ''].join('/'),
|
||||||
email: () => [host, 'email'].join('/'),
|
|
||||||
birthday: () => [host, 'birthday'].join('/'),
|
birthday: () => [host, 'birthday'].join('/'),
|
||||||
birthtime: () => [host, 'birthtime'].join('/'),
|
birthtime: () => [host, 'birthtime'].join('/'),
|
||||||
|
emailEnter: () => [host, 'email'].join('/'),
|
||||||
subscription: () => [host, 'subscription'].join('/'),
|
subscription: () => [host, 'subscription'].join('/'),
|
||||||
createProfile: () => [host, 'profile', 'create'].join('/'),
|
createProfile: () => [host, 'profile', 'create'].join('/'),
|
||||||
},
|
},
|
||||||
|
|||||||
8
src/types.ts
Normal file
8
src/types.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
export interface FormField<T> {
|
||||||
|
name: string
|
||||||
|
value: T
|
||||||
|
label?: string
|
||||||
|
placeholder?: string
|
||||||
|
onValid: (value: T) => void
|
||||||
|
onInvalid: () => void
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user