Skip to content

Commit

Permalink
Change post visibility (#83)
Browse files Browse the repository at this point in the history
* Change post visibility

Closes #64

* Fix imports + right align controls
  • Loading branch information
jazcarate committed Apr 14, 2022
1 parent 5df56fb commit b9d26e1
Show file tree
Hide file tree
Showing 4 changed files with 192 additions and 6 deletions.
111 changes: 111 additions & 0 deletions client/components/badges/visibility-control/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import PasswordModal from "@components/new-post/password-modal"
import { Button, ButtonGroup, Loading, useToasts } from "@geist-ui/core"
import type { PostVisibility } from "@lib/types"
import Cookies from "js-cookie"
import { useCallback, useState } from "react"

type Props = {
postId: string
visibility: PostVisibility
setVisibility: (visibility: PostVisibility) => void
}

const VisibilityControl = ({ postId, visibility, setVisibility }: Props) => {
const [isSubmitting, setSubmitting] = useState(false)
const [passwordModalVisible, setPasswordModalVisible] = useState(false)
const { setToast } = useToasts()

const sendRequest = useCallback(
async (visibility: PostVisibility, password?: string) => {
const res = await fetch(`/server-api/posts/${postId}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${Cookies.get("drift-token")}`
},
body: JSON.stringify({ visibility, password })
})

if (res.ok) {
const json = await res.json()
setVisibility(json.visibility)
} else {
const json = await res.json()
setToast({
text: json.error.message,
type: "error"
})
setPasswordModalVisible(false)
}
},
[setToast]
)

const onSubmit = useCallback(
async (visibility: PostVisibility, password?: string) => {
if (visibility === "protected" && !password) {
setPasswordModalVisible(true)
return
}
setPasswordModalVisible(false)
const timeout = setTimeout(() => setSubmitting(true), 100)

await sendRequest(visibility, password)
clearTimeout(timeout)
setSubmitting(false)
},
[sendRequest]
)

const onClosePasswordModal = () => {
setPasswordModalVisible(false)
setSubmitting(false)
}

const submitPassword = useCallback(
(password) => onSubmit("protected", password),
[onSubmit]
)

return (
<>
{isSubmitting ? (
<Loading />
) : (
<ButtonGroup margin={0}>
<Button
disabled={visibility === "private"}
onClick={() => onSubmit("private")}
>
Make private
</Button>
<Button
disabled={visibility === "public"}
onClick={() => onSubmit("public")}
>
Make Public
</Button>
<Button
disabled={visibility === "unlisted"}
onClick={() => onSubmit("unlisted")}
>
Unlist
</Button>
<Button onClick={() => onSubmit("protected")}>
{visibility === "protected"
? "Change Password"
: "Protect with password"}
</Button>
</ButtonGroup>
)}
<PasswordModal
creating={true}
isOpen={passwordModalVisible}
onClose={onClosePasswordModal}
onSubmit={submitPassword}
/>
</>
)
}

export default VisibilityControl
17 changes: 11 additions & 6 deletions client/components/post-page/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import DocumentComponent from "@components/view-document"
import styles from "./post-page.module.css"
import homeStyles from "@styles/Home.module.css"

import type { File, Post } from "@lib/types"
import type { File, Post, PostVisibility } from "@lib/types"
import { Page, Button, Text, ButtonGroup, useMediaQuery } from "@geist-ui/core"
import { useEffect, useState } from "react"
import Archive from "@geist-ui/icons/archive"
Expand All @@ -18,6 +18,7 @@ import CreatedAgoBadge from "@components/badges/created-ago-badge"
import Cookies from "js-cookie"
import getPostPath from "@lib/get-post-path"
import PasswordModalPage from "./password-modal-wrapper"
import VisibilityControl from "@components/badges/visibility-control"

type Props = {
post: Post
Expand All @@ -26,18 +27,19 @@ type Props = {

const PostPage = ({ post: initialPost, isProtected }: Props) => {
const [post, setPost] = useState<Post>(initialPost)
const [visibility, setVisibility] = useState<PostVisibility>(post.visibility)
const [isExpired, setIsExpired] = useState(
post.expiresAt ? new Date(post.expiresAt) < new Date() : null
)
const [isLoading, setIsLoading] = useState(true)
const [isOwner] = useState(
post.users ? post.users[0].id === Cookies.get("drift-userid") : false
)

const router = useRouter()
const isMobile = useMediaQuery("mobile")

useEffect(() => {
const isOwner = post.users
? post.users[0].id === Cookies.get("drift-userid")
: false
if (!isOwner && isExpired) {
router.push("/expired")
}
Expand All @@ -59,7 +61,7 @@ const PostPage = ({ post: initialPost, isProtected }: Props) => {
return () => {
if (interval) clearInterval(interval)
}
}, [isExpired, post.expiresAt, post.users, router])
}, [isExpired, isOwner, post.expiresAt, post.users, router])

const download = async () => {
if (!post.files) return
Expand Down Expand Up @@ -143,7 +145,7 @@ const PostPage = ({ post: initialPost, isProtected }: Props) => {
<span className={styles.title}>
<Text h3>{post.title}</Text>
<span className={styles.badges}>
<VisibilityBadge visibility={post.visibility} />
<VisibilityBadge visibility={visibility} />
<CreatedAgoBadge createdAt={post.createdAt} />
<ExpirationBadge postExpirationDate={post.expiresAt} />
</span>
Expand All @@ -164,6 +166,9 @@ const PostPage = ({ post: initialPost, isProtected }: Props) => {
content={content}
/>
))}
{isOwner && <span className={styles.controls}>
<VisibilityControl postId={post.id} visibility={visibility} setVisibility={setVisibility} />
</span>}
<ScrollToTop />
</Page.Content>
</Page>
Expand Down
5 changes: 5 additions & 0 deletions client/components/post-page/post-page.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@
margin-bottom: var(--gap);
}

.controls {
display: flex;
justify-content: flex-end;
}

@media screen and (max-width: 900px) {
.header {
flex-direction: column;
Expand Down
65 changes: 65 additions & 0 deletions server/src/routes/posts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -402,3 +402,68 @@ posts.delete("/:id", jwt, async (req: UserJwtRequest, res, next) => {
next(e)
}
})


posts.put(
"/:id",
jwt,
celebrate({
params: {
id: Joi.string().required()
},
body: {
visibility: Joi.string()
.custom(postVisibilitySchema, "valid visibility")
.required(),
password: Joi.string().optional(),
}
}),
async (req: UserJwtRequest, res, next) => {
try {
const isUserAuthor = (post: Post) => {
return (
req.user?.id &&
post.users?.map((user) => user.id).includes(req.user?.id)
)
}

const { visibility, password } = req.body;

let hashedPassword: string = ""
if (visibility === "protected") {
hashedPassword = crypto
.createHash("sha256")
.update(password)
.digest("hex")
}

const { id } = req.params;
const post = await Post.findByPk(id, {
include: [
{
model: User,
as: "users",
attributes: ["id"]
},
]
})

if (!post) {
return res.status(404).json({ error: "Post not found" })
}

if (!isUserAuthor(post)) {
return res.status(403).json({ error: "This post does not belong to you" })
}

await Post.update(
{ password: hashedPassword, visibility },
{ where: { id } }
)

res.json({ id, visibility })
} catch (e) {
res.status(400).json(e)
}
}
)

0 comments on commit b9d26e1

Please sign in to comment.