From 1b9ee67545c80076ad73074fcb42770201ce55bc Mon Sep 17 00:00:00 2001
From: cialloo
Date: Sun, 26 Oct 2025 16:12:27 +0800
Subject: [PATCH] feat: implement blog post and tag fetching with pagination
---
src/blog/api.ts | 73 ++++
src/pages/Blog.tsx | 837 ++++++++++++++++++++++-----------------------
2 files changed, 473 insertions(+), 437 deletions(-)
diff --git a/src/blog/api.ts b/src/blog/api.ts
index ca10bb1..21d5438 100644
--- a/src/blog/api.ts
+++ b/src/blog/api.ts
@@ -87,6 +87,79 @@ export async function uploadImage(
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
*/
diff --git a/src/pages/Blog.tsx b/src/pages/Blog.tsx
index df90d87..2dbcf88 100644
--- a/src/pages/Blog.tsx
+++ b/src/pages/Blog.tsx
@@ -1,107 +1,115 @@
-import { useTranslation } from 'react-i18next'
-import { Link } from 'react-router-dom'
-import Layout from '../components/Layout'
-import '../App.css'
+import { useTranslation } from 'react-i18next';
+import { Link } from 'react-router-dom';
+import { useState, useEffect } from 'react';
+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 {
- id: number
- title: string
- excerpt: string
- content: string
- author: string
- date: string
- readTime: string
- category: string
- tags: string[]
- image?: string
+ postId: string;
+ title: string;
+ coverImageUrl: string;
+ createdAt: number;
+ updatedAt: number;
+}
+
+interface BlogTag {
+ tagId: string;
+ tagName: string;
+ postCount: number;
}
function Blog() {
- const { t } = useTranslation()
+ const { t } = useTranslation();
+ const { toasts, removeToast, error } = useToast();
+
+ const [posts, setPosts] = useState([]);
+ const [tags, setTags] = useState([]);
+ const [totalCount, setTotalCount] = useState(0);
+ const [currentPage, setCurrentPage] = useState(1);
+ const [selectedTags, setSelectedTags] = useState([]);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isLoadingMore, setIsLoadingMore] = useState(false);
+
+ const pageSize = 6;
- // Mock blog posts data
- const blogPosts: BlogPost[] = [
- {
- id: 1,
- title: "Mastering Counter-Strike: Advanced Tactics and Strategies",
- excerpt: "Learn the advanced tactics that separate professional players from casual gamers. From positioning to communication, discover the secrets of competitive CS.",
- content: "Full article content here...",
- author: "ProGamer99",
- date: "2025-10-01",
- readTime: "8 min read",
- category: "Strategy",
- tags: ["Counter-Strike", "Tactics", "Professional"],
- image: "🎯"
- },
- {
- id: 2,
- title: "The Evolution of Esports: From LAN Parties to Global Tournaments",
- excerpt: "Explore how esports has grown from small local tournaments to billion-dollar industry with global audiences and professional athletes.",
- content: "Full article content here...",
- author: "EsportsAnalyst",
- date: "2025-09-28",
- readTime: "6 min read",
- category: "Esports",
- tags: ["Esports", "History", "Tournaments"],
- image: "🏆"
- },
- {
- 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.",
- content: "Full article content here...",
- author: "TechReviewer",
- date: "2025-09-25",
- readTime: "12 min read",
- category: "Hardware",
- tags: ["Gaming Setup", "Hardware", "PC Building"],
- image: "🖥️"
- },
- {
- id: 4,
- title: "Community Spotlight: Rising Stars in CS Competitive Scene",
- excerpt: "Meet the up-and-coming players who are making waves in the Counter-Strike competitive scene. Their stories, strategies, and paths to success.",
- content: "Full article content here...",
- author: "CommunityManager",
- 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: "🖧"
+ // Fetch tags on mount
+ useEffect(() => {
+ const fetchTags = async () => {
+ try {
+ const response = await listBlogTags();
+ setTags(response.tags);
+ } catch (err) {
+ error('Failed to load tags');
+ console.error('Failed to fetch tags:', err);
+ }
+ };
+ fetchTags();
+ }, [error]);
+
+ // Fetch posts when page or tags change
+ useEffect(() => {
+ const fetchPosts = async () => {
+ try {
+ setIsLoading(true);
+ const response = await listBlogPosts(currentPage, pageSize, selectedTags);
+ setPosts(response.posts);
+ setTotalCount(response.totalCount);
+ } catch (err) {
+ error('Failed to load blog posts');
+ console.error('Failed to fetch posts:', err);
+ } finally {
+ setIsLoading(false);
+ }
+ };
+ fetchPosts();
+ }, [currentPage, selectedTags, error]);
+
+ const handleLoadMore = async () => {
+ try {
+ setIsLoadingMore(true);
+ const nextPage = currentPage + 1;
+ const response = await listBlogPosts(nextPage, pageSize, selectedTags);
+ setPosts([...posts, ...response.posts]);
+ setCurrentPage(nextPage);
+ } catch (err) {
+ error('Failed to load more posts');
+ console.error('Failed to load more posts:', err);
+ } finally {
+ setIsLoadingMore(false);
}
- ]
+ };
- const categories = [...new Set(blogPosts.map(post => post.category))]
- const featuredPost = blogPosts[0]
- const recentPosts = blogPosts.slice(1, 4)
+ const handleTagClick = (tagId: string) => {
+ setSelectedTags((prev) => {
+ 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 (
- {/* Blog Page Header */}
+
+
+ {/* Header Section */}
- {/* Create Post Button */}
- {
- 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
- {/* Featured Post */}
-
-
+ {/* Loading State */}
+ {isLoading && (
+
+
+ ⏳
+
+
+ Loading posts...
+
+
+ )}
+
+ {/* Empty State */}
+ {!isLoading && posts.length === 0 && (
+
+ 📝
- {t('blog.featuredPost')}
+ No Posts Yet
-
-
-
- {featuredPost.category}
-
-
- {featuredPost.title}
-
-
- {featuredPost.excerpt}
-
-
-
- {t('blog.by')} {featuredPost.author}
-
-
- {featuredPost.date}
-
-
- {featuredPost.readTime}
-
-
-
{
- e.currentTarget.style.transform = 'translateY(-2px)'
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.transform = 'translateY(0)'
- }}
- >
- {t('blog.readMore')}
-
-
-
- {featuredPost.image}
-
-
-
-
+ Be the first to create a blog post!
+
+
+ ✍️ Create First Post
+
+
+ )}
- {/* Blog Content */}
-
-
-
- {/* Main Content */}
-
+ {/* Featured Post */}
+ {!isLoading && featuredPost && (
+
+
- {t('blog.recentPosts')}
+ {t('blog.featuredPost')}
+
+
+
+
+ {featuredPost.title}
+
+
+
+ 📅 {formatDate(featuredPost.createdAt)}
+
+
+ 🔄 {formatDate(featuredPost.updatedAt)}
+
+
+
+
+
+
+
+
+ )}
-
- {recentPosts.map(post => (
-
0 && (
+
+
+ {/* Posts Grid */}
+
+
+ {t('blog.recentPosts')}
+
+
+ {recentPosts.map(post => (
+
+
+
+
+
+
+ {post.title}
+
+
+
+ 📅 {formatDate(post.createdAt)}
+
+
+ 🔄 {formatDate(post.updatedAt)}
+
+
+
+
+
+
+ ))}
+
+
+ {/* Load More Button */}
+ {hasMorePosts && (
+
+
+
+ )}
+
+
+ {/* Sidebar */}
+
-
- {/* Sidebar */}
-
+ )}
- {/* Newsletter Signup */}
-
-
- {t('blog.subscribe')}
-
-
- {t('blog.subscribeDesc')}
-
-
+ {t('blog.subscribe')}
+
+
-
-
-
-
-
+ }}>
+ {t('blog.subscribeDesc')}
+
+
+
+
+
+
+
+ )}
- )
+ );
}
-export default Blog
\ No newline at end of file
+export default Blog;