From 2333c9dbb252962786338c3cf18ac597ebb6a755 Mon Sep 17 00:00:00 2001 From: Mohammed Faisal Hussain Date: Thu, 19 Oct 2023 20:17:52 +0530 Subject: [PATCH] feat: add reusable upload component to storybook (#1095) --- .../feedback-widget/FeedbackWidget.tsx | 217 ++---------- .../components/upload/Upload.stories.tsx | 113 ++++++ dashboard/components/upload/Upload.tsx | 324 ++++++++++++++++++ 3 files changed, 466 insertions(+), 188 deletions(-) create mode 100644 dashboard/components/upload/Upload.stories.tsx create mode 100644 dashboard/components/upload/Upload.tsx diff --git a/dashboard/components/feedback-widget/FeedbackWidget.tsx b/dashboard/components/feedback-widget/FeedbackWidget.tsx index 51bf836d4..52e7feff5 100644 --- a/dashboard/components/feedback-widget/FeedbackWidget.tsx +++ b/dashboard/components/feedback-widget/FeedbackWidget.tsx @@ -1,5 +1,4 @@ import { useState, useRef, useCallback, memo, SyntheticEvent } from 'react'; -import { FileUploader } from 'react-drag-drop-files'; // eslint-disable-next-line import/no-extraneous-dependencies import { toBlob } from 'html-to-image'; import Image from 'next/image'; @@ -10,6 +9,7 @@ import settingsService from '@services/settingsService'; import Button from '@components/button/Button'; import useToast from '@components/toast/hooks/useToast'; import Toast from '@components/toast/Toast'; +import Upload from '@components/upload/Upload'; // We define the placeholder here for convenience // It's difficult to read when passed inline @@ -29,9 +29,7 @@ Outcome const useFeedbackWidget = (defaultState: boolean = false) => { const [showFeedbackModel, setShowFeedbackModal] = useState(defaultState); - const FILE_TYPES = ['JPG', 'PNG', 'GIF', 'TXT', 'LOG', 'MP4', 'AVI', 'MOV']; const FEEDBACK_MODAL_ID = 'feedback-modal'; - const MAX_FILE_SIZE_MB = 37; function openFeedbackModal() { setShowFeedbackModal(true); @@ -151,7 +149,7 @@ const useFeedbackWidget = (defaultState: boolean = false) => { } } - function uploadFile(attachement: File) { + function uploadFile(attachement: File | null): void { setFileAttachement(attachement); } @@ -198,7 +196,7 @@ const useFeedbackWidget = (defaultState: boolean = false) => { <>
takeScreenshot()} - className="w-[50%] grow cursor-pointer rounded border-2 border-black-170 py-5 text-center text-xs transition hover:border-[#B6EAEA] hover:bg-black-100" + className="flex-1 grow cursor-pointer rounded border-2 border-black-170 py-5 text-center text-xs transition hover:border-[#B6EAEA] hover:bg-black-100" > { )} - - setToast({ - hasError: true, - title: 'File upload failed', - message: err - }) - } - onSizeError={(err: string) => - setToast({ - hasError: true, - title: 'File upload failed', - message: err - }) - } - dropMessageStyle={{ - width: '100%', - height: '100%', - position: 'absolute', - background: '#F4F9F9', - top: 0, - right: 2, - display: 'flex', - flexGrow: 2, - opacity: 1, - zIndex: 20, - color: '#008484', - fontSize: 14, - border: 'none' - }} - > - {fileAttachement === null && ( -
- - - - - - -

- Drag and drop or{' '} - - choose a file - -

-
- )} - {fileAttachement !== null && ( -
-
- - - - - - -
-

{fileAttachement.name}

-

- {(fileAttachement.size / (1024 * 1024)).toFixed( - 2 - )} - MB -

-
-
- { - e.preventDefault(); - if (!isSendingFeedback && !isTakingScreenCapture) - setFileAttachement(null); - return false; - }} - className="absolute right-4 top-4 block h-4 w-4 cursor-pointer" - aria-disabled={ - isTakingScreenCapture || isSendingFeedback - } - > - - - - - -
- )} - +
+ setFileAttachement(null)} + disabled={ + fileAttachement !== null || + isSendingFeedback || + isTakingScreenCapture + } + onTypeError={(err: string) => + setToast({ + hasError: true, + title: 'File upload failed', + message: err + }) + } + onSizeError={(err: string) => + setToast({ + hasError: true, + title: 'File upload failed', + message: err + }) + } + /> +
diff --git a/dashboard/components/upload/Upload.stories.tsx b/dashboard/components/upload/Upload.stories.tsx new file mode 100644 index 000000000..ebd7597f2 --- /dev/null +++ b/dashboard/components/upload/Upload.stories.tsx @@ -0,0 +1,113 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { useEffect, useState } from 'react'; +import Upload, { UploadProps } from './Upload'; + +function UploadWrapper({ + multiple, + fileOrFiles, + handleChange, + onClose, + ...otherProps +}: UploadProps) { + const [selectedFile, setSelectedFile] = useState(null); + + useEffect(() => { + setSelectedFile(null); + }, [multiple]); + + const uploadFile = (file: File | File[] | null): void => { + if (file instanceof FileList) { + const filesArray = Array.from(file); + setSelectedFile(filesArray); + } else if (file instanceof File) { + setSelectedFile(file); + } else { + setSelectedFile(null); + } + }; + + return ( + setSelectedFile(null)} + {...otherProps} + /> + ); +} + +const meta: Meta = { + title: 'Komiser/FileUpload', + component: UploadWrapper, + decorators: [ + Story => ( +
{Story()}
+ ) + ], + tags: ['autodocs'], + argTypes: { + name: { + control: 'text', + description: 'the name for your form (if exist)', + defaultValue: 'attachment' + }, + multiple: { + control: 'boolean', + description: + 'a boolean to determine whether the multiple files is enabled or not', + defaultValue: false + }, + disabled: { + control: 'boolean', + description: 'disables the input', + defaultValue: false + }, + required: { + control: 'boolean', + description: 'Conditionally set the input field as required', + defaultValue: false + }, + hoverTitle: { + control: 'text', + description: 'text appears(hover) when trying to drop a file', + defaultValue: 'drop here' + }, + maxSize: { + control: 'number', + description: 'the maximum size of the file (number in mb)', + defaultValue: 37 + }, + minSize: { + control: 'number', + description: 'the minimum size of the file (number in mb)', + defaultValue: 0 + } + } +}; + +export default meta; + +type Story = StoryObj; + +export const SingleFile: Story = { + args: { + name: 'attachment', + disabled: false, + hoverTitle: 'drop here', + maxSize: 37, + minSize: 0 + } +}; + +export const MultipleFiles: Story = { + args: { + name: 'attachment', + multiple: true, + disabled: false, + hoverTitle: 'drop here', + maxSize: 37, + minSize: 0 + } +}; diff --git a/dashboard/components/upload/Upload.tsx b/dashboard/components/upload/Upload.tsx new file mode 100644 index 000000000..b3c8179c6 --- /dev/null +++ b/dashboard/components/upload/Upload.tsx @@ -0,0 +1,324 @@ +import React from 'react'; +import { FileUploader } from 'react-drag-drop-files'; + +type BaseUploadProps = { + name?: string; + label?: string; + required?: boolean; + disabled?: boolean; + hoverTitle?: string; + classes?: string; + childClassName?: string; + types?: string[]; + onTypeError?: (error: string) => void; + children?: any; + maxSize?: number; + minSize?: number; + onSizeError?: (error: string) => void; + onDrop?: (file: File | null) => void; + onSelect?: (file: File | null) => void; + onClose: () => void; + onDraggingStateChange?: () => void; + dropMessageStyle?: React.CSSProperties; +}; + +export type SingleUploadProps = BaseUploadProps & { + multiple?: false; + fileOrFiles: File | null; + handleChange: (file: File | null) => void; +}; + +export type MultipleUploadProps = BaseUploadProps & { + multiple: true; + fileOrFiles: File[] | null; + handleChange: (files: File[] | null) => void; +}; + +export type UploadProps = SingleUploadProps | MultipleUploadProps; + +const FILE_TYPES = ['JPG', 'PNG', 'GIF', 'TXT', 'LOG', 'MP4', 'AVI', 'MOV']; +const MAX_FILE_SIZE_MB = 37; + +const defaultDropMessageStyle: React.CSSProperties = { + width: '100%', + height: '100%', + position: 'absolute', + background: '#F4F9F9', + top: 0, + right: 2, + display: 'flex', + flexGrow: 2, + opacity: 1, + zIndex: 20, + color: '#008484', + fontSize: 14, + border: 'none' +}; + +function Upload({ + name = 'attachment', + multiple, + label, + required, + disabled, + hoverTitle = 'drop here', + fileOrFiles, + handleChange, + classes, + childClassName, + types = FILE_TYPES, + onTypeError, + maxSize = MAX_FILE_SIZE_MB, + minSize, + onSizeError, + onDrop, + onSelect, + onClose, + onDraggingStateChange, + dropMessageStyle = defaultDropMessageStyle +}: UploadProps) { + const defaultChildClassName = + fileOrFiles === null + ? `grow bg-white cursor-pointer rounded border-2 border-dashed border-black-170 py-5 text-center text-xs transition hover:border-[#B6EAEA] hover:bg-black-100 w-full` + : `grow bg-white min-h-full rounded border-2 border-[#B6EAEA] text-center text-xs transition w-full`; + + return ( + +
+ {fileOrFiles === null && ( +
+ + + + + + +

+ Drag and drop or{' '} + choose a file +

+
+ )} + {fileOrFiles !== null && ( +
+ {multiple ? ( + fileOrFiles?.map((file: File, index: number) => ( +
+ {/* Render the multiple files */} +
+ + + + + + +
+

{file.name}

+

+ {(file.size / (1024 * 1024)).toFixed(2)} + MB +

+
+
+ + + + + + +
+ )) + ) : ( +
+ {/* Render the single file */} +
+ + + + + + +
+

{fileOrFiles.name}

+

+ {(fileOrFiles.size / (1024 * 1024)).toFixed(2)} + MB +

+
+
+ + + + + + +
+ )} +
+ )} +
+
+ ); +} + +export default Upload;