feat(ui): refine post interaction surfaces
This commit is contained in:
@@ -126,9 +126,9 @@ export default function NewPostPage() {
|
|||||||
返回社区
|
返回社区
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Card className="hover:shadow-card-hover">
|
<Card className="border-border/80 shadow-sm">
|
||||||
<CardHeader>
|
<CardHeader className="border-b border-border/60">
|
||||||
<CardTitle className="tracking-tighter leading-tight">发布帖子</CardTitle>
|
<CardTitle className="tracking-tight leading-tight">发布帖子</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
|
||||||
@@ -186,17 +186,18 @@ export default function NewPostPage() {
|
|||||||
{imageFiles.map((file) => (
|
{imageFiles.map((file) => (
|
||||||
<div
|
<div
|
||||||
key={`${file.name}-${file.lastModified}-${file.size}`}
|
key={`${file.name}-${file.lastModified}-${file.size}`}
|
||||||
className="h-20 w-20 rounded-lg bg-muted flex items-center justify-center relative"
|
className="relative flex h-20 w-20 items-center justify-center rounded-lg border border-border/60 bg-muted/30"
|
||||||
>
|
>
|
||||||
<span className="max-w-16 truncate text-xs text-muted-foreground">
|
<span className="max-w-16 truncate text-xs text-muted-foreground">
|
||||||
{file.name}
|
{file.name}
|
||||||
</span>
|
</span>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
aria-label={`移除图片 ${file.name}`}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setImageFiles((files) => files.filter((item) => item !== file))
|
setImageFiles((files) => files.filter((item) => item !== file))
|
||||||
}
|
}
|
||||||
className="absolute -top-1 -right-1 h-4 w-4 rounded-full bg-destructive text-destructive-foreground flex items-center justify-center"
|
className="absolute -right-1 -top-1 flex h-5 w-5 items-center justify-center rounded-full bg-destructive text-destructive-foreground shadow-sm"
|
||||||
>
|
>
|
||||||
<X className="h-3 w-3" />
|
<X className="h-3 w-3" />
|
||||||
</button>
|
</button>
|
||||||
@@ -205,7 +206,7 @@ export default function NewPostPage() {
|
|||||||
{imageFiles.length < 9 && (
|
{imageFiles.length < 9 && (
|
||||||
<Label
|
<Label
|
||||||
htmlFor="post-images"
|
htmlFor="post-images"
|
||||||
className="h-20 w-20 cursor-pointer rounded-lg border-2 border-dashed border-muted-foreground/25 flex flex-col items-center justify-center gap-1 text-muted-foreground hover:border-muted-foreground/50 transition-colors"
|
className="flex h-20 w-20 cursor-pointer flex-col items-center justify-center gap-1 rounded-lg border-2 border-dashed border-border/60 bg-muted/20 text-muted-foreground transition-colors hover:border-primary/60 hover:bg-muted/30 hover:text-foreground"
|
||||||
>
|
>
|
||||||
<ImagePlus className="h-5 w-5" />
|
<ImagePlus className="h-5 w-5" />
|
||||||
<span className="text-[10px]">添加</span>
|
<span className="text-[10px]">添加</span>
|
||||||
@@ -229,7 +230,7 @@ export default function NewPostPage() {
|
|||||||
<Badge
|
<Badge
|
||||||
key={tag}
|
key={tag}
|
||||||
variant={selectedTags.includes(tag) ? "default" : "outline"}
|
variant={selectedTags.includes(tag) ? "default" : "outline"}
|
||||||
className="cursor-pointer"
|
className="cursor-pointer border-border/60"
|
||||||
onClick={() => toggleTag(tag)}
|
onClick={() => toggleTag(tag)}
|
||||||
>
|
>
|
||||||
{tag}
|
{tag}
|
||||||
@@ -246,7 +247,7 @@ export default function NewPostPage() {
|
|||||||
>
|
>
|
||||||
{isSubmitting ? "发布中..." : "发布"}
|
{isSubmitting ? "发布中..." : "发布"}
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" variant="outline" asChild>
|
<Button type="button" variant="outline" className="border-border/60" asChild>
|
||||||
<Link href="/community">取消</Link>
|
<Link href="/community">取消</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import { toApiError } from "@/lib/errors"
|
|||||||
import { notifyInfo } from "@/lib/toast"
|
import { notifyInfo } from "@/lib/toast"
|
||||||
import type { Comment } from "@/lib/types"
|
import type { Comment } from "@/lib/types"
|
||||||
import { useRequireAuth } from "@/lib/use-require-auth"
|
import { useRequireAuth } from "@/lib/use-require-auth"
|
||||||
import { Heart } from "lucide-react"
|
import { AlertCircle, Heart, MessageCircle } from "lucide-react"
|
||||||
import { useCallback, useEffect, useState } from "react"
|
import { useCallback, useEffect, useState } from "react"
|
||||||
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"
|
import { Avatar, AvatarFallback, AvatarImage } from "./ui/avatar"
|
||||||
import { Button } from "./ui/button"
|
import { Button } from "./ui/button"
|
||||||
|
import { EmptyState } from "./ui/empty-state"
|
||||||
import { Textarea } from "./ui/textarea"
|
import { Textarea } from "./ui/textarea"
|
||||||
|
|
||||||
interface PostCommentsProps {
|
interface PostCommentsProps {
|
||||||
@@ -65,12 +66,15 @@ export function PostComments({ postId }: PostCommentsProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h2 className="font-semibold">
|
<div className="flex items-center justify-between border-b border-border/60 pb-3">
|
||||||
评论 ({loading && comments.length === 0 ? "..." : comments.length})
|
<h2 className="font-semibold">评论</h2>
|
||||||
</h2>
|
<span className="text-xs text-muted-foreground">
|
||||||
|
{loading && comments.length === 0 ? "加载中" : `${comments.length} 条`}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<form
|
<form
|
||||||
className="flex gap-3"
|
className="space-y-3 rounded-xl border border-border/80 bg-card p-3 shadow-sm"
|
||||||
onSubmit={(event) => {
|
onSubmit={(event) => {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
requireAuth(async () => {
|
requireAuth(async () => {
|
||||||
@@ -95,42 +99,58 @@ export function PostComments({ postId }: PostCommentsProps) {
|
|||||||
>
|
>
|
||||||
<Textarea
|
<Textarea
|
||||||
placeholder="写下你的评论..."
|
placeholder="写下你的评论..."
|
||||||
className="flex-1"
|
className="min-h-20 resize-none bg-transparent"
|
||||||
rows={2}
|
rows={2}
|
||||||
value={content}
|
value={content}
|
||||||
onChange={(event) => setContent(event.target.value)}
|
onChange={(event) => setContent(event.target.value)}
|
||||||
/>
|
/>
|
||||||
<Button className="self-end" disabled={!content.trim() || submitting}>
|
<div className="flex justify-end">
|
||||||
|
<Button size="sm" disabled={!content.trim() || submitting}>
|
||||||
发送
|
发送
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
{loading && comments.length === 0 ? (
|
{loading && comments.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground text-center py-8">加载中...</p>
|
<EmptyState title="评论加载中" icon={MessageCircle} className="min-h-[180px]" />
|
||||||
) : loadError ? (
|
) : loadError ? (
|
||||||
<p className="text-sm text-muted-foreground text-center py-8">加载失败:{loadError}</p>
|
<EmptyState
|
||||||
|
title="评论加载失败"
|
||||||
|
description={loadError}
|
||||||
|
icon={AlertCircle}
|
||||||
|
className="min-h-[180px]"
|
||||||
|
/>
|
||||||
) : comments.length === 0 ? (
|
) : comments.length === 0 ? (
|
||||||
<p className="text-sm text-muted-foreground text-center py-8">还没有评论</p>
|
<EmptyState
|
||||||
|
title="还没有评论"
|
||||||
|
description="可以写下第一条评论。"
|
||||||
|
icon={MessageCircle}
|
||||||
|
className="min-h-[180px] border-dashed"
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4">
|
<div className="overflow-hidden rounded-xl border border-border/80 bg-card shadow-sm">
|
||||||
{comments.map((comment) => (
|
{comments.map((comment) => (
|
||||||
<div key={comment.id} className="flex gap-3">
|
<div
|
||||||
|
key={comment.id}
|
||||||
|
className="flex gap-3 border-b border-border/60 p-4 last:border-0"
|
||||||
|
>
|
||||||
<Avatar className="h-8 w-8">
|
<Avatar className="h-8 w-8">
|
||||||
<AvatarImage src={comment.author.avatar} />
|
<AvatarImage src={comment.author.avatar} />
|
||||||
<AvatarFallback>{comment.author.nickname[0]}</AvatarFallback>
|
<AvatarFallback>{comment.author.nickname[0]}</AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
<div className="flex-1 min-w-0">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center gap-2 mb-0.5">
|
<div className="mb-0.5 flex items-center gap-2">
|
||||||
<span className="text-sm font-medium">{comment.author.nickname}</span>
|
<span className="text-sm font-medium">{comment.author.nickname}</span>
|
||||||
<span className="text-xs text-muted-foreground">
|
<span className="text-xs text-muted-foreground">
|
||||||
{new Date(comment.createdAt).toLocaleString("zh-CN")}
|
{new Date(comment.createdAt).toLocaleString("zh-CN")}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-sm">{comment.content}</p>
|
<p className="text-sm leading-relaxed">{comment.content}</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
aria-label={comment.liked ? "取消点赞评论" : "点赞评论"}
|
||||||
disabled={pendingLike[comment.id]}
|
disabled={pendingLike[comment.id]}
|
||||||
className="flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground mt-1 transition-colors"
|
className="mt-2 flex items-center gap-1 text-xs text-muted-foreground transition-colors hover:text-foreground disabled:pointer-events-none disabled:opacity-60"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
requireAuth(() => {
|
requireAuth(() => {
|
||||||
if (pendingLike[comment.id]) return
|
if (pendingLike[comment.id]) return
|
||||||
@@ -177,7 +197,7 @@ export function PostComments({ postId }: PostCommentsProps) {
|
|||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Heart
|
<Heart
|
||||||
className={`h-3 w-3 ${comment.liked ? "fill-red-500 text-red-500" : ""}`}
|
className={`h-3 w-3 ${comment.liked ? "fill-destructive text-destructive" : ""}`}
|
||||||
/>
|
/>
|
||||||
{comment.likeCount}
|
{comment.likeCount}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ export function PostLikeButton({ postId, initialLiked, initialLikeCount }: PostL
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
aria-label={liked ? "取消点赞帖子" : "点赞帖子"}
|
||||||
disabled={pending}
|
disabled={pending}
|
||||||
className="flex items-center gap-1 hover:text-foreground transition-colors disabled:opacity-60 disabled:pointer-events-none"
|
className="flex items-center gap-1 text-muted-foreground transition-colors hover:text-foreground disabled:pointer-events-none disabled:opacity-60"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
requireAuth(() => {
|
requireAuth(() => {
|
||||||
if (pending) return
|
if (pending) return
|
||||||
@@ -52,7 +53,7 @@ export function PostLikeButton({ postId, initialLiked, initialLikeCount }: PostL
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Heart className={`h-4 w-4 ${liked ? "fill-red-500 text-red-500" : ""}`} />
|
<Heart className={`h-4 w-4 ${liked ? "fill-destructive text-destructive" : ""}`} />
|
||||||
{likeCount}
|
{likeCount}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user