From 7089515e47fb6f7e9aa3db4bbbac2d3300ff0048 Mon Sep 17 00:00:00 2001
From: nsfisis <nsfisis@gmail.com>
Date: Sun, 11 Aug 2024 15:32:47 +0900
Subject: feat(frontend): improve error handling of login form

---
 frontend/app/routes/login.tsx | 150 ++++++++++++++++++++++++++++--------------
 1 file changed, 100 insertions(+), 50 deletions(-)

(limited to 'frontend/app/routes/login.tsx')

diff --git a/frontend/app/routes/login.tsx b/frontend/app/routes/login.tsx
index d6414f7..b1249e0 100644
--- a/frontend/app/routes/login.tsx
+++ b/frontend/app/routes/login.tsx
@@ -3,8 +3,11 @@ import type {
 	LoaderFunctionArgs,
 	MetaFunction,
 } from "@remix-run/node";
-import { Form, useLocation } from "@remix-run/react";
+import { Form, json, useActionData, useLocation } from "@remix-run/react";
 import { ensureUserNotLoggedIn, login } from "../.server/auth";
+import BorderedContainer from "../components/BorderedContainer";
+import InputText from "../components/InputText";
+import SubmitButton from "../components/SubmitButton";
 
 export const meta: MetaFunction = () => [
 	{ title: "Login | iOSDC Japan 2024 Albatross.swift" },
@@ -15,7 +18,42 @@ export async function loader({ request }: LoaderFunctionArgs) {
 }
 
 export async function action({ request }: ActionFunctionArgs) {
-	await login(request);
+	const formData = await request.clone().formData();
+	const username = String(formData.get("username"));
+	const password = String(formData.get("password"));
+	if (username === "" || password === "") {
+		return json(
+			{
+				message: "ユーザー名またはパスワードが誤っています",
+				errors: {
+					username:
+						username === "" ? "ユーザー名を入力してください" : undefined,
+					password:
+						password === "" ? "パスワードを入力してください" : undefined,
+				},
+			},
+			{ status: 400 },
+		);
+	}
+
+	try {
+		await login(request);
+	} catch (error) {
+		if (error instanceof Error) {
+			return json(
+				{
+					message: error.message,
+					errors: {
+						username: undefined,
+						password: undefined,
+					},
+				},
+				{ status: 400 },
+			);
+		} else {
+			throw error;
+		}
+	}
 	return null;
 }
 
@@ -24,56 +62,68 @@ export default function Login() {
 	const searchParams = new URLSearchParams(location.search);
 	const registrationToken = searchParams.get("registration_token");
 
+	const loginErrors = useActionData<typeof action>();
+
 	return (
 		<div className="min-h-screen bg-gray-100 flex items-center justify-center">
-			<Form
-				method="post"
-				className="bg-white p-8 rounded shadow-md w-full max-w-sm"
-			>
-				<h2 className="text-2xl font-bold mb-6 text-center">Login</h2>
-				<div className="mb-4">
-					<label
-						htmlFor="username"
-						className="block text-sm font-medium text-gray-700"
-					>
-						Username
-					</label>
-					<input
-						type="text"
-						name="username"
-						id="username"
-						required
-						className="mt-1 p-2 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
-					/>
-				</div>
-				<div className="mb-6">
-					<label
-						htmlFor="password"
-						className="block text-sm font-medium text-gray-700"
-					>
-						Password
-					</label>
-					<input
-						type="password"
-						name="password"
-						id="password"
-						autoComplete="current-password"
-						required
-						className="mt-1 p-2 block w-full border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500"
-					/>
-				</div>
-				<input
-					type="hidden"
-					name="registration_token"
-					value={registrationToken ?? ""}
-				/>
-				<button
-					type="submit"
-					className="w-full bg-blue-500 text-white py-2 rounded hover:bg-blue-600 transition duration-300"
-				>
-					Log In
-				</button>
-			</Form>
+			<div className="mx-2">
+				<BorderedContainer>
+					<Form method="post" className="w-full max-w-sm p-2">
+						<h2 className="text-2xl mb-6 text-center">
+							fortee アカウントでログイン
+						</h2>
+						<p className="text-sm mb-4">
+							fortee
+							のアカウントをお持ちでない場合は、イベントスタッフにお声がけください。
+						</p>
+						{loginErrors?.message && (
+							<p className="text-red-500 text-sm mb-4">{loginErrors.message}</p>
+						)}
+						<div className="mb-4 flex flex-col gap-1">
+							<label
+								htmlFor="username"
+								className="block text-sm font-medium text-gray-700"
+							>
+								ユーザー名
+							</label>
+							<InputText type="text" name="username" id="username" required />
+							{loginErrors?.errors?.username && (
+								<p className="text-red-500 text-sm">
+									{loginErrors.errors.username}
+								</p>
+							)}
+						</div>
+						<div className="mb-6 flex flex-col gap-1">
+							<label
+								htmlFor="password"
+								className="block text-sm font-medium text-gray-700"
+							>
+								パスワード
+							</label>
+							<InputText
+								type="password"
+								name="password"
+								id="password"
+								autoComplete="current-password"
+								required
+							/>
+							{loginErrors?.errors?.password && (
+								<p className="text-red-500 text-sm">
+									{loginErrors.errors.password}
+								</p>
+							)}
+						</div>
+						<input
+							type="hidden"
+							name="registration_token"
+							value={registrationToken ?? ""}
+						/>
+						<div className="flex justify-center">
+							<SubmitButton type="submit">ログイン</SubmitButton>
+						</div>
+					</Form>
+				</BorderedContainer>
+			</div>
 		</div>
 	);
 }
-- 
cgit v1.2.3-70-g09d2

