iT邦幫忙

2025 iThome 鐵人賽

DAY 27
0
生成式 AI

30天用React打造AI工具箱系列 第 27

30天用React打造AI工具箱 Day27

  • 分享至 

  • xImage
  •  

30天用React打造AI工具箱 Day27

傳統搜尋就像 Google,一次只看一個關鍵字;
但筆記往往有不同的脈絡。
舉個例子:

  • 「React 學習筆記」屬於學習類
  • 「待辦整理」屬於工作類
  • 「AI prompt想法」則跨越學習與創意

這時候,我意識到要讓筆記「有結構」,
需要兩種東西:
分類(Category)和標籤(Tags)

分類

分類是筆記的主體歸屬。
每篇筆記只能有一個分類,
它幫助我在「大方向」上找到想要的東西。
像是:

  • 學習類 → React、Next.js、ChatGPT
  • 工作類 → 專案紀錄、會議筆記
  • 生活類 → 心情、日記、旅行

標籤

分類解決了「大方向」,
但同一個主題下的筆記,還可能橫跨多種議題。
例如我在「學習」分類下,可能有:

  • #React
  • #AI
  • #Hook
  • #useEffect

我在搜尋時只要輸入 Hook 或 AI,
不論在哪個分類都能快速找到相關內容。

程式碼

import { useState, useEffect } from "react"

export default function NoteManager() {
  const [notes, setNotes] = useState(() => {
    const saved = localStorage.getItem("notes")
    return saved ? JSON.parse(saved) : []
  })
  const [title, setTitle] = useState("")
  const [content, setContent] = useState("")
  const [category, setCategory] = useState("學習")
  const [tags, setTags] = useState("")
  const [search, setSearch] = useState("")
  const [filter, setFilter] = useState("全部")

  useEffect(() => {
    localStorage.setItem("notes", JSON.stringify(notes))
  }, [notes])

  const addNote = () => {
    if (!title.trim()) return
    const newNote = {
      id: Date.now(),
      title,
      content,
      category,
      tags: tags.split(",").map((t) => t.trim()).filter(Boolean),
      time: new Date().toLocaleString(),
    }
    setNotes([newNote, ...notes])
    setTitle("")
    setContent("")
    setTags("")
  }

  const deleteNote = (id) => {
    setNotes(notes.filter((n) => n.id !== id))
  }

  const categories = ["全部", "學習", "工作", "生活"]
  const filtered = notes.filter((n) => {
    const tags = Array.isArray(n.tags) ? n.tags : [] // 🧠 防止 undefined
    const matchSearch =
      n.title.toLowerCase().includes(search.toLowerCase()) ||
      tags.some((t) => t.toLowerCase().includes(search.toLowerCase()))
    const matchCategory = filter === "全部" || n.category === filter
    return matchSearch && matchCategory
  })


  return (
    <div className="space-y-4">
      <h1 className="text-2xl font-bold">📒 筆記管理工具 - 升級版</h1>

      <div className="flex space-x-2">
        <select
          value={filter}
          onChange={(e) => setFilter(e.target.value)}
          className="border p-2 rounded"
        >
          {categories.map((c) => (
            <option key={c}>{c}</option>
          ))}
        </select>
        <input
          value={search}
          onChange={(e) => setSearch(e.target.value)}
          placeholder="搜尋標題或標籤..."
          className="flex-1 border p-2 rounded"
        />
      </div>

      <div className="bg-white p-4 rounded shadow space-y-3">
        <input
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          placeholder="筆記標題"
          className="border w-full p-2 rounded"
        />
        <textarea
          value={content}
          onChange={(e) => setContent(e.target.value)}
          placeholder="筆記內容"
          className="border w-full p-2 rounded h-24"
        />
        <div className="flex space-x-2">
          <select
            value={category}
            onChange={(e) => setCategory(e.target.value)}
            className="border p-2 rounded"
          >
            {categories.slice(1).map((c) => (
              <option key={c}>{c}</option>
            ))}
          </select>
          <input
            value={tags}
            onChange={(e) => setTags(e.target.value)}
            placeholder="輸入標籤(以逗號分隔)"
            className="flex-1 border p-2 rounded"
          />
        </div>
        <button
          onClick={addNote}
          className="bg-blue-600 text-white px-4 py-2 rounded hover:bg-blue-700"
        >
          新增筆記
        </button>
      </div>

      <div className="grid md:grid-cols-2 gap-4">
        {filtered.map((n) => (
          <div key={n.id} className="bg-gray-50 border p-4 rounded shadow-sm">
            <h2 className="text-lg font-bold">{n.title}</h2>
            <p className="text-sm text-gray-600">{n.content}</p>
            <div className="mt-2 flex flex-wrap gap-1 text-sm">
              <span className="bg-green-200 px-2 py-0.5 rounded">{n.category}</span>
              {Array.isArray(n.tags) &&
                n.tags.map((t, i) => (
                  <span key={i} className="bg-yellow-200 px-2 py-0.5 rounded">
                    #{t}
                  </span>
                ))}
            </div>
            <div className="text-xs text-gray-500 mt-1">{n.time}</div>
            <button
              onClick={() => deleteNote(n.id)}
              className="text-red-600 text-sm mt-2 hover:underline"
            >
              刪除
            </button>
          </div>
        ))}
      </div>
    </div>
  )
}

上一篇
30天用React打造AI工具箱Day26
系列文
30天用React打造AI工具箱27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言