🥢

飲食店専用メモアプリChopsticksを開発しました

2024/01/25に公開

Chopsticksとは

chopsticks.jpg

Chopsticksとはお気に入りのお店を写真付きで記録して管理できるシンプルな飲食店専用メモアプリです!
タグの付与と、タグごとの絞り込みができて、お店選びの際に役立ちます🎉

幹事や、二次会のお店選びの際などで素敵なお店をすっと提案できるよう、私はiPhoneのメモで
お気に入りのお店を記録していました。
ですが飲食店を管理するにはUIがイマイチだったり絞り込みできなかったりと
何かと不便だったので、専用のアプリで管理することにしました。
類似アプリでは無駄に多機能だったりちょっと使いづらかったりと適したアプリが見つからなかったので自分で開発することにしました🥲

基本的に自分が使うことを想定しているので、スマホレイアウトでios Safariでのみ確認しています!

機能

  • メールアドレスとパスワードによる認証
  • 店舗情報の投稿、編集、削除、画像アップロード
  • タグの付与と絞り込み

開発しているとレビューとか共有とかそういう機能をついついいっぱい付けたくなりますが、操作・選択負担の少ない、必要最低限の機能だけでシンプルなものを開発しました。

技術・環境

  • React
  • TypeScript
  • ChakraUI
  • Firebase
  • GithubActions

工夫した点

heic画像のアップロード

iPhoneで撮影した画像の拡張子はheicになりますがこの拡張子が、Safariしか対応していなかったりライブラリでもサポートされていなかったりと、何かと不便です。

https://caniuse.com/?search=heic

スマホで撮影したものをそのままアップロードさせるためにheicをjpgに変換する処理を実装しました。
いろいろ調べたところ、現状下記、heic2anyというライブラリしかなさそうでした。

https://alexcorvi.github.io/heic2any/

下記のようなコードで、jpgに変換しています。

import { useEffect, useState } from 'react'
import heic2any from 'heic2any'
import { useToast } from '@chakra-ui/react'

export const useGetImageBlob = (
  { file }: { file: File | null },
  setIsLoading: (isLoading: boolean) => void
) => {
  const [imageBlob, setImageBlob] = useState<Blob | null>(null)
  const toast = useToast()

  useEffect(() => {
    if (!file) {
      setImageBlob(null)
      setIsLoading(false)
      return
    }

    setIsLoading(true)

    if (file.type === 'image/heic') {
      heic2any({ blob: file, toType: 'image/jpeg', quality: 0.7 })
        .then((conversionResult) => {
          const blob = Array.isArray(conversionResult) ? conversionResult[0] : conversionResult
          setImageBlob(blob)
          setIsLoading(false)
        })
        .catch(() => {
          toast({
            title: '画像変換エラーです🙇',
            description: '別の画像を選択してください',
            status: 'error',
            isClosable: true,
          })
          setIsLoading(false)
        })
    } else {
      // HEIC形式でない場合、そのままBlobとして設定
      setImageBlob(file)
      setIsLoading(false)
    }
  }, [file, setIsLoading])

  return {
    imageBlob,
  }
}

UI

iPhoneでの操作で、直感的でみやすいUIを心がけました。
テキストサイズも小さくて全然良かったので10~14pxあたりで作成して、
カードグリッド表示で一覧を表示した際に、結構な情報量が見れるようなUIにしました。
ですが、form入力を行う際に、iPhoneだと勝手に拡大されました..。
調べてみるとどうやらSafariでは16px以下のテキストサイズのformの場合、自動で拡大されるようです🙃
拡大されて横スクロールも発生したり、ボタンが見切れたりしてたのでform部分だけ16pxで作成しました。

ChakraUIはお気に入りのUIコンポーネントライブラリなので、今回も使用しました。
複雑なスタイル調整でのみstyleを記述しました。

例えば画像アップロード部分です。

スクリーンショット 2024-01-19 22.24.05.png

<FormControl mt={4}>
                <FormLabel
                  htmlFor={IMAGE_ID}
                  style={{
                    border: 'rgb(199 215 219) 3px dotted',
                    width: FIELD_SIZE,
                    height: FIELD_SIZE,
                    display: 'flex',
                    borderRadius: 12,
                    justifyContent: 'center',
                    alignItems: 'center',
                    overflow: 'hidden',
                    cursor: 'pointer',
                    color: '#c3c3c3',
                  }}
                  fontSize={16}
                >
                  {isLoading ? (
                    <Spinner speed="0.65s" emptyColor="gray.200" color="teal" />
                  ) : previewUrl || props.shop?.imageUrl ? (
                    <img
                      src={previewUrl || props.shop?.imageUrl || ''}
                      alt="アップロード画像"
                      style={{ objectFit: 'cover', width: '100%', height: '100%' }}
                    />
                  ) : (
                    '+ アップロード'
                  )}
                  <InputImage ref={fileInputRef} id={IMAGE_ID} onChange={handleFileChange} />
                </FormLabel>
                {previewUrl && (
                  <Button fontSize={16} onClick={handleCancelImageFile}>
                    画像キャンセル
                  </Button>
                )}
              </FormControl>
import React, { InputHTMLAttributes, forwardRef } from 'react'

export type Props = {
  onChange: (event: React.ChangeEvent<HTMLInputElement>) => void
  id: InputHTMLAttributes<HTMLInputElement>['id']
}

const InputImage = forwardRef<HTMLInputElement, Props>(({ onChange, id }, ref) => (
  <input ref={ref} id={id} type="file" accept="image/*, image/HEIC" onChange={onChange} hidden />
))

export default InputImage

デフォルトのinputを非表示にして、画像アップロード用のフィールドUIをスタイリングしています。

CI/CD

GitHub Actionsを使用して、コードの静的・型チェックとビルド、デプロイをワークフローとして実行しています。
これまでGitHub ActionsやCI/CDを設定する機会がなかったのでこれを機に使用してみました。
CI/CDのプロセスではGitHub ActionsのSecrets機能を使用して環境変数の参照先を指定する必要がある点に躓きました..。

Githubの該当リポジトリ Settings → Secrets and variables → Actionsから
環境変数のkeyとvalueを設定します。

スクリーンショット 2024-01-19 22.43.21.png

そしてGitHub Actionsのワークフロー定義ファイル内で環境変数の参照先をを下記のように設定します。

name: Deploy to Firebase Hosting on merge
'on':
  push:
    branches:
      - main
jobs:
  build_and_deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - run: npm ci && npm run build
        env:
          REACT_APP_FIREBASE_API_KEY: ${{ secrets.REACT_APP_FIREBASE_API_KEY }}
          # ..他の環境変数の設定
      - uses: FirebaseExtended/action-hosting-deploy@v0
        with:
          # ...

まとめ

土日に数時間と、平日に数十分程度で、1週間程度かなと見積もっていましたが
いろいろと試しては、躓いたりして結局2週間くらいかかってしまいました..。
ですが、Safariで自動拡大される原因やheicの変換、CI/CD整備など、
必要なアプリを作成しただけではなく、色々と学びがあって良かったです!
皆さんも良かったら利用してみてください🚀
ホーム画面に追加して利用するとよりネイティブアプリっぽい挙動になります🥳

https://chopsticks-ms.web.app/login

Discussion