From b8cdc2cf725ba7979dfe5b0f4508347e4216657c Mon Sep 17 00:00:00 2001 From: Max Leiter Date: Fri, 1 Apr 2022 22:55:27 -0700 Subject: [PATCH] client/server: add the ability to copy a post, view a posts parent --- .../badges/created-ago-badge/index.tsx | 2 +- .../badges/expiration-badge/index.tsx | 1 + client/components/file-dropdown/index.tsx | 9 ++- client/components/new-post/index.tsx | 48 +++++++++-- .../components/post-list/list-item.module.css | 30 +++++++ client/components/post-list/list-item.tsx | 51 +++++++----- client/components/post-page/index.tsx | 46 ++++++++--- .../components/post-page/post-page.module.css | 42 +++++----- client/components/view-document/index.tsx | 4 +- client/lib/types.d.ts | 1 + client/pages/new/from/[id].tsx | 81 +++++++++++++++++++ client/pages/{new.tsx => new/index.tsx} | 2 +- client/tsconfig.json | 70 ++++++++-------- server/migrate.js | 1 + server/src/lib/config.ts | 10 ++- server/src/lib/models/Post.ts | 4 + server/src/migrations/06_duplicate_post.ts | 12 +++ server/src/routes/posts.ts | 29 ++++++- 18 files changed, 333 insertions(+), 110 deletions(-) create mode 100644 client/components/post-list/list-item.module.css create mode 100644 client/pages/new/from/[id].tsx rename client/pages/{new.tsx => new/index.tsx} (93%) create mode 100644 server/src/migrations/06_duplicate_post.ts diff --git a/client/components/badges/created-ago-badge/index.tsx b/client/components/badges/created-ago-badge/index.tsx index 88f63d07..d2f25457 100644 --- a/client/components/badges/created-ago-badge/index.tsx +++ b/client/components/badges/created-ago-badge/index.tsx @@ -16,7 +16,7 @@ const CreatedAgoBadge = ({ createdAt }: { }, [createdDate]) const formattedTime = `${createdDate.toLocaleDateString()} ${createdDate.toLocaleTimeString()}` - return ( Created {time}) + return ( Created {time}) } export default CreatedAgoBadge diff --git a/client/components/badges/expiration-badge/index.tsx b/client/components/badges/expiration-badge/index.tsx index 62655ae8..4d6a1acb 100644 --- a/client/components/badges/expiration-badge/index.tsx +++ b/client/components/badges/expiration-badge/index.tsx @@ -51,6 +51,7 @@ const ExpirationBadge = ({ {isExpired ? "Expired" : `Expires ${timeUntilString}`} + hideArrow ) diff --git a/client/components/file-dropdown/index.tsx b/client/components/file-dropdown/index.tsx index f0a91d6d..758d26a6 100644 --- a/client/components/file-dropdown/index.tsx +++ b/client/components/file-dropdown/index.tsx @@ -13,9 +13,11 @@ type Item = File & { } const FileDropdown = ({ - files + files, + isMobile }: { - files: File[] + files: File[], + isMobile: boolean }) => { const [expanded, setExpanded] = useState(false) const [items, setItems] = useState([]) @@ -66,10 +68,11 @@ const FileDropdown = ({ // a list of files with an icon and a title return ( <> - diff --git a/client/components/new-post/index.tsx b/client/components/new-post/index.tsx index 679946f8..b681b664 100644 --- a/client/components/new-post/index.tsx +++ b/client/components/new-post/index.tsx @@ -1,32 +1,63 @@ import { Button, useToasts, ButtonDropdown, Toggle, Input, useClickAway } from '@geist-ui/core' import { useRouter } from 'next/router'; -import { useCallback, useRef, useState } from 'react' +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 { PostVisibility, Document as DocumentType } from '@lib/types'; +import type { Post as PostType, PostVisibility, Document as DocumentType } from '@lib/types'; import PasswordModal from './password-modal'; import getPostPath from '@lib/get-post-path'; import EditDocumentList from '@components/edit-document-list'; import { ChangeEvent } from 'react'; import DatePicker from 'react-datepicker'; -const Post = () => { + +const Post = ({ + initialPost, + newPostParent +}: { + initialPost?: PostType, + newPostParent?: string +}) => { const { setToast } = useToasts() const router = useRouter(); const [title, setTitle] = useState() const [expiresAt, setExpiresAt] = useState(null) - const [docs, setDocs] = useState([{ + const emptyDoc = useMemo(() => [{ title: '', content: '', id: generateUUID() - }]) + }], []) + + const [docs, setDocs] = useState(emptyDoc) + + // the /new/from/{id} route fetches an initial post + useEffect(() => { + if (initialPost) { + setTitle(`Copy of ${initialPost.title}`) + setDocs(initialPost.files?.map(doc => ({ + title: doc.title, + content: doc.content, + id: doc.id + })) || emptyDoc) + } + }, [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 }) => { + 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: { @@ -106,9 +137,10 @@ const Post = () => { visibility, password, userId: Cookies.get('drift-userid') || '', - expiresAt + expiresAt, + parentId: newPostParent }) - }, [docs, expiresAt, sendRequest, setToast, title]) + }, [docs, expiresAt, newPostParent, sendRequest, setToast, title]) const onClosePasswordModal = () => { setPasswordModalVisible(false) diff --git a/client/components/post-list/list-item.module.css b/client/components/post-list/list-item.module.css new file mode 100644 index 00000000..5c8b732c --- /dev/null +++ b/client/components/post-list/list-item.module.css @@ -0,0 +1,30 @@ +.title { + display: flex; + justify-content: space-between; +} + +.badges { + display: flex; + gap: var(--gap-half); +} + +.buttons { + display: flex; + gap: var(--gap-half); +} + +@media screen and (max-width: 700px) { + .badges { + flex-direction: column; + align-items: flex-start; + } + + .badges > * { + width: min-content; + } + + .title { + flex-direction: column; + gap: var(--gap); + } +} diff --git a/client/components/post-list/list-item.tsx b/client/components/post-list/list-item.tsx index 051119ea..0ba5c841 100644 --- a/client/components/post-list/list-item.tsx +++ b/client/components/post-list/list-item.tsx @@ -1,49 +1,58 @@ import NextLink from "next/link" -import { useEffect, useMemo, useState } from "react" -import { timeAgo } from "@lib/time-ago" import VisibilityBadge from "../badges/visibility-badge" import getPostPath from "@lib/get-post-path" import { Link, Text, Card, Tooltip, Divider, Badge, Button } from "@geist-ui/core" import { File, Post } from "@lib/types" import FadeIn from "@components/fade-in" import Trash from "@geist-ui/icons/trash" -import Cookies from "js-cookie" import ExpirationBadge from "@components/badges/expiration-badge" import CreatedAgoBadge from "@components/badges/created-ago-badge" +import Edit from "@geist-ui/icons/edit" +import { useRouter } from "next/router" +import Parent from '@geist-ui/icons/arrowUpCircle' +import styles from "./list-item.module.css" // TODO: isOwner should default to false so this can be used generically const ListItem = ({ post, isOwner = true, deletePost }: { post: Post, isOwner?: boolean, deletePost: () => void }) => { + const router = useRouter() + + const editACopy = () => { + router.push(`/new/from/${post.id}`) + } return (
  • - + {post.title} - {isOwner && - - + + {console.log(post)} + {post.parent && } + diff --git a/client/components/post-page/post-page.module.css b/client/components/post-page/post-page.module.css index a5137bb9..af80908b 100644 --- a/client/components/post-page/post-page.module.css +++ b/client/components/post-page/post-page.module.css @@ -1,51 +1,51 @@ -.header { - display: flex; - justify-content: space-between; - align-items: center; -} - .header .title { display: flex; - text-align: center; + flex-direction: row; justify-content: space-between; align-items: center; + margin-bottom: var(--gap); +} + +.header .title .badges { + display: flex; + gap: var(--gap-half); } .header .title h3 { margin: 0; padding: 0; -} - -.header .title .badges > * { - margin-left: var(--gap); + display: inline-block; } .header .buttons { display: flex; - justify-content: space-between; - align-items: flex-end; + justify-content: flex-end; } @media screen and (max-width: 900px) { .header { flex-direction: column; + gap: var(--gap); } } @media screen and (max-width: 700px) { .header .title { flex-direction: column; - padding-bottom: var(--gap-double); - } - - .header .title .badges > * { - margin-left: 0; + gap: var(--gap-half); } .header .title .badges { + flex-direction: column; + align-items: center; + } + + .header .title .badges > * { + width: min-content; + } + + .header .buttons { display: flex; - flex-direction: row; - justify-content: space-between; - width: 100%; + justify-content: center; } } diff --git a/client/components/view-document/index.tsx b/client/components/view-document/index.tsx index 37edca90..925f8e1d 100644 --- a/client/components/view-document/index.tsx +++ b/client/components/view-document/index.tsx @@ -22,7 +22,7 @@ type Props = { const DownloadButton = ({ rawLink }: { rawLink?: string }) => { return (
    - +