added pagination and users page

This commit is contained in:
Joel Gustafson
2021-12-19 18:31:57 -05:00
parent 53c50ac014
commit 2c92db1740
8 changed files with 254 additions and 25 deletions

View File

@@ -2,8 +2,13 @@ import React from "react"
export function About({}) { export function About({}) {
return ( return (
<div className="p-4 bg-white rounded-lg cursor-pointer hover:underline"> <div className="p-4 bg-white rounded-lg">
<a href="https://github.com/joeltg/zkmessage.xyz">GitHub</a> <a
className="hover:underline"
href="https://github.com/joeltg/zkmessage.xyz"
>
GitHub
</a>
</div> </div>
) )
} }

41
components/Directory.tsx Normal file
View File

@@ -0,0 +1,41 @@
import React from "react"
import { User } from "utils/types"
import { UserIcon } from "./UserIcon"
interface DirectoryProps {
users: User[]
userCount: number
}
export const Directory: React.FC<DirectoryProps> = (props) => {
const rest = props.userCount - props.users.length
return (
<div className="p-4 bg-white rounded-lg flex flex-col gap-2">
{props.users.map((user) => (
<div key={user.publicKey} className="flex gap-2 items-center">
<UserIcon user={user} />
<a
className="mt-0.5 hover:underline flex-1"
href={`https://twitter.com/${user.twitterHandle}`}
>
{user.twitterHandle}
</a>
<a
href={`https://twitter.com/${user.twitterHandle}/status/${user.verificationTweetId}`}
>
<img
width={16}
src="/key.svg"
className="inline-block opacity-20 hover:opacity-70"
/>
</a>
</div>
))}
{rest > 0 && (
<a href="/users" className="hover:underline self-end">
... all users
</a>
)}
</div>
)
}

View File

@@ -20,7 +20,7 @@ export function MessageView(props: MessageViewProps) {
}, []) }, [])
return ( return (
<div className="my-4 px-4 pt-4 pb-2 bg-white rounded-lg"> <div className="px-4 pt-4 pb-2 bg-white rounded-lg">
<div>{props.message.body}</div> <div>{props.message.body}</div>
<div className="mt-2 flex gap-1 items-baseline"> <div className="mt-2 flex gap-1 items-baseline">
<div className="flex-1 text-sm text-gray-400 "> <div className="flex-1 text-sm text-gray-400 ">

25
components/PageNav.tsx Normal file
View File

@@ -0,0 +1,25 @@
import React from "react"
interface PageNavProps {
currentPage: number
lastPage: number
}
export const PageNav: React.FC<PageNavProps> = (props) => {
return (
<div className="p-2 flex gap-1 text-gray-500">
<div className="flex-1">
page {props.currentPage} of {props.lastPage}
</div>
<a href={`?page=1`}>first</a>
<span>·</span>
<a href={`?page=${Math.max(props.currentPage - 1, 1)}`}>previous</a>
<span>·</span>
<a href={`?page=${Math.min(props.currentPage + 1, props.lastPage)}`}>
next
</a>
<span>·</span>
<a href={`?page=${props.lastPage}`}>last</a>
</div>
)
}

View File

@@ -29,7 +29,7 @@ export function ThreadView(props: ThreadViewProps) {
}, []) }, [])
return ( return (
<div className="my-4 px-4 pt-4 pb-2 bg-white rounded-lg"> <div className="px-4 pt-4 pb-2 bg-white rounded-lg">
<div>{props.thread.firstMessage.body}</div> <div>{props.thread.firstMessage.body}</div>
<div className="flex gap-1 items-center my-2"> <div className="flex gap-1 items-center my-2">
<div className="flex gap-1 flex-1"> <div className="flex gap-1 flex-1">

View File

@@ -21,8 +21,14 @@ import { CreateThread } from "components/CreateThread"
import { ThreadView } from "components/ThreadView" import { ThreadView } from "components/ThreadView"
import { getVKeys } from "utils/server/vkeys" import { getVKeys } from "utils/server/vkeys"
import { About } from "components/About" import { About } from "components/About"
import { Directory } from "components/Directory"
import { number } from "fp-ts"
import { PageNav } from "components/PageNav"
interface IndexPageProps extends PageProps { interface IndexPageProps extends PageProps {
currentPage: number
threadCount: number
userCount: number
defaultUsers: User[] defaultUsers: User[]
vKeys: VKeys vKeys: VKeys
threads: (Thread & { threads: (Thread & {
@@ -32,19 +38,29 @@ interface IndexPageProps extends PageProps {
})[] })[]
} }
const threadPageSize = 20
const defaultUserLimit = 20
export const getServerSideProps: GetServerSideProps<IndexPageProps, {}> = export const getServerSideProps: GetServerSideProps<IndexPageProps, {}> =
async (ctx) => { async (ctx) => {
const { publicKey } = nookies.get(ctx) const { publicKey } = nookies.get(ctx)
const defaultUsers = await prisma.user.findMany({ const defaultUsers = await prisma.user.findMany({
// afaikt this should work but doesn't...
// orderBy: { threads: { _count: "desc" } }, // orderBy: { threads: { _count: "desc" } },
select: userProps, select: userProps,
take: 20, take: defaultUserLimit,
}) })
const userCount = await prisma.user.count()
const page =
typeof ctx.query.page === "string" ? parseInt(ctx.query.page) : NaN
const threads = await prisma.thread.findMany({ const threads = await prisma.thread.findMany({
take: 20, take: threadPageSize,
orderBy: { createdAt: "desc" }, skip: isNaN(page) ? 0 : (page - 1) * threadPageSize,
orderBy: { updatedAt: "desc" },
where: { firstMessageId: { not: null } }, where: { firstMessageId: { not: null } },
select: { select: {
...threadProps, ...threadProps,
@@ -56,6 +72,8 @@ export const getServerSideProps: GetServerSideProps<IndexPageProps, {}> =
}, },
}) })
const threadCount = await prisma.thread.count()
const serializedThreads = threads.map( const serializedThreads = threads.map(
({ createdAt, updatedAt, firstMessage, _count, ...thread }) => ({ ({ createdAt, updatedAt, firstMessage, _count, ...thread }) => ({
id: thread.id, id: thread.id,
@@ -73,9 +91,18 @@ export const getServerSideProps: GetServerSideProps<IndexPageProps, {}> =
) )
const vKeys = getVKeys() const vKeys = getVKeys()
const currentPage = isNaN(page) ? 1 : page
if (publicKey === undefined) { if (publicKey === undefined) {
return { return {
props: { vKeys, user: null, threads: serializedThreads, defaultUsers }, props: {
vKeys,
user: null,
threads: serializedThreads,
defaultUsers,
userCount,
currentPage,
threadCount,
},
} }
} else { } else {
const user = await prisma.user.findUnique({ const user = await prisma.user.findUnique({
@@ -88,24 +115,35 @@ export const getServerSideProps: GetServerSideProps<IndexPageProps, {}> =
} }
return { return {
props: { vKeys, user, threads: serializedThreads, defaultUsers }, props: {
vKeys,
user,
threads: serializedThreads,
defaultUsers,
userCount,
currentPage,
threadCount,
},
} }
} }
} }
export default function IndexPage(props: IndexPageProps) { export default function IndexPage(props: IndexPageProps) {
const lastPage = Math.ceil(props.threadCount / threadPageSize)
return ( return (
<div className="max-w-4xl m-auto px-4 font-mono"> <div className="max-w-4xl m-auto px-4 font-mono">
<Header /> <Header />
<div className="grid grid-cols-4 gap-6 pt-2 pb-14"> <div className="grid grid-cols-4 gap-6 pt-2 pb-14">
<div className="col-span-3"> <div className="col-span-3 flex flex-col gap-4">
<CreateThread defaultUsers={props.defaultUsers} /> <CreateThread defaultUsers={props.defaultUsers} />
{props.threads.map((thread) => ( {props.threads.map((thread) => (
<ThreadView key={thread.id} vKeys={props.vKeys} thread={thread} /> <ThreadView key={thread.id} vKeys={props.vKeys} thread={thread} />
))} ))}
<PageNav currentPage={props.currentPage} lastPage={lastPage} />
</div> </div>
<div className="col-span-1"> <div className="col-span-1 flex flex-col gap-2">
<About /> <About />
<Directory users={props.defaultUsers} userCount={props.userCount} />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -22,6 +22,7 @@ import { UserIcon } from "components/UserIcon"
import { CreateMessage } from "components/CreateMessage" import { CreateMessage } from "components/CreateMessage"
import { PageContext } from "utils/context" import { PageContext } from "utils/context"
import { PageNav } from "components/PageNav"
type ThreadPageParams = { id: string } type ThreadPageParams = { id: string }
@@ -30,8 +31,12 @@ interface ThreadPageProps extends PageProps {
thread: Thread thread: Thread
group: User[] group: User[]
messages: Message[] messages: Message[]
currentPage: number
messageCount: number
} }
const messagePageSize = 20
export const getServerSideProps: GetServerSideProps< export const getServerSideProps: GetServerSideProps<
ThreadPageProps, ThreadPageProps,
ThreadPageParams ThreadPageParams
@@ -42,12 +47,23 @@ export const getServerSideProps: GetServerSideProps<
const { publicKey } = nookies.get(ctx, "publicKey") const { publicKey } = nookies.get(ctx, "publicKey")
const page =
typeof ctx.query.page === "string" ? parseInt(ctx.query.page) : NaN
const thread = await prisma.thread.findUnique({ const thread = await prisma.thread.findUnique({
where: { id: ctx.params.id }, where: { id: ctx.params.id },
select: { select: {
...threadProps, ...threadProps,
group: { select: userProps }, group: { select: userProps },
messages: { select: messageProps }, messages: {
take: messagePageSize,
skip: isNaN(page) ? 0 : (page - 1) * messagePageSize,
orderBy: { createdAt: "desc" },
select: messageProps,
},
_count: {
select: { messages: true },
},
}, },
}) })
@@ -56,6 +72,7 @@ export const getServerSideProps: GetServerSideProps<
} }
const { id, createdAt, updatedAt, group, messages } = thread const { id, createdAt, updatedAt, group, messages } = thread
const messageCount = thread._count.messages
const serializedThread = { const serializedThread = {
id, id,
@@ -73,6 +90,7 @@ export const getServerSideProps: GetServerSideProps<
) )
const vKeys = getVKeys() const vKeys = getVKeys()
const currentPage = isNaN(page) ? 1 : page
if (publicKey === undefined) { if (publicKey === undefined) {
return { return {
props: { props: {
@@ -81,6 +99,8 @@ export const getServerSideProps: GetServerSideProps<
thread: serializedThread, thread: serializedThread,
group, group,
messages: serializedMessages, messages: serializedMessages,
currentPage,
messageCount,
}, },
} }
} else { } else {
@@ -100,6 +120,8 @@ export const getServerSideProps: GetServerSideProps<
thread: serializedThread, thread: serializedThread,
group, group,
messages: serializedMessages, messages: serializedMessages,
currentPage,
messageCount,
}, },
} }
} }
@@ -114,21 +136,13 @@ export default function ThreadPage(props: ThreadPageProps) {
[user] [user]
) )
const lastPage = Math.ceil(props.messageCount / messagePageSize)
return ( return (
<div className="max-w-4xl m-auto px-4 font-mono"> <div className="max-w-4xl m-auto px-4 font-mono">
<Header /> <Header />
<div className="grid grid-cols-4 gap-6 pt-2 pb-14"> <div className="grid grid-cols-4 gap-6 pt-2 pb-14">
<div className="col-span-3"> <div className="col-span-3 flex flex-col gap-4">
<div className="bg-white rounded-lg flex gap-x-2">
{props.group.map((user) => (
<div
key={user.publicKey}
className="bg-gray-200 rounded flex items-center gap-x-2 text-sm py-1 px-2 m-2"
>
<UserIcon user={user} /> {user.twitterHandle}
</div>
))}
</div>
{props.messages.map((message) => ( {props.messages.map((message) => (
<MessageView <MessageView
key={message.id} key={message.id}
@@ -137,12 +151,35 @@ export default function ThreadPage(props: ThreadPageProps) {
message={message} message={message}
/> />
))} ))}
{userInGroup && ( {userInGroup && props.currentPage === lastPage && (
<CreateMessage thread={props.thread} group={props.group} /> <CreateMessage thread={props.thread} group={props.group} />
)} )}
<PageNav currentPage={props.currentPage} lastPage={lastPage} />
</div> </div>
<div className="col-span-1"> <div className="col-span-1">
<About /> <div className="p-4 bg-white rounded-lg">
<div className="mb-4">in this thread</div>
{props.group.map((user) => (
<div key={user.publicKey} className="flex items-center gap-x-2">
<UserIcon user={user} />
<a
className="mt-0.5 hover:underline flex-1"
href={`https://twitter.com/${user.twitterHandle}`}
>
{user.twitterHandle}
</a>
<a
href={`https://twitter.com/${user.twitterHandle}/status/${user.verificationTweetId}`}
>
<img
width={16}
src="/key.svg"
className="inline-block opacity-20 hover:opacity-70"
/>
</a>
</div>
))}
</div>
</div> </div>
</div> </div>
</div> </div>

83
pages/users.tsx Normal file
View File

@@ -0,0 +1,83 @@
import React from "react"
import type { GetServerSideProps } from "next"
import nookies from "nookies"
import { PageProps, User, userProps } from "utils/types"
import { UserIcon } from "components/UserIcon"
interface UsersPageProps extends PageProps {
users: User[]
}
const pageSize = 100
export const getServerSideProps: GetServerSideProps<UsersPageProps, {}> =
async (ctx) => {
const { publicKey } = nookies.get(ctx)
const { after } = ctx.query
const cursor =
typeof after === "string" ? { cursor: { publicKey: after } } : null
const users = await prisma.user.findMany({
select: userProps,
take: pageSize,
orderBy: { publicKey: "asc" },
skip: typeof after === "string" ? 1 : 0,
...cursor,
})
if (publicKey === undefined) {
return { props: { user: null, users } }
} else {
const user = await prisma.user.findUnique({
where: { publicKey },
select: userProps,
})
if (user === null) {
nookies.destroy(ctx, "publicKey", { path: "/", httpOnly: true })
}
return { props: { user, users } }
}
}
export default function UsersPage(props: UsersPageProps) {
const cursor: User | undefined = props.users[pageSize - 1]
return (
<div className="mt-16 max-w-lg m-auto font-mono">
<div className="m-2 border bg-white rounded-xl p-6 flex flex-col gap-2">
{props.users.map((user) => (
<div key={user.publicKey} className="flex gap-2 items-center">
<UserIcon user={user} />
<a
className="mt-0.5 hover:underline flex-1"
href={`https://twitter.com/${user.twitterHandle}`}
>
{user.twitterHandle}
</a>
<a
href={`https://twitter.com/${user.twitterHandle}/status/${user.verificationTweetId}`}
>
<img
width={16}
src="/key.svg"
className="inline-block opacity-20 hover:opacity-70"
/>
</a>
</div>
))}
{cursor && (
<a
href={`?after=${cursor.publicKey}`}
className="self-end hover:underline"
>
next
</a>
)}
</div>
</div>
)
}