Files
d3m0k1d.ru/frontend/src/pages/BlogPost.tsx
2026-02-15 16:34:37 +03:00

282 lines
7.0 KiB
TypeScript

// frontend/src/pages/BlogPost.tsx
import { useState, useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import rehypeRaw from "rehype-raw";
interface Post {
id: number;
title: string;
content: string;
created_at: string;
}
function BlogPost() {
const { id } = useParams<{ id: string }>();
const [post, setPost] = useState<Post | null>(null);
const [isLoading, setIsLoading] = useState(true);
const navigate = useNavigate();
useEffect(() => {
if (id) {
fetchPost(id);
}
}, [id]);
const fetchPost = async (postId: string) => {
try {
const response = await fetch(`/api/v1/posts/${postId}`);
if (response.ok) {
const data = await response.json();
setPost(data.data || data);
} else {
navigate("/blog");
}
} catch (error) {
console.error("Failed to fetch post:", error);
navigate("/blog");
} finally {
setIsLoading(false);
}
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
});
};
if (isLoading) {
return (
<div className="min-h-screen bg-black flex items-center justify-center">
<div className="text-[hsl(270,73%,63%)] text-xl font-['Commit_Mono',monospace]">
Loading...
</div>
</div>
);
}
if (!post) {
return (
<div className="min-h-screen bg-black flex items-center justify-center">
<div className="text-gray-500 text-xl font-['Commit_Mono',monospace]">
Post not found
</div>
</div>
);
}
return (
<div className="min-h-screen bg-black text-white font-['Commit_Mono',monospace]">
<div className="max-w-[900px] mx-auto px-4 sm:px-6 py-12 sm:py-16">
{/* Back Button */}
<button
onClick={() => navigate("/blog")}
className="flex items-center gap-2 text-gray-400 hover:text-[hsl(270,73%,63%)] transition-colors mb-8 text-sm sm:text-base"
>
<svg
className="w-4 h-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
Back to blog page
</button>
{/* Article Header */}
<article>
<header className="mb-8 sm:mb-12">
<h1 className="text-3xl sm:text-4xl md:text-5xl font-bold text-[hsl(270,73%,63%)] mb-4 leading-tight">
{post.title}
</h1>
<div className="flex items-center gap-4 text-sm text-gray-500">
<time dateTime={post.created_at}>
{formatDate(post.created_at)}
</time>
<span></span>
<span>{Math.ceil(post.content.length / 1000)} min reading</span>
</div>
</header>
{/* Article Content */}
<div className="prose prose-invert prose-lg max-w-none blog-content">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeRaw]}
>
{post.content}
</ReactMarkdown>
</div>
</article>
</div>
<style>{`
.blog-content {
color: #ffffff;
line-height: 1.8;
}
/* Заголовки - фиолетовые */
.blog-content h1,
.blog-content h2,
.blog-content h3,
.blog-content h4,
.blog-content h5,
.blog-content h6 {
color: hsl(270, 73%, 63%);
font-weight: 700;
margin-top: 2em;
margin-bottom: 0.75em;
line-height: 1.3;
}
.blog-content h1 {
font-size: 2.25rem;
}
.blog-content h2 {
font-size: 1.875rem;
}
.blog-content h3 {
font-size: 1.5rem;
}
.blog-content h4 {
font-size: 1.25rem;
}
/* Параграфы - белые */
.blog-content p {
color: #ffffff;
margin-bottom: 1.5em;
}
/* Ссылки */
.blog-content a {
color: hsl(270, 73%, 63%);
text-decoration: underline;
transition: color 0.2s;
}
.blog-content a:hover {
color: hsl(270, 73%, 70%);
}
/* Списки */
.blog-content ul,
.blog-content ol {
color: #ffffff;
margin: 1.5em 0;
padding-left: 1.5em;
}
.blog-content li {
margin-bottom: 0.5em;
}
/* Код */
.blog-content code {
background: #1a1a1a;
color: hsl(270, 73%, 63%);
padding: 0.2em 0.4em;
border-radius: 4px;
font-size: 0.9em;
font-family: 'Commit Mono', monospace;
}
.blog-content pre {
background: #1a1a1a;
border: 1px solid #333;
border-radius: 8px;
padding: 1.5em;
overflow-x: auto;
margin: 1.5em 0;
}
.blog-content pre code {
background: transparent;
padding: 0;
color: #ffffff;
}
/* Цитаты */
.blog-content blockquote {
border-left: 4px solid hsl(270, 73%, 63%);
padding-left: 1.5em;
margin: 1.5em 0;
color: #cccccc;
font-style: italic;
}
/* Картинки */
.blog-content img {
max-width: 100%;
height: auto;
border-radius: 8px;
margin: 2em 0;
border: 1px solid #333;
}
/* Таблицы */
.blog-content table {
width: 100%;
border-collapse: collapse;
margin: 1.5em 0;
}
.blog-content th,
.blog-content td {
border: 1px solid #333;
padding: 0.75em;
text-align: left;
}
.blog-content th {
background: #1a1a1a;
color: hsl(270, 73%, 63%);
font-weight: 600;
}
.blog-content td {
color: #ffffff;
}
/* Горизонтальная линия */
.blog-content hr {
border: none;
border-top: 1px solid #333;
margin: 2em 0;
}
/* Strong/Bold */
.blog-content strong {
color: hsl(270, 73%, 63%);
font-weight: 700;
}
/* Responsive */
@media (max-width: 640px) {
.blog-content h1 { font-size: 1.875rem; }
.blog-content h2 { font-size: 1.5rem; }
.blog-content h3 { font-size: 1.25rem; }
.blog-content h4 { font-size: 1.125rem; }
}
`}</style>
</div>
);
}
export default BlogPost;