add: creating profile page logic
10
index.html
@ -2,10 +2,14 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="theme-color" content="#ffffff" />
|
<link rel="apple-touch-icon" sizes="180x180" href="/src/assets/apple-touch-icon.png">
|
||||||
<title>Aura Web App</title>
|
<link rel="icon" type="image/png" sizes="32x32" href="/src/assets/favicon-32x32.png">
|
||||||
|
<link rel="icon" type="image/png" sizes="16x16" href="/src/assets/favicon-16x16.png">
|
||||||
|
<link rel="manifest" href="/src/assets/site.webmanifest">
|
||||||
|
<meta name="msapplication-TileColor" content="#da532c">
|
||||||
|
<meta name="theme-color" content="#ffffff">
|
||||||
|
<title>AURA</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||||
|
|||||||
39
package-lock.json
generated
@ -9,6 +9,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
"react-circular-progressbar": "^2.1.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.11.0"
|
"react-router-dom": "^6.11.0"
|
||||||
},
|
},
|
||||||
@ -23,7 +24,8 @@
|
|||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-refresh": "^0.3.4",
|
"eslint-plugin-react-refresh": "^0.3.4",
|
||||||
"typescript": "^5.0.2",
|
"typescript": "^5.0.2",
|
||||||
"vite": "^4.3.2"
|
"vite": "^4.3.2",
|
||||||
|
"vite-plugin-copy": "^0.1.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@ampproject/remapping": {
|
"node_modules/@ampproject/remapping": {
|
||||||
@ -2551,6 +2553,14 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-circular-progressbar": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-circular-progressbar/-/react-circular-progressbar-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-xp4THTrod4aLpGy68FX/k1Q3nzrfHUjUe5v6FsdwXBl3YVMwgeXYQKDrku7n/D6qsJA9CuunarAboC2xCiKs1g==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "18.2.0",
|
"version": "18.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
||||||
@ -2963,6 +2973,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vite-plugin-copy": {
|
||||||
|
"version": "0.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite-plugin-copy/-/vite-plugin-copy-0.1.6.tgz",
|
||||||
|
"integrity": "sha512-bqIaefZOE2Jx8P5wJuHKL5GzCERa/pcwdUQWaocyTNXgalN2xkxXH7LmqRJ34V2OlKF2F9E/zj0zITS7U6PpUg==",
|
||||||
|
"dev": true,
|
||||||
|
"dependencies": {
|
||||||
|
"fast-glob": "^3.2.7"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/which": {
|
"node_modules/which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
@ -4724,6 +4746,12 @@
|
|||||||
"loose-envify": "^1.1.0"
|
"loose-envify": "^1.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-circular-progressbar": {
|
||||||
|
"version": "2.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-circular-progressbar/-/react-circular-progressbar-2.1.0.tgz",
|
||||||
|
"integrity": "sha512-xp4THTrod4aLpGy68FX/k1Q3nzrfHUjUe5v6FsdwXBl3YVMwgeXYQKDrku7n/D6qsJA9CuunarAboC2xCiKs1g==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"react-dom": {
|
"react-dom": {
|
||||||
"version": "18.2.0",
|
"version": "18.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
|
||||||
@ -4968,6 +4996,15 @@
|
|||||||
"rollup": "^3.21.0"
|
"rollup": "^3.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"vite-plugin-copy": {
|
||||||
|
"version": "0.1.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/vite-plugin-copy/-/vite-plugin-copy-0.1.6.tgz",
|
||||||
|
"integrity": "sha512-bqIaefZOE2Jx8P5wJuHKL5GzCERa/pcwdUQWaocyTNXgalN2xkxXH7LmqRJ34V2OlKF2F9E/zj0zITS7U6PpUg==",
|
||||||
|
"dev": true,
|
||||||
|
"requires": {
|
||||||
|
"fast-glob": "^3.2.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
"which": {
|
"which": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
|
"react-circular-progressbar": "^2.1.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-router-dom": "^6.11.0"
|
"react-router-dom": "^6.11.0"
|
||||||
},
|
},
|
||||||
@ -25,6 +26,7 @@
|
|||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-refresh": "^0.3.4",
|
"eslint-plugin-react-refresh": "^0.3.4",
|
||||||
"typescript": "^5.0.2",
|
"typescript": "^5.0.2",
|
||||||
"vite": "^4.3.2"
|
"vite": "^4.3.2",
|
||||||
|
"vite-plugin-copy": "^0.1.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
src/assets/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 76 KiB |
BIN
src/assets/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 438 KiB |
BIN
src/assets/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 68 KiB |
9
src/assets/browserconfig.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<browserconfig>
|
||||||
|
<msapplication>
|
||||||
|
<tile>
|
||||||
|
<square150x150logo src="/mstile-150x150.png"/>
|
||||||
|
<TileColor>#da532c</TileColor>
|
||||||
|
</tile>
|
||||||
|
</msapplication>
|
||||||
|
</browserconfig>
|
||||||
BIN
src/assets/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
src/assets/favicon.ico
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/assets/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 42 KiB |
19
src/assets/site.webmanifest
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"short_name": "",
|
||||||
|
"icons": [
|
||||||
|
{
|
||||||
|
"src": "/assets/android-chrome-192x192.png",
|
||||||
|
"sizes": "192x192",
|
||||||
|
"type": "image/png"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"src": "/assets/android-chrome-512x512.png",
|
||||||
|
"sizes": "512x512",
|
||||||
|
"type": "image/png"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"theme_color": "#ffffff",
|
||||||
|
"background_color": "#ffffff",
|
||||||
|
"display": "standalone"
|
||||||
|
}
|
||||||
67
src/components/CreateProfilePage/ProcessFlow.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
import ProcessItem from "./ProcessItem"
|
||||||
|
|
||||||
|
interface Task {
|
||||||
|
(): Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProcessItem = {
|
||||||
|
task: Task
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProcessFlowProps = {
|
||||||
|
items: ProcessItem[]
|
||||||
|
onDone: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ProcessStatus {
|
||||||
|
Idle,
|
||||||
|
Pending,
|
||||||
|
Done,
|
||||||
|
}
|
||||||
|
|
||||||
|
const createChaining = (tasks: Task[], callback: (idx: number) => void) => {
|
||||||
|
return tasks.reduce((chain, task, idx) => {
|
||||||
|
return chain.then(task).then(()=> callback(idx))
|
||||||
|
}, Promise.resolve())
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMultiplier = (currentIdx: number, length: number): number => {
|
||||||
|
return Math.max(length - (currentIdx + 1) - 1, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculateTop = (currentIdx: number, length: number): number => {
|
||||||
|
const itemHeight = 56
|
||||||
|
return getMultiplier(currentIdx, length) * itemHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProcessFlow({ items, onDone }: ProcessFlowProps): JSX.Element {
|
||||||
|
const [status, setStatus] = useState(ProcessStatus.Idle)
|
||||||
|
const [doneTaskIdx, setDoneTaskIdx] = useState(-1)
|
||||||
|
const tasks = items.map(({ task }) => task)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (status !== ProcessStatus.Idle) return
|
||||||
|
setStatus(ProcessStatus.Pending)
|
||||||
|
createChaining(tasks, setDoneTaskIdx)
|
||||||
|
.then(() => setStatus(ProcessStatus.Done))
|
||||||
|
.then(() => onDone())
|
||||||
|
}, [status, tasks, onDone])
|
||||||
|
|
||||||
|
const toItems = ({ label }: ProcessItem, idx: number): JSX.Element => {
|
||||||
|
return <ProcessItem
|
||||||
|
key={idx}
|
||||||
|
label={label}
|
||||||
|
top={calculateTop(doneTaskIdx, items.length)}
|
||||||
|
isDone={idx <= doneTaskIdx}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className='process-items mt-24'>
|
||||||
|
{items.map(toItems)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProcessFlow
|
||||||
23
src/components/CreateProfilePage/ProcessItem.tsx
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
type ProcessItemProps = {
|
||||||
|
top: number
|
||||||
|
label: string
|
||||||
|
isDone: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
function ProcessItem({ top, label, isDone }: ProcessItemProps): JSX.Element {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='process-item' style={{ top: top }}>
|
||||||
|
<div className='process-item__pick'>
|
||||||
|
{
|
||||||
|
isDone
|
||||||
|
? <div className='process-item__icon'>✅</div>
|
||||||
|
: <div className="process-item__loader"><span></span></div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<div className='process-item__label'>{label}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ProcessItem
|
||||||
@ -1,19 +1,44 @@
|
|||||||
import { useEffect } from "react"
|
import { useState } from "react"
|
||||||
import { useNavigate } from "react-router-dom"
|
import { useNavigate } from "react-router-dom"
|
||||||
|
import { CircularProgressbar, buildStyles } from 'react-circular-progressbar'
|
||||||
|
import ProcessFlow from "./ProcessFlow"
|
||||||
import Title from "../Title"
|
import Title from "../Title"
|
||||||
import routes from "../../routes"
|
import routes from "../../routes"
|
||||||
|
import './styles.css'
|
||||||
|
|
||||||
|
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))
|
||||||
|
|
||||||
function CreateProfilePage(): JSX.Element {
|
function CreateProfilePage(): JSX.Element {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
|
const [progress, setProgress] = useState(0)
|
||||||
useEffect(() => {
|
const processItems = [
|
||||||
const timerId = setTimeout(() => navigate(routes.client.emailEnter()), 3000)
|
{ task: () => sleep(3300).then(() => setProgress(35)), label: 'Zodiac data analysis' },
|
||||||
return () => clearTimeout(timerId)
|
{ task: () => sleep(2550).then(() => setProgress(61)), label: 'Drawing Wallpapers' },
|
||||||
}, [navigate])
|
{ task: () => sleep(3789).then(() => setProgress(98)), label: 'Preparing results' },
|
||||||
|
]
|
||||||
|
const handleDone = () => Promise.resolve()
|
||||||
|
.then(() => setProgress(100))
|
||||||
|
.then(() => sleep(1000))
|
||||||
|
.then(() => navigate(routes.client.emailEnter()))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className='page'>
|
<section className='page'>
|
||||||
<Title variant="h2" className="mt-24">Creating your profile</Title>
|
<Title variant="h2" className="mt-24">Creating your profile</Title>
|
||||||
|
<div className="progressbar">
|
||||||
|
<CircularProgressbar
|
||||||
|
value={progress}
|
||||||
|
text={`${progress}%`}
|
||||||
|
strokeWidth={4}
|
||||||
|
styles={buildStyles({
|
||||||
|
strokeLinecap: 'butt',
|
||||||
|
textSize: '12px',
|
||||||
|
pathColor: '#000',
|
||||||
|
textColor: '#000',
|
||||||
|
pathTransitionDuration: 1,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<ProcessFlow items={processItems} onDone={handleDone} />
|
||||||
</section>
|
</section>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
91
src/components/CreateProfilePage/styles.css
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
.progressbar {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 250px;
|
||||||
|
margin: 24px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-items {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-item {
|
||||||
|
display: flex;
|
||||||
|
font-size: 24px;
|
||||||
|
font-weight: 400;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
line-height: 32px;
|
||||||
|
position: relative;
|
||||||
|
transition: top .4s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-item__pick {
|
||||||
|
position: relative;
|
||||||
|
margin-right: 15px;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-item__icon {
|
||||||
|
font-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-item__loader {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: auto;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
animation: loader-1-1 4.8s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loader-1-1 {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-item__loader span {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: auto;
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
clip: rect(0, 32px, 32px, 16px);
|
||||||
|
animation: loader-1-2 1.2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loader-1-2 {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(220deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
.process-item__loader span::after {
|
||||||
|
position: absolute;
|
||||||
|
box-sizing: border-box;
|
||||||
|
content: "";
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
margin: auto;
|
||||||
|
height: 32px;
|
||||||
|
width: 32px;
|
||||||
|
clip: rect(0, 32px, 32px, 16px);
|
||||||
|
border: 3px solid #000;
|
||||||
|
border-radius: 50%;
|
||||||
|
animation: loader-1-3 1.2s cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes loader-1-3 {
|
||||||
|
0% { transform: rotate(-140deg); }
|
||||||
|
50% { transform: rotate(-160deg); }
|
||||||
|
100% { transform: rotate(140deg); }
|
||||||
|
}
|
||||||
@ -25,11 +25,8 @@ function SubscriptionPage(): JSX.Element {
|
|||||||
<>
|
<>
|
||||||
<UserHeader email={userEmail} />
|
<UserHeader email={userEmail} />
|
||||||
<section className='page'>
|
<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 />
|
<CallToAction />
|
||||||
|
<Countdown start={10}/>
|
||||||
<Payment items={paymentItems} currency={currency} locale={locale}/>
|
<Payment items={paymentItems} currency={currency} locale={locale}/>
|
||||||
<MainButton label='Get access' onClick={handleClick} />
|
<MainButton label='Get access' onClick={handleClick} />
|
||||||
<Policy links={links}>
|
<Policy links={links}>
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import init from './init'
|
import init from './init'
|
||||||
|
import 'react-circular-progressbar/dist/styles.css'
|
||||||
import './fonts.css'
|
import './fonts.css'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,21 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
import { copy } from 'vite-plugin-copy'
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
build: {
|
||||||
|
manifest: false,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
copy([
|
||||||
|
{ src: 'src/assets/favicon.ico', dest: 'dist' },
|
||||||
|
{ src: 'src/assets/browserconfig.xml', dest: 'dist' },
|
||||||
|
{ src: 'src/assets/mstile-150x150.png', dest: 'dist' },
|
||||||
|
{ src: 'src/assets/android-chrome-192x192.png', dest: 'dist/assets' },
|
||||||
|
{ src: 'src/assets/android-chrome-512x512.png', dest: 'dist/assets' },
|
||||||
|
{ src: 'src/assets/android-chrome-512x512.png', dest: 'dist/assets' },
|
||||||
|
], { hook: 'writeBundle' }),
|
||||||
|
],
|
||||||
})
|
})
|
||||||
|
|||||||