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 ? (
+
+ ) : (
+
+ )}
+