mirror of
https://github.com/0xPARC/zkmessage.xyz.git
synced 2026-01-08 22:07:58 -05:00
added pagination and users page
This commit is contained in:
@@ -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
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 (
|
||||
<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
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 (
|
||||
<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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
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