Add a "Posts" tab to groups (#926)
* Add a "Posts" sidebar item to groups * Fix James's nits * Show "Add Post" button only to users
This commit is contained in:
parent
ebcecd4fe9
commit
5a10132e2b
|
@ -10,6 +10,7 @@ export type Group = {
|
||||||
totalContracts: number
|
totalContracts: number
|
||||||
totalMembers: number
|
totalMembers: number
|
||||||
aboutPostId?: string
|
aboutPostId?: string
|
||||||
|
postIds: string[]
|
||||||
chatDisabled?: boolean
|
chatDisabled?: boolean
|
||||||
mostRecentContractAddedTime?: number
|
mostRecentContractAddedTime?: number
|
||||||
cachedLeaderboard?: {
|
cachedLeaderboard?: {
|
||||||
|
|
|
@ -61,6 +61,7 @@ export const creategroup = newEndpoint({}, async (req, auth) => {
|
||||||
anyoneCanJoin,
|
anyoneCanJoin,
|
||||||
totalContracts: 0,
|
totalContracts: 0,
|
||||||
totalMembers: memberIds.length,
|
totalMembers: memberIds.length,
|
||||||
|
postIds: [],
|
||||||
}
|
}
|
||||||
|
|
||||||
await groupRef.create(group)
|
await groupRef.create(group)
|
||||||
|
|
|
@ -34,11 +34,12 @@ const contentSchema: z.ZodType<JSONContent> = z.lazy(() =>
|
||||||
const postSchema = z.object({
|
const postSchema = z.object({
|
||||||
title: z.string().min(1).max(MAX_POST_TITLE_LENGTH),
|
title: z.string().min(1).max(MAX_POST_TITLE_LENGTH),
|
||||||
content: contentSchema,
|
content: contentSchema,
|
||||||
|
groupId: z.string().optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
export const createpost = newEndpoint({}, async (req, auth) => {
|
export const createpost = newEndpoint({}, async (req, auth) => {
|
||||||
const firestore = admin.firestore()
|
const firestore = admin.firestore()
|
||||||
const { title, content } = validate(postSchema, req.body)
|
const { title, content, groupId } = validate(postSchema, req.body)
|
||||||
|
|
||||||
const creator = await getUser(auth.uid)
|
const creator = await getUser(auth.uid)
|
||||||
if (!creator)
|
if (!creator)
|
||||||
|
@ -60,6 +61,18 @@ export const createpost = newEndpoint({}, async (req, auth) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
await postRef.create(post)
|
await postRef.create(post)
|
||||||
|
if (groupId) {
|
||||||
|
const groupRef = firestore.collection('groups').doc(groupId)
|
||||||
|
const group = await groupRef.get()
|
||||||
|
if (group.exists) {
|
||||||
|
const groupData = group.data()
|
||||||
|
if (groupData) {
|
||||||
|
const postIds = groupData.postIds ?? []
|
||||||
|
postIds.push(postRef.id)
|
||||||
|
await groupRef.update({ postIds })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { status: 'success', post }
|
return { status: 'success', post }
|
||||||
})
|
})
|
||||||
|
|
|
@ -41,6 +41,7 @@ const createGroup = async (
|
||||||
anyoneCanJoin: true,
|
anyoneCanJoin: true,
|
||||||
totalContracts: contracts.length,
|
totalContracts: contracts.length,
|
||||||
totalMembers: 1,
|
totalMembers: 1,
|
||||||
|
postIds: [],
|
||||||
}
|
}
|
||||||
await groupRef.create(group)
|
await groupRef.create(group)
|
||||||
// create a GroupMemberDoc for the creator
|
// create a GroupMemberDoc for the creator
|
||||||
|
|
94
web/components/create-post.tsx
Normal file
94
web/components/create-post.tsx
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
import { useState } from 'react'
|
||||||
|
import { Spacer } from 'web/components/layout/spacer'
|
||||||
|
import { Title } from 'web/components/title'
|
||||||
|
import Textarea from 'react-expanding-textarea'
|
||||||
|
|
||||||
|
import { TextEditor, useTextEditor } from 'web/components/editor'
|
||||||
|
import { createPost } from 'web/lib/firebase/api'
|
||||||
|
import clsx from 'clsx'
|
||||||
|
import Router from 'next/router'
|
||||||
|
import { MAX_POST_TITLE_LENGTH } from 'common/post'
|
||||||
|
import { postPath } from 'web/lib/firebase/posts'
|
||||||
|
import { Group } from 'common/group'
|
||||||
|
|
||||||
|
export function CreatePost(props: { group?: Group }) {
|
||||||
|
const [title, setTitle] = useState('')
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
|
const { group } = props
|
||||||
|
|
||||||
|
const { editor, upload } = useTextEditor({
|
||||||
|
disabled: isSubmitting,
|
||||||
|
})
|
||||||
|
|
||||||
|
const isValid = editor && title.length > 0 && editor.isEmpty === false
|
||||||
|
|
||||||
|
async function savePost(title: string) {
|
||||||
|
if (!editor) return
|
||||||
|
const newPost = {
|
||||||
|
title: title,
|
||||||
|
content: editor.getJSON(),
|
||||||
|
groupId: group?.id,
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await createPost(newPost).catch((e) => {
|
||||||
|
console.log(e)
|
||||||
|
setError('There was an error creating the post, please try again')
|
||||||
|
return e
|
||||||
|
})
|
||||||
|
if (result.post) {
|
||||||
|
await Router.push(postPath(result.post.slug))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto w-full max-w-3xl">
|
||||||
|
<div className="rounded-lg px-6 py-4 sm:py-0">
|
||||||
|
<Title className="!mt-0" text="Create a post" />
|
||||||
|
<form>
|
||||||
|
<div className="form-control w-full">
|
||||||
|
<label className="label">
|
||||||
|
<span className="mb-1">
|
||||||
|
Title<span className={'text-red-700'}> *</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<Textarea
|
||||||
|
placeholder="e.g. Elon Mania Post"
|
||||||
|
className="input input-bordered resize-none"
|
||||||
|
autoFocus
|
||||||
|
maxLength={MAX_POST_TITLE_LENGTH}
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value || '')}
|
||||||
|
/>
|
||||||
|
<Spacer h={6} />
|
||||||
|
<label className="label">
|
||||||
|
<span className="mb-1">
|
||||||
|
Content<span className={'text-red-700'}> *</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<TextEditor editor={editor} upload={upload} />
|
||||||
|
<Spacer h={6} />
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
className={clsx(
|
||||||
|
'btn btn-primary normal-case',
|
||||||
|
isSubmitting && 'loading disabled'
|
||||||
|
)}
|
||||||
|
disabled={isSubmitting || !isValid || upload.isLoading}
|
||||||
|
onClick={async () => {
|
||||||
|
setIsSubmitting(true)
|
||||||
|
await savePost(title)
|
||||||
|
setIsSubmitting(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isSubmitting ? 'Creating...' : 'Create a post'}
|
||||||
|
</button>
|
||||||
|
{error !== '' && <div className="text-red-700">{error}</div>}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
|
@ -16,29 +16,26 @@ import { usePost } from 'web/hooks/use-post'
|
||||||
export function GroupAboutPost(props: {
|
export function GroupAboutPost(props: {
|
||||||
group: Group
|
group: Group
|
||||||
isEditable: boolean
|
isEditable: boolean
|
||||||
post: Post
|
post: Post | null
|
||||||
}) {
|
}) {
|
||||||
const { group, isEditable } = props
|
const { group, isEditable } = props
|
||||||
const post = usePost(group.aboutPostId) ?? props.post
|
const post = usePost(group.aboutPostId) ?? props.post
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="rounded-md bg-white p-4 ">
|
<div className="rounded-md bg-white p-4 ">
|
||||||
{isEditable ? (
|
{isEditable && <RichEditGroupAboutPost group={group} post={post} />}
|
||||||
<RichEditGroupAboutPost group={group} post={post} />
|
{!isEditable && post && <Content content={post.content} />}
|
||||||
) : (
|
|
||||||
<Content content={post.content} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function RichEditGroupAboutPost(props: { group: Group; post: Post }) {
|
function RichEditGroupAboutPost(props: { group: Group; post: Post | null }) {
|
||||||
const { group, post } = props
|
const { group, post } = props
|
||||||
const [editing, setEditing] = useState(false)
|
const [editing, setEditing] = useState(false)
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
const { editor, upload } = useTextEditor({
|
const { editor, upload } = useTextEditor({
|
||||||
defaultValue: post.content,
|
defaultValue: post?.content,
|
||||||
disabled: isSubmitting,
|
disabled: isSubmitting,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -49,7 +46,7 @@ function RichEditGroupAboutPost(props: { group: Group; post: Post }) {
|
||||||
content: editor.getJSON(),
|
content: editor.getJSON(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if (group.aboutPostId == null) {
|
if (post == null) {
|
||||||
const result = await createPost(newPost).catch((e) => {
|
const result = await createPost(newPost).catch((e) => {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
return e
|
return e
|
||||||
|
@ -65,6 +62,7 @@ function RichEditGroupAboutPost(props: { group: Group; post: Post }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteGroupAboutPost() {
|
async function deleteGroupAboutPost() {
|
||||||
|
if (post == null) return
|
||||||
await deletePost(post)
|
await deletePost(post)
|
||||||
await deleteFieldFromGroup(group, 'aboutPostId')
|
await deleteFieldFromGroup(group, 'aboutPostId')
|
||||||
}
|
}
|
||||||
|
@ -91,7 +89,7 @@ function RichEditGroupAboutPost(props: { group: Group; post: Post }) {
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{group.aboutPostId == null ? (
|
{post == null ? (
|
||||||
<div className="text-center text-gray-500">
|
<div className="text-center text-gray-500">
|
||||||
<p className="text-sm">
|
<p className="text-sm">
|
||||||
No post has been added yet.
|
No post has been added yet.
|
||||||
|
|
|
@ -11,3 +11,29 @@ export const usePost = (postId: string | undefined) => {
|
||||||
|
|
||||||
return post
|
return post
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const usePosts = (postIds: string[]) => {
|
||||||
|
const [posts, setPosts] = useState<Post[]>([])
|
||||||
|
useEffect(() => {
|
||||||
|
if (postIds.length === 0) return
|
||||||
|
setPosts([])
|
||||||
|
|
||||||
|
const unsubscribes = postIds.map((postId) =>
|
||||||
|
listenForPost(postId, (post) => {
|
||||||
|
if (post) {
|
||||||
|
setPosts((posts) => [...posts, post])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribes.forEach((unsubscribe) => unsubscribe())
|
||||||
|
}
|
||||||
|
}, [postIds])
|
||||||
|
|
||||||
|
return posts
|
||||||
|
.filter(
|
||||||
|
(post, index, self) => index === self.findIndex((t) => t.id === post.id)
|
||||||
|
)
|
||||||
|
.sort((a, b) => b.createdTime - a.createdTime)
|
||||||
|
}
|
||||||
|
|
|
@ -90,6 +90,10 @@ export function getCurrentUser(params: any) {
|
||||||
return call(getFunctionUrl('getcurrentuser'), 'GET', params)
|
return call(getFunctionUrl('getcurrentuser'), 'GET', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createPost(params: { title: string; content: JSONContent }) {
|
export function createPost(params: {
|
||||||
|
title: string
|
||||||
|
content: JSONContent
|
||||||
|
groupId?: string
|
||||||
|
}) {
|
||||||
return call(getFunctionUrl('createpost'), 'POST', params)
|
return call(getFunctionUrl('createpost'), 'POST', params)
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,7 @@ export function groupPath(
|
||||||
| 'about'
|
| 'about'
|
||||||
| typeof GROUP_CHAT_SLUG
|
| typeof GROUP_CHAT_SLUG
|
||||||
| 'leaderboards'
|
| 'leaderboards'
|
||||||
|
| 'posts'
|
||||||
) {
|
) {
|
||||||
return `/group/${groupSlug}${subpath ? `/${subpath}` : ''}`
|
return `/group/${groupSlug}${subpath ? `/${subpath}` : ''}`
|
||||||
}
|
}
|
||||||
|
|
|
@ -39,3 +39,8 @@ export function listenForPost(
|
||||||
) {
|
) {
|
||||||
return listenForValue(doc(posts, postId), setPost)
|
return listenForValue(doc(posts, postId), setPost)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function listPosts(postIds?: string[]) {
|
||||||
|
if (postIds === undefined) return []
|
||||||
|
return Promise.all(postIds.map(getPost))
|
||||||
|
}
|
||||||
|
|
|
@ -1,93 +0,0 @@
|
||||||
import { useState } from 'react'
|
|
||||||
import { Spacer } from 'web/components/layout/spacer'
|
|
||||||
import { Page } from 'web/components/page'
|
|
||||||
import { Title } from 'web/components/title'
|
|
||||||
import Textarea from 'react-expanding-textarea'
|
|
||||||
|
|
||||||
import { TextEditor, useTextEditor } from 'web/components/editor'
|
|
||||||
import { createPost } from 'web/lib/firebase/api'
|
|
||||||
import clsx from 'clsx'
|
|
||||||
import Router from 'next/router'
|
|
||||||
import { MAX_POST_TITLE_LENGTH } from 'common/post'
|
|
||||||
import { postPath } from 'web/lib/firebase/posts'
|
|
||||||
|
|
||||||
export default function CreatePost() {
|
|
||||||
const [title, setTitle] = useState('')
|
|
||||||
const [error, setError] = useState('')
|
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
|
||||||
|
|
||||||
const { editor, upload } = useTextEditor({
|
|
||||||
disabled: isSubmitting,
|
|
||||||
})
|
|
||||||
|
|
||||||
const isValid = editor && title.length > 0 && editor.isEmpty === false
|
|
||||||
|
|
||||||
async function savePost(title: string) {
|
|
||||||
if (!editor) return
|
|
||||||
const newPost = {
|
|
||||||
title: title,
|
|
||||||
content: editor.getJSON(),
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await createPost(newPost).catch((e) => {
|
|
||||||
console.log(e)
|
|
||||||
setError('There was an error creating the post, please try again')
|
|
||||||
return e
|
|
||||||
})
|
|
||||||
if (result.post) {
|
|
||||||
await Router.push(postPath(result.post.slug))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Page>
|
|
||||||
<div className="mx-auto w-full max-w-3xl">
|
|
||||||
<div className="rounded-lg px-6 py-4 sm:py-0">
|
|
||||||
<Title className="!mt-0" text="Create a post" />
|
|
||||||
<form>
|
|
||||||
<div className="form-control w-full">
|
|
||||||
<label className="label">
|
|
||||||
<span className="mb-1">
|
|
||||||
Title<span className={'text-red-700'}> *</span>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<Textarea
|
|
||||||
placeholder="e.g. Elon Mania Post"
|
|
||||||
className="input input-bordered resize-none"
|
|
||||||
autoFocus
|
|
||||||
maxLength={MAX_POST_TITLE_LENGTH}
|
|
||||||
value={title}
|
|
||||||
onChange={(e) => setTitle(e.target.value || '')}
|
|
||||||
/>
|
|
||||||
<Spacer h={6} />
|
|
||||||
<label className="label">
|
|
||||||
<span className="mb-1">
|
|
||||||
Content<span className={'text-red-700'}> *</span>
|
|
||||||
</span>
|
|
||||||
</label>
|
|
||||||
<TextEditor editor={editor} upload={upload} />
|
|
||||||
<Spacer h={6} />
|
|
||||||
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className={clsx(
|
|
||||||
'btn btn-primary normal-case',
|
|
||||||
isSubmitting && 'loading disabled'
|
|
||||||
)}
|
|
||||||
disabled={isSubmitting || !isValid || upload.isLoading}
|
|
||||||
onClick={async () => {
|
|
||||||
setIsSubmitting(true)
|
|
||||||
await savePost(title)
|
|
||||||
setIsSubmitting(false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isSubmitting ? 'Creating...' : 'Create a post'}
|
|
||||||
</button>
|
|
||||||
{error !== '' && <div className="text-red-700">{error}</div>}
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Page>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
import { Row } from 'web/components/layout/row'
|
import { Row } from 'web/components/layout/row'
|
||||||
import { firebaseLogin, getUser, User } from 'web/lib/firebase/users'
|
import { firebaseLogin, getUser, User } from 'web/lib/firebase/users'
|
||||||
import { Col } from 'web/components/layout/col'
|
import { Col } from 'web/components/layout/col'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser, useUserById } from 'web/hooks/use-user'
|
||||||
import {
|
import {
|
||||||
useGroup,
|
useGroup,
|
||||||
useGroupContractIds,
|
useGroupContractIds,
|
||||||
|
@ -42,10 +42,10 @@ import { GroupComment } from 'common/comment'
|
||||||
import { REFERRAL_AMOUNT } from 'common/economy'
|
import { REFERRAL_AMOUNT } from 'common/economy'
|
||||||
import { UserLink } from 'web/components/user-link'
|
import { UserLink } from 'web/components/user-link'
|
||||||
import { GroupAboutPost } from 'web/components/groups/group-about-post'
|
import { GroupAboutPost } from 'web/components/groups/group-about-post'
|
||||||
import { getPost } from 'web/lib/firebase/posts'
|
import { getPost, listPosts, postPath } from 'web/lib/firebase/posts'
|
||||||
import { Post } from 'common/post'
|
import { Post } from 'common/post'
|
||||||
import { Spacer } from 'web/components/layout/spacer'
|
import { Spacer } from 'web/components/layout/spacer'
|
||||||
import { usePost } from 'web/hooks/use-post'
|
import { usePost, usePosts } from 'web/hooks/use-post'
|
||||||
import { useAdmin } from 'web/hooks/use-admin'
|
import { useAdmin } from 'web/hooks/use-admin'
|
||||||
import { track } from '@amplitude/analytics-browser'
|
import { track } from '@amplitude/analytics-browser'
|
||||||
import { ArrowLeftIcon } from '@heroicons/react/solid'
|
import { ArrowLeftIcon } from '@heroicons/react/solid'
|
||||||
|
@ -53,6 +53,10 @@ import { SelectMarketsModal } from 'web/components/contract-select-modal'
|
||||||
import { BETTORS } from 'common/user'
|
import { BETTORS } from 'common/user'
|
||||||
import { Page } from 'web/components/page'
|
import { Page } from 'web/components/page'
|
||||||
import { Tabs } from 'web/components/layout/tabs'
|
import { Tabs } from 'web/components/layout/tabs'
|
||||||
|
import { Avatar } from 'web/components/avatar'
|
||||||
|
import { Title } from 'web/components/title'
|
||||||
|
import { fromNow } from 'web/lib/util/time'
|
||||||
|
import { CreatePost } from 'web/components/create-post'
|
||||||
|
|
||||||
export const getStaticProps = fromPropz(getStaticPropz)
|
export const getStaticProps = fromPropz(getStaticPropz)
|
||||||
export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
||||||
|
@ -70,7 +74,8 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
||||||
? 'all'
|
? 'all'
|
||||||
: 'open'
|
: 'open'
|
||||||
const aboutPost =
|
const aboutPost =
|
||||||
group && group.aboutPostId != null && (await getPost(group.aboutPostId))
|
group && group.aboutPostId != null ? await getPost(group.aboutPostId) : null
|
||||||
|
|
||||||
const messages = group && (await listAllCommentsOnGroup(group.id))
|
const messages = group && (await listAllCommentsOnGroup(group.id))
|
||||||
|
|
||||||
const cachedTopTraderIds =
|
const cachedTopTraderIds =
|
||||||
|
@ -83,6 +88,9 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
||||||
|
|
||||||
const creator = await creatorPromise
|
const creator = await creatorPromise
|
||||||
|
|
||||||
|
const posts = ((group && (await listPosts(group.postIds))) ?? []).filter(
|
||||||
|
(p) => p != null
|
||||||
|
) as Post[]
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
group,
|
group,
|
||||||
|
@ -93,6 +101,7 @@ export async function getStaticPropz(props: { params: { slugs: string[] } }) {
|
||||||
messages,
|
messages,
|
||||||
aboutPost,
|
aboutPost,
|
||||||
suggestedFilter,
|
suggestedFilter,
|
||||||
|
posts,
|
||||||
},
|
},
|
||||||
|
|
||||||
revalidate: 60, // regenerate after a minute
|
revalidate: 60, // regenerate after a minute
|
||||||
|
@ -107,6 +116,7 @@ const groupSubpages = [
|
||||||
'markets',
|
'markets',
|
||||||
'leaderboards',
|
'leaderboards',
|
||||||
'about',
|
'about',
|
||||||
|
'posts',
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
export default function GroupPage(props: {
|
export default function GroupPage(props: {
|
||||||
|
@ -116,8 +126,9 @@ export default function GroupPage(props: {
|
||||||
topTraders: { user: User; score: number }[]
|
topTraders: { user: User; score: number }[]
|
||||||
topCreators: { user: User; score: number }[]
|
topCreators: { user: User; score: number }[]
|
||||||
messages: GroupComment[]
|
messages: GroupComment[]
|
||||||
aboutPost: Post
|
aboutPost: Post | null
|
||||||
suggestedFilter: 'open' | 'all'
|
suggestedFilter: 'open' | 'all'
|
||||||
|
posts: Post[]
|
||||||
}) {
|
}) {
|
||||||
props = usePropz(props, getStaticPropz) ?? {
|
props = usePropz(props, getStaticPropz) ?? {
|
||||||
group: null,
|
group: null,
|
||||||
|
@ -127,8 +138,9 @@ export default function GroupPage(props: {
|
||||||
topCreators: [],
|
topCreators: [],
|
||||||
messages: [],
|
messages: [],
|
||||||
suggestedFilter: 'open',
|
suggestedFilter: 'open',
|
||||||
|
posts: [],
|
||||||
}
|
}
|
||||||
const { creator, topTraders, topCreators, suggestedFilter } = props
|
const { creator, topTraders, topCreators, suggestedFilter, posts } = props
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { slugs } = router.query as { slugs: string[] }
|
const { slugs } = router.query as { slugs: string[] }
|
||||||
|
@ -137,6 +149,12 @@ export default function GroupPage(props: {
|
||||||
const group = useGroup(props.group?.id) ?? props.group
|
const group = useGroup(props.group?.id) ?? props.group
|
||||||
const aboutPost = usePost(props.aboutPost?.id) ?? props.aboutPost
|
const aboutPost = usePost(props.aboutPost?.id) ?? props.aboutPost
|
||||||
|
|
||||||
|
let groupPosts = usePosts(group?.postIds ?? []) ?? posts
|
||||||
|
|
||||||
|
if (aboutPost != null) {
|
||||||
|
groupPosts = [aboutPost, ...groupPosts]
|
||||||
|
}
|
||||||
|
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
const isAdmin = useAdmin()
|
const isAdmin = useAdmin()
|
||||||
const memberIds = useMemberIds(group?.id ?? null) ?? props.memberIds
|
const memberIds = useMemberIds(group?.id ?? null) ?? props.memberIds
|
||||||
|
@ -172,6 +190,16 @@ export default function GroupPage(props: {
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const postsPage = (
|
||||||
|
<>
|
||||||
|
<Col>
|
||||||
|
<div className="mt-4 flex flex-col gap-8 px-4 md:flex-row">
|
||||||
|
{posts && <GroupPosts posts={groupPosts} group={group} />}
|
||||||
|
</div>
|
||||||
|
</Col>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
const aboutTab = (
|
const aboutTab = (
|
||||||
<Col>
|
<Col>
|
||||||
{(group.aboutPostId != null || isCreator || isAdmin) && (
|
{(group.aboutPostId != null || isCreator || isAdmin) && (
|
||||||
|
@ -234,6 +262,10 @@ export default function GroupPage(props: {
|
||||||
title: 'About',
|
title: 'About',
|
||||||
content: aboutTab,
|
content: aboutTab,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Posts',
|
||||||
|
content: postsPage,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -413,6 +445,84 @@ function GroupLeaderboard(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function GroupPosts(props: { posts: Post[]; group: Group }) {
|
||||||
|
const { posts, group } = props
|
||||||
|
const [showCreatePost, setShowCreatePost] = useState(false)
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
|
const createPost = <CreatePost group={group} />
|
||||||
|
|
||||||
|
const postList = (
|
||||||
|
<div className=" align-start w-full items-start">
|
||||||
|
<Row className="flex justify-between">
|
||||||
|
<Col>
|
||||||
|
<Title text={'Posts'} className="!mt-0" />
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
{user && (
|
||||||
|
<Button
|
||||||
|
className="btn-md"
|
||||||
|
onClick={() => setShowCreatePost(!showCreatePost)}
|
||||||
|
>
|
||||||
|
Add a Post
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<div className="mt-2">
|
||||||
|
{posts.map((post) => (
|
||||||
|
<PostCard key={post.id} post={post} />
|
||||||
|
))}
|
||||||
|
{posts.length === 0 && (
|
||||||
|
<div className="text-center text-gray-500">No posts yet</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return showCreatePost ? createPost : postList
|
||||||
|
}
|
||||||
|
|
||||||
|
function PostCard(props: { post: Post }) {
|
||||||
|
const { post } = props
|
||||||
|
const creatorId = post.creatorId
|
||||||
|
|
||||||
|
const user = useUserById(creatorId)
|
||||||
|
|
||||||
|
if (!user) return <> </>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="py-1">
|
||||||
|
<Link href={postPath(post.slug)}>
|
||||||
|
<Row
|
||||||
|
className={
|
||||||
|
'relative gap-3 rounded-lg bg-white p-2 shadow-md hover:cursor-pointer hover:bg-gray-100'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<Avatar className="h-12 w-12" username={user?.username} />
|
||||||
|
</div>
|
||||||
|
<div className="">
|
||||||
|
<div className="text-sm text-gray-500">
|
||||||
|
<UserLink
|
||||||
|
className="text-neutral"
|
||||||
|
name={user?.name}
|
||||||
|
username={user?.username}
|
||||||
|
/>
|
||||||
|
<span className="mx-1">•</span>
|
||||||
|
<span className="text-gray-500">{fromNow(post.createdTime)}</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-lg font-medium text-gray-900">
|
||||||
|
{post.title}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Row>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
function AddContractButton(props: { group: Group; user: User }) {
|
function AddContractButton(props: { group: Group; user: User }) {
|
||||||
const { group, user } = props
|
const { group, user } = props
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user