Skip to content

Commit

Permalink
feat(form): form input components
Browse files Browse the repository at this point in the history
  • Loading branch information
faisalEsMagico committed Oct 10, 2023
1 parent b86a760 commit 8706e49
Show file tree
Hide file tree
Showing 13 changed files with 26,164 additions and 5,508 deletions.
19,986 changes: 19,986 additions & 0 deletions package-lock.json

Large diffs are not rendered by default.

98 changes: 98 additions & 0 deletions src/components/forms/DatePicker.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import clsx from 'clsx';
import get from 'lodash.get';

Check failure on line 2 in src/components/forms/DatePicker.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint, ʦ TypeScript, 💅 Prettier, and 🃏 Test

Cannot find module 'lodash.get' or its corresponding type declarations.
import ReactDatePicker, { ReactDatePickerProps } from 'react-datepicker';

Check failure on line 3 in src/components/forms/DatePicker.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint, ʦ TypeScript, 💅 Prettier, and 🃏 Test

Cannot find module 'react-datepicker' or its corresponding type declarations.
import { Controller, RegisterOptions, useFormContext } from 'react-hook-form';

Check failure on line 4 in src/components/forms/DatePicker.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint, ʦ TypeScript, 💅 Prettier, and 🃏 Test

Cannot find module 'react-hook-form' or its corresponding type declarations.
import { HiOutlineCalendar } from 'react-icons/hi';

import 'react-datepicker/dist/react-datepicker.css';

type DatePickerProps = {
validation?: RegisterOptions;
label: string;
id: string;
placeholder?: string;
defaultYear?: number;
defaultMonth?: number;
defaultValue?: string;
helperText?: string;
readOnly?: boolean;
} & Omit<ReactDatePickerProps, 'onChange'>;

export default function DatePicker({
validation,
label,
id,
placeholder,
defaultYear,
defaultMonth,
defaultValue,
helperText,
readOnly = false,
...rest
}: DatePickerProps) {
const {
formState: { errors },
control,
} = useFormContext();
const error = get(errors, id);

// If there is a year default, then change the year to the props
const defaultDate = new Date();
if (defaultYear) defaultDate.setFullYear(defaultYear);
if (defaultMonth) defaultDate.setMonth(defaultMonth);

return (
<div className='relative'>
<label htmlFor={id} className='block text-sm font-normal text-gray-700'>
{label}
</label>

<Controller
control={control}
rules={validation}
defaultValue={defaultValue}
name={id}
render={({ field: { onChange, onBlur, value } }) => (

Check failure on line 55 in src/components/forms/DatePicker.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint, ʦ TypeScript, 💅 Prettier, and 🃏 Test

Binding element 'onChange' implicitly has an 'any' type.

Check failure on line 55 in src/components/forms/DatePicker.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint, ʦ TypeScript, 💅 Prettier, and 🃏 Test

Binding element 'onBlur' implicitly has an 'any' type.

Check failure on line 55 in src/components/forms/DatePicker.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint, ʦ TypeScript, 💅 Prettier, and 🃏 Test

Binding element 'value' implicitly has an 'any' type.
<>
<div className='relative mt-1'>
<ReactDatePicker
name={id}
onChange={onChange}
onBlur={onBlur}
selected={value ? new Date(value) : undefined}
className={clsx(
readOnly
? 'cursor-not-allowed border-gray-300 bg-gray-100 focus:border-gray-300 focus:ring-0'
: error
? 'border-red-500 focus:border-red-500 focus:ring-red-500'
: 'focus:border-primary-500 focus:ring-primary-500 border-gray-300',
'block w-full rounded-md shadow-sm'
)}
placeholderText={placeholder}
aria-describedby={id}
showMonthDropdown
showYearDropdown
dropdownMode='select'
openToDate={value ? new Date(value) : defaultDate}
dateFormat='dd/MM/yyyy'
readOnly={readOnly}
{...rest}
/>
<HiOutlineCalendar className='pointer-events-none absolute right-4 top-1/2 -translate-y-1/2 transform text-lg text-gray-500' />
</div>
<div className='mt-1'>
{helperText !== '' && (
<p className='text-xs text-gray-500'>{helperText}</p>
)}
{error && (
<span className='text-sm text-red-500'>
{error.message?.toString()}
</span>
)}
</div>
</>
)}
/>
</div>
);
}
224 changes: 224 additions & 0 deletions src/components/forms/DropzoneInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
import clsx from 'clsx';
import get from 'lodash.get';

Check failure on line 2 in src/components/forms/DropzoneInput.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint, ʦ TypeScript, 💅 Prettier, and 🃏 Test

Cannot find module 'lodash.get' or its corresponding type declarations.
import * as React from 'react';
import { Accept, FileRejection, useDropzone } from 'react-dropzone';

Check failure on line 4 in src/components/forms/DropzoneInput.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint, ʦ TypeScript, 💅 Prettier, and 🃏 Test

Cannot find module 'react-dropzone' or its corresponding type declarations.
import { Controller, useFormContext } from 'react-hook-form';

Check failure on line 5 in src/components/forms/DropzoneInput.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint, ʦ TypeScript, 💅 Prettier, and 🃏 Test

Cannot find module 'react-hook-form' or its corresponding type declarations.

import FilePreview from '@/components/forms/FilePreview';

import { FileWithPreview } from '@/types/dropzone';

type DropzoneInputProps = {
accept?: Accept;
helperText?: string;
id: string;
label: string;
maxFiles?: number;
readOnly?: boolean;
validation?: Record<string, unknown>;
};

export default function DropzoneInput({
accept,
helperText = '',
id,
label,
maxFiles = 1,
validation,
readOnly,
}: DropzoneInputProps) {
const {
control,
getValues,
setValue,
setError,
clearErrors,
formState: { errors },
} = useFormContext();
const error = get(errors, id);

//#region //*=========== Error Focus ===========
const dropzoneRef = React.useRef<HTMLDivElement>(null);

React.useEffect(() => {
error && dropzoneRef.current?.focus();
}, [error]);
//#endregion //*======== Error Focus ===========

const [files, setFiles] = React.useState<FileWithPreview[]>(
getValues(id) || []
);

const onDrop = React.useCallback(
<T extends File>(acceptedFiles: T[], rejectedFiles: FileRejection[]) => {
if (rejectedFiles && rejectedFiles.length > 0) {
setValue(id, files ? [...files] : null);
setError(id, {
type: 'manual',
message: rejectedFiles && rejectedFiles[0].errors[0].message,
});
} else {
const acceptedFilesPreview = acceptedFiles.map((file: T) =>
Object.assign(file, {
preview: URL.createObjectURL(file),
})
);

setFiles(
files
? [...files, ...acceptedFilesPreview].slice(0, maxFiles)
: acceptedFilesPreview
);

setValue(
id,
files
? [...files, ...acceptedFiles].slice(0, maxFiles)
: acceptedFiles,
{
shouldValidate: true,
}
);
clearErrors(id);
}
},
[clearErrors, files, id, maxFiles, setError, setValue]
);

React.useEffect(() => {
return () => {
() => {
files.forEach((file) => URL.revokeObjectURL(file.preview));
};
};
}, [files]);

const deleteFile = (
e: React.MouseEvent<HTMLButtonElement, MouseEvent>,
file: FileWithPreview
) => {
e.preventDefault();
const newFiles = [...files];

newFiles.splice(newFiles.indexOf(file), 1);

if (newFiles.length > 0) {
setFiles(newFiles);
setValue(id, newFiles, {
shouldValidate: true,
shouldDirty: true,
shouldTouch: true,
});
} else {
setFiles([]);
setValue(id, null, {
shouldValidate: true,
shouldDirty: true,
shouldTouch: true,
});
}
};

const { getRootProps, getInputProps } = useDropzone({
onDrop,
accept,
maxFiles,
maxSize: 1000000,
});

return (
<div>
<label className='block text-sm font-normal text-gray-700' htmlFor={id}>
{label}
</label>

{readOnly && !(files?.length > 0) ? (
<div className='divide-y divide-gray-300 rounded-md border border-gray-300 py-3 pl-3 pr-4 text-sm'>
No file uploaded
</div>
) : files?.length >= maxFiles ? (
<ul className='mt-1 divide-y divide-gray-300 rounded-md border border-gray-300'>
{files.map((file, index) => (
<FilePreview
key={index}
readOnly={readOnly}
file={file}
deleteFile={deleteFile}
/>
))}
</ul>
) : (
<Controller
control={control}
name={id}
rules={validation}
render={({ field }) => (

Check failure on line 155 in src/components/forms/DropzoneInput.tsx

View workflow job for this annotation

GitHub Actions / ⬣ ESLint, ʦ TypeScript, 💅 Prettier, and 🃏 Test

Binding element 'field' implicitly has an 'any' type.
<>
<div
className='focus:ring-dark-400 group mt-1 focus:outline-none'
{...getRootProps()}
ref={dropzoneRef}
>
<input {...field} {...getInputProps()} />
<div
className={clsx(
'w-full cursor-pointer rounded border-2 border-dashed border-gray-300 px-2 py-8',
error
? 'border-red-500 group-focus:border-red-500'
: 'group-focus:border-primary-500'
)}
>
<div className='space-y-1 text-center'>
<svg
className='mx-auto h-12 w-12 text-gray-400'
stroke='currentColor'
fill='none'
viewBox='0 0 48 48'
aria-hidden='true'
>
<path
d='M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02'
strokeWidth={2}
strokeLinecap='round'
strokeLinejoin='round'
/>
</svg>
<p className='text-gray-500'>
Drag and drop file here, or click to choose file
</p>
<p className='text-xs text-gray-500'>{`${
maxFiles - (files?.length || 0)
} file(s) remaining`}</p>
</div>
</div>
</div>

<div className='mt-1'>
{helperText !== '' && (
<p className='text-xs text-gray-500'>{helperText}</p>
)}
{error && (
<p className='text-sm text-red-500'>
{error.message?.toString()}
</p>
)}
</div>
{!readOnly && !!files?.length && (
<ul className='mt-1 divide-y divide-gray-300 rounded-md border border-gray-300'>
{files.map((file, index) => (
<FilePreview
key={index}
readOnly={readOnly}
file={file}
deleteFile={deleteFile}
/>
))}
</ul>
)}
</>
)}
/>
)}
</div>
);
}
23 changes: 23 additions & 0 deletions src/components/forms/ErrorMessage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as React from 'react';
import { get, useFormState } from 'react-hook-form';

import clsxm from '@/lib/clsxm';

type ErrorMessageProps = {
id: string;
} & React.ComponentPropsWithoutRef<'p'>;

export default function ErrorMessage({
id,
className,
...rest
}: ErrorMessageProps) {
const { errors } = useFormState();
const error = get(errors, id);

return (
<p className={clsxm('text-sm text-red-500', className)} {...rest}>
{error.message?.toString()}
</p>
);
}
Loading

0 comments on commit 8706e49

Please sign in to comment.