Add basic /author/{id} page

This commit is contained in:
Max Leiter 2022-12-04 01:55:20 -08:00
parent 56eefc8419
commit 7eeadbe065
7 changed files with 86 additions and 26 deletions

View file

@ -37,7 +37,7 @@ const Post = ({
initialPost?: string initialPost?: string
newPostParent?: string newPostParent?: string
}) => { }) => {
const parsedPost = JSON.parse(stringifiedInitialPost || "{}") const parsedPost = JSON.parse(stringifiedInitialPost || "{}")
const initialPost = parsedPost?.id ? parsedPost : null const initialPost = parsedPost?.id ? parsedPost : null
const { setToast } = useToasts() const { setToast } = useToasts()
const router = useRouter() const router = useRouter()

View file

@ -1,8 +1,9 @@
import CreatedAgoBadge from '@components/badges/created-ago-badge' import CreatedAgoBadge from "@components/badges/created-ago-badge"
import ExpirationBadge from '@components/badges/expiration-badge' import ExpirationBadge from "@components/badges/expiration-badge"
import VisibilityBadge from '@components/badges/visibility-badge' import VisibilityBadge from "@components/badges/visibility-badge"
import Skeleton from '@components/skeleton' import Link from "@components/link"
import styles from './title.module.css' import Skeleton from "@components/skeleton"
import styles from "./title.module.css"
type TitleProps = { type TitleProps = {
title: string title: string
@ -11,22 +12,25 @@ type TitleProps = {
visibility?: string visibility?: string
createdAt?: Date createdAt?: Date
expiresAt?: Date expiresAt?: Date
authorId?: string
} }
export const PostTitle = ({ export const PostTitle = ({
title, title,
displayName, displayName,
visibility, visibility,
createdAt, createdAt,
expiresAt, expiresAt,
loading loading,
authorId
}: TitleProps) => { }: TitleProps) => {
return ( return (
<span className={styles.title}> <span className={styles.title}>
<h3> <h3>
{title}{" "} {title}{" "}
<span style={{ color: "var(--gray)" }}> <span style={{ color: "var(--gray)" }}>
by {displayName || "anonymous"} by{" "}
<Link href={`/author/${authorId}`}>{displayName || "anonymous"}</Link>
</span> </span>
</h3> </h3>
{!loading && ( {!loading && (

View file

@ -65,9 +65,10 @@ const getPost = async (id: string) => {
title: "", title: "",
createdAt: new Date("1970-01-01"), createdAt: new Date("1970-01-01"),
author: { author: {
displayName: "" displayName: "",
}, },
description: "" description: "",
authorId: "",
}, },
isProtected: true, isProtected: true,
isAuthor: isAuthorOrAdmin isAuthor: isAuthorOrAdmin
@ -110,6 +111,7 @@ const PostView = async ({
createdAt={post.createdAt} createdAt={post.createdAt}
displayName={post.author?.displayName || ""} displayName={post.author?.displayName || ""}
visibility={post.visibility} visibility={post.visibility}
authorId={post.authorId}
/> />
</div> </div>
{post.description && ( {post.description && (

View file

@ -0,0 +1,38 @@
import PostList from "@components/post-list"
import { getPostsByUser, getUserById } from "@lib/server/prisma"
import { Suspense } from "react"
async function PostListWrapper({
posts,
userId,
}: {
posts: ReturnType<typeof getPostsByUser>
userId: string
}) {
const data = (await posts).filter((post) => post.visibility === "public")
return <PostList morePosts={false} userId={userId} initialPosts={JSON.stringify(data)} />
}
export default async function UserPage({
params
}: {
params: {
username: string
}
}) {
// TODO: the route should be user.name, not id
const id = params.username
const user = await getUserById(id)
const posts = getPostsByUser(id, true)
return (
<>
<h1>{user?.displayName}&apos;s public posts</h1>
<Suspense fallback={<PostList initialPosts={JSON.stringify({})} />}>
{/* @ts-ignore because TS async JSX support is iffy */}
<PostListWrapper posts={posts} userId={id} />
</Suspense>
</>
)
}

View file

@ -1,7 +1,6 @@
"use client" "use client"
import styles from "./post-list.module.css" import styles from "./post-list.module.css"
import { ListItemSkeleton } from "./list-item-skeleton"
import ListItem from "./list-item" import ListItem from "./list-item"
import { ChangeEvent, useCallback, useEffect, useState } from "react" import { ChangeEvent, useCallback, useEffect, useState } from "react"
import useDebounce from "@lib/hooks/use-debounce" import useDebounce from "@lib/hooks/use-debounce"
@ -10,10 +9,11 @@ import type { PostWithFiles } from "@lib/server/prisma"
import Input from "@components/input" import Input from "@components/input"
import Button from "@components/button" import Button from "@components/button"
import { useToasts } from "@components/toasts" import { useToasts } from "@components/toasts"
import { ListItemSkeleton } from "./list-item-skeleton"
type Props = { type Props = {
initialPosts: string | PostWithFiles[] initialPosts: string | PostWithFiles[]
morePosts: boolean morePosts?: boolean
userId?: string userId?: string
} }
@ -120,12 +120,12 @@ const PostList = ({
/> />
</div> </div>
{!posts && <p style={{ color: "var(--warning)" }}>Failed to load.</p>} {!posts && <p style={{ color: "var(--warning)" }}>Failed to load.</p>}
{/* {!posts?.length && ( {!posts?.length && (
<ul> <ul>
<ListItemSkeleton /> <ListItemSkeleton />
<ListItemSkeleton /> <ListItemSkeleton />
</ul> </ul>
)} */} )}
{posts?.length === 0 && posts && ( {posts?.length === 0 && posts && (
<p> <p>
No posts found. Create one{" "} No posts found. Create one{" "}

View file

@ -218,10 +218,12 @@ export const searchPosts = async (
query: string, query: string,
{ {
withFiles = false, withFiles = false,
userId userId,
publicOnly
}: { }: {
withFiles?: boolean withFiles?: boolean
userId?: User["id"] userId?: User["id"]
publicOnly?: boolean
} = {} } = {}
): Promise<PostWithFiles[]> => { ): Promise<PostWithFiles[]> => {
const posts = await prisma.post.findMany({ const posts = await prisma.post.findMany({
@ -231,7 +233,8 @@ export const searchPosts = async (
title: { title: {
search: query search: query
}, },
authorId: userId authorId: userId,
visibility: publicOnly ? "public" : undefined
}, },
{ {
files: { files: {
@ -241,7 +244,8 @@ export const searchPosts = async (
}, },
userId: userId userId: userId
} }
} },
visibility: publicOnly ? "public" : undefined
} }
] ]
}, },

View file

@ -1,11 +1,14 @@
import { withMethods } from "@lib/api-middleware/with-methods" import { withMethods } from "@lib/api-middleware/with-methods"
import { parseQueryParam } from "@lib/server/parse-query-param" import { parseQueryParam } from "@lib/server/parse-query-param"
import { searchPosts } from "@lib/server/prisma" import { PostWithFiles, searchPosts } from "@lib/server/prisma"
import { NextApiRequest, NextApiResponse } from "next" import { NextApiRequest, NextApiResponse } from "next"
import { getSession } from "next-auth/react"
const handler = async (req: NextApiRequest, res: NextApiResponse) => { const handler = async (req: NextApiRequest, res: NextApiResponse) => {
const { q, userId } = req.query const { q, userId } = req.query
const session = await getSession()
const query = parseQueryParam(q) const query = parseQueryParam(q)
if (!query) { if (!query) {
res.status(400).json({ error: "Invalid query" }) res.status(400).json({ error: "Invalid query" })
@ -13,9 +16,18 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
} }
try { try {
const posts = await searchPosts(query, { let posts: PostWithFiles[]
userId: parseQueryParam(userId), if (session?.user.id === userId || session?.user.role === "admin") {
}) posts = await searchPosts(query, {
userId: parseQueryParam(userId),
publicOnly: true
})
} else {
posts = await searchPosts(query, {
userId: parseQueryParam(userId),
publicOnly: true
})
}
res.status(200).json(posts) res.status(200).json(posts)
} catch (err) { } catch (err) {