商品一覧
商品を検索・カテゴリで絞り込み、並び替えて、6件ずつページに分けて見せます。
商品一覧
ここがポイント
- 1
元のデータは変えずに、見せる分だけ作る
全部の商品から、filter(絞り込み)→ sort(並び替え)→ slice(ページ分け)の順に、画面に出す分を作る。元の一覧はそのまま
- 2
条件を変えたら、1ページ目に戻す
2ページ目を見ているときに検索すると、結果が1ページしかなくて空っぽになることがある。条件を変えたら、ページを1に戻す
- 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: "家電", price: 12800, rating: 4.5, color: "#0ea5e9" }, { id: 2, name: "ステンレスボトル", category: "キッチン", price: 2980, rating: 4.2, color: "#10b981" }, { id: 3, name: "リュック 20L", category: "ファッション", price: 7480, rating: 4.7, color: "#f59e0b" }, { id: 4, name: "デスクライト", category: "家電", price: 5980, rating: 4.0, color: "#8b5cf6" }, { id: 5, name: "コーヒーミル", category: "キッチン", price: 4580, rating: 4.6, color: "#ef4444" }, { id: 6, name: "スニーカー", category: "ファッション", price: 9800, rating: 4.3, color: "#ec4899" }, { id: 7, name: "モバイルバッテリー", category: "家電", price: 3980, rating: 4.4, color: "#14b8a6" }, { id: 8, name: "フライパン 26cm", category: "キッチン", price: 3480, rating: 4.1, color: "#f97316" }, { id: 9, name: "キャップ", category: "ファッション", price: 2480, rating: 3.9, color: "#6366f1" }, { id: 10, name: "電気ケトル", category: "家電", price: 4980, rating: 4.5, color: "#84cc16" }, { id: 11, name: "まな板", category: "キッチン", price: 1980, rating: 4.0, color: "#a855f7" }, { id: 12, name: "腕時計", category: "ファッション", price: 15800, rating: 4.8, color: "#64748b" }, ] const categories = ["すべて", "家電", "キッチン", "ファッション"] // 1ページに出す数 const pageSize = 6 // 商品1つ分のカード 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">¥{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) //page が 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> ) }