View on GitHub

評価値コンポーネントの作成

Home

評価値コンポーネントの作成

画像ファイルの準備

  1. 下記ファイルをダウンロードする。
frontend$ mv ~/Downloads/star*.png public/img/
frontend$ ls public/img/
dummy_poster.png  star_00.png  star_01.png  star_10.png  star_11.png

データ型

src/frontend/src/types/data.d.ts

......
/**
 * 映画モデル
 */
export type Movie = {
  movie_id: number;
  title: string;
  year: number;
  genres: Genre[];
  imdb_id: number;
  tmdb_id: number;
  rating: Rating | null; // <- 追加
};

// ↓追加
/**
 * 評価モデル
 */
export type Rating = {
  id: string;
  user_id: string;
  movie_id: number;
  rating: number;
  rated_at: string;
};
// ↑追加

定数の定義

src/frontend/src/constants/styles.ts

/**
 * スタイル関連の定数
 */
export const STYLES = {
  ......
  // 映画(詳細)カード関連
  ......
  MOVIE_DETAIL_TAG_GENRE: `rounded bg-gray-100 px-3 py-1 text-sm`,
  MOVIE_DETAIL_STAR_WIDTH: 48, // <- 追加

  // ↓追加
  // 評価値関連
  STAR_BUTTON: `cursor-pointer rounded-lg bg-white hover:bg-indigo-100`,
  STAR_IMAGE: `opacity-75 hover:opacity-100 active:scale-125 active:opacity-100`,
  STAR_RATING: `flex`,
  // ↑追加
} as const;

コンポーネント

スターコンポーネント

src/frontend/src/app/components/ui/rating/Star.tsx

"use client";

import { useCallback } from "react";
import Image from "next/image";

import { STYLES } from "@/constants";

interface Props {
  index: number;
  width: number;
  rating: number;
  setRating: Function;
}

const Star = (props: Props) => {
  const handleRatingClick = useCallback(() => {
    const rating = (props.index + 1) / 2;
    props.setRating(rating);
  }, [props.setRating]);

  return (
    <>
      <button
        className={`${STYLES.STAR_BUTTON}`}
        onClick={() => handleRatingClick()}
      >
        <Image
          className={`${STYLES.STAR_IMAGE}`}
          src={`/img/star_${props.index % 2}${props.index < props.rating * 2 ? 1 : 0}.png`}
          alt=""
          width={props.width / 2}
          height={props.width}
        />
      </button>
    </>
  );
};

export default Star;

評価値コンポーネント

src/frontend/src/app/components/ui/rating/StarRating.tsx

"use client";

import { useState } from "react";

import { STYLES } from "@/constants";

import Star from "./Star";

interface Props {
  starWidth: number;
  rating: number;
}

const StarRating = (props: Props) => {
  const [rating, setRating] = useState<number>(props.rating);

  return (
    <>
      <div className={`${STYLES.STAR_RATING}`}>
        {(function () {
          const stars = [];
          for (let i = 0; i < 10; i++) {
            stars.push(
              <Star
                key={i}
                index={i}
                width={props.starWidth}
                rating={rating}
                setRating={setRating}
              />,
            );
          }
          return <div>{stars}</div>;
        })()}
      </div>
    </>
  );
};

export default StarRating;

映画詳細カードコンポーネント

src/frontend/src/app/components/ui/card/CardMovieDetail.tsx

......
import { Movie, User } from "@/types/data"; // <- Userを追加
import { MESSAGES, STYLES, ERROR_MESSAGES } from "@/constants";
import getMovie from "@/api/movies/getMovie";
import getMyAccount from "@/api/auth/getMyAccount"; // <- 追加

import Loading from "../Loading";
import StarRating from "../rating/StarRating"; // <- 追加
......
/**
 * 映画(詳細)カードコンポーネント
 */
const CardMovieDetail = (props: Props) => {
  const [loading, setLoading] = useState(true);
  const [loadingUser, setLoadingUser] = useState(true); // <- 追加
  const [user, setUser] = useState<User | null>(null); // <- 追加
  const [movie, setMovie] = useState<Movie | null>(null);
  ......
  useEffect(() => {
    const load = async () => {
      // ↓追加
      try {
        const user_ = await getMyAccount();
        setUser(user_);
      } catch (e) {
        setUser(null);
      } finally {
        setLoadingUser(false);
      }
      // ↑追加

      try {
        const { movie: movie_ } = await getMovie(props.movieId);
        setMovie(movie_);
      } catch (e) {
        console.error(ERROR_MESSAGES.MOVIE_GET_FAILED, e);
        setMovie(null);
      } finally {
        setLoading(false);
      }
    };
    load();
  }, []);
  ......
  return (
    <div className={`${STYLES.MOVIE_DETAIL_PAGE}`}>
      <article className={`${STYLES.MOVIE_DETAIL_CARD}`} key={movie.movie_id}>
        ...(略)...
        {/* ↓追加 */}
        {!loadingUser && user && (
          <StarRating starWidth={STYLES.MOVIE_DETAIL_STAR_WIDTH} rating={movie.rating?.rating!} />
        )}
        {/* ↑追加 */}
      </article>
    </div>
  );
};

export default CardMovieDetail;

実行確認

ブラウザで下記 URL にアクセスしてください。

サインインすると、映画詳細カードに五つのスターで構成される評価値コンポーネントが表示されます。評価値コンポーネント上で任意の評価値をクリックすると、その評価値までのスターが黄色く表示されます。ただし、現時点では、評価値は記憶されないため、ブラウザを更新すると元に戻ります。また、現時点では、ユーザ情報は評価値コンポーネントの表示/非表示の判定のために参照しているだけですので、ユーザ依存の評価値は取得できません。