Project_BlogApp.jsx
React.js/lessons/15_react_router/projects/Project_BlogApp.jsx
/**
* Project: 部落格應用 — React Router 練習
*
* 學習重點:
* - BrowserRouter, Routes, Route 路由設定
* - Link / NavLink 導航
* - useParams 動態路由參數
* - Outlet 巢狀路由
* - useNavigate 程式化導航
* - 404 頁面
*
* 安裝依賴:npm install react-router-dom
* 使用方式:將此檔案內容複製到你的 App.jsx 中
*/
import { useState } from 'react';
import {
BrowserRouter,
Routes,
Route,
Link,
NavLink,
Outlet,
useParams,
useNavigate,
} from 'react-router-dom';
// 文章資料
const POSTS = [
{ id: 1, title: 'React 入門指南', category: 'React', date: '2024-01-15', excerpt: '從零開始學習 React,包含 JSX、元件、Props 等基礎概念。', content: 'React 是由 Meta 開發的 JavaScript UI 函式庫。它採用宣告式的設計,讓開發者可以輕鬆地建構使用者介面。React 的核心概念包括元件化、虛擬 DOM、單向資料流等。本文將從基礎開始,一步步帶你認識 React 的世界。' },
{ id: 2, title: 'useState 完全指南', category: 'Hooks', date: '2024-01-20', excerpt: '深入了解 useState Hook 的所有用法和常見陷阱。', content: 'useState 是 React 中最基本也最常用的 Hook。它讓你在函式元件中加入狀態管理的能力。本文涵蓋基本用法、惰性初始化、不可變性更新、批次更新等進階主題。' },
{ id: 3, title: 'useEffect 生命週期', category: 'Hooks', date: '2024-02-01', excerpt: '掌握 useEffect 的三種模式和清除函式。', content: 'useEffect 讓你在函式元件中處理副作用。它取代了 class 元件的 componentDidMount、componentDidUpdate 和 componentWillUnmount。正確使用依賴陣列和清除函式是避免 bug 的關鍵。' },
{ id: 4, title: 'React Router 實戰', category: 'Router', date: '2024-02-10', excerpt: '使用 React Router v6 建構多頁面 SPA。', content: 'React Router 是 React 生態系中最受歡迎的路由解決方案。v6 版本帶來了許多改進,包括更簡潔的 API、巢狀路由、相對路徑等。本文將透過實際範例帶你掌握 React Router。' },
{ id: 5, title: 'CSS-in-JS 比較', category: 'CSS', date: '2024-02-15', excerpt: 'Styled Components、Emotion、Tailwind CSS 的比較分析。', content: '在 React 中處理樣式有許多方式。CSS Modules、Styled Components、Emotion、Tailwind CSS 各有優缺點。本文將從效能、DX、維護性等角度進行全面比較。' },
];
const categories = ['全部', ...new Set(POSTS.map((p) => p.category))];
// ============================================================
// 共用元件
// ============================================================
const Layout = () => {
const navLinkStyle = ({ isActive }) => ({
color: isActive ? '#4f46e5' : '#64748b',
textDecoration: 'none',
fontWeight: isActive ? '600' : '400',
padding: '8px 16px',
borderRadius: '8px',
backgroundColor: isActive ? '#eef2ff' : 'transparent',
fontSize: '0.9rem',
transition: 'all 0.2s',
});
return (
<div style={{ minHeight: '100vh', backgroundColor: '#f8fafc', fontFamily: "'Segoe UI', sans-serif" }}>
<nav style={{
display: 'flex', justifyContent: 'space-between', alignItems: 'center',
padding: '12px 32px', backgroundColor: 'white', borderBottom: '1px solid #e2e8f0',
position: 'sticky', top: 0, zIndex: 10,
}}>
<Link to="/" style={{ textDecoration: 'none', color: '#0f172a', fontWeight: 'bold', fontSize: '1.2rem' }}>
📝 React Blog
</Link>
<div style={{ display: 'flex', gap: '4px' }}>
<NavLink to="/" end style={navLinkStyle}>首頁</NavLink>
<NavLink to="/posts" style={navLinkStyle}>文章</NavLink>
<NavLink to="/about" style={navLinkStyle}>關於</NavLink>
</div>
</nav>
<main style={{ maxWidth: '800px', margin: '0 auto', padding: '32px 16px' }}>
<Outlet />
</main>
</div>
);
};
// ============================================================
// 頁面元件
// ============================================================
const HomePage = () => {
const navigate = useNavigate();
const latestPosts = POSTS.slice(0, 3);
return (
<div>
<div style={{ textAlign: 'center', padding: '40px 0' }}>
<h1 style={{ color: '#0f172a', fontSize: '2.5rem', marginBottom: '8px' }}>React Blog</h1>
<p style={{ color: '#64748b', fontSize: '1.1rem' }}>分享 React 開發技巧與最佳實踐</p>
<button
onClick={() => navigate('/posts')}
style={{
marginTop: '16px', padding: '12px 32px', borderRadius: '10px',
border: 'none', backgroundColor: '#4f46e5', color: 'white',
cursor: 'pointer', fontWeight: 'bold', fontSize: '1rem',
}}
>
瀏覽所有文章 →
</button>
</div>
<h2 style={{ color: '#475569', fontSize: '1.1rem', marginBottom: '16px' }}>最新文章</h2>
{latestPosts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
);
};
const PostCard = ({ post }) => (
<Link to={`/posts/${post.id}`} style={{ textDecoration: 'none', display: 'block', marginBottom: '16px' }}>
<div style={{
padding: '24px', borderRadius: '12px', backgroundColor: 'white',
border: '1px solid #f1f5f9', transition: 'box-shadow 0.2s',
}}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
<span style={{
padding: '2px 10px', borderRadius: '6px', backgroundColor: '#eef2ff',
color: '#4f46e5', fontSize: '0.75rem', fontWeight: '600',
}}>{post.category}</span>
<span style={{ color: '#94a3b8', fontSize: '0.8rem' }}>{post.date}</span>
</div>
<h3 style={{ margin: '0 0 8px', color: '#0f172a', fontSize: '1.1rem' }}>{post.title}</h3>
<p style={{ margin: 0, color: '#64748b', fontSize: '0.9rem', lineHeight: 1.5 }}>{post.excerpt}</p>
</div>
</Link>
);
const PostListPage = () => {
const [selectedCat, setSelectedCat] = useState('全部');
const filtered = selectedCat === '全部' ? POSTS : POSTS.filter((p) => p.category === selectedCat);
return (
<div>
<h1 style={{ color: '#0f172a', marginBottom: '16px' }}>所有文章</h1>
<div style={{ display: 'flex', gap: '8px', marginBottom: '24px', flexWrap: 'wrap' }}>
{categories.map((cat) => (
<button
key={cat}
onClick={() => setSelectedCat(cat)}
style={{
padding: '6px 16px', borderRadius: '20px', border: 'none',
backgroundColor: selectedCat === cat ? '#4f46e5' : '#f1f5f9',
color: selectedCat === cat ? 'white' : '#64748b',
cursor: 'pointer', fontSize: '0.85rem',
}}
>
{cat}
</button>
))}
</div>
<p style={{ color: '#94a3b8', fontSize: '0.85rem', marginBottom: '16px' }}>
共 {filtered.length} 篇文章
</p>
{filtered.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
);
};
const PostDetailPage = () => {
const { postId } = useParams();
const navigate = useNavigate();
const post = POSTS.find((p) => p.id === Number(postId));
if (!post) {
return (
<div style={{ textAlign: 'center', padding: '60px 0' }}>
<h1 style={{ color: '#ef4444' }}>文章不存在</h1>
<button onClick={() => navigate('/posts')} style={{
padding: '10px 24px', borderRadius: '8px', border: 'none',
backgroundColor: '#4f46e5', color: 'white', cursor: 'pointer', marginTop: '16px',
}}>
返回文章列表
</button>
</div>
);
}
return (
<article>
<button onClick={() => navigate(-1)} style={{
background: 'none', border: 'none', color: '#4f46e5', cursor: 'pointer',
fontSize: '0.9rem', padding: 0, marginBottom: '16px',
}}>
← 返回
</button>
<span style={{
padding: '4px 12px', borderRadius: '6px', backgroundColor: '#eef2ff',
color: '#4f46e5', fontSize: '0.8rem', fontWeight: '600',
}}>{post.category}</span>
<h1 style={{ color: '#0f172a', margin: '12px 0 8px', fontSize: '2rem' }}>{post.title}</h1>
<p style={{ color: '#94a3b8', fontSize: '0.9rem' }}>📅 {post.date}</p>
<div style={{
padding: '24px', backgroundColor: 'white', borderRadius: '12px',
border: '1px solid #f1f5f9', lineHeight: 1.8, color: '#334155', fontSize: '1rem',
}}>
{post.content}
</div>
</article>
);
};
const AboutPage = () => (
<div style={{ textAlign: 'center', padding: '40px 0' }}>
<h1 style={{ color: '#0f172a' }}>關於 React Blog</h1>
<p style={{ color: '#64748b', maxWidth: '500px', margin: '0 auto', lineHeight: 1.8 }}>
這是一個使用 React Router v6 建構的部落格應用範例。
展示了路由設定、動態路由、巢狀路由、NavLink 和程式化導航等功能。
</p>
</div>
);
const NotFoundPage = () => {
const navigate = useNavigate();
return (
<div style={{ textAlign: 'center', padding: '60px 0' }}>
<h1 style={{ fontSize: '6rem', margin: '0 0 8px', color: '#e2e8f0' }}>404</h1>
<p style={{ color: '#64748b', marginBottom: '16px' }}>找不到此頁面</p>
<button onClick={() => navigate('/')} style={{
padding: '10px 24px', borderRadius: '8px', border: 'none',
backgroundColor: '#4f46e5', color: 'white', cursor: 'pointer',
}}>
回到首頁
</button>
</div>
);
};
// ============================================================
// App
// ============================================================
function App() {
return (
<BrowserRouter>
<Routes>
<Route element={<Layout />}>
<Route path="/" element={<HomePage />} />
<Route path="/posts" element={<PostListPage />} />
<Route path="/posts/:postId" element={<PostDetailPage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="*" element={<NotFoundPage />} />
</Route>
</Routes>
</BrowserRouter>
);
}
export default App;
Bài viết liên quan
App.js
App.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/App.js).
Đọc bài viết →App.test.js
App.test.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/App.test.js).
Đọc bài viết →index.js
index.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/index.js).
Đọc bài viết →reportWebVitals.js
reportWebVitals.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/reportWebVitals.js).
Đọc bài viết →setupTests.js
setupTests.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/setupTests.js).
Đọc bài viết →Project_HelloReact.jsx
Project_HelloReact.jsx — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/projects/Project_HelloReact.jsx).
Đọc bài viết →