aboutsummaryrefslogtreecommitdiffhomepage
path: root/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'frontend')
-rw-r--r--frontend/app/.server/auth.ts5
-rw-r--r--frontend/app/components/BorderedContainer.tsx13
-rw-r--r--frontend/app/components/InputText.tsx12
-rw-r--r--frontend/app/components/SubmitButton.tsx12
-rw-r--r--frontend/app/routes/_index.tsx25
-rw-r--r--frontend/app/routes/login.tsx150
6 files changed, 155 insertions, 62 deletions
diff --git a/frontend/app/.server/auth.ts b/frontend/app/.server/auth.ts
index 943f424..4df0924 100644
--- a/frontend/app/.server/auth.ts
+++ b/frontend/app/.server/auth.ts
@@ -32,9 +32,12 @@ export type User = components["schemas"]["User"];
// Remix's createCookie() returns "structured" cookies, which cannot be reused directly by non-Remix servers.
const tokenCookie = createUnstructuredCookie("albatross_token", cookieOptions);
+/**
+ * @throws Error on failure
+ */
export async function login(request: Request): Promise<never> {
const jwt = await authenticator.authenticate("default", request, {
- failureRedirect: request.url,
+ throwOnError: true,
});
const session = await sessionStorage.getSession(
diff --git a/frontend/app/components/BorderedContainer.tsx b/frontend/app/components/BorderedContainer.tsx
new file mode 100644
index 0000000..cbbfbde
--- /dev/null
+++ b/frontend/app/components/BorderedContainer.tsx
@@ -0,0 +1,13 @@
+import React from "react";
+
+type Props = {
+ children: React.ReactNode;
+};
+
+export default function BorderedContainer({ children }: Props) {
+ return (
+ <div className="bg-white border-2 border-pink-600 rounded-xl p-4">
+ {children}
+ </div>
+ );
+}
diff --git a/frontend/app/components/InputText.tsx b/frontend/app/components/InputText.tsx
new file mode 100644
index 0000000..3f2c526
--- /dev/null
+++ b/frontend/app/components/InputText.tsx
@@ -0,0 +1,12 @@
+import React from "react";
+
+type InputProps = React.InputHTMLAttributes<HTMLInputElement>;
+
+export default function InputText(props: InputProps) {
+ return (
+ <input
+ {...props}
+ className="p-2 block w-full border border-pink-600 rounded-md transition duration-300 focus:ring focus:ring-pink-400 focus:outline-none"
+ />
+ );
+}
diff --git a/frontend/app/components/SubmitButton.tsx b/frontend/app/components/SubmitButton.tsx
new file mode 100644
index 0000000..1400a7b
--- /dev/null
+++ b/frontend/app/components/SubmitButton.tsx
@@ -0,0 +1,12 @@
+import React from "react";
+
+type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement>;
+
+export default function SubmitButton(props: ButtonProps) {
+ return (
+ <button
+ {...props}
+ className="text-lg text-white bg-pink-600 px-4 py-2 rounded transition duration-300 hover:bg-pink-500 focus:ring focus:ring-pink-400 focus:outline-none"
+ />
+ );
+}
diff --git a/frontend/app/routes/_index.tsx b/frontend/app/routes/_index.tsx
index ec87e86..9f9c2bb 100644
--- a/frontend/app/routes/_index.tsx
+++ b/frontend/app/routes/_index.tsx
@@ -2,6 +2,7 @@ import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
import { Link } from "@remix-run/react";
import "@fortawesome/fontawesome-svg-core/styles.css";
import { ensureUserNotLoggedIn } from "../.server/auth";
+import BorderedContainer from "../components/BorderedContainer";
export const meta: MetaFunction = () => [
{ title: "iOSDC Japan 2024 Albatross.swift" },
@@ -23,19 +24,21 @@ export default function Index() {
<div className="text-center">
<div className="font-bold text-transparent bg-clip-text bg-gradient-to-r from-orange-400 via-pink-500 to-purple-400 flex flex-col gap-y-2">
<div className="text-3xl">iOSDC Japan 2024</div>
- <div className="text-6xl">
- Swift <wbr />
- Code Battle
- </div>
+ <div className="text-6xl">Swift Code Battle</div>
</div>
</div>
- <p className="text-gray-900 max-w-prose bg-white p-4 rounded-xl border-2 border-pink-600 mx-2">
- Swift コードバトルは指示された動作をする Swift
- コードをより短く書けた方が勝ち、という 1 対 1
- の対戦コンテンツです。8/22(木)day0 前夜祭では 8/12
- に実施された予選を勝ち抜いたプレイヤーによるトーナメント形式での Swift
- コードバトルを実施します。ここでは短いコードが正義です!可読性も保守性も放り投げた、イベントならではのコードをお楽しみください!
- </p>
+ <div className="mx-2">
+ <BorderedContainer>
+ <p className="text-gray-900 max-w-prose">
+ Swift コードバトルは指示された動作をする Swift
+ コードをより短く書けた方が勝ち、という 1 対 1
+ の対戦コンテンツです。8/22(木)day0 前夜祭では 8/12
+ に実施された予選を勝ち抜いたプレイヤーによるトーナメント形式での
+ Swift
+ コードバトルを実施します。ここでは短いコードが正義です!可読性も保守性も放り投げた、イベントならではのコードをお楽しみください!
+ </p>
+ </BorderedContainer>
+ </div>
<div>
<Link
to="/login"
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>
);
}