feat: Built the Inplace Image Uploading component and functionality

This commit is contained in:
MTG2000
2022-08-27 14:45:03 +03:00
committed by Dolu
parent 1ed976b211
commit 1735f9a84f
10 changed files with 436 additions and 40 deletions

View File

@@ -3,7 +3,7 @@ import { ComponentStory, ComponentMeta } from '@storybook/react';
import FileUploadInput from './FileUploadInput';
export default {
title: 'Shared/Inputs/File Upload Input',
title: 'Shared/Inputs/Files Inputs/Basic',
component: FileUploadInput,
} as ComponentMeta<typeof FileUploadInput>;

View File

@@ -1,4 +1,4 @@
import Uploady, { useUploady, useRequestPreSend, UPLOADER_EVENTS } from "@rpldy/uploady";
import Uploady, { useUploady, useRequestPreSend, UPLOADER_EVENTS, } from "@rpldy/uploady";
import { asUploadButton } from "@rpldy/upload-button";
import Button from "src/Components/Button/Button";
import { fetchUploadUrl } from "./fetch-upload-img-url";
@@ -66,7 +66,6 @@ const UploadBtn = asUploadButton((props: any) => {
const DropZone = forwardRef<any, any>((props, ref) => {
const { onClick, ...buttonProps } = props;
useRequestPreSend(async (data) => {
const filename = data.items?.[0].file.name ?? ''
@@ -95,10 +94,10 @@ const DropZone = forwardRef<any, any>((props, ref) => {
ref={ref}
onDragOverClassName={styles.active}
extraProps={{ onClick: onZoneClick }}
className={`${styles.zone} border-2 w-full min-h-[200px] rounded-16 flex flex-col justify-center items-center text text-body3`}
className={`${styles.zone} border-2 w-full min-h-[200px] max-w-[600px] rounded-16 flex flex-col justify-center items-center text text-body3 border-dashed`}
>
<div className={`${styles.idle_content} text-gray-600`}>
Drop your files here or <button className="font-bold underline">click to browse</button>
Drop your <span className="font-bold uppercase">IMAGES</span> here or <button className="font-bold text-blue-400 underline">Click to browse</button>
</div>
<motion.div

View File

@@ -1,31 +0,0 @@
import { ComponentStory, ComponentMeta } from '@storybook/react';
import { BsImages } from 'react-icons/bs';
import Button from 'src/Components/Button/Button';
import FilesInput from './FilesInput';
import FileDropInput from './FilesDropInput';
export default {
title: 'Shared/Inputs/Files Input',
component: FilesInput,
} as ComponentMeta<typeof FilesInput>;
const Template: ComponentStory<typeof FilesInput> = (args) => <FilesInput {...args} />
export const DefaultButton = Template.bind({});
DefaultButton.args = {
}
export const CustomizedButton = Template.bind({});
CustomizedButton.args = {
multiple: true,
uploadBtn: <Button color='primary'><span className="align-middle">Drop Images</span> <BsImages className='ml-12 scale-125' /></Button>
}
const DropTemplate: ComponentStory<typeof FileDropInput> = (args) => <div className="max-w-[500px]"><FileDropInput {...args as any} /></div>
export const DropZoneInput = DropTemplate.bind({});
DropZoneInput.args = {
onChange: console.log,
}

View File

@@ -4,7 +4,7 @@ import ScreenshotsInput, { ScreenshotType } from './ScreenshotsInput';
import { WrapForm, WrapFormController } from 'src/utils/storybook/decorators';
export default {
title: 'Shared/Inputs/Screenshots Input',
title: 'Shared/Inputs/Files Inputs/Screenshots',
component: ScreenshotsInput,
decorators: [
WrapFormController<{ screenshots: Array<ScreenshotType> }>({

View File

@@ -39,9 +39,6 @@ export default function ScreenshotsInput(props: Props) {
const [uploadingCount, setUploadingCount] = useState(0)
if (!Array.isArray(uploadedFiles))
throw new Error("screenshots field should be an array");
const canUploadMore = uploadingCount + uploadedFiles.length < MAX_UPLOAD_COUNT;
const placeholdersCount = (MAX_UPLOAD_COUNT - (uploadingCount + uploadedFiles.length + 1));

View File

@@ -0,0 +1,97 @@
import UploadPreview, { PreviewComponentProps, PreviewMethods } from '@rpldy/upload-preview'
import { useAbortItem, useItemAbortListener, useItemCancelListener, useItemErrorListener, useItemProgressListener } from '@rpldy/uploady';
import { useState } from 'react'
import ScreenShotsThumbnail from './ScreenshotThumbnail'
export default function ImagePreviews() {
return (
<UploadPreview PreviewComponent={CustomImagePreview} rememberPreviousBatches />
)
}
function CustomImagePreview({ id, url }: PreviewComponentProps) {
const [progress, setProgress] = useState<number>(0);
const [itemState, setItemState] = useState<string>(STATES.PROGRESS);
const abortItem = useAbortItem();
useItemProgressListener(item => {
if (item.completed > progress) {
setProgress(() => item.completed);
if (item.completed === 100) {
setItemState(STATES.DONE)
} else {
setItemState(STATES.PROGRESS)
}
}
}, id);
useItemAbortListener(item => {
setItemState(STATES.CANCELLED);
}, id);
useItemCancelListener(item => {
setItemState(STATES.CANCELLED);
}, id);
useItemErrorListener(item => {
setItemState(STATES.ERROR);
}, id);
if (itemState === STATES.DONE || itemState === STATES.CANCELLED)
return null
return <ScreenShotsThumbnail
url={url}
isLoading={itemState === STATES.PROGRESS}
isError={itemState === STATES.ERROR}
onCancel={() => {
abortItem(id)
}}
/>
// return <div className="aspect-video relative rounded-12 md:rounded-16 overflow-hidden border-2 border-gray-200">
// <img src={url}
// className={`
// w-full h-full object-cover
// ${itemState === STATES.PROGRESS && 'opacity-50'}
// `}
// alt="" />
// <div className="text-body5 absolute inset-0"
// >
// </div>
// {itemState === STATES.PROGRESS &&
// <div
// className="absolute inset-0 bg-gray-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold transition-transform"
// >
// <RotatingLines
// strokeColor="#fff"
// strokeWidth="3"
// animationDuration="0.75"
// width="48"
// visible={true}
// />
// </div>}
// {itemState === STATES.ERROR &&
// <div className="absolute inset-0 bg-red-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold">
// Failed...
// </div>}
// {itemState === STATES.CANCELLED &&
// <div className="absolute inset-0 bg-red-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold">
// Cancelled
// </div>}
// </div>;
};
const STATES = {
PROGRESS: "PROGRESS",
DONE: "DONE",
CANCELLED: "CANCELLED",
ERROR: "ERROR"
};

View File

@@ -0,0 +1,52 @@
import React from 'react'
import { FaTimes } from 'react-icons/fa';
import { RotatingLines } from 'react-loader-spinner';
interface Props {
url?: string,
isLoading?: boolean;
isError?: boolean;
onCancel?: () => void;
}
export default function ScreenshotThumbnail({ url, isLoading, isError, onCancel }: Props) {
const isEmpty = !url;
return (
<div className={`
aspect-video relative rounded-16 md:rounded-14 overflow-hidden border-2 border-gray-200
${isEmpty && "border-dashed"}
`}>
{!isEmpty && <img src={url}
className={`
w-full h-full object-cover
${isLoading && 'opacity-50'}
`}
alt="" />}
<div className="text-body5 absolute inset-0"
>
</div>
{isLoading &&
<div
className="absolute inset-0 bg-gray-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold transition-transform"
>
<RotatingLines
strokeColor="#fff"
strokeWidth="3"
animationDuration="0.75"
width="48"
visible={true}
/>
</div>}
{isError &&
<div className="absolute inset-0 bg-red-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold">
Failed...
</div>}
{!isEmpty &&
<button className="absolute bg-gray-900 hover:bg-opacity-100 bg-opacity-60 text-white rounded-full w-32 h-32 top-8 right-8" onClick={() => onCancel?.()}><FaTimes /></button>
}
</div>
)
}

View File

@@ -0,0 +1,118 @@
import React from 'react'
import { ComponentStory, ComponentMeta } from '@storybook/react';
import SingleImageUploadInput, { ImageType } from './SingleImageUploadInput';
import { WrapFormController } from 'src/utils/storybook/decorators';
import { RotatingLines } from 'react-loader-spinner';
import { FiCamera, } from 'react-icons/fi';
import { FaExchangeAlt, FaImage } from 'react-icons/fa';
export default {
title: 'Shared/Inputs/Files Inputs/Single Image Upload ',
component: SingleImageUploadInput,
decorators: [
WrapFormController<{ avatar: ImageType | null }>({
logValues: true,
name: "avatar",
defaultValues: {
avatar: null
}
})]
} as ComponentMeta<typeof SingleImageUploadInput>;
const Template: ComponentStory<typeof SingleImageUploadInput> = (args, context) => {
return <SingleImageUploadInput {...context.controller} {...args} />
}
export const Avatar = Template.bind({});
Avatar.args = {
wrapperClass: "inline-block cursor-pointer ",
render: ({ img, isUploading }) => <div className="w-[120px] hover:bg-gray-100 aspect-square rounded-full border-2 border-gray200 overflow-hidden relative flex flex-col justify-center items-center">
{img && <img src={img.url} className='w-full h-full object-cover rounded-full' alt="" />}
{!img &&
<>
<p className="text-center text-gray-400 text-body2 mb-8"><FiCamera /></p>
<div className={`text-gray-400 text-center text-body5`}>
Add Image
</div>
</>}
{isUploading &&
<div
className="absolute inset-0 bg-gray-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold transition-transform"
>
<RotatingLines
strokeColor="#fff"
strokeWidth="3"
animationDuration="0.75"
width="48"
visible={true}
/>
</div>
}
</div>
}
export const Thumbnail = Template.bind({});
Thumbnail.args = {
wrapperClass: "inline-block cursor-pointer ",
render: ({ img, isUploading }) => <div className="w-[120px] aspect-square rounded-16 border-2 border-gray200 overflow-hidden relative flex flex-col justify-center items-center">
{img && <img src={img.url} className='w-full h-full object-cover rounded-16' alt="" />}
{!img &&
<>
<p className="text-center text-gray-400 text-body2 mb-8"><FiCamera /></p>
<div className={`text-gray-400 text-center text-body5`}>
Add Image
</div>
</>}
{isUploading &&
<div
className="absolute inset-0 bg-gray-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold transition-transform"
>
<RotatingLines
strokeColor="#fff"
strokeWidth="3"
animationDuration="0.75"
width="48"
visible={true}
/>
</div>
}
</div>
}
export const Cover = Template.bind({});
Cover.args = {
wrapperClass: "block cursor-pointer ",
render: ({ img, isUploading }) => <div className="w-full group aspect-[5/2] md:aspect-[4/1] bg-gray-700 rounded-16 border-2 border-gray200 overflow-hidden relative flex flex-col justify-center items-center">
{img && <>
<img src={img.url} className='w-full h-full object-cover rounded-16' alt="" />
{!isUploading &&
<button className='absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 py-16 px-24 rounded-12 bg-black bg-opacity-70 opacity-0 group-hover:opacity-100 hover:bg-opacity-90 transition-opacity text-white text-h1'>
<FaExchangeAlt />
</button>}
</>}
{!img &&
<>
<p className="text-center text-gray-100 text-body1 md:text-h1 mb-8"><FaImage /></p>
<div className={`text-gray-100 text-center text-body4`}>
Drop a <span className="font-bold">COVER IMAGE</span> here or <br /> <span className="text-blue-300 underline">Click to browse</span>
</div>
</>}
{isUploading &&
<div
className="absolute inset-0 bg-gray-400 bg-opacity-60 flex flex-col justify-center items-center text-white font-bold transition-transform"
>
<RotatingLines
strokeColor="#fff"
strokeWidth="3"
animationDuration="0.75"
width="48"
visible={true}
/>
</div>
}
</div>
}

View File

@@ -0,0 +1,139 @@
import Uploady, { useRequestPreSend, UPLOADER_EVENTS, useAbortAll } from "@rpldy/uploady";
import { asUploadButton } from "@rpldy/upload-button";
// import { fetchUploadUrl } from "./fetch-upload-img-url";
import UploadDropZone from "@rpldy/upload-drop-zone";
import { forwardRef, ReactElement, useCallback, useState } from "react";
import styles from './styles.module.scss'
import { getMockSenderEnhancer } from "@rpldy/mock-sender";
const mockSenderEnhancer = getMockSenderEnhancer({
delay: 1500,
});
export interface ImageType {
id: string,
name: string,
url: string;
}
type RenderPropArgs = {
isUploading?: boolean;
img: ImageType | null,
onAbort: () => void
}
interface Props {
value: ImageType,
onChange: (new_value: ImageType | null) => void;
wrapperClass?: string;
render: (args: RenderPropArgs) => ReactElement;
}
export default function ScreenshotsInput(props: Props) {
const { value, onChange, render } = props;
const [currentlyUploadingItem, setCurrentlyUploadingItem] = useState<ImageType | null>(null)
return (
<Uploady
inputFieldName='file'
grouped={false}
enhancer={mockSenderEnhancer}
listeners={{
[UPLOADER_EVENTS.ITEM_START]: (item) => {
onChange(null)
setCurrentlyUploadingItem({
id: item.id,
url: URL.createObjectURL(item.file),
name: item.file.name,
})
},
[UPLOADER_EVENTS.ITEM_FINALIZE]: () => setCurrentlyUploadingItem(null),
[UPLOADER_EVENTS.ITEM_FINISH]: (item) => {
// Just for mocking purposes
const dataUrl = URL.createObjectURL(item.file);
const { id, filename, variants } = item?.uploadResponse?.data?.result ?? {
id: Math.random().toString(),
filename: item.file.name,
variants: [
"",
dataUrl
]
}
if (id) {
onChange({ id, name: filename, url: variants[1] })
}
}
}}
>
<DropZoneButton
extraProps={{
renderProps: {
isUploading: !!currentlyUploadingItem,
img: currentlyUploadingItem || value || null,
render,
wrapperClass: props.wrapperClass
}
}
}
/>
</Uploady>
)
}
const DropZone = forwardRef<any, any>((props, ref) => {
const { onClick, children, renderProps, ...buttonProps } = props;
useRequestPreSend(async (data) => {
const filename = data.items?.[0].file.name ?? ''
// const url = await fetchUploadUrl({ filename });
return {
options: {
destination: {
url: "URL"
}
}
}
})
const onZoneClick = useCallback(
(e: any) => {
if (onClick) {
onClick(e);
}
},
[onClick]
);
return <UploadDropZone
{...buttonProps}
ref={ref}
onDragOverClassName={styles.active}
extraProps={{ onClick: onZoneClick }}
className={renderProps.wrapperClass}
>
{renderProps.render({
img: renderProps.img,
isUploading: renderProps.isUploading,
})}
</UploadDropZone>
})
const DropZoneButton = asUploadButton(DropZone);

View File

@@ -0,0 +1,25 @@
.zone {
background-color: #f2f4f7;
border-color: #e4e7ec;
.active_content {
display: none;
}
.idle_content {
display: block;
}
&.active {
background-color: #b3a0ff;
border-color: #9e88ff;
.active_content {
display: block;
}
.idle_content {
display: none;
}
}
}