From e1f19c52abf5ab1144de2084c1dbcded2836b611 Mon Sep 17 00:00:00 2001 From: FRC Date: Tue, 30 Aug 2022 13:39:10 +0100 Subject: [PATCH] Post in a group about page. (#803) * Dashboards in Group about page * Rename group dashboard to 'About Post' * Fixed James nits --- common/group.ts | 1 + firestore.rules | 2 +- web/components/groups/group-about-post.tsx | 141 +++++++++++++++++++++ web/hooks/use-post.ts | 13 ++ web/lib/firebase/groups.ts | 5 + web/lib/firebase/posts.ts | 9 +- web/pages/group/[...slugs]/index.tsx | 21 ++- 7 files changed, 189 insertions(+), 3 deletions(-) create mode 100644 web/components/groups/group-about-post.tsx create mode 100644 web/hooks/use-post.ts diff --git a/common/group.ts b/common/group.ts index 7d3215ae..181ad153 100644 --- a/common/group.ts +++ b/common/group.ts @@ -10,6 +10,7 @@ export type Group = { anyoneCanJoin: boolean contractIds: string[] + aboutPostId?: string chatDisabled?: boolean mostRecentChatActivityTime?: number mostRecentContractAddedTime?: number diff --git a/firestore.rules b/firestore.rules index fe45071b..26aa52e0 100644 --- a/firestore.rules +++ b/firestore.rules @@ -160,7 +160,7 @@ service cloud.firestore { allow update: if request.auth.uid == resource.data.creatorId && request.resource.data.diff(resource.data) .affectedKeys() - .hasOnly(['name', 'about', 'contractIds', 'memberIds', 'anyoneCanJoin' ]); + .hasOnly(['name', 'about', 'contractIds', 'memberIds', 'anyoneCanJoin', 'aboutPostId' ]); allow update: if (request.auth.uid in resource.data.memberIds || resource.data.anyoneCanJoin) && request.resource.data.diff(resource.data) .affectedKeys() diff --git a/web/components/groups/group-about-post.tsx b/web/components/groups/group-about-post.tsx new file mode 100644 index 00000000..1b42c04d --- /dev/null +++ b/web/components/groups/group-about-post.tsx @@ -0,0 +1,141 @@ +import { useAdmin } from 'web/hooks/use-admin' +import { Row } from '../layout/row' +import { Content } from '../editor' +import { TextEditor, useTextEditor } from 'web/components/editor' +import { Button } from '../button' +import { Spacer } from '../layout/spacer' +import { Group } from 'common/group' +import { deleteFieldFromGroup, updateGroup } from 'web/lib/firebase/groups' +import PencilIcon from '@heroicons/react/solid/PencilIcon' +import { DocumentRemoveIcon } from '@heroicons/react/solid' +import { createPost } from 'web/lib/firebase/api' +import { Post } from 'common/post' +import { deletePost, updatePost } from 'web/lib/firebase/posts' +import { useState } from 'react' +import { usePost } from 'web/hooks/use-post' + +export function GroupAboutPost(props: { + group: Group + isCreator: boolean + post: Post +}) { + const { group, isCreator } = props + const post = usePost(group.aboutPostId) ?? props.post + const isAdmin = useAdmin() + + if (group.aboutPostId == null && !isCreator) { + return

No post has been created

+ } + + return ( +
+ {isCreator || isAdmin ? ( + + ) : ( + + )} +
+ ) +} + +function RichEditGroupAboutPost(props: { group: Group; post: Post }) { + const { group, post } = props + const [editing, setEditing] = useState(false) + const [isSubmitting, setIsSubmitting] = useState(false) + + const { editor, upload } = useTextEditor({ + defaultValue: post.content, + disabled: isSubmitting, + }) + + async function savePost() { + if (!editor) return + const newPost = { + title: group.name, + content: editor.getJSON(), + } + + if (group.aboutPostId == null) { + const result = await createPost(newPost).catch((e) => { + console.error(e) + return e + }) + await updateGroup(group, { + aboutPostId: result.post.id, + }) + } else { + await updatePost(post, { + content: newPost.content, + }) + } + } + + async function deleteGroupAboutPost() { + await deletePost(post) + await deleteFieldFromGroup(group, 'aboutPostId') + } + + return editing ? ( + <> + + + + + + + + ) : ( + <> + {group.aboutPostId == null ? ( +
+

+ No post has been added yet. + + +

+
+ ) : ( +
+
+ + + +
+ + + +
+ )} + + ) +} diff --git a/web/hooks/use-post.ts b/web/hooks/use-post.ts new file mode 100644 index 00000000..9daf2d22 --- /dev/null +++ b/web/hooks/use-post.ts @@ -0,0 +1,13 @@ +import { useEffect, useState } from 'react' +import { Post } from 'common/post' +import { listenForPost } from 'web/lib/firebase/posts' + +export const usePost = (postId: string | undefined) => { + const [post, setPost] = useState() + + useEffect(() => { + if (postId) return listenForPost(postId, setPost) + }, [postId]) + + return post +} diff --git a/web/lib/firebase/groups.ts b/web/lib/firebase/groups.ts index 3f5d18af..28515a35 100644 --- a/web/lib/firebase/groups.ts +++ b/web/lib/firebase/groups.ts @@ -1,5 +1,6 @@ import { deleteDoc, + deleteField, doc, getDocs, query, @@ -36,6 +37,10 @@ export function updateGroup(group: Group, updates: Partial) { return updateDoc(doc(groups, group.id), updates) } +export function deleteFieldFromGroup(group: Group, field: string) { + return updateDoc(doc(groups, group.id), { [field]: deleteField() }) +} + export function deleteGroup(group: Group) { return deleteDoc(doc(groups, group.id)) } diff --git a/web/lib/firebase/posts.ts b/web/lib/firebase/posts.ts index 10bea499..162933af 100644 --- a/web/lib/firebase/posts.ts +++ b/web/lib/firebase/posts.ts @@ -7,7 +7,7 @@ import { where, } from 'firebase/firestore' import { Post } from 'common/post' -import { coll, getValue } from './utils' +import { coll, getValue, listenForValue } from './utils' export const posts = coll('posts') @@ -32,3 +32,10 @@ export async function getPostBySlug(slug: string) { const docs = (await getDocs(q)).docs return docs.length === 0 ? null : docs[0].data() } + +export function listenForPost( + postId: string, + setPost: (post: Post | null) => void +) { + return listenForValue(doc(posts, postId), setPost) +} diff --git a/web/pages/group/[...slugs]/index.tsx b/web/pages/group/[...slugs]/index.tsx index 28658a16..5271a395 100644 --- a/web/pages/group/[...slugs]/index.tsx +++ b/web/pages/group/[...slugs]/index.tsx @@ -45,6 +45,11 @@ import { Button } from 'web/components/button' import { listAllCommentsOnGroup } from 'web/lib/firebase/comments' import { GroupComment } from 'common/comment' import { REFERRAL_AMOUNT } from 'common/economy' +import { GroupAboutPost } from 'web/components/groups/group-about-post' +import { getPost } from 'web/lib/firebase/posts' +import { Post } from 'common/post' +import { Spacer } from 'web/components/layout/spacer' +import { usePost } from 'web/hooks/use-post' export const getStaticProps = fromPropz(getStaticPropz) export async function getStaticPropz(props: { params: { slugs: string[] } }) { @@ -57,6 +62,8 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) { const contracts = (group && (await listContractsByGroupSlug(group.slug))) ?? [] + const aboutPost = + group && group.aboutPostId != null && (await getPost(group.aboutPostId)) const bets = await Promise.all( contracts.map((contract: Contract) => listAllBets(contract.id)) ) @@ -83,6 +90,7 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) { creatorScores, topCreators, messages, + aboutPost, }, revalidate: 60, // regenerate after a minute @@ -121,6 +129,7 @@ export default function GroupPage(props: { creatorScores: { [userId: string]: number } topCreators: User[] messages: GroupComment[] + aboutPost: Post }) { props = usePropz(props, getStaticPropz) ?? { group: null, @@ -146,6 +155,7 @@ export default function GroupPage(props: { const page = slugs?.[1] as typeof groupSubpages[number] const group = useGroup(props.group?.id) ?? props.group + const aboutPost = usePost(props.aboutPost?.id) ?? props.aboutPost const user = useUser() @@ -176,6 +186,16 @@ export default function GroupPage(props: { const aboutTab = ( + {group.aboutPostId != null || isCreator ? ( + + ) : ( +
+ )} +