mirror of
https://github.com/aljazceru/landscape-template.git
synced 2026-01-01 13:34:30 +01:00
feat: Built the Inplace Image Uploading component and functionality
This commit is contained in:
@@ -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>;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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> }>({
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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"
|
||||
};
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user