fix: 前端设计规范对齐 + UX 改进 #1

Open
openclaw wants to merge 1 commits from openclaw/Rs_blog_front:fix/design-review-improvements into main
13 changed files with 57 additions and 39 deletions
Showing only changes of commit 61e86bf215 - Show all commits

View File

@@ -2,6 +2,18 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
images: {
remotePatterns: [
{
protocol: "https",
hostname: "**",
},
{
protocol: "http",
hostname: "**",
},
],
},
};
export default nextConfig;

View File

@@ -8,6 +8,7 @@ import { Skeleton } from "@/components/ui/Skeleton";
import { MarkdownEditor } from "@/components/editor/MarkdownEditor";
import { ImageUpload } from "@/components/editor/ImageUpload";
import { PostContent } from "@/components/post/PostContent";
import { SaveStatus } from "@/components/SaveStatus";
import { api } from "@/lib/api";
interface EditPostPageProps {
@@ -22,7 +23,7 @@ export default function EditPostEditor({ params }: EditPostPageProps) {
const [published, setPublished] = useState(false);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved" | "error">("idle");
useEffect(() => {
async function fetchPost() {
@@ -45,15 +46,16 @@ export default function EditPostEditor({ params }: EditPostPageProps) {
if (!title.trim()) return;
setSaving(true);
setSaveStatus("saving");
try {
await api.updatePost(id, {
title,
content,
published,
});
setSaved(true);
setSaveStatus("saved");
} catch {
alert("Failed to save post");
setSaveStatus("error");
} finally {
setSaving(false);
}
@@ -65,11 +67,11 @@ export default function EditPostEditor({ params }: EditPostPageProps) {
};
useEffect(() => {
if (saved) {
const timer = setTimeout(() => setSaved(false), 3000);
if (saveStatus === "saved" || saveStatus === "error") {
const timer = setTimeout(() => setSaveStatus("idle"), 3000);
return () => clearTimeout(timer);
}
}, [saved]);
}, [saveStatus]);
if (loading) {
return (
@@ -92,6 +94,8 @@ export default function EditPostEditor({ params }: EditPostPageProps) {
/>
<div className="flex items-center gap-4 ml-6">
<SaveStatus status={saveStatus} />
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
@@ -103,7 +107,7 @@ export default function EditPostEditor({ params }: EditPostPageProps) {
</label>
<Button onClick={handleSave} disabled={saving || !title.trim()}>
{saving ? "Saving..." : saved ? "Saved." : "Save"}
{saving ? "Saving..." : "Save"}
</Button>
</div>
</div>
@@ -112,7 +116,7 @@ export default function EditPostEditor({ params }: EditPostPageProps) {
<ImageUpload onUpload={handleImageUpload} />
</div>
<div className="grid grid-cols-2 gap-8 h-[calc(100vh-280px)]">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 h-[calc(100vh-280px)]">
<div className="border border-neutral-200 p-6 overflow-auto">
<MarkdownEditor value={content} onChange={setContent} />
</div>

View File

@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/Button";
import { MarkdownEditor } from "@/components/editor/MarkdownEditor";
import { ImageUpload } from "@/components/editor/ImageUpload";
import { PostContent } from "@/components/post/PostContent";
import { SaveStatus } from "@/components/SaveStatus";
import { api } from "@/lib/api";
export default function NewPostEditor() {
@@ -15,24 +16,25 @@ export default function NewPostEditor() {
const [content, setContent] = useState("");
const [published, setPublished] = useState(false);
const [saving, setSaving] = useState(false);
const [saved, setSaved] = useState(false);
const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved" | "error">("idle");
const handleSave = useCallback(async () => {
if (!title.trim()) return;
setSaving(true);
setSaveStatus("saving");
try {
const post = await api.createPost({
title,
content,
published,
});
setSaved(true);
setSaveStatus("saved");
setTimeout(() => {
router.push(`/admin/editor/${post.id}`);
}, 1000);
} catch {
alert("Failed to save post");
setSaveStatus("error");
} finally {
setSaving(false);
}
@@ -44,11 +46,11 @@ export default function NewPostEditor() {
};
useEffect(() => {
if (saved) {
const timer = setTimeout(() => setSaved(false), 3000);
if (saveStatus === "saved" || saveStatus === "error") {
const timer = setTimeout(() => setSaveStatus("idle"), 3000);
return () => clearTimeout(timer);
}
}, [saved]);
}, [saveStatus]);
return (
<Container className="py-12">
@@ -62,6 +64,8 @@ export default function NewPostEditor() {
/>
<div className="flex items-center gap-4 ml-6">
<SaveStatus status={saveStatus} />
<label className="flex items-center gap-2 cursor-pointer">
<input
type="checkbox"
@@ -73,7 +77,7 @@ export default function NewPostEditor() {
</label>
<Button onClick={handleSave} disabled={saving || !title.trim()}>
{saving ? "Saving..." : saved ? "Saved." : "Save"}
{saving ? "Saving..." : "Save"}
</Button>
</div>
</div>
@@ -82,7 +86,7 @@ export default function NewPostEditor() {
<ImageUpload onUpload={handleImageUpload} />
</div>
<div className="grid grid-cols-2 gap-8 h-[calc(100vh-280px)]">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 h-[calc(100vh-280px)]">
<div className="border border-neutral-200 p-6 overflow-auto">
<MarkdownEditor value={content} onChange={setContent} />
</div>

View File

@@ -95,7 +95,6 @@ function DraftsContent() {
size="sm"
onClick={() => handlePublish(post.id)}
disabled={publishing === post.id}
className="border-green-600 text-green-600 hover:bg-green-600 hover:text-white"
>
{publishing === post.id ? "Publishing..." : "Publish"}
</Button>

View File

@@ -31,7 +31,7 @@ export default function RootLayout({
return (
<html lang="en">
<body
className={`${inter.variable} ${jetbrainsMono.variable} font-sans font-medium antialiased bg-white text-black min-h-screen flex flex-col`}
className={`${inter.variable} ${jetbrainsMono.variable} font-sans font-normal antialiased bg-white text-black min-h-screen flex flex-col`}
>
<AuthProvider>
<Header />

View File

@@ -43,7 +43,7 @@ export default function HomePage() {
</h1>
{error && (
<p className="text-neutral-400 text-lg">{error}</p>
<p className="text-red-600 text-lg">{error}</p>
)}
{loading && (

View File

@@ -17,7 +17,7 @@ interface EditPostPageProps {
params: Promise<{ id: string }>;
}
const AUTOSAVE_DELAY = 300;
const AUTOSAVE_DELAY = 2000;
function EditPostContent({ id }: { id: string }) {
const router = useRouter();
@@ -173,7 +173,7 @@ function EditPostContent({ id }: { id: string }) {
<ImageUpload onUpload={handleImageUpload} />
</div>
<div className="grid grid-cols-2 gap-8 h-[calc(100vh-280px)]">
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 h-[calc(100vh-280px)]">
<div className="border border-neutral-200 p-6 overflow-auto">
<MarkdownEditor value={content} onChange={handleContentChange} />
</div>

View File

@@ -1,7 +1,7 @@
"use client";
import { useState, useEffect, use } from "react";
import { useRouter } from "next/navigation";
import { useRouter, notFound } from "next/navigation";
import Link from "next/link";
import Image from "next/image";
import { Container } from "@/components/layout/Container";
@@ -49,8 +49,7 @@ export default function PostPage({ params }: PostPageProps) {
};
if (error) {
router.push("/not-found");
return null;
notFound();
}
return (

View File

@@ -93,14 +93,14 @@ export default function RegisterPage() {
? "border-red-500 animate-shake"
: password.length > 0
? passwordValid
? "border-green-500"
: "border-yellow-500"
? "border-black"
: "border-neutral-400"
: "border-neutral-200 focus:border-black"
}`}
/>
<span
className={`absolute right-0 top-1/2 -translate-y-1/2 text-xs ${
passwordValid ? "text-green-500" : "text-neutral-400"
passwordValid ? "text-black" : "text-neutral-400"
}`}
>
{password.length}/{MIN_PASSWORD_LENGTH}

View File

@@ -87,10 +87,10 @@ export function ImageUpload({ onUpload }: ImageUploadProps) {
onDragLeave={handleDragLeave}
onDrop={handleDrop}
className={clsx(
"border-2 border-dashed p-8 text-center cursor-pointer transition-colors",
"border p-8 text-center cursor-pointer transition-colors",
isDragging && "border-black bg-neutral-50",
error && "border-red-500 animate-shake",
!isDragging && !error && "border-neutral-300 hover:border-neutral-400"
!isDragging && !error && "border-neutral-200 hover:border-neutral-400"
)}
>
<input

View File

@@ -10,9 +10,9 @@ interface PostCardProps {
export function PostCard({ post, isOwner = false }: PostCardProps) {
return (
<article className="grid grid-cols-12 gap-6 md:gap-12 mb-24">
<article className="group grid grid-cols-12 gap-6 md:gap-12 mb-24 cursor-pointer">
{post.cover_image && (
<div className="col-span-12 md:col-span-5">
<div className="col-span-12 md:col-span-5 relative">
<Link href={`/posts/${post.id}`}>
<div className="aspect-[3/2] overflow-hidden bg-neutral-100">
<Image
@@ -20,7 +20,7 @@ export function PostCard({ post, isOwner = false }: PostCardProps) {
alt={post.title}
width={600}
height={400}
className="w-full h-full object-cover hover:scale-105 transition-transform duration-500"
className="w-full h-full object-cover transition-transform duration-700 ease-out group-hover:scale-105 group-hover:opacity-90"
/>
</div>
</Link>
@@ -37,7 +37,7 @@ export function PostCard({ post, isOwner = false }: PostCardProps) {
<PostMeta date={post.created_at} />
<Link href={`/posts/${post.id}`}>
<h2 className="text-3xl md:text-5xl font-bold tracking-tighter mt-4 hover:opacity-70 transition-opacity leading-tight">
<h2 className="text-3xl md:text-5xl font-bold tracking-tighter mt-4 leading-[1.1] group-hover:underline decoration-1 underline-offset-8">
{post.title}
{isOwner && !post.published && (
<span className="text-neutral-400 text-sm ml-2 font-normal">(Draft)</span>
@@ -45,16 +45,16 @@ export function PostCard({ post, isOwner = false }: PostCardProps) {
</h2>
</Link>
<p className="text-neutral-600 mt-6 leading-relaxed line-clamp-3">
<p className="text-lg text-neutral-600 mt-6 leading-relaxed line-clamp-3">
{post.content.slice(0, 200).replace(/[#*`]/g, "")}
{post.content.length > 200 ? "..." : ""}
</p>
<Link
href={`/posts/${post.id}`}
className="mt-6 text-sm font-bold tracking-widest uppercase border-b border-black pb-0.5 hover:border-transparent transition-colors self-start"
className="mt-6 flex items-center text-sm font-bold tracking-widest uppercase group-hover:translate-x-2 transition-transform duration-300 self-start"
>
Read More
Read More <span className="ml-2"></span>
</Link>
</div>
</article>

View File

@@ -10,7 +10,7 @@ interface PostContentProps {
export function PostContent({ content }: PostContentProps) {
return (
<div className="prose prose-lg max-w-none prose-headings:tracking-tight prose-headings:font-bold prose-p:leading-relaxed prose-p:text-neutral-700 prose-a:border-b prose-a:border-black prose-a:no-underline hover:prose-a:border-transparent prose-img:my-8 prose-pre:bg-[#f6f8fa] prose-pre:p-6 prose-code:font-mono prose-code:text-base">
<div className="prose prose-lg max-w-none prose-headings:tracking-tight prose-headings:font-bold prose-p:leading-relaxed prose-p:text-neutral-700 prose-a:border-b prose-a:border-black prose-a:no-underline hover:prose-a:border-transparent prose-img:my-8 prose-pre:bg-[#fafafa] prose-pre:p-6 prose-code:font-mono prose-code:text-base">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[[rehypeHighlight, { detect: true }]]}
@@ -24,7 +24,7 @@ export function PostContent({ content }: PostContentProps) {
/>
),
pre: ({ children }) => (
<pre className="bg-[#f6f8fa] p-6 font-mono text-base text-black rounded-none border border-neutral-200 whitespace-pre-wrap break-words overflow-hidden">
<pre className="bg-[#fafafa] p-6 font-mono text-base text-black rounded-none border border-neutral-200 whitespace-pre-wrap break-words overflow-hidden">
{children}
</pre>
),

View File

@@ -14,7 +14,7 @@ export function PostMeta({ date }: PostMetaProps) {
};
return (
<time className="text-xs font-bold tracking-widest uppercase text-neutral-400">
<time className="text-xs font-bold tracking-widest uppercase text-neutral-500">
{formatDate(date)}
</time>
);