Use Cases

Building an Image Gallery with Claude Code

Learn about building an image gallery using Claude Code. Includes practical code examples.

画像ギャラリーをClaude Codeで構築する

画像ギャラリーはポートフォリオ、ECサイト、写真共有アプリなど、多くの場面で必要になるコンポーネントです。Claude Codeを使えば、マソンリーレイアウト、ライトボックス、遅延読み込みを備えた高品質なギャラリーを効率的に構築できます。

ギャラリーの要件を指示する

> 画像ギャラリーコンポーネントを作って。
> マソンリーレイアウト、ライトボックス表示、
> 遅延読み込み、カテゴリフィルタリングを実装して。
> レスポンシブ対応で。

マソンリーレイアウト

// src/components/MasonryGallery.tsx
'use client';
import { useState, useMemo } from 'react';

interface GalleryImage {
  id: string;
  src: string;
  alt: string;
  width: number;
  height: number;
  category: string;
}

interface Props {
  images: GalleryImage[];
  columns?: number;
}

export function MasonryGallery({ images, columns = 3 }: Props) {
  const [selectedCategory, setSelectedCategory] = useState<string | null>(null);
  const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);

  const categories = useMemo(
    () => [...new Set(images.map((img) => img.category))],
    [images]
  );

  const filteredImages = useMemo(
    () => selectedCategory
      ? images.filter((img) => img.category === selectedCategory)
      : images,
    [images, selectedCategory]
  );

  // カラムに画像を分配
  const columnImages = useMemo(() => {
    const cols: GalleryImage[][] = Array.from({ length: columns }, () => []);
    filteredImages.forEach((img, i) => {
      cols[i % columns].push(img);
    });
    return cols;
  }, [filteredImages, columns]);

  return (
    <div>
      {/* カテゴリフィルター */}
      <div className="flex gap-2 mb-6 flex-wrap">
        <button
          onClick={() => setSelectedCategory(null)}
          className={`px-4 py-2 rounded-full text-sm ${
            !selectedCategory ? 'bg-blue-600 text-white' : 'bg-gray-100 dark:bg-gray-800'
          }`}
        >
          すべて
        </button>
        {categories.map((cat) => (
          <button
            key={cat}
            onClick={() => setSelectedCategory(cat)}
            className={`px-4 py-2 rounded-full text-sm ${
              selectedCategory === cat ? 'bg-blue-600 text-white' : 'bg-gray-100 dark:bg-gray-800 dark:text-gray-300'
            }`}
          >
            {cat}
          </button>
        ))}
      </div>

      {/* マソンリーグリッド */}
      <div className="flex gap-4">
        {columnImages.map((col, colIndex) => (
          <div key={colIndex} className="flex-1 flex flex-col gap-4">
            {col.map((img) => (
              <div
                key={img.id}
                onClick={() => setLightboxIndex(filteredImages.indexOf(img))}
                className="cursor-pointer overflow-hidden rounded-lg hover:opacity-90 transition"
              >
                <img
                  src={img.src}
                  alt={img.alt}
                  loading="lazy"
                  className="w-full h-auto"
                  style={{ aspectRatio: `${img.width}/${img.height}` }}
                />
              </div>
            ))}
          </div>
        ))}
      </div>

      {/* ライトボックス */}
      {lightboxIndex !== null && (
        <Lightbox
          images={filteredImages}
          currentIndex={lightboxIndex}
          onClose={() => setLightboxIndex(null)}
          onNavigate={setLightboxIndex}
        />
      )}
    </div>
  );
}

ライトボックスコンポーネント

// src/components/Lightbox.tsx
'use client';
import { useEffect, useCallback } from 'react';

interface Props {
  images: GalleryImage[];
  currentIndex: number;
  onClose: () => void;
  onNavigate: (index: number) => void;
}

export function Lightbox({ images, currentIndex, onClose, onNavigate }: Props) {
  const current = images[currentIndex];

  const handleKeyDown = useCallback((e: KeyboardEvent) => {
    switch (e.key) {
      case 'Escape':
        onClose();
        break;
      case 'ArrowLeft':
        if (currentIndex > 0) onNavigate(currentIndex - 1);
        break;
      case 'ArrowRight':
        if (currentIndex < images.length - 1) onNavigate(currentIndex + 1);
        break;
    }
  }, [currentIndex, images.length, onClose, onNavigate]);

  useEffect(() => {
    document.addEventListener('keydown', handleKeyDown);
    document.body.style.overflow = 'hidden';
    return () => {
      document.removeEventListener('keydown', handleKeyDown);
      document.body.style.overflow = '';
    };
  }, [handleKeyDown]);

  return (
    <div
      className="fixed inset-0 bg-black/90 z-50 flex items-center justify-center"
      onClick={onClose}
    >
      <button onClick={onClose} className="absolute top-4 right-4 text-white text-3xl z-10">
        &times;
      </button>

      {currentIndex > 0 && (
        <button
          onClick={(e) => { e.stopPropagation(); onNavigate(currentIndex - 1); }}
          className="absolute left-4 text-white text-4xl hover:text-gray-300"
        >
          &#8249;
        </button>
      )}

      <img
        src={current.src}
        alt={current.alt}
        className="max-h-[90vh] max-w-[90vw] object-contain"
        onClick={(e) => e.stopPropagation()}
      />

      {currentIndex < images.length - 1 && (
        <button
          onClick={(e) => { e.stopPropagation(); onNavigate(currentIndex + 1); }}
          className="absolute right-4 text-white text-4xl hover:text-gray-300"
        >
          &#8250;
        </button>
      )}

      <div className="absolute bottom-4 text-white text-sm">
        {currentIndex + 1} / {images.length}
      </div>
    </div>
  );
}

パフォーマンス最適化

画像のパフォーマンスを高めるために、以下のポイントをClaude Codeに追加で依頼しましょう。

  • srcset属性: デバイス幅に応じた画像サイズの提供
  • BlurHash: 読み込み中のプレースホルダー表示
  • WebP/AVIF: 次世代フォーマットの自動変換
  • CDN配信: CloudflareやCloudFrontでのキャッシュ

関連記事

画像処理全般は画像処理の実装、パフォーマンス最適化はパフォーマンス最適化ガイドも参考にしてください。

画像最適化にはCloudinary(cloudinary.com)のようなサービスの活用も検討するとよいでしょう。

#Claude Code #画像ギャラリー #React #responsive #performance