Next.js+FirebaseでレスポンシブにInstagram風に画像一覧表示

Insta風画像一覧

前回、

React+Next.js+TypeScript+firebaseで画像アップローダを作った - あおいろメモ

で画像アップローダを作ったので、今回は画像一覧表示を作った。

ストレージ

FireabaseのFirestoreを使った。

Firestore内のデータ一覧をlistAll()で取得する。

List files with Cloud Storage on Web  |  Firebase

  • 最近(2021/9/1)Firebaseがv8からv9に代わり、大きく記法が変わっているので注意
  • for文の中の中のPromiseがすべて終わってから処理したい処理は次の記事参照

Promiseの直列処理をループする(完了後に実行したい処理もある) - Qiita

バックエンド

React + Next.js

またnext/imageモジュールのImageタグを使った。通常のimgタグに比べ、いろいろ利点があるらしい

Next.js の Image コンポーネントで画像を表示する (next/image)|まくろぐ

また、アップロードファイルの拡張子をチェックして、画像以外取得しないようにした。

JavaScriptでアップロードするファイルの拡張子をチェックするサンプル

そしてInstagram風に画像を一覧表示するImageListコンポーネントを作成した。 このコンポーネントはurlの配列を受け取ってli要素を生成し、instagram風に並べる。

コンポーネントに配列を渡して内部で要素の配列を作成し、表示する方法については以下参照

React コンポーネントのプロパティで配列データを渡す|まくろぐ

レイアウト

まずは要素を並べるなら基本のflexを使う。下記記事参照。

今覚えたい!エンジニアのための CSS の基礎講座 〜Flexbox レイアウト編〜 | 株式会社ヌーラボ(Nulab inc.)

そして、要素を正方形にして、レスポンシブに対応させて並べるのは意外と複雑。 下の記事にも書いてある通り、いろいろな方法があるが、親要素の::before疑似要素についてpadding-top:100%にする方法が1番良い。

CSSで正方形を作る方法!レスポンシブに対応させるには? | 向壁虚造

そしてCSS Moduleでスタイルを適用した。

Next.js でコンポーネント単位の CSS を作成する (CSS Modules)|まくろぐ

また、scssを導入した

[SCSS]便利な&(アンパサンド)の使い方メモ - Qiita

ソース

Home.module.scss

// 正方形の作り方についてはhttps://kouhekikyozou.com/css_square
.image_list {
  display: flex;
  justify-content: flex-start;
  flex-wrap: wrap;
  width: 100%;
  max-width: 900px;

  li {
    position: relative; // 浮かせたimg要素の基準
    width: calc((100% - 20px) / 3); // 3カラムで表示させる
    margin-top: 10px;
    list-style-type: none;

    &::before {
      content: "";
      display: block;
      padding-top: 100%; 
    }

    &>div {
      position: absolute; // 浮かせる
      width: 90%; // 親要素の90%
      height: 90%; // 親要素の90%
      top: 5%;
      left: 5%;
    }
  }
}

index.tsx

import Head from 'next/head'
import styles from '../../styles/Home.module.scss'
import Image from 'next/image'

import fireabaseApp from "../../components/fire"
import { getStorage, ref, listAll, getDownloadURL } from 'firebase/storage'
import { useEffect, useState } from 'react'

const storage = getStorage(fireabaseApp)

// 拡張子チェック。ここを参照
// https://blog.ver001.com/javascript-get-extension/
function getExt(filename: string) {
  var pos = filename.lastIndexOf('.');
  if (pos === -1) return '';
  return filename.slice(pos + 1);
}

//許可する拡張子
var allow_exts = new Array('jpg', 'jpeg', 'png');

//ファイル名の拡張子が許可されているか確認する関数
function checkExt(filename: string) {
  //比較のため小文字にする
  var ext = getExt(filename).toLowerCase();
  //許可する拡張子の一覧(allow_exts)から対象の拡張子があるか確認する
  if (allow_exts.indexOf(ext) === -1) return false;
  return true;
}

// 配列データをコンポーネントに渡すやり方はここを参照
// https://maku.blog/p/av9mxak/
// idとurlを一緒にしたものの型
interface ImageInfo {
  id: number;
  url: string;
}

// ImageListに渡すpropsの型
interface ImageListProps {
  imlist: ImageInfo[]
}

const Index = () => {
  // useStateの初期値は必ず[]を指定。
  // useState()のままだと、undefinedが初期値となり、
  // 型が一致しないといわれてしまう。
  const [imlist, setImlist] = useState<ImageInfo[]>([])

  const store_url = () => {
    let temp_imlist: ImageInfo[] = [];
    let url = ""
    var listRef = ref(storage, "images")
    listAll(listRef)
      .then((res: any) => {
        let tasks: Array<any> = [];
        let count = 0;
        res.items.forEach((itemRef: any) => {
          // もし画像ファイルだったら、
          if (checkExt(itemRef.name)) {
            // 画像URLを取得、保存するPromiseを生成し、
            // そのPromiseをひとまとまりにする。
            tasks.push(getDownloadURL(itemRef)
              .then((fireBaseUrl: string) => {
                temp_imlist.push(
                  {
                    id: count,
                    url: fireBaseUrl
                  }
                )
                count++;
              })
            )
          }
        })
        // 複数のpromiseを待つ
        // https://qiita.com/saka212/items/ff61a6de9c3e19810c5d
        Promise.all(tasks).then(() => {
          setImlist(temp_imlist)
        })
      })
  }

  useEffect(() => {
    // ページを読み込んだ時のみで画像urlを取得
    store_url()
  }, [])

  return (
    <div>
      <Head>
        <title>TS Test</title>
      </Head>
      {/* ImageListコンポーネントに画像URLの配列を渡す */}
      <ImageList imlist={imlist} />
    </div>
  )
}

export default Index

// ImageListコンポーネントはurlの配列を受け取って
// li要素を生成し、instagram風に並べるコンポーネント
const ImageList = ({ imlist }: ImageListProps) => {
  // imlistプロパティの要素数が 0 であれば何も描画しない
  if (imlist.length == 0) return null;

  // imlistからitemを取り出し、item.idとitem.urlを組み込んで
  // li要素配列を生成する。
  const listItems = imlist.map((item: ImageInfo) =>
    <li key={item.id}>
      <div><Image src={item.url} objectFit="contain" layout="fill" alt="" /></div>
    </li>
  );

  return <div>
    <ul className={styles.image_list}>
      {listItems}
    </ul>
  </div>
}