From e94a7e68ff12b1e6183ca9fd143750603cfb21ab Mon Sep 17 00:00:00 2001 From: zetaloop Date: Sat, 28 Feb 2026 17:25:57 +0800 Subject: [PATCH] feat(posts): wire community pages to backend posts API --- app/(main)/community/page.tsx | 175 +++++++++++++++++--------------- app/(main)/post/[id]/page.tsx | 8 +- app/(main)/user/[id]/page.tsx | 2 +- components/post-like-button.tsx | 47 +++++++-- lib/api/posts.ts | 99 ++++++++++++------ 5 files changed, 204 insertions(+), 127 deletions(-) diff --git a/app/(main)/community/page.tsx b/app/(main)/community/page.tsx index e2a3d17..6fb8646 100644 --- a/app/(main)/community/page.tsx +++ b/app/(main)/community/page.tsx @@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button" import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card" import { listGames, listOrders, listPlayers, listPosts } from "@/lib/api" import { roleLabels } from "@/lib/constants" -import type { Game, Player } from "@/lib/types" +import type { Game, Player, Post } from "@/lib/types" import { ClipboardList, Heart, MessageCircle, PenSquare, Pin } from "lucide-react" import Link from "next/link" import { useEffect, useState } from "react" @@ -14,7 +14,8 @@ import { useEffect, useState } from "react" export default function CommunityPage() { const [games, setGames] = useState([]) const [players, setPlayers] = useState([]) - const posts = listPosts() + const [posts, setPosts] = useState([]) + const [postsLoading, setPostsLoading] = useState(true) const orders = listOrders() const [sortMode, setSortMode] = useState<"latest" | "hot">("latest") @@ -23,16 +24,22 @@ export default function CommunityPage() { useEffect(() => { let cancelled = false - Promise.all([listGames(), listPlayers()]) - .then(([gamesItems, playersItems]) => { + setPostsLoading(true) + + Promise.all([listGames(), listPlayers(), listPosts()]) + .then(([gamesItems, playersItems, postsItems]) => { if (cancelled) return setGames(gamesItems) setPlayers(playersItems) + setPosts(postsItems) + setPostsLoading(false) }) .catch(() => { if (cancelled) return setGames([]) setPlayers([]) + setPosts([]) + setPostsLoading(false) }) return () => { @@ -98,86 +105,92 @@ export default function CommunityPage() {
- {filteredPosts.map((post) => - (() => { - const linkedOrder = post.linkedOrderId - ? orders.find((order) => order.id === post.linkedOrderId) - : null - const linkedPlayer = linkedOrder - ? players.find((player) => player.id === linkedOrder.playerId) - : null + {postsLoading ? ( +
加载中...
+ ) : filteredPosts.length === 0 ? ( +
暂无帖子
+ ) : ( + filteredPosts.map((post) => + (() => { + const linkedOrder = post.linkedOrderId + ? orders.find((order) => order.id === post.linkedOrderId) + : null + const linkedPlayer = linkedOrder + ? players.find((player) => player.id === linkedOrder.playerId) + : null - return ( - - - -
- - - {post.author.nickname[0]} - -
-
- {post.author.nickname} - - {roleLabels[post.authorRole]} - - {post.pinned && } -
- - {new Date(post.createdAt).toLocaleDateString("zh-CN")} - -
-
-
- -

{post.title}

-

{post.content}

- {post.tags.length > 0 && ( -
- {post.tags.map((tag) => ( - - {tag} - - ))} -
- )} - {post.linkedOrderId && ( -
-
- - 关联订单秀单 -
- {linkedOrder && ( -
-

- {linkedOrder.service.gameName} · {linkedOrder.service.title} -

-

- {linkedOrder.playerName} - {linkedPlayer ? ` · ${linkedPlayer.rating}` : ""} -

+ return ( + + + +
+ + + {post.author.nickname[0]} + +
+
+ {post.author.nickname} + + {roleLabels[post.authorRole]} + + {post.pinned && }
- )} + + {new Date(post.createdAt).toLocaleDateString("zh-CN")} + +
- )} - - - - - {post.likeCount} - - - - {post.commentCount} - - -
- - ) - })(), + + +

{post.title}

+

{post.content}

+ {post.tags.length > 0 && ( +
+ {post.tags.map((tag) => ( + + {tag} + + ))} +
+ )} + {post.linkedOrderId && ( +
+
+ + 关联订单秀单 +
+ {linkedOrder && ( +
+

+ {linkedOrder.service.gameName} · {linkedOrder.service.title} +

+

+ {linkedOrder.playerName} + {linkedPlayer ? ` · ${linkedPlayer.rating}` : ""} +

+
+ )} +
+ )} +
+ + + + {post.likeCount} + + + + {post.commentCount} + + + + + ) + })(), + ) )}
diff --git a/app/(main)/post/[id]/page.tsx b/app/(main)/post/[id]/page.tsx index f36b202..511cb83 100644 --- a/app/(main)/post/[id]/page.tsx +++ b/app/(main)/post/[id]/page.tsx @@ -14,7 +14,7 @@ import { notFound } from "next/navigation" export default async function PostDetailPage({ params }: { params: Promise<{ id: string }> }) { const { id } = await params - const post = getPostById(id) + const post = await getPostById(id) if (!post) notFound() const linkedOrder = post.linkedOrderId ? getOrderById(post.linkedOrderId) : null @@ -91,7 +91,11 @@ export default async function PostDetailPage({ params }: { params: Promise<{ id: )}
- +
diff --git a/app/(main)/user/[id]/page.tsx b/app/(main)/user/[id]/page.tsx index a26ba8c..2404c58 100644 --- a/app/(main)/user/[id]/page.tsx +++ b/app/(main)/user/[id]/page.tsx @@ -22,7 +22,7 @@ export default async function UserProfilePage({ params }: { params: Promise<{ id } const [userPosts, userFavorites, players, shops] = await Promise.all([ - listPostsByAuthor(user.id), + listPostsByAuthor(id), listFavoritesByUser(user.id), listPlayers(), listShops(), diff --git a/components/post-like-button.tsx b/components/post-like-button.tsx index 99d21aa..0186b06 100644 --- a/components/post-like-button.tsx +++ b/components/post-like-button.tsx @@ -1,34 +1,59 @@ "use client" import { togglePostLike } from "@/lib/api/posts" +import { toApiError } from "@/lib/errors" +import { notifyInfo } from "@/lib/toast" import { useRequireAuth } from "@/lib/use-require-auth" -import { usePostStore } from "@/store/posts" import { Heart } from "lucide-react" +import { useState } from "react" interface PostLikeButtonProps { postId: string + initialLiked: boolean + initialLikeCount: number } -export function PostLikeButton({ postId }: PostLikeButtonProps) { +export function PostLikeButton({ postId, initialLiked, initialLikeCount }: PostLikeButtonProps) { const { requireAuth } = useRequireAuth() - const post = usePostStore((state) => state.posts.find((item) => item.id === postId)) - - if (!post) { - return null - } + const [liked, setLiked] = useState(initialLiked) + const [likeCount, setLikeCount] = useState(initialLikeCount) + const [pending, setPending] = useState(false) return ( ) } diff --git a/lib/api/posts.ts b/lib/api/posts.ts index 4fb5a0a..9aa2ec1 100644 --- a/lib/api/posts.ts +++ b/lib/api/posts.ts @@ -1,43 +1,78 @@ -import { addNotification } from "@/lib/api/notifications" -import { allow, deny } from "@/lib/decision" -import { useAuthStore } from "@/store/auth" -import { usePostStore } from "@/store/posts" +import { isApiError } from "@/lib/errors" +import type { Post } from "@/lib/types" -export function listPosts() { - return usePostStore.getState().posts -} +import { httpJson } from "./http" -export function getPostById(postId: string) { - return usePostStore.getState().posts.find((post) => post.id === postId) -} - -export function listPostsByAuthor(userId: string) { - return usePostStore.getState().posts.filter((post) => post.author.id === userId) -} - -export function togglePostLike(postId: string) { - const user = useAuthStore.getState().user - if (!user) { - return deny(401, "请先登录") +type Paginated = { + items: T[] + meta: { + total: number + offset: number + limit: number } +} - const post = usePostStore.getState().posts.find((item) => item.id === postId) - if (!post) { - return deny(404, "帖子不存在") - } +type ListOptions = { + offset?: number + limit?: number +} - const shouldNotify = !post.liked +function withOffsetLimit(path: string, options?: ListOptions): string { + const offset = options?.offset ?? 0 + const limit = options?.limit ?? 1000 - usePostStore.getState().togglePostLike(postId) + const searchParams = new URLSearchParams({ + offset: String(offset), + limit: String(limit), + }) + return `${path}?${searchParams.toString()}` +} - if (shouldNotify) { - addNotification({ - type: "community", - title: "帖子收到点赞", - content: `《${post.title}》有新的点赞`, - link: `/post/${post.id}`, +export async function listPosts(options?: ListOptions): Promise { + const res = await httpJson>(withOffsetLimit("/api/v1/posts", options), { + cache: "no-store", + }) + return res.items +} + +export async function getPostById(postId: string): Promise { + try { + return await httpJson(`/api/v1/posts/${encodeURIComponent(postId)}`, { + cache: "no-store", }) + } catch (error) { + if (error instanceof Error && error.message === "UNAUTHORIZED") { + throw error + } + if (isApiError(error) && error.code === 404) { + return undefined + } + throw error + } +} + +export async function listPostsByAuthor(userId: string, options?: ListOptions): Promise { + const res = await httpJson>( + withOffsetLimit(`/api/v1/users/${encodeURIComponent(userId)}/posts`, options), + { + cache: "no-store", + }, + ) + return res.items +} + +export async function togglePostLike(postId: string, currentlyLiked: boolean): Promise { + const encodedId = encodeURIComponent(postId) + if (currentlyLiked) { + await httpJson(`/api/v1/posts/${encodedId}/like`, { + method: "DELETE", + cache: "no-store", + }) + return } - return allow() + await httpJson(`/api/v1/posts/${encodedId}/like`, { + method: "POST", + cache: "no-store", + }) }