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:
FRC 2022-10-05 11:37:23 +01:00 committed by GitHub
parent 49e97ddac1
commit 83d9a1f3e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 126 additions and 87 deletions

View File

@ -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

View File

@ -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,

View File

@ -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>

View File

@ -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(),
} }

View File

@ -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[]

View File

@ -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

View File

@ -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>
)
}

View File

@ -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,

View File

@ -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)

View File

@ -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'}>