You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
127 lines
4.4 KiB
127 lines
4.4 KiB
5 months ago
|
/**
|
||
|
* ユーザーログイン機能を提供するページコンポーネント
|
||
|
* ユーザー名とパスワードによる認証フォームを表示し、認証APIと連携
|
||
|
*/
|
||
|
import React, { useState } from 'react';
|
||
|
import { useNavigate } from 'react-router-dom';
|
||
|
import {
|
||
|
Container,
|
||
|
Box,
|
||
|
Typography,
|
||
|
TextField,
|
||
|
Button,
|
||
|
Paper,
|
||
|
Alert,
|
||
|
Link,
|
||
|
Grid,
|
||
|
} from '@mui/material';
|
||
|
import { LoginCredentials } from '../types/types';
|
||
|
import { authApi } from '../services/api';
|
||
|
import { GENERAL_ERRORS } from '../constants/errorMessages';
|
||
|
|
||
|
const LoginPage: React.FC = () => {
|
||
|
const navigate = useNavigate();
|
||
|
// ログイン情報の状態管理
|
||
|
const [credentials, setCredentials] = useState<LoginCredentials>({
|
||
|
username: '',
|
||
|
password: '',
|
||
|
});
|
||
|
// エラーメッセージの状態管理
|
||
|
const [error, setError] = useState<string>('');
|
||
|
|
||
|
/**
|
||
|
* フォーム入力値の変更を処理するハンドラー
|
||
|
* 入力フィールドの変更をcredentials状態に反映
|
||
|
*/
|
||
|
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
|
const { name, value } = e.target;
|
||
|
setCredentials(prev => ({
|
||
|
...prev,
|
||
|
[name]: value, // 動的にプロパティ名を使用して状態を更新
|
||
|
}));
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* ログインフォームの送信を処理するハンドラー
|
||
|
* 認証APIを呼び出し、成功時はトークンを保存してタスク一覧ページに遷移
|
||
|
* 失敗時はエラーメッセージを表示
|
||
|
*/
|
||
|
const handleSubmit = async (e: React.FormEvent) => {
|
||
|
e.preventDefault(); // フォームのデフォルト送信動作を防止
|
||
|
try {
|
||
|
const response = await authApi.login(credentials);
|
||
|
localStorage.setItem('token', response.token); // 認証トークンをローカルストレージに保存
|
||
|
navigate('/tasks'); // タスク一覧ページにリダイレクト
|
||
|
} catch (err) {
|
||
|
setError(err instanceof Error ? err.message : GENERAL_ERRORS.UNEXPECTED_ERROR);
|
||
|
}
|
||
|
};
|
||
|
|
||
|
return (
|
||
|
<Container maxWidth="md" sx={{ mt: 8, height: '100vh'}}>
|
||
|
<Grid container justifyContent="center" alignItems="center" sx={{ height: '100%' }}>
|
||
|
<Grid item xs={12} md={5}>
|
||
|
<Paper elevation={3} sx={{ p: 4 }}>
|
||
|
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
|
||
|
<Typography component="h1" variant="h5" gutterBottom>
|
||
|
ToDoアプリ ログイン
|
||
|
</Typography>
|
||
|
{/* エラーがある場合のみアラートを表示 */}
|
||
|
{error && (
|
||
|
<Alert severity="error" sx={{ mb: 2, width: '100%' }}>
|
||
|
{error}
|
||
|
</Alert>
|
||
|
)}
|
||
|
{/* ログインフォーム */}
|
||
|
<Box component="form" onSubmit={handleSubmit} sx={{ width: '100%' }}>
|
||
|
{/* ユーザー名入力フィールド */}
|
||
|
<TextField
|
||
|
margin="normal"
|
||
|
required
|
||
|
fullWidth
|
||
|
id="username"
|
||
|
label="ユーザー名"
|
||
|
name="username"
|
||
|
autoComplete="username"
|
||
|
autoFocus
|
||
|
value={credentials.username}
|
||
|
onChange={handleChange}
|
||
|
/>
|
||
|
{/* パスワード入力フィールド */}
|
||
|
<TextField
|
||
|
margin="normal"
|
||
|
required
|
||
|
fullWidth
|
||
|
name="password"
|
||
|
label="パスワード"
|
||
|
type="password"
|
||
|
id="password"
|
||
|
autoComplete="current-password"
|
||
|
value={credentials.password}
|
||
|
onChange={handleChange}
|
||
|
/>
|
||
|
{/* ログインボタン */}
|
||
|
<Button
|
||
|
type="submit"
|
||
|
fullWidth
|
||
|
variant="contained"
|
||
|
sx={{ mt: 3, mb: 2 }}
|
||
|
>
|
||
|
ログイン
|
||
|
</Button>
|
||
|
{/* 新規登録ページへのリンク */}
|
||
|
<Box sx={{ textAlign: 'center' }}>
|
||
|
<Link href="/register" variant="body2">
|
||
|
アカウントをお持ちでない方はこちら
|
||
|
</Link>
|
||
|
</Box>
|
||
|
</Box>
|
||
|
</Box>
|
||
|
</Paper>
|
||
|
</Grid>
|
||
|
</Grid>
|
||
|
</Container>
|
||
|
);
|
||
|
};
|
||
|
|
||
|
export default LoginPage;
|