Change post visibility (#83)
* Change post visibility Closes #64 * Fix imports + right align controls
This commit is contained in:
parent
5df56fbdae
commit
b9d26e16f7
4 changed files with 192 additions and 6 deletions
111
client/components/badges/visibility-control/index.tsx
Normal file
111
client/components/badges/visibility-control/index.tsx
Normal file
|
@ -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
|
|
@ -4,7 +4,7 @@ import DocumentComponent from "@components/view-document"
|
||||||
import styles from "./post-page.module.css"
|
import styles from "./post-page.module.css"
|
||||||
import homeStyles from "@styles/Home.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 { Page, Button, Text, ButtonGroup, useMediaQuery } from "@geist-ui/core"
|
||||||
import { useEffect, useState } from "react"
|
import { useEffect, useState } from "react"
|
||||||
import Archive from "@geist-ui/icons/archive"
|
import Archive from "@geist-ui/icons/archive"
|
||||||
|
@ -18,6 +18,7 @@ import CreatedAgoBadge from "@components/badges/created-ago-badge"
|
||||||
import Cookies from "js-cookie"
|
import Cookies from "js-cookie"
|
||||||
import getPostPath from "@lib/get-post-path"
|
import getPostPath from "@lib/get-post-path"
|
||||||
import PasswordModalPage from "./password-modal-wrapper"
|
import PasswordModalPage from "./password-modal-wrapper"
|
||||||
|
import VisibilityControl from "@components/badges/visibility-control"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
post: Post
|
post: Post
|
||||||
|
@ -26,18 +27,19 @@ type Props = {
|
||||||
|
|
||||||
const PostPage = ({ post: initialPost, isProtected }: Props) => {
|
const PostPage = ({ post: initialPost, isProtected }: Props) => {
|
||||||
const [post, setPost] = useState<Post>(initialPost)
|
const [post, setPost] = useState<Post>(initialPost)
|
||||||
|
const [visibility, setVisibility] = useState<PostVisibility>(post.visibility)
|
||||||
const [isExpired, setIsExpired] = useState(
|
const [isExpired, setIsExpired] = useState(
|
||||||
post.expiresAt ? new Date(post.expiresAt) < new Date() : null
|
post.expiresAt ? new Date(post.expiresAt) < new Date() : null
|
||||||
)
|
)
|
||||||
const [isLoading, setIsLoading] = useState(true)
|
const [isLoading, setIsLoading] = useState(true)
|
||||||
|
const [isOwner] = useState(
|
||||||
|
post.users ? post.users[0].id === Cookies.get("drift-userid") : false
|
||||||
|
)
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const isMobile = useMediaQuery("mobile")
|
const isMobile = useMediaQuery("mobile")
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const isOwner = post.users
|
|
||||||
? post.users[0].id === Cookies.get("drift-userid")
|
|
||||||
: false
|
|
||||||
if (!isOwner && isExpired) {
|
if (!isOwner && isExpired) {
|
||||||
router.push("/expired")
|
router.push("/expired")
|
||||||
}
|
}
|
||||||
|
@ -59,7 +61,7 @@ const PostPage = ({ post: initialPost, isProtected }: Props) => {
|
||||||
return () => {
|
return () => {
|
||||||
if (interval) clearInterval(interval)
|
if (interval) clearInterval(interval)
|
||||||
}
|
}
|
||||||
}, [isExpired, post.expiresAt, post.users, router])
|
}, [isExpired, isOwner, post.expiresAt, post.users, router])
|
||||||
|
|
||||||
const download = async () => {
|
const download = async () => {
|
||||||
if (!post.files) return
|
if (!post.files) return
|
||||||
|
@ -143,7 +145,7 @@ const PostPage = ({ post: initialPost, isProtected }: Props) => {
|
||||||
<span className={styles.title}>
|
<span className={styles.title}>
|
||||||
<Text h3>{post.title}</Text>
|
<Text h3>{post.title}</Text>
|
||||||
<span className={styles.badges}>
|
<span className={styles.badges}>
|
||||||
<VisibilityBadge visibility={post.visibility} />
|
<VisibilityBadge visibility={visibility} />
|
||||||
<CreatedAgoBadge createdAt={post.createdAt} />
|
<CreatedAgoBadge createdAt={post.createdAt} />
|
||||||
<ExpirationBadge postExpirationDate={post.expiresAt} />
|
<ExpirationBadge postExpirationDate={post.expiresAt} />
|
||||||
</span>
|
</span>
|
||||||
|
@ -164,6 +166,9 @@ const PostPage = ({ post: initialPost, isProtected }: Props) => {
|
||||||
content={content}
|
content={content}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
{isOwner && <span className={styles.controls}>
|
||||||
|
<VisibilityControl postId={post.id} visibility={visibility} setVisibility={setVisibility} />
|
||||||
|
</span>}
|
||||||
<ScrollToTop />
|
<ScrollToTop />
|
||||||
</Page.Content>
|
</Page.Content>
|
||||||
</Page>
|
</Page>
|
||||||
|
|
|
@ -22,6 +22,11 @@
|
||||||
margin-bottom: var(--gap);
|
margin-bottom: var(--gap);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 900px) {
|
@media screen and (max-width: 900px) {
|
||||||
.header {
|
.header {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -402,3 +402,68 @@ posts.delete("/:id", jwt, async (req: UserJwtRequest, res, next) => {
|
||||||
next(e)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in a new issue