├── src ├── react-app-env.d.ts ├── assets │ ├── home.png │ ├── result.png │ └── ErrorImg.tsx ├── setupTests.ts ├── App.tsx ├── components │ ├── ui │ │ ├── Tick.tsx │ │ ├── DownNav.tsx │ │ ├── UpNav.tsx │ │ ├── Arrow.tsx │ │ ├── UploadArrow.tsx │ │ └── Upload.tsx │ ├── Results.tsx │ ├── ProgressBar.tsx │ ├── Home.tsx │ ├── Navigation.tsx │ ├── ResumeQuestion.tsx │ ├── InputQuestion.tsx │ ├── TextAreaQuestion.tsx │ └── MyForm.tsx ├── reportWebVitals.ts ├── data │ ├── variants.ts │ └── schema.ts ├── index.tsx ├── hooks │ └── useCheckPressEnter.tsx ├── context │ └── FormContextProvider.tsx ├── logo.svg └── index.css ├── public ├── favicon.ico ├── logo192.png ├── logo512.png ├── robots.txt ├── manifest.json └── index.html ├── tests └── App.test.tsx ├── .gitignore ├── tsconfig.json ├── package.json └── README.md /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tahseenio/typeform-clone/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tahseenio/typeform-clone/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tahseenio/typeform-clone/HEAD/public/logo512.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/assets/home.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tahseenio/typeform-clone/HEAD/src/assets/home.png -------------------------------------------------------------------------------- /src/assets/result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tahseenio/typeform-clone/HEAD/src/assets/result.png -------------------------------------------------------------------------------- /src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import MyForm from './components/MyForm'; 2 | import FormContextProvider from './context/FormContextProvider'; 3 | 4 | function App() { 5 | return ( 6 | 7 | 8 | 9 | ); 10 | } 11 | 12 | export default App; 13 | -------------------------------------------------------------------------------- /src/components/ui/Tick.tsx: -------------------------------------------------------------------------------- 1 | const Tick = () => { 2 | return ( 3 | 4 | 8 | 9 | ); 10 | }; 11 | 12 | export default Tick; 13 | -------------------------------------------------------------------------------- /tests/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render, screen } from '@testing-library/react'; 3 | import App from '../src/App'; 4 | 5 | test('renders learn react link', () => { 6 | render(); 7 | const linkElement = screen.getByText(/learn react/i); 8 | expect(linkElement).toBeInTheDocument(); 9 | }); 10 | -------------------------------------------------------------------------------- /src/components/ui/DownNav.tsx: -------------------------------------------------------------------------------- 1 | const DownNav = ({ fill }: { fill: string }) => { 2 | return ( 3 | 4 | 8 | 9 | ); 10 | }; 11 | 12 | export default DownNav; 13 | -------------------------------------------------------------------------------- /src/components/ui/UpNav.tsx: -------------------------------------------------------------------------------- 1 | const UpNav = ({ fill }: { fill: string }) => { 2 | return ( 3 | 4 | 8 | 9 | ); 10 | }; 11 | 12 | export default UpNav; 13 | -------------------------------------------------------------------------------- /src/components/ui/Arrow.tsx: -------------------------------------------------------------------------------- 1 | const Arrow = () => { 2 | return ( 3 | 4 | 8 | 9 | 10 | ); 11 | }; 12 | 13 | export default Arrow; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/components/ui/UploadArrow.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const UploadArrow = () => { 4 | return ( 5 | 11 | ); 12 | }; 13 | 14 | export default UploadArrow; 15 | -------------------------------------------------------------------------------- /src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /src/components/Results.tsx: -------------------------------------------------------------------------------- 1 | import { useFormContext } from '../context/FormContextProvider'; 2 | import resultImg from '../assets/result.png'; 3 | 4 | const Results = ({ resultPara }: { resultPara: string }) => { 5 | const { formData } = useFormContext(); 6 | console.log(formData); 7 | 8 | return ( 9 | <> 10 | handstanding people final page 15 |

{resultPara}

16 | 17 | ); 18 | }; 19 | 20 | export default Results; 21 | -------------------------------------------------------------------------------- /src/data/variants.ts: -------------------------------------------------------------------------------- 1 | export const Qvariants = { 2 | initial: { opacity: 0, y: '600px' }, 3 | animate: { opacity: 1, y: 0, transition: { type: 'ease' } }, 4 | exit: { opacity: 0, y: '-600px', transition: { type: 'ease' } }, 5 | }; 6 | 7 | export const reverseVariants = { 8 | initial: { opacity: 0, y: '-600px' }, 9 | animate: { opacity: 1, y: 0, transition: { type: 'ease' } }, 10 | exit: { opacity: 0, y: '600px', transition: { type: 'ease' } }, 11 | }; 12 | 13 | // if current tab number is greater than next value then -100vh 14 | // if current tab number is lower than the next value then 100vh 15 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "jsx": "react-jsx" 18 | }, 19 | "include": ["src", "tests/App.test.tsx"] 20 | } 21 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | const root = ReactDOM.createRoot( 8 | document.getElementById('root') as HTMLElement 9 | ); 10 | root.render( 11 | // 12 | 13 | // 14 | ); 15 | 16 | // If you want to start measuring performance in your app, pass a function 17 | // to log results (for example: reportWebVitals(console.log)) 18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 19 | reportWebVitals(); 20 | -------------------------------------------------------------------------------- /src/components/ProgressBar.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from 'framer-motion'; 2 | import { useFormContext } from '../context/FormContextProvider'; 3 | 4 | const ProgressBar = () => { 5 | const { tab } = useFormContext(); 6 | 7 | return ( 8 | <> 9 | {tab !== 0 && tab !== 10 ? ( 10 |
11 | 16 |
17 | ) : null} 18 | 19 | ); 20 | }; 21 | 22 | export default ProgressBar; 23 | -------------------------------------------------------------------------------- /src/hooks/useCheckPressEnter.tsx: -------------------------------------------------------------------------------- 1 | // TODO MAKE THIS WORK 2 | 3 | import { RefObject, useEffect } from 'react'; 4 | 5 | interface Props { 6 | reference: RefObject; 7 | } 8 | 9 | const useCheckPressEnter = ({ reference }: Props) => { 10 | useEffect(() => { 11 | const keyHandler = (e: KeyboardEvent) => { 12 | if (e.key === 'Enter') { 13 | reference.current!.click(); 14 | } 15 | }; 16 | document.addEventListener('keypress', (e) => keyHandler(e)); 17 | 18 | return () => document.removeEventListener('keypress', (e) => keyHandler(e)); 19 | }); 20 | }; 21 | 22 | export default useCheckPressEnter; 23 | -------------------------------------------------------------------------------- /src/components/ui/Upload.tsx: -------------------------------------------------------------------------------- 1 | const Upload = () => { 2 | return ( 3 | 9 | ); 10 | }; 11 | 12 | export default Upload; 13 | -------------------------------------------------------------------------------- /src/assets/ErrorImg.tsx: -------------------------------------------------------------------------------- 1 | const ErrorImg = () => { 2 | return ( 3 | 4 | 9 | 10 | ); 11 | }; 12 | 13 | export default ErrorImg; 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "typeform-clone", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@hookform/resolvers": "^2.9.5", 7 | "@testing-library/jest-dom": "^5.16.4", 8 | "@testing-library/react": "^13.3.0", 9 | "@testing-library/user-event": "^13.5.0", 10 | "@types/jest": "^27.5.2", 11 | "@types/node": "^16.11.43", 12 | "@types/react": "^18.0.15", 13 | "@types/react-dom": "^18.0.6", 14 | "framer-motion": "^6.4.3", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "react-hook-form": "^7.33.1", 18 | "react-scripts": "5.0.1", 19 | "react-textarea-autosize": "^8.3.4", 20 | "typescript": "^4.7.4", 21 | "web-vitals": "^2.1.4", 22 | "yup": "^0.32.11" 23 | }, 24 | "scripts": { 25 | "start": "react-scripts start", 26 | "build": "react-scripts build", 27 | "test": "react-scripts test", 28 | "eject": "react-scripts eject" 29 | }, 30 | "eslintConfig": { 31 | "extends": [ 32 | "react-app", 33 | "react-app/jest" 34 | ] 35 | }, 36 | "browserslist": { 37 | "production": [ 38 | ">0.2%", 39 | "not dead", 40 | "not op_mini all" 41 | ], 42 | "development": [ 43 | "last 1 chrome version", 44 | "last 1 firefox version", 45 | "last 1 safari version" 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Typeform Clone Visit Here 2 | 3 | 4 | 5 | ### Description 6 | A modern aesthetic multi-step form which makes filling forms fun for the user. Utilises react-hook-form, yup for input validation, Context API and uses Framer Motion to create user friendly animations. 7 | 8 | ## Screenshots 9 | 10 | ![image](https://user-images.githubusercontent.com/55749172/195984783-a24597ec-92aa-4e76-bae5-a89204011baf.png) 11 | 12 | 13 | ## Tech and Packages Used 14 |

15 | 16 | 17 | 18 | 19 |

20 | 21 | - react-hook-form and yup for input validation and handling of all forms. 22 | 23 | ## Lessons Learned 24 | - In this project I learned how to create a multi-step form. 25 | - I learned how to incorporate keyboards shortcuts that allow the user to quickly and efficiently progress through the form. 26 | 27 | 28 | ## Future optimizations 29 | N/A 30 | -------------------------------------------------------------------------------- /src/components/Home.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from 'framer-motion'; 2 | import { useCallback, useEffect, useRef } from 'react'; 3 | import homeImg from '../assets/home.png'; 4 | import { useFormContext } from '../context/FormContextProvider'; 5 | import { Qvariants } from '../data/variants'; 6 | 7 | const Home = () => { 8 | const { setTab } = useFormContext(); 9 | 10 | const buttonRef = useRef(null); 11 | 12 | const keyHandler = useCallback((e: KeyboardEvent) => { 13 | if (e.key === 'Enter') { 14 | buttonRef.current?.click(); 15 | } 16 | }, []); 17 | 18 | useEffect(() => { 19 | document.addEventListener('keypress', (e) => keyHandler(e)); 20 | 21 | return () => document.removeEventListener('keypress', (e) => keyHandler(e)); 22 | }, [keyHandler]); 23 | 24 | return ( 25 | 26 |
27 | home page 28 |
29 |

Front-end Developer at Somewhere

30 |
31 | 38 |

39 | press Enter ↵ 40 |

41 |
42 |
43 | ); 44 | }; 45 | 46 | export default Home; 47 | -------------------------------------------------------------------------------- /src/components/Navigation.tsx: -------------------------------------------------------------------------------- 1 | import UpNav from './ui/UpNav'; 2 | import DownNav from './ui/DownNav'; 3 | import { useRef } from 'react'; 4 | import { useFormContext } from '../context/FormContextProvider'; 5 | 6 | const Navigation = () => { 7 | const leftButton = useRef(null); 8 | const rightButton = useRef(null); 9 | 10 | const { setIsReversed, tab, setTab } = useFormContext(); 11 | 12 | const handleClickBack = () => { 13 | setIsReversed(true); 14 | setTab((state) => state - 1); 15 | }; 16 | 17 | const handleClickForward = () => { 18 | // check for errors on current page and if they exist just return 19 | // console.log(globalErrors); 20 | // if (globalErrors !== {}) return; 21 | setIsReversed(false); 22 | setTab((state) => state + 1); 23 | }; 24 | 25 | return ( 26 | <> 27 | {tab !== 0 && tab !== 10 ? ( 28 |
29 | 37 | 45 |
46 | ) : null} 47 | 48 | ); 49 | }; 50 | 51 | export default Navigation; 52 | -------------------------------------------------------------------------------- /src/context/FormContextProvider.tsx: -------------------------------------------------------------------------------- 1 | import { createContext, useContext, useEffect, useState } from 'react'; 2 | export interface ContextProps { 3 | tab: number; 4 | setTab: React.Dispatch>; 5 | isReversed: boolean; 6 | setIsReversed: React.Dispatch>; 7 | formData: Record[]; 8 | setFormData: React.Dispatch[]>>; 9 | handleClickForward: () => void; 10 | globalErrors: any; 11 | setGlobalErrors: any; 12 | } 13 | 14 | export interface ProviderProps { 15 | children: JSX.Element; 16 | } 17 | 18 | const FormContext = createContext({}); 19 | 20 | const FormContextProvider = ({ children }: ProviderProps) => { 21 | const [globalErrors, setGlobalErrors] = useState({}); 22 | const [formData, setFormData] = useState<[]>([]); 23 | 24 | useEffect(() => { 25 | // console.log('FORM DATA IS: ', formData); 26 | // 27 | }, [formData]); 28 | 29 | const [tab, setTab] = useState(0); 30 | 31 | const [isReversed, setIsReversed] = useState(false); 32 | 33 | const handleClickForward = () => { 34 | setIsReversed(false); 35 | setTab((state) => state + 1); 36 | }; 37 | 38 | return ( 39 | 52 | {children} 53 | 54 | ); 55 | }; 56 | 57 | export default FormContextProvider; 58 | 59 | export const useFormContext = () => useContext(FormContext) as ContextProps; 60 | -------------------------------------------------------------------------------- /src/data/schema.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | 3 | export const Q1schema = yup.object().shape({ 4 | Q1: yup.string().required('A name is required'), 5 | }); 6 | 7 | const Q2schema = yup.object().shape({ 8 | Q2: yup 9 | .string() 10 | .email('Please enter a valid email') 11 | .required('An email is required'), 12 | }); 13 | 14 | const Q3schema = yup.object().shape({ 15 | Q3: yup 16 | .number() 17 | .typeError('Please enter numbers only') 18 | .min(1, 'Please enter a valid number') 19 | .required('A phone number is required'), 20 | }); 21 | 22 | const Q4schema = yup.object().shape({ 23 | Q4: yup.string(), 24 | }); 25 | 26 | const Q5schema = yup.object().shape({ 27 | Q5: yup.string().required('A response is required'), 28 | }); 29 | 30 | const Q6schema = yup.object().shape({ 31 | Q6: yup.string(), 32 | }); 33 | 34 | const Q7schema = yup.object().shape({ 35 | Q7: yup.string(), 36 | }); 37 | 38 | const Q8schema = yup.object().shape({ 39 | Q9: yup.string(), 40 | }); 41 | const Q9schema = yup.object().shape({ 42 | Q9: yup.string(), 43 | }); 44 | 45 | export const schemaSelector = (questionNumber: number): any => { 46 | if (questionNumber === 1) { 47 | return Q1schema; 48 | } else if (questionNumber === 2) { 49 | return Q2schema; 50 | } else if (questionNumber === 3) { 51 | return Q3schema; 52 | } else if (questionNumber === 4) { 53 | return Q4schema; 54 | } else if (questionNumber === 5) { 55 | return Q5schema; 56 | } else if (questionNumber === 6) { 57 | return Q6schema; 58 | } else if (questionNumber === 7) { 59 | return Q7schema; 60 | } else if (questionNumber === 8) { 61 | return Q8schema; 62 | } else if (questionNumber === 9) { 63 | return Q9schema; 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ResumeQuestion.tsx: -------------------------------------------------------------------------------- 1 | import React, { useCallback, useEffect, useRef } from 'react'; 2 | import { useFormContext } from '../context/FormContextProvider'; 3 | import Arrow from './ui/Arrow'; 4 | import Tick from './ui/Tick'; 5 | import Upload from './ui/Upload'; 6 | import UploadArrow from './ui/UploadArrow'; 7 | import { motion } from 'framer-motion'; 8 | import { Qvariants, reverseVariants } from '../data/variants'; 9 | import { useForm } from 'react-hook-form'; 10 | 11 | interface Props { 12 | number: number; 13 | } 14 | 15 | const ResumeQuestion = ({ number }: Props) => { 16 | const buttonRef = useRef(null); 17 | const { formData, setFormData, isReversed, handleClickForward } = 18 | useFormContext(); 19 | 20 | const keyHandler = useCallback((e: KeyboardEvent) => { 21 | if (e.key === 'Enter') { 22 | buttonRef.current?.click(); 23 | } 24 | }, []); 25 | 26 | useEffect(() => { 27 | document.addEventListener('keypress', (e) => keyHandler(e)); 28 | 29 | return () => document.removeEventListener('keypress', (e) => keyHandler(e)); 30 | }, [keyHandler]); 31 | 32 | const { register, handleSubmit } = useForm>(); 33 | const onSubmit = (data: Record) => { 34 | // console.log('completed resume form'); 35 | if (!!formData[number - 1]) { 36 | // console.log('updating resume'); 37 | const newArr = formData.map((item) => { 38 | if (item[`Q${number}`]) { 39 | return { 40 | ...data, 41 | }; 42 | } else return item; 43 | }); 44 | setFormData(newArr); 45 | } else { 46 | console.log('new resume'); 47 | setFormData([...formData, { ...data }]); 48 | } 49 | handleClickForward(); 50 | }; 51 | 52 | return ( 53 | 60 |

61 | Do you have a Resume / CV you'd like to share? If so, please upload 62 |
63 | {number} 64 |
65 |

66 |
67 |
68 | 69 |
70 |
71 |
72 | 73 |
74 | 75 |
76 |

77 | Choose file or{' '} 78 | drag here 79 |

80 |

Size limit: 10MB

81 |
82 |
83 |
84 | 87 |
88 |
89 |
90 | ); 91 | }; 92 | 93 | export default ResumeQuestion; 94 | -------------------------------------------------------------------------------- /src/components/InputQuestion.tsx: -------------------------------------------------------------------------------- 1 | import { motion } from 'framer-motion'; 2 | import { useCallback, useEffect, useRef } from 'react'; 3 | import { useForm } from 'react-hook-form'; 4 | import { useFormContext } from '../context/FormContextProvider'; 5 | import { Qvariants, reverseVariants } from '../data/variants'; 6 | import { yupResolver } from '@hookform/resolvers/yup'; 7 | import { schemaSelector } from '../data/schema'; 8 | 9 | import Arrow from './ui/Arrow'; 10 | import Tick from './ui/Tick'; 11 | import ErrorImg from '../assets/ErrorImg'; 12 | 13 | interface Props { 14 | number: number; 15 | question: string; 16 | } 17 | 18 | const InputQuestion = ({ number, question }: Props) => { 19 | const buttonRef = useRef(null); 20 | 21 | const { 22 | formData, 23 | setFormData, 24 | isReversed, 25 | tab, 26 | handleClickForward, 27 | setGlobalErrors, 28 | globalErrors, 29 | } = useFormContext(); 30 | 31 | const keyHandler = useCallback((e: KeyboardEvent) => { 32 | if (e.key === 'Enter') { 33 | buttonRef.current?.click(); 34 | } 35 | }, []); 36 | 37 | useEffect(() => { 38 | document.addEventListener('keypress', (e) => keyHandler(e)); 39 | 40 | return () => document.removeEventListener('keypress', (e) => keyHandler(e)); 41 | }, [keyHandler]); 42 | 43 | const { 44 | register, 45 | handleSubmit, 46 | setFocus, 47 | formState: { errors }, 48 | } = useForm>({ 49 | mode: 'onBlur', 50 | resolver: yupResolver(schemaSelector(tab)), 51 | }); 52 | 53 | // useEffect(() => { 54 | // console.log('ERRORS from react-form', errors); 55 | // setGlobalErrors(errors); 56 | // console.log('ERRORS from globalErrors', globalErrors); 57 | // }, [errors]); 58 | 59 | useEffect(() => { 60 | setFocus(`Q${number}`); 61 | }, [number, setFocus]); 62 | 63 | const onSubmit = (data: Record) => { 64 | if (!!formData[number - 1]) { 65 | const newArr = formData.map((item) => { 66 | if (item[`Q${number}`]) { 67 | return { 68 | ...data, 69 | }; 70 | } else return item; 71 | }); 72 | setFormData(newArr); 73 | } else { 74 | setFormData([...formData, { ...data }]); 75 | } 76 | handleClickForward(); 77 | }; 78 | 79 | return ( 80 | 87 |

88 | {question} 89 |
90 | {number} 91 |
92 |

93 |
94 | 100 | {errors[`Q${number}`] && ( 101 |

102 | 103 | {errors[`Q${number}`]?.message} 104 |

105 | )} 106 |
107 | 110 |

111 | press Enter ↵ 112 |

113 |
114 |
115 |
116 | ); 117 | }; 118 | 119 | export default InputQuestion; 120 | -------------------------------------------------------------------------------- /src/components/TextAreaQuestion.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useRef } from 'react'; 2 | import ReactTextareaAutosize from 'react-textarea-autosize'; 3 | import { useFormContext } from '../context/FormContextProvider'; 4 | import Arrow from './ui/Arrow'; 5 | import Tick from './ui/Tick'; 6 | import { motion } from 'framer-motion'; 7 | import { Qvariants, reverseVariants } from '../data/variants'; 8 | import { useForm } from 'react-hook-form'; 9 | import { yupResolver } from '@hookform/resolvers/yup'; 10 | import { schemaSelector } from '../data/schema'; 11 | import ErrorImg from '../assets/ErrorImg'; 12 | 13 | interface Props { 14 | number: number; 15 | question: string; 16 | buttonText?: string; 17 | helperText?: string; 18 | } 19 | 20 | const TextAreaQuestion = ({ 21 | number, 22 | question, 23 | buttonText = 'OK', 24 | helperText = 'Enter ↵', 25 | }: Props) => { 26 | const buttonRef = useRef(null); 27 | 28 | const { formData, setFormData, tab, isReversed, handleClickForward } = 29 | useFormContext(); 30 | 31 | const keyHandler = useCallback( 32 | (e: KeyboardEvent) => { 33 | if (e.key === 'Enter' && e.shiftKey === false) { 34 | e.preventDefault(); 35 | } 36 | if (e.shiftKey === true) return; 37 | if (tab === 9) { 38 | if (e.ctrlKey === true) { 39 | buttonRef.current?.click(); 40 | } 41 | } 42 | if (e.key === 'Enter' && tab !== 9) { 43 | buttonRef.current?.click(); 44 | } 45 | }, 46 | [tab] 47 | ); 48 | 49 | useEffect(() => { 50 | document.addEventListener('keypress', (e) => keyHandler(e)); 51 | 52 | return () => document.removeEventListener('keypress', (e) => keyHandler(e)); 53 | }, [keyHandler]); 54 | 55 | const { 56 | register, 57 | handleSubmit, 58 | setFocus, 59 | formState: { errors }, 60 | } = useForm>({ 61 | mode: 'onBlur', 62 | resolver: yupResolver(schemaSelector(tab)), 63 | }); 64 | 65 | useEffect(() => { 66 | setFocus(`Q${number}`); 67 | }, [number, setFocus]); 68 | 69 | const onSubmit = (data: Record) => { 70 | if (!!formData[number - 1]) { 71 | const newArr = formData.map((item) => { 72 | if (item[`Q${number}`]) { 73 | return { 74 | ...data, 75 | }; 76 | } else return item; 77 | }); 78 | setFormData(newArr); 79 | } else { 80 | setFormData([...formData, { ...data }]); 81 | } 82 | handleClickForward(); 83 | }; 84 | 85 | return ( 86 | 93 |

94 | {question} 95 |
96 | {number} 97 |
98 |

99 |
100 | 106 |

107 | Shift + Enter ↵ to make a line break 108 |

109 | {errors[`Q${number}`] && ( 110 |

111 | 112 | {errors[`Q${number}`]?.message} 113 |

114 | )} 115 |
116 | 119 |

120 | press {helperText} 121 |

122 |
123 | 124 |
125 | ); 126 | }; 127 | 128 | export default TextAreaQuestion; 129 | -------------------------------------------------------------------------------- /src/components/MyForm.tsx: -------------------------------------------------------------------------------- 1 | // I realised how complex it can get as the project gets bigger and bigger. Something you may not have noticed beforehand, that is now causing an issue, has caused me to redesign my whole logic e.g my array update logic for certain questions as some users may skip a few questions and complete the later ones. 2 | 3 | // TODO 4 | // upload state for resume 5 | // dont allow submission until all values have been filled 6 | // -- add submit validation, if required parts are not filled -> throw error 7 | 8 | // disallow moving to next screen if question is required 9 | // BUG If form is not done in order then there will be issues tracking question answers and its default values. 10 | // //Possible fix: ADD A GLOBAL ERRORS AND IF THE ERROR EXISTS THEN STOP OTHERWISE CONTINUE BUT ITS NOT WORKING 11 | // // I want it so that if the next button is clicked just click the actual button to start the process again 12 | 13 | import Navigation from './Navigation'; 14 | import { useFormContext } from '../context/FormContextProvider'; 15 | import { AnimatePresence } from 'framer-motion'; 16 | import Home from './Home'; 17 | import InputQuestion from './InputQuestion'; 18 | import TextAreaQuestion from './TextAreaQuestion'; 19 | import ResumeQuestion from './ResumeQuestion'; 20 | import Results from './Results'; 21 | import ProgressBar from './ProgressBar'; 22 | 23 | const MyForm = () => { 24 | const { tab, formData } = useFormContext(); 25 | const pages = [ 26 | { number: 0, page: }, 27 | { 28 | number: 1, 29 | page: ( 30 | 35 | ), 36 | }, 37 | { 38 | number: 2, 39 | page: ( 40 | 47 | ), 48 | }, 49 | { 50 | number: 3, 51 | page: ( 52 | 57 | ), 58 | }, 59 | { 60 | number: 4, 61 | page: ( 62 | 68 | ), 69 | }, 70 | { 71 | number: 5, 72 | page: ( 73 | 78 | ), 79 | }, 80 | { 81 | number: 6, 82 | page: ( 83 | 89 | ), 90 | }, 91 | { 92 | number: 7, 93 | page: , 94 | }, 95 | { 96 | number: 8, 97 | page: ( 98 | 104 | ), 105 | }, 106 | { 107 | number: 9, 108 | page: ( 109 | 116 | ), 117 | }, 118 | { 119 | number: 10, 120 | page: ( 121 | 127 | ), 128 | }, 129 | ]; 130 | 131 | return ( 132 |
133 | 134 | 135 | 136 | {pages.map((item) => { 137 | if (tab === Number(item.number)) { 138 | return item.page; 139 | } else return null; 140 | })} 141 | 142 |
143 | ); 144 | }; 145 | 146 | export default MyForm; 147 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap'); 2 | 3 | * { 4 | margin: 0; 5 | padding: 0; 6 | box-sizing: border-box; 7 | font-family: 'Open Sans', sans-serif; 8 | } 9 | 10 | html { 11 | overflow: hidden; 12 | } 13 | 14 | input { 15 | outline: none; 16 | } 17 | 18 | :root { 19 | --blue: #0445af; 20 | --light-blue: rgba(4, 69, 175, 0.3); 21 | --lighter-blue: #e5ecf7; 22 | } 23 | 24 | .blue { 25 | color: var(--blue); 26 | } 27 | 28 | .button__helper { 29 | font-size: 0.8rem; 30 | } 31 | 32 | .title { 33 | font-weight: 400; 34 | font-size: 1.5rem; 35 | position: relative; 36 | text-align: start; 37 | } 38 | 39 | .button { 40 | font-size: 1.4rem; 41 | font-weight: 700; 42 | color: white; 43 | background-color: var(--blue); 44 | border: none; 45 | border-radius: 4px; 46 | padding: 8px 16px; 47 | transition: filter 300ms ease; 48 | } 49 | 50 | .button:hover { 51 | cursor: pointer; 52 | filter: contrast(0.8); 53 | } 54 | 55 | .button:active { 56 | filter: brightness(0.8); 57 | } 58 | 59 | .textarea { 60 | outline: none; 61 | border: none; 62 | border-bottom: 2px solid var(--light-blue); 63 | color: var(--blue); 64 | width: 100%; 65 | resize: none; 66 | overflow: hidden; 67 | font-size: 32px; 68 | line-height: 1.5; 69 | height: 40px !important; 70 | margin: 32px 0 12px 0; 71 | } 72 | 73 | .textarea:focus { 74 | color: var(--blue); 75 | } 76 | 77 | .input { 78 | font-size: 2rem; 79 | line-height: 3.5rem; 80 | border: none; 81 | border-bottom: 2px solid var(--light-blue); 82 | color: var(--blue); 83 | width: 100%; 84 | margin: 32px 0 12px 0; 85 | } 86 | 87 | .input:focus, 88 | .textarea:focus { 89 | border-bottom: 2px solid var(--blue); 90 | } 91 | 92 | .input::placeholder, 93 | .textarea::placeholder { 94 | color: var(--light-blue); 95 | } 96 | 97 | .input--file { 98 | position: relative; 99 | cursor: pointer; 100 | background-color: var(--lighter-blue); 101 | border: 1px dashed var(--light-blue); 102 | border-radius: 4px; 103 | width: 100%; 104 | height: 300px; 105 | margin: 32px 0 12px 0; 106 | transition: background-color 200ms ease; 107 | } 108 | 109 | .file__info { 110 | display: flex; 111 | flex-direction: column; 112 | align-items: center; 113 | gap: 8px; 114 | position: absolute; 115 | top: 50%; 116 | left: 50%; 117 | transform: translate(-50%, -50%); 118 | } 119 | 120 | .file { 121 | cursor: pointer; 122 | width: 100%; 123 | height: 100%; 124 | opacity: 0; 125 | } 126 | 127 | .file__info--para1 { 128 | font-size: 0.9rem; 129 | } 130 | .file__info--para2 { 131 | font-size: 0.8rem; 132 | } 133 | 134 | .upload_image--wrapper { 135 | position: relative; 136 | } 137 | 138 | .upload-arrow { 139 | position: absolute; 140 | top: 50%; 141 | left: 50%; 142 | transform: translate(-50%, -50%); 143 | } 144 | 145 | .input--file:hover { 146 | background-color: var(--light-blue); 147 | } 148 | 149 | .question__number { 150 | color: var(--blue); 151 | font-size: 1rem; 152 | position: absolute; 153 | top: 6px; 154 | left: -36px; 155 | } 156 | 157 | .container { 158 | position: relative; 159 | min-height: 100vh; 160 | display: flex; 161 | flex-direction: column; 162 | align-items: center; 163 | justify-content: center; 164 | padding: 0px 32px; 165 | } 166 | 167 | .error-para { 168 | /* display: inline-block; */ 169 | display: inline-flex; 170 | background-color: #f7e6e6; 171 | border-radius: 4px; 172 | padding: 5px; 173 | color: #af0404; 174 | margin-bottom: 12px; 175 | } 176 | 177 | /* 178 | 179 | Home 180 | 181 | */ 182 | 183 | .home { 184 | display: flex; 185 | flex-direction: column; 186 | align-items: center; 187 | gap: 32px; 188 | } 189 | 190 | .home__img--wrapper { 191 | max-width: 550px; 192 | max-height: 550px; 193 | user-select: none; 194 | } 195 | 196 | .home__img { 197 | width: 100%; 198 | height: 100%; 199 | } 200 | 201 | .button--wrapper { 202 | display: flex; 203 | align-items: center; 204 | gap: 8px; 205 | } 206 | 207 | /* 208 | 209 | Question 210 | 211 | */ 212 | 213 | .question { 214 | max-width: 750px; 215 | width: 100%; 216 | padding: 0 20px; 217 | } 218 | 219 | .textarea--helper { 220 | text-align: start; 221 | font-size: 0.75rem; 222 | color: var(--blue); 223 | margin-bottom: 12px; 224 | } 225 | 226 | /* 227 | 228 | Navigation 229 | 230 | */ 231 | 232 | .navigation { 233 | width: 30px; 234 | position: fixed; 235 | display: flex; 236 | bottom: 16px; 237 | right: 60px; 238 | } 239 | 240 | .nav__button { 241 | cursor: pointer; 242 | border: none; 243 | padding: 6px 10px; 244 | background-color: var(--blue); 245 | color: white; 246 | transition: background-color 200ms ease; 247 | } 248 | 249 | .nav__button:disabled { 250 | cursor: default; 251 | background-color: gray; 252 | } 253 | 254 | .nav__button--left { 255 | border-top-left-radius: 4px; 256 | border-bottom-left-radius: 4px; 257 | border-right: 1px solid rgba(255, 255, 255, 0.3); 258 | } 259 | 260 | .nav__button--right { 261 | border-top-right-radius: 4px; 262 | border-bottom-right-radius: 4px; 263 | } 264 | 265 | /* 266 | 267 | Progress Bar 268 | 269 | */ 270 | 271 | .progress-bar { 272 | position: absolute; 273 | top: 0; 274 | background-color: var(--light-blue); 275 | width: 100%; 276 | height: 4px; 277 | } 278 | 279 | .progress-bar--actual-progress { 280 | height: 100%; 281 | background-color: var(--blue); 282 | } 283 | 284 | /* 285 | 286 | results 287 | 288 | */ 289 | 290 | .result__img { 291 | max-width: 600px; 292 | height: 100%; 293 | width: 100%; 294 | } 295 | 296 | .result__para { 297 | text-align: center; 298 | } 299 | --------------------------------------------------------------------------------