S SmartDocs
Serie: React.js javascript 249 righe · Aggiornato 2026-04-02

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;

Articoli correlati