完成イメージ

このチュートリアルでは、以下のような機能を持つギャラリーを作成します

ソースファイル

imageGallery.tsx

// src/components/imageGallery.tsx
'use client'
import { useState } from 'react'
import { X, ChevronLeft, ChevronRight } from 'lucide-react'  // Lucide Reactのアイコンを使用
import styles from '@/styles/imageGallery.module.css'

const ImageGallery = () => {
  const [selectedImage, setSelectedImage] = useState<string | null>(null);
  const [currentIndex, setCurrentIndex] = useState<number>(0);

  const images = [
    "https://bizlabo.site/sample-img/column1.jpg",
    "https://bizlabo.site/sample-img/column2.jpg",
    "https://bizlabo.site/sample-img/column3.jpg",
    "https://bizlabo.site/sample-img/column4.jpg",
    "https://bizlabo.site/sample-img/column5.jpg",
    "https://bizlabo.site/sample-img/column6.jpg",
    "https://bizlabo.site/sample-img/column7.jpg",
    "https://bizlabo.site/sample-img/column8.jpg",
  ];

  // 引数はReact.MouseEvent型として、イベントオブジェクトを受け取り、イベント伝播を停止する
  const handleNext = (e: React.MouseEvent) => {
    e.stopPropagation(); // 親要素へのイベント伝播を停止
    setCurrentIndex((prev) => (prev + 1) % images.length); // 画像の数で割った余りを新しいインデックスとするため、画像の数を超えた場合は0に戻る
    setSelectedImage(images[(currentIndex + 1) % images.length]);
  };

  const handlePrev = (e: React.MouseEvent) => {
    e.stopPropagation();
    setCurrentIndex((prev) => (prev - 1 + images.length) % images.length);
    setSelectedImage(images[(currentIndex - 1 + images.length) % images.length]);
  };

  return (
    <div className={styles.container}>
      <div className={styles.grid}>
        {images.map((image, index) => (
          <img
            key={index}
            src={image}
            alt={`画像 ${index + 1}`}
            className={styles.image}
            onClick={() => {
              setSelectedImage(image);
              setCurrentIndex(index);
            }}
          />
        ))}
      </div>

      {selectedImage && (
        <div className={styles.modal} onClick={() => setSelectedImage(null)}>
          <div className={styles.modalContent}>
            <button className={styles.closeButton} onClick={() => setSelectedImage(null)}>
              <X size={32} />  {/* Xアイコン */}
            </button>
            <button className={styles.prevButton} onClick={handlePrev}>
              <ChevronLeft size={40} />  {/* 左矢印アイコン */}
            </button>
            <img
              src={selectedImage}
              alt="拡大画像"
              className={styles.modalImage}
              onClick={(e) => e.stopPropagation()}
            />
            <button className={styles.nextButton} onClick={handleNext}>
              <ChevronRight size={40} />  {/* 右矢印アイコン */}
            </button>
          </div>
        </div>
      )}
    </div>
  );
};

export default ImageGallery;

imageGallery.module.css

.modal {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.6);
  display: flex;
  align-items: center;
  justify-content: center;
}

.modalContent {
  position: relative;
  max-width: 90vw;
  max-height: 90vh;
}

.closeButton {
  position: absolute;
  top: -40px; /* 画像の上に配置 */
  right: -40px; /* 画像の右に配置 */
  background: none;
  border: none;
  color: white;
  cursor: pointer;
  padding: 8px;
  z-index: 1;
  transition: all 0.2s;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.closeButton:hover {
  background-color: rgba(255, 255, 255, 0.1);
  transform: scale(1.1);
}

.prevButton,
.nextButton {
  position: absolute;
  top: 50%;
  transform: translateY(-50%);
  background: none;
  border: none;
  color: white;
  cursor: pointer;
  padding: 16px;
  z-index: 1;
  transition: all 0.2s;
  border-radius: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.prevButton:hover,
.nextButton:hover {
  background-color: rgba(255, 255, 255, 0.1);
  transform: translateY(-50%) scale(1.1);
}

.prevButton {
  left: -60px;
}

.nextButton {
  right: -60px;
}

/* 既存のスタイル */
.container {
  padding: 1rem;
}

.grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 1rem;
}

.image {
  width: 100%;
  height: auto;
  object-fit: contain;
  cursor: pointer;
  transition: transform 0.2s;
}

.image:hover {
  transform: scale(1.05);
}

.modalImage {
  max-height: 90vh;
  max-width: 90vw;
  object-fit: contain;
}

@media (min-width: 768px) {
  .grid {
    grid-template-columns: repeat(3, 1fr);
  }
}

@media (min-width: 1024px) {
  .grid {
    grid-template-columns: repeat(4, 1fr);
  }
}

map関数について

map関数の特徴

  1. 元の配列の各要素に対して処理を行う
  2. 新しい配列を返す
map関数の重要な2つの特徴 元の配列: [1, 2, 3] 特徴1: 各要素に対して同じ処理を適用 1 → multiply by 2 → 2 2 → multiply by 2 → 4 3 → multiply by 2 → 6 特徴2: 新しい配列を返す(元の配列は変更されない) 元の配列: [1, 2, 3] (変更されない) 新しい配列: [2, 4, 6] (処理結果の新しい配列)