├── .all-contributorsrc ├── .eslintrc ├── .gitignore ├── .husky ├── commit-msg ├── post-merge └── pre-commit ├── .prettierignore ├── .prettierrc.js ├── .vscode └── settings.json ├── CHANGELOG.md ├── README.md ├── commitlint.config.js ├── components ├── Button.tsx ├── CustomLink.tsx ├── CustomMap │ ├── LoadingMap.tsx │ ├── LocationMarker.tsx │ ├── ReadOnlyMap.tsx │ ├── index.tsx │ └── types.ts ├── Forms │ ├── DatePicker.tsx │ ├── DropzoneInput.tsx │ ├── FilePreview.tsx │ ├── Input.tsx │ ├── PasswordInput.tsx │ └── Select.tsx ├── Nav.tsx ├── Seo.tsx └── UnstyledLink.tsx ├── lib ├── helper.ts └── yup.ts ├── next-env.d.ts ├── package.json ├── pages ├── _app.tsx ├── _document.tsx ├── form │ ├── recap.tsx │ ├── step-1.tsx │ ├── step-2.tsx │ └── step-3.tsx ├── index.tsx ├── map.tsx └── recap-json.tsx ├── postcss.config.js ├── public ├── favicon │ ├── android-icon-144x144.png │ ├── android-icon-192x192.png │ ├── android-icon-36x36.png │ ├── android-icon-48x48.png │ ├── android-icon-72x72.png │ ├── android-icon-96x96.png │ ├── apple-icon-114x114.png │ ├── apple-icon-120x120.png │ ├── apple-icon-144x144.png │ ├── apple-icon-152x152.png │ ├── apple-icon-180x180.png │ ├── apple-icon-57x57.png │ ├── apple-icon-60x60.png │ ├── apple-icon-72x72.png │ ├── apple-icon-76x76.png │ ├── apple-icon-precomposed.png │ ├── apple-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── large-og.jpg │ ├── manifest.json │ ├── ms-icon-144x144.png │ ├── ms-icon-150x150.png │ ├── ms-icon-310x310.png │ └── ms-icon-70x70.png ├── fonts │ └── inter-var-latin.woff2 └── images │ └── leaflet │ ├── layers-2x.png │ ├── layers.png │ ├── marker-icon-2x.png │ ├── marker-icon.png │ └── marker-shadow.png ├── store └── useFormStore.tsx ├── styles └── globals.css ├── tailwind.config.js ├── tsconfig.json ├── types.ts ├── vercel.json └── yarn.lock /.all-contributorsrc: -------------------------------------------------------------------------------- 1 | { 2 | "files": [ 3 | "README.md" 4 | ], 5 | "imageSize": 100, 6 | "commit": false, 7 | "contributors": [ 8 | { 9 | "login": "theodorusclarence", 10 | "name": "Theodorus Clarence", 11 | "avatar_url": "https://avatars.githubusercontent.com/u/55318172?v=4", 12 | "profile": "https://theodorusclarence.com", 13 | "contributions": [ 14 | "code" 15 | ] 16 | }, 17 | { 18 | "login": "rizqitsani", 19 | "name": "Muhammad Rizqi Tsani", 20 | "avatar_url": "https://avatars.githubusercontent.com/u/68275535?v=4", 21 | "profile": "https://github.com/rizqitsani", 22 | "contributions": [ 23 | "code" 24 | ] 25 | } 26 | ], 27 | "contributorsPerLine": 7, 28 | "projectName": "rhf-stepform", 29 | "projectOwner": "theodorusclarence", 30 | "repoType": "github", 31 | "repoHost": "https://github.com", 32 | "skipCi": true 33 | } 34 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "node": true 6 | }, 7 | "extends": ["eslint:recommended", "next"], 8 | "rules": { 9 | "no-unused-vars": "off", 10 | "no-console": "warn" 11 | }, 12 | "globals": { 13 | "React": true, 14 | "JSX": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.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 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # vercel 34 | .vercel 35 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.husky/post-merge: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn lint-staged -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 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 | # next.js 12 | .next 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env.local 30 | .env.development.local 31 | .env.test.local 32 | .env.production.local 33 | 34 | # vercel 35 | .vercel 36 | -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'always', 3 | singleQuote: true, 4 | jsxSingleQuote: true, 5 | tabWidth: 2, 6 | overrides: [ 7 | { 8 | files: '*.mdx', 9 | options: { 10 | tabWidth: 2, 11 | }, 12 | }, 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "css.validate": false, 3 | "editor.formatOnSave": true, 4 | "editor.tabSize": 2, 5 | "prettier.tabWidth": 2 6 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## 0.1.0 (2021-08-28) 6 | 7 | 8 | ### Features 9 | 10 | * add dropzone to preview ([843a8b6](https://github.com/theodorusclarence/rhf-stepform/commit/843a8b655da3a55e0f2c352f93e441b6cbe6d8bc)) 11 | * add dropzone with lightbox preview ([#1](https://github.com/theodorusclarence/rhf-stepform/issues/1)) ([68cad7e](https://github.com/theodorusclarence/rhf-stepform/commit/68cad7e6c09cc720473ec73b446ef956920737f7)) 12 | * add file on step-2 ([76e84a6](https://github.com/theodorusclarence/rhf-stepform/commit/76e84a65efb1718ed47620b0bade7b9f90eeb7d4)) 13 | * add file preview in progress text ([5fb54df](https://github.com/theodorusclarence/rhf-stepform/commit/5fb54df2ec99b1e1f9b3845b5a9b30648af2b7c0)) 14 | * add forms and yup validation step 1 ([c62826a](https://github.com/theodorusclarence/rhf-stepform/commit/c62826a0602618b25140ac685f447e0276153c4a)) 15 | * add identity card field ([385dedb](https://github.com/theodorusclarence/rhf-stepform/commit/385dedb761b1662263787ebeacc16866f8b0450e)) 16 | * add input and distance ([6eab229](https://github.com/theodorusclarence/rhf-stepform/commit/6eab22990d68ed4041fa067bbd3942d60faee605)) 17 | * add latlng fallback for ReadOnlyMap ([78e02d9](https://github.com/theodorusclarence/rhf-stepform/commit/78e02d908c2b84de34eca4b42154376593dc83c9)) 18 | * add map to step-3 and geosearch ([1fbbc5a](https://github.com/theodorusclarence/rhf-stepform/commit/1fbbc5a503d4394a1a7069fe3f06ce636ac42f79)) 19 | * add ReadOnly on recap ([147136e](https://github.com/theodorusclarence/rhf-stepform/commit/147136e4bc3d24f25e9558a0eeab4a0c4d4abe84)) 20 | * add recap and select ([6ff4f16](https://github.com/theodorusclarence/rhf-stepform/commit/6ff4f1617c11e4a74ba85ce79cabb9b6ad46c8d3)) 21 | * add recap-json ([1c4a793](https://github.com/theodorusclarence/rhf-stepform/commit/1c4a7939dd9bf2cd39fa8d523cb3adfe5876562d)) 22 | * add step-2 ([17f7e90](https://github.com/theodorusclarence/rhf-stepform/commit/17f7e90f3b118183c98ba11399fdd29ec376efb0)) 23 | * add step-3 ([182571c](https://github.com/theodorusclarence/rhf-stepform/commit/182571c5faf98e31e121a9b523ddab810c0b8879)) 24 | * add toast ([c4c461c](https://github.com/theodorusclarence/rhf-stepform/commit/c4c461c62e6b723a5ec141ffaefc2164000f382c)) 25 | * add types for files ([8a678f8](https://github.com/theodorusclarence/rhf-stepform/commit/8a678f82fda7997a97126eb510a51d789982fd7f)) 26 | * add typing and validate after remove ([e36a50d](https://github.com/theodorusclarence/rhf-stepform/commit/e36a50d30b0d2a43aecb25441843a71a8f13f00d)) 27 | * allow to drop files one at a time ([c9cbd98](https://github.com/theodorusclarence/rhf-stepform/commit/c9cbd98bdba4816dac5ef5367503fe37b819b737)) 28 | * check for data to access recap ([f277f9a](https://github.com/theodorusclarence/rhf-stepform/commit/f277f9a9f5e027f9b18daf8ebac35c0ef705e6ee)) 29 | * make index page text-center ([c594205](https://github.com/theodorusclarence/rhf-stepform/commit/c5942057309e335fdd60de2b42e49a034d22de51)) 30 | * **map:** add map component ([98479f0](https://github.com/theodorusclarence/rhf-stepform/commit/98479f0e0eddc4b39a191087657fc4d4584e1137)) 31 | * remove step push ([22def10](https://github.com/theodorusclarence/rhf-stepform/commit/22def10022738f856ad3effa11f4f427d646cbfe)) 32 | * use PasswordInput ([4a1bc25](https://github.com/theodorusclarence/rhf-stepform/commit/4a1bc255a8dbeaf4f17a4649214cb80e0e251fc6)) 33 | 34 | 35 | ### Bug Fixes 36 | 37 | * add type to next app and document ([6703ead](https://github.com/theodorusclarence/rhf-stepform/commit/6703eadc75bd2e9d9237916200fda3886bfa8838)) 38 | * bugs when upload multiple files ([7288929](https://github.com/theodorusclarence/rhf-stepform/commit/728892955a43ccc25df392971227094d8d4a2b79)) 39 | * can't delete on revisit ([c35c101](https://github.com/theodorusclarence/rhf-stepform/commit/c35c101e052c0d895d5ecf3b79567a3bfef52e0e)) 40 | * **map:** can't modify input ([ce9100d](https://github.com/theodorusclarence/rhf-stepform/commit/ce9100d11ae9c6b7c0007383cbe178168d05a6b9)) 41 | * wrong mimetypes ([15aecb5](https://github.com/theodorusclarence/rhf-stepform/commit/15aecb5fafd6cd6a869525c0e2ce9ad2b8b1b554)) 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React-Hook-Form Stepform 2 | 3 | [![All Contributors](https://img.shields.io/badge/all_contributors-2-orange.svg?style=flat-square)](#contributors-) 4 | 5 | 6 | ![rhf-stepform](https://socialify.git.ci/theodorusclarence/rhf-stepform/image?description=1&language=1&owner=1&pattern=Charlie%20Brown&stargazers=1&theme=Dark) 7 | 8 | ## Code to observe: 9 | 10 | - https://github.com/theodorusclarence/rhf-stepform/tree/main/pages/form, form pages 11 | - https://github.com/theodorusclarence/rhf-stepform/blob/main/store/useFormStore.tsx, where form data is stored 12 | - https://github.com/theodorusclarence/rhf-stepform/blob/main/lib/yup.ts, yup schema 13 | - https://github.com/theodorusclarence/rhf-stepform/blob/main/types.ts, form type declaration 14 | - https://github.com/theodorusclarence/rhf-stepform/tree/main/components/Forms, form components 15 | 16 | ## Key Points 17 | 18 | ### Each time submitting, data is stored in FormStore 19 | 20 | ```tsx 21 | const onSubmit = (data: StepOneData) => { 22 | setData({ step: 1, data }); 23 | router.push('/form/step-2'); 24 | }; 25 | ``` 26 | 27 | ### The stored data will be used as a default value on revisit 28 | 29 | ```tsx 30 | const { stepOne, setData } = useFormStore(); 31 | 32 | const methods = useForm({ 33 | mode: 'onTouched', 34 | resolver: yupResolver(stepOneSchema), 35 | defaultValues: stepOne || {}, 36 | }); 37 | ``` 38 | 39 | ### Upload Form 40 | 41 | The tricky part lies in Upload Form. The data that is originally stored by the input is `File` object, but if we store it in zustand, it will be transformed into regular object. This will cause an error when we invoke the `URL.createObjectURL(file)` for the FilePreview. 42 | 43 | So we need to invoke it while we get the original File, and store the URL as a new property. In that way, we only invoke it once, and just use the blob url for revisit. 44 | 45 | ```tsx 46 | const acceptedFilesPreview = acceptedFiles.map( 47 | (file: FileWithPreview) => 48 | Object.assign(file, { 49 | preview: URL.createObjectURL(file), 50 | }) 51 | ); 52 | ``` 53 | 54 | ## Contributors ✨ 55 | 56 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |

Theodorus Clarence

💻

Muhammad Rizqi Tsani

💻
67 | 68 | 69 | 70 | 71 | 72 | 73 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-conventional'], 3 | rules: { 4 | // TODO Add Scope Enum Here 5 | // 'scope-enum': [2, 'always', ['yourscope', 'yourscope']], 6 | 'type-enum': [ 7 | 2, 8 | 'always', 9 | [ 10 | 'feat', 11 | 'fix', 12 | 'docs', 13 | 'chore', 14 | 'style', 15 | 'refactor', 16 | 'ci', 17 | 'test', 18 | 'perf', 19 | 'revert', 20 | 'vercel', 21 | ], 22 | ], 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /components/Button.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | 3 | type ButtonProps = { 4 | children: React.ReactChild | string; 5 | className?: string; 6 | variant?: 'primary' | 'secondary'; 7 | } & React.ComponentPropsWithoutRef<'button'>; 8 | 9 | export default function Button({ 10 | children, 11 | className = '', 12 | variant = 'primary', 13 | ...rest 14 | }: ButtonProps) { 15 | return ( 16 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /components/CustomLink.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import UnstyledLink, { UnstyledLinkProps } from './UnstyledLink'; 3 | 4 | export default function CustomLink({ 5 | children, 6 | className = '', 7 | ...rest 8 | }: UnstyledLinkProps) { 9 | return ( 10 | 17 | {children} 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /components/CustomMap/LoadingMap.tsx: -------------------------------------------------------------------------------- 1 | export default function LoadingMap() { 2 | return
; 3 | } 4 | -------------------------------------------------------------------------------- /components/CustomMap/LocationMarker.tsx: -------------------------------------------------------------------------------- 1 | import L from 'leaflet'; 2 | import { Dispatch, SetStateAction } from 'react'; 3 | import { useFormContext } from 'react-hook-form'; 4 | import { useMapEvents, Marker } from 'react-leaflet'; 5 | 6 | import { getMarkerPosition } from '@/lib/helper'; 7 | 8 | type LocationMarkerProps = { 9 | setIsDragging: Dispatch>; 10 | }; 11 | 12 | export default function LocationMarker({ setIsDragging }: LocationMarkerProps) { 13 | const { watch, setValue } = useFormContext(); 14 | const markerPosition = getMarkerPosition(watch); 15 | 16 | const map = useMapEvents({ 17 | drag() { 18 | const { lat, lng } = map.getCenter(); 19 | setValue('lat', lat); 20 | setValue('lng', lng); 21 | }, 22 | dragstart() { 23 | setIsDragging(true); 24 | }, 25 | dragend() { 26 | setIsDragging(false); 27 | }, 28 | locationfound(e) { 29 | map.flyTo(e.latlng, map.getZoom()); 30 | setValue('lat', e.latlng.lat, { shouldValidate: true }); 31 | setValue('lng', e.latlng.lng, { shouldValidate: true }); 32 | }, 33 | }); 34 | 35 | return ; 36 | } 37 | 38 | export const DefaultIcon = L.icon({ 39 | iconSize: [24, 41], 40 | iconAnchor: [12, 41], 41 | iconUrl: '/images/leaflet/marker-icon.png', 42 | shadowUrl: '/images/leaflet/marker-shadow.png', 43 | }); 44 | -------------------------------------------------------------------------------- /components/CustomMap/ReadOnlyMap.tsx: -------------------------------------------------------------------------------- 1 | import L from 'leaflet'; 2 | import 'leaflet/dist/leaflet.css'; 3 | import 'leaflet-geosearch/assets/css/leaflet.css'; 4 | import { useEffect, useState } from 'react'; 5 | import { MapContainer, LayersControl, Marker } from 'react-leaflet'; 6 | const { BaseLayer } = LayersControl; 7 | import { useFormContext } from 'react-hook-form'; 8 | import ReactLeafletGoogleLayer from 'react-leaflet-google-layer'; 9 | 10 | import { getMarkerPosition } from '@/lib/helper'; 11 | 12 | import { DefaultIcon } from '@/components/CustomMap/LocationMarker'; 13 | 14 | import { LatLong } from './types'; 15 | 16 | export default function ReadOnlyMap() { 17 | const [map, setMap] = useState(undefined); 18 | const { watch } = useFormContext(); 19 | const markerPosition = getMarkerPosition(watch, pickedLatlong); 20 | 21 | const handleMapCreated = (map: L.Map) => { 22 | setMap(map); 23 | }; 24 | 25 | // Move map if input is changing 26 | // TODO Optimize setView calling 27 | useEffect(() => { 28 | map?.setView(markerPosition); 29 | // eslint-disable-next-line react-hooks/exhaustive-deps 30 | }, [map, markerPosition.lat, markerPosition.lng]); 31 | 32 | const distance: string = ( 33 | (map?.distance(markerPosition, pickedLatlong) ?? 0) / 1000 34 | ).toFixed(3); 35 | 36 | return ( 37 |
38 | 46 | 47 | 48 | 52 | 53 | 54 | 58 | 59 | 60 | ; 61 | 62 |
63 |

Distance to Monas: {distance}km

64 |
65 |
66 | ); 67 | } 68 | 69 | // Monas latlong 70 | const pickedLatlong: LatLong = { 71 | lat: -6.1754, 72 | lng: 106.8272, 73 | }; 74 | -------------------------------------------------------------------------------- /components/CustomMap/index.tsx: -------------------------------------------------------------------------------- 1 | import L from 'leaflet'; 2 | import 'leaflet/dist/leaflet.css'; 3 | import 'leaflet-geosearch/assets/css/leaflet.css'; 4 | import { useEffect, useState } from 'react'; 5 | import { MapContainer, LayersControl } from 'react-leaflet'; 6 | const { BaseLayer } = LayersControl; 7 | import { useFormContext } from 'react-hook-form'; 8 | import ReactLeafletGoogleLayer from 'react-leaflet-google-layer'; 9 | import { SearchControl, GoogleProvider } from 'leaflet-geosearch'; 10 | 11 | import { getMarkerPosition } from '@/lib/helper'; 12 | 13 | import LocationMarker from '@/components/CustomMap/LocationMarker'; 14 | import Button from '@/components/Button'; 15 | import { LatLong } from './types'; 16 | 17 | export default function CustomMap() { 18 | const [map, setMap] = useState(undefined); 19 | const [isDragging, setIsDragging] = useState(false); 20 | const { watch } = useFormContext(); 21 | const markerPosition = getMarkerPosition(watch); 22 | 23 | const handleMapCreated = (map: L.Map) => { 24 | setMap(map); 25 | 26 | if (map) { 27 | const provider = new GoogleProvider({ 28 | params: { 29 | key: '', 30 | language: 'id', 31 | region: 'id', 32 | }, 33 | }); 34 | 35 | map.addControl( 36 | // @ts-ignore 37 | new SearchControl({ 38 | provider: provider, 39 | style: 'bar', 40 | showMarker: false, 41 | showPopup: false, 42 | searchLabel: "Search (Won't work due to no API Key)", 43 | }) 44 | ); 45 | } 46 | }; 47 | 48 | const handleUseCurrent = () => { 49 | map?.locate(); 50 | }; 51 | 52 | // Move map if input is changing 53 | // TODO Optimize setView calling 54 | useEffect(() => { 55 | if (isDragging) return; 56 | 57 | map?.setView(markerPosition); 58 | // eslint-disable-next-line react-hooks/exhaustive-deps 59 | }, [isDragging, map, markerPosition.lat, markerPosition.lng]); 60 | 61 | const distance: string = ( 62 | (map?.distance(markerPosition, pickedLatlong) ?? 0) / 1000 63 | ).toFixed(3); 64 | 65 | return ( 66 |
67 | 75 | 76 | 77 | 81 | 82 | 83 | 87 | 88 | 89 | 90 | 91 | 94 |
95 |

Distance to Monas: {distance}km

96 |
97 |
98 | ); 99 | } 100 | 101 | // Monas latlong 102 | const pickedLatlong: LatLong = { 103 | lat: -6.1754, 104 | lng: 106.8272, 105 | }; 106 | -------------------------------------------------------------------------------- /components/CustomMap/types.ts: -------------------------------------------------------------------------------- 1 | export type LatLong = { 2 | lat: number; 3 | lng: number; 4 | }; 5 | -------------------------------------------------------------------------------- /components/Forms/DatePicker.tsx: -------------------------------------------------------------------------------- 1 | import ReactDatePicker, { ReactDatePickerProps } from 'react-datepicker'; 2 | import 'react-datepicker/dist/react-datepicker.css'; 3 | import { Controller, RegisterOptions, useFormContext } from 'react-hook-form'; 4 | import { HiOutlineCalendar } from 'react-icons/hi'; 5 | 6 | import { classNames } from '@/lib/helper'; 7 | 8 | type DatePickerProps = { 9 | validation?: RegisterOptions; 10 | label: string; 11 | id: string; 12 | placeholder?: string; 13 | defaultYear?: number; 14 | defaultMonth?: number; 15 | defaultValue?: string; 16 | helperText?: string; 17 | readOnly?: boolean; 18 | } & Omit; 19 | 20 | export default function DatePicker({ 21 | validation, 22 | label, 23 | id, 24 | placeholder, 25 | defaultYear, 26 | defaultMonth, 27 | defaultValue, 28 | helperText, 29 | readOnly = false, 30 | ...rest 31 | }: DatePickerProps) { 32 | const { 33 | formState: { errors }, 34 | control, 35 | } = useFormContext(); 36 | 37 | // If there is a year default, then change the year to the props 38 | const defaultDate = new Date(); 39 | if (defaultYear) defaultDate.setFullYear(defaultYear); 40 | if (defaultMonth) defaultDate.setMonth(defaultMonth); 41 | 42 | return ( 43 |
44 | 47 | 48 | ( 54 | <> 55 |
56 | 79 | 80 |
81 |
82 | {helperText !== '' && ( 83 |

{helperText}

84 | )} 85 | {errors[id] && ( 86 | 87 | {errors[id].message} 88 | 89 | )} 90 |
91 | 92 | )} 93 | /> 94 |
95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /components/Forms/DropzoneInput.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { MouseEvent, useCallback, useEffect, useState } from 'react'; 3 | import { Controller, useFormContext } from 'react-hook-form'; 4 | import { FileWithPath, useDropzone } from 'react-dropzone'; 5 | 6 | import { FilePreview } from './FilePreview'; 7 | import { FileWithPreview } from '@/types'; 8 | 9 | type DropzoneInputProps = { 10 | accept?: string; 11 | helperText?: string; 12 | id: string; 13 | label: string; 14 | maxFiles?: number; 15 | readOnly?: boolean; 16 | validation?: object; 17 | }; 18 | 19 | export default function DropzoneInput({ 20 | accept, 21 | helperText = '', 22 | id, 23 | label, 24 | maxFiles = 1, 25 | validation, 26 | readOnly, 27 | }: DropzoneInputProps) { 28 | const { 29 | control, 30 | getValues, 31 | setValue, 32 | setError, 33 | clearErrors, 34 | watch, 35 | formState: { errors }, 36 | } = useFormContext(); 37 | 38 | const [files, setFiles] = useState(getValues(id) || []); 39 | 40 | const onDrop = useCallback( 41 | (acceptedFiles, rejectedFiles) => { 42 | if (rejectedFiles && rejectedFiles.length > 0) { 43 | setValue(id, files ? [...files] : null); 44 | setError(id, { 45 | type: 'manual', 46 | message: rejectedFiles && rejectedFiles[0].errors[0].message, 47 | }); 48 | } else { 49 | const acceptedFilesPreview = acceptedFiles.map( 50 | (file: FileWithPreview) => 51 | Object.assign(file, { 52 | preview: URL.createObjectURL(file), 53 | }) 54 | ); 55 | 56 | setFiles( 57 | files 58 | ? [...files, ...acceptedFilesPreview].slice(0, maxFiles) 59 | : acceptedFilesPreview 60 | ); 61 | 62 | setValue( 63 | id, 64 | files 65 | ? [...files, ...acceptedFiles].slice(0, maxFiles) 66 | : acceptedFiles, 67 | { 68 | shouldValidate: true, 69 | } 70 | ); 71 | clearErrors(id); 72 | } 73 | }, 74 | [clearErrors, files, id, maxFiles, setError, setValue] 75 | ); 76 | 77 | useEffect(() => { 78 | return () => { 79 | () => { 80 | files.forEach((file) => URL.revokeObjectURL(file.preview)); 81 | }; 82 | }; 83 | }, [files]); 84 | 85 | const deleteFile = (e: MouseEvent, file: FileWithPreview) => { 86 | e.preventDefault(); 87 | const newFiles = [...files]; 88 | 89 | newFiles.splice(newFiles.indexOf(file), 1); 90 | 91 | if (newFiles.length > 0) { 92 | setFiles(newFiles); 93 | setValue(id, newFiles, { 94 | shouldValidate: true, 95 | shouldDirty: true, 96 | shouldTouch: true, 97 | }); 98 | } else { 99 | setFiles([]); 100 | setValue(id, null, { 101 | shouldValidate: true, 102 | shouldDirty: true, 103 | shouldTouch: true, 104 | }); 105 | } 106 | }; 107 | 108 | const { getRootProps, getInputProps } = useDropzone({ 109 | onDrop, 110 | accept, 111 | maxFiles, 112 | maxSize: 2000000, 113 | }); 114 | 115 | return ( 116 |
117 | 120 | 121 | {readOnly && !(files?.length > 0) ? ( 122 |
123 | No file uploaded 124 |
125 | ) : files?.length >= maxFiles ? ( 126 |
    127 | {files.map((file, index) => ( 128 | 134 | ))} 135 |
136 | ) : ( 137 | ( 142 | <> 143 |
148 | 149 |
157 |
158 |

159 | Drag 'n' drop some files here, or click to 160 | select files 161 |

162 |

{`${ 163 | maxFiles - (files?.length || 0) 164 | } file(s) remaining`}

165 |
166 |
167 |
168 | 169 |
170 | {helperText !== '' && ( 171 |

{helperText}

172 | )} 173 | {errors[id] && ( 174 |

{errors[id].message}

175 | )} 176 |
177 | {!readOnly && !!files?.length && ( 178 |
    179 | {files.map((file, index) => ( 180 | 186 | ))} 187 |
188 | )} 189 | 190 | )} 191 | /> 192 | )} 193 |
194 | ); 195 | } 196 | -------------------------------------------------------------------------------- /components/Forms/FilePreview.tsx: -------------------------------------------------------------------------------- 1 | import { MouseEvent, ReactElement, useState } from 'react'; 2 | import Lightbox from 'react-image-lightbox'; 3 | import { 4 | HiOutlineExternalLink, 5 | HiOutlineEye, 6 | HiOutlinePaperClip, 7 | HiOutlinePhotograph, 8 | HiX, 9 | } from 'react-icons/hi'; 10 | 11 | import UnstyledLink from '../UnstyledLink'; 12 | import { FileWithPreview } from '@/types'; 13 | 14 | type FilePreviewProps = { 15 | file: FileWithPreview; 16 | } & ( 17 | | { 18 | deleteFile?: (e: MouseEvent, file: FileWithPreview) => void; 19 | readOnly?: true; 20 | } 21 | | { 22 | deleteFile: (e: MouseEvent, file: FileWithPreview) => void; 23 | readOnly?: false; 24 | } 25 | ); 26 | 27 | export const FilePreview = ({ 28 | deleteFile, 29 | file, 30 | readOnly, 31 | }: FilePreviewProps): ReactElement => { 32 | const [index, setIndex] = useState(0); 33 | const [isOpen, setIsOpen] = useState(false); 34 | 35 | const images = [file.preview]; 36 | 37 | const handleDelete = (e: MouseEvent) => { 38 | e.stopPropagation(); 39 | deleteFile?.(e, file); 40 | }; 41 | 42 | const imagesType = ['image/png', 'image/jpg', 'image/jpeg']; 43 | 44 | return imagesType.includes(file.type) ? ( 45 | <> 46 |
  • 50 |
    51 |
    57 |
    58 | 65 | {!readOnly && ( 66 | 73 | )} 74 |
    75 |
  • 76 | {isOpen && ( 77 | setIsOpen(false)} 82 | onMovePrevRequest={() => 83 | setIndex( 84 | (prevIndex) => (prevIndex + images.length - 1) % images.length 85 | ) 86 | } 87 | onMoveNextRequest={() => 88 | setIndex((prevIndex) => (prevIndex + 1) % images.length) 89 | } 90 | /> 91 | )} 92 | 93 | ) : ( 94 |
  • 98 |
    99 |
    105 |
    106 | 110 | 111 | 112 | {!readOnly && ( 113 | 120 | )} 121 |
    122 |
  • 123 | ); 124 | }; 125 | -------------------------------------------------------------------------------- /components/Forms/Input.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { useFormContext, RegisterOptions } from 'react-hook-form'; 3 | import { HiExclamationCircle } from 'react-icons/hi'; 4 | 5 | export type InputProps = { 6 | label: string; 7 | id: string; 8 | placeholder?: string; 9 | helperText?: string; 10 | type?: string; 11 | readOnly?: boolean; 12 | validation?: RegisterOptions; 13 | } & React.ComponentPropsWithoutRef<'input'>; 14 | 15 | export default function Input({ 16 | label, 17 | placeholder = '', 18 | helperText, 19 | id, 20 | type = 'text', 21 | readOnly = false, 22 | validation, 23 | ...rest 24 | }: InputProps) { 25 | const { 26 | register, 27 | formState: { errors }, 28 | } = useFormContext(); 29 | 30 | return ( 31 |
    32 | 35 |
    36 | 54 | 55 | {errors[id] && ( 56 |
    57 | 58 |
    59 | )} 60 |
    61 |
    62 | {helperText &&

    {helperText}

    } 63 | {errors[id] && ( 64 | {errors[id].message} 65 | )} 66 |
    67 |
    68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /components/Forms/PasswordInput.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useFormContext } from 'react-hook-form'; 3 | import { HiEye, HiEyeOff } from 'react-icons/hi'; 4 | 5 | import { classNames } from '@/lib/helper'; 6 | import clsx from 'clsx'; 7 | import { InputProps } from './Input'; 8 | 9 | export default function PasswordInput({ 10 | label, 11 | placeholder = '', 12 | helperText, 13 | id, 14 | type = 'text', 15 | readOnly = false, 16 | validation, 17 | ...rest 18 | }: InputProps) { 19 | const { 20 | register, 21 | formState: { errors }, 22 | } = useFormContext(); 23 | 24 | const [showPassword, setShowPassword] = useState(false); 25 | const togglePassword = () => setShowPassword((prev) => !prev); 26 | 27 | return ( 28 |
    29 | 32 |
    33 | 51 | 52 | 66 |
    67 |
    68 | {helperText &&

    {helperText}

    } 69 | {errors[id] && ( 70 | {errors[id].message} 71 | )} 72 |
    73 |
    74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /components/Forms/Select.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { Children, cloneElement, isValidElement, ReactNode } from 'react'; 3 | import { RegisterOptions, useFormContext } from 'react-hook-form'; 4 | import { HiExclamationCircle } from 'react-icons/hi'; 5 | 6 | export type SelectProps = { 7 | label: string; 8 | id: string; 9 | placeholder?: string; 10 | helperText?: string; 11 | type?: string; 12 | readOnly?: boolean; 13 | validation?: RegisterOptions; 14 | children: ReactNode; 15 | } & React.ComponentPropsWithoutRef<'select'>; 16 | 17 | export default function Select({ 18 | label, 19 | helperText, 20 | id, 21 | placeholder, 22 | readOnly = false, 23 | children, 24 | validation, 25 | ...rest 26 | }: SelectProps) { 27 | const { 28 | register, 29 | formState: { errors }, 30 | } = useFormContext(); 31 | 32 | // Add disabled and selected attribute to option, will be used if readonly 33 | const readOnlyChildren = Children.map( 34 | children, 35 | (child) => { 36 | if (isValidElement(child)) { 37 | return cloneElement(child, { 38 | disabled: child.props.value !== rest?.defaultValue, 39 | // selected: child.props.value === rest?.defaultValue, 40 | }); 41 | } 42 | } 43 | ); 44 | 45 | return ( 46 |
    47 | 50 |
    51 | 75 | 76 | {errors[id] && ( 77 |
    78 | 79 |
    80 | )} 81 |
    82 |
    83 | {helperText &&

    {helperText}

    } 84 | {errors[id] && ( 85 | {errors[id].message} 86 | )} 87 |
    88 |
    89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /components/Nav.tsx: -------------------------------------------------------------------------------- 1 | import Link from 'next/link'; 2 | import UnstyledLink from './UnstyledLink'; 3 | 4 | const links = [ 5 | { href: '/', label: 'Route' }, 6 | { href: '/', label: 'Route' }, 7 | ]; 8 | 9 | export default function Nav() { 10 | return ( 11 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /components/Seo.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'next/head'; 2 | import { useRouter } from 'next/router'; 3 | 4 | const defaultMeta = { 5 | title: 'Step Form Example', 6 | site_name: 'Step Form Example', 7 | description: 8 | 'Step form built using React Hook Form, Yup, Typescript, and Zustand', 9 | url: 'https://rhf-step.thcl.dev', 10 | image: 'https://theodorusclarence.com/favicon/large-og.jpg', 11 | type: 'website', 12 | robots: 'follow, index', 13 | }; 14 | 15 | type SeoProps = { 16 | date?: string; 17 | templateTitle?: string; 18 | } & Partial; 19 | 20 | export default function Seo(props: SeoProps) { 21 | const router = useRouter(); 22 | const meta = { 23 | ...defaultMeta, 24 | ...props, 25 | }; 26 | meta['title'] = props.templateTitle 27 | ? `${props.templateTitle} | ${meta.site_name}` 28 | : meta.title; 29 | 30 | return ( 31 | 32 | {meta.title} 33 | 34 | 35 | 36 | 37 | {/* Open Graph */} 38 | 39 | 40 | 41 | 42 | 43 | {/* Twitter */} 44 | 45 | 46 | 47 | 48 | 49 | {meta.date && ( 50 | <> 51 | 52 | 57 | 62 | 63 | )} 64 | 65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /components/UnstyledLink.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Link, { LinkProps } from 'next/link'; 3 | 4 | export type UnstyledLinkProps = { 5 | href: string; 6 | children: React.ReactChild | string; 7 | openNewTab?: boolean; 8 | className?: string; 9 | } & React.ComponentPropsWithoutRef<'a'> & 10 | LinkProps; 11 | 12 | export default function UnstyledLink({ 13 | children, 14 | href, 15 | openNewTab = undefined, 16 | className, 17 | ...rest 18 | }: UnstyledLinkProps) { 19 | const isNewTab = 20 | openNewTab !== undefined 21 | ? openNewTab 22 | : href && !href.startsWith('/') && !href.startsWith('#'); 23 | 24 | if (!isNewTab) { 25 | return ( 26 | 27 | 28 | {children} 29 | 30 | 31 | ); 32 | } 33 | 34 | return ( 35 | 42 | {children} 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /lib/helper.ts: -------------------------------------------------------------------------------- 1 | import { LatLong } from '@/types'; 2 | 3 | export function classNames(...classes: string[]): string { 4 | return classes.filter(Boolean).join(' '); 5 | } 6 | 7 | function clean(input: string): number { 8 | const str = input + ''; 9 | 10 | if (str === '') return 0; 11 | 12 | const replaced = str.replace(/[^0-9.-]/g, ''); 13 | if (replaced === '-') return 0; 14 | else return parseFloat(replaced); 15 | } 16 | 17 | export function getMarkerPosition( 18 | watch: (name: string) => number, 19 | fallback?: LatLong 20 | ): LatLong { 21 | const lat: number = watch('lat'); 22 | const lng: number = watch('lng'); 23 | 24 | if (lat && lng) 25 | return { 26 | lat: clean(lat + ''), 27 | lng: clean(lng + ''), 28 | }; 29 | 30 | if (fallback) return fallback; 31 | 32 | return { 33 | lat: 0, 34 | lng: 0, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /lib/yup.ts: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | import { LatLong, StepOneData, StepThreeData, StepTwoData } from '@/types'; 3 | 4 | export const stepOneSchema: yup.SchemaOf = yup.object().shape({ 5 | name: yup.string().required('Name is required'), 6 | email: yup 7 | .string() 8 | .email('Need to be a valid email') 9 | .required('Email is required'), 10 | password: yup 11 | .string() 12 | .required('Password is required') 13 | .min(8, 'Password must be at least 8 characters long'), 14 | age: yup 15 | .number() 16 | .typeError('Must be a number') 17 | .positive('Must be a positive value') 18 | .integer('Must be a number') 19 | .required('Age is required'), 20 | phone: yup 21 | .string() 22 | .matches(/^\+628[1-9][0-9]{8,11}$/, 'Must use +62 format') 23 | .required('Phone is required'), 24 | }); 25 | 26 | // @ts-ignore - file type still not found 27 | // TODO Should be ArraySchema | Lazy, any>, AnyObject 28 | export const stepTwoSchema: yup.SchemaOf = yup.object().shape({ 29 | score_1: yup 30 | .number() 31 | .typeError('Must be a number') 32 | .positive('Must be a positive value') 33 | .lessThan(101, 'Max score is 100') 34 | .required('Age is required'), 35 | score_2: yup 36 | .number() 37 | .typeError('Must be a number') 38 | .positive('Must be a positive value') 39 | .lessThan(101, 'Max score is 100') 40 | .required('Age is required'), 41 | score_3: yup 42 | .number() 43 | .typeError('Must be a number') 44 | .positive('Must be a positive value') 45 | .lessThan(101, 'Max score is 100') 46 | .required('Age is required'), 47 | identity_card: yup.mixed().required('File is required'), 48 | score_file: yup.mixed().required('File is required'), 49 | }); 50 | 51 | // @ts-ignore - override correct yup type 52 | // https://github.com/jquense/yup/issues/1183 53 | export const requiredDateSchema: yup.SchemaOf = yup 54 | .date() 55 | .required('Birth date is required'); 56 | 57 | export const stepThreeSchema: yup.SchemaOf = yup.object().shape({ 58 | // yup date 59 | birth_date: requiredDateSchema, 60 | gender: yup.string().required('Gender is required'), 61 | lat: yup.number().typeError('Must be a number').required('Lat is required'), 62 | lng: yup.number().typeError('Must be a number').required('Long is required'), 63 | }); 64 | 65 | export const mapSchema: yup.SchemaOf = yup.object().shape({ 66 | lat: yup.number().typeError('Must be a number').required('Lat is required'), 67 | lng: yup.number().typeError('Must be a number').required('Long is required'), 68 | }); 69 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rhf-stepform", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "prepare": "husky install", 10 | "lint": "next lint", 11 | "release": "standard-version" 12 | }, 13 | "dependencies": { 14 | "@hookform/resolvers": "^2.7.1", 15 | "@tailwindcss/forms": "^0.3.3", 16 | "autoprefixer": "^10.3.1", 17 | "clsx": "^1.1.1", 18 | "leaflet": "^1.7.1", 19 | "leaflet-geosearch": "^3.3.2", 20 | "next": "^11.0.0", 21 | "postcss": "^8.3.6", 22 | "react": "^17.0.2", 23 | "react-datepicker": "^4.2.0", 24 | "react-dom": "^17.0.2", 25 | "react-dropzone": "^11.3.4", 26 | "react-hook-form": "^7.12.2", 27 | "react-hot-toast": "^2.1.0", 28 | "react-icons": "^4.2.0", 29 | "react-image-lightbox": "^5.1.4", 30 | "react-leaflet": "^3.2.1", 31 | "react-leaflet-google-layer": "^2.0.4", 32 | "yup": "^0.32.9", 33 | "zustand": "^3.5.7" 34 | }, 35 | "devDependencies": { 36 | "@commitlint/cli": "^13.1.0", 37 | "@commitlint/config-conventional": "^13.1.0", 38 | "@types/leaflet": "^1.7.4", 39 | "@types/react": "^17.0.16", 40 | "@types/react-datepicker": "^4.1.4", 41 | "eslint": "^7.32.0", 42 | "eslint-config-next": "^11.0.1", 43 | "husky": "^7.0.1", 44 | "lint-staged": "^11.1.2", 45 | "prettier": "^2.3.0", 46 | "standard-version": "^9.3.1", 47 | "tailwindcss": "^2.2.7", 48 | "typescript": "^4.3.5" 49 | }, 50 | "lint-staged": { 51 | "**/*.{js,jsx,ts,tsx}": [ 52 | "yarn prettier --write" 53 | ] 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { AppProps } from 'next/app'; 2 | import { Toaster } from 'react-hot-toast'; 3 | import 'react-image-lightbox/style.css'; 4 | import '@/styles/globals.css'; 5 | 6 | function MyApp({ Component, pageProps }: AppProps) { 7 | return ( 8 | <> 9 |
    10 | 21 |
    22 | 23 | 24 | ); 25 | } 26 | 27 | export default MyApp; 28 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import Document, { 2 | Html, 3 | Head, 4 | Main, 5 | NextScript, 6 | DocumentContext, 7 | } from 'next/document'; 8 | 9 | class MyDocument extends Document { 10 | static async getInitialProps(ctx: DocumentContext) { 11 | const initialProps = await Document.getInitialProps(ctx); 12 | return { ...initialProps }; 13 | } 14 | 15 | render() { 16 | return ( 17 | 18 | 19 | 26 | 31 | 36 | 41 | 46 | 51 | 56 | 61 | 66 | 71 | 77 | 83 | 89 | 95 | 96 | 97 | 101 | 102 | 103 | 104 |
    105 | 106 | 107 | 108 | ); 109 | } 110 | } 111 | 112 | export default MyDocument; 113 | -------------------------------------------------------------------------------- /pages/form/recap.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import { useEffect } from 'react'; 3 | import dynamic from 'next/dynamic'; 4 | import toast from 'react-hot-toast'; 5 | import { useRouter } from 'next/router'; 6 | import { FormProvider, useForm } from 'react-hook-form'; 7 | import { HiChevronLeft, HiChevronRight } from 'react-icons/hi'; 8 | 9 | import useFormStore from '@/store/useFormStore'; 10 | 11 | import { FormData } from '@/types'; 12 | 13 | import Seo from '@/components/Seo'; 14 | import UnstyledLink from '@/components/UnstyledLink'; 15 | import CustomLink from '@/components/CustomLink'; 16 | import DatePicker from '@/components/Forms/DatePicker'; 17 | import Select from '@/components/Forms/Select'; 18 | import Input from '@/components/Forms/Input'; 19 | import PasswordInput from '@/components/Forms/PasswordInput'; 20 | import DropzoneInput from '@/components/Forms/DropzoneInput'; 21 | import LoadingMap from '@/components/CustomMap/LoadingMap'; 22 | // @ts-ignore 23 | const ReadOnlyMap = dynamic( 24 | () => import('@/components/CustomMap/ReadOnlyMap'), 25 | { 26 | ssr: false, 27 | loading: LoadingMap, 28 | } 29 | ); 30 | 31 | export default function RecapPage() { 32 | const router = useRouter(); 33 | 34 | const { stepOne, stepTwo, stepThree } = useFormStore(); 35 | 36 | // useEffect(() => { 37 | // if (!stepOne) { 38 | // toast.error('Please fill step one first'); 39 | // router.push('/form/step-1'); 40 | // } else if (!stepTwo) { 41 | // toast.error('Please fill step two first'); 42 | // router.push('/form/step-2'); 43 | // } else if (!stepThree) { 44 | // toast.error('Please fill step three first'); 45 | // router.push('/form/step-3'); 46 | // } 47 | // }, [router, stepOne, stepThree, stepTwo]); 48 | 49 | //#region //? forms ================================== 50 | const methods = useForm({ 51 | mode: 'onTouched', 52 | defaultValues: { ...stepOne, ...stepTwo, ...stepThree }, 53 | }); 54 | const { handleSubmit } = methods; 55 | //#endregion forms 56 | 57 | //#region //? action ================================== 58 | const onSubmit = (data: FormData) => { 59 | // eslint-disable-next-line no-console 60 | console.log(data); 61 | }; 62 | //#endregion action 63 | 64 | return ( 65 | <> 66 | 67 | 68 |
    69 |
    70 |
    71 |

    Recap

    72 |
    73 | 77 | 78 | 79 | 83 | 84 | 85 |
    86 | 87 | 88 |
    92 |
    93 |

    94 | 98 | Step 1 99 | 100 |

    101 | 102 | 103 | 104 | 105 | 111 |
    112 | 113 |
    114 |

    115 | 119 | Step 2 120 | 121 |

    122 | 123 | 124 | 125 | 126 | 131 |

    132 | File preview in progress 133 |

    134 |
    135 | 136 |
    137 |

    138 | 142 | Step 3 143 | 144 |

    145 | 146 | 155 |
    156 |
    157 | 158 | 159 |
    160 |
    161 | 162 |
    163 |
    164 |
    165 | 166 | 175 | View JSON 176 | 177 |
    178 |
    179 |
    180 |
    181 |
    182 | 183 | ); 184 | } 185 | -------------------------------------------------------------------------------- /pages/form/step-1.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import { FormProvider, useForm } from 'react-hook-form'; 3 | import { yupResolver } from '@hookform/resolvers/yup'; 4 | import { HiChevronLeft, HiChevronRight } from 'react-icons/hi'; 5 | 6 | import useFormStore from '@/store/useFormStore'; 7 | import { stepOneSchema } from '@/lib/yup'; 8 | 9 | import { StepOneData } from '@/types'; 10 | 11 | import Seo from '@/components/Seo'; 12 | import Button from '@/components/Button'; 13 | import UnstyledLink from '@/components/UnstyledLink'; 14 | import Input from '@/components/Forms/Input'; 15 | import PasswordInput from '@/components/Forms/PasswordInput'; 16 | 17 | export default function StepOnePage() { 18 | const router = useRouter(); 19 | 20 | const { stepOne, setData } = useFormStore(); 21 | 22 | //#region //? forms ================================== 23 | const methods = useForm({ 24 | mode: 'onTouched', 25 | resolver: yupResolver(stepOneSchema), 26 | defaultValues: stepOne || {}, 27 | }); 28 | const { handleSubmit } = methods; 29 | //#endregion forms 30 | 31 | //#region //? action ================================== 32 | const onSubmit = (data: StepOneData) => { 33 | // eslint-disable-next-line no-console 34 | console.log(data); 35 | setData({ step: 1, data }); 36 | router.push('/form/step-2'); 37 | }; 38 | //#endregion action 39 | 40 | return ( 41 | <> 42 | 43 | 44 |
    45 |
    46 |
    47 |

    Step 1

    48 |
    49 | 53 | 54 | 55 | 59 | 60 | 61 |
    62 | 63 | 64 |
    68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |
    77 |
    78 |
    79 |
    80 | 81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /pages/form/step-2.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import toast from 'react-hot-toast'; 3 | import { useRouter } from 'next/router'; 4 | import { FormProvider, useForm } from 'react-hook-form'; 5 | import { yupResolver } from '@hookform/resolvers/yup'; 6 | import { HiChevronLeft, HiChevronRight } from 'react-icons/hi'; 7 | 8 | import useFormStore from '@/store/useFormStore'; 9 | import { stepTwoSchema } from '@/lib/yup'; 10 | 11 | import { StepTwoData } from '@/types'; 12 | 13 | import Seo from '@/components/Seo'; 14 | import Input from '@/components/Forms/Input'; 15 | import Button from '@/components/Button'; 16 | import UnstyledLink from '@/components/UnstyledLink'; 17 | import DropzoneInput from '@/components/Forms/DropzoneInput'; 18 | 19 | export default function StepTwoPage() { 20 | const router = useRouter(); 21 | 22 | const { stepOne, stepTwo, setData } = useFormStore(); 23 | 24 | // useEffect(() => { 25 | // if (!stepOne) { 26 | // toast.error('Please fill step one first'); 27 | // router.push('/form/step-1'); 28 | // } 29 | // }, [router, stepOne]); 30 | 31 | //#region //? forms ================================== 32 | const methods = useForm({ 33 | mode: 'onTouched', 34 | resolver: yupResolver(stepTwoSchema), 35 | defaultValues: stepTwo || {}, 36 | }); 37 | const { handleSubmit } = methods; 38 | //#endregion forms 39 | 40 | //#region //? action ================================== 41 | const onSubmit = (data: StepTwoData) => { 42 | // eslint-disable-next-line no-console 43 | console.log(data); 44 | setData({ step: 2, data }); 45 | 46 | router.push('/form/step-3'); 47 | }; 48 | //#endregion action 49 | 50 | return ( 51 | <> 52 | 53 | 54 |
    55 |
    56 |
    57 |

    Step 2

    58 |
    59 | 63 | 64 | 65 | 69 | 70 | 71 |
    72 | 73 | 74 |
    78 | 79 | 80 | 81 | 88 | 95 | 96 | 97 | 98 |
    99 |
    100 |
    101 |
    102 | 103 | ); 104 | } 105 | -------------------------------------------------------------------------------- /pages/form/step-3.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import dynamic from 'next/dynamic'; 3 | import toast from 'react-hot-toast'; 4 | import { useRouter } from 'next/router'; 5 | import { FormProvider, useForm } from 'react-hook-form'; 6 | import { yupResolver } from '@hookform/resolvers/yup'; 7 | import { HiChevronLeft, HiChevronRight } from 'react-icons/hi'; 8 | 9 | import useFormStore from '@/store/useFormStore'; 10 | import { stepThreeSchema } from '@/lib/yup'; 11 | 12 | import { StepThreeData } from '@/types'; 13 | 14 | import Seo from '@/components/Seo'; 15 | import Button from '@/components/Button'; 16 | import UnstyledLink from '@/components/UnstyledLink'; 17 | import Input from '@/components/Forms/Input'; 18 | import DatePicker from '@/components/Forms/DatePicker'; 19 | import Select from '@/components/Forms/Select'; 20 | // @ts-ignore 21 | const CustomMap = dynamic(() => import('@/components/CustomMap'), { 22 | ssr: false, 23 | }); 24 | 25 | export default function StepThreePage() { 26 | const router = useRouter(); 27 | 28 | const { stepOne, stepTwo, stepThree, setData } = useFormStore(); 29 | 30 | // useEffect(() => { 31 | // if (!stepOne) { 32 | // toast.error('Please fill step one first'); 33 | // router.push('/form/step-1'); 34 | // } else if (!stepTwo) { 35 | // toast.error('Please fill step two first'); 36 | // router.push('/form/step-2'); 37 | // } 38 | // }, [router, stepOne, stepTwo]); 39 | 40 | //#region //? forms ================================== 41 | const methods = useForm({ 42 | mode: 'onTouched', 43 | resolver: yupResolver(stepThreeSchema), 44 | defaultValues: stepThree || { 45 | lat: -6.1754, 46 | lng: 106.8272, 47 | }, 48 | }); 49 | const { handleSubmit } = methods; 50 | //#endregion forms 51 | 52 | //#region //? action ================================== 53 | const onSubmit = (data: StepThreeData) => { 54 | // eslint-disable-next-line no-console 55 | console.log(data); 56 | setData({ step: 3, data }); 57 | router.push('/form/recap'); 58 | }; 59 | //#endregion action 60 | 61 | return ( 62 | <> 63 | 64 | 65 |
    66 |
    67 |
    68 |

    Step 3

    69 |
    70 | 74 | 75 | 76 | 80 | 81 | 82 |
    83 | 84 | 85 |
    89 | 94 | 98 | 99 |
    100 | 101 | 102 |
    103 |
    104 | 105 |
    106 | 107 | 108 | 109 |
    110 |
    111 |
    112 |
    113 | 114 | ); 115 | } 116 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Seo from '@/components/Seo'; 2 | import CustomLink from '@/components/CustomLink'; 3 | 4 | export default function Home() { 5 | return ( 6 | <> 7 | 8 | 9 |
    10 |
    11 |
    12 |

    Step Form Example

    13 |

    14 | Built using React Hook Form, Yup, Typescript, and Zustand 15 |

    16 |

    17 | 18 | See the repository 19 | 20 |

    21 | 22 | 23 | Go to form → 24 | 25 | 26 |
    27 | © {new Date().getFullYear()} By{' '} 28 | 29 | Theodorus Clarence 30 | {' '} 31 | &{' '} 32 | 33 | Muhammad Rizqi Tsani 34 | 35 |
    36 |
    37 |
    38 |
    39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /pages/map.tsx: -------------------------------------------------------------------------------- 1 | import dynamic from 'next/dynamic'; 2 | import { FormProvider, useForm } from 'react-hook-form'; 3 | import { yupResolver } from '@hookform/resolvers/yup'; 4 | 5 | import { mapSchema } from '@/lib/yup'; 6 | 7 | import Seo from '@/components/Seo'; 8 | import Input from '@/components/Forms/Input'; 9 | import LoadingMap from '@/components/CustomMap/LoadingMap'; 10 | import clsx from 'clsx'; 11 | // @ts-ignore 12 | const CustomMap = dynamic(() => import('@/components/CustomMap'), { 13 | ssr: false, 14 | loading: LoadingMap, 15 | }); 16 | // @ts-ignore 17 | const ReadOnlyMap = dynamic( 18 | () => import('@/components/CustomMap/ReadOnlyMap'), 19 | { 20 | ssr: false, 21 | loading: LoadingMap, 22 | } 23 | ); 24 | 25 | export default function MapPage() { 26 | //#region //? forms ================================== 27 | const methods = useForm({ 28 | mode: 'onTouched', 29 | defaultValues: { 30 | lat: -6.1754, 31 | lng: 106.8272, 32 | }, 33 | resolver: yupResolver(mapSchema), 34 | }); 35 | const { handleSubmit, watch } = methods; 36 | const lat = watch('lat'); 37 | const lng = watch('lng'); 38 | //#endregion forms 39 | 40 | //#region //? action ================================== 41 | const onSubmit = (data: { lat: number; lng: number }) => { 42 | // eslint-disable-next-line no-console 43 | console.log(data); 44 | }; 45 | //#endregion action 46 | return ( 47 | <> 48 | 49 | 50 |
    51 |
    52 |
    53 | 54 |
    58 |
    59 |

    Map Component

    60 |
    61 | 62 | 63 |
    64 |
    65 | 66 |
    67 |
    68 | 69 |
    70 |

    Read Only

    71 |
    72 |
    73 |

    74 | Lat 75 |

    76 |
    77 |
    83 | {lat} 84 |
    85 |
    86 |
    87 |
    88 |

    89 | Long 90 |

    91 |
    92 |
    98 | {lng} 99 |
    100 |
    101 |
    102 |
    103 |
    104 | 105 |
    106 |
    107 |
    108 |
    109 |
    110 |
    111 |
    112 | 113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /pages/recap-json.tsx: -------------------------------------------------------------------------------- 1 | import useFormStore from '@/store/useFormStore'; 2 | 3 | import Seo from '@/components/Seo'; 4 | import CustomLink from '@/components/CustomLink'; 5 | 6 | export default function RecapJSON() { 7 | const { stepOne, stepTwo, stepThree } = useFormStore(); 8 | 9 | return ( 10 | <> 11 | 12 | 13 |
    14 |
    15 |
    16 |

    Recap JSON

    17 | 18 | ← Back to recap 19 | 20 |
    21 |               {JSON.stringify({ stepOne, stepTwo, stepThree }, null, 2)}
    22 |             
    23 |
    24 |
    25 |
    26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/favicon/android-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/favicon/android-icon-144x144.png -------------------------------------------------------------------------------- /public/favicon/android-icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/favicon/android-icon-192x192.png -------------------------------------------------------------------------------- /public/favicon/android-icon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/favicon/android-icon-36x36.png -------------------------------------------------------------------------------- /public/favicon/android-icon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/favicon/android-icon-48x48.png -------------------------------------------------------------------------------- /public/favicon/android-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/favicon/android-icon-72x72.png -------------------------------------------------------------------------------- /public/favicon/android-icon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/favicon/android-icon-96x96.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/favicon/apple-icon-114x114.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/favicon/apple-icon-120x120.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/favicon/apple-icon-144x144.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/favicon/apple-icon-152x152.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/favicon/apple-icon-180x180.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/favicon/apple-icon-57x57.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/favicon/apple-icon-60x60.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/favicon/apple-icon-72x72.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/favicon/apple-icon-76x76.png -------------------------------------------------------------------------------- /public/favicon/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/favicon/apple-icon-precomposed.png -------------------------------------------------------------------------------- /public/favicon/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/favicon/apple-icon.png -------------------------------------------------------------------------------- /public/favicon/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #ffffff -------------------------------------------------------------------------------- /public/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/favicon/favicon-96x96.png -------------------------------------------------------------------------------- /public/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/favicon/favicon.ico -------------------------------------------------------------------------------- /public/favicon/large-og.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/favicon/large-og.jpg -------------------------------------------------------------------------------- /public/favicon/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "App", 3 | "icons": [ 4 | { 5 | "src": "\/android-icon-36x36.png", 6 | "sizes": "36x36", 7 | "type": "image\/png", 8 | "density": "0.75" 9 | }, 10 | { 11 | "src": "\/android-icon-48x48.png", 12 | "sizes": "48x48", 13 | "type": "image\/png", 14 | "density": "1.0" 15 | }, 16 | { 17 | "src": "\/android-icon-72x72.png", 18 | "sizes": "72x72", 19 | "type": "image\/png", 20 | "density": "1.5" 21 | }, 22 | { 23 | "src": "\/android-icon-96x96.png", 24 | "sizes": "96x96", 25 | "type": "image\/png", 26 | "density": "2.0" 27 | }, 28 | { 29 | "src": "\/android-icon-144x144.png", 30 | "sizes": "144x144", 31 | "type": "image\/png", 32 | "density": "3.0" 33 | }, 34 | { 35 | "src": "\/android-icon-192x192.png", 36 | "sizes": "192x192", 37 | "type": "image\/png", 38 | "density": "4.0" 39 | } 40 | ] 41 | } -------------------------------------------------------------------------------- /public/favicon/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/favicon/ms-icon-144x144.png -------------------------------------------------------------------------------- /public/favicon/ms-icon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/favicon/ms-icon-150x150.png -------------------------------------------------------------------------------- /public/favicon/ms-icon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/favicon/ms-icon-310x310.png -------------------------------------------------------------------------------- /public/favicon/ms-icon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/favicon/ms-icon-70x70.png -------------------------------------------------------------------------------- /public/fonts/inter-var-latin.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/fonts/inter-var-latin.woff2 -------------------------------------------------------------------------------- /public/images/leaflet/layers-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/images/leaflet/layers-2x.png -------------------------------------------------------------------------------- /public/images/leaflet/layers.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/images/leaflet/layers.png -------------------------------------------------------------------------------- /public/images/leaflet/marker-icon-2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/images/leaflet/marker-icon-2x.png -------------------------------------------------------------------------------- /public/images/leaflet/marker-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/images/leaflet/marker-icon.png -------------------------------------------------------------------------------- /public/images/leaflet/marker-shadow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/theodorusclarence/rhf-stepform/df57d06b024b1240148af259a47c1026f2d68c68/public/images/leaflet/marker-shadow.png -------------------------------------------------------------------------------- /store/useFormStore.tsx: -------------------------------------------------------------------------------- 1 | import create from 'zustand'; 2 | import { devtools } from 'zustand/middleware'; 3 | 4 | import { StepOneData, StepThreeData, StepTwoData } from '@/types'; 5 | 6 | const stepVariant = { 7 | 1: 'stepOne', 8 | 2: 'stepTwo', 9 | 3: 'stepThree', 10 | }; 11 | 12 | type setDataType = 13 | | { step: 1; data: StepOneData } 14 | | { step: 2; data: StepTwoData } 15 | | { step: 3; data: StepThreeData }; 16 | 17 | const useFormStore = create<{ 18 | stepOne: StepOneData | null; 19 | stepTwo: StepTwoData | null; 20 | stepThree: StepThreeData | null; 21 | setData: ({ step, data }: setDataType) => void; 22 | }>( 23 | devtools((set) => ({ 24 | stepOne: null, 25 | stepTwo: null, 26 | stepThree: null, 27 | setData: ({ step, data }) => 28 | set((state) => ({ 29 | ...state, 30 | [stepVariant[step]]: data, 31 | })), 32 | })) 33 | ); 34 | 35 | export default useFormStore; 36 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | /* inter var - latin */ 7 | @font-face { 8 | font-family: 'Inter'; 9 | font-style: normal; 10 | font-weight: 100 900; 11 | font-display: optional; 12 | src: url('/fonts/inter-var-latin.woff2') format('woff2'); 13 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 14 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, 15 | U+2215, U+FEFF, U+FFFD; 16 | } 17 | 18 | /* Write your own custom base styles here */ 19 | h1 { 20 | @apply text-3xl font-bold md:text-5xl font-primary; 21 | } 22 | 23 | h2 { 24 | @apply text-2xl font-bold md:text-4xl font-primary; 25 | } 26 | 27 | h3 { 28 | @apply text-xl font-bold md:text-3xl font-primary; 29 | } 30 | 31 | h4 { 32 | @apply text-lg font-bold font-primary; 33 | } 34 | 35 | body { 36 | @apply text-sm font-primary md:text-base; 37 | } 38 | 39 | .layout { 40 | /* 750px */ 41 | /* max-width: 43.75rem; */ 42 | 43 | /* 1100px */ 44 | max-width: 68.75rem; 45 | @apply w-11/12 mx-auto; 46 | } 47 | } 48 | 49 | @layer utilities { 50 | .animated-underline { 51 | background-image: linear-gradient(#33333300, #33333300), 52 | linear-gradient(to right, #00e0f3, #00c4fd); 53 | background-size: 100% 2px, 0 2px; 54 | background-position: 100% 100%, 0 100%; 55 | background-repeat: no-repeat; 56 | transition: background-size 0.3s ease; 57 | } 58 | .animated-underline:hover, 59 | .animated-underline:focus { 60 | background-size: 0 2px, 100% 2px; 61 | } 62 | } 63 | 64 | /* React Datepicker Reset Style */ 65 | .react-datepicker-wrapper { 66 | display: block; 67 | width: 100%; 68 | } 69 | 70 | .react-datepicker__navigation.react-datepicker__navigation--previous, 71 | .react-datepicker__navigation.react-datepicker__navigation--next { 72 | top: 6px; 73 | } 74 | 75 | .react-datepicker__header__dropdown.react-datepicker__header__dropdown--select { 76 | padding: 0 5px; 77 | } 78 | 79 | .react-datepicker__header__dropdown { 80 | margin-top: 0.5rem; 81 | } 82 | 83 | .react-datepicker__year-select, 84 | .react-datepicker__month-select { 85 | padding-top: 0.2rem; 86 | padding-bottom: 0.2rem; 87 | padding-left: 0.7rem; 88 | border-radius: 0.25rem; 89 | } 90 | 91 | .react-datepicker-popper { 92 | z-index: 99999 !important; 93 | } 94 | 95 | /* Leaflet */ 96 | .leaflet-control-geosearch.leaflet-geosearch-bar form { 97 | padding: 0; 98 | } 99 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | const { fontFamily } = require('tailwindcss/defaultTheme'); 2 | 3 | module.exports = { 4 | mode: 'jit', 5 | purge: ['./pages/**/*.{js,jsx,ts,tsx}', './components/**/*.{js,jsx,ts,tsx}'], 6 | darkMode: false, // or 'media' or 'class' 7 | theme: { 8 | extend: { 9 | fontFamily: { 10 | primary: ['Inter', ...fontFamily.sans], 11 | }, 12 | colors: { 13 | primary: { 14 | 400: '#00E0F3', 15 | 500: '#00c4fd', 16 | }, 17 | dark: '#222222', 18 | }, 19 | }, 20 | }, 21 | variants: { 22 | extend: {}, 23 | }, 24 | plugins: [require('@tailwindcss/forms')], 25 | }; 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "baseUrl": ".", 17 | "paths": { 18 | "@/*": ["*"] 19 | } 20 | }, 21 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 22 | "exclude": ["node_modules"], 23 | "moduleResolution": ["node_modules", ".next", "node"] 24 | } 25 | -------------------------------------------------------------------------------- /types.ts: -------------------------------------------------------------------------------- 1 | import { FileWithPath } from 'react-dropzone'; 2 | 3 | export type StepOneData = { 4 | name: string; 5 | email: string; 6 | password: string; 7 | age: number; 8 | phone: string; 9 | }; 10 | 11 | export type StepTwoData = { 12 | score_1: number; 13 | score_2: number; 14 | score_3: number; 15 | score_file: FileWithPreview[]; 16 | identity_card: FileWithPreview[]; 17 | }; 18 | 19 | export type StepThreeData = { 20 | birth_date: Date; 21 | gender: string; 22 | lat: number; 23 | lng: number; 24 | }; 25 | 26 | export type FormData = StepOneData & StepTwoData & StepThreeData; 27 | 28 | export type LatLong = { 29 | lat: number; 30 | lng: number; 31 | }; 32 | 33 | export type FileWithPreview = FileWithPath & { preview: string }; 34 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "headers": [ 3 | { 4 | "source": "/fonts/inter-var-latin.woff2", 5 | "headers": [ 6 | { 7 | "key": "Cache-Control", 8 | "value": "public, max-age=31536000, immutable" 9 | } 10 | ] 11 | } 12 | ] 13 | } 14 | --------------------------------------------------------------------------------