wheatandcatの開発ブログ

React Nativeで開発しているペペロミア & memoirの技術系記事を投稿してます

T3 Stackで「OOMAKA」というサービスを作ったので紹介

前から記事で紹介していたサービスが公開できるレベルまで開発が完了。
コードがGitHubで公開しているので、諸々紹介

GitHub

github.com

URL

oomaka.vercel.app

使用技術

web & サーバー

その他

サービス概要

  • 年間のスケジュールをおおまかに登録できるwebサービス
  • 1ヶ月に5つまでタスクが登録できて、1画面で1年間を確認できる
  • ログインしないでも使用可能
  • ログインすると閲覧にパスワード機能が使用できる(ログインしない場合はURLを知っている場合は誰でも閲覧可能)

機能紹介 & こだわりポイント紹介

印刷ボタンを押した時にきっちり一枚になるように実装

印刷ボタンを押した時にきっちり一枚になるように実装
印刷時の表示はcssは@media printを使用すれば、styleの変更が可能
js側は以下で印刷時のハンドリングが可能

// 印刷プレビューを開く直前のハンドリング
window.addEventListener('beforeprint', handleBeforePrint);

// 印刷プレビューが閉じた直前のハンドリング
window.addEventListener("afterprint", handleAfterPrint);

ログイン

ログインはNextAuth.jsで実装
以下のページを参考に、Discord、Google、Appleのログインが利用可能

next-auth.js.org

アイコン系は全てemojiで表現

アイコン系は全てemojiで表現。サイト全体でほぼ画像を使用していないのでパフォーマンスも結構良い感じになっている。 Lighthouseの結果は以下の通り

Faviconも干支で切り替わるように設定

Next.jsだと簡単に画面毎に動的にFaviconを変わるように実装可能
表示自体画面のpathにicon.tsxを配置するのみでOK。コード以下のような感じになる

src/app/schedule/[id]/icon.tsx

import { ImageResponse } from "next/og";
import dayjs from "dayjs";
import { JpZodiac } from "~/utils/emoji";

export const size = {
  width: 32,
  height: 32,
};

export const runtime = "edge";

export const contentType = "image/png";

export default function Icon() {
  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 24,
          borderRadius: "50%",
          width: "100%",
          height: "100%",
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          color: "#333",
        }}
      >
        <span>{JpZodiac[(dayjs().year() + 8) % 12]} </span>
      </div>
    ),
    {
      ...size,
    },
  );
}

ちなみにOGP画像も以下みたいな感じ実装できる

src/app/api/og/route.tsx

import { ImageResponse } from "@vercel/og";

export const runtime = "edge";

export async function GET() {
  const fontData = await fetch(
    new URL("../../../../public/NotoSansJP-Bold2.ttf", import.meta.url),
  ).then((res) => res.arrayBuffer());

  return new ImageResponse(
    (
      <div
        style={{
          background: "white",
          display: "flex",
          flexDirection: "column",
          alignItems: "center",
          justifyContent: "center",
          width: "100%",
          height: "100%",
          fontFamily: '"NotoSansJP"',
        }}
      >
        <div
          style={{ height: 40, backgroundColor: "#000000", width: "100%" }}
        />
        <div
          style={{
            flex: 1,
            maxWidth: "80%",
            display: "flex",
            justifyContent: "center",
            flexDirection: "column",
            paddingTop: 30,
            paddingBottom: 30,
          }}
        >
          <div
            style={{
              fontSize: 28,
            }}
          >
            年間スケジュール、まとめるなら
          </div>
          <div
            style={{
              fontSize: 72,
              fontWeight: 800,
              letterSpacing: "0.5rem",
              paddingLeft: "2rem",
            }}
          >
            OOMAKA
          </div>
        </div>
        <div
          style={{ height: 40, backgroundColor: "#000000", width: "100%" }}
        />
      </div>
    ),
    {
      width: 1200,
      height: 630,
      fonts: [
        {
          name: "NotoSansJP",
          data: fontData,
          style: "normal",
        },
      ],
    },
  );
}

サービスを作成した感想

  • 個人開発でwebサービスを作ったのが久々だったので学びが多かった
  • Next.jsのサポートしている機能が多く、痒いところにも手が届く感が凄い
    • ただやりすぎている箇所もあるのでRemix派の気持ちも分かった
  • tRPCは最初はTypescriptでしか使えないなら微妙かなと思っていたが、Prismaとの相性が良すぎて生産性の向上に、かなり貢献した
  • NextAuthはサーバーサイドとクライアントサイドのどちらでも認証情報が取得できてNext.jsでの認証では必須
  • 最近アプリの開発ばかりしていたが、久々に別の最新技術に触れると刺激的で楽しかった