📷

Next.js × microCMSで写真ギャラリー

2023/09/24に公開

先日こんな感じで自分の写真をまとめるべくギャラリーサイトを作りました。
(2023/9/26にポートフォリオサイトと合体しました)

https://gohshi54.com/photos

1番の目的は自分の写真上達度を一目でわかりやすくした、Instagramのようなサイトが欲しいというところからでした。

「microCMS 写真ギャラリー」と検索したときに上位表示されたこちらのひまらつさんの記事を参考に、Next.jsで作成しました。

https://zenn.dev/himara2/articles/ebda12f84aa9df

※ pagesディレクトリ構成で作っております。

どのように作っていったのかを解説していきたいと思います。

microCMS側の設定

microCMS側の設定は上記のひまらつさんの記事と全く同じAPIスキーマの設定をしております。

コンテンツ数も2つなで無料の枠内で利用することができます。

microCMSからデータフェッチ

client.tsx
import { createClient } from "microcms-js-sdk";
import { PictureType, AlbumType } from "@/types/types";

if (!process.env.API_KEY) {
    throw new Error('API_KEYが設定されていません。');
}

export const client = createClient({
    serviceDomain: 'xxxxxxxxx',
    apiKey: process.env.API_KEY,
});

export const fetchPicture = async () => {
    try {
        return await client.get<PictureType[]>({ endpoint: 'picture' });
    } catch (error) {
        console.error('Error fetching picture', error);
        throw error;
    }
}

export const fetchAlbum = async () => {
    try {
        return await client.get<AlbumType[]>({ endpoint: 'album' });
    } catch (error) {
        console.error('Error fetching album', error);
        throw error;
    }
}

microCMSよりデータをフェッチします。

TypeScript で書いているため、別にTypeの定義もしています。

"xxxxxxxx"部分はご自身のサービスドメインをご入力してください。

あとは.envファイルの方でAPIキーを登録してあります。

ギャラリーを作る

index.tsx
import React, { useEffect, useRef, useState } from 'react';
import Link from 'next/link';
import { gsap } from 'gsap';
import Image from 'next/image';
import { client } from '@/utils/client';
import AlbumSelector from '@/components/AlbumSelector';
import { AlbumType, PictureType } from '@/types/types';

export const getStaticProps = async () => {
  try {
    const picture = await client.get({ endpoint: 'picture', queries: { limit: 100 } });
    const album = await client.get({ endpoint: 'album', queries: { limit: 100 } });
    return {
      props: {
        picture: picture.contents,
        album: album.contents,
      },
      revalidate: 60,
    };
  } catch (error) {
    console.error(error);
    return {
      props: {
        picture: [],
        album: [],
      },
    };
  }
};

export default function Home({ picture, album }: { picture: PictureType[], album: AlbumType[] }) {
  const [showModal, setShowModal] = useState(false);
  const [selectedImage, setSelectedImage] = useState('');
  const [selectedAlbum, setSelectedAlbum] = useState('all');
  const imageRefs = useRef<(HTMLDivElement | null)[]>([]);
  const modalRef = useRef<HTMLDivElement | null>(null);

  useEffect(() => {
    const validImageRefs = imageRefs.current.filter(ref => ref !== null);
    if (validImageRefs.length > 0) {
      gsap.killTweensOf(validImageRefs);
      gsap.fromTo(validImageRefs,
        { autoAlpha: 0, y: 50 },
        {
          autoAlpha: 1,
          y: 0,
          stagger: 0.2,
          duration: 0.5,
        }
      );
    }
  }, [selectedAlbum]);

  useEffect(() => {
    if (modalRef.current) {
      gsap.killTweensOf(modalRef.current);
      if (showModal) {
        gsap.fromTo(modalRef.current,
          { autoAlpha: 0, y: 100 },
          {
            autoAlpha: 1,
            y: 0,
            duration: 0.8,
          }
        );
      } else {
        gsap.to(modalRef.current,
          {
            autoAlpha: 0,
            y: 100,
            duration: 0.8,
          }
        );
      }
    }
  }, [showModal]);

  const handleImageClick = (url: string) => {
    setSelectedImage(url);
    setShowModal(true);
  };

  const filteredPictures = selectedAlbum === 'all' ? picture : picture.filter(pic => pic.album.name === selectedAlbum);

  return (
    <>
      <AlbumSelector album={album} selectedAlbum={selectedAlbum} setSelectedAlbum={setSelectedAlbum} />
      <main className='bg-black min-h-screen p-6 md:p-8 lg:p-12'>
        <div className='flex flex-col items-center justify-center'>
          <div className='grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5'>
            {filteredPictures.map((pic, index) => (
              <div key={pic.id} ref={el => imageRefs.current[index] = el}>
                <div className='cursor-pointer hover:opacity-75 relative' onClick={() => handleImageClick(pic.photo.url)}>
                  <Image src={pic.photo.url} alt={pic.caption} width={500} height={500} />
                  <div className="absolute inset-0 flex items-center justify-center opacity-0 hover:opacity-100 transition-opacity duration-300">
                    <span className="text-white">{pic.caption}</span>
                  </div>
                </div>
              </div>
            ))}

            {showModal && (
              <div className="fixed inset-0 flex items-center justify-center bg-black text-white" ref={modalRef}>
                <div className="p-4 text-center">
                  <Image className="mb-4" src={selectedImage} alt="Selected" width={800} height={450} />
                  <button className=' text-glay-800 font- py-4' onClick={() => setShowModal(false)}>
                    Close
                  </button>
                </div>
              </div>
            )}
          </div>
        </div>
      </main>
      <footer className='bg-black text-white text-center p-4'>
        <Link
          href='https://gohshi54.com'
          target='_blank'
          rel='noopener noreferrer'
        >
          <Image
            className='mx-auto'
            src='/logo.jpg'
            alt='logo'
            width={100}
            height={100} />
        </Link>
      </footer>
    </>
  );
}

あとはトップページに写真を次々と投稿していくメソッドとスタイリングをするだけです。

今回はSSRでmicroCMSより投稿した写真を取得していきます。

先日microCMSさんよりこのような発表がありました。

https://blog.microcms.io/api-limit-change/

一応この点も踏まえてリミットは100に設定する感じにしております。

また画像が増えたり、無いとは思いますが訪問者がめちゃ増えるなんてことがあったら修正予定です。

スタイリングに関してはTailwindCSSでさくっとスタイリングしてます。

各写真をクリックするとモーダルで写真が大きく見れるようにしました。

(リファクタリングしておらず、Footer部分も一緒に入っています。申し訳ありません。)

あとはGSAPを使ってページが表示したときに写真が1枚1枚ふわっと出るような感じにしてみました。

いずれ写真が増えて鬱陶しそうだったら消す予定ではいますが笑

アルバム選択機能

最後に写真並べるだけだとつまらないので、せっかくですしアルバムごと写真表示できる機能も作成しました。

AlbumSelector.tsx
import React from 'react';
import FormControl from '@mui/material/FormControl';
import InputLabel from '@mui/material/InputLabel';
import NativeSelect from '@mui/material/NativeSelect';
import { AlbumSelectorProps } from '@/types/types';

const AlbumSelector: React.FC<AlbumSelectorProps> = ({ album, selectedAlbum, setSelectedAlbum }) => {
    return (
        <header className='flex justify-center items-center my-4'>
            <FormControl>
                <InputLabel htmlFor="album-native-select">Select Album</InputLabel>
                <NativeSelect
                    value={selectedAlbum}
                    onChange={(e) => setSelectedAlbum(e.target.value)}
                    inputProps={{
                        name: 'album',
                        id: 'album-native-select',
                    }}
                >
                    <option value="all">All</option>
                    {album.map((alb, index) => (
                        <option key={alb.id} value={alb.name}>
                            {alb.name}
                        </option>

                    ))}
                </NativeSelect>
            </FormControl>
        </header>
    );
};

export default AlbumSelector;

そこまでこだわりはないのでマテリアルUIでさくっとプルダウンメニューを作っています。

これでアルバムごとに写真が表示できるのでいつでも見たいジャンルに簡単に飛ぶことができます。

まとめ

こんな感じで今回はさくっと約1日にもかけずに簡単な写真ギャラリーができました。

先日のブログでも書きましたが、あくまで自分の振り返りようなので特にパフォーマンス等も気にしていない

テキトーなアプリケーションですが、ちょっとだけ見てもらえると嬉しいです。

ギャラリーサイト部分だけのGitHubのリポジトリはこちらです。
https://github.com/Gohshi0514/Goshi-Photo-gallery

Discussion