概要
BtoC向けECショップのクローンサイトを作っています。
今回はカテゴリーメニューを実装してカテゴリー毎に商品表示できるようにします。
前回の記事の続きでページ左側にカテゴリーメニューを追加します。
フロントNext.js バックSpringBoot 商品一覧から商品を選択して買い物カゴに加える動きを実装する
ユースケース
杏林堂ネットスーパーを参考にします。
1.カテゴリーの項目を選択する
2.選択したカテゴリーの商品群が表示される。
※カテゴリーについて
カテゴリーには親カテゴリーと子カテゴリーがある。
親カテゴリーはクリックできない。子カテゴリーのみ商品の切り替えができる
孫カテゴリーは対応していない
開発環境
OS:windows10
バックエンド側:
IDE:IntelliJ Community
spring-boot-starter-parent 2.75
java : 11
データベース
mysql:8.0.29
クライアントソフト: MySQL Workbench
フロントエンド:
IDE:VScode
├── @types/node@18.11.15
├── @types/react-dom@18.0.9
├── @types/react@18.0.26
├── axios@1.2.1
├── eslint-config-next@13.0.6
├── eslint@8.29.0
├── next-auth@4.18.7
├── next@13.0.6
├── react-bootstrap@2.7.0
├── react-dom@18.2.0
├── react@18.2.0
├── styled-components@5.3.6
├── styled-jsx@5.1.1
└── typescript@4.9.4
実装
作成したファイル一覧
ファイル名 | 説明 |
---|---|
index.tsx | 既存のファイル。カテゴリーに関するStateを2つを追加 |
categoryMenu.tsx | カテゴリーメニューのコンポーネント 子カテゴリーの動きはCSSのみで実現 |
category.d.ts | カテゴリー単体の型定義ファイル |
categoryService.ts | Api処理が入っている。axiosを利用 |
productView.tsx | 既存のファイル。カテゴリーが変化したときに商品情報を更新するファイルを追加 |
実装の方針
useEffectを使って、初回時にカテゴリー情報を取得する。
カテゴリーを切り替える時の動き
1.カテゴリーを押すとCategoryId(UseStateになっている)の値が更新される。
2.CategoryIdの値が変化したら商品情報を再取得する。(useEffectを利用)
3.取得した商品情報はproductResponseに代入する。
productResponseはState化されているので、再レンダリングされる。
※CategoryIdはcontextを使って他のコンポーネント渡している。(index.tsxでcreateContextをしてる)
コード部分
index.tsx
import { createContext, useState,useEffect } from "react";
import { CartItemResponse } from "../../../types/cartItem/cartItemResponse";
import { Customer } from "../../../types/customer";
import { ProductResponse } from "../../../types/product/productResponse";
import * as CustomerService from "../../../service/customerService";
import * as CartItemService2 from "../../../service/cartItemServiceV2";
+import * as CategoryService from "../../../service/categoryService";
import ProductView from "./productView";
import CartItemView from "./cartItemView";
+import CategoryMenu from "./categoryMenu";
+import { Category } from "../../../types/category/category";
+import * as productServiceV2 from '../../../service/productServiceV2';
export const mainContext = createContext(0);
// 商品一覧機能と買い物カゴ機能をつなげる方法を確認するページ
// http://127.0.0.1:3000/develop/cartItemAndProduct
export default function Index() {
// 商品情報のhttpレスポンス
const [productResponse, setProductResponse] = useState<ProductResponse>();
// 買い物カゴ情報のhttpレスポンス
const [cartItemsResponse, setCartItemsResponse] = useState<CartItemResponse>();
// カテゴリー情報のhttpレスポンス
+ const [categoryResponse,setCategoryResponse] = useState<Category[]>([]);
+ const[categoryId,setCategoryId] = useState<number>(1);
// 初回時 ログインしてユーザーの買い物かご情報を取得する。
useEffect(() => {
loginAndGetCartItem();
},[]);
// ログインするときに必要なユーザー情報
const loginRequestBody: Customer = {
email: "aaaba@gmail.com",
password: "test",
append: function (arg0: string, inputEmail: string): unknown {
throw new Error("Function not implemented.");
},
};
// ログインしてカートアイテムの情報を取得する
const loginAndGetCartItem = async () => {
const response = await CustomerService.signIn2(loginRequestBody);
localStorage.setItem("accessToken", response);
await getCartItem();
+ await getCategory();
};
// カテゴリーの情報を取得する
+ const getCategory = async () => {
+ const response = await CategoryService.getCategories();
+ await setCategoryResponse(response);
+ }
// ユーザーのカート情報を取得する
const getCartItem = async () => {
let response: CartItemResponse = await CartItemService2.getCartItems2(
localStorage.getItem("accessToken") as unknown as string
);
+ await setCartItemsResponse(response);
};
return(
<div className="Layout">
+ <div className="CategoryMenuPosition">
+ <mainContext.Provider value={{ categoryResponse,setCategoryId}}>
+ <CategoryMenu/>
+ </mainContext.Provider>
+ </div>
<div className="ProductViewPosition">
+ <mainContext.Provider value={{ productResponse, +setProductResponse,setCartItemsResponse,categoryId }}>
<ProductView />
</mainContext.Provider>
</div>
<div className="CartItemViewPosition">
<mainContext.Provider value={{ cartItemsResponse, setCartItemsResponse }}>
<CartItemView />
</mainContext.Provider>
</div>
<style jsx>{`
.Layout{
display:flex;
flex-direction:row;
}
+ .CategorymenuPosition{
+ flex-basis:400px;
}
// ProductViewは中心に置く
.ProductViewPosition{
display:flex;
flex-basis:1000px;
flex-wrap:wrap;
// width:500px;
}
// CartItemViewは右側に置く
.CartItemViewPosition{
//align-self:stretch;
}
`}</style>
</div>
);
}
categoryMenu.tsx
import test from "node:test";
import { JSXElementConstructor, ReactElement, ReactFragment, ReactPortal, useContext, useEffect, useState } from "react";
import { Category } from "../../../types/category/category";
import { CategoryResponse } from '../../../types/category/categoryResponse';
import { mainContext } from './index';
// todo リファクタリング必要
// カテゴリーメニューのコンポーネント
export default function CategoryMenu() {
const {categoryResponse,setCategoryId} : any = useContext(mainContext);
const handlerClick = (onClickedCategoryId: number) => {
console.log(onClickedCategoryId)
setCategoryId(onClickedCategoryId);
}
return (
<div>
カテゴリー覧
<nav>
<ul>
{
categoryResponse.map((category: Category) => {
// 親カテゴリーだった場合表示する。
if (category.parent == null)
return (
// eslint-disable-next-line react/jsx-key
<li className='mainMenu'>
<a href="#">{category.name}</a>
<ul>
{
// 子カテゴリーを表示する。
category.children.map(count => {
const title = categoryResponse.at(count).name;
return(
<li key={count}>
<button className="ButtonLayout" onClick={() => handlerClick(categoryResponse.at(count).id)} >{title}</button>
</li>
)
})
}
</ul>
</li>
);
})
}
</ul>
</nav>
<>
</>
<style jsx>{`
// 親カテゴリの設定 リストの・を消す。
nav ul{
list-style: none;
}
// 子カテゴリのデフォルトのcss設定
// hiddenで非表示にしてleftで位置崩れを防いでいる
nav ul li ul{
position:absolute;
visibility: hidden;
left:0px;
}
// 親カテゴリをマウスオーバーしたときに、子カテゴリを表示する。
nav li.mainMenu:active > ul,
nav li.mainMenu:hover > ul,
{
position:absolute;
z-index:5;
visibility: visible;
list-style: none;
// ↓↓使うコンポーネントによっては、修正の必要があるかも…
padding: 20px;
margin: 20px 10px 100px 0px;
top:0px;
left:100px;
width:180px;
background-color: white;
// ↑↑使うコンポーネントによっては、修正の必要があるかも…
}
.ButtonLayout{
background-color: white;
border: none;
}
`}</style>
</div>
)
}
category.d.ts
/**
* @remarks 商品のカテゴリーを表す型です。
*
* @auther RYA234
*
*/
export type Category = {
/** MySQL側のIDです。*/
id:number
/** リンクとしてつかうので英語が入ります。 */
alias:string
/** html上で表示されるカテゴリー名称です。日本語が入ることを想定してます。 */
name:string
/** サブカテゴリーです。サブカテゴリーを持つ場合対応するidが入ります。サブカテゴリーがない場合はnullが入ります。 */
children:number[];
// 親カテゴリーのidが入ります。親が存在する場合はそのidが入ります。親が存在しない場合はnullが入ります。
parent : number;
}
categoryService.ts
import axios, { AxiosResponse } from 'axios';
import { Category } from '../types/category/category';
import { CategoryResponse } from '../types/category/categoryResponse';
export const getCategories = async () =>{
return await axios.get(`http://127.0.0.1:5000/api/category/all`)
.then((response)=>{
return response.data as AxiosResponse<Category[]>;
})
.catch((error)=>{
return error;
});
};
productView.tsx
import { ProductResponse } from '../../../types/product/productResponse';
import { createContext, SetStateAction, useContext, useEffect, useState } from 'react';
import { mainContext } from '.';
import * as productServiceV2 from '../../../service/productServiceV2';
import { Product } from '../../../types/product/product';
import ProductContentV2 from './productContentV2';
import PaginationV2 from './paginationV2';
export default function ProductView(){
// 商品情報のhttpレスポンス
+ const {productResponse,setProductResponse,categoryId}:any = useContext(mainContext)
// ページ番号
const [pageNo,setPageNo] = useState<number>(1)
// 1ページあたりの商品数
const pageSize : number = 10;
- // 検証のためにカテゴリーIDを1に固定
- // const categoryId : number = 1;
// pageNoが変わるとgetProductByCategoryが実行される
useEffect(() => {
getProductByCategory()
},[pageNo])
+ // categoryIdが変わるとgetProductByCategoryが実行されてpageNoを1に戻す。
+ useEffect(() => {
+ getProductOnChangeCategory()
+ },[categoryId])
// ページネーション化されたカテゴリー毎の商品情報たちを取得する。
const getProductByCategory = async () => {
const response : ProductResponse= await productServiceV2.getProductsByCategoryId2(pageNo - 1,pageSize,categoryId)
+ await setProductResponse(response)
+ }
+ const getProductOnChangeCategory = async () => {
+ const response : ProductResponse= await productServiceV2.getProductsByCategoryId2(0,pageSize,categoryId)
+ await setPageNo(1);
+ await setProductResponse(response)
}
return(
<>
<div className="Layout">
{
productResponse?.content.map((product: Product,index:number)=>{
return(
<div className="Child" key={index}>
<ProductContentV2 productName={productResponse.content[index].name} priceWithoutTax={productResponse.content[index].price} priceIncludingTax={(productResponse.content[index].price * (1 + productResponse.content[index].taxRate)).toFixed() as string} imageURL={'/sampleProduct1.JPG'} id={productResponse.content[index].id}/>
</div>
)
})
}
</div>
<div className="PaginationPosition">
<PaginationV2 totalPage={productResponse?.totalPages as number} pageNo={pageNo} setPageNo={setPageNo}/>
</div>
<style jsx>{`
// 大枠
.Layout{
display:flex;
flex-wrap:wrap;
background-color:white;
}
// 子の幅を指定 これしないとレイアウトが崩れる
.Child{
flex-basis:200px;
//align-self:stretch;
}
.PaginationPosition{
margin:auto;
width:50%
}
`}</style>
</>
)
}
実装後の確認
カテゴリーボタンを押して商品一覧が切り替わることを確認
下gifアニメはその時の様子です。初期のカテゴリーは肉から魚、野菜と切り替えています。
参考
参考にしたサイト(杏林堂ネットスーパー)
カテゴリーメニューの実装方法 cssのみで実装できる(当時はcssの擬似クラス(hoverとか)がわからなくて質問した)