feat: onboarding home and compatibility pages
This commit is contained in:
parent
73a9da6a4e
commit
2fc26aceb0
BIN
public/finger.png
Normal file
BIN
public/finger.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
@ -2,10 +2,10 @@ import { useTranslation } from "react-i18next";
|
||||
import MainButton from "../MainButton";
|
||||
import Title from "../Title";
|
||||
import styles from "./styles.module.css";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import NameInput from "./nameInput";
|
||||
import DatePicker from "./DatePicker";
|
||||
import { IDate } from "@/services/date";
|
||||
import { IDate, getDateAsString } from "@/services/date";
|
||||
import { AICompatCategories, useApi, useApiCall } from "@/api";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import routes from "@/routes";
|
||||
@ -18,6 +18,8 @@ import {
|
||||
} from "@/services/zodiac-sign";
|
||||
import { Asset } from "@/api/resources/Assets";
|
||||
import { getRandomArbitrary } from "@/services/random-value";
|
||||
import Onboarding, { EDirectionOnboarding } from "../Onboarding";
|
||||
import TextWithFinger from "../TextWithFinger";
|
||||
|
||||
function CompatibilityPage(): JSX.Element {
|
||||
const { t, i18n } = useTranslation();
|
||||
@ -26,15 +28,24 @@ function CompatibilityPage(): JSX.Element {
|
||||
const [isDisabled, setIsDisabled] = useState(true);
|
||||
const [isDisabledName, setIsDisabledName] = useState(true);
|
||||
const [isDisabledDate, setIsDisabledDate] = useState(true);
|
||||
const [isChangeDate, setIsChangeDate] = useState(false);
|
||||
const [currentOnboarding, setCurrentOnboarding] = useState(0);
|
||||
const [name, setName] = useState<string>("");
|
||||
const [selectedDate, setSelectedDate] = useState<string | IDate>("");
|
||||
const [compatCategory, setCompatCategory] = useState(1);
|
||||
const homeConfig = useSelector(selectors.selectHome);
|
||||
const showNavbarFooter = homeConfig.isShowNavbar;
|
||||
const birthdate = useSelector(selectors.selectBirthdate);
|
||||
const onboardingCompatibility = useSelector(
|
||||
selectors.selectOnboardingCompatibility
|
||||
);
|
||||
const zodiacSign = getZodiacSignByDate(birthdate);
|
||||
const [asset, setAsset] = useState<Asset>();
|
||||
const api = useApi();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const dateRef = useRef<HTMLInputElement>(null);
|
||||
const categoriesRef = useRef<HTMLInputElement>(null);
|
||||
const mainButtonRef = useRef<HTMLInputElement>(null);
|
||||
const locale = i18n.language;
|
||||
|
||||
const assetsData = useCallback(async () => {
|
||||
@ -59,6 +70,13 @@ function CompatibilityPage(): JSX.Element {
|
||||
|
||||
const handleNext = () => {
|
||||
if (isDisabled) return;
|
||||
dispatch(
|
||||
actions.onboardingConfig.update({
|
||||
compatibility: {
|
||||
isShown: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
dispatch(
|
||||
actions.compatibility.update({
|
||||
rightUser: {
|
||||
@ -112,6 +130,7 @@ function CompatibilityPage(): JSX.Element {
|
||||
|
||||
const handleValidDate = (date: string | IDate) => {
|
||||
setIsDisabledDate(date === "");
|
||||
setIsChangeDate(date !== getDateAsString(new Date()));
|
||||
setSelectedDate(date);
|
||||
checkAllDisabled();
|
||||
};
|
||||
@ -147,23 +166,115 @@ function CompatibilityPage(): JSX.Element {
|
||||
+
|
||||
</Title>
|
||||
<div className={styles["inputs-container"]}>
|
||||
<div className={styles["input-container__name-container"]}>
|
||||
{!onboardingCompatibility.isShown && <>
|
||||
{currentOnboarding === 0 && (
|
||||
<Onboarding
|
||||
targetRef={inputRef}
|
||||
isShow={currentOnboarding === 0}
|
||||
direction={EDirectionOnboarding.BOTTOM}
|
||||
showBackground={true}
|
||||
>
|
||||
<TextWithFinger
|
||||
text={t("au.web_onbording.name")}
|
||||
direction={EDirectionOnboarding.BOTTOM}
|
||||
crossClickHandler={() => setCurrentOnboarding(1)}
|
||||
/>
|
||||
</Onboarding>
|
||||
)}
|
||||
{currentOnboarding === 1 && (
|
||||
<Onboarding
|
||||
targetRef={dateRef}
|
||||
isShow={currentOnboarding === 1}
|
||||
direction={EDirectionOnboarding.BOTTOM}
|
||||
showBackground={true}
|
||||
>
|
||||
<>
|
||||
<button
|
||||
className={styles["compatibility-onboarding__button"]}
|
||||
disabled={!isChangeDate}
|
||||
onClick={() => setCurrentOnboarding(2)}
|
||||
>
|
||||
Done
|
||||
</button>
|
||||
<TextWithFinger
|
||||
text={t("au.web_onbording.date")}
|
||||
direction={EDirectionOnboarding.BOTTOM}
|
||||
showCross={false}
|
||||
/>
|
||||
</>
|
||||
</Onboarding>
|
||||
)}
|
||||
{currentOnboarding === 3 && (
|
||||
<Onboarding
|
||||
targetRef={mainButtonRef}
|
||||
isShow={currentOnboarding === 3}
|
||||
direction={EDirectionOnboarding.TOP}
|
||||
showBackground={true}
|
||||
>
|
||||
<TextWithFinger
|
||||
text={""}
|
||||
direction={EDirectionOnboarding.TOP}
|
||||
showCross={false}
|
||||
/>
|
||||
</Onboarding>
|
||||
)}
|
||||
</>}
|
||||
<div
|
||||
className={styles["input-container__name-container"]}
|
||||
style={{ zIndex: currentOnboarding === 0 ? 99 : 1 }}
|
||||
ref={inputRef}
|
||||
>
|
||||
<NameInput
|
||||
name="name"
|
||||
value={name}
|
||||
placeholder={t("name")}
|
||||
onKeyDown={(e) => {
|
||||
if (!name.length) return;
|
||||
if (e.key === "Enter") {
|
||||
setCurrentOnboarding(1);
|
||||
(e.target as HTMLInputElement).blur();
|
||||
dateRef.current?.scrollIntoView({ behavior: "smooth" });
|
||||
}
|
||||
}}
|
||||
onValid={handleValidName}
|
||||
onInvalid={() => setIsDisabledName(true)}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles["input-container__date-container"]}>
|
||||
<div
|
||||
className={styles["input-container__date-container"]}
|
||||
style={{ zIndex: currentOnboarding === 1 ? 99 : 1 }}
|
||||
ref={dateRef}
|
||||
>
|
||||
<DatePicker onDateChange={handleValidDate} />
|
||||
</div>
|
||||
</div>
|
||||
{currentOnboarding === 2 && !onboardingCompatibility.isShown && (
|
||||
<Onboarding
|
||||
targetRef={categoriesRef}
|
||||
isShow={currentOnboarding === 2}
|
||||
direction={EDirectionOnboarding.TOP}
|
||||
showBackground={true}
|
||||
>
|
||||
<TextWithFinger
|
||||
text={t("au.web_onbording.category")}
|
||||
direction={EDirectionOnboarding.TOP}
|
||||
showCross={true}
|
||||
crossClickHandler={() => setCurrentOnboarding(3)}
|
||||
/>
|
||||
</Onboarding>
|
||||
)}
|
||||
{data && data.length && (
|
||||
<div className={styles["compatibility-categories"]}>
|
||||
<div
|
||||
className={styles["compatibility-categories"]}
|
||||
style={{ zIndex: currentOnboarding === 2 ? 99 : 1 }}
|
||||
ref={categoriesRef}
|
||||
>
|
||||
{data.map((item, index) => (
|
||||
<div className="compatibility-categories__item" key={index}>
|
||||
<div
|
||||
className="compatibility-categories__item"
|
||||
key={index}
|
||||
onClick={() => setCurrentOnboarding(3)}
|
||||
>
|
||||
<input
|
||||
className={`${styles["compatibility-categories__input"]} ${
|
||||
compatCategory === item.id
|
||||
@ -182,13 +293,21 @@ function CompatibilityPage(): JSX.Element {
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<MainButton
|
||||
className={styles["check-btn"]}
|
||||
onClick={handleNext}
|
||||
disabled={isDisabled}
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
zIndex: currentOnboarding === 3 ? 99 : 1,
|
||||
}}
|
||||
ref={mainButtonRef}
|
||||
>
|
||||
{t("check")}
|
||||
</MainButton>
|
||||
<MainButton
|
||||
className={styles["check-btn"]}
|
||||
onClick={handleNext}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{t("check")}
|
||||
</MainButton>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@ -1,37 +1,48 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { FormField } from '@/types'
|
||||
import styles from './styles.module.css'
|
||||
import { useEffect, useState } from "react";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
const isValidName = (name: string) => {
|
||||
return name.length > 0 && name.length < 30
|
||||
interface INameInput<T> {
|
||||
name: string;
|
||||
value: T;
|
||||
label?: string | null;
|
||||
placeholder?: string | null;
|
||||
inputClassName?: string;
|
||||
onKeyDown: (event: React.KeyboardEvent<HTMLInputElement>) => void;
|
||||
onValid: (value: string) => void;
|
||||
onInvalid: () => void;
|
||||
}
|
||||
|
||||
function NameInput(props: FormField<string>): JSX.Element {
|
||||
const { name, value, placeholder, onValid, onInvalid } = props
|
||||
const [userName, setUserName] = useState(value)
|
||||
const isValidName = (name: string) => {
|
||||
return name.length > 0 && name.length < 30;
|
||||
};
|
||||
|
||||
function NameInput(props: INameInput<string>): JSX.Element {
|
||||
const { name, value, placeholder, onValid, onInvalid, onKeyDown } = props;
|
||||
const [userName, setUserName] = useState(value);
|
||||
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUserName(event.target.value)
|
||||
}
|
||||
setUserName(event.target.value);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (isValidName(userName)) {
|
||||
onValid(userName)
|
||||
onValid(userName);
|
||||
} else {
|
||||
onInvalid()
|
||||
onInvalid();
|
||||
}
|
||||
}, [userName, onInvalid, onValid])
|
||||
}, [userName, onInvalid, onValid]);
|
||||
|
||||
return (
|
||||
<div className={styles['name-input-container']}>
|
||||
<div className={styles["name-input-container"]}>
|
||||
<input
|
||||
name={name}
|
||||
type="text"
|
||||
value={userName}
|
||||
onChange={handleChange}
|
||||
placeholder={placeholder ?? ' '}
|
||||
onKeyDown={onKeyDown}
|
||||
placeholder={placeholder ?? " "}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default NameInput
|
||||
export default NameInput;
|
||||
|
||||
@ -9,7 +9,6 @@
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
|
||||
.cross {
|
||||
position: absolute;
|
||||
top: 24px;
|
||||
@ -46,10 +45,15 @@
|
||||
|
||||
.content {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
/* position: relative; */
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.input-container__name-container,
|
||||
.input-container__date-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.blurring {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@ -143,6 +147,7 @@
|
||||
}
|
||||
|
||||
.compatibility-categories {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
@ -203,3 +208,19 @@
|
||||
background-color: #ea445a;
|
||||
border-color: #ea445a;
|
||||
}
|
||||
|
||||
.compatibility-onboarding__button {
|
||||
font-size: 15;
|
||||
font-weight: 400;
|
||||
border: none;
|
||||
border-radius: 25px;
|
||||
width: 92px;
|
||||
height: 40px;
|
||||
background-color: #18D136;
|
||||
margin-top: 14px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.compatibility-onboarding__button:disabled {
|
||||
background-color: gray;
|
||||
}
|
||||
@ -4,7 +4,7 @@ import routes from "@/routes";
|
||||
import styles from "./styles.module.css";
|
||||
import { useApi, useApiCall } from "@/api";
|
||||
import { Asset } from "@/api/resources/Assets";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import BlurringSubstrate from "../BlurringSubstrate";
|
||||
import EnergyValues from "../EnergyValues";
|
||||
import { UserAura } from "@/api/resources/Auras";
|
||||
@ -19,7 +19,8 @@ import Title from "../Title";
|
||||
import { UserDailyForecast } from "@/api/resources/UserDailyForecasts";
|
||||
import { EPathsFromHome } from "@/store/siteConfig";
|
||||
import { buildFilename, saveFile } from "../WallpaperPage/utils";
|
||||
|
||||
import Onboarding from "../Onboarding";
|
||||
import TextWithFinger from "../TextWithFinger";
|
||||
|
||||
const buttonTextFormatter = (text: string): JSX.Element => {
|
||||
const sentences = text.split(".");
|
||||
@ -38,8 +39,26 @@ function HomePage(): JSX.Element {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
const buttonsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const homeConfig = useSelector(selectors.selectHome);
|
||||
const isShowNavbar = homeConfig.isShowNavbar;
|
||||
const onboardingConfigHome = useSelector(selectors.selectOnboardingHome);
|
||||
|
||||
const [isShowOnboardingHome, setIsShowOnboardingHome] = useState(
|
||||
!onboardingConfigHome.isShown
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(
|
||||
actions.onboardingConfig.update({
|
||||
home: {
|
||||
isShown: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
}, [dispatch]);
|
||||
|
||||
const handleCompatibility = () => {
|
||||
dispatch(
|
||||
actions.siteConfig.update({
|
||||
@ -57,7 +76,6 @@ function HomePage(): JSX.Element {
|
||||
navigate(routes.client.breath());
|
||||
};
|
||||
|
||||
|
||||
const { i18n } = useTranslation();
|
||||
const locale = i18n.language;
|
||||
const birthdate = useSelector(selectors.selectBirthdate);
|
||||
@ -107,7 +125,7 @@ function HomePage(): JSX.Element {
|
||||
|
||||
const downloadImg = () => {
|
||||
if (!asset) return;
|
||||
saveFile(asset.url.replace("http://", "https://"), buildFilename('1'));
|
||||
saveFile(asset.url.replace("http://", "https://"), buildFilename("1"));
|
||||
};
|
||||
|
||||
return (
|
||||
@ -133,23 +151,43 @@ function HomePage(): JSX.Element {
|
||||
{/* <a href={asset?.url.replace('http://', 'https://')} download></a> */}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.content} style={{ marginTop: isShowNavbar ? "calc(100vh - 570px)" : "calc(100vh - 500px)"}}>
|
||||
{<div className={`${styles["content__buttons"]} ${isShowNavbar ? styles["content__buttons--hidden"] : ""}`}>
|
||||
<BlurringSubstrate
|
||||
style={{ color: "#fa71ea" }}
|
||||
className={styles["content__buttons-item"]}
|
||||
clickHandler={handleCompatibility}
|
||||
<div
|
||||
className={styles.content}
|
||||
style={{
|
||||
marginTop: isShowNavbar
|
||||
? "calc(100vh - 570px)"
|
||||
: "calc(100vh - 500px)",
|
||||
}}
|
||||
>
|
||||
{
|
||||
<div
|
||||
ref={buttonsRef}
|
||||
className={`${styles["content__buttons"]} ${
|
||||
isShowNavbar ? styles["content__buttons--hidden"] : ""
|
||||
}`}
|
||||
>
|
||||
{buttonTextFormatter(t("aura-money_compatibility-button"))}
|
||||
</BlurringSubstrate>
|
||||
<BlurringSubstrate
|
||||
style={{ color: "#00f0ff" }}
|
||||
className={styles["content__buttons-item"]}
|
||||
clickHandler={handleBreath}
|
||||
>
|
||||
{buttonTextFormatter(t("aura-10_breath-button"))}
|
||||
</BlurringSubstrate>
|
||||
</div>}
|
||||
<Onboarding targetRef={buttonsRef} isShow={isShowOnboardingHome}>
|
||||
<TextWithFinger
|
||||
text={t("au.web_onbording.start")}
|
||||
crossClickHandler={() => setIsShowOnboardingHome(false)}
|
||||
/>
|
||||
</Onboarding>
|
||||
<BlurringSubstrate
|
||||
style={{ color: "#fa71ea" }}
|
||||
className={styles["content__buttons-item"]}
|
||||
clickHandler={handleCompatibility}
|
||||
>
|
||||
{buttonTextFormatter(t("aura-money_compatibility-button"))}
|
||||
</BlurringSubstrate>
|
||||
<BlurringSubstrate
|
||||
style={{ color: "#00f0ff" }}
|
||||
className={styles["content__buttons-item"]}
|
||||
clickHandler={handleBreath}
|
||||
>
|
||||
{buttonTextFormatter(t("aura-10_breath-button"))}
|
||||
</BlurringSubstrate>
|
||||
</div>
|
||||
}
|
||||
<div className={styles["content__daily-forecast"]}>
|
||||
{dailyForecast &&
|
||||
dailyForecast.forecasts.map((forecast, index) => (
|
||||
|
||||
116
src/components/Onboarding/index.tsx
Normal file
116
src/components/Onboarding/index.tsx
Normal file
@ -0,0 +1,116 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
export enum EDirectionOnboarding {
|
||||
LEFT = "left",
|
||||
RIGHT = "right",
|
||||
TOP = "top",
|
||||
BOTTOM = "bottom",
|
||||
}
|
||||
|
||||
interface OnboardingProps {
|
||||
targetRef: React.RefObject<HTMLElement>;
|
||||
isShow: boolean;
|
||||
direction?: EDirectionOnboarding;
|
||||
showBackground?: boolean;
|
||||
children: JSX.Element;
|
||||
}
|
||||
interface IBoardingCoordinates {
|
||||
top: number;
|
||||
left: number;
|
||||
}
|
||||
|
||||
const getCoordinates = (
|
||||
targetRef: React.RefObject<HTMLElement>,
|
||||
direction: EDirectionOnboarding,
|
||||
onboardingRef: React.RefObject<HTMLDivElement>
|
||||
): IBoardingCoordinates => {
|
||||
if (targetRef.current && onboardingRef.current) {
|
||||
switch (direction) {
|
||||
case EDirectionOnboarding.LEFT:
|
||||
return {
|
||||
top:
|
||||
targetRef.current.offsetTop +
|
||||
targetRef.current.offsetHeight / 2 -
|
||||
onboardingRef.current.offsetHeight / 2,
|
||||
left:
|
||||
targetRef.current.offsetLeft - onboardingRef.current.offsetWidth,
|
||||
};
|
||||
case EDirectionOnboarding.RIGHT:
|
||||
return {
|
||||
top:
|
||||
targetRef.current.offsetTop +
|
||||
targetRef.current.offsetHeight / 2 -
|
||||
onboardingRef.current.offsetHeight / 2,
|
||||
left: targetRef.current.offsetLeft + targetRef.current.offsetWidth,
|
||||
};
|
||||
case EDirectionOnboarding.TOP:
|
||||
return {
|
||||
top: targetRef.current.offsetTop - onboardingRef.current.offsetHeight,
|
||||
left:
|
||||
targetRef.current.offsetLeft +
|
||||
targetRef.current.offsetWidth / 2 -
|
||||
onboardingRef.current.offsetWidth / 2,
|
||||
};
|
||||
case EDirectionOnboarding.BOTTOM:
|
||||
return {
|
||||
top: targetRef.current.offsetTop + targetRef.current.offsetHeight,
|
||||
left:
|
||||
targetRef.current.offsetLeft +
|
||||
targetRef.current.offsetWidth / 2 -
|
||||
onboardingRef.current.offsetWidth / 2,
|
||||
};
|
||||
}
|
||||
}
|
||||
return {
|
||||
top: 0,
|
||||
left: 0,
|
||||
};
|
||||
};
|
||||
|
||||
const getClassNameContainer = (
|
||||
direction: EDirectionOnboarding,
|
||||
showBackground: boolean
|
||||
) => {
|
||||
return `${styles["onboarding-container"]} ${
|
||||
styles[`direction-${direction}`]
|
||||
} ${showBackground ? styles["background"] : ""}`;
|
||||
};
|
||||
|
||||
function Onboarding({
|
||||
targetRef,
|
||||
isShow,
|
||||
direction = EDirectionOnboarding.TOP,
|
||||
showBackground = false,
|
||||
children,
|
||||
}: OnboardingProps): JSX.Element {
|
||||
const onboardingRef = useRef<HTMLDivElement>(null);
|
||||
const [coordinates, setCoordinates] = useState<IBoardingCoordinates>({
|
||||
top: 0,
|
||||
left: 0,
|
||||
});
|
||||
useEffect(() => {
|
||||
setCoordinates(getCoordinates(targetRef, direction, onboardingRef));
|
||||
}, [direction, targetRef, children]);
|
||||
const [top, left] = [coordinates.top, coordinates.left];
|
||||
|
||||
if (!isShow) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={getClassNameContainer(direction, showBackground)}>
|
||||
<div
|
||||
className={`${styles["onboarding"]} ${
|
||||
!targetRef.current || !onboardingRef.current ? styles["hide"] : ""
|
||||
}`}
|
||||
style={{ top: `${top}px`, left: `${left}px` }}
|
||||
ref={onboardingRef}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Onboarding;
|
||||
52
src/components/Onboarding/styles.module.css
Normal file
52
src/components/Onboarding/styles.module.css
Normal file
@ -0,0 +1,52 @@
|
||||
.onboarding-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
z-index: 99;
|
||||
}
|
||||
|
||||
/* .onboarding-container.background {
|
||||
pointer-events: all;
|
||||
background-color: #000000db;
|
||||
} */
|
||||
.background {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: all;
|
||||
background-color: #000000db;
|
||||
/* z-index: 1; */
|
||||
}
|
||||
|
||||
.onboarding {
|
||||
position: absolute;
|
||||
width: 200px;
|
||||
height: fit-content;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.direction-top > .onboarding {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.direction-bottom > .onboarding {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
.direction-left > .onboarding {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.direction-right > .onboarding {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.hide {
|
||||
opacity: 0;
|
||||
}
|
||||
36
src/components/TextWithFinger/index.tsx
Normal file
36
src/components/TextWithFinger/index.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { EDirectionOnboarding } from "../Onboarding";
|
||||
import styles from "./styles.module.css";
|
||||
|
||||
interface OnboardingProps {
|
||||
text: string;
|
||||
showCross?: boolean;
|
||||
direction?: EDirectionOnboarding;
|
||||
crossClickHandler?: () => void;
|
||||
}
|
||||
|
||||
function Onboarding({
|
||||
text,
|
||||
showCross = true,
|
||||
direction = EDirectionOnboarding.TOP,
|
||||
crossClickHandler,
|
||||
}: OnboardingProps): JSX.Element {
|
||||
return (
|
||||
<>
|
||||
{text.length && (
|
||||
<div className={styles["onboarding-text"]}>
|
||||
{showCross && (
|
||||
<div className={styles["cross"]} onClick={crossClickHandler}></div>
|
||||
)}
|
||||
{text}
|
||||
</div>
|
||||
)}
|
||||
<img
|
||||
className={`${styles["finger"]} ${styles[`direction-${direction}`]}`}
|
||||
src="/finger.png"
|
||||
alt="finger"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default Onboarding;
|
||||
83
src/components/TextWithFinger/styles.module.css
Normal file
83
src/components/TextWithFinger/styles.module.css
Normal file
@ -0,0 +1,83 @@
|
||||
.onboarding-text {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
border-radius: 10px;
|
||||
padding: 24px 10px 10px;
|
||||
height: fit-content;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
.cross {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
rotate: 45deg;
|
||||
}
|
||||
|
||||
.cross::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 14px;
|
||||
height: 2px;
|
||||
background-color: #bdbdbd;
|
||||
}
|
||||
|
||||
.cross::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 2px;
|
||||
height: 14px;
|
||||
background-color: #bdbdbd;
|
||||
}
|
||||
|
||||
.finger {
|
||||
margin: 8px 0;
|
||||
animation: jump 3s ease infinite;
|
||||
}
|
||||
|
||||
.direction-bottom.finger {
|
||||
rotate: 180deg;
|
||||
}
|
||||
|
||||
.direction-left.finger {
|
||||
rotate: -90deg;
|
||||
}
|
||||
|
||||
.direction-right.finger {
|
||||
rotate: 90deg;
|
||||
}
|
||||
|
||||
@keyframes jump {
|
||||
0% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
15% {
|
||||
transform: translateY(-8px);
|
||||
}
|
||||
30% {
|
||||
transform: translateY(7px);
|
||||
}
|
||||
45% {
|
||||
transform: translateY(-6px);
|
||||
}
|
||||
60% {
|
||||
transform: translateY(5px);
|
||||
}
|
||||
75% {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
100% {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
@ -1,24 +1,64 @@
|
||||
import { combineReducers, configureStore, createAction } from '@reduxjs/toolkit'
|
||||
import token, { actions as tokenActions, selectToken } from './token'
|
||||
import user, { actions as userActions, selectUser } from './user'
|
||||
import form, { actions as formActions, selectors as formSelectors } from './form'
|
||||
import aura, { actions as auraActions } from './aura'
|
||||
import siteConfig, { selectHome, actions as siteConfigActions } from './siteConfig'
|
||||
import payment, { actions as paymentActions, selectIsDiscount } from './payment'
|
||||
import subscriptionPlans, { actions as subscriptionPlasActions, selectPlanById } from './subscriptionPlan'
|
||||
import status, { actions as userStatusActions, selectStatus } from './status'
|
||||
import compatibility, { actions as compatibilityActions } from './compatibility'
|
||||
import userCallbacks, { actions as userCallbacksActions } from './userCallbacks'
|
||||
import { loadStore, backupStore } from './storageHelper'
|
||||
import { selectAuraCoordinates } from './aura'
|
||||
import { selectSelectedPrice } from './payment'
|
||||
import { selectRightUser, selectCategoryId } from './compatibility'
|
||||
import { selectUserCallbacksDescription, selectUserCallbacksPrevStat } from './userCallbacks'
|
||||
import {
|
||||
combineReducers,
|
||||
configureStore,
|
||||
createAction,
|
||||
} from "@reduxjs/toolkit";
|
||||
import token, { actions as tokenActions, selectToken } from "./token";
|
||||
import user, { actions as userActions, selectUser } from "./user";
|
||||
import form, {
|
||||
actions as formActions,
|
||||
selectors as formSelectors,
|
||||
} from "./form";
|
||||
import aura, { actions as auraActions } from "./aura";
|
||||
import siteConfig, {
|
||||
selectHome,
|
||||
actions as siteConfigActions,
|
||||
} from "./siteConfig";
|
||||
import onboardingConfig, {
|
||||
selectOnboarding,
|
||||
selectOnboardingBreath,
|
||||
selectOnboardingCompatibility,
|
||||
selectOnboardingHome,
|
||||
actions as onboardingConfigActions,
|
||||
} from "./onboarding";
|
||||
import payment, {
|
||||
actions as paymentActions,
|
||||
selectIsDiscount,
|
||||
} from "./payment";
|
||||
import subscriptionPlans, {
|
||||
actions as subscriptionPlasActions,
|
||||
selectPlanById,
|
||||
} from "./subscriptionPlan";
|
||||
import status, { actions as userStatusActions, selectStatus } from "./status";
|
||||
import compatibility, {
|
||||
actions as compatibilityActions,
|
||||
} from "./compatibility";
|
||||
import userCallbacks, {
|
||||
actions as userCallbacksActions,
|
||||
} from "./userCallbacks";
|
||||
import { loadStore, backupStore } from "./storageHelper";
|
||||
import { selectAuraCoordinates } from "./aura";
|
||||
import { selectSelectedPrice } from "./payment";
|
||||
import { selectRightUser, selectCategoryId } from "./compatibility";
|
||||
import {
|
||||
selectUserCallbacksDescription,
|
||||
selectUserCallbacksPrevStat,
|
||||
} from "./userCallbacks";
|
||||
|
||||
|
||||
|
||||
const preloadedState = loadStore()
|
||||
export const reducer = combineReducers({ token, user, form, status, subscriptionPlans, aura, payment, compatibility, userCallbacks, siteConfig })
|
||||
const preloadedState = loadStore();
|
||||
export const reducer = combineReducers({
|
||||
token,
|
||||
user,
|
||||
form,
|
||||
status,
|
||||
subscriptionPlans,
|
||||
aura,
|
||||
payment,
|
||||
compatibility,
|
||||
userCallbacks,
|
||||
siteConfig,
|
||||
onboardingConfig,
|
||||
});
|
||||
export const actions = {
|
||||
token: tokenActions,
|
||||
user: userActions,
|
||||
@ -30,8 +70,9 @@ export const actions = {
|
||||
compatibility: compatibilityActions,
|
||||
payment: paymentActions,
|
||||
userCallbacks: userCallbacksActions,
|
||||
reset: createAction('reset'),
|
||||
}
|
||||
onboardingConfig: onboardingConfigActions,
|
||||
reset: createAction("reset"),
|
||||
};
|
||||
export const selectors = {
|
||||
selectToken,
|
||||
selectUser,
|
||||
@ -45,14 +86,18 @@ export const selectors = {
|
||||
selectUserCallbacksPrevStat,
|
||||
selectHome,
|
||||
selectIsDiscount,
|
||||
selectOnboarding,
|
||||
selectOnboardingHome,
|
||||
selectOnboardingCompatibility,
|
||||
selectOnboardingBreath,
|
||||
...formSelectors,
|
||||
}
|
||||
export type RootState = ReturnType<typeof reducer>
|
||||
};
|
||||
export type RootState = ReturnType<typeof reducer>;
|
||||
export const store = configureStore({
|
||||
reducer,
|
||||
preloadedState,
|
||||
devTools: import.meta.env.DEV,
|
||||
})
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
export type StoreType = typeof store
|
||||
export const unsubscribe = backupStore(store)
|
||||
});
|
||||
export type AppDispatch = typeof store.dispatch;
|
||||
export type StoreType = typeof store;
|
||||
export const unsubscribe = backupStore(store);
|
||||
|
||||
57
src/store/onboarding.ts
Normal file
57
src/store/onboarding.ts
Normal file
@ -0,0 +1,57 @@
|
||||
import { createSlice, createSelector } from "@reduxjs/toolkit";
|
||||
import type { PayloadAction } from "@reduxjs/toolkit";
|
||||
|
||||
interface IOnboardingConfig {
|
||||
home: {
|
||||
isShown: boolean;
|
||||
};
|
||||
compatibility: {
|
||||
isShown: boolean;
|
||||
};
|
||||
breath: {
|
||||
isShown: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
const initialState: IOnboardingConfig = {
|
||||
home: {
|
||||
isShown: false,
|
||||
},
|
||||
compatibility: {
|
||||
isShown: false,
|
||||
},
|
||||
breath: {
|
||||
isShown: false,
|
||||
},
|
||||
};
|
||||
|
||||
const onboardingConfigSlice = createSlice({
|
||||
name: "onboardingConfig",
|
||||
initialState,
|
||||
reducers: {
|
||||
update(state, action: PayloadAction<Partial<IOnboardingConfig>>) {
|
||||
return { ...state, ...action.payload };
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => builder.addCase("reset", () => initialState),
|
||||
});
|
||||
|
||||
export const { actions } = onboardingConfigSlice;
|
||||
export const selectOnboarding = createSelector(
|
||||
(state: { onboardingConfig: IOnboardingConfig }) => state.onboardingConfig,
|
||||
(onboardingConfig) => onboardingConfig
|
||||
);
|
||||
export const selectOnboardingHome = createSelector(
|
||||
(state: { onboardingConfig: IOnboardingConfig }) => state.onboardingConfig.home,
|
||||
(onboardingConfig) => onboardingConfig
|
||||
);
|
||||
export const selectOnboardingCompatibility = createSelector(
|
||||
(state: { onboardingConfig: IOnboardingConfig }) =>
|
||||
state.onboardingConfig.compatibility,
|
||||
(onboardingConfig) => onboardingConfig
|
||||
);
|
||||
export const selectOnboardingBreath = createSelector(
|
||||
(state: { onboardingConfig: IOnboardingConfig }) => state.onboardingConfig.breath,
|
||||
(onboardingConfig) => onboardingConfig
|
||||
);
|
||||
export default onboardingConfigSlice.reducer;
|
||||
Loading…
Reference in New Issue
Block a user