forked from carrydela/Rs_blog_front
fix: align frontend with design spec + UX improvements
Design spec compliance:
- Fix body font-weight: font-medium → font-normal (spec requires 400)
- PostCard: add group hover effects, underline on title hover, arrow →, translate animation
- PostCard: add text-lg to excerpt (spec Body style)
- PostMeta: fix date color neutral-400 → neutral-500 (spec Ash color)
- Homepage: fix error color neutral-400 → red-600 (spec Error color)
- Register: replace green/yellow with black/gray (spec: no non-functional colors)
- Drafts: remove green from Publish button
- PostContent: unify pre background to #fafafa (spec Vapor color)
- ImageUpload: replace dashed border with solid thin line
Bug fixes:
- PostPage: use notFound() instead of router.push('/not-found')
- next.config.ts: add images.remotePatterns for external images
UX improvements:
- Editor: responsive grid-cols-1 md:grid-cols-2 for mobile
- Editor pages: replace alert() with SaveStatus component
- Autosave delay: 300ms → 2000ms to reduce API pressure
This commit is contained in:
@@ -2,6 +2,18 @@ import type { NextConfig } from "next";
|
|||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
output: "standalone",
|
output: "standalone",
|
||||||
|
images: {
|
||||||
|
remotePatterns: [
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "**",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: "http",
|
||||||
|
hostname: "**",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { Skeleton } from "@/components/ui/Skeleton";
|
|||||||
import { MarkdownEditor } from "@/components/editor/MarkdownEditor";
|
import { MarkdownEditor } from "@/components/editor/MarkdownEditor";
|
||||||
import { ImageUpload } from "@/components/editor/ImageUpload";
|
import { ImageUpload } from "@/components/editor/ImageUpload";
|
||||||
import { PostContent } from "@/components/post/PostContent";
|
import { PostContent } from "@/components/post/PostContent";
|
||||||
|
import { SaveStatus } from "@/components/SaveStatus";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
|
|
||||||
interface EditPostPageProps {
|
interface EditPostPageProps {
|
||||||
@@ -22,7 +23,7 @@ export default function EditPostEditor({ params }: EditPostPageProps) {
|
|||||||
const [published, setPublished] = useState(false);
|
const [published, setPublished] = useState(false);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [saved, setSaved] = useState(false);
|
const [saveStatus, setSaveStatus] = useState<"idle" | "saving" | "saved" | "error">("idle");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
async function fetchPost() {
|
async function fetchPost() {
|
||||||
@@ -45,15 +46,16 @@ export default function EditPostEditor({ params }: EditPostPageProps) {
|
|||||||
if (!title.trim()) return;
|
if (!title.trim()) return;
|
||||||
|
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
|
setSaveStatus("saving");
|
||||||
try {
|
try {
|
||||||
await api.updatePost(id, {
|
await api.updatePost(id, {
|
||||||
title,
|
title,
|
||||||
content,
|
content,
|
||||||
published,
|
published,
|
||||||
});
|
});
|
||||||
setSaved(true);
|
setSaveStatus("saved");
|
||||||
} catch {
|
} catch {
|
||||||
alert("Failed to save post");
|
setSaveStatus("error");
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -65,11 +67,11 @@ export default function EditPostEditor({ params }: EditPostPageProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (saved) {
|
if (saveStatus === "saved" || saveStatus === "error") {
|
||||||
const timer = setTimeout(() => setSaved(false), 3000);
|
const timer = setTimeout(() => setSaveStatus("idle"), 3000);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
}, [saved]);
|
}, [saveStatus]);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
@@ -92,6 +94,8 @@ export default function EditPostEditor({ params }: EditPostPageProps) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex items-center gap-4 ml-6">
|
<div className="flex items-center gap-4 ml-6">
|
||||||
|
<SaveStatus status={saveStatus} />
|
||||||
|
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -103,7 +107,7 @@ export default function EditPostEditor({ params }: EditPostPageProps) {
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<Button onClick={handleSave} disabled={saving || !title.trim()}>
|
<Button onClick={handleSave} disabled={saving || !title.trim()}>
|
||||||
{saving ? "Saving..." : saved ? "Saved." : "Save"}
|
{saving ? "Saving..." : "Save"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -112,7 +116,7 @@ export default function EditPostEditor({ params }: EditPostPageProps) {
|
|||||||
<ImageUpload onUpload={handleImageUpload} />
|
<ImageUpload onUpload={handleImageUpload} />
|
||||||
</div>
|
</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">
|
<div className="border border-neutral-200 p-6 overflow-auto">
|
||||||
<MarkdownEditor value={content} onChange={setContent} />
|
<MarkdownEditor value={content} onChange={setContent} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { Button } from "@/components/ui/Button";
|
|||||||
import { MarkdownEditor } from "@/components/editor/MarkdownEditor";
|
import { MarkdownEditor } from "@/components/editor/MarkdownEditor";
|
||||||
import { ImageUpload } from "@/components/editor/ImageUpload";
|
import { ImageUpload } from "@/components/editor/ImageUpload";
|
||||||
import { PostContent } from "@/components/post/PostContent";
|
import { PostContent } from "@/components/post/PostContent";
|
||||||
|
import { SaveStatus } from "@/components/SaveStatus";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
|
|
||||||
export default function NewPostEditor() {
|
export default function NewPostEditor() {
|
||||||
@@ -15,24 +16,25 @@ export default function NewPostEditor() {
|
|||||||
const [content, setContent] = useState("");
|
const [content, setContent] = useState("");
|
||||||
const [published, setPublished] = useState(false);
|
const [published, setPublished] = useState(false);
|
||||||
const [saving, setSaving] = 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 () => {
|
const handleSave = useCallback(async () => {
|
||||||
if (!title.trim()) return;
|
if (!title.trim()) return;
|
||||||
|
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
|
setSaveStatus("saving");
|
||||||
try {
|
try {
|
||||||
const post = await api.createPost({
|
const post = await api.createPost({
|
||||||
title,
|
title,
|
||||||
content,
|
content,
|
||||||
published,
|
published,
|
||||||
});
|
});
|
||||||
setSaved(true);
|
setSaveStatus("saved");
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
router.push(`/admin/editor/${post.id}`);
|
router.push(`/admin/editor/${post.id}`);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} catch {
|
} catch {
|
||||||
alert("Failed to save post");
|
setSaveStatus("error");
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -44,11 +46,11 @@ export default function NewPostEditor() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (saved) {
|
if (saveStatus === "saved" || saveStatus === "error") {
|
||||||
const timer = setTimeout(() => setSaved(false), 3000);
|
const timer = setTimeout(() => setSaveStatus("idle"), 3000);
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}
|
}
|
||||||
}, [saved]);
|
}, [saveStatus]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container className="py-12">
|
<Container className="py-12">
|
||||||
@@ -62,6 +64,8 @@ export default function NewPostEditor() {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="flex items-center gap-4 ml-6">
|
<div className="flex items-center gap-4 ml-6">
|
||||||
|
<SaveStatus status={saveStatus} />
|
||||||
|
|
||||||
<label className="flex items-center gap-2 cursor-pointer">
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -73,7 +77,7 @@ export default function NewPostEditor() {
|
|||||||
</label>
|
</label>
|
||||||
|
|
||||||
<Button onClick={handleSave} disabled={saving || !title.trim()}>
|
<Button onClick={handleSave} disabled={saving || !title.trim()}>
|
||||||
{saving ? "Saving..." : saved ? "Saved." : "Save"}
|
{saving ? "Saving..." : "Save"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -82,7 +86,7 @@ export default function NewPostEditor() {
|
|||||||
<ImageUpload onUpload={handleImageUpload} />
|
<ImageUpload onUpload={handleImageUpload} />
|
||||||
</div>
|
</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">
|
<div className="border border-neutral-200 p-6 overflow-auto">
|
||||||
<MarkdownEditor value={content} onChange={setContent} />
|
<MarkdownEditor value={content} onChange={setContent} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -95,7 +95,6 @@ function DraftsContent() {
|
|||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => handlePublish(post.id)}
|
onClick={() => handlePublish(post.id)}
|
||||||
disabled={publishing === 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"}
|
{publishing === post.id ? "Publishing..." : "Publish"}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export default function RootLayout({
|
|||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body
|
<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>
|
<AuthProvider>
|
||||||
<Header />
|
<Header />
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default function HomePage() {
|
|||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<p className="text-neutral-400 text-lg">{error}</p>
|
<p className="text-red-600 text-lg">{error}</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{loading && (
|
{loading && (
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ interface EditPostPageProps {
|
|||||||
params: Promise<{ id: string }>;
|
params: Promise<{ id: string }>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AUTOSAVE_DELAY = 300;
|
const AUTOSAVE_DELAY = 2000;
|
||||||
|
|
||||||
function EditPostContent({ id }: { id: string }) {
|
function EditPostContent({ id }: { id: string }) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -173,7 +173,7 @@ function EditPostContent({ id }: { id: string }) {
|
|||||||
<ImageUpload onUpload={handleImageUpload} />
|
<ImageUpload onUpload={handleImageUpload} />
|
||||||
</div>
|
</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">
|
<div className="border border-neutral-200 p-6 overflow-auto">
|
||||||
<MarkdownEditor value={content} onChange={handleContentChange} />
|
<MarkdownEditor value={content} onChange={handleContentChange} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect, use } from "react";
|
import { useState, useEffect, use } from "react";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter, notFound } from "next/navigation";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { Container } from "@/components/layout/Container";
|
import { Container } from "@/components/layout/Container";
|
||||||
@@ -49,8 +49,7 @@ export default function PostPage({ params }: PostPageProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
router.push("/not-found");
|
notFound();
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -93,14 +93,14 @@ export default function RegisterPage() {
|
|||||||
? "border-red-500 animate-shake"
|
? "border-red-500 animate-shake"
|
||||||
: password.length > 0
|
: password.length > 0
|
||||||
? passwordValid
|
? passwordValid
|
||||||
? "border-green-500"
|
? "border-black"
|
||||||
: "border-yellow-500"
|
: "border-neutral-400"
|
||||||
: "border-neutral-200 focus:border-black"
|
: "border-neutral-200 focus:border-black"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className={`absolute right-0 top-1/2 -translate-y-1/2 text-xs ${
|
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}
|
{password.length}/{MIN_PASSWORD_LENGTH}
|
||||||
|
|||||||
@@ -87,10 +87,10 @@ export function ImageUpload({ onUpload }: ImageUploadProps) {
|
|||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
className={clsx(
|
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",
|
isDragging && "border-black bg-neutral-50",
|
||||||
error && "border-red-500 animate-shake",
|
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
|
<input
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ interface PostCardProps {
|
|||||||
|
|
||||||
export function PostCard({ post, isOwner = false }: PostCardProps) {
|
export function PostCard({ post, isOwner = false }: PostCardProps) {
|
||||||
return (
|
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 && (
|
{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}`}>
|
<Link href={`/posts/${post.id}`}>
|
||||||
<div className="aspect-[3/2] overflow-hidden bg-neutral-100">
|
<div className="aspect-[3/2] overflow-hidden bg-neutral-100">
|
||||||
<Image
|
<Image
|
||||||
@@ -20,7 +20,7 @@ export function PostCard({ post, isOwner = false }: PostCardProps) {
|
|||||||
alt={post.title}
|
alt={post.title}
|
||||||
width={600}
|
width={600}
|
||||||
height={400}
|
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>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -37,7 +37,7 @@ export function PostCard({ post, isOwner = false }: PostCardProps) {
|
|||||||
<PostMeta date={post.created_at} />
|
<PostMeta date={post.created_at} />
|
||||||
|
|
||||||
<Link href={`/posts/${post.id}`}>
|
<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}
|
{post.title}
|
||||||
{isOwner && !post.published && (
|
{isOwner && !post.published && (
|
||||||
<span className="text-neutral-400 text-sm ml-2 font-normal">(Draft)</span>
|
<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>
|
</h2>
|
||||||
</Link>
|
</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.slice(0, 200).replace(/[#*`]/g, "")}
|
||||||
{post.content.length > 200 ? "..." : ""}
|
{post.content.length > 200 ? "..." : ""}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href={`/posts/${post.id}`}
|
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>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ interface PostContentProps {
|
|||||||
|
|
||||||
export function PostContent({ content }: PostContentProps) {
|
export function PostContent({ content }: PostContentProps) {
|
||||||
return (
|
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
|
<ReactMarkdown
|
||||||
remarkPlugins={[remarkGfm]}
|
remarkPlugins={[remarkGfm]}
|
||||||
rehypePlugins={[[rehypeHighlight, { detect: true }]]}
|
rehypePlugins={[[rehypeHighlight, { detect: true }]]}
|
||||||
@@ -24,7 +24,7 @@ export function PostContent({ content }: PostContentProps) {
|
|||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
pre: ({ children }) => (
|
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}
|
{children}
|
||||||
</pre>
|
</pre>
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export function PostMeta({ date }: PostMetaProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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)}
|
{formatDate(date)}
|
||||||
</time>
|
</time>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user