├── .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 | [](#contributors-)
4 |
5 |
6 | 
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 |
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 |
55 | {file.name}
56 |
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 |
103 | {file.name}
104 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------