feat(image): add direct image upload components
This commit is contained in:
parent
0b2d488bbe
commit
b33015cbaf
14
src/components/Inputs/Error.tsx
Normal file
14
src/components/Inputs/Error.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
type InputErrorProps = {
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InputError = ({ message }: InputErrorProps) => {
|
||||||
|
if (!message) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='errorMain'>
|
||||||
|
<div className='errorMainColor'></div>
|
||||||
|
<p className='errorMainText'>{message}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
14
src/components/Inputs/MediaInputError.module.scss
Normal file
14
src/components/Inputs/MediaInputError.module.scss
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
.accordion-button::after {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.75rem;
|
||||||
|
color: rgba(255, 255, 255, 0.5) !important;
|
||||||
|
|
||||||
|
top: unset !important;
|
||||||
|
bottom: unset !important;
|
||||||
|
}
|
||||||
|
.accordion-body > * {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.accordion-item + .accordion-item {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
64
src/components/Inputs/MediaInputError.tsx
Normal file
64
src/components/Inputs/MediaInputError.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { FileError } from 'react-dropzone'
|
||||||
|
import styles from './MediaInputError.module.scss'
|
||||||
|
|
||||||
|
type MediaInputErrorProps = {
|
||||||
|
rootId: string
|
||||||
|
index: number
|
||||||
|
message: string
|
||||||
|
errors?: readonly FileError[] | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MediaInputError = ({
|
||||||
|
rootId,
|
||||||
|
index,
|
||||||
|
message,
|
||||||
|
errors
|
||||||
|
}: MediaInputErrorProps) => {
|
||||||
|
if (!message) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={['accordion-item', styles['accordion-item']].join(' ')}>
|
||||||
|
<h2 className='accordion-header' role='tab'>
|
||||||
|
<button
|
||||||
|
className={[
|
||||||
|
'accordion-button collapsed',
|
||||||
|
styles['accordion-button']
|
||||||
|
].join(' ')}
|
||||||
|
type='button'
|
||||||
|
data-bs-toggle='collapse'
|
||||||
|
data-bs-target={`#${rootId} .item-${index}`}
|
||||||
|
aria-expanded='false'
|
||||||
|
aria-controls={`${rootId} .item-${index}`}
|
||||||
|
>
|
||||||
|
<div className='errorMain'>
|
||||||
|
<div className='errorMainColor'></div>
|
||||||
|
<p className='errorMainText'>{message}</p>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</h2>
|
||||||
|
{errors && (
|
||||||
|
<div
|
||||||
|
className={`accordion-collapse collapse item-${index}`}
|
||||||
|
role='tabpanel'
|
||||||
|
data-bs-parent={`#${rootId}`}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={['accordion-body', styles['accordion-body']].join(' ')}
|
||||||
|
>
|
||||||
|
{errors.map((e) => {
|
||||||
|
return typeof e === 'string' ? (
|
||||||
|
<div className='errorMain' key={e}>
|
||||||
|
{e}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className='errorMain' key={e.code}>
|
||||||
|
{e.message}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
45
src/components/Inputs/MediaInputPopover.module.scss
Normal file
45
src/components/Inputs/MediaInputPopover.module.scss
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
.popover {
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 0 16px 0px rgb(0 0 0 / 15%);
|
||||||
|
background: #232323;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
max-height: 500px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 25px;
|
||||||
|
> *:not(:first-child) {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.trigger {
|
||||||
|
position: absolute;
|
||||||
|
top: 25px;
|
||||||
|
right: 25px;
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mediaInputError {
|
||||||
|
--bs-accordion-color: unset;
|
||||||
|
--bs-accordion-bg: unset;
|
||||||
|
--bs-accordion-transition: unset;
|
||||||
|
--bs-accordion-border-color: unset;
|
||||||
|
--bs-accordion-border-width: unset;
|
||||||
|
--bs-accordion-border-radius: unset;
|
||||||
|
--bs-accordion-inner-border-radius: unset;
|
||||||
|
--bs-accordion-btn-padding-x: unset;
|
||||||
|
--bs-accordion-btn-padding-y: unset;
|
||||||
|
--bs-accordion-btn-color: unset;
|
||||||
|
--bs-accordion-btn-bg: unset;
|
||||||
|
--bs-accordion-btn-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='gray'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
|
||||||
|
--bs-accordion-btn-icon-width: 1.25rem;
|
||||||
|
--bs-accordion-btn-icon-transform: rotate(-180deg);
|
||||||
|
--bs-accordion-btn-icon-transition: transform 0.2s ease-in-out;
|
||||||
|
--bs-accordion-btn-active-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='gray'%3e%3cpath fill-rule='evenodd' d='M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z'/%3e%3c/svg%3e");
|
||||||
|
--bs-accordion-btn-focus-border-color: unset;
|
||||||
|
--bs-accordion-btn-focus-box-shadow: unset;
|
||||||
|
--bs-accordion-body-padding-x: unset;
|
||||||
|
--bs-accordion-body-padding-y: unset;
|
||||||
|
--bs-accordion-active-color: unset;
|
||||||
|
--bs-accordion-active-bg: unset;
|
||||||
|
}
|
108
src/components/Inputs/MediaInputPopover.tsx
Normal file
108
src/components/Inputs/MediaInputPopover.tsx
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
import * as Popover from '@radix-ui/react-popover'
|
||||||
|
import { useMemo } from 'react'
|
||||||
|
import { FileRejection, FileWithPath } from 'react-dropzone'
|
||||||
|
import { MediaInputError } from './MediaInputError'
|
||||||
|
import { InputSuccess } from './Success'
|
||||||
|
import styles from './MediaInputPopover.module.scss'
|
||||||
|
|
||||||
|
interface MediaInputPopoverProps {
|
||||||
|
name: string
|
||||||
|
acceptedFiles: readonly FileWithPath[]
|
||||||
|
fileRejections: readonly FileRejection[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MediaInputPopover = ({
|
||||||
|
name,
|
||||||
|
acceptedFiles,
|
||||||
|
fileRejections
|
||||||
|
}: MediaInputPopoverProps) => {
|
||||||
|
const acceptedFileItems = useMemo(
|
||||||
|
() =>
|
||||||
|
acceptedFiles.map((file) => (
|
||||||
|
<InputSuccess
|
||||||
|
key={file.path}
|
||||||
|
message={`${file.path} - ${file.size} bytes`}
|
||||||
|
/>
|
||||||
|
)),
|
||||||
|
[acceptedFiles]
|
||||||
|
)
|
||||||
|
const fileRejectionItems = useMemo(() => {
|
||||||
|
const id = `${name}-errors`
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`accordion accordion-flush ${styles.mediaInputError}`}
|
||||||
|
role='tablist'
|
||||||
|
id={id}
|
||||||
|
>
|
||||||
|
{fileRejections.map(({ file, errors }, index) => (
|
||||||
|
<MediaInputError
|
||||||
|
rootId={id}
|
||||||
|
index={index}
|
||||||
|
key={file.path}
|
||||||
|
message={`${file.path} - ${file.size} bytes`}
|
||||||
|
errors={errors}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}, [fileRejections, name])
|
||||||
|
|
||||||
|
if (acceptedFiles.length === 0 && fileRejections.length === 0) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Popover.Root>
|
||||||
|
<Popover.Trigger asChild>
|
||||||
|
<div className={styles.trigger}>
|
||||||
|
{acceptedFiles.length > 0 ? (
|
||||||
|
<svg
|
||||||
|
width='1.5em'
|
||||||
|
height='1.5em'
|
||||||
|
fill='currentColor'
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='0 0 576 512'
|
||||||
|
>
|
||||||
|
<path d='M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 38.6C310.1 219.5 256 287.4 256 368c0 59.1 29.1 111.3 73.7 143.3c-3.2 .5-6.4 .7-9.7 .7L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zM288 368a144 144 0 1 1 288 0 144 144 0 1 1 -288 0zm211.3-43.3c-6.2-6.2-16.4-6.2-22.6 0L416 385.4l-28.7-28.7c-6.2-6.2-16.4-6.2-22.6 0s-6.2 16.4 0 22.6l40 40c6.2 6.2 16.4 6.2 22.6 0l72-72c6.2-6.2 6.2-16.4 0-22.6z' />
|
||||||
|
</svg>
|
||||||
|
) : (
|
||||||
|
<svg
|
||||||
|
width='1.5em'
|
||||||
|
height='1.5em'
|
||||||
|
fill='tomato'
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='0 0 576 512'
|
||||||
|
>
|
||||||
|
<path d='M0 64C0 28.7 28.7 0 64 0L224 0l0 128c0 17.7 14.3 32 32 32l128 0 0 38.6C310.1 219.5 256 287.4 256 368c0 59.1 29.1 111.3 73.7 143.3c-3.2 .5-6.4 .7-9.7 .7L64 512c-35.3 0-64-28.7-64-64L0 64zm384 64l-128 0L256 0 384 128zm48 96a144 144 0 1 1 0 288 144 144 0 1 1 0-288zm0 240a24 24 0 1 0 0-48 24 24 0 1 0 0 48zm0-192c-8.8 0-16 7.2-16 16l0 80c0 8.8 7.2 16 16 16s16-7.2 16-16l0-80c0-8.8-7.2-16-16-16z' />
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Popover.Trigger>
|
||||||
|
<Popover.Portal>
|
||||||
|
<Popover.Content className={styles.popover} sideOffset={5}>
|
||||||
|
<div className='popUpMainCardTop'>
|
||||||
|
<div className='popUpMainCardTopInfo'>
|
||||||
|
<h3>Selected files</h3>
|
||||||
|
</div>
|
||||||
|
<Popover.Close asChild aria-label='Close'>
|
||||||
|
<div className='popUpMainCardTopClose'>
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
viewBox='-96 0 512 512'
|
||||||
|
width='1em'
|
||||||
|
height='1em'
|
||||||
|
fill='currentColor'
|
||||||
|
style={{ zIndex: 1 }}
|
||||||
|
>
|
||||||
|
<path d='M310.6 361.4c12.5 12.5 12.5 32.75 0 45.25C304.4 412.9 296.2 416 288 416s-16.38-3.125-22.62-9.375L160 301.3L54.63 406.6C48.38 412.9 40.19 416 32 416S15.63 412.9 9.375 406.6c-12.5-12.5-12.5-32.75 0-45.25l105.4-105.4L9.375 150.6c-12.5-12.5-12.5-32.75 0-45.25s32.75-12.5 45.25 0L160 210.8l105.4-105.4c12.5-12.5 32.75-12.5 45.25 0s12.5 32.75 0 45.25l-105.4 105.4L310.6 361.4z'></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</Popover.Close>
|
||||||
|
</div>
|
||||||
|
<div className={styles.content}>
|
||||||
|
{acceptedFileItems}
|
||||||
|
{fileRejectionItems}
|
||||||
|
</div>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Portal>
|
||||||
|
</Popover.Root>
|
||||||
|
)
|
||||||
|
}
|
14
src/components/Inputs/Success.tsx
Normal file
14
src/components/Inputs/Success.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
type InputSuccessProps = {
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const InputSuccess = ({ message }: InputSuccessProps) => {
|
||||||
|
if (!message) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className='successMain'>
|
||||||
|
<div className='successMainColor'></div>
|
||||||
|
<p className='successMainText'>{message}</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
@ -661,6 +661,7 @@ a:hover {
|
|||||||
padding-right: 50px;
|
padding-right: 50px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.successMain,
|
||||||
.errorMain {
|
.errorMain {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@ -671,13 +672,17 @@ a:hover {
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
grid-gap: 10px;
|
grid-gap: 10px;
|
||||||
}
|
}
|
||||||
|
.successMainColor,
|
||||||
.errorMainColor {
|
.errorMainColor {
|
||||||
width: 5px;
|
width: 5px;
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
.errorMainColor {
|
||||||
background: tomato;
|
background: tomato;
|
||||||
}
|
}
|
||||||
|
.successMainColor {
|
||||||
|
background: #60ae60;
|
||||||
|
}
|
||||||
.errorMainText {
|
.errorMainText {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -709,12 +714,14 @@ a:hover {
|
|||||||
.uploadBoxMain {
|
.uploadBoxMain {
|
||||||
background: hsl(0deg 0% 0% / 10%);
|
background: hsl(0deg 0% 0% / 10%);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
height: 10px;
|
height: 150px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
border: solid 1px hsl(0deg 0% 100% / 5%);
|
border: solid 1px hsl(0deg 0% 100% / 5%);
|
||||||
|
transition: padding ease-in-out 0.4s;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.uploadBoxMain:hover > .uploadBoxMainInside {
|
.uploadBoxMain:hover {
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -729,4 +736,5 @@ a:hover {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
color: hsl(0deg 0% 100% / 20%);
|
color: hsl(0deg 0% 100% / 20%);
|
||||||
grid-gap: 10px;
|
grid-gap: 10px;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
Loading…
x
Reference in New Issue
Block a user