🐷

compnents AuthForm.tsx

2025/03/13に公開

ユーザー認証フォームを提供し、ログイン、新規登録、パスワードリセットの機能を持っています。各関数や状態の役割を理解することで、Reactコンポーネントの構築方法や状態管理の方法を学ぶことができます。

import React, { useState } from 'react'; // ReactとuseStateフックをインポート
import { supabase } from '../lib/supabase'; // Supabaseクライアントをインポート
import { LogIn, UserPlus, Loader2, KeyRound, Mail, Eye, EyeOff, AlertCircle } from 'lucide-react'; // アイコンをインポート

interface AuthFormProps { // AuthFormコンポーネントのプロパティ型を定義
onSuccess: () => void; // 認証成功時に呼ばれるコールバック関数
}

export function AuthForm({ onSuccess }: AuthFormProps) { // AuthFormコンポーネントを定義
const [isLogin, setIsLogin] = useState(true); // ログインモードか新規登録モードかを管理する状態
const [isResetPassword, setIsResetPassword] = useState(false); // パスワードリセットモードかどうかを管理する状態
const [email, setEmail] = useState(''); // メールアドレスを管理する状態
const [password, setPassword] = useState(''); // パスワードを管理する状態
const [showPassword, setShowPassword] = useState(false); // パスワードの表示/非表示を管理する状態
const [loading, setLoading] = useState(false); // ローディング状態を管理
const [error, setError] = useState<string | null>(null); // エラーメッセージを管理
const [message, setMessage] = useState<string | null>(null); // 成功メッセージを管理
const [showVerificationMessage, setShowVerificationMessage] = useState(false); // メール認証メッセージの表示を管理
const [validationErrors, setValidationErrors] = useState<{ // バリデーションエラーを管理
email?: string;
password?: string;
}>({});

const validateEmail = (email: string) => { // メールアドレスのバリデーション関数
const emailRegex = /[1]+@[a-zA-Z0-9.-]+.[a-zA-Z]{2,}$/; // メールアドレスの正規表現
if (!emailRegex.test(email)) { // 正規表現にマッチしない場合
return '有効なメールアドレスを入力してください'; // エラーメッセージを返す
}
return ''; // エラーがない場合は空文字を返す
};

const validatePassword = (password: string) => { // パスワードのバリデーション関数
if (password.length < 8) { // パスワードが8文字未満の場合
return 'パスワードは8文字以上で入力してください'; // エラーメッセージを返す
}
if (!/[2]+$/.test(password)) { // パスワードが半角英数字以外を含む場合
return 'パスワードは半角英数字のみ使用できます'; // エラーメッセージを返す
}
if (!/[A-Za-z]/.test(password) || !/[0-9]/.test(password)) { // パスワードに英字と数字が含まれていない場合
return 'パスワードは英字と数字を含める必要があります'; // エラーメッセージを返す
}
return ''; // エラーがない場合は空文字を返す
};

const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => { // メールアドレス変更時のハンドラ
const newEmail = e.target.value; // 入力されたメールアドレスを取得
setEmail(newEmail); // メールアドレスの状態を更新
const error = validateEmail(newEmail); // メールアドレスのバリデーションを実行
setValidationErrors(prev => ({ ...prev, email: error })); // バリデーションエラーを更新
};

const handlePasswordChange = (e: React.ChangeEvent<HTMLInputElement>) => { // パスワード変更時のハンドラ
const newPassword = e.target.value; // 入力されたパスワードを取得
setPassword(newPassword); // パスワードの状態を更新
if (!isResetPassword) { // パスワードリセットモードでない場合
const error = validatePassword(newPassword); // パスワードのバリデーションを実行
setValidationErrors(prev => ({ ...prev, password: error })); // バリデーションエラーを更新
}
};

const getErrorMessage = (error: Error): string => { // エラーメッセージを取得する関数
if (error.message === 'Invalid login credentials') { // ログイン情報が無効な場合
return 'メールアドレスまたはパスワードが正しくありません。メール認証が完了していない場合は、メール内のリンクをクリックしてください。'; // エラーメッセージを返す
}
if (error.message === 'User already registered') { // ユーザーが既に登録されている場合
return 'このメールアドレスは既に登録されています'; // エラーメッセージを返す
}
if (error.message === 'Email not confirmed') { // メールが確認されていない場合
return 'メール認証が完了していません。確認メール内のリンクをクリックしてください。'; // エラーメッセージを返す
}
return '認証エラーが発生しました'; // その他のエラーの場合
};

const handleSubmit = async (e: React.FormEvent) => { // フォーム送信時のハンドラ
e.preventDefault(); // デフォルトのフォーム送信を防止

// バリデーションチェック
const emailError = validateEmail(email); // メールアドレスのバリデーションを実行
const passwordError = !isResetPassword ? validatePassword(password) : ''; // パスワードのバリデーションを実行

if (emailError || passwordError) { // バリデーションエラーがある場合
  setValidationErrors({
    email: emailError,
    password: passwordError,
  });
  return; // 処理を中断
}

setLoading(true); // ローディング状態を開始
setError(null); // エラーメッセージをクリア
setMessage(null); // 成功メッセージをクリア

try {
  if (isResetPassword) { // パスワードリセットモードの場合
    const { error } = await supabase.auth.resetPasswordForEmail(email, {
      redirectTo: `${window.location.origin}/reset-password`, // パスワードリセット後のリダイレクトURL
    });
    if (error) throw error; // エラーが発生した場合は例外をスロー
    setMessage('パスワード再設定用のメールを送信しました。メールをご確認ください。'); // 成功メッセージを設定
  } else if (isLogin) { // ログインモードの場合
    const { error, data } = await supabase.auth.signInWithPassword({
      email,
      password,
    });
    if (error) throw error; // エラーが発生した場合は例外をスロー
    
    // メール認証が完了しているか確認
    if (!data.user?.email_confirmed_at) {
      throw new Error('Email not confirmed'); // メールが確認されていない場合は例外をスロー
    }
    
    onSuccess(); // 認証成功時のコールバックを呼び出し
  } else { // 新規登録モードの場合
    const { error } = await supabase.auth.signUp({
      email,
      password,
      options: {
        emailRedirectTo: `${window.location.origin}`, // メール認証後のリダイレクトURL
      }
    });
    
    if (error) throw error; // エラーが発生した場合は例外をスロー
    
    setShowVerificationMessage(true); // メール認証メッセージを表示
    setEmail(''); // メールアドレスをクリア
    setPassword(''); // パスワードをクリア
  }
} catch (err) {
  setError(err instanceof Error ? getErrorMessage(err) : '認証エラーが発生しました'); // エラーメッセージを設定
} finally {
  setLoading(false); // ローディング状態を終了
}

};

const handleBackToLogin = () => { // ログイン画面に戻るハンドラ
setIsResetPassword(false); // パスワードリセットモードを解除
setShowVerificationMessage(false); // メール認証メッセージを非表示
setError(null); // エラーメッセージをクリア
setMessage(null); // 成功メッセージをクリア
setValidationErrors({}); // バリデーションエラーをクリア
};

if (showVerificationMessage) { // メール認証メッセージが表示されている場合
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-6">
<div className="w-full max-w-md">
<div className="bg-white rounded-xl shadow-lg p-8 text-center">
<Mail className="w-16 h-16 text-indigo-600 mx-auto mb-6" />
<h2 className="text-2xl font-bold text-gray-900 mb-4">
メール認証を完了してください
</h2>
<p className="text-gray-600 mb-6">
{email} に確認メールを送信しました。

メール内のリンクをクリックして、アカウントを有効化してください。
</p>
<button
onClick={handleBackToLogin}
className="text-indigo-600 hover:text-indigo-800 font-medium"
>
ログイン画面に戻る
</button>
</div>
</div>
</div>
);
}

return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 flex items-center justify-center p-6">
<div className="w-full max-w-md">
<div className="text-center mb-8">
<h1 className="text-4xl font-bold text-indigo-900 mb-3">Age</h1>
<p className="text-gray-600 text-lg">誕生日を登録して自動計算</p>
</div>

    <div className="bg-white rounded-xl shadow-lg p-8">
      {!isResetPassword && (
        <div className="flex justify-between items-center mb-8">
          <button
            onClick={() => setIsLogin(true)}
            className={`flex-1 py-3 text-lg font-medium ${
              isLogin
                ? 'text-indigo-600 border-b-2 border-indigo-600'
                : 'text-gray-500 border-b border-gray-200'
            }`}
          >
            ログイン
          </button>
          <button
            onClick={() => setIsLogin(false)}
            className={`flex-1 py-3 text-lg font-medium ${
              !isLogin
                ? 'text-indigo-600 border-b-2 border-indigo-600'
                : 'text-gray-500 border-b border-gray-200'
            }`}
          >
            新規登録
          </button>
        </div>
      )}

      {isResetPassword && (
        <div className="mb-8">
          <h2 className="text-2xl font-bold text-gray-900 mb-2">パスワードをリセット</h2>
          <p className="text-gray-600">
            登録したメールアドレスを入力してください。パスワード再設定用のリンクを送信します。
          </p>
        </div>
      )}

      <form onSubmit={handleSubmit} className="space-y-6">
        <div>
          <label className="block text-lg font-medium text-gray-700 mb-2">
            メールアドレス
          </label>
          <input
            type="email"
            value={email}
            onChange={handleEmailChange}
            className={`w-full px-4 py-3 text-lg border rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 ${
              validationErrors.email ? 'border-red-500' : 'border-gray-300'
            }`}
            placeholder="example@email.com"
            required
          />
          {validationErrors.email && (
            <div className="mt-2 text-red-600 text-sm flex items-center">
              <AlertCircle className="w-4 h-4 mr-1" />
              {validationErrors.email}
            </div>
          )}
        </div>
        {!isResetPassword && (
          <div>
            <label className="block text-lg font-medium text-gray-700 mb-2">
              パスワード
            </label>
            <div className="relative">
              <input
                type={showPassword ? "text" : "password"}
                value={password}
                onChange={handlePasswordChange}
                className={`w-full px-4 py-3 text-lg border rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 ${
                  validationErrors.password ? 'border-red-500' : 'border-gray-300'
                }`}
                placeholder="8文字以上の半角英数字"
                required
              />
              <button
                type="button"
                onClick={() => setShowPassword(!showPassword)}
                className="absolute right-3 top-1/2 transform -translate-y-1/2 text-gray-500 hover:text-gray-700"
              >
                {showPassword ? (
                  <EyeOff className="w-5 h-5" />
                ) : (
                  <Eye className="w-5 h-5" />
                )}
              </button>
            </div>
            {validationErrors.password && (
              <div className="mt-2 text-red-600 text-sm flex items-center">
                <AlertCircle className="w-4 h-4 mr-1" />
                {validationErrors.password}
              </div>
            )}
            {!isLogin && !validationErrors.password && (
              <p className="mt-2 text-sm text-gray-500">
                ※ パスワードは8文字以上の半角英数字で、英字と数字を含める必要があります
              </p>
            )}
          </div>
        )}

        {error && (
          <div className="p-3 bg-red-50 text-red-700 rounded-lg text-base">
            {error}
          </div>
        )}

        {message && (
          <div className="p-3 bg-green-50 text-green-700 rounded-lg text-base">
            {message}
          </div>
        )}

        <button
          type="submit"
          disabled={loading || Object.keys(validationErrors).some(key => validationErrors[key as keyof typeof validationErrors])}
          className="w-full bg-indigo-600 text-white py-3 px-6 rounded-lg hover:bg-indigo-700 transition duration-200 flex items-center justify-center text-lg font-medium disabled:opacity-50 disabled:cursor-not-allowed"
        >
          {loading ? (
            <Loader2 className="w-5 h-5 animate-spin" />
          ) : isResetPassword ? (
            <>
              <KeyRound className="w-5 h-5 mr-2" />
              パスワードリセットメールを送信
            </>
          ) : isLogin ? (
            <>
              <LogIn className="w-5 h-5 mr-2" />
              ログイン
            </>
          ) : (
            <>
              <UserPlus className="w-5 h-5 mr-2" />
              アカウント作成
            </>
          )}
        </button>

        <div className="text-center space-y-3">
          {!isResetPassword && isLogin && (
            <button
              type="button"
              onClick={() => setIsResetPassword(true)}
              className="text-indigo-600 hover:text-indigo-800 text-base"
            >
              パスワードをお忘れの方
            </button>
          )}
          {isResetPassword && (
            <button
              type="button"
              onClick={handleBackToLogin}
              className="text-indigo-600 hover:text-indigo-800 text-base"
            >
              ログイン画面に戻る
            </button>
          )}
        </div>
      </form>
    </div>
  </div>
</div>

);
}

脚注
  1. a-zA-Z0-9._%+- ↩︎

  2. a-zA-Z0-9 ↩︎

Discussion