Skip to content

Commit

Permalink
feat: Add pagination support
Browse files Browse the repository at this point in the history
  • Loading branch information
OXeu committed Jun 6, 2024
1 parent 20a5ab0 commit dd33b7f
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 45 deletions.
3 changes: 2 additions & 1 deletion client/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ API_URL=http://localhost:3001
OAUTH_URL=http://localhost:3001/user/github
AVATAR=your-avatar-url
NAME=your-name
DESCRIPTION=your-description
DESCRIPTION=your-description
PAGE_SIZE=10
51 changes: 46 additions & 5 deletions client/src/page/feeds.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,56 @@
import { useContext, useEffect, useRef, useState } from "react"
import { Link, useSearch } from "wouter"
import { FeedCard } from "../components/feed_card"
import { Waiting } from "../components/loading"
import { client } from "../main"
import { ProfileContext } from "../state/profile"
import { headersWithAuth } from "../utils/auth"

function tryInt(defaultValue: number, ...args: (string | number | undefined | null)[]): number {
for (const v of args) {
if (typeof v === "number") return v
if (typeof v === "string") {
const n = parseInt(v)
if (!isNaN(n)) return n
}
}
return defaultValue
}

export function FeedsPage() {
const query = new URLSearchParams(useSearch());
const profile = useContext(ProfileContext);
const [listState, setListState] = useState<'draft' | 'unlisted' | 'normal'>('normal')
const [feeds, setFeeds] = useState<any>()
const [hasNext, setHasNext] = useState<boolean>(false)
const page = tryInt(1, query.get("page"))
const limit = tryInt(10, query.get("limit"), process.env.PAGE_SIZE)
const ref = useRef(false)
useEffect(() => {
if (ref.current) return
function fetchFeeds() {
client.feed.index.get({
query: {
page: page,
limit: limit
},
headers: headersWithAuth()
}).then(({ data }) => {
if (data)
setFeeds(data)
if (data) {
setFeeds(data.data)
setHasNext(data.hasNext)
}
})
}
useEffect(() => {
if (ref.current) return
fetchFeeds()
ref.current = true
}, [])

useEffect(() => {
if (feeds) {
fetchFeeds()
}
}, [query.get("page")])
const feed_filtered = feeds?.filter(
({ draft, listed }: { draft: number | undefined, listed: number | undefined }) =>
listState === 'draft' ? draft === 1 : listState === 'unlisted' ? listed === 0 : draft != 1 && listed != 0)
Expand Down Expand Up @@ -50,8 +81,18 @@ export function FeedsPage() {
{feed_filtered && feed_filtered.map(({ id, ...feed }: any) => (
<FeedCard key={id} id={id} {...feed} />
))}
<div className="wauto flex justify-between items-center">
<Link href={"/?page=" + (page - 1)}
className={`text-sm mt-4 font-normal rounded-full px-4 py-2 text-white bg-theme ${page > 1 ? '' : 'invisible'}`}>
上一页
</Link>
<Link href={"/?page=" + (page + 1)}
className={`text-sm mt-4 font-normal rounded-full px-4 py-2 text-white bg-theme ${hasNext ? '' : 'invisible'}`}>
下一页
</Link>
</div>
</div>
</Waiting>
</>
)
}
}
1 change: 1 addition & 0 deletions docs/DEPLOY.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ NAME=Xeu # 昵称,显示在左上角
DESCRIPTION=杂食动物 # 个人描述,显示在左上角昵称下方
AVATAR=https://avatars.githubusercontent.com/u/36541432 # 头像地址,显示在左上角
API_URL=https://rin.xeu.life # 服务端域名,可以先留空后面再改
PAGE_SIZE=10 # 默认分页大小
SKIP_DEPENDENCY_INSTALL=true
UNSTABLE_PRE_BUILD=asdf install bun latest && asdf global bun latest && bun i
```
Expand Down
73 changes: 38 additions & 35 deletions docs/ENV.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,49 +2,52 @@

## 前端环境变量列表

| 名称 | 是否必须 | 描述 | 默认值| 示例值 |
|--------|--------|------|------|------|
| API_URL || 后端地址 ||http://localhost:3001|
| OAUTH_URL || OAuth 登录时前端跳转的地址 | ${API_URL}/user/github | http://localhost:3001/user/github|
| AVATAR || 网站左上角头像地址 || https://avatars.githubusercontent.com/u/36541432|
|NAME||网站左上角名称 & 标题|| Xeu |
|DESCRIPTION||网站左上角描述||杂食动物|
| 名称 | 是否必须 | 描述 | 默认值 | 示例值 |
| ----------- | -------- | -------------------------- | ---------------------- | ------------------------------------------------ |
| API_URL || 后端地址 || http://localhost:3001 |
| OAUTH_URL || OAuth 登录时前端跳转的地址 | ${API_URL}/user/github | http://localhost:3001/user/github |
| AVATAR || 网站左上角头像地址 || https://avatars.githubusercontent.com/u/36541432 |
| NAME || 网站左上角名称 & 标题 || Xeu |
| DESCRIPTION || 网站左上角描述 || 杂食动物 |
| PAGE_SIZE || 默认分页限制 | 10 | 10 |

**部署环境变量列表**
>[!CAUTION]
以下环境变量为部署到 Cloudflare Pages 必须,不能修改)

|名称||描述|
|---|---|---|
|SKIP_DEPENDENCY_INSTALL|true|跳过默认的 npm install 命令|
|UNSTABLE_PRE_BUILD|asdf install bun latest && asdf global bun latest && bun i|安装并使用 Bun 进行依赖安装|
> [!CAUTION]
> 以下环境变量为部署到 Cloudflare Pages 必须,不能修改)
| 名称 || 描述 |
| ----------------------- | ---------------------------------------------------------- | --------------------------- |
| SKIP_DEPENDENCY_INSTALL | true | 跳过默认的 npm install 命令 |
| UNSTABLE_PRE_BUILD | asdf install bun latest && asdf global bun latest && bun i | 安装并使用 Bun 进行依赖安装 |

## 后端环境变量列表

**明文环境变量**
>[!NOTE]
以下变量在 Cloudflare Workers 中保持不加密即可

| 名称 | 是否必须 | 描述 | 默认值| 示例值|
|--------|--------|------|------|------|
|FRONTEND_URL| 暂时必须 | 评论通知 Webhook 时包含评论文章链接时所需,可留空||https://xeu.life|
|S3_FOLDER|必须|上传保存图片时资源存放的文件路径||images/|
> [!NOTE]
> 以下变量在 Cloudflare Workers 中保持不加密即可
| 名称 | 是否必须 | 描述 | 默认值 | 示例值 |
| ------------ | -------- | ------------------------------------------------- | ------ | ---------------- |
| FRONTEND_URL | 暂时必须 | 评论通知 Webhook 时包含评论文章链接时所需,可留空 || https://xeu.life |
| S3_FOLDER | 必须 | 上传保存图片时资源存放的文件路径 || images/ |

**加密环境变量,以下所有内容均为必须(Webhook 除外)**
>[!NOTE]
由于部署时会清除所有不在 `wrangler.toml` 中的明文变量。\
以下环境变量在 Cloudflare Workers 中调试完毕后必须加密,否则会被清除

| 名称 | 描述 | 示例值|
|--------|------|------|
|GITHUB_CLIENT_ID| Github OAuth 的客户端 ID |Ux66poMrKi1k11M1Q1b2|
|GITHUB_CLIENT_SECRET|Github OAuth 的客户端密钥 |1234567890abcdef1234567890abcdef12345678|
|JWT_SECRET| JWT 认证所需密钥,可为常规格式的任意密码|J0sT%Ch@nge#Me1|
|S3_BUCKET|S3 存储桶名称 | images |
|S3_REGION| S3 存储桶所在区域,如使用 Cloudflare R2 填写 auto 即可| auto |
|S3_ENDPOINT|S3 存储桶接入点地址|https://1234567890abcdef1234567890abcd.r2.cloudflarestorage.com|
|S3_ACCESS_HOST|S3 存储桶访问地址| https://image.xeu.life |
|S3_ACCESS_KEY_ID| S3 存储桶访问所需的 KEY ID,使用 Cloudflare R2 时为拥有 R2 编辑权限的 API 令牌 ID|1234567890abcdef1234567890abcd|
|S3_SECRET_ACCESS_KEY|S3 存储桶访问所需的 Secret,使用 Cloudflare R2 时为拥有 R2 编辑权限的 API 令牌|1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef|
|WEBHOOK_URL|唯一非必须环境变量|https://webhook.example.com/webhook|

> [!NOTE]
> 由于部署时会清除所有不在 `wrangler.toml` 中的明文变量。\
> 以下环境变量在 Cloudflare Workers 中调试完毕后必须加密,否则会被清除
| 名称 | 描述 | 示例值 |
| -------------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| GITHUB_CLIENT_ID | Github OAuth 的客户端 ID | Ux66poMrKi1k11M1Q1b2 |
| GITHUB_CLIENT_SECRET | Github OAuth 的客户端密钥 | 1234567890abcdef1234567890abcdef12345678 |
| JWT_SECRET | JWT 认证所需密钥,可为常规格式的任意密码 | J0sT%Ch@nge#Me1 |
| S3_BUCKET | S3 存储桶名称 | images |
| S3_REGION | S3 存储桶所在区域,如使用 Cloudflare R2 填写 auto 即可 | auto |
| S3_ENDPOINT | S3 存储桶接入点地址 | https://1234567890abcdef1234567890abcd.r2.cloudflarestorage.com |
| S3_ACCESS_HOST | S3 存储桶访问地址 | https://image.xeu.life |
| S3_ACCESS_KEY_ID | S3 存储桶访问所需的 KEY ID,使用 Cloudflare R2 时为拥有 R2 编辑权限的 API 令牌 ID | 1234567890abcdef1234567890abcd |
| S3_SECRET_ACCESS_KEY | S3 存储桶访问所需的 Secret,使用 Cloudflare R2 时为拥有 R2 编辑权限的 API 令牌 | 1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef |
| WEBHOOK_URL | 唯一非必须环境变量 | https://webhook.example.com/webhook |
28 changes: 24 additions & 4 deletions server/src/services/feed.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import { and, desc, eq, or } from "drizzle-orm";
import Elysia, { t } from "elysia";
import type { DB } from "../_worker";
import type { Env } from "../db/db";
import { feeds } from "../db/schema";
import { setup } from "../setup";
import { bindTagToPost } from "./tag";
import type { Env } from "../db/db";

export const FeedService = (db: DB, env: Env) => new Elysia({ aot: false })
.use(setup(db, env))
.group('/feed', (group) =>
group
.get('/', async ({ admin }) => {
.get('/', async ({ admin, query: { page, limit } }) => {
const page_num = (page ? page > 0 ? page : 1 : 1) - 1;
const limit_num = limit ? +limit > 50 ? 50 : +limit : 20;
const feed_list = (await db.query.feeds.findMany({
where: admin ? undefined : and(eq(feeds.draft, 0), eq(feeds.listed, 1)),
columns: admin ? undefined : {
Expand All @@ -28,7 +30,9 @@ export const FeedService = (db: DB, env: Env) => new Elysia({ aot: false })
columns: { id: true, username: true, avatar: true }
}
},
orderBy: [desc(feeds.createdAt), desc(feeds.updatedAt)]
orderBy: [desc(feeds.createdAt), desc(feeds.updatedAt)],
offset: page_num * limit_num,
limit: limit_num + 1,
})).map(({ content, hashtags, summary, ...other }) => {
// 提取首图
const img_reg = /!\[.*?\]\((.*?)\)/;
Expand All @@ -44,7 +48,23 @@ export const FeedService = (db: DB, env: Env) => new Elysia({ aot: false })
...other
}
});
return feed_list;
if (feed_list.length === limit_num + 1) {
feed_list.pop();
return {
data: feed_list,
hasNext: true
}
} else {
return {
data: feed_list,
hasNext: false
}
}
}, {
query: t.Object({
page: t.Optional(t.Numeric()),
limit: t.Optional(t.Numeric())
})
})
.post('/', async ({ admin, set, uid, body: { title, alias, listed, content, summary, draft, tags } }) => {
if (!admin) {
Expand Down

0 comments on commit dd33b7f

Please sign in to comment.