商品列表

用關鍵字搜尋商品、依分類篩選、排序,並以每頁 6 件分頁顯示。

商品列表

重點

  1. 1

    不改動原始資料,只產生要顯示的部分

    從全部商品開始,依序經過 filter(篩選)→ sort(排序)→ slice(分頁),產生要顯示在畫面上的部分。原本的列表維持不變。

  2. 2

    條件一改變,就回到第 1 頁

    在看第 2 頁時搜尋,結果可能只有 1 頁,畫面就會變成空的。只要條件改變,就把頁碼改回 1。

  3. 3

    準備好「找不到結果」時的畫面

    0 件時顯示「沒有符合條件的商品」,並提供清除條件的按鈕。什麼都不顯示的話,看起來就像壞掉了。

使用的工具

點擊查看第 2 部分的比較

修改程式碼

修改程式碼,右邊的預覽會跟著變化。

import { useState } from "react"
import { SearchIcon } from "lucide-react"
import { Button } from "./components/ui/button"
import { Input } from "./components/ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./components/ui/select"

type Product = {
  id: number
  name: string
  category: string
  price: number
  rating: number
  color: string // 用顏色代替商品照片
}

// 商品資料(12 件)
const products: Product[] = [
  { id: 1, name: "無線耳機", category: "3C 家電", price: 2550, rating: 4.5, color: "#0ea5e9" },
  { id: 2, name: "不鏽鋼保溫瓶", category: "廚房", price: 600, rating: 4.2, color: "#10b981" },
  { id: 3, name: "後背包 20L", category: "服飾", price: 1500, rating: 4.7, color: "#f59e0b" },
  { id: 4, name: "檯燈", category: "3C 家電", price: 1200, rating: 4.0, color: "#8b5cf6" },
  { id: 5, name: "咖啡磨豆機", category: "廚房", price: 900, rating: 4.6, color: "#ef4444" },
  { id: 6, name: "運動鞋", category: "服飾", price: 1950, rating: 4.3, color: "#ec4899" },
  { id: 7, name: "行動電源", category: "3C 家電", price: 780, rating: 4.4, color: "#14b8a6" },
  { id: 8, name: "平底鍋 26cm", category: "廚房", price: 690, rating: 4.1, color: "#f97316" },
  { id: 9, name: "棒球帽", category: "服飾", price: 480, rating: 3.9, color: "#6366f1" },
  { id: 10, name: "電熱水壺", category: "3C 家電", price: 990, rating: 4.5, color: "#84cc16" },
  { id: 11, name: "砧板", category: "廚房", price: 390, rating: 4.0, color: "#a855f7" },
  { id: 12, name: "手錶", category: "服飾", price: 3150, rating: 4.8, color: "#64748b" },
]

const categories = ["全部", "3C 家電", "廚房", "服飾"]

// 每頁顯示的數量
const pageSize = 6

// 一個商品的卡片
function ProductCard({ product }: { product: Product }) {
  return (
    <div className="overflow-hidden rounded-lg border bg-card">
      <div className="h-24" style={{ backgroundColor: product.color }} />
      <div className="p-3">
        <p className="text-xs text-muted-foreground">{product.category}</p>
        <p className="mt-0.5 text-sm font-medium">{product.name}</p>
        <div className="mt-2 flex items-center justify-between">
          <p className="text-sm font-semibold">NT${product.price.toLocaleString()}</p>
          <p className="text-xs text-amber-500">★ {product.rating}</p>
        </div>
      </div>
    </div>
  )
}

export default function App() {
  // ① 畫面要記住的值(state)
  const [query, setQuery] = useState("")
  const [category, setCategory] = useState("全部")
  const [sort, setSort] = useState("recommended")
  const [page, setPage] = useState(1)

  // ② 要顯示的商品:篩選 → 排序 → 分頁
  const filtered = products.filter((product) => product.name.includes(query) && (category === "全部" || product.category === category))

  // 價格由低到高
  let sorted = filtered
  if (sort === "cheap") sorted = [...filtered].sort((a, b) => a.price - b.price)

  // 價格由高到低
  if (sort === "expensive") sorted = [...filtered].sort((a, b) => b.price - a.price)

  // 評分由高到低
  if (sort === "rating") sorted = [...filtered].sort((a, b) => b.rating - a.rating)

  // 根據篩選後的件數計算總頁數
  const totalPages = Math.ceil(sorted.length / pageSize)

  // 第 1 頁 → slice(0, 6),第 2 頁 → slice(6, 12)
  const shown = sorted.slice((page - 1) * pageSize, page * pageSize)

  //console.log(query, category, shown)

  return (
    <div className="mx-auto my-6 w-full max-w-3xl rounded-xl border bg-card p-6 text-card-foreground">
      {/* 搜尋與排序 */}
      <div className="flex flex-wrap items-center gap-2">
        {/* 輸入框左側的線條放大鏡圖示 */}
        <div className="relative flex-1">
          <SearchIcon className="absolute top-1/2 left-3 size-4 -translate-y-1/2 text-muted-foreground" />
          <Input value={query}
        onChange={(e) => {
        setQuery(e.target.value)
        setPage(1) 
        // 搜尋後回到第 1 頁
        }}
        className="pl-9" placeholder="搜尋商品名稱" />
        </div>
        <Select value={sort} onValueChange={(value) => 
        {
        setSort(value)
        setPage(1)
        // 變更排序後回到第 1 頁
        }}>
          <SelectTrigger className="w-40">
            <SelectValue />
          </SelectTrigger>
          <SelectContent position="popper">
            <SelectItem value="recommended">綜合推薦</SelectItem>
            <SelectItem value="cheap">價格由低到高</SelectItem>
            <SelectItem value="expensive">價格由高到低</SelectItem>
            <SelectItem value="rating">評分最高</SelectItem>
          </SelectContent>
        </Select>
      </div>

      {/* 分類 */}
      <div className="mt-3 flex flex-wrap gap-2">
        {categories.map((name) => (
          <Button key={name} size="sm" variant={ category === name ? "default" : "outline" } onClick={() => {
            setCategory(name)
            setPage(1) // 變更分類後回到第 1 頁
          }} 
          className="rounded-full">
            {name}
          </Button>
        ))}
      </div>

      <p className="mt-4 text-xs text-muted-foreground">共 {sorted.length} 件</p>

      {/* 商品卡片 */}
      {sorted.length === 0 ? ( <p className="py-10 text-center text-sm text-muted-foreground">找不到符合的商品</p> ) : (
      <div className="mt-2 grid grid-cols-2 gap-3 sm:grid-cols-3">
        {shown.map((product) => (
        <ProductCard key={product.id} product={product} />
        ))}
      </div>
      )}

      {/* 分頁 */}
      <div className="mt-5 flex items-center justify-center gap-1">
        <Button size="sm" variant="outline" disabled={page === 1} onClick={page === 1 ? undefined : () => setPage(page - 1)}>
          上一頁
        </Button>
        {Array.from({ length: totalPages }, (_, i) => i + 1).map
        ((number) => (
          <Button key={number} size="sm" variant={page === number ? "default" : "outline"} onClick={() => setPage(number)}>
            {number}
          </Button>
        ))}
        <Button size="sm" variant="outline" disabled={page >= totalPages} onClick={() => setPage(page + 1)}>
          下一頁
        </Button>
      </div>
    </div>
  )
}