feat(image): add direct image upload components

This commit is contained in:
enes 2025-01-07 09:48:30 +01:00
parent 0b2d488bbe
commit b33015cbaf
7 changed files with 272 additions and 5 deletions

View 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>
)
}

View 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;
}

View 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>
)
}

View 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;
}

View 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>
)
}

View 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>
)
}

View File

@ -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;
}