First commit:
This commit is contained in:
14
index.html
14
index.html
@@ -1,14 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>React Redux App</title>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
19327
package-lock.json
generated
19327
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
31
package.json
31
package.json
@@ -1,31 +1,17 @@
|
||||
{
|
||||
"name": "@armco/react-vite-rtk-template",
|
||||
"name": "@armco/uploader",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"start": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"generate": "plop",
|
||||
"atom": "plop atom",
|
||||
"molecule": "plop molecule",
|
||||
"component": "plop component",
|
||||
"page": "plop page",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest",
|
||||
"format": "prettier --write .",
|
||||
"lint": "eslint .",
|
||||
"type-check": "tsc"
|
||||
"lint": "eslint ."
|
||||
},
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^1.8.1",
|
||||
"react": "^18.2.0",
|
||||
"react-app-polyfill": "^3.0.0",
|
||||
"react-dev-utils": "^12.0.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-redux": "^8.0.1",
|
||||
"react-router-dom": "^6.13.0"
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/dom": "^9.2.0",
|
||||
@@ -40,7 +26,6 @@
|
||||
"eslint-config-react-app": "^7.0.1",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"jsdom": "^21.1.0",
|
||||
"plop": "^3.1.2",
|
||||
"prettier": "^2.7.1",
|
||||
"prettier-config-nick": "^1.0.2",
|
||||
"sass": "^1.63.4",
|
||||
@@ -48,6 +33,10 @@
|
||||
"vite": "^4.0.0",
|
||||
"vitest": "^0.30.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">16.8.0",
|
||||
"react-dom": ">16.8.0"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
@@ -65,7 +54,7 @@
|
||||
"main": "index.tsx",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/ReStruct-Corporate-Advantage/.git"
|
||||
"url": "git+https://github.com/ReStruct-Corporate-Advantage/uploader.git"
|
||||
},
|
||||
"keywords": [
|
||||
"components",
|
||||
@@ -75,7 +64,7 @@
|
||||
],
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/ReStruct-Corporate-Advantage/react-vite-rtk-template/issues"
|
||||
"url": "https://github.com/ReStruct-Corporate-Advantage/uploader/issues"
|
||||
},
|
||||
"homepage": "https://github.com/ReStruct-Corporate-Advantage/react-vite-rtk-template#readme"
|
||||
"homepage": "https://github.com/ReStruct-Corporate-Advantage/uploader#readme"
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.c-{{pascalCase name}} {
|
||||
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
import React from "react"
|
||||
import {{pascalCase name}} from "./{{pascalCase name}}"
|
||||
|
||||
describe("{{pascalCase name}}", () => {
|
||||
it("renders without error", () => {
|
||||
|
||||
})
|
||||
})
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from "react"
|
||||
import "./{{pascalCase name}}.component.scss"
|
||||
|
||||
interface {{pascalCase name}}Props {}
|
||||
|
||||
const {{pascalCase name}} = (props: {{pascalCase name}}Props): JSX.Element => {
|
||||
return <div className="c-{{pascalCase name}}">In Component {{pascalCase name}}</div>
|
||||
}
|
||||
|
||||
export default {{pascalCase name}}
|
||||
@@ -1,3 +0,0 @@
|
||||
import {{pascalCase name}} from "./{{pascalCase name}}.jsx"
|
||||
|
||||
export default {{pascalCase name}}
|
||||
@@ -1,3 +0,0 @@
|
||||
.c-{{pascalCase name}} {
|
||||
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { createSlice } from "@reduxjs/toolkit"
|
||||
|
||||
export interface {{pascalCase name}}State {}
|
||||
|
||||
const initialState: {{pascalCase name}}State = {}
|
||||
|
||||
export const {{snakeCase name}}Slice = createSlice({
|
||||
name: "{{snakeCase name}}",
|
||||
initialState,
|
||||
reducers: {
|
||||
increment: (state) => {},
|
||||
},
|
||||
extraReducers: (builder) => {},
|
||||
})
|
||||
|
||||
export const { increment } = {{snakeCase name}}Slice.actions
|
||||
|
||||
export default {{snakeCase name}}Slice.reducer
|
||||
@@ -1,8 +0,0 @@
|
||||
import React from "react"
|
||||
import {{pascalCase name}} from "./{{pascalCase name}}"
|
||||
|
||||
describe("{{pascalCase name}}", () => {
|
||||
it("renders without error", () => {
|
||||
|
||||
})
|
||||
})
|
||||
@@ -1,10 +0,0 @@
|
||||
import React from "react"
|
||||
import "./{{pascalCase name}}.module.scss"
|
||||
|
||||
interface {{pascalCase name}}Props {}
|
||||
|
||||
const {{pascalCase name}} = (props: {{pascalCase name}}Props): JSX.Element => {
|
||||
return <div className="c-{{pascalCase name}}">In Page {{pascalCase name}}</div>
|
||||
}
|
||||
|
||||
export default {{pascalCase name}}
|
||||
@@ -1,3 +0,0 @@
|
||||
import {{pascalCase name}} from "./{{pascalCase name}}.jsx"
|
||||
|
||||
export default {{pascalCase name}}
|
||||
@@ -1,5 +0,0 @@
|
||||
/* PLOP_INJECT_IMPORT */
|
||||
|
||||
export {
|
||||
/* PLOP_INJECT_EXPORT */
|
||||
}
|
||||
207
plopfile.cjs
207
plopfile.cjs
@@ -1,207 +0,0 @@
|
||||
module.exports = (plop) => {
|
||||
plop.setGenerator("component", {
|
||||
description: "Create a component",
|
||||
prompts: [
|
||||
{
|
||||
type: "input",
|
||||
name: "name",
|
||||
message: "What is your component name?",
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/{{pascalCase name}}/{{pascalCase name}}.tsx",
|
||||
templateFile: "plop-templates/Component/Component.tsx.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/{{pascalCase name}}/{{pascalCase name}}.test.ts",
|
||||
templateFile: "plop-templates/Component/Component.test.ts.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/{{pascalCase name}}/{{pascalCase name}}.component.scss",
|
||||
templateFile: "plop-templates/Component/Component.component.scss.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/{{pascalCase name}}/index.ts",
|
||||
templateFile: "plop-templates/Component/index.ts.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/index.ts",
|
||||
templateFile: "plop-templates/injectable-index.ts.hbs",
|
||||
skipIfExists: true,
|
||||
},
|
||||
{
|
||||
type: "append",
|
||||
path: "src/app/components/index.ts",
|
||||
pattern: `/* PLOP_INJECT_IMPORT */`,
|
||||
template: `import {{pascalCase name}} from "./{{pascalCase name}}"`,
|
||||
},
|
||||
{
|
||||
type: "append",
|
||||
path: "src/app/components/index.ts",
|
||||
pattern: `/* PLOP_INJECT_EXPORT */`,
|
||||
template: `\t{{pascalCase name}},`,
|
||||
},
|
||||
],
|
||||
})
|
||||
plop.setGenerator("atom", {
|
||||
description: "Create a component",
|
||||
prompts: [
|
||||
{
|
||||
type: "input",
|
||||
name: "name",
|
||||
message: "What is your component name?",
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/atoms/{{pascalCase name}}/{{pascalCase name}}.tsx",
|
||||
templateFile: "plop-templates/Component/Component.tsx.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/atoms/{{pascalCase name}}/{{pascalCase name}}.test.ts",
|
||||
templateFile: "plop-templates/Component/Component.test.ts.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/atoms/{{pascalCase name}}/{{pascalCase name}}.component.scss",
|
||||
templateFile: "plop-templates/Component/Component.component.scss.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/atoms/{{pascalCase name}}/index.ts",
|
||||
templateFile: "plop-templates/Component/index.ts.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/index.ts",
|
||||
templateFile: "plop-templates/injectable-index.ts.hbs",
|
||||
skipIfExists: true,
|
||||
},
|
||||
{
|
||||
type: "append",
|
||||
path: "src/app/components/index.ts",
|
||||
pattern: `/* PLOP_INJECT_IMPORT */`,
|
||||
template: `import {{pascalCase name}} from "./atoms/{{pascalCase name}}"`,
|
||||
},
|
||||
{
|
||||
type: "append",
|
||||
path: "src/app/components/index.ts",
|
||||
pattern: `/* PLOP_INJECT_EXPORT */`,
|
||||
template: `\t{{pascalCase name}},`,
|
||||
},
|
||||
],
|
||||
})
|
||||
plop.setGenerator("molecule", {
|
||||
description: "Create a rich component",
|
||||
prompts: [
|
||||
{
|
||||
type: "input",
|
||||
name: "name",
|
||||
message: "What is your component name?",
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/molecules/{{pascalCase name}}/{{pascalCase name}}.tsx",
|
||||
templateFile: "plop-templates/Component/Component.tsx.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/molecules/{{pascalCase name}}/{{pascalCase name}}.test.ts",
|
||||
templateFile: "plop-templates/Component/Component.test.ts.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/molecules/{{pascalCase name}}/{{pascalCase name}}.component.scss",
|
||||
templateFile: "plop-templates/Component/Component.component.scss.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/molecules/{{pascalCase name}}/index.ts",
|
||||
templateFile: "plop-templates/Component/index.ts.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/components/index.ts",
|
||||
templateFile: "plop-templates/injectable-index.ts.hbs",
|
||||
skipIfExists: true,
|
||||
},
|
||||
{
|
||||
type: "append",
|
||||
path: "src/app/components/index.ts",
|
||||
pattern: `/* PLOP_INJECT_IMPORT */`,
|
||||
template: `import {{pascalCase name}} from "./molecules/{{pascalCase name}}"`,
|
||||
},
|
||||
{
|
||||
type: "append",
|
||||
path: "src/app/components/index.ts",
|
||||
pattern: `/* PLOP_INJECT_EXPORT */`,
|
||||
template: `\t{{pascalCase name}},`,
|
||||
},
|
||||
],
|
||||
})
|
||||
plop.setGenerator("page", {
|
||||
description: "Create a page",
|
||||
prompts: [
|
||||
{
|
||||
type: "input",
|
||||
name: "name",
|
||||
message: "What is your page name?",
|
||||
},
|
||||
],
|
||||
actions: [
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/pages/{{pascalCase name}}/{{pascalCase name}}.tsx",
|
||||
templateFile: "plop-templates/Page/Page.tsx.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/pages/{{pascalCase name}}/{{pascalCase name}}.test.ts",
|
||||
templateFile: "plop-templates/Page/Page.test.ts.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/pages/{{pascalCase name}}/{{pascalCase name}}.module.scss",
|
||||
templateFile: "plop-templates/Page/Page.module.scss.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/pages/{{pascalCase name}}/index.ts",
|
||||
templateFile: "plop-templates/Page/index.ts.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/pages/{{pascalCase name}}/{{pascalCase name}}.slice.ts",
|
||||
templateFile: "plop-templates/Page/Page.slice.ts.hbs",
|
||||
},
|
||||
{
|
||||
type: "add",
|
||||
path: "src/app/pages/index.ts",
|
||||
templateFile: "plop-templates/injectable-index.ts.hbs",
|
||||
skipIfExists: true,
|
||||
},
|
||||
{
|
||||
type: "append",
|
||||
path: "src/app/pages/index.ts",
|
||||
pattern: `/* PLOP_INJECT_IMPORT */`,
|
||||
template: `import {{pascalCase name}} from "./{{pascalCase name}}"`,
|
||||
},
|
||||
{
|
||||
type: "append",
|
||||
path: "src/app/pages/index.ts",
|
||||
pattern: `/* PLOP_INJECT_EXPORT */`,
|
||||
template: `\t{{pascalCase name}},`,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
38
src/Uploader.component.scss
Executable file
38
src/Uploader.component.scss
Executable 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);
|
||||
}
|
||||
|
||||
}
|
||||
605
src/Uploader.tsx
Executable file
605
src/Uploader.tsx
Executable file
@@ -0,0 +1,605 @@
|
||||
import {
|
||||
ChangeEvent,
|
||||
MouseEvent,
|
||||
memo,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react"
|
||||
import { v4 as uuid } from "uuid"
|
||||
import { NativeTypes } from "react-dnd-html5-backend"
|
||||
import {
|
||||
ArAlertType,
|
||||
ArButtonVariants,
|
||||
ArPopoverSlots,
|
||||
ArProgress,
|
||||
ArSizes,
|
||||
FunctionType,
|
||||
ImageProps,
|
||||
UploaderProps,
|
||||
UploadResult,
|
||||
UploadResults,
|
||||
} from "@armco/types"
|
||||
import {
|
||||
useAppDispatch,
|
||||
useUpload,
|
||||
notify,
|
||||
Droppable,
|
||||
Helper,
|
||||
Button,
|
||||
Text,
|
||||
LoadableIcon,
|
||||
ProgressIndicator,
|
||||
Tooltip,
|
||||
Avatar,
|
||||
Network,
|
||||
API_CONFIG,
|
||||
} from "../.."
|
||||
import "./Uploader.component.scss"
|
||||
|
||||
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) && (
|
||||
<LoadableIcon
|
||||
classes={disabled ? "" : "cursor-pointer"}
|
||||
color={result.status === ArProgress.FAILED ? "#ff4d4f" : "grey"}
|
||||
icon="ri.RiDeleteBin6Fill"
|
||||
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): JSX.Element => {
|
||||
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 [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 dispatch = useAppDispatch()
|
||||
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 = Helper.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.")
|
||||
dispatch(
|
||||
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)
|
||||
dispatch(
|
||||
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
|
||||
Network.put(url, resultClone).then((res) => {
|
||||
if (res.status === 200) {
|
||||
dispatch(
|
||||
notify({
|
||||
message: "Image updaetd successfully!",
|
||||
type: ArAlertType.SUCCESS,
|
||||
uid: uuid(),
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
dispatch(
|
||||
notify({
|
||||
message: "Failed to update image!",
|
||||
type: ArAlertType.ERROR,
|
||||
uid: uuid(),
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
dispatch(
|
||||
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
|
||||
/>
|
||||
)}
|
||||
{
|
||||
<LoadableIcon
|
||||
classes={`ms-2 ${
|
||||
disabled ? "" : "cursor-pointer"
|
||||
}`}
|
||||
color="red"
|
||||
icon="io5.IoClose"
|
||||
onClick={() => {
|
||||
if (disabled) {
|
||||
clear()
|
||||
if (inputRef.current) {
|
||||
inputRef.current.value = ""
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<LoadableIcon
|
||||
classes={variant === "large" ? "mb-2" : "me-3"}
|
||||
icon="io5.IoFileTrayOutline"
|
||||
size={variant === "large" ? "3rem" : "1rem"}
|
||||
color="#1677ff"
|
||||
/>
|
||||
<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}
|
||||
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
|
||||
@@ -1,12 +0,0 @@
|
||||
import { useRoutes } from "react-router-dom"
|
||||
import * as pages from "./pages"
|
||||
import Helper from "./utils/helper"
|
||||
import ROUTES from "./routes"
|
||||
|
||||
Helper.populateComponentsInRoutes(ROUTES, pages)
|
||||
|
||||
interface RouterProps {}
|
||||
|
||||
const Router = (props: RouterProps): JSX.Element | null => useRoutes(ROUTES)
|
||||
|
||||
export default Router
|
||||
@@ -1,6 +0,0 @@
|
||||
import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"
|
||||
import type { RootState, AppDispatch } from "./store"
|
||||
|
||||
// Use throughout your app instead of plain `useDispatch` and `useSelector`
|
||||
export const useAppDispatch: () => AppDispatch = useDispatch
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
|
||||
@@ -1,3 +0,0 @@
|
||||
.c-Home {
|
||||
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { createSlice } from "@reduxjs/toolkit"
|
||||
|
||||
export interface HomeState {}
|
||||
|
||||
const initialState: HomeState = {}
|
||||
|
||||
export const homeSlice = createSlice({
|
||||
name: "home",
|
||||
initialState,
|
||||
reducers: {
|
||||
increment: (state) => {},
|
||||
},
|
||||
extraReducers: (builder) => {},
|
||||
})
|
||||
|
||||
export const { increment } = homeSlice.actions
|
||||
|
||||
export default homeSlice.reducer
|
||||
@@ -1,8 +0,0 @@
|
||||
import React from "react"
|
||||
import Home from "./Home"
|
||||
|
||||
describe("Home", () => {
|
||||
it("renders without error", () => {
|
||||
|
||||
})
|
||||
})
|
||||
@@ -1,14 +0,0 @@
|
||||
import React from "react"
|
||||
import "./Home.module.scss"
|
||||
|
||||
interface HomeProps {}
|
||||
|
||||
const Home = props => {
|
||||
return (
|
||||
<div className="c-Home">
|
||||
In Page Home
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
@@ -1,3 +0,0 @@
|
||||
import Home from "./Home.jsx"
|
||||
|
||||
export default Home
|
||||
@@ -1,7 +0,0 @@
|
||||
/* PLOP_INJECT_IMPORT */
|
||||
import Home from "./Home"
|
||||
|
||||
export {
|
||||
/* PLOP_INJECT_EXPORT */
|
||||
Home,
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
const ROUTES = [
|
||||
{
|
||||
path: "/",
|
||||
class: "landing",
|
||||
element: "Home",
|
||||
},
|
||||
]
|
||||
|
||||
export default ROUTES
|
||||
@@ -1,18 +0,0 @@
|
||||
html, body, #root {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit"
|
||||
import counterReducer from "../features/counter/counterSlice"
|
||||
|
||||
export const store = configureStore({
|
||||
reducer: {
|
||||
counter: counterReducer,
|
||||
},
|
||||
})
|
||||
|
||||
export type AppDispatch = typeof store.dispatch
|
||||
export type RootState = ReturnType<typeof store.getState>
|
||||
export type AppThunk<ReturnType = void> = ThunkAction<
|
||||
ReturnType,
|
||||
RootState,
|
||||
unknown,
|
||||
Action<string>
|
||||
>
|
||||
6
src/app/types/route.d.ts
vendored
6
src/app/types/route.d.ts
vendored
@@ -1,6 +0,0 @@
|
||||
interface RouteConfig {
|
||||
path: String
|
||||
class?: String
|
||||
element: String | JSX.Element | null
|
||||
children?: Array<RouteConfig>
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
class Helper {
|
||||
static populateComponentsInRoutes(routes: RouteConfig[], components: any) {
|
||||
routes &&
|
||||
routes.forEach((route) => {
|
||||
const Component: JSX.ElementType =
|
||||
components[route.element as keyof object]
|
||||
route.element = <Component />
|
||||
if (route.children) {
|
||||
Helper.populateComponentsInRoutes(route.children, components)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export default Helper
|
||||
@@ -1,67 +0,0 @@
|
||||
import { useState } from "react"
|
||||
|
||||
import { useAppSelector, useAppDispatch } from "../../app/hooks"
|
||||
import {
|
||||
decrement,
|
||||
increment,
|
||||
incrementByAmount,
|
||||
incrementAsync,
|
||||
incrementIfOdd,
|
||||
selectCount,
|
||||
} from "./counterSlice"
|
||||
|
||||
export function Counter() {
|
||||
const count = useAppSelector(selectCount)
|
||||
const dispatch = useAppDispatch()
|
||||
const [incrementAmount, setIncrementAmount] = useState("2")
|
||||
|
||||
const incrementValue = Number(incrementAmount) || 0
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="row">
|
||||
<button
|
||||
className="button"
|
||||
aria-label="Decrement value"
|
||||
onClick={() => dispatch(decrement())}
|
||||
>
|
||||
-
|
||||
</button>
|
||||
<span className="value">{count}</span>
|
||||
<button
|
||||
className="button"
|
||||
aria-label="Increment value"
|
||||
onClick={() => dispatch(increment())}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
<div className="row">
|
||||
<input
|
||||
className="textbox"
|
||||
aria-label="Set increment amount"
|
||||
value={incrementAmount}
|
||||
onChange={(e) => setIncrementAmount(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className="button"
|
||||
onClick={() => dispatch(incrementByAmount(incrementValue))}
|
||||
>
|
||||
Add Amount
|
||||
</button>
|
||||
<button
|
||||
className="asyncButton"
|
||||
onClick={() => dispatch(incrementAsync(incrementValue))}
|
||||
>
|
||||
Add Async
|
||||
</button>
|
||||
<button
|
||||
className="button"
|
||||
onClick={() => dispatch(incrementIfOdd(incrementValue))}
|
||||
>
|
||||
Add If Odd
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// A mock function to mimic making an async request for data
|
||||
export function fetchCount(amount = 1) {
|
||||
return new Promise<{ data: number }>((resolve) =>
|
||||
setTimeout(() => resolve({ data: amount }), 500),
|
||||
)
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
import counterReducer, {
|
||||
CounterState,
|
||||
increment,
|
||||
decrement,
|
||||
incrementByAmount,
|
||||
} from "./counterSlice"
|
||||
|
||||
describe("counter reducer", () => {
|
||||
const initialState: CounterState = {
|
||||
value: 3,
|
||||
status: "idle",
|
||||
}
|
||||
it("should handle initial state", () => {
|
||||
expect(counterReducer(undefined, { type: "unknown" })).toEqual({
|
||||
value: 0,
|
||||
status: "idle",
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle increment", () => {
|
||||
const actual = counterReducer(initialState, increment())
|
||||
expect(actual.value).toEqual(4)
|
||||
})
|
||||
|
||||
it("should handle decrement", () => {
|
||||
const actual = counterReducer(initialState, decrement())
|
||||
expect(actual.value).toEqual(2)
|
||||
})
|
||||
|
||||
it("should handle incrementByAmount", () => {
|
||||
const actual = counterReducer(initialState, incrementByAmount(2))
|
||||
expect(actual.value).toEqual(5)
|
||||
})
|
||||
})
|
||||
@@ -1,84 +0,0 @@
|
||||
import { createAsyncThunk, createSlice, PayloadAction } from "@reduxjs/toolkit"
|
||||
import { RootState, AppThunk } from "../../app/store"
|
||||
import { fetchCount } from "./counterAPI"
|
||||
|
||||
export interface CounterState {
|
||||
value: number
|
||||
status: "idle" | "loading" | "failed"
|
||||
}
|
||||
|
||||
const initialState: CounterState = {
|
||||
value: 0,
|
||||
status: "idle",
|
||||
}
|
||||
|
||||
// The function below is called a thunk and allows us to perform async logic. It
|
||||
// can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This
|
||||
// will call the thunk with the `dispatch` function as the first argument. Async
|
||||
// code can then be executed and other actions can be dispatched. Thunks are
|
||||
// typically used to make async requests.
|
||||
export const incrementAsync = createAsyncThunk(
|
||||
"counter/fetchCount",
|
||||
async (amount: number) => {
|
||||
const response = await fetchCount(amount)
|
||||
// The value we return becomes the `fulfilled` action payload
|
||||
return response.data
|
||||
},
|
||||
)
|
||||
|
||||
export const counterSlice = createSlice({
|
||||
name: "counter",
|
||||
initialState,
|
||||
// The `reducers` field lets us define reducers and generate associated actions
|
||||
reducers: {
|
||||
increment: (state) => {
|
||||
// Redux Toolkit allows us to write "mutating" logic in reducers. It
|
||||
// doesn"t actually mutate the state because it uses the Immer library,
|
||||
// which detects changes to a "draft state" and produces a brand new
|
||||
// immutable state based off those changes
|
||||
state.value += 1
|
||||
},
|
||||
decrement: (state) => {
|
||||
state.value -= 1
|
||||
},
|
||||
// Use the PayloadAction type to declare the contents of `action.payload`
|
||||
incrementByAmount: (state, action: PayloadAction<number>) => {
|
||||
state.value += action.payload
|
||||
},
|
||||
},
|
||||
// The `extraReducers` field lets the slice handle actions defined elsewhere,
|
||||
// including actions generated by createAsyncThunk or in other slices.
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
.addCase(incrementAsync.pending, (state) => {
|
||||
state.status = "loading"
|
||||
})
|
||||
.addCase(incrementAsync.fulfilled, (state, action) => {
|
||||
state.status = "idle"
|
||||
state.value += action.payload
|
||||
})
|
||||
.addCase(incrementAsync.rejected, (state) => {
|
||||
state.status = "failed"
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
export const { increment, decrement, incrementByAmount } = counterSlice.actions
|
||||
|
||||
// The function below is called a selector and allows us to select a value from
|
||||
// the state. Selectors can also be defined inline where they"re used instead of
|
||||
// in the slice file. For example: `useSelector((state: RootState) => state.counter.value)`
|
||||
export const selectCount = (state: RootState) => state.counter.value
|
||||
|
||||
// We can also write thunks by hand, which may contain both sync and async logic.
|
||||
// Here"s an example of conditionally dispatching actions based on current state.
|
||||
export const incrementIfOdd =
|
||||
(amount: number): AppThunk =>
|
||||
(dispatch, getState) => {
|
||||
const currentValue = selectCount(getState())
|
||||
if (currentValue % 2 === 1) {
|
||||
dispatch(incrementByAmount(amount))
|
||||
}
|
||||
}
|
||||
|
||||
export default counterSlice.reducer
|
||||
@@ -1,19 +0,0 @@
|
||||
import React from "react"
|
||||
import ReactDOM from "react-dom/client"
|
||||
import { BrowserRouter } from "react-router-dom"
|
||||
import { Provider } from "react-redux"
|
||||
import { store } from "./app/store"
|
||||
import Router from "./app/Router"
|
||||
import "./app/static/styles/global.scss"
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement)
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<Provider store={store}>
|
||||
<Router />
|
||||
</Provider>
|
||||
</BrowserRouter>
|
||||
</React.StrictMode>,
|
||||
)
|
||||
216
src/useUpload.ts
Normal file
216
src/useUpload.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
// useUpload.ts
|
||||
import { useEffect, useRef, useState } from "react"
|
||||
import {
|
||||
ArProgress,
|
||||
FunctionType,
|
||||
UploadHandler,
|
||||
UploadResults,
|
||||
} from "@armco/types"
|
||||
import { Helper, Network } 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[Helper.generateFileKey(file)] = {
|
||||
...file,
|
||||
progress: 100,
|
||||
status: ArProgress.COMPLETED,
|
||||
file: typeof file === "object" ? file : { name: 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 = Helper.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 Network.upload(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 = Helper.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
|
||||
Reference in New Issue
Block a user