feat: implement blog post and tag fetching with pagination
All checks were successful
CI - Build and Push / Build and Push Docker Image (push) Successful in 22s

This commit is contained in:
2025-10-26 16:12:27 +08:00
parent 38ca76e2fb
commit 1b9ee67545
2 changed files with 473 additions and 437 deletions

View File

@@ -87,6 +87,79 @@ export async function uploadImage(
return { fileKey, url }; return { fileKey, url };
} }
/**
* Get a list of blog posts with pagination
*/
export async function listBlogPosts(
page: number,
pageSize: number,
tagIds: string[] = []
): Promise<{
posts: Array<{
postId: string;
title: string;
coverImageUrl: string;
createdAt: number;
updatedAt: number;
}>;
totalCount: number;
}> {
const response = await apiRequest('/api/blog/view/posts', {
method: 'POST',
body: JSON.stringify({ page, pageSize, tagIds }),
});
if (!response.ok) {
throw new Error(`Failed to list posts: ${response.statusText}`);
}
return response.json();
}
/**
* Get a single blog post by ID
*/
export async function getBlogPost(postId: string): Promise<{
postId: string;
title: string;
content: string;
coverImageUrl: string;
createdAt: number;
updatedAt: number;
}> {
const response = await apiRequest('/api/blog/view/post', {
method: 'POST',
body: JSON.stringify({ postId }),
});
if (!response.ok) {
throw new Error(`Failed to get post: ${response.statusText}`);
}
return response.json();
}
/**
* Get a list of blog tags
*/
export async function listBlogTags(): Promise<{
tags: Array<{
tagId: string;
tagName: string;
postCount: number;
}>;
}> {
const response = await apiRequest('/api/blog/view/tags', {
method: 'POST',
});
if (!response.ok) {
throw new Error(`Failed to list tags: ${response.statusText}`);
}
return response.json();
}
/** /**
* Create a new blog post * Create a new blog post
*/ */

View File

@@ -1,107 +1,115 @@
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom';
import Layout from '../components/Layout' import { useState, useEffect } from 'react';
import '../App.css' import Layout from '../components/Layout';
import { listBlogPosts, listBlogTags } from '../blog/api';
import { getS3Url } from '../blog/s3Config';
import { Toast } from '../components/Toast';
import { useToast } from '../hooks/useToast';
import '../App.css';
interface BlogPost { interface BlogPost {
id: number postId: string;
title: string title: string;
excerpt: string coverImageUrl: string;
content: string createdAt: number;
author: string updatedAt: number;
date: string }
readTime: string
category: string interface BlogTag {
tags: string[] tagId: string;
image?: string tagName: string;
postCount: number;
} }
function Blog() { function Blog() {
const { t } = useTranslation() const { t } = useTranslation();
const { toasts, removeToast, error } = useToast();
const [posts, setPosts] = useState<BlogPost[]>([]);
const [tags, setTags] = useState<BlogTag[]>([]);
const [totalCount, setTotalCount] = useState(0);
const [currentPage, setCurrentPage] = useState(1);
const [selectedTags, setSelectedTags] = useState<string[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [isLoadingMore, setIsLoadingMore] = useState(false);
const pageSize = 6;
// Mock blog posts data // Fetch tags on mount
const blogPosts: BlogPost[] = [ useEffect(() => {
{ const fetchTags = async () => {
id: 1, try {
title: "Mastering Counter-Strike: Advanced Tactics and Strategies", const response = await listBlogTags();
excerpt: "Learn the advanced tactics that separate professional players from casual gamers. From positioning to communication, discover the secrets of competitive CS.", setTags(response.tags);
content: "Full article content here...", } catch (err) {
author: "ProGamer99", error('Failed to load tags');
date: "2025-10-01", console.error('Failed to fetch tags:', err);
readTime: "8 min read", }
category: "Strategy", };
tags: ["Counter-Strike", "Tactics", "Professional"], fetchTags();
image: "🎯" }, [error]);
},
{ // Fetch posts when page or tags change
id: 2, useEffect(() => {
title: "The Evolution of Esports: From LAN Parties to Global Tournaments", const fetchPosts = async () => {
excerpt: "Explore how esports has grown from small local tournaments to billion-dollar industry with global audiences and professional athletes.", try {
content: "Full article content here...", setIsLoading(true);
author: "EsportsAnalyst", const response = await listBlogPosts(currentPage, pageSize, selectedTags);
date: "2025-09-28", setPosts(response.posts);
readTime: "6 min read", setTotalCount(response.totalCount);
category: "Esports", } catch (err) {
tags: ["Esports", "History", "Tournaments"], error('Failed to load blog posts');
image: "🏆" console.error('Failed to fetch posts:', err);
}, } finally {
{ setIsLoading(false);
id: 3, }
title: "Building the Perfect Gaming Setup: Hardware Guide 2025", };
excerpt: "A comprehensive guide to building the ultimate gaming setup for Counter-Strike and other competitive games. From monitors to peripherals.", fetchPosts();
content: "Full article content here...", }, [currentPage, selectedTags, error]);
author: "TechReviewer",
date: "2025-09-25", const handleLoadMore = async () => {
readTime: "12 min read", try {
category: "Hardware", setIsLoadingMore(true);
tags: ["Gaming Setup", "Hardware", "PC Building"], const nextPage = currentPage + 1;
image: "🖥️" const response = await listBlogPosts(nextPage, pageSize, selectedTags);
}, setPosts([...posts, ...response.posts]);
{ setCurrentPage(nextPage);
id: 4, } catch (err) {
title: "Community Spotlight: Rising Stars in CS Competitive Scene", error('Failed to load more posts');
excerpt: "Meet the up-and-coming players who are making waves in the Counter-Strike competitive scene. Their stories, strategies, and paths to success.", console.error('Failed to load more posts:', err);
content: "Full article content here...", } finally {
author: "CommunityManager", setIsLoadingMore(false);
date: "2025-09-22",
readTime: "10 min read",
category: "Community",
tags: ["Players", "Community", "Rising Stars"],
image: "⭐"
},
{
id: 5,
title: "Mental Health in Competitive Gaming: Staying Sharp",
excerpt: "The importance of mental health in competitive gaming. Tips and strategies for maintaining focus, managing stress, and performing at your best.",
content: "Full article content here...",
author: "SportsPsychologist",
date: "2025-09-20",
readTime: "7 min read",
category: "Wellness",
tags: ["Mental Health", "Gaming", "Performance"],
image: "🧠"
},
{
id: 6,
title: "Server Administration: Running Your Own CS Game Server",
excerpt: "A complete guide to setting up and managing your own Counter-Strike game server. From basic setup to advanced configuration and maintenance.",
content: "Full article content here...",
author: "ServerAdmin",
date: "2025-09-18",
readTime: "15 min read",
category: "Technical",
tags: ["Server", "Administration", "Technical"],
image: "🖧"
} }
] };
const categories = [...new Set(blogPosts.map(post => post.category))] const handleTagClick = (tagId: string) => {
const featuredPost = blogPosts[0] setSelectedTags((prev) => {
const recentPosts = blogPosts.slice(1, 4) if (prev.includes(tagId)) {
return prev.filter((id) => id !== tagId);
}
return [...prev, tagId];
});
setCurrentPage(1); // Reset to first page when filtering
};
const formatDate = (timestamp: number) => {
return new Date(timestamp * 1000).toLocaleDateString('en-US', {
year: 'numeric',
month: 'short',
day: 'numeric',
});
};
const hasMorePosts = posts.length < totalCount;
const featuredPost = posts[0];
const recentPosts = posts.slice(1);
return ( return (
<Layout currentPage="blog"> <Layout currentPage="blog">
{/* Blog Page Header */} <Toast toasts={toasts} onRemove={removeToast} />
{/* Header Section */}
<section className="blog-header" style={{ <section className="blog-header" style={{
padding: '120px 2rem 60px', padding: '120px 2rem 60px',
background: 'linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%)', background: 'linear-gradient(135deg, var(--bg-primary) 0%, var(--bg-secondary) 100%)',
@@ -124,377 +132,332 @@ function Blog() {
}}> }}>
{t('blog.subtitle')} {t('blog.subtitle')}
</p> </p>
{/* Create Post Button */} <Link to="/blog/create" style={{
<Link display: 'inline-block',
to="/blog/create" background: 'var(--accent-color)',
style={{ color: 'white',
display: 'inline-block', padding: '1rem 2rem',
background: 'var(--accent-color)', borderRadius: '8px',
color: 'white', textDecoration: 'none',
padding: '1rem 2rem', fontWeight: 'bold',
borderRadius: '8px', fontSize: '1.1rem',
textDecoration: 'none', transition: 'transform 0.3s ease'
fontWeight: 'bold', }}>
fontSize: '1.1rem',
transition: 'transform 0.3s ease, box-shadow 0.3s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-2px)';
e.currentTarget.style.boxShadow = '0 4px 12px var(--accent-shadow)';
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.boxShadow = 'none';
}}
>
Create New Post Create New Post
</Link> </Link>
</div> </div>
</section> </section>
{/* Featured Post */} {/* Loading State */}
<section className="featured-post" style={{ padding: '0 2rem 60px' }}> {isLoading && (
<div style={{ maxWidth: '1200px', margin: '0 auto' }}> <section style={{ padding: '60px 2rem', textAlign: 'center' }}>
<div style={{
fontSize: '3rem',
color: 'var(--text-secondary)',
marginBottom: '1rem'
}}>
</div>
<p style={{
color: 'var(--text-secondary)',
fontSize: '1.2rem'
}}>
Loading posts...
</p>
</section>
)}
{/* Empty State */}
{!isLoading && posts.length === 0 && (
<section style={{ padding: '60px 2rem', textAlign: 'center' }}>
<div style={{ fontSize: '5rem', marginBottom: '1rem' }}>📝</div>
<h2 style={{ <h2 style={{
fontSize: '2rem', fontSize: '2rem',
color: 'var(--text-primary)', color: 'var(--text-primary)',
marginBottom: '2rem', marginBottom: '1rem'
textAlign: 'center'
}}> }}>
{t('blog.featuredPost')} No Posts Yet
</h2> </h2>
<div style={{ <p style={{
background: 'var(--bg-card)', color: 'var(--text-secondary)',
border: '1px solid var(--border-color)', fontSize: '1.2rem',
borderRadius: '12px', marginBottom: '2rem'
overflow: 'hidden',
boxShadow: '0 4px 20px var(--shadow)',
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '2rem',
alignItems: 'center'
}}> }}>
<div style={{ padding: '3rem' }}> Be the first to create a blog post!
<div style={{ </p>
display: 'inline-block', <Link to="/blog/create" style={{
background: 'var(--accent-color)', display: 'inline-block',
color: 'white', background: 'var(--accent-color)',
padding: '0.5rem 1rem', color: 'white',
borderRadius: '20px', padding: '1rem 2rem',
fontSize: '0.9rem', borderRadius: '8px',
fontWeight: 'bold', textDecoration: 'none',
marginBottom: '1rem' fontWeight: 'bold',
}}> fontSize: '1.1rem'
{featuredPost.category} }}>
</div> Create First Post
<h3 style={{ </Link>
fontSize: '2rem', </section>
color: 'var(--text-primary)', )}
marginBottom: '1rem',
lineHeight: '1.3'
}}>
{featuredPost.title}
</h3>
<p style={{
color: 'var(--text-secondary)',
fontSize: '1.1rem',
lineHeight: '1.6',
marginBottom: '1.5rem'
}}>
{featuredPost.excerpt}
</p>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '1rem',
marginBottom: '1.5rem'
}}>
<span style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>
{t('blog.by')} {featuredPost.author}
</span>
<span style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>
{featuredPost.date}
</span>
<span style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>
{featuredPost.readTime}
</span>
</div>
<Link
to={`/blog/${featuredPost.id}`}
style={{
display: 'inline-block',
background: 'var(--accent-color)',
color: 'white',
padding: '1rem 2rem',
borderRadius: '8px',
textDecoration: 'none',
fontWeight: 'bold',
fontSize: '1rem',
transition: 'transform 0.3s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-2px)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)'
}}
>
{t('blog.readMore')}
</Link>
</div>
<div style={{
background: 'var(--bg-secondary)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '8rem',
height: '300px'
}}>
{featuredPost.image}
</div>
</div>
</div>
</section>
{/* Blog Content */} {/* Featured Post */}
<section className="blog-content" style={{ padding: '0 2rem 80px' }}> {!isLoading && featuredPost && (
<div style={{ maxWidth: '1200px', margin: '0 auto', display: 'grid', gridTemplateColumns: '1fr 300px', gap: '3rem' }}> <section style={{ padding: '0 2rem 60px' }}>
<div style={{ maxWidth: '1200px', margin: '0 auto' }}>
{/* Main Content */}
<div>
<h2 style={{ <h2 style={{
fontSize: '2rem', fontSize: '2rem',
color: 'var(--text-primary)', color: 'var(--text-primary)',
marginBottom: '2rem' marginBottom: '2rem',
textAlign: 'center'
}}> }}>
{t('blog.recentPosts')} {t('blog.featuredPost')}
</h2> </h2>
<Link to={`/blog/${featuredPost.postId}`} style={{ textDecoration: 'none', display: 'block' }}>
<div style={{
background: 'var(--bg-card)',
border: '1px solid var(--border-color)',
borderRadius: '12px',
overflow: 'hidden',
boxShadow: '0 4px 20px var(--shadow)',
display: 'grid',
gridTemplateColumns: '1fr 1fr',
gap: '2rem',
alignItems: 'center'
}}>
<div style={{ padding: '3rem' }}>
<h3 style={{
fontSize: '2rem',
color: 'var(--text-primary)',
marginBottom: '1rem',
lineHeight: '1.3'
}}>
{featuredPost.title}
</h3>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '1rem',
marginTop: '1.5rem'
}}>
<span style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>
📅 {formatDate(featuredPost.createdAt)}
</span>
<span style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>
🔄 {formatDate(featuredPost.updatedAt)}
</span>
</div>
</div>
<div style={{
background: `url(${getS3Url(featuredPost.coverImageUrl)}) center/cover`,
height: '300px',
borderRadius: '0 12px 12px 0'
}} />
</div>
</Link>
</div>
</section>
)}
<div style={{ display: 'grid', gap: '2rem' }}> {/* Recent Posts & Sidebar */}
{recentPosts.map(post => ( {!isLoading && posts.length > 0 && (
<article key={post.id} style={{ <section style={{ padding: '0 2rem 80px' }}>
<div style={{
maxWidth: '1200px',
margin: '0 auto',
display: 'grid',
gridTemplateColumns: '1fr 300px',
gap: '3rem'
}}>
{/* Posts Grid */}
<div>
<h2 style={{
fontSize: '2rem',
color: 'var(--text-primary)',
marginBottom: '2rem'
}}>
{t('blog.recentPosts')}
</h2>
<div style={{ display: 'grid', gap: '2rem' }}>
{recentPosts.map(post => (
<Link key={post.postId} to={`/blog/${post.postId}`} style={{ textDecoration: 'none' }}>
<article style={{
background: 'var(--bg-card)',
border: '1px solid var(--border-color)',
borderRadius: '12px',
overflow: 'hidden'
}}>
<div style={{
display: 'grid',
gridTemplateColumns: '200px 1fr',
gap: '0'
}}>
<div style={{
background: `url(${getS3Url(post.coverImageUrl)}) center/cover`,
height: '200px'
}} />
<div style={{ padding: '2rem' }}>
<h3 style={{
fontSize: '1.5rem',
color: 'var(--text-primary)',
marginBottom: '1rem',
lineHeight: '1.3'
}}>
{post.title}
</h3>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '1rem'
}}>
<span style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>
📅 {formatDate(post.createdAt)}
</span>
<span style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>
🔄 {formatDate(post.updatedAt)}
</span>
</div>
</div>
</div>
</article>
</Link>
))}
</div>
{/* Load More Button */}
{hasMorePosts && (
<div style={{ textAlign: 'center', marginTop: '3rem' }}>
<button
onClick={handleLoadMore}
disabled={isLoadingMore}
style={{
background: isLoadingMore ? 'var(--bg-secondary)' : 'var(--accent-color)',
color: 'white',
border: 'none',
padding: '1rem 2rem',
borderRadius: '8px',
fontSize: '1rem',
fontWeight: 'bold',
cursor: isLoadingMore ? 'not-allowed' : 'pointer',
opacity: isLoadingMore ? 0.6 : 1
}}
>
{isLoadingMore ? 'Loading...' : t('blog.loadMore')}
</button>
</div>
)}
</div>
{/* Sidebar */}
<aside>
{/* Tags */}
{tags.length > 0 && (
<div style={{
background: 'var(--bg-card)', background: 'var(--bg-card)',
border: '1px solid var(--border-color)', border: '1px solid var(--border-color)',
borderRadius: '12px', borderRadius: '12px',
overflow: 'hidden', padding: '2rem',
transition: 'transform 0.3s ease, box-shadow 0.3s ease', marginBottom: '2rem'
cursor: 'pointer' }}>
}} <h3 style={{
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-5px)'
e.currentTarget.style.boxShadow = '0 10px 30px var(--accent-shadow)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)'
e.currentTarget.style.boxShadow = 'none'
}}
>
<div style={{ display: 'grid', gridTemplateColumns: '200px 1fr', gap: '0' }}>
<div style={{
background: 'var(--bg-secondary)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '4rem'
}}>
{post.image}
</div>
<div style={{ padding: '2rem' }}>
<div style={{
display: 'inline-block',
background: 'var(--accent-color)',
color: 'white',
padding: '0.3rem 0.8rem',
borderRadius: '15px',
fontSize: '0.8rem',
fontWeight: 'bold',
marginBottom: '1rem'
}}>
{post.category}
</div>
<h3 style={{
fontSize: '1.5rem',
color: 'var(--text-primary)',
marginBottom: '1rem',
lineHeight: '1.3'
}}>
{post.title}
</h3>
<p style={{
color: 'var(--text-secondary)',
fontSize: '1rem',
lineHeight: '1.6',
marginBottom: '1rem'
}}>
{post.excerpt}
</p>
<div style={{
display: 'flex',
alignItems: 'center',
gap: '1rem',
marginBottom: '1rem'
}}>
<span style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>
{t('blog.by')} {post.author}
</span>
<span style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>
{post.date}
</span>
<span style={{ color: 'var(--text-secondary)', fontSize: '0.9rem' }}>
{post.readTime}
</span>
</div>
<div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap' }}>
{post.tags.map(tag => (
<span key={tag} style={{
background: 'var(--bg-secondary)',
color: 'var(--text-secondary)',
padding: '0.2rem 0.6rem',
borderRadius: '12px',
fontSize: '0.8rem'
}}>
#{tag}
</span>
))}
</div>
</div>
</div>
</article>
))}
</div>
{/* Load More Button */}
<div style={{ textAlign: 'center', marginTop: '3rem' }}>
<button style={{
background: 'var(--accent-color)',
color: 'white',
border: 'none',
padding: '1rem 2rem',
borderRadius: '8px',
fontSize: '1rem',
fontWeight: 'bold',
cursor: 'pointer',
transition: 'transform 0.3s ease'
}}
onMouseEnter={(e) => {
e.currentTarget.style.transform = 'translateY(-2px)'
}}
onMouseLeave={(e) => {
e.currentTarget.style.transform = 'translateY(0)'
}}
>
{t('blog.loadMore')}
</button>
</div>
</div>
{/* Sidebar */}
<aside>
{/* Categories */}
<div style={{
background: 'var(--bg-card)',
border: '1px solid var(--border-color)',
borderRadius: '12px',
padding: '2rem',
marginBottom: '2rem'
}}>
<h3 style={{
color: 'var(--text-primary)',
marginBottom: '1rem',
fontSize: '1.3rem'
}}>
{t('blog.categories')}
</h3>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
{categories.map(category => (
<button key={category} style={{
background: 'var(--bg-secondary)',
color: 'var(--text-primary)', color: 'var(--text-primary)',
border: 'none', marginBottom: '1rem',
padding: '0.75rem 1rem', fontSize: '1.3rem'
borderRadius: '8px', }}>
cursor: 'pointer', {t('blog.categories')}
textAlign: 'left', </h3>
transition: 'background 0.3s ease' <div style={{
}} display: 'flex',
onMouseEnter={(e) => { flexDirection: 'column',
e.currentTarget.style.background = 'var(--accent-color)' gap: '0.5rem'
e.currentTarget.style.color = 'white' }}>
}} {tags.map(tag => (
onMouseLeave={(e) => { <button
e.currentTarget.style.background = 'var(--bg-secondary)' key={tag.tagId}
e.currentTarget.style.color = 'var(--text-primary)' onClick={() => handleTagClick(tag.tagId)}
}} style={{
> background: selectedTags.includes(tag.tagId) ? 'var(--accent-color)' : 'var(--bg-secondary)',
{category} color: selectedTags.includes(tag.tagId) ? 'white' : 'var(--text-primary)',
</button> border: 'none',
))} padding: '0.75rem 1rem',
</div> borderRadius: '8px',
</div> cursor: 'pointer',
textAlign: 'left',
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}}
>
<span>{tag.tagName}</span>
<span style={{
background: 'rgba(255, 255, 255, 0.2)',
padding: '0.2rem 0.6rem',
borderRadius: '12px',
fontSize: '0.8rem'
}}>
{tag.postCount}
</span>
</button>
))}
</div>
</div>
)}
{/* Newsletter Signup */} {/* Newsletter */}
<div style={{ <div style={{
background: 'var(--bg-card)', background: 'var(--bg-card)',
border: '1px solid var(--border-color)', border: '1px solid var(--border-color)',
borderRadius: '12px', borderRadius: '12px',
padding: '2rem', padding: '2rem',
textAlign: 'center' textAlign: 'center'
}}>
<h3 style={{
color: 'var(--text-primary)',
marginBottom: '1rem',
fontSize: '1.3rem'
}}> }}>
{t('blog.subscribe')} <h3 style={{
</h3>
<p style={{
color: 'var(--text-secondary)',
marginBottom: '1.5rem',
fontSize: '0.9rem'
}}>
{t('blog.subscribeDesc')}
</p>
<input
type="email"
placeholder={t('blog.emailPlaceholder')}
style={{
width: '100%',
padding: '0.75rem',
border: '1px solid var(--border-color)',
borderRadius: '6px',
background: 'var(--bg-secondary)',
color: 'var(--text-primary)', color: 'var(--text-primary)',
marginBottom: '1rem', marginBottom: '1rem',
fontSize: '1.3rem'
}}>
{t('blog.subscribe')}
</h3>
<p style={{
color: 'var(--text-secondary)',
marginBottom: '1.5rem',
fontSize: '0.9rem' fontSize: '0.9rem'
}} }}>
/> {t('blog.subscribeDesc')}
<button style={{ </p>
width: '100%', <input
background: 'var(--accent-color)', type="email"
color: 'white', placeholder={t('blog.emailPlaceholder')}
border: 'none', style={{
padding: '0.75rem', width: '100%',
borderRadius: '6px', padding: '0.75rem',
fontSize: '0.9rem', border: '1px solid var(--border-color)',
fontWeight: 'bold', borderRadius: '6px',
cursor: 'pointer', background: 'var(--bg-secondary)',
transition: 'transform 0.3s ease' color: 'var(--text-primary)',
}} marginBottom: '1rem',
onMouseEnter={(e) => { fontSize: '0.9rem'
e.currentTarget.style.transform = 'translateY(-2px)' }}
}} />
onMouseLeave={(e) => { <button style={{
e.currentTarget.style.transform = 'translateY(0)' width: '100%',
}} background: 'var(--accent-color)',
> color: 'white',
{t('blog.subscribeBtn')} border: 'none',
</button> padding: '0.75rem',
</div> borderRadius: '6px',
</aside> fontSize: '0.9rem',
</div> fontWeight: 'bold',
</section> cursor: 'pointer'
}}>
{t('blog.subscribeBtn')}
</button>
</div>
</aside>
</div>
</section>
)}
</Layout> </Layout>
) );
} }
export default Blog export default Blog;