Next.js+FirebaseでレスポンシブにInstagram風に画像一覧表示
前回、
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> }