3
2

More than 1 year has passed since last update.

フロントNext.js バックSpringBoot カテゴリーメニューを実装する

Last updated at Posted at 2023-02-20

概要

BtoC向けECショップのクローンサイトを作っています。
今回はカテゴリーメニューを実装してカテゴリー毎に商品表示できるようにします。
前回の記事の続きでページ左側にカテゴリーメニューを追加します。

フロントNext.js バックSpringBoot 商品一覧から商品を選択して買い物カゴに加える動きを実装する

ユースケース

杏林堂ネットスーパーを参考にします。

product_category.gif

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

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

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

category.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

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

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アニメはその時の様子です。初期のカテゴリーは肉から魚、野菜と切り替えています。
product_category_sample.gif

参考

参考にしたサイト(杏林堂ネットスーパー)

カテゴリーメニューの実装方法 cssのみで実装できる(当時はcssの擬似クラス(hoverとか)がわからなくて質問した)

3
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
2