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({}) {
return (
<div className="p-4 bg-white rounded-lg cursor-pointer hover:underline">
<a href="https://github.com/joeltg/zkmessage.xyz">GitHub</a>
<div className="p-4 bg-white rounded-lg">
<a
className="hover:underline"
href="https://github.com/joeltg/zkmessage.xyz"
>
GitHub
</a>
</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 (
<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 className="mt-2 flex gap-1 items-baseline">
<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 (
<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 className="flex gap-1 items-center my-2">
<div className="flex gap-1 flex-1">

View File

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

View File

@@ -22,6 +22,7 @@ import { UserIcon } from "components/UserIcon"
import { CreateMessage } from "components/CreateMessage"
import { PageContext } from "utils/context"
import { PageNav } from "components/PageNav"
type ThreadPageParams = { id: string }
@@ -30,8 +31,12 @@ interface ThreadPageProps extends PageProps {
thread: Thread
group: User[]
messages: Message[]
currentPage: number
messageCount: number
}
const messagePageSize = 20
export const getServerSideProps: GetServerSideProps<
ThreadPageProps,
ThreadPageParams
@@ -42,12 +47,23 @@ export const getServerSideProps: GetServerSideProps<
const { publicKey } = nookies.get(ctx, "publicKey")
const page =
typeof ctx.query.page === "string" ? parseInt(ctx.query.page) : NaN
const thread = await prisma.thread.findUnique({
where: { id: ctx.params.id },
select: {
...threadProps,
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 messageCount = thread._count.messages
const serializedThread = {
id,
@@ -73,6 +90,7 @@ export const getServerSideProps: GetServerSideProps<
)
const vKeys = getVKeys()
const currentPage = isNaN(page) ? 1 : page
if (publicKey === undefined) {
return {
props: {
@@ -81,6 +99,8 @@ export const getServerSideProps: GetServerSideProps<
thread: serializedThread,
group,
messages: serializedMessages,
currentPage,
messageCount,
},
}
} else {
@@ -100,6 +120,8 @@ export const getServerSideProps: GetServerSideProps<
thread: serializedThread,
group,
messages: serializedMessages,
currentPage,
messageCount,
},
}
}
@@ -114,21 +136,13 @@ export default function ThreadPage(props: ThreadPageProps) {
[user]
)
const lastPage = Math.ceil(props.messageCount / messagePageSize)
return (
<div className="max-w-4xl m-auto px-4 font-mono">
<Header />
<div className="grid grid-cols-4 gap-6 pt-2 pb-14">
<div className="col-span-3">
<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>
<div className="col-span-3 flex flex-col gap-4">
{props.messages.map((message) => (
<MessageView
key={message.id}
@@ -137,12 +151,35 @@ export default function ThreadPage(props: ThreadPageProps) {
message={message}
/>
))}
{userInGroup && (
{userInGroup && props.currentPage === lastPage && (
<CreateMessage thread={props.thread} group={props.group} />
)}
<PageNav currentPage={props.currentPage} lastPage={lastPage} />
</div>
<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>

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