feat: full redy blog and admin panel
This commit is contained in:
281
frontend/src/pages/BlogPost.tsx
Normal file
281
frontend/src/pages/BlogPost.tsx
Normal file
@@ -0,0 +1,281 @@
|
||||
// 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;
|
||||
Reference in New Issue
Block a user