Project_RestaurantMenu.jsx
React.js/lessons/03_components/projects/Project_RestaurantMenu.jsx
/**
* Project: 餐廳菜單頁面 — 元件組合練習
*
* 學習重點:
* - 將 UI 拆分成多個獨立元件
* - 元件組合 (Composition)
* - 元件匯出 / 匯入概念
* - 陣列渲染
*
* 使用方式:將此檔案內容複製到你的 App.jsx 中
*/
// ============================================================
// 資料
// ============================================================
const menuData = {
restaurant: {
name: '和風亭',
subtitle: '日式料理 · 精緻定食',
description: '嚴選新鮮食材,傳承日本職人精神',
},
categories: [
{
id: 'appetizer',
name: '前菜',
icon: '🥗',
items: [
{ id: 1, name: '和風沙拉', price: 120, description: '新鮮生菜佐和風醬', popular: false, spicy: false },
{ id: 2, name: '毛豆', price: 60, description: '鹽味毛豆,開胃小品', popular: true, spicy: false },
{ id: 3, name: '胡麻豆腐', price: 90, description: '手作豆腐淋胡麻醬', popular: false, spicy: false },
],
},
{
id: 'main',
name: '主餐',
icon: '🍱',
items: [
{ id: 4, name: '照燒雞腿定食', price: 320, description: '嫩煎雞腿佐照燒醬,附白飯、味噌湯、小菜三品', popular: true, spicy: false },
{ id: 5, name: '炸蝦天婦羅定食', price: 360, description: '酥脆炸蝦天婦羅,附白飯、味噌湯', popular: true, spicy: false },
{ id: 6, name: '味噌豬排定食', price: 340, description: '厚切豬排淋味噌醬', popular: false, spicy: false },
{ id: 7, name: '辣味擔擔烏龍麵', price: 280, description: '濃郁芝麻辣湯底搭配Q彈烏龍麵', popular: false, spicy: true },
{ id: 8, name: '鮭魚親子丼', price: 380, description: '新鮮鮭魚生魚片與鮭魚卵', popular: true, spicy: false },
],
},
{
id: 'dessert',
name: '甜點',
icon: '🍡',
items: [
{ id: 9, name: '抹茶冰淇淋', price: 80, description: '京都宇治抹茶', popular: false, spicy: false },
{ id: 10, name: '黑糖蕨餅', price: 100, description: '手作蕨餅佐黑糖蜜與黃豆粉', popular: true, spicy: false },
{ id: 11, name: '焙茶布丁', price: 90, description: '濃郁焙茶風味滑順布丁', popular: false, spicy: false },
],
},
{
id: 'drink',
name: '飲品',
icon: '🍵',
items: [
{ id: 12, name: '煎茶', price: 50, description: '日本綠茶', popular: false, spicy: false },
{ id: 13, name: '梅酒蘇打', price: 150, description: '自家釀梅酒搭配氣泡水', popular: true, spicy: false },
{ id: 14, name: '可爾必思', price: 60, description: '經典乳酸飲品', popular: false, spicy: false },
],
},
],
};
// ============================================================
// 元件:餐廳標頭
// ============================================================
const RestaurantHeader = ({ name, subtitle, description }) => (
<header
style={{
background: 'linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)',
color: 'white',
textAlign: 'center',
padding: '48px 24px',
}}
>
<h1 style={{ fontSize: '2.5rem', margin: '0 0 8px', letterSpacing: '8px' }}>
{name}
</h1>
<p style={{ color: '#94a3b8', margin: '0 0 4px', fontSize: '1rem' }}>{subtitle}</p>
<p style={{ color: '#64748b', margin: 0, fontSize: '0.85rem' }}>{description}</p>
<div
style={{
width: '60px',
height: '2px',
backgroundColor: '#c9a96e',
margin: '20px auto 0',
}}
/>
</header>
);
// ============================================================
// 元件:價格標籤
// ============================================================
const PriceTag = ({ price }) => (
<span
style={{
fontSize: '1.1rem',
fontWeight: 'bold',
color: '#c9a96e',
whiteSpace: 'nowrap',
}}
>
NT$ {price}
</span>
);
// ============================================================
// 元件:徽章
// ============================================================
const Badge = ({ text, bgColor, textColor }) => (
<span
style={{
display: 'inline-block',
padding: '2px 8px',
borderRadius: '4px',
backgroundColor: bgColor,
color: textColor,
fontSize: '0.7rem',
fontWeight: 'bold',
marginLeft: '8px',
verticalAlign: 'middle',
}}
>
{text}
</span>
);
// ============================================================
// 元件:菜單項目
// ============================================================
const MenuItem = ({ item }) => (
<div
style={{
display: 'flex',
justifyContent: 'space-between',
alignItems: 'flex-start',
padding: '16px 0',
borderBottom: '1px dashed #e2e8f0',
}}
>
<div style={{ flex: 1, marginRight: '16px' }}>
<div style={{ display: 'flex', alignItems: 'center', flexWrap: 'wrap' }}>
<span style={{ fontWeight: '600', color: '#1e293b', fontSize: '1rem' }}>
{item.name}
</span>
{item.popular && <Badge text="人氣" bgColor="#fef3c7" textColor="#92400e" />}
{item.spicy && <Badge text="辣" bgColor="#fee2e2" textColor="#991b1b" />}
</div>
<p style={{ margin: '4px 0 0', color: '#94a3b8', fontSize: '0.85rem', lineHeight: 1.4 }}>
{item.description}
</p>
</div>
<PriceTag price={item.price} />
</div>
);
// ============================================================
// 元件:菜單分類
// ============================================================
const MenuCategory = ({ category }) => (
<section style={{ marginBottom: '40px' }}>
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '10px',
marginBottom: '16px',
paddingBottom: '12px',
borderBottom: '2px solid #1e293b',
}}
>
<span style={{ fontSize: '1.5rem' }}>{category.icon}</span>
<h2 style={{ margin: 0, color: '#1e293b', fontSize: '1.3rem', letterSpacing: '2px' }}>
{category.name}
</h2>
<span style={{ color: '#94a3b8', fontSize: '0.8rem' }}>
({category.items.length} 品)
</span>
</div>
<div>
{category.items.map((item) => (
<MenuItem key={item.id} item={item} />
))}
</div>
</section>
);
// ============================================================
// 元件:餐廳頁尾
// ============================================================
const RestaurantFooter = () => (
<footer
style={{
textAlign: 'center',
padding: '32px 24px',
backgroundColor: '#f8fafc',
borderTop: '1px solid #e2e8f0',
color: '#94a3b8',
fontSize: '0.8rem',
}}
>
<p style={{ margin: '0 0 4px' }}>營業時間:11:30 - 14:00 / 17:30 - 21:00</p>
<p style={{ margin: '0 0 4px' }}>每週一公休</p>
<p style={{ margin: 0 }}>📍 台北市大安區忠孝東路四段 123 號</p>
</footer>
);
// ============================================================
// App:組合所有元件
// ============================================================
function App() {
const { restaurant, categories } = menuData;
return (
<div
style={{
maxWidth: '650px',
margin: '0 auto',
backgroundColor: '#fff',
minHeight: '100vh',
fontFamily: "'Segoe UI', 'Noto Sans TC', sans-serif",
boxShadow: '0 0 40px rgba(0,0,0,0.08)',
}}
>
<RestaurantHeader
name={restaurant.name}
subtitle={restaurant.subtitle}
description={restaurant.description}
/>
<main style={{ padding: '32px 32px 0' }}>
{categories.map((category) => (
<MenuCategory key={category.id} category={category} />
))}
</main>
<RestaurantFooter />
</div>
);
}
export default App;
Related articles
App.js
App.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/App.js).
Read article →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).
Read article →index.js
index.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/index.js).
Read article →reportWebVitals.js
reportWebVitals.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/reportWebVitals.js).
Read article →setupTests.js
setupTests.js — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/example/src/setupTests.js).
Read article →Project_HelloReact.jsx
Project_HelloReact.jsx — javascript source code from the React.js learning materials (React.js/lessons/01_introduction/projects/Project_HelloReact.jsx).
Read article →