Skip to content

Commit

Permalink
1153 Dropzone-style file upload component (#1437)
Browse files Browse the repository at this point in the history
* First pass at a dropzone Cloudinary uploader

* Style the progress control and provide more complete flow

* Tighten up Dropzone usability

* Add id for accessibility

* Update Changelog with #1437
  • Loading branch information
jaredcwhite authored Jul 6, 2021
1 parent cf214be commit f95fbca
Show file tree
Hide file tree
Showing 9 changed files with 217 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ All notable changes to this project will be documented in this file. The format

- Added:

- Dropzone-style file upload component ([#1437](https://github.com/bloom-housing/bloom/pull/1437)) (Jared White)
- Table image thumbnails component along with minimal left/right flush table styles ([#1339](https://github.com/bloom-housing/bloom/pull/1339)) (Jared White)
- Tabs component based on React Tabs ([#1305](https://github.com/bloom-housing/bloom/pull/1305)) (Jared White)
- **Note**: the previous `Tab` child of `TabNav` has been renamed to `TabNavItem`
Expand Down
1 change: 1 addition & 0 deletions ui-components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"react": "^16.13.1",
"react-accessible-accordion": "^3.3.3",
"react-dom": "^16.13.1",
"react-dropzone": "^11.3.2",
"react-focus-lock": "^2.4.1",
"react-map-gl": "^5.2.9",
"react-media": "^1.10.0",
Expand Down
39 changes: 39 additions & 0 deletions ui-components/src/forms/CloudinaryUpload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import axios from "axios"

interface CloudinaryUploadProps {
file: File
onUploadProgress: (progress: number) => void
cloudName: string
uploadPreset: string
tag?: string
}

export const CloudinaryUpload = async ({
file,
onUploadProgress,
cloudName,
uploadPreset,
tag = "browser_upload",
}: CloudinaryUploadProps) => {
const url = `https://api.cloudinary.com/v1_1/${cloudName}/upload`
const data = new FormData()
data.append("upload_preset", uploadPreset)
data.append("tags", tag)
data.append("file", file)

if (!cloudName || cloudName == "" || !uploadPreset || uploadPreset == "") {
const err = "Please supply a cloud name and upload preset for Cloudinary"
alert(err)
throw err
}

const response = await axios.request({
method: "post",
url: url,
data: data,
onUploadProgress: (p) => {
onUploadProgress(parseInt(((p.loaded / p.total) * 100).toFixed(0), 10))
},
})
return response
}
17 changes: 17 additions & 0 deletions ui-components/src/forms/Dropzone.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
.dropzone {
@apply border-2;
@apply border-gray-600;
@apply border-dashed;
@apply text-center;
padding: 2.5rem;
max-width: 32rem;
cursor: pointer;

&.is-active {
@apply bg-accent-cool-light;
}
}

.dropzone__progress {
max-width: 250px;
}
48 changes: 48 additions & 0 deletions ui-components/src/forms/Dropzone.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import * as React from "react"
import { withKnobs, text } from "@storybook/addon-knobs"
import { CloudinaryUpload } from "./CloudinaryUpload"
import { Dropzone } from "./Dropzone"

export default {
title: "Forms/Dropzone",
decorators: [(storyFn: any) => <div style={{ padding: "1rem" }}>{storyFn()}</div>, withKnobs],
}

export const defaultDropzone = () => {
const [progressValue, setProgressValue] = React.useState(0)
const [cloudinaryImage, setCloudinaryImage] = React.useState("")
const cloudName = text("Cloudinary Cloud", "")
const uploadPreset = text("Upload Preset", "")

const exampleUploader = (file: File) => {
CloudinaryUpload({
file: file,
onUploadProgress: (progress) => {
setProgressValue(progress)
},
cloudName,
uploadPreset,
}).then((response) => {
setProgressValue(100)
const imgUrl = `https://res.cloudinary.com/${cloudName}/image/upload/w_400,c_limit,q_65/${response.data.public_id}.jpg`
setCloudinaryImage(imgUrl)
})
}

return (
<>
<Dropzone
id="test-uploading"
label="Upload File"
helptext="Select JPEG or PNG files"
uploader={exampleUploader}
accept="image/*"
progress={progressValue}
/>
{progressValue == 0 && (
<p className="mt-16">(Provide Cloudinary credentials via the Knobs below.)</p>
)}
<img src={cloudinaryImage} style={{ width: "200px" }} />
</>
)
}
67 changes: 67 additions & 0 deletions ui-components/src/forms/Dropzone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import React, { useCallback } from "react"
import { useDropzone } from "react-dropzone"
import { t } from "../helpers/translator"
import "./Dropzone.scss"

interface DropzoneProps {
uploader: (file: File) => void
id: string
label: string
helptext?: string
accept?: string | string[]
progress?: number
className?: string
}

const Dropzone = (props: DropzoneProps) => {
const { uploader } = props
const classNames = ["field"]
if (props.className) classNames.push(props.className)

const onDrop = useCallback(
(acceptedFiles) => {
acceptedFiles.forEach((file: File) => uploader(file))
},
[uploader]
)
const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop,
accept: props.accept,
maxFiles: 1,
})

const dropzoneClasses = ["dropzone", "control"]
if (isDragActive) dropzoneClasses.push("is-active")

// Three states:
// * File dropzone by default
// * Progress > 0 and < 100 shows a progress bar
// * Progress 100 doesn't show progress bar or dropzone
return (
<div className={classNames.join(" ")}>
<label htmlFor={props.id} className="label">
{props.label}
</label>
{props.helptext && <p className="view-item__label mt-2 mb-4">{props.helptext}</p>}
{props.progress && props.progress === 100 ? (
<></>
) : props.progress && props.progress > 0 ? (
<progress className="dropzone__progress" max="100" value={props.progress}></progress>
) : (
<div className={dropzoneClasses.join(" ")} {...getRootProps()}>
<input id={props.id} {...getInputProps()} />
{isDragActive ? (
<p>{t("t.dropFilesHere")}</p>
) : (
<p>
{t("t.dragFilesHere")} {t("t.or")}{" "}
<u className="text-primary">{t("t.chooseFromFolder").toLowerCase()}</u>
</p>
)}
</div>
)}
</div>
)
}

export { Dropzone as default, Dropzone }
20 changes: 20 additions & 0 deletions ui-components/src/global/forms.scss
Original file line number Diff line number Diff line change
Expand Up @@ -332,3 +332,23 @@ input[type="number"] {
@apply block;
@apply mb-8;
}

progress,
::-webkit-progress-bar {
appearance: none;
width: 100%;
@apply bg-gray-400;
border: 0;
height: 12px;
border-radius: 6px;
}
::-webkit-progress-value {
border-radius: 6px;
@apply bg-primary;
transition: width 0.25s;
}
::-moz-progress-bar {
border-radius: 6px;
@apply bg-primary;
transition: width 0.25s;
}
3 changes: 3 additions & 0 deletions ui-components/src/locales/general.json
Original file line number Diff line number Diff line change
Expand Up @@ -1087,12 +1087,15 @@
"call": "Call",
"cancel": "Cancel",
"confirm": "Confirm",
"chooseFromFolder": "Choose from folder",
"day": "Day",
"delete": "Delete",
"deposit": "Deposit",
"done": "Done",
"description": "Enter Description",
"emailAddressPlaceholder": "you@myemail.com",
"dragFilesHere": "Drag files here",
"dropFilesHere": "Drop files here…",
"export": "Export",
"enterAmount": "Enter amount",
"filter": "Filter",
Expand Down
21 changes: 21 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6048,6 +6048,11 @@ atob@^2.1.2:
resolved "https://registry.yarnpkg.com/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9"
integrity sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==

attr-accept@^2.2.1:
version "2.2.2"
resolved "https://registry.yarnpkg.com/attr-accept/-/attr-accept-2.2.2.tgz#646613809660110749e92f2c10833b70968d929b"
integrity sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==

autoprefixer@^9.4.5, autoprefixer@^9.7.2:
version "9.8.6"
resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.6.tgz#3b73594ca1bf9266320c5acf1588d74dea74210f"
Expand Down Expand Up @@ -9958,6 +9963,13 @@ file-loader@^6.0.0:
loader-utils "^2.0.0"
schema-utils "^3.0.0"

file-selector@^0.2.2:
version "0.2.4"
resolved "https://registry.yarnpkg.com/file-selector/-/file-selector-0.2.4.tgz#7b98286f9dbb9925f420130ea5ed0a69238d4d80"
integrity sha512-ZDsQNbrv6qRi1YTDOEWzf5J2KjZ9KMI1Q2SGeTkCJmNNW25Jg4TW4UMcmoqcg4WrAyKRcpBXdbWRxkfrOzVRbA==
dependencies:
tslib "^2.0.3"

file-system-cache@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/file-system-cache/-/file-system-cache-1.0.5.tgz#84259b36a2bbb8d3d6eb1021d3132ffe64cfff4f"
Expand Down Expand Up @@ -16400,6 +16412,15 @@ react-draggable@^4.0.3:
classnames "^2.2.5"
prop-types "^15.6.0"

react-dropzone@^11.3.2:
version "11.3.2"
resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-11.3.2.tgz#2efb6af800a4779a9daa1e7ba1f8d51d0ab862d7"
integrity sha512-Z0l/YHcrNK1r85o6RT77Z5XgTARmlZZGfEKBl3tqTXL9fZNQDuIdRx/J0QjvR60X+yYu26dnHeaG2pWU+1HHvw==
dependencies:
attr-accept "^2.2.1"
file-selector "^0.2.2"
prop-types "^15.7.2"

react-element-to-jsx-string@^14.3.1:
version "14.3.2"
resolved "https://registry.yarnpkg.com/react-element-to-jsx-string/-/react-element-to-jsx-string-14.3.2.tgz#c0000ed54d1f8b4371731b669613f2d4e0f63d5c"
Expand Down

0 comments on commit f95fbca

Please sign in to comment.