S SmartDocs
Serie: React.js javascript 226 líneas · Actualizado 2026-04-02

Project_ThemeApp.jsx

React.js/lessons/12_useContext/projects/Project_ThemeApp.jsx

/**
 * Project: 多主題應用 — useContext 練習
 *
 * 學習重點:
 * - createContext + Provider + useContext
 * - 建立自訂 Hook 封裝 Context
 * - 多個 Context 並存(ThemeContext + LanguageContext)
 * - 跨元件共享狀態而無需 Props Drilling
 *
 * 使用方式:將此檔案內容複製到你的 App.jsx 中
 */

import { createContext, useContext, useState } from 'react';

// ============================================================
// Theme Context
// ============================================================
const ThemeContext = createContext();

const themes = {
  light: { bg: '#ffffff', surface: '#f8fafc', text: '#0f172a', textSecondary: '#64748b', border: '#e2e8f0', primary: '#4f46e5', primaryBg: '#eef2ff' },
  dark: { bg: '#0f172a', surface: '#1e293b', text: '#f1f5f9', textSecondary: '#94a3b8', border: '#334155', primary: '#818cf8', primaryBg: '#1e1b4b' },
  ocean: { bg: '#042f2e', surface: '#0f3d3c', text: '#ccfbf1', textSecondary: '#5eead4', border: '#115e59', primary: '#2dd4bf', primaryBg: '#134e4a' },
  sunset: { bg: '#1c1917', surface: '#292524', text: '#fef3c7', textSecondary: '#fbbf24', border: '#44403c', primary: '#f97316', primaryBg: '#431407' },
};

function ThemeProvider({ children }) {
  const [themeName, setThemeName] = useState('light');
  const colors = themes[themeName];
  return (
    <ThemeContext.Provider value={{ themeName, setThemeName, colors, availableThemes: Object.keys(themes) }}>
      {children}
    </ThemeContext.Provider>
  );
}

function useTheme() {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
  return ctx;
}

// ============================================================
// Language Context
// ============================================================
const LanguageContext = createContext();

const i18n = {
  'zh-TW': { title: '主題切換應用', greeting: '歡迎', nav: { home: '首頁', about: '關於', settings: '設定' }, card: { title: '功能卡片', desc: '此應用展示了 useContext 的用法' }, theme: '主題', lang: '語言', dark: '深色', light: '淺色', ocean: '海洋', sunset: '夕陽' },
  en: { title: 'Theme Switcher App', greeting: 'Welcome', nav: { home: 'Home', about: 'About', settings: 'Settings' }, card: { title: 'Feature Card', desc: 'This app demonstrates useContext usage' }, theme: 'Theme', lang: 'Language', dark: 'Dark', light: 'Light', ocean: 'Ocean', sunset: 'Sunset' },
  ja: { title: 'テーマ切替アプリ', greeting: 'ようこそ', nav: { home: 'ホーム', about: '概要', settings: '設定' }, card: { title: '機能カード', desc: 'useContextの使い方を示すアプリ' }, theme: 'テーマ', lang: '言語', dark: 'ダーク', light: 'ライト', ocean: 'オーシャン', sunset: 'サンセット' },
};

function LanguageProvider({ children }) {
  const [language, setLanguage] = useState('zh-TW');
  const t = (path) => {
    const keys = path.split('.');
    let result = i18n[language];
    for (const key of keys) {
      result = result?.[key];
    }
    return result || path;
  };
  return (
    <LanguageContext.Provider value={{ language, setLanguage, t, availableLanguages: Object.keys(i18n) }}>
      {children}
    </LanguageContext.Provider>
  );
}

function useLanguage() {
  const ctx = useContext(LanguageContext);
  if (!ctx) throw new Error('useLanguage must be used within LanguageProvider');
  return ctx;
}

// ============================================================
// UI Components
// ============================================================
function Navbar() {
  const { colors, themeName, setThemeName, availableThemes } = useTheme();
  const { language, setLanguage, t, availableLanguages } = useLanguage();

  return (
    <nav style={{
      display: 'flex', justifyContent: 'space-between', alignItems: 'center',
      padding: '16px 32px', backgroundColor: colors.surface,
      borderBottom: `1px solid ${colors.border}`, transition: 'all 0.3s',
    }}>
      <h2 style={{ margin: 0, color: colors.primary, fontSize: '1.2rem' }}>{t('title')}</h2>
      <div style={{ display: 'flex', gap: '20px', alignItems: 'center' }}>
        {Object.values(t('nav')).map((item, i) => (
          <a key={i} href="#" style={{ color: colors.textSecondary, textDecoration: 'none', fontSize: '0.9rem' }}>{item}</a>
        ))}
        <div style={{ width: '1px', height: '20px', backgroundColor: colors.border }} />
        <select value={language} onChange={(e) => setLanguage(e.target.value)} style={{
          padding: '4px 8px', borderRadius: '6px', border: `1px solid ${colors.border}`,
          backgroundColor: colors.surface, color: colors.text, cursor: 'pointer', fontSize: '0.85rem',
        }}>
          {availableLanguages.map((lang) => (
            <option key={lang} value={lang}>{lang === 'zh-TW' ? '中文' : lang === 'en' ? 'English' : '日本語'}</option>
          ))}
        </select>
      </div>
    </nav>
  );
}

function ThemePicker() {
  const { colors, themeName, setThemeName, availableThemes } = useTheme();
  const { t } = useLanguage();

  const themeIcons = { light: '☀️', dark: '🌙', ocean: '🌊', sunset: '🌅' };

  return (
    <div style={{
      padding: '24px', backgroundColor: colors.surface,
      borderRadius: '16px', border: `1px solid ${colors.border}`, transition: 'all 0.3s',
    }}>
      <h3 style={{ margin: '0 0 16px', color: colors.text }}>{t('theme')}</h3>
      <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '8px' }}>
        {availableThemes.map((name) => (
          <button
            key={name}
            onClick={() => setThemeName(name)}
            style={{
              padding: '12px 8px', borderRadius: '10px', cursor: 'pointer',
              border: `2px solid ${themeName === name ? colors.primary : colors.border}`,
              backgroundColor: themeName === name ? colors.primaryBg : 'transparent',
              color: colors.text, fontSize: '0.85rem', transition: 'all 0.2s',
            }}
          >
            <div style={{ fontSize: '1.5rem', marginBottom: '4px' }}>{themeIcons[name]}</div>
            {t(name)}
          </button>
        ))}
      </div>
    </div>
  );
}

function FeatureCards() {
  const { colors } = useTheme();
  const { t } = useLanguage();

  const features = [
    { icon: '🎨', title: 'Context API', desc: 'createContext + Provider' },
    { icon: '🪝', title: 'Custom Hook', desc: 'useTheme() / useLanguage()' },
    { icon: '🌍', title: 'i18n', desc: t('card.desc') },
  ];

  return (
    <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px' }}>
      {features.map((f, i) => (
        <div key={i} style={{
          padding: '24px', borderRadius: '16px', backgroundColor: colors.surface,
          border: `1px solid ${colors.border}`, textAlign: 'center', transition: 'all 0.3s',
        }}>
          <div style={{ fontSize: '2.5rem', marginBottom: '12px' }}>{f.icon}</div>
          <h4 style={{ margin: '0 0 8px', color: colors.text }}>{f.title}</h4>
          <p style={{ margin: 0, color: colors.textSecondary, fontSize: '0.85rem', lineHeight: 1.5 }}>{f.desc}</p>
        </div>
      ))}
    </div>
  );
}

function ContextInfo() {
  const { themeName, colors } = useTheme();
  const { language } = useLanguage();

  return (
    <div style={{
      padding: '20px', backgroundColor: colors.primaryBg,
      borderRadius: '12px', border: `1px solid ${colors.border}`,
      fontFamily: "'SF Mono', monospace", fontSize: '0.8rem',
      color: colors.primary, transition: 'all 0.3s',
    }}>
      <p style={{ margin: '0 0 4px' }}>{'// 目前 Context 值'}</p>
      <p style={{ margin: '0 0 2px' }}>{'{'}</p>
      <p style={{ margin: '0 0 2px', paddingLeft: '16px' }}>theme: "{themeName}",</p>
      <p style={{ margin: '0 0 2px', paddingLeft: '16px' }}>language: "{language}",</p>
      <p style={{ margin: 0 }}>{'}'}</p>
    </div>
  );
}

// ============================================================
// Main App
// ============================================================
function AppContent() {
  const { colors } = useTheme();
  const { t } = useLanguage();

  return (
    <div style={{
      minHeight: '100vh', backgroundColor: colors.bg,
      transition: 'background-color 0.3s', fontFamily: "'Segoe UI', sans-serif",
    }}>
      <Navbar />
      <main style={{ maxWidth: '800px', margin: '0 auto', padding: '32px 16px', display: 'flex', flexDirection: 'column', gap: '24px' }}>
        <div style={{ textAlign: 'center' }}>
          <h1 style={{ color: colors.text, marginBottom: '4px', transition: 'color 0.3s' }}>
            {t('greeting')}! 👋
          </h1>
          <p style={{ color: colors.textSecondary, transition: 'color 0.3s' }}>{t('card.desc')}</p>
        </div>
        <ThemePicker />
        <FeatureCards />
        <ContextInfo />
      </main>
    </div>
  );
}

function App() {
  return (
    <ThemeProvider>
      <LanguageProvider>
        <AppContent />
      </LanguageProvider>
    </ThemeProvider>
  );
}

export default App;

Artículos relacionados