feat(post): persist new posts and wire like interactions
This commit is contained in:
@@ -1,27 +1,24 @@
|
|||||||
import { ArrowLeft, Heart, MessageCircle, Pin, Star } from "lucide-react"
|
import { ArrowLeft, MessageCircle, Pin, Star } from "lucide-react"
|
||||||
import Image from "next/image"
|
import Image from "next/image"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { notFound } from "next/navigation"
|
import { notFound } from "next/navigation"
|
||||||
import { PostComments } from "@/components/post-comments"
|
import { PostComments } from "@/components/post-comments"
|
||||||
|
import { PostLikeButton } from "@/components/post-like-button"
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Card, CardContent, CardHeader } from "@/components/ui/card"
|
import { Card, CardContent, CardHeader } from "@/components/ui/card"
|
||||||
import { Separator } from "@/components/ui/separator"
|
import { Separator } from "@/components/ui/separator"
|
||||||
|
import { getOrderById, getPlayerById, getPostById, listCommentsByPost } from "@/lib/api"
|
||||||
import { roleLabels } from "@/lib/constants"
|
import { roleLabels } from "@/lib/constants"
|
||||||
import { mockComments, mockOrders, mockPlayers, mockPosts } from "@/lib/mock"
|
|
||||||
|
|
||||||
export default async function PostDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
export default async function PostDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||||
const { id } = await params
|
const { id } = await params
|
||||||
const post = mockPosts.find((p) => p.id === id)
|
const post = getPostById(id)
|
||||||
if (!post) notFound()
|
if (!post) notFound()
|
||||||
|
|
||||||
const comments = mockComments.filter((c) => c.postId === id)
|
const comments = listCommentsByPost(id)
|
||||||
const linkedOrder = post.linkedOrderId
|
const linkedOrder = post.linkedOrderId ? getOrderById(post.linkedOrderId) : null
|
||||||
? mockOrders.find((o) => o.id === post.linkedOrderId)
|
const linkedPlayer = linkedOrder ? getPlayerById(linkedOrder.playerId) : null
|
||||||
: null
|
|
||||||
const linkedPlayer = linkedOrder
|
|
||||||
? mockPlayers.find((player) => player.id === linkedOrder.playerId)
|
|
||||||
: null
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto py-8 px-4 max-w-2xl">
|
<div className="container mx-auto py-8 px-4 max-w-2xl">
|
||||||
@@ -94,13 +91,7 @@ export default async function PostDetailPage({ params }: { params: Promise<{ id:
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-4 text-sm text-muted-foreground pt-2">
|
<div className="flex items-center gap-4 text-sm text-muted-foreground pt-2">
|
||||||
<button
|
<PostLikeButton initialLiked={post.liked} initialCount={post.likeCount} />
|
||||||
type="button"
|
|
||||||
className="flex items-center gap-1 hover:text-foreground transition-colors"
|
|
||||||
>
|
|
||||||
<Heart className={`h-4 w-4 ${post.liked ? "fill-red-500 text-red-500" : ""}`} />
|
|
||||||
{post.likeCount}
|
|
||||||
</button>
|
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
<MessageCircle className="h-4 w-4" />
|
<MessageCircle className="h-4 w-4" />
|
||||||
{post.commentCount}
|
{post.commentCount}
|
||||||
|
|||||||
@@ -20,10 +20,10 @@ import {
|
|||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/components/ui/select"
|
} from "@/components/ui/select"
|
||||||
import { Textarea } from "@/components/ui/textarea"
|
import { Textarea } from "@/components/ui/textarea"
|
||||||
import { mockPosts } from "@/lib/mock"
|
|
||||||
import { useRequireAuth } from "@/lib/use-require-auth"
|
import { useRequireAuth } from "@/lib/use-require-auth"
|
||||||
import { useAuthStore } from "@/store/auth"
|
import { useAuthStore } from "@/store/auth"
|
||||||
import { useOrderStore } from "@/store/orders"
|
import { useOrderStore } from "@/store/orders"
|
||||||
|
import { usePostStore } from "@/store/posts"
|
||||||
|
|
||||||
const postSchema = z.object({
|
const postSchema = z.object({
|
||||||
title: z.string().min(2, "标题至少2个字符").max(50, "标题最多50个字符"),
|
title: z.string().min(2, "标题至少2个字符").max(50, "标题最多50个字符"),
|
||||||
@@ -37,7 +37,10 @@ export default function NewPostPage() {
|
|||||||
const { isAuthenticated, requireAuth } = useRequireAuth()
|
const { isAuthenticated, requireAuth } = useRequireAuth()
|
||||||
const currentRole = useAuthStore((state) => state.currentRole)
|
const currentRole = useAuthStore((state) => state.currentRole)
|
||||||
const userId = useAuthStore((state) => state.user?.id)
|
const userId = useAuthStore((state) => state.user?.id)
|
||||||
|
const user = useAuthStore((state) => state.user)
|
||||||
const orders = useOrderStore((state) => state.orders)
|
const orders = useOrderStore((state) => state.orders)
|
||||||
|
const posts = usePostStore((state) => state.posts)
|
||||||
|
const createPost = usePostStore((state) => state.createPost)
|
||||||
const [postType, setPostType] = useState("normal")
|
const [postType, setPostType] = useState("normal")
|
||||||
const [selectedTags, setSelectedTags] = useState<string[]>([])
|
const [selectedTags, setSelectedTags] = useState<string[]>([])
|
||||||
const [imageCount, setImageCount] = useState(0)
|
const [imageCount, setImageCount] = useState(0)
|
||||||
@@ -48,7 +51,7 @@ export default function NewPostPage() {
|
|||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors, isSubmitting },
|
formState: { errors, isSubmitting },
|
||||||
} = useForm({
|
} = useForm<z.infer<typeof postSchema>>({
|
||||||
resolver: standardSchemaResolver(postSchema),
|
resolver: standardSchemaResolver(postSchema),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -68,14 +71,28 @@ export default function NewPostPage() {
|
|||||||
(order) => order.status === "completed" && order.consumerId === userId,
|
(order) => order.status === "completed" && order.consumerId === userId,
|
||||||
)
|
)
|
||||||
|
|
||||||
const onSubmit = async () => {
|
const onSubmit = async (data: z.infer<typeof postSchema>) => {
|
||||||
if (!isAuthenticated) {
|
if (!isAuthenticated) {
|
||||||
requireAuth(() => undefined)
|
requireAuth(() => undefined)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
await new Promise((r) => setTimeout(r, 500))
|
requireAuth(() => {
|
||||||
|
if (!user) return
|
||||||
|
|
||||||
|
createPost({
|
||||||
|
author: user,
|
||||||
|
authorRole: currentRole,
|
||||||
|
title: data.title,
|
||||||
|
content: data.content,
|
||||||
|
images: Array.from({ length: imageCount }).map(() => "/posts/p1-1.jpg"),
|
||||||
|
tags: selectedTags,
|
||||||
|
linkedOrderId: postType === "show_order" ? selectedOrderId : undefined,
|
||||||
|
quotedPostId: postType === "quote" ? selectedQuotePostId : undefined,
|
||||||
|
})
|
||||||
|
|
||||||
router.push("/community")
|
router.push("/community")
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -134,7 +151,7 @@ export default function NewPostPage() {
|
|||||||
<SelectValue placeholder="选择要引用的帖子" />
|
<SelectValue placeholder="选择要引用的帖子" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{mockPosts.map((post) => (
|
{posts.map((post) => (
|
||||||
<SelectItem key={post.id} value={post.id}>
|
<SelectItem key={post.id} value={post.id}>
|
||||||
{post.title}
|
{post.title}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@@ -145,7 +162,7 @@ export default function NewPostPage() {
|
|||||||
<p className="font-medium text-foreground">预览:</p>
|
<p className="font-medium text-foreground">预览:</p>
|
||||||
{selectedQuotePostId ? (
|
{selectedQuotePostId ? (
|
||||||
(() => {
|
(() => {
|
||||||
const post = mockPosts.find((p) => p.id === selectedQuotePostId)
|
const post = posts.find((p) => p.id === selectedQuotePostId)
|
||||||
if (!post) return <p className="mt-1">未找到帖子</p>
|
if (!post) return <p className="mt-1">未找到帖子</p>
|
||||||
return (
|
return (
|
||||||
<div className="mt-2 rounded border bg-background p-3">
|
<div className="mt-2 rounded border bg-background p-3">
|
||||||
|
|||||||
@@ -83,6 +83,23 @@ export function PostComments({ initialComments, postId }: PostCommentsProps) {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground mt-1 transition-colors"
|
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground mt-1 transition-colors"
|
||||||
|
onClick={() =>
|
||||||
|
requireAuth(() => {
|
||||||
|
setComments((prev) =>
|
||||||
|
prev.map((item) => {
|
||||||
|
if (item.id !== comment.id) return item
|
||||||
|
const nextLiked = !item.liked
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
liked: nextLiked,
|
||||||
|
likeCount: nextLiked
|
||||||
|
? item.likeCount + 1
|
||||||
|
: Math.max(0, item.likeCount - 1),
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<Heart
|
<Heart
|
||||||
className={`h-3 w-3 ${comment.liked ? "fill-red-500 text-red-500" : ""}`}
|
className={`h-3 w-3 ${comment.liked ? "fill-red-500 text-red-500" : ""}`}
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import { Heart } from "lucide-react"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { useRequireAuth } from "@/lib/use-require-auth"
|
||||||
|
|
||||||
|
interface PostLikeButtonProps {
|
||||||
|
initialLiked: boolean
|
||||||
|
initialCount: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export function PostLikeButton({ initialLiked, initialCount }: PostLikeButtonProps) {
|
||||||
|
const { requireAuth } = useRequireAuth()
|
||||||
|
const [liked, setLiked] = useState(initialLiked)
|
||||||
|
const [count, setCount] = useState(initialCount)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex items-center gap-1 hover:text-foreground transition-colors"
|
||||||
|
onClick={() =>
|
||||||
|
requireAuth(() => {
|
||||||
|
setLiked((prevLiked) => {
|
||||||
|
const nextLiked = !prevLiked
|
||||||
|
setCount((prevCount) => (nextLiked ? prevCount + 1 : Math.max(0, prevCount - 1)))
|
||||||
|
return nextLiked
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Heart className={`h-4 w-4 ${liked ? "fill-red-500 text-red-500" : ""}`} />
|
||||||
|
{count}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
+4
-4
@@ -1,13 +1,13 @@
|
|||||||
import { mockPosts } from "@/lib/mock"
|
import { usePostStore } from "@/store/posts"
|
||||||
|
|
||||||
export function listPosts() {
|
export function listPosts() {
|
||||||
return mockPosts
|
return usePostStore.getState().posts
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPostById(postId: string) {
|
export function getPostById(postId: string) {
|
||||||
return mockPosts.find((post) => post.id === postId)
|
return usePostStore.getState().posts.find((post) => post.id === postId)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function listPostsByAuthor(userId: string) {
|
export function listPostsByAuthor(userId: string) {
|
||||||
return mockPosts.filter((post) => post.author.id === userId)
|
return usePostStore.getState().posts.filter((post) => post.author.id === userId)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import { create } from "zustand"
|
||||||
|
import { generateId } from "@/lib/id"
|
||||||
|
import { mockPosts } from "@/lib/mock"
|
||||||
|
import type { Post, User, UserRole } from "@/lib/types"
|
||||||
|
|
||||||
|
interface CreatePostInput {
|
||||||
|
author: User
|
||||||
|
authorRole: UserRole
|
||||||
|
title: string
|
||||||
|
content: string
|
||||||
|
images: string[]
|
||||||
|
tags: string[]
|
||||||
|
linkedOrderId?: string
|
||||||
|
quotedPostId?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PostState {
|
||||||
|
posts: Post[]
|
||||||
|
createPost: (input: CreatePostInput) => Post
|
||||||
|
}
|
||||||
|
|
||||||
|
export const usePostStore = create<PostState>((set) => ({
|
||||||
|
posts: mockPosts,
|
||||||
|
createPost: (input) => {
|
||||||
|
const post: Post = {
|
||||||
|
id: generateId("post"),
|
||||||
|
author: input.author,
|
||||||
|
authorRole: input.authorRole,
|
||||||
|
title: input.title.trim(),
|
||||||
|
content: input.content.trim(),
|
||||||
|
images: input.images,
|
||||||
|
tags: input.tags,
|
||||||
|
linkedOrderId: input.linkedOrderId,
|
||||||
|
quotedPostId: input.quotedPostId,
|
||||||
|
likeCount: 0,
|
||||||
|
commentCount: 0,
|
||||||
|
liked: false,
|
||||||
|
pinned: false,
|
||||||
|
createdAt: new Date().toISOString(),
|
||||||
|
}
|
||||||
|
|
||||||
|
set((state) => ({
|
||||||
|
posts: [post, ...state.posts],
|
||||||
|
}))
|
||||||
|
|
||||||
|
return post
|
||||||
|
},
|
||||||
|
}))
|
||||||
Reference in New Issue
Block a user