LoginSignup
4
3

Next.js 13 App router Supabase 認証機能付き vns開発用テンプレート

Last updated at Posted at 2023-09-27

TODO

vns設計
テーブル設計
SQLのテーブル作成

サーバーアクションの取得
Stripeを導入する
DBのバックアップはGitHub Actionsで行うようにする

気づいた問題点

認証済みの状態なのに、さらに認証画面が開ける、
なので、認証状態をチェックして、認証済みならTOPページなりに遷移させる必要がある。
middleware.tsx

優先度低いTODO
discode や、それ以外の
authenticationを増やす


重要コマンド

型の生成 DB
npx supabase gen types typescript --project-id gzctqdrrnnkaxwwtzbsw > src/types/database.types.ts

Supabase開始
supabase start
変更が保存される
supabase stop
変更が破棄される=リセット
supabase stop --no-backup


マイグレーション

サーバーとローカルのマイグレーション状況
supabase migration list

Supabaseのマイグレーション
サーバー側の適用方法

↓REMOTE(サーバー)側への適用方法
supabase migration repair 20230618024722 --status applied
supabase migration repair 20231017082823 --status applied

Supabaseのマイグレーション
ローカル側の削除方法
マイグレーションのファイルを消すだけ

↓REMOTE(サーバー)側の削除方法
supabase migration repair 20231017052010 --status reverted
supabase migration repair 20231017082823 --status reverted

マイグレーションファイルを削除した場合は
supabase db reset

もしくは

supabase stop
supabase start

確認
supabase migration list

バックアップ
スキーマのみ
supabase db dump -f supabase/schema.sql

データのみ
supabase db dump -f supabase/seed.sql --data-only
データの保存、バックアップはデータベース管理ツールを使用します。


Supabaseローカル環境での 認証

初期設定の状態ではOAuth(Google GitHub Slack)認証ありません。
なのでSupabaseへのアクセスもAPIkeyがあれば自由にアクセスできますが
OAuth認証を設定した場合ローカルでもOauth認証の設定をしておく必要があります。
(ローカル環境なのでOAuth認証のどれか一つを有効にすればいいと思います。)

その設定方法は
TODO


初めに

Next.js App routerとSupbaseを土台にしてテンプレートを作りました。

テンプレートとは、実際には具体的な機能を持たず、多数のツールの集合体です。
それぞれのツールを組み合わせた際の不具合などを確認し、インストール時に動作確認のためのコードが含まれています。
この記事は、それらの記録をまとめたものです。

このテンプレートを使えば、「作りたい機能に集中できる」みたいな感じで作りました。

Next.js Supabase i18n メール認証 テスト Storybook shadcn/ui

テンプレートのリポジトリ

masakinihirota/vns_template

↑ブランチ templateがこの記事の内容になります。(メール認証)

↑ブランチ devがこの記事の内容+メール認証+OAuth認証+shadcn/uiになります。
この記事に、この部分の詳細は書かれていません。記事を書き終わった後に追加した機能になります。

Web開発中に気をつけること

GitHubにpushするようにしていますが、その前に必ずビルドをするようにしています。
開発中は簡単に壊れてしまうので、短い間隔でpushをするようにもしています。
(huskyを使用)

使用ツール

メインツール 開発に使うもの

Next.js 13 App router フロントエンドバックエンド
Supabae Postgres データベース
i18next 国際化
NextUI ダークモード
Storybook コンポーネント管理

補助ツール 開発を助けるもの

vitest テスト駆動開発
Plop テンプレートからの自動生成
eslint 整形
prettier 整形
lint-staged チェック
huskey チェック

Windows
VSCode
GitHub
Vercel

外部への公開ドキュメント用

Nextra

予定
Sdhach

予定
Sdhach


Next.jsでは基本的にすべて静的レンダリングとなります。

↓この設定は動的レンダリングを強制します。
export const dynamic = "force-dynamic";

↑どこからもimportしないのは不思議でしたが
Next.jsでの特殊な設定の一つのようです。


Next.jsのヘッダーは
metadataを利用して設定します。

タグで囲んで設定はしません。

Next.jsインストール

推奨リポジトリをインストールします。

npx create-next-app -e with-supabase

Next.jsの公式サンプルの一つです。
↓このリポジトリをそのまま利用します。

next.js/examples/with-supabase at canary · vercel/next.js

動作確認
npm run dev

これではまだ、このサンプルは動きません。
Supabaseの環境ファイルが必要です。

Supabaseのプロジェクトを作って環境変数を取得する必要があります。

環境ファイルを作る

※Supabaseでプロジェクトを作成済みで、
プロジェクトの設定情報は知っているものとします。

touch .env.local

.env.local
NEXT_PUBLIC_SUPABASE_URL=https://zhlmcrnnjbsfhbnctenz.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=ey***********************************74

.env.local.example
は削除します。

動作確認
npm run dev
npm run build
npm run start

※npm run devで起動していると、npm run buildはエラーを出して止まってしまうので、
npm run devは停止させてから npm run buildを実行させてください


ローカルのSupabaseの設定は完了したので

ここで一旦Next.jsのインストールは中断して

Supabaseのローカル開発環境を設定します。

↓Supabase 公式ドキュメントにあるローカル開発環境のドキュメント

Local Development | Supabase Docs

Managing Environments | Supabase Docs

Supabaseでの ローカル開発環境の作成

サーバーにSupabaseのスキーマ、テーブルなどが作られている場合
ローカルにその環境を再現します。

supabase init

supabase login

supabase link --project-ref $PROJECT_ID

↓$PROJECT_IDプロジェクトのダッシュボード URL から取得できます。

ダッシュボードからの変更がある場合に取り入れる。
supabase db pull

このコマンドは
supabase/migrations/_remote_schema.sql
ファイルを作成します。
現在より以前の変更が記録されます。

git add .
git commit -m "init supabase"
supabase start

マイグレーションの作成方法

マイグレーションファイルを作成して、直接編集する。

supabase migration new new_employee

supabase/migrations/_new_employee.sql
↑マイグレーションファイルが作成されるので SQL文を記入します。

_new_employee.sql
create table public.employees (
  id integer primary key generated always as identity,
  name text
);

↓編集したマイグレーションファイルを反映させます。

supabase db reset

※このコマンドはsupabase/migrations フォルダにある
マイグレーションファイルをすべて反映させます。

これで最新の状態になりました。

Tips
マイグレーションファイルを作る時
コマンドラインからマイグレーションファイルを読み込めます。
supabase migration new new_employee < create_employees_table.sql

自動でスキーマの差分を取得

ローカルのSupabaseダッシュボードからSQL Editorなどを使ってスキーマの変更をして、
その後、↓このコマンドでその差分を取る方法です。

supabase db diff -f new_employee

このコマンドでを実行すると
supabase/migrations/_new_employee.sql

↑マイグレーションファイルが自動で作成されます。

↓中身はこのようになります。

-- This script was generated by the Schema Diff utility in pgAdmin 4
-- For the circular dependencies, the order in which Schema Diff writes the objects is not very sophisticated
-- and may require manual changes to the script to ensure changes are applied in the correct order.
-- Please report an issue for any failure with the reproduction steps.

CREATE TABLE IF NOT EXISTS public.employees
(
    id integer NOT NULL GENERATED ALWAYS AS IDENTITY ( INCREMENT 1 START 1 MINVALUE 1 MAXVALUE 2147483647 CACHE 1 ),
    name text COLLATE pg_catalog."default",
    CONSTRAINT employees_pkey PRIMARY KEY (id)
)

TABLESPACE pg_default;

ALTER TABLE IF EXISTS public.employees
    OWNER to postgres;

GRANT ALL ON TABLE public.employees TO anon;

GRANT ALL ON TABLE public.employees TO authenticated;

GRANT ALL ON TABLE public.employees TO postgres;

GRANT ALL ON TABLE public.employees TO service_role;

※自動生成されたマイグレーションの中身は、手動で書かれたものよりも冗長になります。
これは、デフォルトのスキーマ差分ツールが、初期スキーマによって追加されたデフォルトの特権を考慮していないためです。

※別の方法として、--use-migra experimental フラグを渡すと、 migra を使ってより簡潔なマイグレーションを生成することができます。
f file フラグを指定しないと、デフォルトでは標準出力に出力されます。

supabase db diff --use-migra


Deploy a migration

運用環境では、ローカルマシンからデプロイするのではなく、
CI/CDパイプラインを使用して、
GitHubActionsで新しい移行をデプロイすることをお勧めします。

この例では、2つのSupabaseプロジェクト(1つは本番環境用、
もう1つはステージング環境用)を使用します。

以下のように環境を準備してください:

ステージング用と本番用に別々の Supabase プロジェクトを作成する。
git リポジトリを GitHub にプッシュし、GitHub アクションを有効にする。

注意

ステージングには新しいプロジェクトが必要です。
本番用プロジェクトのスキーマを反映するために既に変更されたプロジェクト
は使用できません。

Configure GitHub Actions

Supabase CLI を非インタラクティブモードで実行するには、
いくつかの環境変数が必要です。

SUPABASE_ACCESS_TOKEN はあなたの個人アクセストークンです。
SUPABASE_DB_PASSWORD はプロジェクト固有のデータベースパスワードです。
SUPABASE_PROJECT_ID はプロジェクト固有の参照文字列です。
これらを暗号化したシークレットとして GitHub Actions ランナーに
追加することをおすすめします。

Supabase公式 GitHub Actions ソース

supabase-action-example/.github/workflows at main · supabase/supabase-action-example

Push Database Migration to Prod Using Github Actions - SupabaseTips - YouTube

GitHub シークレットの登録

GitHub の自分のリポジトリで ナビゲーションメニューにあるSettingsをクリックします。

左サイドバーにある
Security > Secrets > Actionsをクリック

Action secrets 画面

右にある緑のボタン「New repository sceret」をクリックします。

シークレット値を登録します。

アクセストークン
SUPABASE_ACCESS_TOKEN

SupabaseのDBパスワード
PRODUCTION_DB_PASSWORD

Reference ID (=project-id)
PRODUCTION_PROJECT_ID

以上で シークレット値の設定は完了です

.github/workflows ディレクトリに以下のファイルを作成します:

.github/workflows/ci.yml
name: CI

on:
  pull_request:
  workflow_dispatch:

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - uses: supabase/setup-cli@v1
        with:
          version: latest

      - name: Start Supabase local development setup
        run: supabase start

      - name: Verify generated types are checked in
        run: |
          supabase gen types typescript --local > types.gen.ts
          if ! git diff --ignore-space-at-eol --exit-code --quiet types.gen.ts; then
            echo "Detected uncommitted changes after build. See status below:"
            git diff
            exit 1
          fi

.github/workflows/staging.yml
name: Deploy Migrations to Staging

on:
  push:
    branches:
      - develop
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest

    env:
      SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
      SUPABASE_DB_PASSWORD: ${{ secrets.STAGING_DB_PASSWORD }}
      SUPABASE_PROJECT_ID: ${{ secrets.STAGING_PROJECT_ID }}

    steps:
      - uses: actions/checkout@v3

      - uses: supabase/setup-cli@v1
        with:
          version: latest

      - run: supabase link --project-ref $SUPABASE_PROJECT_ID
      - run: supabase db push

.github/workflows/production.yml
name: Deploy Migrations to Production

on:
  push:
    branches:
      - main
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest

    env:
      SUPABASE_ACCESS_TOKEN: ${{ secrets.SUPABASE_ACCESS_TOKEN }}
      SUPABASE_DB_PASSWORD: ${{ secrets.PRODUCTION_DB_PASSWORD }}
      SUPABASE_PROJECT_ID: ${{ secrets.PRODUCTION_PROJECT_ID }}

    steps:
      - uses: actions/checkout@v3

      - uses: supabase/setup-cli@v1
        with:
          version: latest

      - run: supabase link --project-ref $SUPABASE_PROJECT_ID
      - run: supabase db push

↑自分の開発環境に合うように設定を変更します。

↑GitHub Actionsの設定が終わったらgitでコミットして、GitHubにPUSHすると、
コマンドが実行されます。

※push時に実行するように設定した場合

完全なサンプルコードはデモリポジトリで入手できます。
これらのファイルをgitにコミットし、
GitHubのメインブランチにプッシュします。
Supabaseプロジェクトに一致するように次の環境変数を更新します。

SUPABASE_ACCESS_TOKEN
PRODUCTION_PROJECT_ID
PRODUCTION_DB_PASSWORD
STAGING_PROJECT_ID
STAGING_DB_PASSWORD

正しく設定されていれば、
リポジトリにはCIとReleaseワークフローがあり、
mainブランチとdevelopブランチにプッシュされた新しいコミットを
トリガーとして動作します。

プルリクエストで実行されます。

新しいマイグレーション#でPRを開く
マイグレーション手順に従って、
supabase/migrations/_new_employee.sql
を作成します。

developから新しいブランチfeat/employeeをチェックアウトし、
マイグレーションファイルをコミットしてGitHubにプッシュします。

git checkout -b feat/employee
git add supabase/migrations/<timestamp>_new_employee.sql
git commit -m "Add employee table"
git push --set-upstream origin feat/employee

feat/employee から develop ブランチに PR をオープンし、CI ワークフローが起動したことを確認します。

テストエラーが解決したら、この PR をマージしてデプロイが実行されるのを確認しましょう。

production#へのリリース
ステージングプロジェクトが正常にマイグレーションされたことを確認したら、developからmainへ別のPRを作成し、本番プロジェクトへマイグレーションをデプロイするためにマージします。

リリースジョブは、supabase/migrationsディレクトリにマージされたすべての新しい移行スクリプトを、リンクされたSupaseプロジェクトに適用します。PROJECT_ID環境変数を使って、ジョブがリンクするプロジェクトを制御できます。

Troubleshooting

Production project to staging# 新しいステージング・プロジェクトをセットアップするとき、本番プロジェクトに以前に適用したマイグレーションと初期スキーマを同期する必要があるかもしれません。

ひとつの方法は、Release ワークフローを活用することです:

新しいブランチ develop を作成し、ブランチソースに main を選択します。develop ブランチを GitHub にプッシュします。GitHub の Actions ランナーが、既存のマイグレーションをステージングプロジェクトにデプロイします。

あるいは、ローカルの CLI を使って、リンクされたリモートデータベースにマイグレーションを適用することもできます。

supabase db push

プッシュしたら、ローカルとリモートの両方のデータベースで移行バージョンが最新であることを確認する。

supabase migration list

db pullでパーミッションが拒否されました

Supabaseホスティングプロジェクトを長く使っていると、db pullを実行する際に以下のようなパーミッションエラーに遭遇することがあります。

Error: Error running pg_dump on remote database: pg_dump: error: query failed: ERROR:  permission denied for table _type

pg_dump: error: query was: LOCK TABLE "graphql"."_type" IN ACCESS SHARE MODE

このエラーを解決するには、graphqlスキーマにpostgresのロールパーミッションを与える必要があります。そのためには、SupabaseダッシュボードのSQLエディタから以下のクエリを実行してください。

grant all on all tables in schema graphql to postgres, anon, authenticated, service_role;
grant all on all functions in schema graphql to postgres, anon, authenticated, service_role;
grant all on all sequences in schema graphql to postgres, anon, authenticated, service_role;

db push# でパーミッションが拒否された

Supabase ダッシュボードでテーブルを作成し、新しい移行スクリプトに ALTER TABLE ステートメントが含まれている場合、ステージングまたは本番データベースに適用するとパーミッションエラーが発生する可能性があります。

ERROR: must be owner of table employees (SQLSTATE 42501); while executing migration <timestamp>

これは、Supabaseダッシュボードで作成されたテーブルはsupabase_adminロールが所有し、CLIで実行される移行スクリプトはpostgresロールが所有するためです。

これを解決する一つの方法は、これらのテーブルの所有者をpostgresロールに再割り当てすることです。
例えば、テーブルの名前がpublicスキーマのusersである場合、以下のコマンドを実行して所有者を再割り当てすることができます。

ALTER TABLE users OWNER TO postgres;

テーブル以外にも、型、関数、スキーマなど、それぞれのコマンドを使用して他のエンティティの所有者を再割り当てする必要がある。

新しいマイグレーションをリベースする

チームメイトが新しいマイグレーションファイルを git メインブランチにマージすることがあります。

古いマイグレーションファイルの名前を新しいタイムスタンプで変更することで、このシナリオをうまく処理することができます。

git pull
supabase migration new dev_A
# Assume the new file is: supabase/migrations/<t+2>_dev_A.sql
mv <time>_dev_A.sql <t+2>_dev_A.sql
supabase db reset

リセットに失敗した場合は、_dev_A.sqlファイルを編集することでコンフリクトを手動で解決できます。

ローカルで検証したら、変更をGitにコミットしてGitHubにプッシュします。

※Supabaseのローカル開発環境の設定はここで終了です。


GitHubへの PUSH と Vercelへのデプロイ + 独自ドメイン

次はGitHubのリポジトリをVercelにデプロイします。

Next.js ✖️ SupabaseプロジェクトのVercelへのデプロイメモ

GitHub へのPUSH

GitHubへはVSCodeの機能 ソース管理で簡単にリポジトリが作成され、PUSH出来ました。

Vercel へのデプロイ

VercelへGitHubに作ったリポジトリをデプロイします。

右上の Add Newボタンを押して
Projectを選択します。

先程作ったリポジトリを選び Importボタンを押します。

Supabaseを使用しているので
Environment Variablesを設定します。

NEXT_PUBLIC_SUPABASE_ANON_KEY="eyJh*************************X8"
NEXT_PUBLIC_SUPABASE_URL="https://gzctqdrrnnkaxwwtzbsw.supabase.co"

環境変数を入力したら Add ボタンを押して一つづつ登録していきます。

デプロイと、ビルドが完了するまでしばらく待ちます。

Congratulationsと表示されたら成功です。


独自ドメインの設定

Vercelで独自ドメインを設定する方法 | YoheiKoブログ
https://yoheiko.com/blog/vercel%E3%81%A7%E3%81%AE%E7%8B%AC%E8%87%AA%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E8%A8%AD%E5%AE%9A/


Vercel上でSupabaseと連携する

Integrationsに行き

連携するSupabaseを探します。

Vercel | Works With Supabase
https://supabase.com/partners/integrations/vercel

これでSupabaseの環境変数などは自動的に取得されます。

ローカルにVercelが取得した環境変数を取得します。

ローカルでVercelへのログイン
npx vercel login
GitHubを選択します。

ローカル上からVercelとリンクするプロジェクトの選択
npx vercel link
リンクするプロジェクトを選択します。

環境変数をローカルに取得します。
npx vercel env pull
.env.localに出力されます。
.gitignoreに入れる必要があります。(デフォルトでは記入されています。)

すでに.env.localがある場合は削除してからコマンドを実行すると再生成されます。

これでローカルで開発を行い。
GitHubへPUSHすると
自動的にVercelへデプロイが走るようになります。

環境は、開発中ですと簡単に壊れるので随時「注視」が必要です。


srcフォルダの作成 ディレクトリの移動

mkdir src

srcの下にappディレクトリとcomponentsディレクトリを移動します。

tsconfig.jsonファイルを一部変更

tsconfig.json
    "paths": {
      "@/*": ["./src/*"]
    }

tailwind.config.jsファイルを一部変更

tailwind.config.js
  content: [
    "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  darkMode: "media",
  theme: {
。。。

※src/pages はNext.jsのpages dirを使っていたときの名残です。
Next.js 13 は pages dir と app dir の両方を同時に使えるので残しておきます。

darkMode: "media",を追加します。

mkdir src/styles

src\app\globals.css

src\styles\globals.css
へ移動します。

パスの修正をします。

src\app\layout.tsx
import "@/styles/globals.css"

src\styles\globals.css内容の修正

src\styles\globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;

src\app\layout.tsx パスの修正

import "@/styles/globals.css";

package.jsonファイルを一部変更

package.json
  "name": "vns.blue",
  "version": "0.1.0",

※名前は自由につけてください。

eslintのインストール

npm i eslint eslint-config-next

touch .eslintrc.json

.eslintrc.json(初期)

.eslintrc.json
{
  "extends": "next/core-web-vitals"
}

tsconfig.json

TypeScriptを開発しやすくする

tsconfig.json (変更箇所)
    "strict": false,
    "strictNullChecks": true,

"strict": false は、以下の設定を無効にします。

"noImplicitAny": true:暗黙的にany型として解釈される式を許可しません。
"strictNullChecks": true:nullとundefinedの扱いに関する厳密な型チェックを有効にします。
"strictFunctionTypes": true:関数の型チェックを厳密に行います。
"strictPropertyInitialization": true:クラスのプロパティの初期化を厳密に行います。
"noImplicitThis": true:thisの型チェックを厳密に行います。

↑この5つのうち

"strictNullChecks"だけ有効にします。

動作確認

npm run dev


i18n

i18n with Next.js 13/14 and app directory / App Router (an i18next guide)

↑このBlogを参考に↓リポジトリを作成しました。

masakinihirota/i18n

i18nサンプル(2023src__vns\lab\nexti18n)の
(これはi18nextの公式Blogサンプルを流用したものです。)
src\app[lng]\ 以下
src\app\i18n\ 以下
をコピーします。

ライブラリのインストール
npm i accept-language i18next i18next-browser-languagedetector i18next-resources-to-backend react-cookie react-i18next

動作確認
npm run dev
npm run build
npm run start


Layout

src\app\layout.tsx
import "@/styles/globals.css";

import { Metadata } from "next";

// Next.jsではmetadataを使ってメタデータを設定します。
export const metadata: Metadata = {
  title: "VNS.BLUE",
  description: "VNS.BLUE",
  keywords: "VNS.BLUE, オアシス宣言",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        {/* ↓ログイン画面に影響を与えている */}
        <main className="flex flex-col items-center min-h-screen bg-background">
          {children}
        </main>
      </body>
    </html>
  );
}

動作確認
npm run dev
npm run build
npm run start

src\app\page.tsx
import { createServerComponentClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers";
import Link from "next/link";
import LogoutButton from "../components/LogoutButton";

export const dynamic = "force-dynamic";

export default async function Index() {
  const supabase = createServerComponentClient({ cookies });

  const {
    data: { user },
  } = await supabase.auth.getUser();

  return (
    // 全体を縦にならべている、幅いっぱいに並べている
    <div className="flex flex-col items-center w-full">
      {/* nav部分 Home ,Advertisement, Dark mode,Language, Login */}
      {/* ナビ部分の高さを決めている、ボーダーラインボトムを描いている ボーダー色は現在の文字色
      ナビ部分とメイン部分の分離箇所 */}
      <nav className="flex justify-center w-full h-16 border-b border-current">
        {/* ナビ部分 上下中央に揃えている 横に均等にならべている 幅いっぱいに使っている */}
        <div className="flex items-center justify-between w-full">
          VNS.BLUE
          <div />
          Dark mode
          <div />
          Language
          <div />
          Advertisement
          <div>
            {user ? (
              <div className="flex items-center gap-4">
                Hey, {user.email}!
                <LogoutButton />
              </div>
            ) : (
              <Link
                href="/login"
                className="px-4 py-2 no-underline rounded-md bg-btn-background hover:bg-btn-background-hover"
              >
                Login
              </Link>
            )}
          </div>
        </div>
      </nav>
      <main className="flex flex-col w-full max-w-4xl p-3 text-xl text-foreground">
        <Link href="./examples/client-component">Client Component Example</Link>
        <Link href="./examples/route-handler">Route Handler Example</Link>
        <Link href="./examples/server-action">Server Action Example</Link>
        <Link href="./examples/server-component">Server Component Example</Link>
        <br />
        {/* <Link href={`/${lng}/login`}></Link> */}
        <Link href={`/en`}>英語</Link>
        <Link href="/de">ドイツ語</Link>
        <Link href="/ja">日本語</Link>
      </main>
      <div />
      <footer className="flex items-center justify-center h-16">
        VNS.BLUE 2023
      </footer>
    </div>
  );
}

src\app[lng]\layout.js
import { dir } from "i18next";
import { languages } from "../i18n/settings";

export async function generateStaticParams() {
  return languages.map((lng) => ({ lng }));
}

export default function RootLayout({ children, params: { lng } }) {
  return (
    <div lang={lng} dir={dir(lng)}>
      {children}
    </div>
  );
}

動作確認
npm run dev
npm run build
npm run start

※LinkボタンでTOP画面に戻ると画面が真っ暗
リロードで治る


Supabaseとのアクセス

Next.js 13 App router と Supabase での4つのアクセス方法 - Qiita

vns\app_examples

vns\app\examples
に変更します。

Supabaseのダッシュボードを開きます。

SQL Editorを開きます。

create table if not exists todos (
  id uuid default gen_random_uuid() primary key,
  created_at timestamp with time zone default timezone('utc'::text, now()) not null,
  title text,
  is_complete boolean default false,
  user_id uuid references auth.users default auth.uid()
);

-- Set up Row Level Security (RLS)
-- See https://supabase.com/docs/guides/auth/row-level-security for more details.
alter table todos
  enable row level security;

create policy "Authenticated users can select todos" on todos
  for select to authenticated using (true);

create policy "Authenticated users can insert their own todos" on todos
  for insert to authenticated with check (auth.uid() = user_id);

を貼り付けます。

右下の 「RUN CTRL」ボタンを押して 成功したら メッセージ ↓Successを確認できます。

「Success. No rows returned」

これで todos テーブルが出来ました。

シードファイルをサーバーに適用する

insert into todos(title)
values
  ('Create Supabase project'),
  ('Create Next.js app from Supabase Starter template'),
  ('Keep building cool stuff!');

シードファイルもテーブル作成と同様にSQL文を貼り付けて実行します。

Supabaseのダッシュボードの Table Editor で todosテーブルを選択すると。
データも入っていることが確認できます。

これでSupabase側の下準備は完了です。

これからNext.jsのコードからSupabaseにアクセスして
そのデータを表示するまでを実践します。

4つの接続方法

  1. クライアントから Supabaseのデータを取得する方法
  2. ルートハンドラーで Supabaseのデータを取得する方法
  3. Server ActionsでSupabaseのDBを操作する方法
  4. サーバーコンポーネントで Supabaseのデータを取得する方法

トップページに4つのリンクが表示されます。

Client Component Example
Route Handler Example
Server Action Example
Server Component Example

動作確認

npm run dev

正常に動作しているのならば

ログインしていない場合
データは表示されません、
Server Actionは実行されません

ログインしている場合
データが表示されます。
Server Actionを利用してテーブルにデータを挿入できます。
※連打すると連打した分だけデータが挿入されます。

コードの解説はBlog記事を読んでください。

これで最低限のWebアプリが完成しました。

最低限のWebアプリとは?
ユーザーを登録、ログインできる。
DBを作り、その中にデータを入力して、データにアクセスできる。
ブラウザからデータを指定して呼び出して、画面に表示させる。
ブラウザからデータを入力してDBに登録できる。
その登録したデータを表示できる。

一段落ついたので
gitのタグを作成
tag i18n


次へ

基本的な使い方 huskey v8 コミット時に自動ビルド or 自動テスト 失敗時にコミットは実行されない。 成功時は通常通り。 - Qiita

huskey インストール

公式サイト
Getting started | 🐶 husky

現在のバージョンは
"husky": "^8.0.0"

自動インストール(推奨)

npx husky-init && npm install

インストールされる中の詳細は公式サイトを御覧ください。

buildコマンドの追加

ファイルを開いて編集します。

.husky\pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm run build   <<<ここに実行したいnpm scriptを書きます。

これでコミット時に「バックグラウンド」で npm run build が走り
buildが失敗するとコミットがされません。<<便利
buildが成功するとコミットは通常通りです。

後でlintコマンド等を追加予定。

コマンドの追加コマンド

npm testを追加したい場合

npx husky add .husky/pre-commit "npm test"

例えば、クライアントの方だけに開発を集中している等の場合は、
buildコマンドをコメントアウトして build時間を削ればサクサク開発できます。

git tag huskey


ファビコン

VNS.BLUEのイメージの色

青系統の色に決める。

RGB #
0 00
126 7E
254 FE

#007EFE

ファビコンを作り書き換えます。

src\app\favicon.ico


middleware

Middleware の配置場所について

Next.jsでのMiddleware は
root/
app/
pages/
と同じ階層に配置できます。

※重要: Next.jsのインストール時に srcフォルダを作った場合は、srcフォルダの下にmiddleware.tsファイルを置いてください。

2つのリポジトリ
with-supabase

i18n
この2つのmiddlewareをまとめます。

middleware.ts
に拡張子を変更します。

middleware.ts
import { createMiddlewareClient } from "@supabase/auth-helpers-nextjs";
import { NextResponse } from "next/server";

import type { NextRequest } from "next/server";

import acceptLanguage from "accept-language";

import { fallbackLng, languages, cookieName } from "@/app/i18n/settings";

acceptLanguage.languages(languages);

export const config = {
  // matcher: '/:lng*'
  matcher: ["/((?!api|_next/static|_next/image|assets|favicon.ico|sw.js).*)"],
};

export async function middleware(req: NextRequest) {
  const res = NextResponse.next();

  // Create a Supabase client configured to use cookies
  const supabase = createMiddlewareClient({ req, res });

  // Refresh session if expired - required for Server Components
  // https://supabase.com/docs/guides/auth/auth-helpers/nextjs#managing-session-with-middleware
  await supabase.auth.getSession();

  let lng;
  if (req.cookies.has(cookieName))
    lng = acceptLanguage.get(req.cookies.get(cookieName)?.value);
  if (!lng) lng = acceptLanguage.get(req.headers.get("Accept-Language"));
  if (!lng) lng = fallbackLng;

  // Redirect if lng in path is not supported
  if (
    !languages.some((loc) => req.nextUrl.pathname.startsWith(`/${loc}`)) &&
    !req.nextUrl.pathname.startsWith("/_next")
  ) {
    return NextResponse.redirect(
      new URL(`/${lng}${req.nextUrl.pathname}`, req.url)
    );
  }

  const refererHeaderValue = req.headers.get("referer");

  if (refererHeaderValue !== null) {
    const refererUrl = new URL(refererHeaderValue);
    const lngInReferer = languages.find((l) =>
      refererUrl.pathname.startsWith(`/${l}`)
    );
    const res = NextResponse.next();
    if (lngInReferer) res.cookies.set(cookieName, lngInReferer);
    return res;
  }

  return res;
}

動作、エラー確認
ブラウザの調査検証画面を開いておく
ブラウザのコンソール画面を開く

npm run dev


ここでブラウザのコンソール画面にエラーが出ているのに気づく

app-index.js:31 Warning: Extra attributes from the server: class
    at body
    at html
    at RedirectErrorBoundary

原因は
ブラウザの拡張機能

問題解決
https://www.slingacademy.com/article/next-js-warning-extra-attributes-from-the-server/

対応策は
ブラウザのシークレット画面を利用する。とエラーが出てこない。

拡張機能を使用しない。とエラーが出てこない。

bodyタグに プロパティを設定します。

↑suppressHydrationWarningプロパティは、
Reactがクライアント側でレンダリングされたコンポーネントを
ハイドレーションする際に、
警告を抑制するために使用されます。

src\app\layout.tsx
import "@/styles/globals.css";

import { Metadata } from "next";

// Next.jsではmetadataを使ってメタデータを設定します。
export const metadata: Metadata = {
  title: "VNS.BLUE",
  description: "VNS.BLUE",
  keywords: "VNS.BLUE, オアシス宣言",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html>
      <body suppressHydrationWarning={true}>{children}</body>
    </html>
  );
}

動作、エラー確認
ブラウザの調査検証画面を開いておく
ブラウザのコンソール画面を開く

npm run dev


eslint & prettier

VSCodeの設定 Default FormatterをPrettierにする。

setting.json
"editor.defaultFormatter": "esbenp.prettier-vscode"

prettier

公式サイト Install Prettier

prettier のインストール

npm install -D prettier

eslint と設定が衝突するのを避けるための設定

npm install -D eslint-config-prettier

空の.prettierrc を作成して、Prettier を使用していることをエディターやその他のツールに知らせます。

touch .prettierrc

.prettierrc

Prettier を VSCode で使うにはデフォルトのフォーマッターを Prettier に設定する必要があります。

.eslintrc.json に prettier 設定の追加をします。

.eslintrc.json
{
  "extends": [
    "next/core-web-vitals",
    "prettier"
  ]
}

package.json に format コマンドを追加します。

package.json (追記)
"scripts": {
  "lint": "eslint --ext .ts,.js,.tsx . ",
  "format": "prettier --write ."
  ...

.prettierrc 設定ファイルの追加

.prettierrc ファイルを設定します。

.prettierrc
{
  "tabWidth": 2,
  "useTabs": false,
  "trailingComma": "none",
  "semi": false
}

tailwindcssを整形してくれるプラグインです
prettier-plugin-tailwindcss

const styleguide = require('@vercel/style-guide/prettier');

module.exports = {
  ...styleguide,
  plugins: [...styleguide.plugins, 'prettier-plugin-tailwindcss'],
};

Vercelのスタイルガイドは、Vercelが開発するアプリケーションのコードスタイルを定めたもので、Prettierの設定を含んでいます。この設定は、Vercelの開発チームがコードを書く際のガイドラインを提供するもので、他の開発者がVercelのコードスタイルに従うことを容易にします。

vercel/style-guide: Vercel's engineering style guide
https://github.com/vercel/style-guide

※ルールは各自で好きなように設定してください。

↓ このプラグインは推奨されなくなりました。
eslint-plugin-prettier

次に、.prettierignore ファイルを作成して、どのファイルをフォーマットしないかを Prettier CLI とエディターに知らせます。

touch .prettierignore

.prettierignore
# Ignore artifacts:
build
coverage

# dotfile
.env*

# markdown
*.md

# next.js
/.next/
/out/

# production
/build

package-lock.json

*.stories.ts
*.stories.tsx

Prettier コマンド一覧

※通常は VSCode 側を設定すれば、保存時に自動で実行されるので、コマンドを打つことはほとんどありません。

動作確認のため prettier を実行します。

全体をフォーマットします。
npx prettier . --write

src ディレクトリ以下をフォーマットします。
npx prettier src/ --write

巨大プロジェクトの場合に時間短縮のためフォルダを指定したい場合
npx prettier [フォルダ名] --write

直接ファイルを指定する場合
npx prettier src\app\page.tsx --write

prettier が実行済みかの確認(上書きはしない)
npx prettier . --check

動作確認
npm run build確認


eslintの詳細な設定

.eslintignore ファイルの作成

.eslintignore を作成します。

touch .eslintignore

↓lint 対象から外したいファイルを設定します。

.eslintignore
# config
.eslintrc.json
.prettierrc
next.config.js
tailwind.config.js
tsconfig.json
postcss.config.js

# build dir
build/
bin/
obj/
out/
.next/

# 自動生成されたファイル
package-lock.json

# Storybook
*.stories.ts
*.stories.tsx

# CSSファイル
*.css

# mdxファイル
*.mdx

# すべての画像ファイルを除外する
**/*.png
**/*.jpg
**/*.jpeg
**/*.gif
**/*.ico

node_modules
public

ESLint のインストール

next.js/packages/eslint-config-next/index.js at canary · vercel/next.js · GitHub

上記コードを調べると next/core-web-vitals は
next/recommended
react/recommended
react-hooks/recommended
を読み込んでいるので、↑ これらのインストールは不要です。

eslint-plugin-import

Next.js 開発 ESLint で import 文の自動挿入、自動削除、自動ソート - Qiita

npm install -D eslint-plugin-unused-imports --legacy-peer-deps
npm install -D @typescript-eslint/eslint-plugin@latest

.eslintrc.json
{
  "extends": ["next/core-web-vitals", "prettier"],
  "plugins": ["unused-imports"],
  "rules": {
    "unused-imports/no-unused-imports": "warn"
  }
}

※ ↑この設定は必要な最小限の設定にしてあるので、自動挿入、自動削除の確認用として有効です。

さらに、並べ替えのルールを追加します。

.eslintrc.json
{
  "extends": ["next/core-web-vitals", "prettier"],
  "plugins": ["unused-imports"],
  "rules": {
    // TypeScriptで未使用の変数を許可するかどうかを指定します。この例では、offに設定されているため、未使用の変数を許可します。
    "@typescript-eslint/no-unused-vars": "off",
    // 未使用のインポートに関するルールを指定します。この例では、warnに設定されているため、未使用のインポートがある場合に警告を出します。
    "unused-imports/no-unused-imports": "warn",
    // モジュールのインポート順序に関するルールを指定します。この例では、配列の中に複数のグループが定義されています。各グループは、groupsプロパティで定義されています。
    "import/order": [
      "warn",
      {
        "groups": [
          // builtin: Node.js に組み込まれているモジュール
          // external: npm install  プロジェクト外部からインストールされたモジュール
          // internal: プロジェクト内のモジュールで、パスを指定してインポートされたもの
          // parent: 親モジュール 相対パスを使用してインポートされたもの
          // sibling: 兄弟モジュール 相対パスを使用してインポートされたもの
          // index: インデックスファイルで、相対パスを使用してインポートされたもの
          // object: オブジェクトファイルで、相対パスを使用してインポートされたもの
          // type: 型ファイルで、相対パスを使用してインポートされたもの
          "builtin",
          "external",
          "internal",
          ["parent", "sibling"],
          "index",
          "object",
          "type"
        ],
        // それぞれのgroupsとの間は1行分空けます。
        "newlines-between": "always",
        // 特定のグループの import 文を除外するかどうかを指定します。
        "pathGroupsExcludedImportTypes": ["builtin", "external"],
        // order オプションは、アルファベット順にします。
        // caseInsensitive オプションは、大文字小文字を無視してアルファベット順に並べるかどうかを指定します。
        "alphabetize": { "order": "asc", "caseInsensitive": true },
        "pathGroups": [
          // pattern: インポートパスのパターンを指定します。この例では、src/ディレクトリ以下のすべてのファイルを指定しています。
          // group: インポートパスが一致した場合に、どのグループに属するかを指定します。この例では、internalグループに属するように指定しています。
          // position: インポートパスが一致した場合に、どの位置に挿入するかを指定します。この例では、beforeに指定しているため、他のグループよりも前に挿入されます。
          {
            "pattern": "src/**",
            "group": "internal",
            "position": "before"
          }
        ]
      }
    ]
  }
}

確認用のページ

import 文の自動削除や自動挿入が試せます。

src\app\XXX.tsx
// import文の順番も自動整形されます。
import { type NextPage } from "next";
import Link from "next/link";

const Home: NextPage = () => {
  return (
    <main>
      {/* TailwindCSSのプロパティ値も自動整列されます。 */}
      <h1 className="pt-2 p-4">VNS.BLUE</h1>
      Next.js app router 開発用テンプレート (Storybook Supabase shadcn/ui)
      <br />
      {/* ↓この行を消すとimport文が自動削除されます。 */}
      <Link href="/">Home</Link>
    </main>
  );
};

export default Home;

.eslintrc.json
{
  "extends": [
    "plugin:@typescript-eslint/recommended",
    "eslint:recommended",
    "next/core-web-vitals",
    "prettier"
  ],
  "parser": "@typescript-eslint/parser",
  "parserOptions": {
    // tsx ファイル内のJSX構文を正しく解析できるようになります。
    "ecmaFeatures": {
      "jsx": true
    },
    "ecmaVersion": 2020,
    "sourceType": "module",
    "jsx": "react"
  },
  // react: Reactの構文を解析するためのルールを提供します。
  // import: import文の構文を解析するためのルールを提供します。
  // unused-imports: 未使用のimport文を検出するためのルールを提供します。
  "plugins": ["react", "import", "unused-imports"],
  "root": true,
  "rules": {
    // TypeScriptで未使用の変数を許可するかどうかを指定します。この例では、offに設定されているため、未使用の変数を許可します。
    "@typescript-eslint/no-unused-vars": "off",
    "@typescript-eslint/no-explicit-any": "off",
    // 未使用のインポートに関するルールを指定します。この例では、warnに設定されているため、未使用のインポートがある場合に警告を出します。
    "unused-imports/no-unused-imports": "warn",
    // モジュールのインポート順序に関するルールを指定します。この例では、配列の中に複数のグループが定義されています。各グループは、groupsプロパティで定義されています。
    "import/order": [
      // エラーだと動作が止まります。
      // 警告だと警告表示はされますが動作は止まりません。
      "warn",
      {
        "groups": [
          // builtin: Node.js に組み込まれているモジュール
          // external: npm install  プロジェクト外部からインストールされたモジュール
          // internal: プロジェクト内のモジュールで、パスを指定してインポートされたもの
          // parent: 親モジュール 相対パスを使用してインポートされたもの
          // sibling: 兄弟モジュール 相対パスを使用してインポートされたもの
          // index: インデックスファイルで、相対パスを使用してインポートされたもの
          // object: オブジェクトファイルで、相対パスを使用してインポートされたもの
          // type: 型ファイルで、相対パスを使用してインポートされたもの
          "builtin",
          "external",
          "internal",
          ["parent", "sibling"],
          "index",
          "object",
          "type"
        ],
        // それぞれのgroupsとの間は1行分空けます。
        "newlines-between": "always",
        // 特定のグループの import 文を除外するかどうかを指定します。
        "pathGroupsExcludedImportTypes": ["builtin", "external"],
        // order オプションは、アルファベット順にします。
        // caseInsensitive オプションは、大文字小文字を無視してアルファベット順に並べるかどうかを指定します。
        "alphabetize": { "order": "asc", "caseInsensitive": true },
        "pathGroups": [
          // pattern: インポートパスのパターンを指定します。この例では、src/ディレクトリ以下のすべてのファイルを指定しています。
          // group: インポートパスが一致した場合に、どのグループに属するかを指定します。この例では、internalグループに属するように指定しています。
          // position: インポートパスが一致した場合に、どの位置に挿入するかを指定します。この例では、beforeに指定しているため、他のグループよりも前に挿入されます。
          {
            "pattern": "src/**",
            "group": "internal",
            "position": "before"
          }
        ]
      }
    ]
  }
}

ESlintでのチェックを実行する

*.tsxファイルのみをチェックする場合

npx eslint src/**/*.tsx

全ファイルをチェックする場合

npx eslint src/**/*

※チェックをしないファイルタイプは .eslintignore に登録します。

prettierのルールで書き換える

npx prettier . --write


lint-staged

npm i --save-dev lint-staged

npx husky set .husky/pre-commit "npx lint-staged"

#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm run lint:fix
npx lint-staged
npm run build

package.json
{
  "name": "vns.blue",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "eslint --ext .ts,.js,.tsx . && prettier --check \"./**/*.{ts,js,tsx}\"",
    "lint:fix": "eslint --ext .ts,.js,.tsx . --fix && prettier --write \"./**/*.{ts,js,tsx,scss}\"",
    "format": "prettier --write './**/*.{js,ts,tsx}'"
  },
  "dependencies": {
    "@heroicons/react": "^2.0.18",
    "@nextui-org/react": "^2.1.12",
    "@supabase/auth-helpers-nextjs": "latest",
    "@supabase/supabase-js": "latest",
    "accept-language": "^3.0.18",
    "autoprefixer": "10.4.14",
    "eslint": "^8.49.0",
    "eslint-config-next": "^13.4.19",
    "framer-motion": "^10.16.4",
    "i18next": "^23.5.1",
    "i18next-browser-languagedetector": "^7.1.0",
    "i18next-resources-to-backend": "^1.1.4",
    "next": "latest",
    "next-themes": "^0.2.1",
    "postcss": "8.4.24",
    "react": "18.2.0",
    "react-cookie": "^6.1.1",
    "react-dom": "18.2.0",
    "react-i18next": "^13.2.2",
    "tailwindcss": "3.3.2",
    "typescript": "5.1.3"
  },
  "devDependencies": {
    "@types/node": "20.3.1",
    "@types/react": "18.2.12",
    "@types/react-dom": "18.2.5",
    "@types/tailwindcss": "^3.1.0",
    "@typescript-eslint/eslint-plugin": "^6.7.2",
    "encoding": "^0.1.13",
    "eslint-config-prettier": "^9.0.0",
    "eslint-plugin-unused-imports": "^3.0.0",
    "husky": "^8.0.0",
    "lint-staged": "^14.0.1",
    "prettier": "^3.0.3"
  },
  "lint-staged": {
    "*.{tsx,ts}": "npm run lint"
  }
}

turborepo の導入

ビルドのキャッシュ目的で turborepo を導入してみます。
キャッシュが効いている場合のビルドは早くなりますが、
実運用では早くなるのでしょうか?

Add Turborepo to your existing project – Turborepo

インストール

pnpm add turbo --global

turbo.json
{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "outputs": [".next/**", "!.next/cache/**"]
    },
    "lint": {}
  }
}

.gitignore
# turborepo
.turbo

.husky\pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

turbo build lint:fix

デプロイ時に整形とビルドを行うように設定します。

turbo build lint:fix

lint-stagedは更新されたファイルのlint チェックを行います。
turbo lint は全体のlint チェックを行います。

※実際に使ってみて

何処かでエラーが発生した場合に、エラー箇所を修正して、turbo build コマンドを実行して動作確認をする場合があります。

もしこの時に turbo build コマンドが成功した場合は、GitHubにPUSHするタイミングでも turbo build lint:fix を実行するように設定してあるので、この時はキャッシュを読み込んで時間短縮が出来ます。

Next.js のプロジェクトを開発しやすいようにするlintとformatterなどの設定


追記 2023年10月9日

huskyで追加しました。

↓ファイルを作成します。

.husky\pre-commit

.husky\pre-push

pre-commitファイルはコミット時に、
pre-pushはpush時に自動実行します。

.husky\pre-commit
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

# npm run format:fix
npm run lint-staged
git add .

.husky\pre-push
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

turbo build

↑ コミット時には ESLintと Prettierを走らせます。
PUSH時にはビルドを走らせます。

ローカルで複数回コミットしたら、一定時間の後ビルドを走らせるという方法を取っています。

ビルドには長い時間がかかるのでこの方式を採用してみました。

turborepoを導入しているのは同じコードだった場合、ビルド時にキャッシュを参照してくれて時間短縮になるからです。


ダークモードの実装

NextUIのダークモードを実装します。

NextUIをインストール

Next.js | NextUI - Beautiful, fast and modern React UI Library

本体
pnpm i @nextui-org/react @nextui-org/theme @nextui-org/system framer-motion

コンポーネント、ライブラリ
pnpm i @nextui-org/button @nextui-org/switch

npm i -D @types/tailwindcss

※もしnextuiを pnpmでインストールしていた場合

pnpmを使用している場合は、root直下に .npmrcファイルを作り、
以下のコードを追加する必要があります:

.npmrc
public-hoist-pattern[]=*@nextui-org/*。

.npmrc ファイルを修正した後、pnpm install を再度実行して、依存関係が正しくインストールされていることを確認してください。

Installation | NextUI - Beautiful, fast and modern React UI Library

tailwind.config.js
import { nextui } from "@nextui-org/react";

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
    "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
    "./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  darkMode: "class",
  plugins: [nextui()],
};

app\globals.css
@tailwind base;
@tailwind components;
@tailwind utilities;

src\app\page.tsx
import { createServerComponentClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers";
import Link from "next/link";

import LogoutButton from "../components/LogoutButton";

// 動的レンダリングを強制します。
export const dynamic = "force-dynamic";

export default async function Index() {
  const supabase = createServerComponentClient({ cookies });

  const {
    data: { user },
  } = await supabase.auth.getUser();

  return (
    // 全体を縦にならべている、幅いっぱいに並べている
    <div className="flex flex-col items-center w-full">
      {/* nav部分 Home ,Advertisement, Dark mode,Language, Login */}
      {/* ナビ部分の高さを決めている、ボーダーラインボトムを描いている ボーダー色は現在の文字色
      ナビ部分とメイン部分の分離箇所 */}
      <nav className="flex justify-center w-full h-16 border-b border-current">
        {/* ナビ部分 上下中央に揃えている 横に均等にならべている 幅いっぱいに使っている */}
        <div className="flex items-center justify-between w-full">
          VNS.BLUE
          <div />
          Dark mode
          <div />
          Language
          <div />
          Advertisement
          <div>
            {user ? (
              <div className="flex items-center gap-4">
                Welcome to VNS.BLUE, {user.email}!
                <LogoutButton />
              </div>
            ) : (
              <Link
                href="/login"
                className="px-4 py-2 no-underline rounded-md bg-btn-background hover:bg-btn-background-hover"
              >
                Login
              </Link>
            )}
          </div>
        </div>
      </nav>

      {/* メイン部分 */}
      <main className="flex flex-col w-full max-w-4xl p-3 text-xl text-foreground">
        <Link href="./examples/client-component">Client Component Example</Link>
        <Link href="./examples/route-handler">Route Handler Example</Link>
        <Link href="./examples/server-action">Server Action Example</Link>
        <Link href="./examples/server-component">Server Component Example</Link>
        <br />
        {/* <Link href={`/${lng}/login`}></Link> */}
        <Link href={`/en`}>英語</Link>
        <Link href="/de">ドイツ語</Link>
        <Link href="/ja">日本語</Link>
      </main>
      <h1 className="text-3xl font-bold underline">Hello world!</h1>
      <div />
      <footer className="flex items-center justify-center h-16">
        VNS.BLUE 2023
      </footer>
    </div>
  );
}

動作確認
npm run dev

Hello world!の文字にTailwindCSSのプロパティが有効だったら成功です。

インストールの続き

touch src/app/providers.tsx

app/providers.tsx
"use client";

import { NextUIProvider } from "@nextui-org/react";
import React from "react";

export function Providers({ children }: { children: React.ReactNode }) {
  return <NextUIProvider>{children}</NextUIProvider>;
}

↑これはクライアントコンポーネントです。

ダークモードを設定する場合トップページのLayoutに設定をするのですが
でもそこのLayoutでクライアントコンポーネントとして設定をすると
Next.jsのツリーの頂点(root)から
クライアントコンポーネントになってしまいます。

そんなときはこのようなテクニックで
NextUIのラップ部分だけを切り出してクライアントコンポーネントにして
あとはサーバーコンポーネントに戻しています。

次に、ルートレイアウトページに移動し、↑これをNextUIProviderでラップします:

src\app\page.tsx
????


※NextUIは自動的にlightとdarkの2つのテーマをアプリケーションに追加します。

ダークテーマを使用するには、
dark/lightクラスを追加することで、どちらのテーマも使用できます。

↑これらのタグに テーマ要素(dark light)をclassNameに追加するだけです。


`src\app\layout.tsx


↑このようにプロパティを設定してモードを切り替えています。



次に全体に反映するようにラップします。


```src\app\layout.tsx
import "@/styles/globals.css";
import React from "react";

import { Providers } from "./providers";

import { Metadata } from "next";

// Next.jsではmetadataを使ってメタデータを設定します。
export const metadata: Metadata = {
  title: "VNS.BLUE",
  description: "VNS.BLUE",
  keywords: "VNS.BLUE, オアシス宣言",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en" className="dark">
      <body suppressHydrationWarning={true}>
        <Providers>
          {/* ↓ログイン画面に影響を与えている */}
          <main className="flex flex-col items-center min-h-screen bg-background">
            {children}
          </main>
        </Providers>
      </body>
    </html>
  );
}

詳しくはテーマのドキュメントをご覧ください。

Customize theme | NextUI - Beautiful, fast and modern React UI Library

NextUIコンポーネントを使う
"use client"ディレクティブを使わなくても、
NextUIコンポーネントを直接サーバーコンポーネントにインポートできるようになりました

※↑重要

app/page.tsx
import {Button} from '@nextui-org/button';

...

    <div>
      <Button>Click me</Button>
    </div>

動作確認

npm run dev

ボタンが押せるようになっていて、TailwindCSSも効いています。

これでトップページでもクライアントコンポーネント("use client")にせずに、
使用できるようになりました。


動的にテーマを切り替える

テーマをインストール
npm i next-themes

Heroiconsをインストール
npm install @heroicons/react

touch src/components/ThemeSwitcher.tsx

src\app\layout.tsx
import "@/styles/globals.css";
import React from "react";

import { Providers } from "./providers";

import { Metadata } from "next";

// Next.jsではmetadataを使ってメタデータを設定します。
export const metadata: Metadata = {
  title: "VNS.BLUE",
  description: "VNS.BLUE",
  keywords: "VNS.BLUE, オアシス宣言",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body suppressHydrationWarning={true}>
        {/* ↓ログイン画面に影響を与えている */}
        <main className="flex flex-col items-center min-h-screen bg-background">
          <Providers>{children}</Providers>
        </main>
      </body>
    </html>
  );
}

src\app\providers.tsx
"use client";

import { NextUIProvider } from "@nextui-org/react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import React from "react";

export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <NextUIProvider>
      <NextThemesProvider attribute="class" defaultTheme="dark">
        {children}
      </NextThemesProvider>
    </NextUIProvider>
  );
}

touch src\components\ThemeSwitcher.tsx

src\components\ThemeSwitcher.tsx
"use client";

import { MoonIcon } from "@heroicons/react/24/outline";
import { SunIcon } from "@heroicons/react/24/solid";
import { Switch } from "@nextui-org/switch";
import { useTheme } from "next-themes";
import { useEffect, useState } from "react";

export function ThemeSwitcher() {
  const [mounted, setMounted] = useState(false);
  // eslint-disable-next-line no-unused-vars
  const { theme, setTheme } = useTheme();

  useEffect(() => {
    setMounted(true);
  }, []);

  if (!mounted) return null;

  return (
    <div>
      <Switch
        defaultSelected
        size="lg"
        color="primary"
        startContent={<SunIcon />}
        endContent={<MoonIcon />}
        onValueChange={(isSelected) => setTheme(isSelected ? "light" : "dark")}
      />
    </div>
  );
}

src\app\page.tsx
import { Button } from "@nextui-org/button";
import { createServerComponentClient } from "@supabase/auth-helpers-nextjs";
import { cookies } from "next/headers";
import Link from "next/link";

import { ThemeSwitcher } from "@/components/ThemeSwitcher";

import LogoutButton from "../components/LogoutButton";

export default async function Index() {
  const supabase = createServerComponentClient({ cookies });

  const {
    data: { user },
  } = await supabase.auth.getUser();

  return (
    <div className="flex flex-col items-center w-full min-h-screen bg-background">
      <nav className="flex justify-center w-full h-16 border-b border-b-foreground/10">
        <div className="flex items-center justify-between w-full max-w-4xl p-3 text-sm text-foreground">
          <div />
          <div>
            {user ? (
              <div className="flex items-center gap-4">
                Hey, {user.email}!
                <LogoutButton />
              </div>
            ) : (
              <Link
                href="/login"
                className="px-4 py-2 no-underline rounded-md bg-btn-background hover:bg-btn-background-hover"
              >
                Login
              </Link>
            )}
          </div>
        </div>
      </nav>
      {/* 認証に関係のないエリア
      Buttonコンポーネントなどお試し用のエリア */}
      <h1 className="text-3xl font-bold underline">VNS.BLUE</h1>
      <Link href="/authui">authui</Link>

      <Button>Click me</Button>
      <ThemeSwitcher />
    </div>
  );
}

これで太陽と月のアイコンが表示されているはずです。
アイコンをクリックするとライトモードとダークモードが切り替わります。

これで動的なモード切り替えボタンの設定は終了です。

git tag addDarkmode


参考

NextUI - Beautiful, fast and modern React UI Library

pacocoursey/next-themes: Perfect Next.js dark mode in 2 lines of code. Support System preference and any other theme with no flashing

【Next.js】NextUI でダークモードに対応する方法


Supabase のローカル環境を構築

Docker Desktopを利用して
ローカル環境にSupabaseを導入します。

ローカル環境でPostgresのDBを触って開発します。
そしてそれをサーバーのDBに反映させる方法を取ります。

これらは↓この記事を見てください。

Supabase ローカル開発環境 + サーバー運用 2023年9月 (with Next.js 13 App router) - Qiita


Nextra ドキュメントジェネレーター

ドキュメントを作成するためのツールです。

ドキュメントのテーマ – Nextra

クイックスタートを利用してVercelにデプロイします。

アプリとドキュメントの2つのリポジトリ体制になったので
VSCodeのワークスペースを利用します。

リポジトリが2つになった時
それぞれ独立しているので別々に管理するか、
それとも一つにまとめて管理するのか。
を決める。

1つにする場合
VSCodeのワークスペースを利用する。

npm、yarn、pnpmのワークスペース機能を利用してモノレポ構成にする。

Turborepoというモノレポ専用ツールをつかう。

1つ目は2つのフォルダをまとめてVSCode上で管理する。
2つ目はパッケージ管理ツールを利用する。
3つ目は専用ツールを利用する。 他にはNx learn等がある。

最初の方ほど易しい。

今回はドキュメントにNextraというツールを使うだけです。
リポジトリを一つに管理すれば楽だけど、そもそもWebアプリにそのドキュメントなのでモノレポ構成にしても何の意味もない。
2つを一緒の場所に置くだけでいい。
これではモノレポにしてもgit管理が多少は楽になるだけ。
なので1番目を利用します。


ドキュメントのテーマ – Nextra

これは↑のDeployボタンを押すだけで公開されます。
あとはGitHub経由でローカル上でVSCodeを使って編集していくだけです。

特に難しいことはありません。

これでWebアプリと、その説明をするドキュメントの2つのリポジトリになりました。

これを一箇所で管理するためにVSCodeのワークスペース機能を使います。

開きたいフォルダを追加登録するだけで完成です。

VSCodeのファイルメニューから
フォルダをワークスペースに追加 から指定したフォルダを追加してきます。

保存すると
***.code-workspace
が作成されますので、
次回からはこれを開いてVSCodeを立ち上げれば良いだけです。

作成した
***.code-workspace
をダブルクリックで立ち上がります。

※編集する時は 現在のpathに注意する必要があります。
編集してからCLIを実行しようとしても、現在どこのRootにいるかを確認してください。

ターミナルをすべて閉じてから新しく開くと現在のファイルのある場所にターミナルが開きます。

現在開いているファイルのrootに、ターミナルが開きます。


vitest

テスト駆動開発に
vitest
React Testing Library
を利用します。


vitestを初めて導入するので
Next.js 公式サンプル with-vitest を参考に
vitestを追加します。

next.js/examples/with-vitest at canary · vercel/next.js

vitestではすべてのテストを __tests__ に置くこともできますし、
App Router 内で他のファイルと一緒に配置することもできます。

例えば ↓コンポーネントファイルのすぐ隣にテストファイルを置くことが出来ます。


src/component.tsx <<コンポーネントファイル
src/component.test.tsx <<コンポーネントのテストファイル

※この動作を確認するためにテストファイルを テストファイルを置く専用フォルダ(test)ではなく、 src/app内に一緒に置いています。

さらにすすめて ↑component.tsx ファイル内に テストコードを書くことも可能です。

参考
In-source testing | Guide | Vitest

インストール
npm i @vitejs/plugin-react
※このライブラリは -D フラグでインストールすると認識しない。

npm i server-only

npm i -D @testing-library/jest-dom @testing-library/react @testing-library/user-event @vitest/ui jsdom vitest

ツールのインストール

typesync

typesyncは、TypeScriptの型定義を調べてダウンロードしてくれます。
package.jsonを見て足りない型定義パッケージがあれば自動で追加してくれます。

インストール
npm i -D typesync

使い方
npx typesync

VSCode拡張機能

Vitest - Visual Studio Marketplace

この拡張機能を使用するためには、npm run test を実行させておく必要があります。
(vitest の ウォッチモード)

VSCodeのエディタ画面の行の左にGREENやREDのアイコンが表示されています。
左クリックでテストの実行
右クリックでメニューが開きます。

スクリプトの追加

package.json
    "test": "vitest",
    "test:ui": "vitest --ui",
    "coverage": "vitest run --coverage",

testはウォッチ形式でソースコードを保存するたびにテストが回ります。
test:uiはブラウザでテスト結果を表示してくれます。

vitestのコンフィグ設定

touch vitest.config.ts

vitest.config.ts
/// <reference types="vitest" />
import react from "@vitejs/plugin-react";
import { defineConfig } from "vitest/config";

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [react()],
  test: {
    globals: true,
    environment: "jsdom",
    include: ["app/**/*.test.{js,ts,jsx,tsx}"],
  },
});

App routerで書かれる 基本的な4種類のテストコード を調べます。

  1. 基本コンポーネントのテストコード
  2. Hooksコンポーネントのテストコード
  3. 動的フォルダのテストコード
  4. RSC(React Server Components)のテストコード

サンプル01 基礎 シンプルなクライアントコンポーネントとそのテストコード

mkdir src_tests_\client
touch src_tests_\client\page.tsx
touch src_tests_\client\page.test.tsx

src__tests__\client\page.tsx
"use client"
import React from "react"

export default function ClientComponent() {
  return <h1>Client Component</h1>
}

src__tests__\client\page.test.tsx
import { render, screen } from "@testing-library/react"
import React from "react"
import { expect, test } from "vitest"

import ClientComponent from "./page"

test("App Router: Works with Client Components", () => {
  render(<ClientComponent />)
  expect(
    screen.getByRole("heading", { level: 1, name: "Client Component" })
  ).toBeDefined()
})

テストの動作確認

npm test

ブラウザで表示

app/page.tsx
import ClientComponent from "@/__tests__/client/page"

・・・

      <h1 className="text-3xl font-bold underline">VNS.BLUE</h1>
      {/* テストとストーリーファイル 4種類 */}
      <ClientComponent />

動作確認

npm run dev

サンプル02 Hooksを使用したクライアントコンポーネントとそのテストコード

コンポーネントファイルの作成

mkdir src_tests_\components
touch src_tests_\components\component.tsx
touch src_tests_\components\component.test.tsx

src__tests__\components\component.tsx
"use client"

import React from "react"
import { useState } from "react"

const Counter = () => {
  const [count, setCount] = useState(0)
  return (
    <div>
      テスト用カウンター
      <h2>{count}</h2>
      <button type="button" onClick={() => setCount(count + 1)}>
        +
      </button>
    </div>
  )
}

export default Counter

useStateを使っているので 'use client'ディレクティブを付けます。

コンポーネントのテストファイル

src__tests__\components\component.test.tsx
import { render, screen, fireEvent } from "@testing-library/react"
import React from "react"
import { expect, test } from "vitest"

import Component from "./component"

test("App Router: Works with Client Components (React State)", () => {
  render(<Component />)
  expect(screen.getByRole("heading", { level: 2, name: "0" })).toBeDefined()
  fireEvent.click(screen.getByRole("button"))
  expect(screen.getByRole("heading", { level: 2, name: "1" })).toBeDefined()
})

テストの動作確認

npm test

コンポーネントをブラウザにも表示させます。

app\page.tsx
import Counter from "@/__tests__/components/component"

・・・

      {/* テストとストーリーファイル 4種類 */}
      <ClientComponent />
      <Counter />

動作確認

npm run dev

サンプル03 動的なルートセグメントを使用した場合のテスト

動的セグメントのテストなのでappフォルダの下に置く必要があります。

mkdir src\app\blog[slug]
touch src\app\blog[slug]\page.tsx
touch src\app\blog[slug]\page.test.tsx

src\app\blog[slug]\page.tsx
type Params = {
  params: {
    slug: string
  }
}

export async function generateMetadata({ params }: Params) {
  return { title: `Post: ${params.slug}` }
}

export default function Page({ params }: Params) {
  return <h1>Slug: {params.slug}</h1>
}

src\app\blog[slug]\page.test.tsx
import { render, screen } from "@testing-library/react"
import { expect, test } from "vitest"

import Page from "./page"

test("App Router: Works with dynamic route segments", () => {
  render(<Page params={{ slug: "Test" }} />)
  expect(
    screen.getByRole("heading", { level: 1, name: "Slug: Test" })
  ).toBeDefined()
})

テストの動作確認

npm test

このテストは、ReactコンポーネントPageが、動的なルートセグメントを使用して正しく動作することを確認するためのテストです。

テストでは、render()関数を使用してPageコンポーネントをレンダリングし、paramsプロパティに{ slug: "Test" }を渡しています。その後、screen.getByRole()関数を使用して、レンダリングされたコンポーネントから<h1>要素を取得し、そのテキストがSlug: Testであることを確認しています。

つまり、このテストは、Pageコンポーネントが、動的なルートセグメントを使用して、正しくslugパラメータを受け取り、表示することを確認しています。

ブラウザに表示します。

123がslugにあたり、動的なページ生成をしてくれます。

この機能を使うことで日付+タイトルといったURLを事前に用意しなくても動的にページが作成できます。

Topページを編集します。

app\page.tsx

      {/* テストとストーリーファイル 4種類 */}
      <ClientComponent />
      <Counter />
      <Link href={`/blog/${blogId}`}>Blogページ</Link>

動作確認

npm run dev

サンプル04 RSCのテスト

React server componentsのテスト

サーバーコンポーネントのテスト。

Hooksもインタラクティブな操作もないのでサーバーコンポーネントに出来ます。

mkdir src_tests_\rsc
touch src_tests_\rsc\page.test.tsx
touch src_tests_\rsc\page.tsx

app/rsc/page.tsx
// import 'server-only' does not currently
// work with Vitest

import React from "react"

export default function Page() {
  return <h1>App Router</h1>
}

server-onlyを使用することで完全にサーバーサイドでのみ実行されます。
しかし現在vitestで動きません。

app/rsc/page.test.tsx
import { render, screen } from "@testing-library/react"
import React from "react"
import { expect, test } from "vitest"

import Page from "./page"

test("App Router: Works with Server Components", () => {
  render(<Page />)
  expect(
    screen.getByRole("heading", { level: 1, name: "App Router" })
  ).toBeDefined()
})

テストの動作確認

npm test

topページに表示させます。

app\page.tsx
import Page from "@/__tests__/rsc/page"

      {/* テストとストーリーファイル 4種類 */}
      <ClientComponent />
      <Counter />
      <Link href={`/blog/${blogId}`}>Blogページ</Link>
      <Page />

動作確認

npm run dev

以上4つのコンポーネントとテストファイルでした。


Storybook

Storybookのコンポーネントやストーリーファイルは、公式サイトで公開されているため、そちらを参照することをおすすめします。

Component Encyclopedia | Storybook

ここにアクセスすると、多数のコンポーネントやストーリーファイルが公開されています。

ただし、古い場合は、SF3 (ストーリーファイル version 3) を使用していない場合があるため、SortをNewertにして参考にすることをおすすめします。

各コンポーネントのGitHubページにアクセスし、stories.tsxで検索することで、ストーリーファイルを見つけることができます。

インストール

npx storybook@latest init

実行方法

npm run storybook

Storybookの設定

Storybook でも TailwindCSS が使えるようにします。

import "../src/styles/globals.css"
を ↓ のファイルに追加します。

.storybook\preview.ts
import type { Preview } from "@storybook/react"
import "../src/styles/globals.css"

const preview: Preview = {
  parameters: {
    actions: { argTypesRegex: "^on[A-Z].*" },
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/,
      },
    },
  },
}

export default preview


定型文を自動生成

テンプレートからファイルを自動生成するツールplopをつかって

ベースとなる3種類の

  • 関数コンポーネントファイル
  • テストファイル
  • ストーリーファイル

をコマンド一つで作ります。

コードジェネレーター入門 コンポーネントの自動生成ツールplopを使用したハンズオン (Next.js app router Storybook TailwindCSS Jestの複数ファイルを同時に作る) - Qiita

テンプレートの書き方のルール

  • 拡張子

テンプレートファイルは .hbs という拡張子を使います。

  • パス

{{path}}

  • 名前

{{name}}

  • 名前の変更

パスカルケース
{{pascalCase name}}

CurrentUserItem のように書きます。
要素語( current user item )の最初を大文字で書き始めます。

例えば、nameプロパティが"foo-bar"の場合、{{ pascalCase name }}は"FooBar"になります。

ケバブケース
{{ kebabCase name}}

current-user-item のように書きます。
ハイフン で要素語( current user item )を連結します。

例えば、nameプロパティが"FooBar"の場合、{{ kebabCase name }}は"foo-bar"になります。

VSCode の拡張機能

File Templates - Visual Studio Marketplace
https://marketplace.visualstudio.com/items?itemName=SamKirkland.plop-templates

※同じ名前のVSCode拡張機能がいくつかあるので間違えないようにしてください。

この VSCode の拡張機能は、右クリックから plop のテンプレートを使ってコンポーネントを自動生成できるようになります。今のところマウスからでも自動生成できるようになるだけです。
設定ファイルでpathを固定しなければマウスで指定した場所に自動生成が出来るようになります。

この拡張機能はデフォルトで、グローバルにインストールされた plop を使用することを想定しています。

plop のインストール

ツールPlop を使うには設定ファイルとテンプレートファイルが必要です。

plop をグローバルにインストールします。

npm install -g plop

※後で紹介するplop専用のVSCode拡張機能を使用する場合、グローバルにインストールしておくと簡単に使用できます。そうでない場合、いくつかの設定が必要になる場合があります。」

plopの設定ファイル

設定ファイルはroot直下に置きます。

touch plopfile.mjs

plopfile.mjs
/* eslint-disable import/no-anonymous-default-export */
// ESLintのルールを無効にするためのコメントです。import/no-anonymous-default-exportというルールは、デフォルトエクスポートが無名関数である場合に警告を出すルールです。このルールを無効にすることで、このファイルでデフォルトエクスポートが無名関数であっても、警告が出なくなります。
export default function (
  // JSDocコメントを使用して、import('plop').NodePlopAPIという型を指定しています。これは、plopというライブラリが提供するNodePlopAPIという型をインポートしていることを示しています。この型は、plopfile.mjsで使用されるplopオブジェクトの型を定義しています。
  /** @type {import('plop').NodePlopAPI} */
  plop
) {
  plop.setGenerator("component", {
    description: "Create a new component",
    prompts: [
      {
        type: "input",
        name: "path",
        message: "どこにコンポーネントを置きますか?"
      },
      {
        type: "input",
        name: "name",
        message: "コンポーネントの名前を入力してください"
      },
      {
        type: "list",
        name: "componentType",
        message: "Component type",
        // サーバーコンポーネント、クライアントコンポーネント
        choices: ["server", "client"]
      }
    ],
    actions: [
      {
        type: "add",
        path: "src/components/{{componentType}}/{{path}}/{{pascalCase name}}/{{name}}.tsx",
        templateFile: "templates/component/component.tsx.hbs"
      },
      {
        type: "add",
        path: "src/components/{{componentType}}/{{path}}/{{pascalCase name}}/{{name}}.test.tsx",
        templateFile: "templates/component/component.test.tsx.hbs"
      },
      {
        type: "add",
        path: "src/components/{{componentType}}/{{path}}/{{pascalCase name}}/{{name}}.stories.tsx",
        templateFile: "templates/component/component.stories.tsx.hbs"
      }
    ]
  })
}

.prettierignore
# テンプレートファイル
**/*.hbs


.hbsの拡張子は、Prettierで対応していないため、コード整形の対象から外しました。

plop テンプレートファイルの保存場所

mkdir templates\component

このフォルダの中にテンプレートファイルを置きます。


templates\component[ファイル名].tsx.hbs


コンポーネントのテンプレートファイルを作成

touch templates\component\component.tsx.hbs

templates\component\component.tsx.hbs
// "use client";

import React from "react";
import { FC } from 'react'; export

default function {{pascalCase name}}() {
	return <h1>{{pascalCase name}}</h1>;
}

テストファイルのテンプレートファイルを作成

touch templates\component\component.test.tsx.hbs

templates\component\component.test.tsx.hbs
import { render, screen } from "@testing-library/react";
import React from "react";
import { expect, test } from "vitest";

import {{pascalCase name}} from "./{{name}}";

test("template component", () => {
  render(<{{pascalCase name}} />);
  expect(
    screen.getByRole("heading", { level: 1, name: "{{pascalCase name}}" })
  ).toBeDefined();
});

// // Vitestの基本構文
// describe("Vitest", () => {
//   beforeAll(() => {
//     // console.log("テストファイル開始前");
//   });
//   afterAll(() => {
//     // console.log("テストファイル終了後");
//   });

//   beforeEach(() => {
//     // console.log("テスト開始前");
//   });
//   afterEach(() => {
//     // console.log("テスト終了後");
//   });

//   test("マッチャー", () => {
//     expect(1 + 1).toBe(2);

//     expect({foo: "bar"}).toEqual({foo: "bar"});
//     expect([1, 2, 3]).toStrictEqual([1, 2, 3]);

//     expect(undefined).toBeUndefined();
//     expect("foo").toBeDefined();

//     expect(true).toBeTruthy();
//     expect(false).toBeFalsy();

//     expect(null).toBeNull();
//     expect("foo").not.toBeNull();

//     expect("foo").toHaveLength(3);
//     expect([1, 2, 3]).toHaveLength(3);

//     expect({foo: "bar", baz: "hoge"}).toHaveProperty("foo");
//     expect(["foo", "bar"]).toContain("foo");
//     expect([{foo: "bar"}, {foo: "hoge"}]).toContainEqual({foo: "bar"});
//     expect("foo12345").toMatch(/foo\d{5}/);

//     class CustomError extends Error {
//     }

//     const throwError = (message: string) => {
//       throw new CustomError(message);
//     };
//     expect(() => throwError("")).toThrow(); // エラーになることを検証
//     expect(() => throwError("")).toThrow(CustomError); // 送出したエラーの型判定
//   });

//   test.each`
//     unitPrice | quantity | expected
//     ${100}    | ${1}     | ${100}
//     ${150}    | ${2}     | ${300}
//     ${200}    | ${0}     | ${0}
//   `(
//     "パラメタライズドテスト:$unitPrice * $quantity = $expected",
//     ({unitPrice, quantity, expected}) => {
//       expect(unitPrice * quantity).toBe(expected);
//     }
//   );

//   test("モック", () => {
//     const mockFn = vi.fn((a: number) => a * 10);
//     mockFn(1);
//     mockFn(2);

//     expect(mockFn.mock.calls).toHaveLength(2);

//     expect(mockFn.mock.calls[0][0]).toBe(1); // 1回目の呼出の引数
//     expect(mockFn.mock.calls[1][0]).toBe(2); // 2回目の呼出の引数

//     expect(mockFn.mock.results[0].value).toBe(10); // 1回目の呼出の戻り値
//     expect(mockFn.mock.results[1].value).toBe(20); // 1回目の呼出の戻り値
//   });

//   test("Expectマッチャーユーティリティ", () => {
//     const obj = {
//       foo: "bar",
//       count: 10,
//       id: "123-456",
//       nested: {hoge: true, fuga: false},
//       array: [1, 2, 3],
//     };
//     expect(obj).toEqual({
//       foo: expect.any(String), // String
//       count: expect.anything(), // 値は何でもOK
//       id: expect.stringMatching(/\d{3}-\d{3}/), // 正規表現
//       nested: expect.objectContaining({hoge: true}), // 指定したkey-valueが含まれていること
//       array: expect.arrayContaining([1, 2]), // 配列に要素が含まれていること
//     });
//   });

//   test("スナップショットテスト", () => {
//     const html = `<div class="container">
//   <article>
//     <p class="title">UI生成結果</p>
//   </article>
// </div>`;
//     expect(html).toMatchSnapshot();
//   });
// });

Storybook ストーリーファイルのテンプレートファイルを作成

touch templates\component\component.stories.tsx.hbs

templates\component\component.stories.tsx.hbs
import type { Meta, StoryObj } from "@storybook/react";

// 作ったコンポーネントをインポートします。
import {{pascalCase name}}, { {{pascalCase name}}Props } from './{{name}}';

type T = typeof {{pascalCase name}}

const meta = {
  // Storybookのダッシュボードのサイドバーに表示されるタイトルを定義します。
  title: "{{componentType}}/{{pascalCase name}}",
  component: {{pascalCase name}},
  parameters: {},
  argTypes: {
  },
} satisfies Meta<T>;

export default meta;
type Story = StoryObj<T>;

// Storybookのダッシュボードのサイドバーに表示されるコンポーネントの色々なパーターンを定義します。
export const {{pascalCase name}}First: Story = {};
export const {{pascalCase name}}Second: Story = {};
export const {{pascalCase name}}Third: Story = {
    args : {
  },
};

動作確認

plop

plopコマンドでインタラクティブにコードの自動生成が行われます。
引数に フォルダ名、ファイル名をつけることも出来ます。

↓実際の動作例

> plop
? どこにコンポーネントを置きますか? mount
? コンポーネントの名前を入力してください basement
? Component type server
✔  ++ \src\components\server\mount\Basement\basement.tsx
✔  ++ \src\components\server\mount\Basement\basement.test.tsx
✔  ++ \src\components\server\mount\Basement\basement.stories.tsx

テストのウォッチモードとStorybookのダッシュボードを立ち上げます。

npm run test

npm run storybook

それぞれ動作の確認ができたらこの項目は終了です。

※plopの項目の最後に
テスト駆動開発をするのならばテンプレートを作るとき
わかりやすいエラーを入れておくといいと思います。
テスト駆動開発ならば他のテストをGREENにしてから
新しい次の開発につなげて、その最初のテストはREDに
したほうがいいとおもいます。

Consistency Made Simple : PLOP


テスト駆動開発

↓記事にしました。

基本的な Next.js 13 App router での vitest テストファイルの書き方 (テンプレートから始めるテスト駆動開発 Next.js 13 App router、 vitest 、 Storybook、 Plop) - Qiita

理想的なテスト駆動開発のサイクル

  1. コンポーネントの設計をします。

  2. plopを使い *.tsx (コンポーネントのテンプレートファイル)を作成します。同時に *.stories.js (Storybookファイル)と *.test.tsx (テストファイル)も作成します。

  3. テスト駆動開発 vitest を使ってサイクルを回します。

    • テストを書きます。 RED
    • コードを書きます。 GREEN
    • Storybookで動作確認をします。
    • リファクタリングを行います。 Refactoring
      完成に近づくまで繰り返します。
  4. 設計図通りに完成させます。

  5. コンポーネントを本体に取り込みます。

以上を繰り返します。

前提知識

コロケーション

簡単に書くと、同じ場所にコードとテストファイルを置いておいたほうが保守しやすくなるのではないかという考え方。

Next.js App router におけるサーバーコンポーネントとクライアントコンポーネント

Next.js で開発する上で知っておく知識

Next.jsでは基本的にコンポーネントはサーバーコンポーネントに設定されました。

クライアントコンポーネントとして使うには 最初の行に 'use client' というディレクティブをつけるなければなりません。

サーバーコンポーネントとクライアントコンポーネントの違いを知っておく必要があります。

Rendering: Composition Patterns | Next.js

何がしたいか? Server Component Client Component
データの取得
バックエンドのリソースに(直接)アクセスする。
サーバー上に機密情報を保持する(アクセストークン、APIキーなど)。
大きな依存関係をサーバに残す / クライアントサイドのJavaScriptを減らす。
インタラクティブ性とイベントリスナーを追加する (onClick()onChange()など)
ステートとライフサイクルエフェクトを使う (useState()useReducer()useEffect() など)
ブラウザ専用のAPIを使う
状態、エフェクト、またはブラウザ専用APIに依存するカスタムフックを使用する。
React Classのコンポーネントを使う。

サーバーコンポーネント側

データのフェッチ
バックエンドのリソースに(直接)アクセスする。
機密情報(アクセストークン、APIキーなど)をサーバーに保管する。
大きな依存関係をサーバに残す / クライアントサイドJavaScriptを減らす

クライアントコンポーネント側

インタラクティブ性とイベントリスナーの追加 (onClick()、onChange()など)
ステートとライフサイクルエフェクトの使用 (useState(), useReducer(), useEffect() など)
ブラウザのみのAPIを使用する。
ステート、エフェクト、またはブラウザ専用APIに依存するカスタムフックを使用する。
React クラスコンポーネントを使用する。

これでテンプレートとしては一旦完成のはず。
しかし、後から便利ツールのインストールは十分ありえる。

※コロケーションの考え方により、コンポーネントコードのそばにテストファイルとストーリーファイルを置きたいと思います。
実際使用する時は srcフォルダ を作ってその下を見てもらえるようにします。


テンプレート完成後のtodo

GitHub認証の実装

VNSの全体を設計する

開発設計

DB設計

画面設計

基本日本語で開発する

基本的な Next.js 13 App router での vitest テストファイルの書き方 (テンプレートから始めるテスト駆動開発 Next.js 13 App router、 vitest 、 Storybook、 Plop) - Qiita

理想的なテスト駆動開発のサイクル

  1. コンポーネントの設計をします。

  2. plopを使い *.tsx (コンポーネントのテンプレートファイル)を作成します。同時に *.stories.js (Storybookファイル)と *.test.tsx (テストファイル)も作成します。

  3. テスト駆動開発 vitest を使ってサイクルを回します。

    • テストを書きます。 RED
    • コードを書きます。 GREEN
    • Storybookで動作確認をします。
    • リファクタリングを行います。 Refactoring
      完成に近づくまで繰り返します。
  4. 設計図通りに完成させます。

  5. コンポーネントを本体に取り込みます。

以上を繰り返します。

漫画リストを基本に作る

DB設計は漫画から

トップ画面 言語を選択したらその言語がトップに移動する

終了

shadcnをインストール



shadcn

npx shadcn-ui@latest init
npx @shadcn/ui add

5個 インストール可能
10個 インストール不可能

( )   accordion
( )   alert
( )   alert-dialog
( )   aspect-ratio
( )   avatar
( )   badge
( )   button

X( ) calendar
( ) card
( ) checkbox
( ) collapsible
( ) command
( ) context-menu
( ) dialog
( ) dropdown-menu
( ) hover-card
( ) input
( ) label
( ) menubar
( ) navigation-menu
( ) popover
( ) progress
( ) radio-group
( ) scroll-area
( ) select
( ) separator
( ) sheet
( ) skeleton
( ) slider
( ) switch
( ) table
( ) tabs
( ) textarea
( ) toast
( ) toggle
( ) tooltip

calendarのインストール問題

Errors while adding the Calendar component · Issue #201 · shadcn-ui/ui
https://github.com/shadcn-ui/ui/issues/201

npx shadcn-ui@latest add calendar
↑最新版でインストールすると問題解決

テスト導入時期についての私見

0から作る場合は
最初は静的サイトを作る感じで
モックをつくるように、文字だけ、リンクだけ、レイアウトだけで
動的な要素は一切考えないでつくります。

この時にライブラリやコンポーネント集を組み合わせます。

この時期は色々画面構成をいじって使いやすいように
再構成を繰り返します。

一旦完成したら

DBを繋げて動的サイトにする時期に来ますが
このタイミングでDBからのデータを受け取れているか
テストで確認するのがいいと思います。

コロケーションテクニック

Route Groups に数字を使う。

フォルダ名をメニューの順番通りに並べるため
(01_メニューA)
(02_メニューB)
(03_メニューC)
(04_メニューD)
(05_メニューE)

とフォルダ名の前に数字を置くとWebアプリ側のメニューと揃えることが出来る。
Next.jsはフォルダ名を()で囲むとそれぞれ独立して扱えるので直感でメニューを探せる。

Routing: Route Groups | Next.js

今のところ不具合はない。

前提知識

コロケーション

簡単に書くと、同じ場所にコードとテストファイルを置いておいたほうが保守しやすくなるのではないかという考え方。

Next.js App router におけるサーバーコンポーネントとクライアントコンポーネント

Next.js で開発する上で知っておく知識

Next.jsでは基本的にコンポーネントはサーバーコンポーネントに設定されました。

クライアントコンポーネントとして使うには 最初の行に 'use client' というディレクティブをつけるなければなりません。

サーバーコンポーネントとクライアントコンポーネントの違いを知っておく必要があります。

Rendering: Composition Patterns | Next.js

何がしたいか? Server Component Client Component
データの取得
バックエンドのリソースに(直接)アクセスする。
サーバー上に機密情報を保持する(アクセストークン、APIキーなど)。
大きな依存関係をサーバに残す / クライアントサイドのJavaScriptを減らす。
インタラクティブ性とイベントリスナーを追加する (onClick()onChange()など)
ステートとライフサイクルエフェクトを使う (useState()useReducer()useEffect() など)
ブラウザ専用のAPIを使う
状態、エフェクト、またはブラウザ専用APIに依存するカスタムフックを使用する。
React Classのコンポーネントを使う。

サーバーコンポーネント側

データのフェッチ
バックエンドのリソースに(直接)アクセスする。
機密情報(アクセストークン、APIキーなど)をサーバーに保管する。
大きな依存関係をサーバに残す / クライアントサイドJavaScriptを減らす

クライアントコンポーネント側

インタラクティブ性とイベントリスナーの追加 (onClick()、onChange()など)
ステートとライフサイクルエフェクトの使用 (useState(), useReducer(), useEffect() など)
ブラウザのみのAPIを使用する。
ステート、エフェクト、またはブラウザ専用APIに依存するカスタムフックを使用する。
React クラスコンポーネントを使用する。

これでテンプレートとしては一旦完成のはず。
しかし、後から便利ツールのインストールは十分ありえる。

※コロケーションの考え方により、コンポーネントコードのそばにテストファイルとストーリーファイルを置きたいと思います。
実際使用する時は srcフォルダ を作ってその下を見てもらえるようにします。


GitHub認証 無料

Login with GitHub | Supabase Docs

GitHub ログインの設定は、次の 3 つの部分で構成されます。

  1. GitHub上で GitHub OAuth アプリを作成および構成する。
  2. GitHub OAuth キーをSupabase プロジェクトに追加します。
  3. ログイン コードをSupabase JS クライアント アプリに追加します。

プロバイダー トークンは、プロジェクトのデータベースには意図的に保存されません。
プロバイダー トークンは、プロジェクトのデータベースには意図的に保存されません。
プロバイダー トークンは、プロジェクトのデータベースには意図的に保存されません。

OAuthフローを完了したブラウザーの外でプロバイダートークンを使用したい場合は、
管理下の安全なサーバーにプロバイダートークンを手動で送信する必要があります。

1. GitHub Oauth アプリを作成する

GitHubへ移動
GitHubの左上の写真から setting
一番下のDeveloper settings
OAuth Apps

右上写真下のNew OAuth APpボタンを押します。

作成します。

その前にSupabase からコールバックの情報を取得します。

Supabase ダッシュボードへ移動
https://supabase.com/dashboard/project/gzctqdrrnnkaxwwtzbsw/auth/providers
https://supabase.com/dashboard/project/gzctqdrrnnkaxwwtzbsw/auth/providers

左側のサイドバーのAuthentication
Providersを選択。

GitHubを選択

設定します。

Client ID
まだ登録しません。

Client Secret
まだ登録しません。

Callback URL (for OAuth) <<固定
https://gzctqdrrnnkaxwwtzbsw.supabase.co/auth/v1/callback
https://gzctqdrrnnkaxwwtzbsw.supabase.co/auth/v1/callback

↑このコールバックを取得しGitHub側で登録します。

GitHubへ戻って作成の続きです。

Application name
vns.blue
vns.blue

Homepage URL
https://www.vns.blue/

Application description
vns

Authorization callback URL
https://gzctqdrrnnkaxwwtzbsw.supabase.co/auth/v1/callback
https://gzctqdrrnnkaxwwtzbsw.supabase.co/auth/v1/callback

登録すると
vns.blueのClient ID等が表示されます。

Supabase に戻ってGitHubで取得した値を登録します。

GitHub
vns
vns

Client ID


Client Secret 携帯認証が必要


Callback URL (for OAuth)
https://gzctqdrrnnkaxwwtzbsw.supabase.co/auth/v1/callback
https://gzctqdrrnnkaxwwtzbsw.supabase.co/auth/v1/callback

Saveボタンを押します。

以上でGitHubとSupabase間の認証の設定が終わりました。

実際にGitHubで認証をしてみる

仕組み#
ユーザがサインアップする。
Supabaseはauth.usersテーブルに新しいユーザーを作成する。

SupabaseはユーザーのUUIDを含む新しいJWTを返す。

データベースへの全てのリクエストはJWTを送信します。

PostgresはJWTを検査し、リクエストを行ったユーザーを特定します。

ユーザのUIDは、行へのアクセスを制限するポリシーで使用できます。

SupabaseはPostgresの特別な関数、auth.uid()を提供し、

JWTからユーザのUIDを抽出します。
これはポリシーを作成する際に特に便利です。

ユーザー管理
Supabaseはユーザーを認証・管理するための複数のエンドポイントを提供します:

サインアップ
パスワードによるサインイン
パスワードレス/ワンタイムパスワード(OTP)によるサインイン
OAuthによるサインイン
サインアウト

ユーザーがサインアップすると、
SupabaseはそのユーザーにユニークなIDを割り当てます。

このIDはデータベースのどこからでも参照できます。
例えば、user_idフィールドを使ってauth.usersテーブルのidを
参照するprofilesテーブルを作成することができます。

Redirect URLs | Supabase Docs

リダイレクトURL
Supabase AuthでリダイレクトURLを設定する。

概要#

パスワードレスサインインやサードパーティプロバイダを使用する場合、
Supabaseクライアントライブラリのメソッドには、

認証後にユーザをどこにリダイレクトさせるかを指定する
redirectToパラメータが用意されています。

デフォルトでは、ユーザはSITE_URLにリダイレクトされますが、
SITE_URLを変更したり、
リダイレクト先のURLを許可リストに追加したりすることができます。

必要なURLを許可リストに追加したら、
redirectToパラメータにユーザーをリダイレクトさせたいURLを
指定します。

リダイレクトURLにワイルドカードを使う#。
Supabaseでは、
リダイレクトURLを許可リストに追加する際にワイルドカードを指定することができます。

NetlifyやVercelのようなプロバイダからのプレビューURLをサポートするために、
ワイルドカードのマッチパターンを使うことができます。


Slack認証 無料

GitHubと似たような感じで登録できます。

Supabase から Callback URLを取得して Slackに登録
IDとパスワードを Slackから取得してSupabase に登録

参考
Supabase AuthでSlack認証を試してみた | DevelopersIO


Google認証 無料

Login with Google | Supabase Docs

Supabase ダッシュボードで Google プロバイダーを構成する。

Enable Sign in with Google をオンにする。

Client ID (for OAuth)

Client Secret (for OAuth)

Authorized Client IDs (for Android, One Tap, and Chrome extensions)

Callback URL (for OAuth)
https://gzctqdrrnnkaxwwtzbsw.supabase.co/auth/v1/callback

3 つの一般的な方法

Web の OAuth フローの使用
ネイティブサインインの使用
Chrome 拡張機能のネイティブ サインインの使用

可能な限り、ネイティブの「Google でサインイン」機能を
使用することがベスト プラクティスです。

「Googleでサインイン」を使用する前に、
GoogleCloudPlatformアカウントを
取得してプロジェクトを準備するか、
新しいプロジェクトを作成する必要があります。

話題のSupabaseでサクッとGoogle認証機能をつくってみた! - Qiita

GCP(Google Cloud Platform) で、Googleプロジェクトの作成

サインインをします。
右上のコンソールを押します。
画面遷移先左上のプロジェクトの選択を押します。
右上の新しいプロジェクトを押します。

プロジェクト名と場所を設定します。

プロジェクト名
vns-blue

場所
組織なし

作成ボタンを押します。

ダッシュボードのブラウザの最上段に作成したプロジェクト名がでていればOKです。

APIとサービス
OAuth同意画面
を選択します。

User Typeを選択します。
外部を選択し作成ボタンを押します。

アプリ登録の編集 画面になります。

アプリ名
vns.blue
vns.blue

ユーザー サポートメール

デベロッパーの連絡先情報

次の2つはそのままで大丈夫です。
認証の範囲
テストユーザー

ダッシュボードの左サイドバー
認証情報
+認証情報を作成
OAuthクライアントID
を選択します。

アプリケーションの種類
ウェブアプリケーションを選択します。

名前を入力します。
vns.blue
vns.blue

作成ボタンを押します。

ここで
クライアントIDとクライアント シークレット
が入手できます。

もう一度戻って
認証情報
OAuth 2.0 クライアント ID
先程登録したのを選択します。
編集ボタンを押します。(鉛筆のアイコン)

承認済みのリダイレクト URI
Supabase で取得した、承認済みのリダイレクト URIを入力します。
+URLを追加ボタンを押します。

登録します。

保存ボタンを押します。

Supabaseに戻って
クライアントIDとクライアント シークレットを登録します。

Save ボタンを押します。


この記事を書いているこのタイミングで
Supabase のコミュニティで認証のサンプルを公開しているのを見つけました。

supabase-community/supabase-by-example

メールでのマジックリンク
OAuth認証
これらのサンプルを見ることが出来ます。

この時点でテンプレートにメール認証とOAuthを複数実装できたことや認証の動作を確認しました。
この時点でテンプレートにメール認証とOAuthを複数実装できたことや認証の動作を確認しました。
この時点でテンプレートにメール認証とOAuthを複数実装できたことや認証の動作を確認しました。




【Next.js】管理者用ページを Route Groups で実現する
https://zenn.dev/chot/articles/next-layout-for-admin-page

app
└ (admin-only)
├── dashboard
│ └── page.tsx
├── settings
│ └── page.tsx
└── layout.tsx


SupabaseでTypeScriptを使う場合の準備

型ファイルの作成
DBのスキーマから型を生成します。
(型ファイルがすでにあったとしても上書きされます。)

npx supabase gen types typescript --project-id gzctqdrrnnkaxwwtzbsw > src/types/database.types.ts
npx supabase gen types typescript --project-id gzctqdrrnnkaxwwtzbsw > src/types/database.types.ts




この段階での制作方針

これまでは、オリジナルの機能が無い土台部分、つまりテンプレートを作成してきました。

ここからは、自分の作成したい機能を追加する段階に入ることになりました。

最初にやることは
静的でハードコーディングしたデータを使って枠だけ作ります。
最小限の機能に対する画面構成を作成します。文字列は固定です。Linkボタンと画面遷移を作成します。

最初から完成形は目指しません。
吊り橋を作成するように、最初は一本のロープを対岸に渡すように。
最小限の機能で十分です。そこの土台を作ってから後から拡張していきます。

使いやすい画面構成にする必要があります。なるべく直感に沿った画面構成とリンクを作成します。

自分は大まかな画面構成とリンクができるまで、テストもStorybookも作りません。
設計図を実際の画面に落とし込んでいる時期だからです。

紙の上の画面設計図をNext.jsのApp routerにそのまま反映できるようにした後、実際の使用感を見ながら調整していきます。

文字列などはハードコーディングしておきます。コンポーネント化している部分は、認証等のすでに出来上がっているものを組み込んだものをそのまま持ってきたからです。自作のコンポーネントを作る時に、ページから一部分を切り出す時にテストやStorybookを追加していこうと考えています。


TailwindCSS

簡単な装飾をします。

shadcn のコンポーネントを組み合わせてページの基礎を作る

Combobox
リストの絞り込み

Context Menu
右クリックメニュー

●Data Table
Table
解説を読む
作品リスト

Dialog
データ編集
自分は直感的に編集したい

Dropdown Menu
左クリックメニュー
セッティングページへの移動

●From
React Hook Formのラッパー
作品登録
リスト登録

Hover Card
補足説明

Input
入力の基本

Menubar
TopNav用?

Navigation Menu
TopNav用?

Popover
細かい調整用データ入力?

Radio Group
ラジオグループ
選択

Select
選択 リスト形式

Separator
セパレーター

Sheet
別ウィンドウが開いて
編集画面が出る

Skeleton
ローディング画面用

Slider
評価用

Switch
スイッチ切り替え用

Tabs
画面切り替え用

Textarea
テキスト表示
なるべく使いたくない

Toast
簡易メッセージ表示

Toggle
使い道がわからない

Tooltip
ホバーした時に表示


plop
テンプレート再考

フォルダを作る
コンポーネント置き場でも、他の場所でも置けるようにする。
page.tsxを作る
test
storybookを作る


DBのテーブル作成
DBにデータを取り出します。



4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3