FowApps/fow.ui

View on GitHub
src/components/molecules/Upload/Upload.tsx

Summary

Maintainability
C
1 day
Test Coverage
F
0%
/* eslint-disable react-hooks/exhaustive-deps */
import React, {
    useRef,
    useState,
    FormEvent,
    useEffect,
    useContext,
} from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { DefaultTheme, withTheme } from 'styled-components';

import convertBytesToKB from '../../../utils/convertBytesToKB';

import Card from '../../atoms/Card';
import Icon from '../../atoms/Icon';
import Space from '../../atoms/Space';
import Caption from '../../atoms/Typography/Caption';
import Heading from '../../atoms/Typography/Heading';
import Overline from '../../atoms/Typography/Overline';
import Subtitle from '../../atoms/Typography/Subtitle';
import Button, { ButtonProps } from '../../atoms/Button';

import {
    Wrapper,
    FileUploadContainer,
    FormField,
    Label,
    FileListContainer,
} from './styles';
import useToast from '../Toast/useToast';
import useIsMountFirstTime from '../../../hooks/useIsMountFirstTime';
// language files
import { tr } from './locales/tr';
import { en } from './locales/en';

import { ConfigContext } from '../../../theme/FowThemeProvider';

const DEFAULT_MAX_FILE_SIZE_IN_BYTES = 50000000;

export interface LocalizationType {
    placeholder?: string;
    description?: string;
    sizeError?: string;
    sizeInfo?: string;
}

export interface UploadProps {
    /**
     * label of upload field
     */
    label?: string;
    /**
     * change event
     */
    onChange?: (files: File | File[] | null) => void;
    /**
     * file types accepted
     */
    accept?: string;
    /**
     * allow multiple selection
     */
    multiple?: boolean;
    /**
     * allowed max file size in byte
     */
    maxFileSizeInBytes?: number;
    error?: any;
    /**
     * disabled flag
     */
    disabled?: any;
    /**
     * required
     */
    required?: any;
    /**
     * localization
     */
    localization?: LocalizationType;
    theme?: DefaultTheme;
    /**
     * upload button props
     */
    uploadButtonProps?: ButtonProps;
    /**
     * upload button text
     */
    uploadButtonText?: string;
    /**
     * show file list
     */
    showFileList?: boolean;
    /**
     * type of upload
     */
    type?: 'dragger' | 'button';
}

const itemVariants = {
    hidden: {
        opacity: 0,
        x: -5,
        transition: {
            duration: 0.3,
        },
    },
    show: {
        opacity: 1,
        x: 0,
        transition: {
            duration: 0.3,
        },
    },
};

const wrapperVariants = {
    hidden: {
        opacity: 0,
    },
    show: {
        opacity: 1,
        transition: {
            staggerChildren: 0.1,
        },
    },
};

const localization = {
    tr,
    en,
};

const Upload = ({
    label,
    onChange,
    accept,
    multiple = false,
    maxFileSizeInBytes = DEFAULT_MAX_FILE_SIZE_IN_BYTES,
    error,
    disabled = false,
    required = false,
    uploadButtonProps = {},
    uploadButtonText,
    showFileList = true,
    type = 'dragger',
    theme,
}: UploadProps): JSX.Element => {
    const { language } = useContext(ConfigContext);
    const fileInputField = useRef<HTMLInputElement>(null);
    const [files, setFiles] = useState<File[]>([]);
    const toast = useToast();
    const isMountFirstTime = useIsMountFirstTime();

    const addNewFiles = (newFiles: FileList) =>
        Array.from(newFiles).forEach((newFile: File) => {
            // size control
            if (newFile.size <= maxFileSizeInBytes) {
                if (multiple) {
                    // Same file control
                    if (!files.some((file) => file.name === newFile.name)) {
                        setFiles((currFiles: File[]) => [
                            ...currFiles,
                            newFile,
                        ]);
                    }
                } else {
                    setFiles([newFile]);
                }
            } else {
                toast.add(
                    `${localization[language].sizeError}(${newFile.name})` ||
                        '',
                    {
                        appearance: 'error',
                        duration: 3000,
                    },
                );
            }
        });

    const handleChange = (event: FormEvent<HTMLInputElement>) => {
        const { files: newFiles } = event.target as HTMLInputElement;
        if (newFiles?.length) {
            addNewFiles(newFiles);
        }
    };

    const removeFile = (fileName: string) => {
        setFiles((currFiles: File[]) =>
            currFiles.filter((file) => file.name !== fileName),
        );
        if (fileInputField.current) fileInputField.current.value = '';
    };

    const renderFiles = () =>
        files.map((file) => {
            const isImageFile = file.type.split('/')[0] === 'image';
            return (
                <motion.div
                    key={file.name}
                    variants={itemVariants}
                    initial="hidden"
                    animate="show"
                    exit="hidden"
                    style={{ width: '100%' }}>
                    <Card>
                        <Space inline={false} align="center" size="large">
                            <Icon
                                size="2x"
                                color={theme?.fow.colors.text.primary}
                                icon={isImageFile ? 'image' : 'file'}
                            />
                            <Space
                                justify="space-between"
                                inline={false}
                                align="center">
                                <Space
                                    direction="vertical"
                                    align="start"
                                    size="xxsmall">
                                    <Overline>{file.name}</Overline>
                                    <Caption>
                                        {convertBytesToKB(file.size)} kb
                                    </Caption>
                                </Space>
                                <Icon
                                    icon="trash-alt"
                                    cursor="pointer"
                                    color={theme?.fow.colors.error.main}
                                    onClick={() => removeFile(file.name)}
                                />
                            </Space>
                        </Space>
                    </Card>
                </motion.div>
            );
        });

    useEffect(() => {
        if (!isMountFirstTime) {
            onChange?.(files);
        }
    }, [files]);

    return (
        <Wrapper>
            {label && (
                <Label required={required} hasError={!!error}>
                    {localization[language].label}
                </Label>
            )}
            {type === 'button' && (
                <Button {...uploadButtonProps} disabled={disabled}>
                    <FormField
                        type="file"
                        ref={fileInputField}
                        onChange={handleChange}
                        accept={accept}
                        multiple={multiple}
                        disabled={disabled}
                    />
                    {uploadButtonText || localization[language].upload}
                </Button>
            )}
            {type === 'dragger' && (
                <FileUploadContainer disabled={disabled}>
                    <Space size="xxlarge">
                        <Icon
                            icon="cloud"
                            size="10x"
                            color={theme?.fow.colors.grey.lighter}
                        />
                        <Space direction="vertical" align="start" size="xsmall">
                            <Heading as="h5">
                                {localization[language].placeholder}
                            </Heading>
                            <Subtitle level={2}>
                                {localization[language].description}
                            </Subtitle>
                            <Caption>
                                {localization[language].sizeInfo}:{' '}
                                {convertBytesToKB(maxFileSizeInBytes)} Kb
                            </Caption>
                        </Space>
                    </Space>
                    <FormField
                        type="file"
                        ref={fileInputField}
                        onChange={handleChange}
                        accept={accept}
                        multiple={multiple}
                        disabled={disabled}
                    />
                </FileUploadContainer>
            )}
            {showFileList && (
                <motion.div
                    variants={wrapperVariants}
                    initial="hidden"
                    animate="show">
                    <Space direction="vertical" inline={false} align="start">
                        <AnimatePresence>
                            {files.length > 0 && (
                                <FileListContainer>
                                    {renderFiles()}
                                </FileListContainer>
                            )}
                        </AnimatePresence>
                    </Space>
                </motion.div>
            )}
        </Wrapper>
    );
};

export default withTheme(Upload);