improve creator

This commit is contained in:
srgooglo 2022-10-13 21:56:40 +02:00
parent 7e74e7c1cc
commit 44750cb506
2 changed files with 325 additions and 66 deletions

View File

@ -1,46 +1,46 @@
import React from "react" import React from "react"
import * as antd from "antd" import * as antd from "antd"
import classnames from "classnames" import classnames from "classnames"
import { DateTime } from "luxon"
import humanSize from "@tsmx/human-readable"
import { Icons } from "components/Icons" import { Icons } from "components/Icons"
import PostAdditions from "components/PostCard/components/additions"
import "./index.less" import "./index.less"
// TODO: Handle `cntr+v` to paste data from the clipboard to the post additions const DEFAULT_POST_POLICY = {
// TODO: Fetch `maxMessageLength` value from server API maxMessageLength: 512,
const maxMessageLength = 512 acceptedMimeTypes: ["image/gif", "image/png", "image/jpeg", "image/bmp"],
maximumFileSize: 10 * 1024 * 1024,
maximunFilesPerRequest: 10
}
export default (props) => { export default (props) => {
const api = window.app.api.withEndpoints("main") const api = window.app.api.withEndpoints("main")
const additionsRef = React.useRef(null) const creatorRef = React.useRef(null)
const [pending, setPending] = React.useState([]) const [pending, setPending] = React.useState([])
const [loading, setLoading] = React.useState(false) const [loading, setLoading] = React.useState(false)
const [uploaderVisible, setUploaderVisible] = React.useState(false) const [uploaderVisible, setUploaderVisible] = React.useState(false)
const [focused, setFocused] = React.useState(false) const [focused, setFocused] = React.useState(false)
const [postData, setPostData] = React.useState({ const [postMessage, setPostMessage] = React.useState("")
message: "", const [postAttachments, setPostAttachments] = React.useState([])
additions: [] const [fileList, setFileList] = React.useState([])
})
const [uploadPolicy, setUploadPolicy] = React.useState(null) const [postingPolicy, setPostingPolicy] = React.useState(DEFAULT_POST_POLICY)
const updatePostData = (update) => {
setPostData({
...postData,
...update
})
}
const cleanPostData = () => { const cleanPostData = () => {
setPostData({ setPostMessage("")
message: "", setPostAttachments([])
date: new Date(), setFileList([])
additions: [] }
})
const fetchUploadPolicy = async () => {
const policy = await api.get.postingPolicy()
setPostingPolicy(policy)
} }
const submit = () => { const submit = () => {
@ -50,7 +50,13 @@ export default (props) => {
setUploaderVisible(false) setUploaderVisible(false)
setFocused(false) setFocused(false)
const response = api.post.post({ ...postData }).catch(error => { const payload = {
message: postMessage,
attachments: postAttachments,
timestamp: DateTime.local().toISO(),
}
const response = api.post.post(payload).catch(error => {
console.error(error) console.error(error)
antd.message.error(error) antd.message.error(error)
@ -70,13 +76,14 @@ export default (props) => {
const onUploadFile = async (req) => { const onUploadFile = async (req) => {
// hide uploader // hide uploader
setUploaderVisible(false) //setUploaderVisible(false)
// get file data // get file data
const file = req.file const file = req.file
// append to form data // append to form data
const formData = new FormData() const formData = new FormData()
formData.append("files", file) formData.append("files", file)
// send request // send request
@ -95,48 +102,75 @@ export default (props) => {
} }
const canSubmit = () => { const canSubmit = () => {
const messageLengthValid = postData.message.length !== 0 && postData.message.length < maxMessageLength const messageLengthValid = postMessage.length !== 0 && postMessage.length < postingPolicy.maxMessageLength
return Boolean(messageLengthValid) && Boolean(pending.length === 0) if (pending.length !== 0) {
return false
}
if (!messageLengthValid && postAttachments.length === 0) {
return false
}
return true
} }
const removeAddition = (file_uid) => { const removeAttachment = (file_uid) => {
updatePostData({ setPostAttachments(postAttachments.filter((file) => file.uid !== file_uid))
additions: postData.additions.filter(addition => addition.file.uid !== file_uid)
})
} }
const onDraggerChange = (change) => { const addAttachment = (file) => {
console.log(change) if (Array.isArray(file)) {
return setPostAttachments([...postAttachments, ...file])
}
return setPostAttachments([...postAttachments, file])
}
const uploaderScrollToEnd = () => {
// scroll to max right
const element = document.querySelector(".ant-upload-list-picture-card")
// calculate the element's width and scroll to the end
const scrollToLeft = element.scrollWidth - element.clientWidth
if (element) {
element.scrollTo({
top: 0,
left: scrollToLeft,
behavior: "smooth",
})
}
}
const onUploaderChange = (change) => {
setFileList(change.fileList)
switch (change.file.status) { switch (change.file.status) {
case "uploading": { case "uploading": {
setPending([...pending, change.file.uid]) setPending([...pending, change.file.uid])
uploaderScrollToEnd()
break break
} }
case "done": { case "done": {
let additions = postData.additions ?? []
console.log(change.file)
additions.push(...change.file.response)
// remove pending file // remove pending file
setPending(pending.filter(uid => uid !== change.file.uid)) setPending(pending.filter(uid => uid !== change.file.uid))
// update post data // update post data
updatePostData({ additions }) addAttachment(change.file.response)
// force update additions uploaderScrollToEnd()
if (additionsRef.current) {
additionsRef.current.forceUpdate()
}
break break
} }
case "error": { case "error": {
// remove pending file // remove pending file
setPending(pending.filter(uid => uid !== change.file.uid)) setPending(pending.filter(uid => uid !== change.file.uid))
removeAttachment(change.file.uid)
} }
default: { default: {
break break
@ -150,9 +184,7 @@ export default (props) => {
event.target.value = event.target.value.slice(1) event.target.value = event.target.value.slice(1)
} }
updatePostData({ setPostMessage(event.target.value)
message: event.target.value
})
} }
const toggleUploader = (to) => { const toggleUploader = (to) => {
@ -173,8 +205,72 @@ export default (props) => {
} }
} }
const handlePaste = ({ clipboardData }) => {
if (clipboardData && clipboardData.items.length > 0) {
const isValidFormat = (fileType) => DEFAULT_ACCEPTED_FILES.includes(fileType)
toggleUploader(true)
for (let index = 0; index < clipboardData.items.length; index++) {
if (!isValidFormat(clipboardData.items[index].type)) {
throw new Error(`Sorry, that's not a format we support ${clipboardData.items[index].type}`)
}
let file = clipboardData.items[index].getAsFile()
app.message.info("Uploading file...")
file.thumbUrl = URL.createObjectURL(file)
file.uid = Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15)
// upload file
onUploadFile({
file,
onSuccess: (response) => {
setFileList([...fileList, file])
addAttachment(response)
}
})
}
}
}
const renderUploadPreviewItem = (item, file, list, actions) => {
const uploading = file.status === "uploading"
const onClickDelete = () => {
actions.remove()
removeAttachment(file.uid)
}
return <div className={classnames("file", { ["uploading"]: uploading })}>
<div className="preview">
<img src={file.thumbUrl ?? "/assets/new_file.png"} />
</div>
<div className="actions">
{
uploading && <Icons.LoadingOutlined style={{ margin: "0 !important" }} />
}
{
!uploading && <antd.Button
type="link"
icon={<Icons.Trash />}
onClick={onClickDelete}
/>
}
</div>
</div>
}
React.useEffect(() => { React.useEffect(() => {
//fetchUploadPolicy() fetchUploadPolicy()
creatorRef.current.addEventListener("paste", handlePaste)
return () => {
creatorRef.current.removeEventListener("paste", handlePaste)
}
}, []) }, [])
// set loading to true menwhile pending is not empty // set loading to true menwhile pending is not empty
@ -184,6 +280,7 @@ export default (props) => {
return <div return <div
className="postCreator" className="postCreator"
ref={creatorRef}
onDragOver={(e) => { onDragOver={(e) => {
e.preventDefault() e.preventDefault()
toggleUploader(true) toggleUploader(true)
@ -205,13 +302,13 @@ export default (props) => {
</div> </div>
<antd.Input.TextArea <antd.Input.TextArea
placeholder="What are you thinking?" placeholder="What are you thinking?"
disabled={loading} value={postMessage}
onKeyDown={handleKeyDown}
onChange={onChangeMessageInput}
autoSize={{ minRows: 3, maxRows: 6 }} autoSize={{ minRows: 3, maxRows: 6 }}
maxLength={maxMessageLength} maxLength={postingPolicy.maxMessageLength}
onChange={onChangeMessageInput}
onKeyDown={handleKeyDown}
disabled={loading}
dragable={false} dragable={false}
value={postData.message}
allowClear allowClear
/> />
<div> <div>
@ -224,8 +321,6 @@ export default (props) => {
</div> </div>
</div> </div>
{postData.additions.length > 0 && <PostAdditions ref={additionsRef} additions={postData.additions} />}
<div className={classnames("actions", { ["hided"]: !focused && !uploaderVisible })}> <div className={classnames("actions", { ["hided"]: !focused && !uploaderVisible })}>
<div> <div>
<antd.Button <antd.Button
@ -240,14 +335,22 @@ export default (props) => {
</div> </div>
<div className={classnames("uploader", { ["hided"]: !uploaderVisible })}> <div className={classnames("uploader", { ["hided"]: !uploaderVisible })}>
<antd.Upload.Dragger <antd.Upload
maxCount={20} maxCount={postingPolicy.maximunFilesPerRequest}
multiple={true} onChange={onUploaderChange}
onChange={onDraggerChange}
customRequest={onUploadFile} customRequest={onUploadFile}
listType="picture-card"
accept={postingPolicy.acceptedMimeTypes}
itemRender={renderUploadPreviewItem}
fileList={fileList}
multiple
> >
<p >Click or drag file to this area to upload</p> <div className="hint">
</antd.Upload.Dragger> <Icons.Plus />
<span>Add attachment</span>
<span>Max {humanSize.fromBytes(postingPolicy.maximumFileSize)}</span>
</div>
</antd.Upload>
</div> </div>
</div> </div>
} }

View File

@ -40,15 +40,18 @@
.uploader { .uploader {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: flex-start;
height: 5vh;
width: 100%; width: 100%;
overflow: hidden; overflow: hidden;
transition: all 150ms ease-in-out; transition: all 150ms ease-in-out;
border: 1px solid var(--border-color);
padding: 10px;
opacity: 1;
>div { >div {
margin-left: 10px; margin-left: 10px;
font-size: 1rem; font-size: 1rem;
@ -59,15 +62,168 @@
} }
&.hided { &.hided {
height: 0; height: 0px;
opacity: 0;
} }
span {
width: 100%;
}
.ant-upload { .ant-upload {
span {
width: fit-content !important;
}
.ant-upload-select-picture-card {
background-color: transparent !important;
}
}
.ant-upload.ant-upload-select-picture-card {
display: flex;
align-self: center;
justify-content: center;
width: 10vw;
height: 10vw;
margin-bottom: 0;
border-color: var(--border-color);
background-color: transparent !important;
}
.ant-upload-list-picture-card .ant-upload-list-item-uploading.ant-upload-list-item {
background-color: transparent !important;
}
.ant-upload-list-item-actions {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 100%; width: 100%;
font-size: 2rem;
}
.ant-upload-list {
display: flex;
flex-direction: row;
width: 100%;
overflow-x: auto;
overflow-y: hidden;
white-space: nowrap;
align-items: center;
justify-content: flex-start;
border-radius: 10px;
}
.ant-upload-list-picture-card-container {
width: 10vw;
height: 10vw;
}
.hint {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
align-self: center;
width: 10vw;
height: 10vw;
text-align: center;
background-color: transparent;
font-size: 0.8rem;
padding: 10px;
svg {
font-size: 1.5rem;
margin-bottom: 6px;
margin-right: 0 !important;
}
}
.file {
position: relative;
width: 10vw;
height: 10vw;
font-size: 2rem;
transition: all 150ms ease-in-out;
border-radius: 10px;
&.uploading {
img {
opacity: 0.5;
}
.actions {
backdrop-filter: blur(5px);
opacity: 1;
}
}
&:hover {
.actions {
opacity: 1;
backdrop-filter: blur(2px);
}
.preview {
opacity: 0.5;
}
}
.preview {
position: absolute;
top: 0;
left: 0;
img {
width: 100%;
height: 100%;
transition: all 150ms ease-in-out;
border-radius: 10px;
}
}
.actions {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
transition: all 150ms ease-in-out;
backdrop-filter: blur(0);
opacity: 0;
svg {
margin: 0;
}
}
} }
} }