🙌

【ネタアプリ】就職カウントダウンサイトを作った話

2021/09/07に公開

何をやったのか?

「人の就職日をカウントダウンして祝うサイト」というネタアプリを作りました。

https://employment-ojisan.vercel.app/

やっているうちにいろいろ手を施したい部分が出てきたり、見た人からフィードバックや PR を頂いたりして結局一日で 50 コミット以上積まれました。当初の想定より思いのほかいろいろやったので、振り返りをします。

最終的な動作の様子はこんな感じです。↓

https://twitter.com/miyaoka/status/1432722689211473925

なぜやったか?

https://twitter.com/miyaoka/status/1432357762898432009?s=20

  • フォロイーの方が、9 月 1 日の就職日に向けて一日ごとにディスプレイネームをカウントダウンしてた
  • 就職当日は秒読みカウントダウンがしたい
  • サイトが有るとみんなでカウントダウンできて良さそう

サイトを作る

作ろう、と思ったものの既に時間は就職前日のため、カウントダウンまで 24 時間を切っている状況です。さっくり 1 時間くらいで作れればいいかなという感じでした。

  • Next.js
  • TypeScript
  • Tailwind

まずは特に考えずこの構成でセットアップします。

(セットアップに微妙につまづく)

これは余談に近いんですが、たまに next を 1 からセットアップしようとして公式サイトを開くと Start LearningDocumentation の 2 つのナビゲーションがあって、Learning のほうが色ついてるからついそっち押してしまいます。そこから左下の TypeScript ってところを辿ると js でセットアップしてから TS 化みたいな手順が示されてて、いやこれじゃない…、って何度かなってます。欲しかった答えは Documentation → Setup で yarn create next-app --typescript です。

差分の時間をカウントダウンする

このアプリでやることは、とりあえず現在時刻と就職時刻の差分の時間を求めて、「就職まで残り何時間何分何秒」とリアルタイムに表示するカウントダウンタイマーです。

就職前

就職後

そうしてとりあえずできたのがこんな感じです。この時点では setInterval で 1 秒毎に更新し、就職前、就職後で文言が変わるだけの仕様です。

デプロイすると時間が違う問題

できたので vercel にデプロイしましたが、vercel のデプロイのスクリーンショットでは時間がずれていました。これはブラウザがのタイムゾーンが日本でないと正しいカウントダウンになってないということです。

この問題にちょっと悩んでしまい、JS のタイムゾーンの取り扱いってどうなんだっけ、new Date().getTimezoneOffset() とか使うんだっけ?と軽く悩んでしまったのですが、結局のところ就職時刻を new Date("2021-09-01T00:00:00") で作っていたためローカル時間になっていたのが原因でした。

これの末尾に時差を追加して new Date("2021-09-01T00:00:00+09:00") とすればタイムゾーンが違う環境でも Date.now()との差分は同じになります。

背景追加

時間だけだと物寂しいので、就職前後のイメージを背景に追加しました。
画面全体の背景なのでbackground center / contain no-repeatなどとやるのが普通なのですが、最近は img 要素でもobject-fit: containとすれば同様の効果があります。

OGP, シェアボタン

やはりこのへんが設定されてないと人に使ってもらいづらいので一通り設定します。

レスポンシブ対応

スマホのことを全く考慮してないレイアウトだったので最低限整えておきます。

🚀 納品

2 時間くらいかかってしまいましたが、だいたいこれで当初の見積もりどおり実装できました。寝る前に納品しましょう。

https://twitter.com/miyaoka/status/1432388942221885443?s=20

追加対応

終わった気でいたのですが、翌朝気づいたら PR が来てました。

アクセシビリティ対応

https://github.com/miyaoka/employment-ojisan/pull/2

自分はあまりアクセシビリティに明るくなかったんですが、この PR では支援技術で読みやすいようにする対応が行われています。

特に工夫がされていたのはカウントダウン部分です。表示上は毎秒更新されるわけですが、読み上げも毎秒だと厳しいので、秒部分を削ったテキストを用意してそちらを読み上げ用に割り当てるというものでした。

https://zenn.dev/yamanoku/scraps/bb713d47a45a55#comment-74a792c2512034

  • 演出用のテキストはaria-hidden="true"で読み上げ対象から外す
  • 読み上げ用のテキストは、tailwind だとsr-onlyというクラスが用意されていてそれを使うと適切に不可視化できる。

https://tailwindcss.jp/docs/screen-readers

就職直後の画面表示

最初の仕様では「就職前」「就職後」の 2 つの表示のみでしたが、就職前から就職後に切り替わった 10 秒間は時間の代わりに「就職しました」という表示をつけるようにしました。

画面を埋めるように就職の文字を出して圧をかけます。

web フォント導入

デバイスフォントなのも物足りないので、最近さらに充実してきた GoogleFonts の日本語フォントを適用しました。

リポジトリへのリンク

OGP、シェアボタンと並んで設定しておきたいやつです。

ミリ秒表示の実装

https://twitter.com/euxn23/status/1432559978347261952?s=20

だいたいやるべきことはやったなと思ったんですが、ミリ秒表示する動画を見せられてこれはぜひ取り入れたいなとなりました。

ダイナミックになりました。

requestAnimationFrame で毎フレーム更新

最初は setInterval で 1 秒毎に更新という実装でしたが、ミリ秒表示するにあたって requestAnimationFrame に変更しました。これでいい感じに hooks にするにはどうすればと思いましたが、ここは下記のコードをそのまま使わせてもらいました。

https://bom-shibuya.hatenablog.com/entry/2020/10/27/182226

背景をアニメーションさせる

秒が動くだけではちょっと物足りないので背景をゆっくり動かすようにしました。

const Rotation = keyframes`
  0%{ transform:rotate(0);}
  100%{ transform:rotate(360deg); }
`;
const BgWrapper = styled.div`
img{
  animation: ${Rotation} 60s linear infinite normal;,
}
`;
const NowBgWrapper = styled.div`
img{
  animation: ${Rotation} 1.2s ease infinite normal;,
}
`;

通常背景が等速 60 秒で一周、お祝い時は ease で 1.2 秒で一周の回転です。

不要な時間単位を削除する

頭に 0 日と出てるのが気になってたんで、省くようにしました。

// 大きい順に値が無い時間単位を省き、逆順にする
const reducedTime = [date, hour, min, sec.toFixed(3)].reduce(
  (acc: (number | string)[], curr) => {
    if (curr === 0 && acc.length === 0) return acc;
    return [curr, ...acc];
  },
  []
);
// 小さい順に時間単位を適用していく
const timeUnit = ["秒", "分", "時間", "日"];
const timeText = reducedTime.reduce((acc, curr, i) => {
  return `${curr}${timeUnit[i]}${acc}`;
}, "");

reduce を 2 回するのよく分からん感じですが、 [日、時、分、秒] の配列の頭から連続する 0 を削除して、時間単位を後ろから結合させたかった感じです。

今考えると文字列として結合してから 正規表現でやればいいじゃんと思いました。

"0日0時間10分25.123秒".replace(/^(0[^\d.]+)+/, "");
// '10分25.123秒'

カウントダウン音を入れる

https://www.youtube.com/watch?v=PS2qPYbzEb4

音源は YouTube の iframe を embed して不可視状態にして api で再生するようにしました。
(ただし API で再生するためにはユーザーは画面をクリックしてアクティブにしている必要あり)

https://github.com/tjallingt/react-youtube

YouTubePlayer を ReactComponent 化したライブラリがあったのでそれを使います。

favicon を設定する

デフォの vercel アイコンのままだったので、ちゃんと設定する PR を就職する本人からいただきました。

<link
  rel="icon"
  href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text x=%2250%%22 y=%2250%%22 style=%22dominant-baseline:central;text-anchor:middle;font-size:90px;%22>👔</text></svg>"
></link>;
{/* Safari / IE */}
<link
  rel="icon alternate"
  type="image/png"
  href=" https://twemoji.maxcdn.com/v/13.1.0/72x72/1f935-1f3fb-200d-2642-fe0f.png"
/>;

https://zenn.dev/catnose99/articles/3d2f439e8ed161

絵文字を favicon にするという例のテクが使われてます(結局 Safari 対応で png も入ってますが)

就職時に紙吹雪が舞い上がるエフェクト追加

https://github.com/thedevelobear/react-rewards

いい感じにパーティクルを出す React コンポーネントがあるそうなので追加されました。

デバッグ用 Params 追加

就職時のエフェクトなどを確認する際、いちいちソースの設定時刻を書き換えていたんですが、だいぶ面倒なのでデバッグ用のパラメータを追加しました。 URL に ?count=20 をつけることで 20 秒前からの表示になるように設定しました。これは最初からつけておくべきでした。

開発中だけでなく、カウントダウンイベントが終わった後にも振り返りスナップショット的に見ることができるので見逃しユーザー対応としてもよかったです。

時分秒表示がガタガタしないように数値を等幅にする

https://twitter.com/uhyo_/status/1432645583345356804?s=20

数値が等幅でないために毎フレーム表示幅が変わってしまいガタガタしているのは気になると言えば気になるところでしたが、まあ今回の要件には含めないと思っていたので対応してませんでした。

しかし、font-variant-numeric: tabular-nums; でガタつきが解消できるというコメントを見かけてそんな CSS プロパティがあるんだと思いやってみましたが、使用フォント的にだめでした。

https://qiita.com/deren2525/items/f443d5ef8d7d5a437118

{/* 本体 */}
<link
  href="https://fonts.googleapis.com/css2?family=Shippori+Mincho+B1&display=swap"
  rel="stylesheet"
/>;
{/* 数値だけ等幅 */}
<link
  href="https://fonts.googleapis.com/css2?family=Noto+Serif&display=swap&text=0123456789"
  rel="stylesheet"
></link>;
{{
  fontFamily: `'Noto Serif', 'Shippori Mincho B1', serif`;
}}

なので、別途数値部分だけの等幅フォントを追加し、そちらを優先させるという手法を適用させたところガタつきが無くなりました。当初は要件外でしたがこれはやってよかったなと思います。

リファクタ:4つの status 定義

https://github.com/miyaoka/employment-ojisan/pull/9

当初は「就職前」「就職後」と 2 つの表示だけでしたが、最終的に「就職 10 秒前カウントダウン中」「就職後 10 秒間お祝い中」という状態が追加されたため、このへんが PR で整理されました。

カウントダウン中に文字が大きくなるようにする

就職まであと 1 時間ちょいですがまだ更新が行われていました。
駆け込みで PR が来たのでマージしました。

お祝い音を追加

カウントダウン表示演出が入ったのでお祝いの方も盛大にしたいなと思い、最後にもう一つサウンドを追加しました。

https://www.youtube.com/watch?v=1ARb7r0yY9k

完成、そしてカウントダウン

何人かの方にカウントダウンの瞬間を見ていただいて、無事に、就職前 → カウントダウン → お祝い演出 と場を共有することができました。

https://twitter.com/hashtag/syusyoku_20210901?src=hashtag_click&f=live

完走した感想

ネタアプリに求められるのはやはりタイミングだと思います。使い捨てである以上、クオリティやメンテナンス性よりもなによりタイミングが求められるのが普段のプロダクトとは違うところでしょう。

なので要件はコンパクトに、とにかく動くものをやっていく姿勢が大事だと思います。

しかしやってみると実は普通のプロダクト作りとそんなに変わらないのではないか?という思いを今回は感じました。それは最初の PR であるアクセシビリティ対応が来たところが特徴的かもしれません。たとえ使い捨てでもアプリである以上、一般的に求められることは一通り実装するんだなと感じました。

そしてこの記事に書いたように、追加対応の部分でいろいろと最初の想定に入っていなかった課題や要望が次々に生まれました。結局見積もりというのは作ったことのある部分しか見積もれないのではないかと思います。これは普段のプロダクトでも往々にして直面する問題であり、限られた工数で何を目指しどうジャッジするかという判断を繰り返していくのでしょう。

特にネタアプリというのは未知のものを手早くやるということであり、そうした見積もりとのせめぎ合いを楽しめるものなのではないかと思いました。


急造ネタアプリ作りにみなさんPRどうもありがとうございました

Discussion