Posts changes (#988)
* Add post subtitle * Add "Post" badge to post card * Move post tab to overview tab, refactor components * Fix styling nits.
This commit is contained in:
parent
49e97ddac1
commit
83d9a1f3e2
|
@ -3,6 +3,7 @@ import { JSONContent } from '@tiptap/core'
|
||||||
export type Post = {
|
export type Post = {
|
||||||
id: string
|
id: string
|
||||||
title: string
|
title: string
|
||||||
|
subtitle: string
|
||||||
content: JSONContent
|
content: JSONContent
|
||||||
creatorId: string // User id
|
creatorId: string // User id
|
||||||
createdTime: number
|
createdTime: number
|
||||||
|
@ -17,3 +18,4 @@ export type DateDoc = Post & {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MAX_POST_TITLE_LENGTH = 480
|
export const MAX_POST_TITLE_LENGTH = 480
|
||||||
|
export const MAX_POST_SUBTITLE_LENGTH = 480
|
||||||
|
|
|
@ -3,7 +3,11 @@ import * as admin from 'firebase-admin'
|
||||||
import { getUser } from './utils'
|
import { getUser } from './utils'
|
||||||
import { slugify } from '../../common/util/slugify'
|
import { slugify } from '../../common/util/slugify'
|
||||||
import { randomString } from '../../common/util/random'
|
import { randomString } from '../../common/util/random'
|
||||||
import { Post, MAX_POST_TITLE_LENGTH } from '../../common/post'
|
import {
|
||||||
|
Post,
|
||||||
|
MAX_POST_TITLE_LENGTH,
|
||||||
|
MAX_POST_SUBTITLE_LENGTH,
|
||||||
|
} from '../../common/post'
|
||||||
import { APIError, newEndpoint, validate } from './api'
|
import { APIError, newEndpoint, validate } from './api'
|
||||||
import { JSONContent } from '@tiptap/core'
|
import { JSONContent } from '@tiptap/core'
|
||||||
import { z } from 'zod'
|
import { z } from 'zod'
|
||||||
|
@ -36,6 +40,7 @@ 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),
|
||||||
|
subtitle: z.string().min(1).max(MAX_POST_SUBTITLE_LENGTH),
|
||||||
content: contentSchema,
|
content: contentSchema,
|
||||||
groupId: z.string().optional(),
|
groupId: z.string().optional(),
|
||||||
|
|
||||||
|
@ -48,10 +53,8 @@ const postSchema = z.object({
|
||||||
|
|
||||||
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, groupId, question, ...otherProps } = validate(
|
const { title, subtitle, content, groupId, question, ...otherProps } =
|
||||||
postSchema,
|
validate(postSchema, req.body)
|
||||||
req.body
|
|
||||||
)
|
|
||||||
|
|
||||||
const creator = await getUser(auth.uid)
|
const creator = await getUser(auth.uid)
|
||||||
if (!creator)
|
if (!creator)
|
||||||
|
@ -89,6 +92,7 @@ export const createpost = newEndpoint({}, async (req, auth) => {
|
||||||
creatorId: creator.id,
|
creatorId: creator.id,
|
||||||
slug,
|
slug,
|
||||||
title,
|
title,
|
||||||
|
subtitle,
|
||||||
createdTime: Date.now(),
|
createdTime: Date.now(),
|
||||||
content: content,
|
content: content,
|
||||||
contractSlug,
|
contractSlug,
|
||||||
|
|
|
@ -13,6 +13,8 @@ import { Group } from 'common/group'
|
||||||
|
|
||||||
export function CreatePost(props: { group?: Group }) {
|
export function CreatePost(props: { group?: Group }) {
|
||||||
const [title, setTitle] = useState('')
|
const [title, setTitle] = useState('')
|
||||||
|
const [subtitle, setSubtitle] = useState('')
|
||||||
|
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||||
|
|
||||||
|
@ -22,12 +24,17 @@ export function CreatePost(props: { group?: Group }) {
|
||||||
disabled: isSubmitting,
|
disabled: isSubmitting,
|
||||||
})
|
})
|
||||||
|
|
||||||
const isValid = editor && title.length > 0 && editor.isEmpty === false
|
const isValid =
|
||||||
|
editor &&
|
||||||
|
title.length > 0 &&
|
||||||
|
subtitle.length > 0 &&
|
||||||
|
editor.isEmpty === false
|
||||||
|
|
||||||
async function savePost(title: string) {
|
async function savePost(title: string) {
|
||||||
if (!editor) return
|
if (!editor) return
|
||||||
const newPost = {
|
const newPost = {
|
||||||
title: title,
|
title: title,
|
||||||
|
subtitle: subtitle,
|
||||||
content: editor.getJSON(),
|
content: editor.getJSON(),
|
||||||
groupId: group?.id,
|
groupId: group?.id,
|
||||||
}
|
}
|
||||||
|
@ -62,6 +69,20 @@ export function CreatePost(props: { group?: Group }) {
|
||||||
onChange={(e) => setTitle(e.target.value || '')}
|
onChange={(e) => setTitle(e.target.value || '')}
|
||||||
/>
|
/>
|
||||||
<Spacer h={6} />
|
<Spacer h={6} />
|
||||||
|
<label className="label">
|
||||||
|
<span className="mb-1">
|
||||||
|
Subtitle<span className={'text-red-700'}> *</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<Textarea
|
||||||
|
placeholder="e.g. How Elon Musk is getting everyone's attention"
|
||||||
|
className="input input-bordered resize-none"
|
||||||
|
autoFocus
|
||||||
|
maxLength={MAX_POST_TITLE_LENGTH}
|
||||||
|
value={subtitle}
|
||||||
|
onChange={(e) => setSubtitle(e.target.value || '')}
|
||||||
|
/>
|
||||||
|
<Spacer h={6} />
|
||||||
<label className="label">
|
<label className="label">
|
||||||
<span className="mb-1">
|
<span className="mb-1">
|
||||||
Content<span className={'text-red-700'}> *</span>
|
Content<span className={'text-red-700'}> *</span>
|
||||||
|
|
|
@ -43,6 +43,7 @@ function RichEditGroupAboutPost(props: { group: Group; post: Post | null }) {
|
||||||
if (!editor) return
|
if (!editor) return
|
||||||
const newPost = {
|
const newPost = {
|
||||||
title: group.name,
|
title: group.name,
|
||||||
|
subtitle: 'About post for the group',
|
||||||
content: editor.getJSON(),
|
content: editor.getJSON(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,8 +36,11 @@ import { CopyLinkButton } from '../copy-link-button'
|
||||||
import { REFERRAL_AMOUNT } from 'common/economy'
|
import { REFERRAL_AMOUNT } from 'common/economy'
|
||||||
import toast from 'react-hot-toast'
|
import toast from 'react-hot-toast'
|
||||||
import { ENV_CONFIG } from 'common/envs/constants'
|
import { ENV_CONFIG } from 'common/envs/constants'
|
||||||
import { PostCard } from '../post-card'
|
import { PostCard, PostCardList } from '../post-card'
|
||||||
import { LoadingIndicator } from '../loading-indicator'
|
import { LoadingIndicator } from '../loading-indicator'
|
||||||
|
import { useUser } from 'web/hooks/use-user'
|
||||||
|
import { CreatePost } from '../create-post'
|
||||||
|
import { Modal } from '../layout/modal'
|
||||||
|
|
||||||
const MAX_TRENDING_POSTS = 6
|
const MAX_TRENDING_POSTS = 6
|
||||||
|
|
||||||
|
@ -59,7 +62,6 @@ export function GroupOverview(props: {
|
||||||
posts={posts}
|
posts={posts}
|
||||||
isEditable={isEditable}
|
isEditable={isEditable}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{(group.aboutPostId != null || isEditable) && (
|
{(group.aboutPostId != null || isEditable) && (
|
||||||
<>
|
<>
|
||||||
<SectionHeader label={'About'} href={'/post/' + group.slug} />
|
<SectionHeader label={'About'} href={'/post/' + group.slug} />
|
||||||
|
@ -87,10 +89,55 @@ export function GroupOverview(props: {
|
||||||
user={user}
|
user={user}
|
||||||
memberIds={memberIds}
|
memberIds={memberIds}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<GroupPosts group={group} posts={posts} />
|
||||||
</Col>
|
</Col>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function GroupPosts(props: { posts: Post[]; group: Group }) {
|
||||||
|
const { posts, group } = props
|
||||||
|
const [showCreatePost, setShowCreatePost] = useState(false)
|
||||||
|
const user = useUser()
|
||||||
|
|
||||||
|
const createPost = (
|
||||||
|
<Modal size="xl" open={showCreatePost} setOpen={setShowCreatePost}>
|
||||||
|
<div className="w-full bg-white py-10">
|
||||||
|
<CreatePost group={group} />
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
|
||||||
|
const postList = (
|
||||||
|
<div className=" align-start w-full items-start">
|
||||||
|
<Row className="flex justify-between">
|
||||||
|
<Col>
|
||||||
|
<SectionHeader label={'Latest Posts'} />
|
||||||
|
</Col>
|
||||||
|
<Col>
|
||||||
|
{user && (
|
||||||
|
<Button
|
||||||
|
className="btn-md"
|
||||||
|
onClick={() => setShowCreatePost(!showCreatePost)}
|
||||||
|
>
|
||||||
|
Add a Post
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
|
||||||
|
<div className="mt-2">
|
||||||
|
<PostCardList posts={posts} />
|
||||||
|
{posts.length === 0 && (
|
||||||
|
<div className="text-center text-gray-500">No posts yet</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
return showCreatePost ? createPost : postList
|
||||||
|
}
|
||||||
|
|
||||||
function GroupOverviewPinned(props: {
|
function GroupOverviewPinned(props: {
|
||||||
group: Group
|
group: Group
|
||||||
posts: Post[]
|
posts: Post[]
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { Contract } from 'common/contract'
|
||||||
import { Group } from 'common/group'
|
import { Group } from 'common/group'
|
||||||
import { Post } from 'common/post'
|
import { Post } from 'common/post'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { PostCardList } from 'web/pages/group/[...slugs]'
|
|
||||||
import { Button } from './button'
|
import { Button } from './button'
|
||||||
import { PillButton } from './buttons/pill-button'
|
import { PillButton } from './buttons/pill-button'
|
||||||
import { ContractSearch } from './contract-search'
|
import { ContractSearch } from './contract-search'
|
||||||
|
@ -10,6 +9,7 @@ import { Col } from './layout/col'
|
||||||
import { Modal } from './layout/modal'
|
import { Modal } from './layout/modal'
|
||||||
import { Row } from './layout/row'
|
import { Row } from './layout/row'
|
||||||
import { LoadingIndicator } from './loading-indicator'
|
import { LoadingIndicator } from './loading-indicator'
|
||||||
|
import { PostCardList } from './post-card'
|
||||||
|
|
||||||
export function PinnedSelectModal(props: {
|
export function PinnedSelectModal(props: {
|
||||||
title: string
|
title: string
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import { track } from '@amplitude/analytics-browser'
|
import { track } from '@amplitude/analytics-browser'
|
||||||
|
import { DocumentIcon } from '@heroicons/react/solid'
|
||||||
import clsx from 'clsx'
|
import clsx from 'clsx'
|
||||||
import { Post } from 'common/post'
|
import { Post } from 'common/post'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
@ -27,7 +28,7 @@ export function PostCard(props: {
|
||||||
<div className="relative py-1">
|
<div className="relative py-1">
|
||||||
<Row
|
<Row
|
||||||
className={clsx(
|
className={clsx(
|
||||||
' relative gap-3 rounded-lg bg-white py-2 shadow-md hover:cursor-pointer hover:bg-gray-100',
|
'relative gap-3 rounded-lg bg-white py-2 px-3 shadow-md hover:cursor-pointer hover:bg-gray-100',
|
||||||
itemIds?.includes(post.id) && highlightClassName
|
itemIds?.includes(post.id) && highlightClassName
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
@ -44,7 +45,18 @@ export function PostCard(props: {
|
||||||
<span className="mx-1">•</span>
|
<span className="mx-1">•</span>
|
||||||
<span className="text-gray-500">{fromNow(post.createdTime)}</span>
|
<span className="text-gray-500">{fromNow(post.createdTime)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="text-lg font-medium text-gray-900">{post.title}</div>
|
<div className=" break-words text-lg font-medium text-gray-900">
|
||||||
|
{post.title}
|
||||||
|
</div>
|
||||||
|
<div className="font-small text-md break-words text-gray-500">
|
||||||
|
{post.subtitle}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className="inline-flex items-center gap-1 whitespace-nowrap rounded-full bg-indigo-300 px-2 py-0.5 text-xs font-medium text-white">
|
||||||
|
<DocumentIcon className={'h3 w-3'} />
|
||||||
|
Post
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Row>
|
</Row>
|
||||||
{onPostClick ? (
|
{onPostClick ? (
|
||||||
|
@ -80,3 +92,23 @@ export function PostCard(props: {
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function PostCardList(props: {
|
||||||
|
posts: Post[]
|
||||||
|
highlightOptions?: CardHighlightOptions
|
||||||
|
onPostClick?: (post: Post) => void
|
||||||
|
}) {
|
||||||
|
const { posts, onPostClick, highlightOptions } = props
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
{posts.map((post) => (
|
||||||
|
<PostCard
|
||||||
|
key={post.id}
|
||||||
|
post={post}
|
||||||
|
onPostClick={onPostClick}
|
||||||
|
highlightOptions={highlightOptions}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@ export default function CreateDateDocPage() {
|
||||||
})
|
})
|
||||||
|
|
||||||
const title = `${user?.name}'s Date Doc`
|
const title = `${user?.name}'s Date Doc`
|
||||||
|
const subtitle = 'Manifold dating docs'
|
||||||
const [birthday, setBirthday] = useState<undefined | string>(undefined)
|
const [birthday, setBirthday] = useState<undefined | string>(undefined)
|
||||||
const [question, setQuestion] = useState(
|
const [question, setQuestion] = useState(
|
||||||
'Will I find a partner in the next 3 months?'
|
'Will I find a partner in the next 3 months?'
|
||||||
|
@ -46,6 +47,7 @@ export default function CreateDateDocPage() {
|
||||||
'id' | 'creatorId' | 'createdTime' | 'slug' | 'contractSlug'
|
'id' | 'creatorId' | 'createdTime' | 'slug' | 'contractSlug'
|
||||||
> & { question: string } = {
|
> & { question: string } = {
|
||||||
title,
|
title,
|
||||||
|
subtitle,
|
||||||
content: editor.getJSON(),
|
content: editor.getJSON(),
|
||||||
bounty: 0,
|
bounty: 0,
|
||||||
birthday: birthdayTime,
|
birthday: birthdayTime,
|
||||||
|
|
|
@ -42,11 +42,7 @@ 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 { Title } from 'web/components/title'
|
|
||||||
import { CreatePost } from 'web/components/create-post'
|
|
||||||
import { GroupOverview } from 'web/components/groups/group-overview'
|
import { GroupOverview } from 'web/components/groups/group-overview'
|
||||||
import { CardHighlightOptions } from 'web/components/contract/contracts-grid'
|
|
||||||
import { PostCard } from 'web/components/post-card'
|
|
||||||
|
|
||||||
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[] } }) {
|
||||||
|
@ -183,16 +179,6 @@ 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 overviewPage = (
|
const overviewPage = (
|
||||||
<>
|
<>
|
||||||
<GroupOverview
|
<GroupOverview
|
||||||
|
@ -249,10 +235,6 @@ export default function GroupPage(props: {
|
||||||
title: 'Leaderboards',
|
title: 'Leaderboards',
|
||||||
content: leaderboardTab,
|
content: leaderboardTab,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
title: 'Posts',
|
|
||||||
content: postsPage,
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -336,63 +318,6 @@ function GroupLeaderboard(props: {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export 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">
|
|
||||||
<PostCardList posts={posts} />
|
|
||||||
{posts.length === 0 && (
|
|
||||||
<div className="text-center text-gray-500">No posts yet</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
return showCreatePost ? createPost : postList
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PostCardList(props: {
|
|
||||||
posts: Post[]
|
|
||||||
highlightOptions?: CardHighlightOptions
|
|
||||||
onPostClick?: (post: Post) => void
|
|
||||||
}) {
|
|
||||||
const { posts, onPostClick, highlightOptions } = props
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
{posts.map((post) => (
|
|
||||||
<PostCard
|
|
||||||
key={post.id}
|
|
||||||
post={post}
|
|
||||||
onPostClick={onPostClick}
|
|
||||||
highlightOptions={highlightOptions}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</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)
|
||||||
|
|
|
@ -25,6 +25,7 @@ import { useCommentsOnPost } from 'web/hooks/use-comments'
|
||||||
import { useUser } from 'web/hooks/use-user'
|
import { useUser } from 'web/hooks/use-user'
|
||||||
import { usePost } from 'web/hooks/use-post'
|
import { usePost } from 'web/hooks/use-post'
|
||||||
import { SEO } from 'web/components/SEO'
|
import { SEO } from 'web/components/SEO'
|
||||||
|
import { Subtitle } from 'web/components/subtitle'
|
||||||
|
|
||||||
export async function getStaticProps(props: { params: { slugs: string[] } }) {
|
export async function getStaticProps(props: { params: { slugs: string[] } }) {
|
||||||
const { slugs } = props.params
|
const { slugs } = props.params
|
||||||
|
@ -75,7 +76,11 @@ export default function PostPage(props: {
|
||||||
url={'/post/' + post.slug}
|
url={'/post/' + post.slug}
|
||||||
/>
|
/>
|
||||||
<div className="mx-auto w-full max-w-3xl ">
|
<div className="mx-auto w-full max-w-3xl ">
|
||||||
<Title className="!mt-0 py-4 px-2" text={post.title} />
|
<div>
|
||||||
|
<Title className="!my-0 px-2 pt-4" text={post.title} />
|
||||||
|
<br />
|
||||||
|
<Subtitle className="!mt-2 px-2 pb-4" text={post.subtitle} />
|
||||||
|
</div>
|
||||||
<Row>
|
<Row>
|
||||||
<Col className="flex-1 px-2">
|
<Col className="flex-1 px-2">
|
||||||
<div className={'inline-flex'}>
|
<div className={'inline-flex'}>
|
||||||
|
|
Loading…
Reference in New Issue
Block a user