今日目標
- 建立 資料檔(skills.ts / projects.ts)
- 用 map() 渲染 Skills 與 Projects
- 在 Projects 加入 useState,準備之後做篩選 / 搜尋
- 認識 Props:如何讓元件接受外部資料
1. 建立資料檔
src/data/skills.ts
export type Skill = {
name: string
category: 'frontend' | 'backend' | 'tools'
}
export const skills: Skill[] = [
{ name: 'HTML / CSS / SCSS', category: 'frontend' },
{ name: 'TypeScript', category: 'frontend' },
{ name: 'React / Vue / Angular', category: 'frontend' },
{ name: 'Node.js / Express', category: 'backend' },
{ name: 'Git / GitHub / Docker', category: 'tools' },
{ name: 'Vite / Webpack', category: 'tools' }
]
src/data/projects.ts
export type Project = {
id: number
slug: string
title: string
tech: string
desc: string
repo: string
demo?: string
featured?: boolean
}
export const projects: Project[] = [
{
id: 1,
slug: 'maomao-shop',
title: '毛毛購物(寵物電商)',
tech: 'React + Node.js|購物車、結帳、RWD',
desc: '主導前端架構,完成商品列表、購物流程與訂單頁。',
repo: '#',
demo: '#',
featured: true
},
{
id: 2,
slug: 'line-bot-reservation',
title: 'LINE Bot 預約系統',
tech: 'Cloud Functions + LINE API|時段預約',
desc: '整合 LINE 聊天介面與雲端排程,完成會員預約流程。',
repo: '#'
}
]
2. Skills 元件:用 Props 接資料
src/components/Skills.tsx
import React from 'react'
import type { Skill } from '../data/skills'
type Props = {
items: Skill[]
}
export default function Skills({ items }: Props) {
return (
<section id="skills" className="container section">
<h2>技能 Skillset</h2>
<ul className="skill-grid">
{items.map((s, i) => (
<li key={i}>{s.name}</li>
))}
</ul>
</section>
)
}
3. Projects 元件:用 useState 管理
src/components/Projects.tsx
import React, { useState } from 'react'
import type { Project } from '../data/projects'
type Props = {
items: Project[]
}
export default function Projects({ items }: Props) {
// 之後會加入篩選 / 搜尋,先用 useState 管理 view
const [view, setView] = useState(items)
return (
<section id="projects" className="container section">
<h2>作品集 Projects</h2>
<div className="project-grid">
{view.map((p) => (
<article className="card" key={p.id}>
<h3>{p.title}</h3>
<p className="muted">{p.tech}</p>
<p>{p.desc}</p>
<div style={{ display: 'flex', gap: '8px', marginTop: '8px' }}>
<a className="btn small" href={p.demo}>Demo</a>
<a className="btn small btn-outline" href={p.repo}>GitHub</a>
</div>
</article>
))}
</div>
</section>
)
}
4. 在 App.tsx 匯入資料
import React from 'react'
import SiteHeader from './components/SiteHeader'
import Hero from './components/Hero'
import About from './components/About'
import Skills from './components/Skills'
import Projects from './components/Projects'
import Contact from './components/Contact'
import SiteFooter from './components/SiteFooter'
import { skills } from './data/skills'
import { projects } from './data/projects'
export default function App() {
return (
<><SiteHeader />
<main>
<Hero />
<About />
<Skills items={skills} />
<Projects items={projects} />
<Contact />
</main>
<SiteFooter />
</>
)
}
成果檢查清單 ✅
- 頁面 Skills 改用
map()
生成列表
- Projects 改用
map()
生成卡片
- 元件由 靜態 → 資料驅動
- Props 正確傳遞,Skills 與 Projects 可重複使用
- Projects 已有 useState,準備明天加篩選
小心踩雷
-
忘了加 key
- ❌
items.map((s) => <li>{s.name}</li>)
- ✅
<li key={i}>…</li>
(最好用 id,而不是 index)
-
Props 型別漏掉
- 在 TS 專案要明確定義
type Props
,否則很難維護
-
把 useState 寫死
- ❌
const [view] = useState(items)
- ✅ 要留 setView,未來能更新
下一步(Day 30 預告)
- 在 Projects 加入 搜尋框 + 篩選按鈕
- 用
useState
更新 view
- 實作小小互動(全部 / 精選 / 關鍵字)
- React 章節初步告一段落 🎉