) => {
+ 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 = (
+
+ variant !== "compact" &&
+ !isLoading &&
+ !disabled &&
+ inputRef.current?.click()
+ }
+ >
+ {isAvatar ? (
+
+ ) : (
+ <>
+
+
+ {variant === "compact" ? (
+ <>
+
+ {message && (
+
+ {message}
+
+ )}
+
+
+ {variant !== "compact" &&
+ localResults &&
+ Object.keys(localResults).length > 0 && (
+
+ >
+ )}
+
+
+ )
+
+ const errorRender = errorMessage && (
+ {errorMessage}
+ )
+
+ const resultsRender = localResults && variant !== "compact" && (
+
+ {Object.entries(localResults).map(([fileName, result]) =>
+ uploadEntryRenderer ? (
+ uploadEntryRenderer(result, () => clear(fileName))
+ ) : (
+ clear(fileName)}
+ demo={!!demo}
+ disabled={!!disabled}
+ key={fileName}
+ result={result}
+ />
+ ),
+ )}
+
+ )
+
+ const uploaderRender = (
+ ) => e.preventDefault()}
+ onDragLeave={(e: MouseEvent) => e.preventDefault()}
+ >
+ {variant !== "compact" ? (
+ {
+ 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}
+
+ ) : (
+ inputRender
+ )}
+ {!isAvatar && errorRender}
+ {!isAvatar && resultsRender}
+
+ )
+
+ 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
diff --git a/src/app/Router.tsx b/src/app/Router.tsx
deleted file mode 100644
index 6b12a09..0000000
--- a/src/app/Router.tsx
+++ /dev/null
@@ -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
diff --git a/src/app/config/constants.ts b/src/app/config/constants.ts
deleted file mode 100644
index e69de29..0000000
diff --git a/src/app/hooks.ts b/src/app/hooks.ts
deleted file mode 100644
index d72616b..0000000
--- a/src/app/hooks.ts
+++ /dev/null
@@ -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 = useSelector
diff --git a/src/app/pages/Home/Home.module.scss b/src/app/pages/Home/Home.module.scss
deleted file mode 100755
index 4aee5e2..0000000
--- a/src/app/pages/Home/Home.module.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-.c-Home {
-
-}
diff --git a/src/app/pages/Home/Home.slice.ts b/src/app/pages/Home/Home.slice.ts
deleted file mode 100644
index 6e35a0c..0000000
--- a/src/app/pages/Home/Home.slice.ts
+++ /dev/null
@@ -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
diff --git a/src/app/pages/Home/Home.test.ts b/src/app/pages/Home/Home.test.ts
deleted file mode 100755
index b4c8c84..0000000
--- a/src/app/pages/Home/Home.test.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import React from "react"
-import Home from "./Home"
-
-describe("Home", () => {
- it("renders without error", () => {
-
- })
-})
\ No newline at end of file
diff --git a/src/app/pages/Home/Home.tsx b/src/app/pages/Home/Home.tsx
deleted file mode 100755
index c881f5d..0000000
--- a/src/app/pages/Home/Home.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import React from "react"
-import "./Home.module.scss"
-
-interface HomeProps {}
-
-const Home = props => {
- return (
-
- In Page Home
-
- )
-}
-
-export default Home
\ No newline at end of file
diff --git a/src/app/pages/Home/index.ts b/src/app/pages/Home/index.ts
deleted file mode 100755
index 50d1f40..0000000
--- a/src/app/pages/Home/index.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import Home from "./Home.jsx"
-
-export default Home
diff --git a/src/app/pages/index.ts b/src/app/pages/index.ts
deleted file mode 100755
index ba9140d..0000000
--- a/src/app/pages/index.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-/* PLOP_INJECT_IMPORT */
-import Home from "./Home"
-
-export {
- /* PLOP_INJECT_EXPORT */
- Home,
-}
\ No newline at end of file
diff --git a/src/app/routes.ts b/src/app/routes.ts
deleted file mode 100644
index 4ba356b..0000000
--- a/src/app/routes.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-const ROUTES = [
- {
- path: "/",
- class: "landing",
- element: "Home",
- },
-]
-
-export default ROUTES
diff --git a/src/app/static/styles/global.scss b/src/app/static/styles/global.scss
deleted file mode 100644
index 7defc06..0000000
--- a/src/app/static/styles/global.scss
+++ /dev/null
@@ -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;
-}
diff --git a/src/app/store.ts b/src/app/store.ts
deleted file mode 100644
index c4249ae..0000000
--- a/src/app/store.ts
+++ /dev/null
@@ -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
-export type AppThunk = ThunkAction<
- ReturnType,
- RootState,
- unknown,
- Action
->
diff --git a/src/app/types/route.d.ts b/src/app/types/route.d.ts
deleted file mode 100644
index 29a8471..0000000
--- a/src/app/types/route.d.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-interface RouteConfig {
- path: String
- class?: String
- element: String | JSX.Element | null
- children?: Array
-}
diff --git a/src/app/utils/helper.tsx b/src/app/utils/helper.tsx
deleted file mode 100644
index 921dac2..0000000
--- a/src/app/utils/helper.tsx
+++ /dev/null
@@ -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 =
- if (route.children) {
- Helper.populateComponentsInRoutes(route.children, components)
- }
- })
- }
-}
-
-export default Helper
diff --git a/src/features/counter/Counter.tsx b/src/features/counter/Counter.tsx
deleted file mode 100644
index cb01aa3..0000000
--- a/src/features/counter/Counter.tsx
+++ /dev/null
@@ -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 (
-
-
-
- {count}
-
-
-
- setIncrementAmount(e.target.value)}
- />
-
-
-
-
-
- )
-}
diff --git a/src/features/counter/counterAPI.ts b/src/features/counter/counterAPI.ts
deleted file mode 100644
index c5c2a7e..0000000
--- a/src/features/counter/counterAPI.ts
+++ /dev/null
@@ -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),
- )
-}
diff --git a/src/features/counter/counterSlice.spec.ts b/src/features/counter/counterSlice.spec.ts
deleted file mode 100644
index 4d465e4..0000000
--- a/src/features/counter/counterSlice.spec.ts
+++ /dev/null
@@ -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)
- })
-})
diff --git a/src/features/counter/counterSlice.ts b/src/features/counter/counterSlice.ts
deleted file mode 100644
index 3870246..0000000
--- a/src/features/counter/counterSlice.ts
+++ /dev/null
@@ -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) => {
- 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
diff --git a/src/index.tsx b/src/index.tsx
deleted file mode 100644
index 4b03c8d..0000000
--- a/src/index.tsx
+++ /dev/null
@@ -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(
-
-
-
-
-
-
- ,
-)
diff --git a/src/useUpload.ts b/src/useUpload.ts
new file mode 100644
index 0000000..257a772
--- /dev/null
+++ b/src/useUpload.ts
@@ -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,
+) => {
+ const [results, setResults] = useState()
+ 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