diff --git a/package-lock.json b/package-lock.json index 1e31c6f..e1139c6 100755 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@stripe/react-stripe-js": "^2.3.1", "@stripe/stripe-js": "^2.1.9", "apng-js": "^1.1.1", + "framer-motion": "^11.0.8", "html-react-parser": "^3.0.16", "i18next": "^22.5.0", "i18next-react-postprocessor": "^3.1.0", @@ -430,6 +431,21 @@ "integrity": "sha512-JsHlIAjZDwX2Q/vDGN4xzKRC8n1K4xCwzKl7wZOOwUH9ow030CRspVRkP3OWHrY5gLmpbmICc/iK2aptF3t/Ow==", "dev": true }, + "node_modules/@emotion/is-prop-valid": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", + "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "optional": true, + "dependencies": { + "@emotion/memoize": "0.7.4" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "optional": true + }, "node_modules/@esbuild/android-arm": { "version": "0.17.18", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.18.tgz", @@ -2145,6 +2161,34 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "node_modules/framer-motion": { + "version": "11.0.8", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.0.8.tgz", + "integrity": "sha512-1KSGNuqe1qZkS/SWQlDnqK2VCVzRVEoval379j0FiUBJAZoqgwyvqFkfvJbgW2IPFo4wX16K+M0k5jO23lCIjA==", + "dependencies": { + "tslib": "^2.4.0" + }, + "optionalDependencies": { + "@emotion/is-prop-valid": "^0.8.2" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/framer-motion/node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3821,6 +3865,21 @@ "integrity": "sha512-JsHlIAjZDwX2Q/vDGN4xzKRC8n1K4xCwzKl7wZOOwUH9ow030CRspVRkP3OWHrY5gLmpbmICc/iK2aptF3t/Ow==", "dev": true }, + "@emotion/is-prop-valid": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", + "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", + "optional": true, + "requires": { + "@emotion/memoize": "0.7.4" + } + }, + "@emotion/memoize": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", + "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", + "optional": true + }, "@esbuild/android-arm": { "version": "0.17.18", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.18.tgz", @@ -4957,6 +5016,22 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, + "framer-motion": { + "version": "11.0.8", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.0.8.tgz", + "integrity": "sha512-1KSGNuqe1qZkS/SWQlDnqK2VCVzRVEoval379j0FiUBJAZoqgwyvqFkfvJbgW2IPFo4wX16K+M0k5jO23lCIjA==", + "requires": { + "@emotion/is-prop-valid": "^0.8.2", + "tslib": "^2.4.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + } + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", diff --git a/package.json b/package.json index 7051e7b..c66ba58 100755 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@stripe/react-stripe-js": "^2.3.1", "@stripe/stripe-js": "^2.1.9", "apng-js": "^1.1.1", + "framer-motion": "^11.0.8", "html-react-parser": "^3.0.16", "i18next": "^22.5.0", "i18next-react-postprocessor": "^3.1.0", diff --git a/src/assets/media/alata-regular.woff2 b/src/assets/media/alata-regular.woff2 new file mode 100644 index 0000000..53b0589 Binary files /dev/null and b/src/assets/media/alata-regular.woff2 differ diff --git a/src/assets/media/open-sans-bold.woff2 b/src/assets/media/open-sans-bold.woff2 new file mode 100644 index 0000000..dd0873b Binary files /dev/null and b/src/assets/media/open-sans-bold.woff2 differ diff --git a/src/assets/media/open-sans-regular.woff2 b/src/assets/media/open-sans-regular.woff2 new file mode 100644 index 0000000..46373fa Binary files /dev/null and b/src/assets/media/open-sans-regular.woff2 differ diff --git a/src/assets/media/open-sans-semibold.woff2 b/src/assets/media/open-sans-semibold.woff2 new file mode 100644 index 0000000..57ce22c Binary files /dev/null and b/src/assets/media/open-sans-semibold.woff2 differ diff --git a/src/components/App/index.tsx b/src/components/App/index.tsx index 128bce2..decbb9a 100755 --- a/src/components/App/index.tsx +++ b/src/components/App/index.tsx @@ -103,6 +103,7 @@ import AdditionalPurchases from "../pages/AdditionalPurchases"; import AddReportPage from "../pages/AdditionalPurchases/pages/AddReport"; import UnlimitedReadingsPage from "../pages/AdditionalPurchases/pages/UnlimitedReadings"; import AddConsultationPage from "../pages/AdditionalPurchases/pages/AddConsultation"; +import StepsManager from "@/components/palmistry/steps-manager/steps-manager"; const isProduction = import.meta.env.MODE === "production"; @@ -470,6 +471,17 @@ function App(): JSX.Element { /> + + } + /> + + } + /> + } /> diff --git a/src/components/Header/index.tsx b/src/components/Header/index.tsx index f55b780..f8a3df2 100755 --- a/src/components/Header/index.tsx +++ b/src/components/Header/index.tsx @@ -10,6 +10,7 @@ import BackButton from "./BackButton"; import iconUrl from "./icon.png"; import menuUrl from "./menu.png"; import styles from "./styles.module.css"; +import usePalmistrySteps from '@/hooks/palmistry/use-steps'; type HeaderProps = { openMenu?: () => void; @@ -37,6 +38,7 @@ function Header({ const showBackButton = isNotEntrypoint(location.pathname); const showMenuButton = hasNavigation(location.pathname); const showCrossButton = hasCrossButton(location.pathname); + const palmistrySteps = usePalmistrySteps(); useEffect(() => { if (!initialPath) { @@ -48,6 +50,10 @@ function Header({ }, [location.pathname, initialPath, isNavigated]); const goBack = () => { + if (location.pathname.includes("/palmistry")) { + palmistrySteps.goBack(); + return; + } if ( location.pathname.includes("/questionnaire") || location.pathname.includes("/about-us") || diff --git a/src/components/Navbar/index.tsx b/src/components/Navbar/index.tsx index 98bf907..7c6b62f 100644 --- a/src/components/Navbar/index.tsx +++ b/src/components/Navbar/index.tsx @@ -20,6 +20,7 @@ function Navbar({ isOpen, closeMenu }: NavbarProps): JSX.Element { const combinedClassNames = ['navbar', isOpen && 'navbar--open'].filter(Boolean).join(' ') const handleLogout = () => { + localStorage.removeItem('palmistry.firstUnpassedStep'); navigate(routes.client.birthday()) logout() } diff --git a/src/components/palmistry/alert-modal/alert-modal.css b/src/components/palmistry/alert-modal/alert-modal.css new file mode 100644 index 0000000..7245b1b --- /dev/null +++ b/src/components/palmistry/alert-modal/alert-modal.css @@ -0,0 +1,7 @@ +.alert-modal__title { + font-size: 24px; + font-weight: 500; + line-height: 21px; + margin-bottom: 24px; + text-align: center; +} diff --git a/src/components/palmistry/alert-modal/alert-modal.tsx b/src/components/palmistry/alert-modal/alert-modal.tsx new file mode 100644 index 0000000..4884f88 --- /dev/null +++ b/src/components/palmistry/alert-modal/alert-modal.tsx @@ -0,0 +1,22 @@ +import './alert-modal.css'; + +import Modal, { ModalType } from '../modal/modal'; +import ModalOverlay, { ModalOverlayType } from '../modal-overlay/modal-overlay'; + +type Props = { + title: string; + onClose: () => void; + children: React.ReactNode; +}; + +export default function AlertModal(props: Props) { + return ( + + +

{props.title}

+ + {props.children} +
+
+ ); +} diff --git a/src/components/palmistry/biometric-data/biometric-data.css b/src/components/palmistry/biometric-data/biometric-data.css new file mode 100644 index 0000000..a999c09 --- /dev/null +++ b/src/components/palmistry/biometric-data/biometric-data.css @@ -0,0 +1,9 @@ +.biometric-data { + color: var(--footer-small-text); + font-size: 14px; + font-weight: 400; + line-height: 18px; + max-width: 400px; + text-align: start; + width: 100%; +} diff --git a/src/components/palmistry/biometric-data/biometric-data.tsx b/src/components/palmistry/biometric-data/biometric-data.tsx new file mode 100644 index 0000000..fc44a67 --- /dev/null +++ b/src/components/palmistry/biometric-data/biometric-data.tsx @@ -0,0 +1,18 @@ +import './biometric-data.css'; + +export default function BiometricData() { + return ( +
+ + + + {' '} + No biometric data collected. All recognition process performs on your device. +
+ ); +} diff --git a/src/components/palmistry/button/button.css b/src/components/palmistry/button/button.css new file mode 100644 index 0000000..02523dd --- /dev/null +++ b/src/components/palmistry/button/button.css @@ -0,0 +1,89 @@ +.button { + align-items: center; + background: var(--button-background); + border: none; + border-radius: 8px; + color: var(--button-color); + cursor: pointer; + display: flex; + font-size: 18px; + font-weight: 500; + justify-content: center; + line-height: 20px; + max-width: 400px; + min-height: 60px; + min-width: 250px; + padding: 12px 16px; + width: 100%; +} + +.button__spinner { + animation: spinner-rotate 2s linear infinite; + height: 50px; + left: 50%; + margin: -25px 0 0 -25px; + position: absolute; + top: 50%; + width: 50px; + z-index: 2; +} + +.button__spinner-path { + stroke: #93bfec; + stroke-linecap: round; + animation: spinner-dash 1.5s ease-in-out infinite; +} + +.button:active:not(.button_disabled) { + animation-duration: 0.2s; + animation-iteration-count: 1; + animation-name: button-scale; + background: var(--button-active-bg); +} + +.button_disabled { + background: var(--light-silver); + cursor: not-allowed; +} + +.button_active { + background: var(--strong-blue); + color: var(--button-active); +} + +@keyframes button-scale { + 0% { + -webkit-transform: scale(1); + transform: scale(1); + } + 50% { + -webkit-transform: scale(0.95); + transform: scale(0.95); + } + + 100% { + -webkit-transform: scale(1); + transform: scale(1); + } +} + +@keyframes spinner-rotate { + 100% { + transform: rotate(1turn); + } +} + +@keyframes spinner-dash { + 0% { + stroke-dasharray: 1, 150; + stroke-dashoffset: 0; + } + 50% { + stroke-dasharray: 90, 150; + stroke-dashoffset: -35; + } + 100% { + stroke-dasharray: 90, 150; + stroke-dashoffset: -124; + } +} diff --git a/src/components/palmistry/button/button.tsx b/src/components/palmistry/button/button.tsx new file mode 100644 index 0000000..55ef70b --- /dev/null +++ b/src/components/palmistry/button/button.tsx @@ -0,0 +1,44 @@ +import './button.css'; + +type Props = { + children: React.ReactNode; + type: 'button' | 'submit'; + disabled?: boolean; + onClick?: () => void; + active?: boolean; + className?: string; + isProcessing?: boolean; +}; + +export default function Button(props: Props) { + const className = ['button']; + + if (props.disabled) { + className.push('button_disabled'); + } + + if (props.active && !props.disabled) { + className.push('button_active'); + } + + if (props.className) { + className.push(props.className); + } + + return ( + + ); +} diff --git a/src/components/palmistry/color-circle/color-circle.css b/src/components/palmistry/color-circle/color-circle.css new file mode 100644 index 0000000..a1253b6 --- /dev/null +++ b/src/components/palmistry/color-circle/color-circle.css @@ -0,0 +1,6 @@ +.color-circle { + border: 2px solid #fff; + border-radius: 50%; + height: 40px; + width: 40px; +} diff --git a/src/components/palmistry/color-circle/color-circle.tsx b/src/components/palmistry/color-circle/color-circle.tsx new file mode 100644 index 0000000..f30f247 --- /dev/null +++ b/src/components/palmistry/color-circle/color-circle.tsx @@ -0,0 +1,9 @@ +import './color-circle.css'; + +type Props = { + color: string; +}; + +export default function ColorCircle(props: Props) { + return
+} diff --git a/src/components/palmistry/discount-screen/discount-screen.css b/src/components/palmistry/discount-screen/discount-screen.css new file mode 100644 index 0000000..e120a92 --- /dev/null +++ b/src/components/palmistry/discount-screen/discount-screen.css @@ -0,0 +1,113 @@ +.discount-screen { + margin: 0 auto; + position: relative; + max-width: 428px; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; +} + +.discount-screen__header { + display: flex; + width: 100%; + padding: 24px 0 11px; + justify-content: center; +} + +.discount-screen__content { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + padding: 0 32px 32px; +} + +.discount-screen__content * { + font-family: OpenSans Regular; +} + +.discount-screen__title { + font-size: 20px; + margin-top: 23px; + font-weight: 600; + line-height: 24px; + text-align: center; + margin-bottom: 24px; +} + +.discount-screen__blocks { + gap: 7px; + display: flex; + align-items: flex-end; +} + +.discount-screen__block { + gap: 12px; + width: 160px; + display: flex; + padding: 16px; + overflow: hidden; + text-align: center; + align-items: center; + border-radius: 10px; + flex-direction: column; +} + +.discount-screen__block:first-child { + border: 2px solid #c7c7c7; +} + +.discount-screen__block:last-child { + padding-top: 0; + border: 2px solid #066fde; +} + +.discount-screen__price-block { + font-weight: 600; + line-height: 24px; +} + +.discount-screen__details { + display: flex; + flex-direction: column; +} + +.discount-screen__details-name { + color: #8e8e93; + font-size: 14px; + line-height: 18px; +} + +.discount-screen__details-value { + font-weight: 600; + line-height: 24px; +} + +.discount-screen__button { + position: relative; + display: flex; + justify-content: center; + align-items: center; + width: 132px; + height: fit-content; + min-height: 38px; + background: #066fde; + border-radius: 10px; + border: none; + font-weight: 600; + font-size: 14px; + line-height: 18px; + color: #fff; +} + +.discount-screen__header-block { + top: 0; + color: #fff; + height: 32px; + display: flex; + background: #066fde; + align-items: center; + width: calc(100% + 32px); + justify-content: center; +} diff --git a/src/components/palmistry/discount-screen/discount-screen.tsx b/src/components/palmistry/discount-screen/discount-screen.tsx new file mode 100644 index 0000000..19e2115 --- /dev/null +++ b/src/components/palmistry/discount-screen/discount-screen.tsx @@ -0,0 +1,78 @@ +import React from "react"; + +import { useNavigate } from 'react-router-dom'; + +import './discount-screen.css'; + +import routes from '@/routes'; +import HeaderLogo from '@/components/palmistry/header-logo/header-logo'; + +export default function DiscountScreen() { + const navigate = useNavigate(); + + const userHasWeeklySubscription = false; + + const goPremiumBundle = () => { + navigate(routes.client.palmistryPremiumBundle()); + }; + + React.useEffect(() => { + if (userHasWeeklySubscription) { + goPremiumBundle(); + } + }, [userHasWeeklySubscription]); + + return ( +
+
+ +
+ +
+ + Not planning on looking back? + + +
+
+ €19 for
1-week plan
+ +
+ Total savings + €0 +
+ +
+ 7-day trial + yes +
+ + +
+ +
+
save 33%
+ + €12.73 for
1-week plan
+ +
+ Total savings + €6.27 +
+ +
+ 3-day trial + no +
+ + +
+
+
+
+ ); +} diff --git a/src/components/palmistry/email-header/email-header.css b/src/components/palmistry/email-header/email-header.css new file mode 100644 index 0000000..1ed7c24 --- /dev/null +++ b/src/components/palmistry/email-header/email-header.css @@ -0,0 +1,37 @@ +.email-header { + align-items: center; + display: flex; + height: 39px; + justify-content: flex-end; + margin-bottom: 32px; + padding: 0 32px; + width: 100%; + background: var(--light-cornflower-blue); +} + +.email-header h1 { + font-size: 14px; + line-height: 140%; + color: var(--white); + font-weight: 500; +} + +.email-header div { + align-items: center; + background: var(--pale-gray); + border-radius: 50%; + display: flex; + height: 27px; + justify-content: center; + margin-left: 10px; + width: 27px; + background: var(--gentle-blue); +} + +.email-header div h1 { + color: #fff; + font-size: 14px; + font-weight: 600; + line-height: 18px; + text-transform: uppercase; +} diff --git a/src/components/palmistry/email-header/email-header.tsx b/src/components/palmistry/email-header/email-header.tsx new file mode 100644 index 0000000..9992b92 --- /dev/null +++ b/src/components/palmistry/email-header/email-header.tsx @@ -0,0 +1,17 @@ +import './email-header.css'; + +type Props = { + email: string; +}; + +export default function EmailHeader(props: Props) { + return ( +
+

{props.email}

+ +
+

{props.email[0]}

+
+
+ ); +} diff --git a/src/components/palmistry/header-logo/header-logo.css b/src/components/palmistry/header-logo/header-logo.css new file mode 100644 index 0000000..51f6326 --- /dev/null +++ b/src/components/palmistry/header-logo/header-logo.css @@ -0,0 +1,12 @@ +.header-logo { + display: flex; + flex-direction: row; + align-items: center; +} + +.header-logo__caption { + font-size: 24px; + font-weight: 600; + margin-left: 10px; + text-transform: uppercase; +} diff --git a/src/components/palmistry/header-logo/header-logo.png b/src/components/palmistry/header-logo/header-logo.png new file mode 100644 index 0000000..644add8 Binary files /dev/null and b/src/components/palmistry/header-logo/header-logo.png differ diff --git a/src/components/palmistry/header-logo/header-logo.tsx b/src/components/palmistry/header-logo/header-logo.tsx new file mode 100644 index 0000000..666cea7 --- /dev/null +++ b/src/components/palmistry/header-logo/header-logo.tsx @@ -0,0 +1,12 @@ +import './header-logo.css'; + +import headerLogoImage from './header-logo.png'; + +export default function HeaderLogo() { + return ( +
+ Logo + Aura +
+ ); +} diff --git a/src/components/palmistry/header/header.css b/src/components/palmistry/header/header.css new file mode 100644 index 0000000..dfa16da --- /dev/null +++ b/src/components/palmistry/header/header.css @@ -0,0 +1,22 @@ +.header { + align-items: center; + display: flex; + justify-content: center; + background: var(--pale-blue); + border-bottom: 1px solid var(--transparent-to-periwinkle); + height: 50px; + min-height: 50px; + position: relative; + width: 100%; +} + +.header__button-wrapper { + left: 28px; + position: absolute; + align-items: center; + cursor: pointer; + display: flex; + height: 30px; + justify-content: center; + width: 30px; +} diff --git a/src/components/palmistry/header/header.tsx b/src/components/palmistry/header/header.tsx new file mode 100644 index 0000000..5aad75f --- /dev/null +++ b/src/components/palmistry/header/header.tsx @@ -0,0 +1,36 @@ +import './header.css'; + +import useSteps from '../../../hooks/palmistry/use-steps'; + +import HeaderLogo from '../header-logo/header-logo'; + +export default function Home() { + const steps = useSteps(); + + return ( +
+ {!steps.isWelcome && ( +
+ + + + +
+ )} + + +
+ ); +} diff --git a/src/components/palmistry/header/logo.png b/src/components/palmistry/header/logo.png new file mode 100644 index 0000000..644add8 Binary files /dev/null and b/src/components/palmistry/header/logo.png differ diff --git a/src/components/palmistry/input-item/input-item.css b/src/components/palmistry/input-item/input-item.css new file mode 100644 index 0000000..596ab50 --- /dev/null +++ b/src/components/palmistry/input-item/input-item.css @@ -0,0 +1,6 @@ +.input-item__label { + color: var(--slate-blue-placeholder); + font-size: 12px; + line-height: 16px; + margin: 0 0 6px 6px; +} diff --git a/src/components/palmistry/input-item/input-item.tsx b/src/components/palmistry/input-item/input-item.tsx new file mode 100644 index 0000000..baa21af --- /dev/null +++ b/src/components/palmistry/input-item/input-item.tsx @@ -0,0 +1,26 @@ +import './input-item.css'; + +import Input, { type Props as InputProps } from '../input/input'; + +type Props = InputProps & { + label: string; +}; + +export default function InputItem(props: Props) { + return ( +
+

{props.label}

+ + +
+ ); +} diff --git a/src/components/palmistry/input-wrapper/input-wrapper.css b/src/components/palmistry/input-wrapper/input-wrapper.css new file mode 100644 index 0000000..5a7abef --- /dev/null +++ b/src/components/palmistry/input-wrapper/input-wrapper.css @@ -0,0 +1,11 @@ +.input-wrapper { + grid-gap: 12px; + background-color: var(--main-gradient); + display: grid; + gap: 12px; + grid-template-columns: repeat(3,1fr); + max-width: 400px; + position: relative; + width: 100%; + z-index: 3; +} diff --git a/src/components/palmistry/input-wrapper/input-wrapper.tsx b/src/components/palmistry/input-wrapper/input-wrapper.tsx new file mode 100644 index 0000000..8614992 --- /dev/null +++ b/src/components/palmistry/input-wrapper/input-wrapper.tsx @@ -0,0 +1,13 @@ +import './input-wrapper.css'; + +type Props = { + children: React.ReactNode; +}; + +export default function InputWrapper(props: Props) { + return ( +
+ {props.children} +
+ ); +} diff --git a/src/components/palmistry/input/input.css b/src/components/palmistry/input/input.css new file mode 100644 index 0000000..dc28bfd --- /dev/null +++ b/src/components/palmistry/input/input.css @@ -0,0 +1,80 @@ +.input { + margin-bottom: 0; + width: 100%; + display: block; + font-size: 16px; + height: 48px; + position: relative; +} + +.input input { + appearance: none; + background: var(--white-to-transparent); + border: 1px solid var(--light-silver-to-lilac-blue); + border-radius: 8px; + color: var(--midnight-black); + font-size: 16px; + height: 48px; + line-height: 18px; + max-width: 400px; + min-width: 250px; + outline: none; + padding: 12px 12px 5px; + transition: border-color .3s ease; + width: 100%; + background: var(--pale-blue-input); + border: 2px solid var(--pale-lavender); + padding-top: 5px; + color: var(--midnight-black-input); + min-width: 96px; +} + +.input input:focus { + border-color: var(--strong-blue); + transition-delay: .1s; +} + +.input input:focus + .input__placeholder { + display: none; +} + +.input_floating-placeholder input:focus + .input__placeholder { + display: block; + font-size: 12px; + top: 12px; + width: auto; +} + +.input.input_filled > .input__placeholder { + display: block; +} + +.input_filled .input__placeholder { + display: none; +} + +.input__placeholder { + color: var(--slate-blue-placeholder); + position: absolute; + top: 50%; + transform: translateY(-50%); + font-size: 18px; + left: 12px; + overflow: hidden; + text-overflow: ellipsis; + transition: top .3s ease,color .3s ease,font-size .3s ease; + white-space: nowrap; +} + +.input__icon-wrapper { + align-items: center; + background: 0 0; + display: flex; + height: 30px; + justify-content: center; + right: 2px; + width: 30px; + position: absolute; + top: 50%; + transform: translateY(-50%); +} diff --git a/src/components/palmistry/input/input.tsx b/src/components/palmistry/input/input.tsx new file mode 100644 index 0000000..8fde258 --- /dev/null +++ b/src/components/palmistry/input/input.tsx @@ -0,0 +1,73 @@ +import React from 'react'; + +import './input.css'; + +export type Props = { + placeholder: string; + maxLength?: number; + type: 'text' | 'password'; + value: string; + className?: string; + inputMode?: 'numeric'; + floatingPlaceholder?: boolean; + onChange: (value: string) => void; + onFocus?: (event: React.FocusEvent) => void; + onBlur?: (value: React.FocusEvent) => void; +}; + +export default function Input(props: Props) { + const [isFocused, setIsFocused] = React.useState(false); + + const onChange = (event: React.ChangeEvent) => { + props.onChange(event.target.value); + }; + + const onBlur = (event: React.FocusEvent) => { + setIsFocused(false); + + if (props.onBlur) { + props.onBlur(event); + } + } + + const onFocus = (event: React.FocusEvent) => { + setIsFocused(true); + + if (props.onFocus) { + props.onFocus(event); + } + }; + + const className = ['input']; + + if (props.value) { + className.push('input_filled'); + } + + if (props.className) { + className.push(props.className); + } + + if (props.floatingPlaceholder) { + className.push('input_floating-placeholder'); + } + + return ( +