mirror of
https://github.com/0xPARC/zkmessage.xyz.git
synced 2026-01-09 22:37:55 -05:00
added pagination and users page
This commit is contained in:
@@ -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
41
components/Directory.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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
25
components/PageNav.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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
83
pages/users.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user