282 lines
7.0 KiB
TypeScript
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;
|