Moved Uploader to shared-components
All checks were successful
armco-org/shared-components/pipeline/head This commit looks good

This commit is contained in:
2026-02-02 00:57:12 +05:30
parent 052f0b83c1
commit 7ea5a6568f
8 changed files with 955 additions and 7 deletions

View File

@@ -1,7 +1,7 @@
{
"name": "@armco/shared-components",
"description": "React Component Lib of independent components that can be utilised by sophisticated ones in @armco/components",
"version": "0.0.60",
"version": "0.0.61",
"type": "module",
"author": "Armco (@restruct-corporate-advantage)",
"types": "build/types/index.d.ts",

View File

@@ -6,11 +6,23 @@ set -e
npm run build
cp package.json build/
sed -i '' -E 's/"build"/"*"/' build/package.json
sed -i '' 's#"build/cjs/index.js"#"cjs/index.js"#' build/package.json
sed -i '' 's#"build/es/index.js"#"es/index.js"#' build/package.json
sed -i '' 's#"build/types/index.d.ts"#"types/index.d.ts"#' build/package.json
# Use Node.js for portable package.json normalization
PKG_PATH="$(pwd)/build/package.json" node - <<'EOF'
const fs = require('fs');
const path = process.env.PKG_PATH;
const pkg = JSON.parse(fs.readFileSync(path, 'utf8'));
pkg.private = false;
delete pkg.scripts;
delete pkg.devDependencies;
if (!pkg.files) pkg.files = ['*'];
else pkg.files = pkg.files.map(x => x === 'build' ? '*' : x);
['main','module','types'].forEach(k => {
if (pkg[k]) pkg[k] = pkg[k].replace(/^\.??\/build\//, '').replace(/^build\//, '');
});
if (pkg.deprecated && typeof pkg.deprecated !== 'string') delete pkg.deprecated;
fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + '\n');
EOF
cd build
npm pack --pack-destination ~/__Projects__/Common

View File

@@ -1,7 +1,7 @@
.ar-Drawer {
border-right: 1px solid var(--ar-color-layout-border);
.ar_Drawer__content {
.ar-Drawer__content {
height: 100%;
}
&.has-title .ar-Drawer__content {

38
src/Uploader.component.scss Executable file
View File

@@ -0,0 +1,38 @@
.ar-Uploader {
.ar-Uploader__file-uploader-progress-bar {
transition: max-height 0.3s, opacity 0.3s, padding 0.3s, margin-bottom 0.3s, background-color 0.3s;
opacity: 0;
max-height: 0;
&.show {
opacity: 1;
max-height: 4rem;
}
&:hover {
background-color: var(--ar-bg-hover-6);
}
}
.ar-Button.loading .pre {
animation: rotating 0.5s linear infinite;
}
&:not(.compact):not(.image-rect):not(.image-circle) .ar-Uploader__file-input {
transition: border 0.3s ease-in-out;
&:has(.ar-Avatar.FAILED) {
border-color: var(--ar-color-danger-soft);
}
border: 1px dashed lightgrey;
border-radius: 0.5rem;
padding: 0.5rem;
background-color: var(--ar-bg-base);
}
.ar-Uploader__upload-prompt-text {
color: var(--ar-color-obscure-4);
}
.ar-Uploader__upload-message {
color: var(--ar-color-obscure-2);
}
}

637
src/Uploader.tsx Executable file
View File

@@ -0,0 +1,637 @@
import {
ChangeEvent,
MouseEvent,
ReactNode,
memo,
useEffect,
useMemo,
useRef,
useState,
} from "react"
import { v4 as uuid } from "uuid"
import { NativeTypes } from "react-dnd-html5-backend"
import { BaseProps, FunctionType } from "@armco/types"
import { API_CONFIG } from "@armco/configs/endpoints"
import { ArAlertType } from "@armco/utils"
import { useNotification } from "@armco/utils/hooks"
import { put } from "@armco/utils/network"
import Icon from "@armco/icon"
import {
ArButtonVariants,
ArPopoverSlots,
ArProgress,
ArSizes,
} from "./enums"
import { ImageProps } from "./Image"
import Droppable from "./Droppable"
import Button from "./Button"
import Text from "./Text"
import ProgressIndicator from "./ProgressIndicator"
import Tooltip from "./Tooltip"
import Avatar from "./Avatar"
import { convertToBytes } from "./utils"
import useUpload from "./useUpload"
import "./Uploader.component.scss"
export type UploadHandler = (file: File) => Promise<UploadResults>
export type UploadResults = {
[key: string]: UploadResult
}
export interface UploadResult {
file: File | { name: string }
imageProps?: Partial<ImageProps>
status: ArProgress
progress: number
type?: string
url?: string
error?: string
httpStatus?: number
}
export interface UploaderProps extends BaseProps {
acceptedFileTypes?: string | Array<string>
acceptedFileCount?: number
afterUpload?: FunctionType
allowMultiple?: boolean
avatarSize?: ArSizes
beforeUpload?: FunctionType
disabled?: boolean
endpoint?: string
errorMessage?: string
files?: Array<File | { name: string; url?: string }>
fileSizeLimit?: string
handler?: UploadHandler
message?: string
updateEndpoint?: string
uploadEntryRenderer?: FunctionType
uploadSequential?: boolean
onFileSelected: FunctionType
variant: "compact" | "regular" | "large" | "image-circle" | "image-rect"
uploadButtonText?: string
uploadingButtonText?: string
}
const UploadedEntry = ({
clear,
demo,
disabled,
result,
}: {
clear: FunctionType
demo: boolean
disabled: boolean
result: UploadResult
}) => {
const [hovered, setHovered] = useState<boolean>()
const [displayed, display] = useState<boolean>()
useEffect(() => {
display(true)
}, [])
const isImage = result.type === "image"
const progressRender = (
<ProgressIndicator
label={result.file.name}
preIcon={isImage ? "" : "im.ImAttachment"}
progress={result.progress}
slot={ArPopoverSlots.ANCHOR}
rightEndContent={
(hovered || result.status === ArProgress.FAILED) && (
<Icon
icon="ri.RiDeleteBin6Fill"
attributes={{
colors: {
fillColor:
result.status === ArProgress.FAILED ? "#ff4d4f" : "grey",
},
classes: disabled ? "" : "cursor-pointer",
}}
events={{
onClick: () => {
if (!disabled) {
display(false)
setTimeout(clear, 300)
}
},
}}
/>
)
}
status={result.status}
thumbnailRenderer={() => {
if (isImage) {
return (
<Avatar
classes="me-3"
item={result}
size={ArSizes.XSMALL}
variant="circle"
viewOnly
/>
)
}
return null
}}
/>
)
const extractError = (errorObj: any) => {
if (typeof errorObj === "string") {
return errorObj
}
return errorObj.error || errorObj.message || "Server Error"
}
return (
<div
className={`ar-Uploader__file-uploader-progress-bar position-relative ${displayed ? " show p-1 border-radius l1 mb-1 cursor-pointer" : " px-1"
} ${result.status}`}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
>
{result.status === ArProgress.FAILED ? (
<Tooltip demo={demo}>
{progressRender}
<span slot={ArPopoverSlots.POPOVER}>
{extractError(result.error)}
</span>
</Tooltip>
) : (
progressRender
)}
</div>
)
}
const Uploader = memo(
(props: UploaderProps): ReactNode => {
const {
acceptedFileTypes,
acceptedFileCount,
afterUpload,
allowMultiple,
beforeUpload,
demo,
disabled,
uploadEntryRenderer,
errorMessage: externalErrorMessage = "Failed to upload",
endpoint,
fileSizeLimit = "5mb",
files,
handler,
message,
onFileSelected,
avatarSize = ArSizes.REGULAR,
variant = "compact",
theme,
updateEndpoint,
uploadSequential,
uploadButtonText = "Upload",
uploadingButtonText = "Uploading...",
} = props
const inputRef = useRef<HTMLInputElement>(null)
const { notify } = useNotification()
const [localResults, setLocalResults] = useState<UploadResults>()
const [localAllowMultiple, setLocalAllowMultiple] = useState<boolean>()
const [localAcceptedFileTypes, setLocalAcceptedFileTypes] = useState<
string | Array<string>
>()
const [errorMessage, setErrorMessage] = useState<string>()
const externalFiles = useMemo(
() =>
Boolean(localAllowMultiple)
? acceptedFileCount !== undefined
? files?.slice(0, acceptedFileCount)
: files
: files?.slice(0, 1),
[files],
)
const { clear, upload, results } = useUpload(
endpoint || handler || "",
uploadSequential,
externalFiles,
)
const isAvatar = variant === "image-circle" || variant === "image-rect"
useEffect(() => {
setLocalResults(results)
if ((!results || Object.keys(results).length === 0) && inputRef.current) {
inputRef.current.value = ""
}
let hasError
localResults &&
Object.keys(localResults).length > 0 &&
Object.keys(localResults).every((resultKey) => {
const result = localResults[resultKey]
hasError ||= result.status === ArProgress.FAILED
return result.status === ArProgress.FAILED
})
hasError && setErrorMessage(externalErrorMessage)
}, [results])
useEffect(() => {
setLocalAllowMultiple(allowMultiple)
}, [allowMultiple])
useEffect(() => {
setLocalAcceptedFileTypes(acceptedFileTypes)
}, [acceptedFileTypes])
useEffect(() => {
if (isAvatar) {
setLocalAllowMultiple(false)
setLocalAcceptedFileTypes([
"image/jpeg",
"image/png",
"image/gif",
"image/svg+xml",
])
}
}, [isAvatar])
const checkAndUpload = (files: Array<File>) => {
const sizeLimit = convertToBytes(fileSizeLimit)
let maxFileCount
try {
maxFileCount =
acceptedFileCount !== undefined &&
(typeof acceptedFileCount === "number"
? acceptedFileCount
: parseInt(acceptedFileCount))
} catch (e) {
console.warn(
"Incorrect prop acceptedFileCount supplied, should be a numeric value",
)
}
if (
typeof maxFileCount === "number" &&
files &&
files.length + Object.keys(localResults || {}).length > maxFileCount
) {
setErrorMessage("Exceeded allowed number of files.")
notify({
message: "You've selected more then allowed number of files",
type: ArAlertType.WARNING,
uid: uuid(),
})
} else if (files.findIndex((file) => file.size > sizeLimit) > -1) {
const message = `${localAllowMultiple
? "One or more files exceed "
: "Selected file exceeds "
}specified size limit of ${fileSizeLimit}`
setErrorMessage(message)
notify({
message,
type: ArAlertType.WARNING,
uid: uuid(),
})
} else if (files && files.length > 0) {
upload(files, afterUpload, localAllowMultiple)
onFileSelected && onFileSelected(files)
}
}
const handleFileChange = (event: ChangeEvent<HTMLInputElement> | any) => {
setErrorMessage("")
const files: Array<File> = Array.from(
(event.target || event.dataTransfer)?.files,
)
if (!beforeUpload || beforeUpload(files, () => checkAndUpload(files))) {
checkAndUpload(files)
}
}
const isLoading =
localResults &&
Object.keys(localResults).findIndex((resultKey) => {
const result = localResults[resultKey]
return result.status === ArProgress.IN_PROGRESS
}) > -1
const allCompleted = !!(
localResults &&
Object.keys(localResults).length > 0 &&
Object.keys(localResults).every((resultKey) => {
const result = localResults[resultKey]
return (
result.status === ArProgress.COMPLETED ||
result.status === ArProgress.FAILED
)
})
)
const onlyResult =
localResults && localResults[Object.keys(localResults)[0]]
const onImageChange = (imageProps: Partial<ImageProps>) => {
if (localResults && onlyResult) {
const fileKey = Object.keys(localResults).find((key) =>
key.startsWith(onlyResult.file.name),
)
if (fileKey) {
const resultClone = { ...localResults[fileKey] }
resultClone.imageProps = imageProps
localResults[fileKey] = resultClone
setLocalResults({ ...localResults })
}
}
}
const onImageCommit = (imageProps: Partial<ImageProps>) => {
if (localResults && onlyResult) {
const fileKey = Object.keys(localResults).find((key) =>
key.startsWith(onlyResult.file.name),
)
if (fileKey) {
const resultClone = localResults[fileKey]
let url = updateEndpoint ? updateEndpoint : endpoint
if (url) {
const environment = process.env.NODE_ENV || "development"
const host = url.startsWith("http")
? ""
: API_CONFIG.STATIC_HOST[environment as keyof object]
url = host + url
put(url, resultClone).then((res) => {
if (res.status === 200) {
notify({
message: "Image updaetd successfully!",
type: ArAlertType.SUCCESS,
uid: uuid(),
})
} else {
notify({
message: "Failed to update image!",
type: ArAlertType.ERROR,
uid: uuid(),
})
}
})
} else {
notify({
message: "Missing image update URL",
type: ArAlertType.WARNING,
uid: uuid(),
})
}
}
}
}
const inputRender = (
<div
className={`ar-Uploader__file-input flex-v-center${isLoading ? " disabled" : ""
}${variant !== "compact" && !isLoading && !disabled
? " cursor-pointer"
: ""
}${variant === "large"
? " flex-h-center flex-column p-3"
: " justify-content-between"
}${variant === "image-circle" ? " border-radius-50" : ""}`}
onClick={() =>
variant !== "compact" &&
!isLoading &&
!disabled &&
inputRef.current?.click()
}
>
{isAvatar ? (
<Avatar
clearImage={clear}
demo={demo}
variant={variant === "image-circle" ? "circle" : "rect"}
item={onlyResult}
onChange={onImageChange}
onCommit={onImageCommit}
size={avatarSize}
uploadButtonText={uploadButtonText}
uploadingButtonText={uploadingButtonText}
/>
) : (
<>
<div className="ar-Uploader__file-input__prompt d-flex flex-column">
<div
className={`d-inline-flex ${variant === "large"
? " flex-center flex-column"
: "flex-v-center"
}`}
>
{variant === "compact" ? (
<>
<Button
classes={isLoading ? "loading" : ""}
content={
isLoading ? uploadingButtonText : uploadButtonText
}
preIcon={
isLoading
? "ri.RiLoader5Fill"
: allCompleted
? errorMessage
? "fa.FaExclamation"
: "fa.FaCheck"
: "md.MdOutlineFileUpload"
}
onClick={() => inputRef.current?.click()}
theme={theme}
disabled={disabled || isLoading}
/>
{localResults &&
Object.keys(localResults).length > 0 &&
variant === "compact" && (
<>
{localAllowMultiple ? (
<span className="fw-bold flex-v-center ms-3">
{`(${Object.keys(localResults).length} file${Object.keys(localResults).length > 1 ? "s" : ""
})`}
</span>
) : (
<Text
classes="fw-bold flex-v-center ms-3 text-decoration-underline"
descriptor={{
id: "temp-text-id",
order: 0,
name: "Text",
text: localResults[Object.keys(localResults)[0]]
.file.name,
chunks: {},
}}
overflowEllipsis
overflowTooltip
/>
)}
{
<Icon
icon="io5.IoClose"
attributes={{
colors: {
fillColor: "red",
},
classes: `ms-2 ${disabled ? "" : "cursor-pointer"
}`,
}}
events={{
onClick: () => {
if (disabled) {
clear()
if (inputRef.current) {
inputRef.current.value = ""
}
}
},
}}
/>
}
</>
)}
</>
) : (
<>
<Icon
icon="io5.IoFileTrayOutline"
attributes={{
colors: {
fillColor: "#1677ff",
},
classes: variant === "large" ? "mb-2" : "me-3",
size: variant === "large" ? "3rem" : "1rem",
}}
/>
<span
className={`ar-Uploader__upload-prompt-text${variant === "large" ? " h5 mb-0" : ""
}`}
>
Click or drag files here to upload
</span>
</>
)}
</div>
{message && (
<small
className={`ar-Uploader__upload-message text-decoration-underline ${variant === "large" ? "fw-bold f3 mt-2 mb-2" : "mt-1"
}`}
>
{message}
</small>
)}
</div>
<div className="ar-Uploader__file-input__actions">
{variant !== "compact" &&
localResults &&
Object.keys(localResults).length > 0 && (
<Button
containerClasses={`text-end${variant === "large" ? " mt-3" : ""
}`}
content="Clear"
variant={ArButtonVariants.DANGER}
size={
variant === "regular" ? ArSizes.XSMALL : ArSizes.SMALL
}
onClick={(e) => {
e.stopPropagation()
clear()
if (inputRef.current) {
inputRef.current.value = ""
}
}}
/>
)}
</div>
</>
)}
<input
className="ar-Uploader__input d-none"
type="file"
onChange={handleFileChange}
ref={inputRef}
title="file-uploader"
accept={
Array.isArray(localAcceptedFileTypes)
? localAcceptedFileTypes.join(",")
: localAcceptedFileTypes
}
disabled={disabled}
multiple={localAllowMultiple}
/>
</div>
)
const errorRender = errorMessage && (
<span className="error my-1 small">{errorMessage}</span>
)
const resultsRender = localResults && variant !== "compact" && (
<div className="ar-Uploader__file-upload-progress mt-2">
{Object.entries(localResults).map(([fileName, result]) =>
uploadEntryRenderer ? (
uploadEntryRenderer(result, () => clear(fileName))
) : (
<UploadedEntry
clear={() => clear(fileName)}
demo={!!demo}
disabled={!!disabled}
key={fileName}
result={result}
/>
),
)}
</div>
)
const uploaderRender = (
<div
className={`ar-Uploader d-flex flex-column${variant ? " " + variant : ""
}${isAvatar ? " flex-center" : " w-100"}`}
onDragOver={(e: MouseEvent<HTMLDivElement>) => e.preventDefault()}
onDragLeave={(e: MouseEvent<HTMLDivElement>) => e.preventDefault()}
>
{variant !== "compact" ? (
<Droppable
dropHandler={(e) => {
if (inputRef.current && !disabled) {
inputRef.current.files = e.dataTransfer.files
inputRef.current.dispatchEvent(
new Event("change", { bubbles: true, cancelable: true }),
)
}
}}
acceptTypes={[NativeTypes.FILE]}
hideHoverEffect={isAvatar || disabled}
>
{inputRender}
</Droppable>
) : (
inputRender
)}
{!isAvatar && errorRender}
{!isAvatar && resultsRender}
</div>
)
return uploaderRender
},
(prevProps, props) => {
return (
prevProps.acceptedFileTypes === props.acceptedFileTypes &&
prevProps.acceptedFileCount === props.acceptedFileCount &&
prevProps.allowMultiple === props.allowMultiple &&
prevProps.disabled === props.disabled &&
prevProps.errorMessage === props.errorMessage &&
prevProps.endpoint === props.endpoint &&
prevProps.fileSizeLimit === props.fileSizeLimit &&
JSON.stringify(prevProps.files) === JSON.stringify(props.files) &&
prevProps.message === props.message &&
prevProps.avatarSize === props.avatarSize &&
prevProps.variant === props.variant &&
prevProps.theme === props.theme &&
prevProps.uploadSequential === props.uploadSequential &&
prevProps.uploadButtonText === props.uploadButtonText &&
prevProps.uploadingButtonText === props.uploadingButtonText
)
},
)
export default Uploader

View File

@@ -3,6 +3,8 @@ import "./styles/global.scss"
/* PLOP_INJECT_IMPORT */
export { default as Avatar } from "./Avatar"
export { default as ImageEditor } from "./ImageEditor"
export { default as Uploader } from "./Uploader"
export { default as useUpload } from "./useUpload"
export { default as Anchor } from "./Anchor"
export { default as Card } from "./Card"
export { default as Component_404 } from "./Component_404"

214
src/useUpload.ts Normal file
View File

@@ -0,0 +1,214 @@
// useUpload.ts
import { useEffect, useRef, useState } from "react"
import { FunctionType } from "@armco/types"
import { upload as httpUpload } from "@armco/utils/network"
import { UploadHandler, UploadResults } from "./Uploader"
import { ArProgress } from "./enums"
import { generateFileKey } from "./utils"
const SUCCESS_RESULT = {
status: ArProgress.COMPLETED,
progress: 100,
}
const FAILED_RESULT = {
status: ArProgress.FAILED,
progress: 0,
}
const useUpload = (
endpointOrHandler: string | UploadHandler,
isSequential?: boolean,
files?: Array<File | { name: string; url?: string }>,
) => {
const [results, setResults] = useState<UploadResults>()
const clearActionRef = useRef<{ [key: string]: boolean }>({})
const intervalRef = useRef<{ [key: string]: NodeJS.Timeout }>({})
useEffect(() => {
if (files) {
const results: UploadResults = {}
files?.forEach((file) => {
results[generateFileKey(file)] = {
...file,
progress: 100,
status: ArProgress.COMPLETED,
file: typeof file === "object" ? file : { name: (file as File).name },
}
})
setResults(results)
}
}, [files])
useEffect(() => {
const interval = intervalRef.current
return () => {
Object.keys(interval).forEach((key) => clearInterval(interval[key]))
}
}, [intervalRef])
const clear = (fileId?: string) => {
if (fileId) {
const resultsClone = { ...results }
delete resultsClone[fileId]
setResults(resultsClone)
clearActionRef.current[fileId] = true
clearInterval(intervalRef.current[fileId])
} else {
setResults(undefined)
results &&
Object.keys(results).forEach(
(key) =>
results[key].status === ArProgress.IN_PROGRESS &&
(clearActionRef.current[key] = true),
)
Object.keys(intervalRef.current).forEach((key) =>
clearInterval(intervalRef.current[key]),
)
}
}
const uploadFile = async (file: File) => {
const isHandler = typeof endpointOrHandler === "function"
const handler = isHandler ? (endpointOrHandler as UploadHandler) : null
const fileKey = generateFileKey(file)
intervalRef.current[fileKey] = setInterval(() => {
setResults((prevResult) => {
const prevResultObj = prevResult && prevResult[fileKey as string]
if (!prevResultObj || prevResultObj.progress >= 95) {
if (intervalRef.current) {
clearInterval(intervalRef.current[fileKey])
}
return prevResult
}
const increment = Math.floor(Math.random() * 8) + 5
return {
...prevResult,
[fileKey]: {
...prevResultObj,
progress: prevResultObj.progress + increment,
},
}
})
}, 500)
if (handler) {
return await handler(file)
.then((res) => {
if (!clearActionRef.current[fileKey]) {
if (!res.ok) {
setResults((prevResult) => {
return {
...prevResult,
[fileKey]: { file, ...FAILED_RESULT },
}
})
}
setResults((prevResult) => ({
...prevResult,
[fileKey]: { file, ...SUCCESS_RESULT },
}))
}
})
.catch(() => {
if (!clearActionRef.current[fileKey]) {
setResults((prevResult) => {
return {
...prevResult,
[fileKey]: { file, ...FAILED_RESULT },
}
})
}
})
.finally(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current[fileKey])
}
})
} else {
return await httpUpload(endpointOrHandler as string, file)
.then((response) => {
if (!clearActionRef.current[fileKey]) {
if (!response.body) {
setResults((prevResult) => {
return {
...prevResult,
[fileKey]: {
file,
...FAILED_RESULT,
error: response.body,
httpStatus: response.status,
},
}
})
return
}
const savedRecord = response.body.saved[0]
setResults((prevResult) => {
return {
...prevResult,
[fileKey]: { ...(savedRecord || {}), file, ...SUCCESS_RESULT },
}
})
} else {
delete clearActionRef.current[fileKey]
delete intervalRef.current[fileKey]
}
})
.catch((error) => {
if (!clearActionRef.current[fileKey]) {
setResults((prevResult) => {
return {
...prevResult,
[fileKey]: { file, ...FAILED_RESULT, error },
}
})
} else {
delete clearActionRef.current[fileKey]
delete intervalRef.current[fileKey]
}
})
.finally(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current[fileKey])
}
})
}
}
const upload = async (
newFiles: File[],
callback?: FunctionType,
shouldAppend?: boolean,
) => {
const newResults: UploadResults = {}
newFiles.forEach((file) => {
const fileKey = generateFileKey(file)
newResults[fileKey] = {
file,
status: ArProgress.IN_PROGRESS,
progress: isSequential ? 1 : Math.floor(Math.random() * 10),
}
})
setResults(shouldAppend ? { ...results, ...newResults } : newResults)
let promises = []
for (const file of newFiles) {
if (isSequential) {
promises.push(await uploadFile(file))
} else {
promises.push(uploadFile(file))
}
}
await Promise.allSettled(promises).then(
(responses) => callback && callback(responses),
)
}
return { clear, upload, results }
}
export default useUpload

View File

@@ -381,4 +381,49 @@ const applyTransformations = (
ctx.translate(translateX, translateY)
}
}
// ======= IMAGE EDITOR UTILS END ======= //
// ======= IMAGE EDITOR UTILS END ======= //
// ======= UPLOADER UTILS ======= //
const validImageMimeTypes = [
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
"image/svg+xml",
"image/tiff",
"image/bmp",
"image/x-icon",
]
export function generateFileKey(file: File | { name: string }) {
return "size" in file
? `${file.name}_${file.size}_${file.type}_${file.lastModified}`
: file.name + "-preexisting-" + Date.now()
}
export function convertToBytes(input: string): number {
const trimmedInput = input.trim().toLowerCase()
try {
if (trimmedInput.endsWith("kb")) {
const number = parseFloat(trimmedInput.slice(0, -2))
return number * 1024
} else if (trimmedInput.endsWith("mb")) {
const number = parseFloat(trimmedInput.slice(0, -2))
return number * 1024 * 1024
} else if (trimmedInput.endsWith("bytes")) {
const number = parseFloat(trimmedInput.slice(0, -5))
return number
} else if (!isNaN(parseFloat(trimmedInput))) {
return parseFloat(trimmedInput)
} else {
return -1
}
} catch (error) {
return -1
}
}
export function isImage(type: string) {
return !!type && validImageMimeTypes.indexOf(type) > -1
}
// ======= UPLOADER UTILS END ======= //