diff --git a/src/api/resources/User.ts b/src/api/resources/User.ts index d814463..f0a1209 100644 --- a/src/api/resources/User.ts +++ b/src/api/resources/User.ts @@ -171,6 +171,8 @@ export interface ICreateAuthorizePayload { source: ESourceAuthorization; profile?: Partial; partner?: Partial>; + sign?: boolean; + signDate?: string; } export interface ICreateAuthorizeResponse { diff --git a/src/components/pages/ABDesign/v1/components/Checkbox/index.tsx b/src/components/pages/ABDesign/v1/components/Checkbox/index.tsx new file mode 100644 index 0000000..4988a4d --- /dev/null +++ b/src/components/pages/ABDesign/v1/components/Checkbox/index.tsx @@ -0,0 +1,34 @@ +import styles from "./styles.module.css"; + +interface ICheckboxProps { + checked: boolean; + onChange: () => void; +} + +function Checkbox({ checked, onChange }: ICheckboxProps) { + return ( +
+ + + + + + + +
+ ); +} + +export default Checkbox; diff --git a/src/components/pages/ABDesign/v1/components/Checkbox/styles.module.css b/src/components/pages/ABDesign/v1/components/Checkbox/styles.module.css new file mode 100644 index 0000000..d760c66 --- /dev/null +++ b/src/components/pages/ABDesign/v1/components/Checkbox/styles.module.css @@ -0,0 +1,92 @@ +.checkbox-wrapper-4 * { + box-sizing: border-box; +} +.checkbox-wrapper-4 .cbx { + -webkit-user-select: none; + user-select: none; + cursor: pointer; + padding: 2px; + border-radius: 6px; + overflow: hidden; + transition: all 0.2s ease; + display: inline-block; +} +.checkbox-wrapper-4 .cbx span { + float: left; + vertical-align: middle; + transform: translate3d(0, 0, 0); +} +.checkbox-wrapper-4 .cbx span:first-child { + position: relative; + width: 20px; + height: 20px; + border-radius: 4px; + transform: scale(1); + border: 1px solid #484848; + transition: all 0.2s ease; + box-shadow: 0 1px 1px rgba(0,16,75,0.05); +} +.checkbox-wrapper-4 .cbx span:first-child svg { + position: absolute; + top: 50%; + left: 50%; + fill: none; + stroke: #fff; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; + stroke-dasharray: 16px; + stroke-dashoffset: 16px; + transition: all 0.3s ease; + transition-delay: 0.1s; + transform: translate3d(-50%, -50%, 0); +} +.checkbox-wrapper-4 .cbx span:last-child { + padding-left: 8px; + line-height: 18px; +} +.checkbox-wrapper-4 .inp-cbx { + position: absolute; + visibility: hidden; +} +.checkbox-wrapper-4 .inp-cbx:checked + .cbx span:first-child { + background: #07f; + border-color: #07f; + animation: wave-4 0.4s ease; +} +.checkbox-wrapper-4 .inp-cbx:checked + .cbx span:first-child svg { + stroke-dashoffset: 0; +} +.checkbox-wrapper-4 .inline-svg { + position: absolute; + width: 0; + height: 0; + pointer-events: none; + user-select: none; +} +@media screen and (max-width: 640px) { + .checkbox-wrapper-4 .cbx { + width: 100%; + display: inline-block; + } +} +@-moz-keyframes wave-4 { + 50% { + transform: scale(0.9); + } +} +@-webkit-keyframes wave-4 { + 50% { + transform: scale(0.9); + } +} +@-o-keyframes wave-4 { + 50% { + transform: scale(0.9); + } +} +@keyframes wave-4 { + 50% { + transform: scale(0.9); + } +} \ No newline at end of file diff --git a/src/components/pages/ABDesign/v1/components/PrivacyPolicy/index.tsx b/src/components/pages/ABDesign/v1/components/PrivacyPolicy/index.tsx new file mode 100644 index 0000000..6ec6744 --- /dev/null +++ b/src/components/pages/ABDesign/v1/components/PrivacyPolicy/index.tsx @@ -0,0 +1,45 @@ +import Checkbox from "../Checkbox"; +import styles from "./styles.module.css"; +import { useDispatch, useSelector } from "react-redux"; +import { actions, selectors } from "@/store"; + +interface IPrivacyPolicyProps { + containerClassName?: string; +} + +function PrivacyPolicy({ containerClassName = "" }: IPrivacyPolicyProps) { + const dispatch = useDispatch(); + const { checked } = useSelector(selectors.selectPrivacyPolicy); + + const handleChange = () => { + dispatch(actions.privacyPolicy.updateChecked(!checked)); + }; + + return ( +
+ +

+ I agree to the{" "} + + Privacy Policy + + , + + Terms of use + {" "} + and to the use of cookies and tracking technologies, that require your + consent +

+
+ ); +} + +export default PrivacyPolicy; diff --git a/src/components/pages/ABDesign/v1/components/PrivacyPolicy/styles.module.css b/src/components/pages/ABDesign/v1/components/PrivacyPolicy/styles.module.css new file mode 100644 index 0000000..3cab999 --- /dev/null +++ b/src/components/pages/ABDesign/v1/components/PrivacyPolicy/styles.module.css @@ -0,0 +1,12 @@ +.container { + width: 100%; + display: flex; + flex-direction: row; + gap: 8px; +} + +.text { + font-size: 14px; + line-height: 125%; + color: #515151; +} \ No newline at end of file diff --git a/src/components/pages/ABDesign/v1/components/Toast/ErrorIcon/index.tsx b/src/components/pages/ABDesign/v1/components/Toast/ErrorIcon/index.tsx new file mode 100644 index 0000000..2a532ab --- /dev/null +++ b/src/components/pages/ABDesign/v1/components/Toast/ErrorIcon/index.tsx @@ -0,0 +1,24 @@ +function ErrorIcon() { + return ( + + + + + + ); +} + +export default ErrorIcon; diff --git a/src/components/pages/ABDesign/v1/components/Toast/index.tsx b/src/components/pages/ABDesign/v1/components/Toast/index.tsx new file mode 100644 index 0000000..6f94359 --- /dev/null +++ b/src/components/pages/ABDesign/v1/components/Toast/index.tsx @@ -0,0 +1,28 @@ +import React from "react"; +import ErrorIcon from "./ErrorIcon"; +import styles from "./styles.module.css"; + +interface IToastProps { + variant: "error"; + children: React.ReactNode; + classNameContainer?: string; + classNameToast?: string; +} + +function Toast({ + variant, + children, + classNameContainer = "", + classNameToast = "", +}: IToastProps) { + return ( +
+
+ {variant === "error" && } + {children} +
+
+ ); +} + +export default Toast; diff --git a/src/components/pages/ABDesign/v1/components/Toast/styles.module.css b/src/components/pages/ABDesign/v1/components/Toast/styles.module.css new file mode 100644 index 0000000..202a15a --- /dev/null +++ b/src/components/pages/ABDesign/v1/components/Toast/styles.module.css @@ -0,0 +1,26 @@ +.toast { + width: 100%; + display: grid; + grid-template-columns: 24px 1fr; + gap: 6px; + align-items: center; + padding: 16px; + border-radius: 12px; + font-size: 14px; + color: #000; + animation: appearance .8s linear(0 0%, 0 1.8%, 0.01 3.6%, 0.08 10.03%, 0.15 14.25%, 0.2 14.34%, 0.31 14.14%, 0.41 17.21%, 0.49 19.04%, 0.58 20.56%, 0.66 22.07%, 0.76 23.87%, 0.84 26.07%, 0.93 28.04%, 1.03 31.14%, 1.09 37.31%, 1.09 44.28%, 1.02 49.41%, 0.96 55%, 0.98 64%, 0.99 74.4%, 1 86.4%, 1 100%); + animation-fill-mode: forwards; +} + +.toast.error { + background-color: #ffdcdc; +} + +@keyframes appearance { + 0% { + transform: translateY(100%); + } + 100% { + transform: translateY(0); + } +} \ No newline at end of file diff --git a/src/components/pages/ABDesign/v1/pages/Gender/index.tsx b/src/components/pages/ABDesign/v1/pages/Gender/index.tsx index 037c415..11fd48f 100644 --- a/src/components/pages/ABDesign/v1/pages/Gender/index.tsx +++ b/src/components/pages/ABDesign/v1/pages/Gender/index.tsx @@ -3,14 +3,16 @@ import Title from "@/components/Title"; import { Gender } from "@/data"; import { EProductKeys } from "@/data/products"; import routes from "@/routes"; -import { actions } from "@/store"; +import { actions, selectors } from "@/store"; import { useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; +import { useDispatch, useSelector } from "react-redux"; import { useNavigate, useParams } from "react-router-dom"; import BackgroundTopBlob from "../../ui/BackgroundTopBlob"; import { useDynamicSize } from "@/hooks/useDynamicSize"; import Header from "../../components/Header"; import { genders } from "../../data/genders"; +import PrivacyPolicy from "../../components/PrivacyPolicy"; +import Toast from "../../components/Toast"; interface IGenderPageProps { productKey?: EProductKeys; @@ -22,16 +24,26 @@ function GenderPage({ productKey }: IGenderPageProps): JSX.Element { const { targetId } = useParams(); const { width: pageWidth, elementRef: pageRef } = useDynamicSize({}); const [selectedGender, setSelectedGender] = useState(null); + const { checked: privacyPolicyChecked } = useSelector( + selectors.selectPrivacyPolicy + ); useEffect(() => { const isShowTryApp = targetId === "i"; dispatch(actions.userConfig.addIsShowTryApp(isShowTryApp)); }, [dispatch, targetId]); - const selectGender = async (gender: Gender) => { - setSelectedGender(gender); + useEffect(() => { + if (privacyPolicyChecked && selectedGender) { + handleNext(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [privacyPolicyChecked, selectedGender]); + + const handleNext = async () => { + if (!selectedGender) return; await new Promise((resolve) => setTimeout(resolve, 1000)); - dispatch(actions.questionnaire.update({ gender: gender.id })); + dispatch(actions.questionnaire.update({ gender: selectedGender.id })); if (productKey === EProductKeys["moons.pdf.aura"]) { return navigate(routes.client.epeBirthdate()); } @@ -41,6 +53,18 @@ function GenderPage({ productKey }: IGenderPageProps): JSX.Element { navigate(`/v1/questionnaire/profile/flowChoice`); }; + const selectGender = async (gender: Gender) => { + if (selectedGender?.id === gender.id) { + setSelectedGender(null); + } else { + setSelectedGender(gender); + } + if (!privacyPolicyChecked) { + return; + } + handleNext(); + }; + const getButtonBGColor = (gender: Gender): string => { const { colorAssociation } = gender; if (Array.isArray(colorAssociation)) { @@ -114,6 +138,12 @@ function GenderPage({ productKey }: IGenderPageProps): JSX.Element { ))} + + {selectedGender && !privacyPolicyChecked && ( + + To continue, please accept our terms and policies + + )} ); } diff --git a/src/components/pages/ABDesign/v1/pages/Gender/styles.module.css b/src/components/pages/ABDesign/v1/pages/Gender/styles.module.css index 09251cc..6f12d89 100644 --- a/src/components/pages/ABDesign/v1/pages/Gender/styles.module.css +++ b/src/components/pages/ABDesign/v1/pages/Gender/styles.module.css @@ -106,18 +106,43 @@ .gender--selected { transform: scale(1.1); - /* animation: gender-click 1s linear; */ + /* animation: gender-click 1.4s linear; */ } .gender--selected .gender__slide-element { left: calc(100% - 27px - 10px); + /* animation: gender-slide 1.4s linear; */ +} + +.privacy-policy { + max-width: 316px; + margin-top: 26px; +} + +.toast-container { + margin-top: 16px; } @keyframes gender-click { 0% { transform: scale(1); } - 100% { + 50% { transform: scale(1.1); } + 100% { + transform: scale(1); + } +} + +@keyframes gender-slide { + 0% { + left: 10px; + } + 50% { + left: calc(100% - 27px - 10px); + } + 100% { + left: 10px; + } } \ No newline at end of file diff --git a/src/hooks/authentication/use-authentication.ts b/src/hooks/authentication/use-authentication.ts index da7602f..a1ec078 100644 --- a/src/hooks/authentication/use-authentication.ts +++ b/src/hooks/authentication/use-authentication.ts @@ -33,6 +33,7 @@ export const useAuthentication = () => { partnerBirthPlace, partnerBirthtime, } = useSelector(selectors.selectQuestionnaire) + const { checked, dateOfCheck } = useSelector(selectors.selectPrivacyPolicy) const birthdateFromForm = useSelector(selectors.selectBirthdate); const birthtimeFromForm = useSelector(selectors.selectBirthtime); @@ -94,7 +95,9 @@ export const useAuthentication = () => { birthplace: { address: partnerBirthPlace, }, - } + }, + sign: checked, + signDate: dateOfCheck }) // eslint-disable-next-line react-hooks/exhaustive-deps }, [ @@ -108,6 +111,8 @@ export const useAuthentication = () => { partnerName, username, birthtime, + checked, + dateOfCheck ]); const authorization = useCallback(async (email: string, source: ESourceAuthorization) => { diff --git a/src/store/index.ts b/src/store/index.ts index 868cc4d..c1357a4 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -69,6 +69,7 @@ import palmistry, { selectPalmistryLines, } from "./palmistry"; import { selectPaywallsIsMustUpdate, selectPaywalls } from "./paywalls"; +import privacyPolicy, { actions as privacyPolicyActions, selectPrivacyPolicy } from "./privacyPolicy"; const preloadedState = loadStore(); export const actions = { @@ -88,6 +89,7 @@ export const actions = { questionnaire: questionnaireActions, userConfig: userConfigActions, palmistry: palmistryActions, + privacyPolicy: privacyPolicyActions, reset: createAction("reset"), }; export const selectors = { @@ -120,6 +122,7 @@ export const selectors = { selectPalmistryLines, selectPaywalls, selectPaywallsIsMustUpdate, + selectPrivacyPolicy, ...formSelectors, }; @@ -140,6 +143,7 @@ export const reducer = combineReducers({ userConfig, palmistry, paywalls, + privacyPolicy }); export type RootState = ReturnType; diff --git a/src/store/privacyPolicy.ts b/src/store/privacyPolicy.ts new file mode 100644 index 0000000..a70b5c0 --- /dev/null +++ b/src/store/privacyPolicy.ts @@ -0,0 +1,30 @@ +import { createSlice, createSelector } from '@reduxjs/toolkit' +import type { PayloadAction } from '@reduxjs/toolkit' + +interface IPrivacyPolicy { + checked: boolean + dateOfCheck: string +} + +const initialState: IPrivacyPolicy = { + checked: false, + dateOfCheck: '', +} + +const privacyPolicySlice = createSlice({ + name: 'privacyPolicy', + initialState, + reducers: { + updateChecked(state, action: PayloadAction) { + return { ...state, checked: action.payload, dateOfCheck: new Date().toISOString() } + }, + }, + extraReducers: (builder) => builder.addCase('reset', () => initialState), +}) + +export const { actions } = privacyPolicySlice +export const selectPrivacyPolicy = createSelector( + (state: { privacyPolicy: IPrivacyPolicy }) => state.privacyPolicy, + (privacyPolicy) => privacyPolicy +) +export default privacyPolicySlice.reducer