YSNHatenaBlog

主にアプリやWebサービス開発について

自作サービスの方針転換

今練習で作ってるテニスコート検索のサイト。各テニスコート詳細ページのタイトルを「(コート名)の場所」にしたけど、SEO的に悪化したような気がするので戻した。

tennico.app

とはいえそもそも流入ほぼ無い状態。検索キーワード候補を見ても、「テニスコート 安い」みたいのが出てくるので、やはり場所より料金検索ニーズの方が高そうだなぁと思い、料金で検索できるようにしたいと考え中。

以前書いた気もするが、テニスコートの料金体系って結構複雑。次の要因で料金が変わる。

  • 時間帯 - 早朝は割引があったりする。
  • 季節 - 冬場は午後のコートを使える時間が短かかったり、ナイターを利用する時間が早まったりする。
  • コート - 同じ場所にあるコートでも、センターコートは高かったり、古いクレーやハードコートは安かったりする。
  • 地域住民かどうか - 地域の住民は割引があったりする。
  • 照明利用有りか - ナイターを利用すると基本的に追加料金を取られる。

あとは、1時間単位で借りられるところもあれば2時間単位、半日単位で借りられたりするところもあるんだけど、どうやって検索フィルタ作ればいいの?とか。1時間あたりの料金??

iOSのフォーム選択で画面が拡大されてしまうのを防ぐ

また細かい話。 ここ参考にさせていただいただけなのですが。formのフォントが16px未満のときに発生するらしい。 これだと入力後にもう一度全画面表示に直さないと検索結果が見られないので不便。

Before

f:id:yosuke403:20210608051148g:plain

pgmemo.tokyo

16pxに設定して修正。

After

f:id:yosuke403:20210608051153g:plain

viewportの設定でもできるけど効かないという話も。

cly7796.net

Redux Toolkitがすごい便利だった

画面遷移しても状態保存しておこうと思い、再度reduxを見直していたが、これを見つけてちょっと感動した。

redux-toolkit.js.org

reduxの構成にしたい人はこれ入れておくとよさそう。

action, reducer, selectorの記述が少ない

reduxは何と言ってもコードの記述量が増えるのがめんどくさいのだが、とてもスマートに書ける。

import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { CourtDoc } from 'models/court'
import type { RootState } from 'store'

type Mode = 'text' | 'location'
type Geo = {
  lat: number
  lng: number
}

interface HomeState {
  mode: Mode
  zoom: number
  center: Geo
  courts?: CourtDoc[]
}

const initialState: HomeState = {
  mode: 'text',
  zoom: 11,
  center: { lat: 35.681236, lng: 139.767125 },
}

export const homeSlice = createSlice({
  name: 'home',
  initialState,
  reducers: {
    changeMode: (state, action: PayloadAction<Mode>) => {
      state.mode = action.payload
    },
    changeZoom: (state, action: PayloadAction<number>) => {
      state.zoom = action.payload
    },
    changeCenter: (state, action: PayloadAction<Geo>) => {
      state.center = action.payload
    },
  },
})

export const { changeMode, changeZoom, changeCenter } = homeSlice.actions

export const selectMode = (state: RootState): Mode => state.home.mode
export const selectZoom = (state: RootState): number => state.home.zoom
export const selectCenter = (state: RootState): Geo => state.home.center
export const selectCourtDocs = (state: RootState): CourtDoc[] | undefined =>
  state.home.courts

export default homeSlice.reducer

actionのstringを自分で書かなくても、 createSlicenameと関数名から自動でいい感じに生成してくれる。 reducers の書き換えをしてもglobalに影響が出ないようになっている。

reselect

まだやってないけど createSelector でSelectorを作ると reselect が使われてパフォーマンス上がる模様。 createSelector | Redux Toolkit

TypeScript対応

型がちゃんと付く。

フラグ一個でredux-devtools-extensionがつく

export const store = configureStore({
  reducer: {
    firebase: firebaseReducer,
    firestore: firestoreReducer,
    home: HomeReducer,
  },
  devTools: true, // <- これだけ
})

副作用の対応

前述のcreateSliceのところで、

import { createAsyncThunk, ... } from '@reduxjs/toolkit'
import { search } from 'models/search'
...

export const searchByText = createAsyncThunk(
  'home/searchByText',
  async (params: { text: string; hits: number }) => {
    const courtDocs = await search(params.text, params.hits)
    return courtDocs
  }
)

export const homeSlice = createSlice({
  ...,
  extraReducers: {
    [searchByText.fulfilled.type]: (
      state,
      action: PayloadAction<CourtDoc[]>
    ) => {
      state.courts = action.payload
    },
  },
})

...
export default homeSlice.reducer

これでいい。

すごい便利だ...。まだかじっただけなので、もうちょっと調べる。

google-map-reactから@react-google-maps/apiに乗り換え

もともと google-map-react を使っていたのを @react-google-maps/api に変更。

google-map-react はダウンロード数が多く、当初必要だった要件を満たしそうだったので導入したものの、デフォルトのマーカーへの変更を考えたときに、

new maps.Marker({
  position: { lat, lng }
  map,
})

のようにライブラリを直接呼び出す必要があり、Reactっぽくなく使いにくかった。 クリックイベントも取得しにくいので、他ライブラリを調査することに。

google-maps-react というそっくりな名前のパッケージもあったが、あまりメンテされてなさそうなので不採用。

結局こちらを使うことに。

www.npmjs.com

<GoogleMap
mapContainerStyle={{
  width: '100%',
  height: '100%',
}}
onIdle={onMapMoved}
onLoad={onLoaded}
>
{courtDocs.map((courtDoc) => {
  return (
    <Marker
      key={id}
      position={{ lat, lng }}
      onClick={() => onClickMarker(id)}
    />
  )
})}
</GoogleMap>

これなら Marker をReact Componentとして扱える。

吹き出しInfoWindowコンポーネントを使う。 これも Marker と同じように GoogleMap の子として追加すればよい。 吹き出しは✗で消せるので、消えたときに InfoWindow も取り除かないと再表示がうまくいかなかった。

23区分だけデータ入れ終わった

tennico.app

公私共に忙しかったので進捗悪かったけど、とりあえず23区だけ。 市区町村入れるとあと44ありそう...。

ただここでデータの形式についてちょっと考えたいかもしれない。 特に料金が複雑で検索パラメータとして入れにくい。

どう探したいかを考えながらデータ決めるといいだろうか。

自作サービスにデータを入れていっている

tennico.app

都内のテニスコートを探してはデータ入力していっている。

改めて思ったのはテニスコートの料金って結構複雑であるということ。

  • 時間帯
  • 季節
  • コート
  • 地域住民かどうか
  • 照明利用有りか

などで料金は変わるし、予約できる時間単位(1時間、2時間など)も重要。

設計段階で予想はされていたので、一旦乱暴に文字列で突っ込んでいる状況(URL参照、みたいのもある...)だが、一通りデータ入力し終わったら、料金のデータ構造どうするか再考したいところ。自分がユーザなら料金と地域住民じゃなくても借りられるかを検索フィルタできるのは結構嬉しい。

あとはデータのメンテナンスはどうするか。当然自分だけでは監視しきれないので、ユーザによる報告か直接編集で対応する感じかな。最初は報告ベースかな。