import { Button, useToasts, ButtonDropdown, Input } from "@geist-ui/core" import { useRouter } from "next/router" import { useCallback, useEffect, useMemo, useRef, useState } from "react" import generateUUID from "@lib/generate-uuid" import FileDropzone from "./drag-and-drop" import styles from "./post.module.css" import Title from "./title" import Cookies from "js-cookie" import type { Post as PostType, PostVisibility, Document as DocumentType } from "@lib/types" import PasswordModal from "./password-modal" import EditDocumentList from "@components/edit-document-list" import { ChangeEvent } from "react" import DatePicker from "react-datepicker" import getTitleForPostCopy from "@lib/get-title-for-post-copy" import Description from "./description" const Post = ({ initialPost, newPostParent }: { initialPost?: PostType newPostParent?: string }) => { const { setToast } = useToasts() const router = useRouter() const [title, setTitle] = useState() const [description, setDescription] = useState() const [expiresAt, setExpiresAt] = useState(null) const emptyDoc = useMemo( () => [ { title: "", content: "", id: generateUUID() } ], [] ) const [docs, setDocs] = useState(emptyDoc) // the /new/from/{id} route fetches an initial post useEffect(() => { if (initialPost) { setDocs( initialPost.files?.map((doc) => ({ title: doc.title, content: doc.content, id: doc.id })) || emptyDoc ) setTitle(getTitleForPostCopy(initialPost.title)) setDescription(initialPost.description) } }, [emptyDoc, initialPost]) const [passwordModalVisible, setPasswordModalVisible] = useState(false) const sendRequest = useCallback( async ( url: string, data: { expiresAt: Date | null visibility?: PostVisibility title?: string files?: DocumentType[] password?: string userId: string parentId?: string } ) => { const res = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${Cookies.get("drift-token")}` }, body: JSON.stringify({ title, description, files: docs, ...data }) }) if (res.ok) { const json = await res.json() router.push(`/post/${json.id}`) } else { const json = await res.json() setToast({ text: json.error.message || "Please fill out all fields", type: "error" }) setPasswordModalVisible(false) setSubmitting(false) } }, [description, docs, router, setToast, title] ) const [isSubmitting, setSubmitting] = useState(false) const onSubmit = useCallback( async (visibility: PostVisibility, password?: string) => { if (visibility === "protected" && !password) { setPasswordModalVisible(true) return } setPasswordModalVisible(false) setSubmitting(true) let hasErrored = false if (!title) { setToast({ text: "Please fill out the post title", type: "error" }) hasErrored = true } if (!docs.length) { setToast({ text: "Please add at least one document", type: "error" }) hasErrored = true } for (const doc of docs) { if (!doc.title) { setToast({ text: "Please fill out all the document titles", type: "error" }) hasErrored = true } } if (hasErrored) { setSubmitting(false) return } await sendRequest("/server-api/posts/create", { title, files: docs, visibility, password, userId: Cookies.get("drift-userid") || "", expiresAt, parentId: newPostParent }) }, [docs, expiresAt, newPostParent, sendRequest, setToast, title] ) const onClosePasswordModal = () => { setPasswordModalVisible(false) setSubmitting(false) } const submitPassword = useCallback( (password: string) => onSubmit("protected", password), [onSubmit] ) const onChangeExpiration = useCallback((date: Date) => setExpiresAt(date), []) const onChangeTitle = useCallback( (e: ChangeEvent) => { setTitle(e.target.value) }, [setTitle] ) const onChangeDescription = useCallback( (e: ChangeEvent) => { setDescription(e.target.value) }, [setDescription] ) const updateDocTitle = useCallback( (i: number) => (title: string) => { setDocs((docs) => docs.map((doc, index) => (i === index ? { ...doc, title } : doc)) ) }, [setDocs] ) const updateDocContent = useCallback( (i: number) => (content: string) => { setDocs((docs) => docs.map((doc, index) => (i === index ? { ...doc, content } : doc)) ) }, [setDocs] ) const removeDoc = useCallback( (i: number) => () => { setDocs((docs) => docs.filter((_, index) => i !== index)) }, [setDocs] ) const uploadDocs = useCallback( (files: DocumentType[]) => { // if no title is set and the only document is empty, const isFirstDocEmpty = docs.length <= 1 && (docs.length ? docs[0].title === "" : true) const shouldSetTitle = !title && isFirstDocEmpty if (shouldSetTitle) { if (files.length === 1) { setTitle(files[0].title) } else if (files.length > 1) { setTitle("Uploaded files") } } if (isFirstDocEmpty) setDocs(files) else setDocs((docs) => [...docs, ...files]) }, [docs, title] ) // pasted files // const files = e.clipboardData.files as File[] // if (files.length) { // const docs = Array.from(files).map((file) => ({ // title: file.name, // content: '', // id: generateUUID() // })) // } const onPaste = useCallback( (e: any) => { const pastedText = e.clipboardData.getData("text") if (pastedText) { if (!title) { setTitle("Pasted text") } } }, [title] ) const CustomTimeInput = ({ date, value, onChange }: { date: Date value: string onChange: (date: string) => void }) => ( { if (!isNaN(date.getTime())) { onChange(e.target.value || date.toISOString().slice(11, 16)) } }} style={{ backgroundColor: "var(--bg)", border: "1px solid var(--light-gray)", borderRadius: "var(--radius)" }} required /> ) return (
<Description description={description} onChange={onChangeDescription} /> <FileDropzone setDocs={uploadDocs} /> <EditDocumentList onPaste={onPaste} docs={docs} updateDocTitle={updateDocTitle} updateDocContent={updateDocContent} removeDoc={removeDoc} /> <div className={styles.buttons}> <Button className={styles.button} onClick={() => { setDocs([ ...docs, { title: "", content: "", id: generateUUID() } ]) }} type="default" > Add a File </Button> <div className={styles.rightButtons}> { <DatePicker onChange={onChangeExpiration} customInput={ <Input label="Expires at" clearable width="100%" height="40px" /> } placeholderText="Won't expire" selected={expiresAt} showTimeInput={true} // @ts-ignore customTimeInput={<CustomTimeInput />} timeInputLabel="Time:" dateFormat="MM/dd/yyyy h:mm aa" className={styles.datePicker} clearButtonTitle={"Clear"} // TODO: investigate why this causes margin shift if true enableTabLoop={false} minDate={new Date()} /> } <ButtonDropdown loading={isSubmitting} type="success"> <ButtonDropdown.Item main onClick={() => onSubmit("private")}> Create Private </ButtonDropdown.Item> <ButtonDropdown.Item onClick={() => onSubmit("public")}> Create Public </ButtonDropdown.Item> <ButtonDropdown.Item onClick={() => onSubmit("unlisted")}> Create Unlisted </ButtonDropdown.Item> <ButtonDropdown.Item onClick={() => onSubmit("protected")}> Create with Password </ButtonDropdown.Item> </ButtonDropdown> </div> </div> <PasswordModal creating={true} isOpen={passwordModalVisible} onClose={onClosePasswordModal} onSubmit={submitPassword} /> </div> ) } export default Post