compnents AuthForm.tsx
ユーザー認証フォームを提供し、ログイン、新規登録、パスワードリセットの機能を持っています。各関数や状態の役割を理解することで、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>
);
}
Discussion