Skip to content

Commit

Permalink
Merge pull request #308 from sohosai/feat/password-reset
Browse files Browse the repository at this point in the history
パスワードリセットを実装
  • Loading branch information
appare45 authored Apr 26, 2024
2 parents e91b75f + 72ab772 commit 9eb367d
Show file tree
Hide file tree
Showing 9 changed files with 186 additions and 33 deletions.
10 changes: 9 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"firebase": "^10.9.0",
"graphemer": "^1.4.0",
"jotai": "^2.7.1",
"jotai-location": "^0.5.4",
"next": "14.1.4",
"openapi-fetch": "^0.9.3",
"openapi-typescript-helpers": "^0.0.7",
Expand Down
1 change: 0 additions & 1 deletion src/common_components/Button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ export const Button: FC<Props> = ({ color, size = "medium", className, children,
const button = cva({
base: {
borderRadius: "sm",
display: "block",
cursor: "pointer",
borderWidth: 2,
borderStyle: "solid",
Expand Down
25 changes: 22 additions & 3 deletions src/common_components/auth/AuthUI.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
"use client";
import React, { FC, PropsWithChildren } from "react";
import { Header } from "@/common_components/header/Header";
import SigninPage from "@/common_components/auth/signin/page";
import SignupPage from "@/common_components/auth/signup/page";
import { atom, useAtomValue } from "jotai";
import { useAtomValue } from "jotai";
import { useAuthState } from "@/lib/firebase";
import { Loading } from "@/common_components/Loading";
import { css } from "@styled-system/css";
import { EmailVerification } from "@/common_components/auth/EmailVerification";
import { usePathname } from "next/navigation";
import { atomWithHash } from "jotai-location";
import { ResetPassword } from "./resetPassword/ResetPassword";

export const authModeAtom = atom<"signIn" | "signUp">("signIn");
export const authModeAtom = atomWithHash<"signIn" | "signUp" | "resetPassword">("authMode", "signIn");

export const AuthUI: FC<PropsWithChildren> = ({ children }) => {
const authMode = useAtomValue(authModeAtom);
Expand All @@ -27,6 +30,22 @@ export const AuthUI: FC<PropsWithChildren> = ({ children }) => {
return children;
}

const Component: React.FC = () => {
switch (authMode) {
case "signIn":
return <SigninPage />;

case "signUp":
return <SignupPage />;

case "resetPassword":
return <ResetPassword />;

default:
return <></>;
}
};

return (
<>
<Header />
Expand All @@ -40,7 +59,7 @@ export const AuthUI: FC<PropsWithChildren> = ({ children }) => {
) : authState.user ? (
<>{authState.user.emailVerified ? <>{children}</> : <EmailVerification />}</>
) : (
<>{authMode === "signIn" ? <SigninPage /> : <SignupPage />}</>
<Component />
)}
</>
);
Expand Down
34 changes: 18 additions & 16 deletions src/common_components/auth/EmailVerification.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { center } from "@styled-system/patterns";
import { css } from "@styled-system/css";
import { Button } from "@/common_components/Button";
import { css, cx } from "@styled-system/css";
import SendButton from "@/assets/SendButton.svg?url";
import Image from "next/image";
import { sendEmailVerification } from "firebase/auth";
import { useAuthState } from "@/lib/firebase";
import { useState } from "react";
import toast from "react-hot-toast";
import { buttonStyle } from "@/recipes/button";

export const EmailVerification = () => {
const authState = useAuthState();
Expand Down Expand Up @@ -58,24 +58,26 @@ export const EmailVerification = () => {
</section>
<p className={css({ color: "gray.700" })}>再送しても届かない場合は時間を空けてからお試しください</p>
<div className={css({ display: "flex", flexDir: "column", gap: 2 })}>
<Button
color={"secondary"}
className={css({
alignSelf: "center",
display: "flex",
alignItems: "flex-end",
gap: 2,
_disabled: {
opacity: 0.5,
cursor: "default",
"&:hover": { opacity: 0.5 },
},
})}
<button
className={cx(
buttonStyle({ visual: "outline", color: "purple" }),
css({
alignSelf: "center",
display: "flex!",
alignItems: "flex-end",
gap: 2,
_disabled: {
opacity: 0.5,
cursor: "default",
"&:hover": { opacity: 0.5 },
},
}),
)}
onClick={handleResend}
disabled={isSent}>
<span>確認メールを再送する</span>
<Image src={SendButton} alt="" width={20} height={20} />
</Button>
</button>
{isSent && (
<p className={css({ color: "gray.700", fontSize: "sm" })}>
確認メールを再送しました
Expand Down
104 changes: 104 additions & 0 deletions src/common_components/auth/resetPassword/ResetPassword.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { basicErrorMessageStyle, basicFormStyle } from "@/common_components/formFields/styles";
import { buttonStyle } from "@/recipes/button";
import { css, cx } from "@styled-system/css";
import { center, vstack } from "@styled-system/patterns";
import { getAuth, sendPasswordResetEmail } from "firebase/auth";
import { useState } from "react";
import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
import Image from "next/image";
import Triangle from "@/assets/Triangle.svg?url";
import { useSetAtom } from "jotai";
import { authModeAtom } from "@/common_components/auth/AuthUI";
const ResetPasswordForm: React.FC = () => {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<{ email: string }>();
const [isSent, setIsSent] = useState(false);
const onSubmit = (data: { email: string }) => {
toast.promise(sendPasswordResetEmail(getAuth(), data.email), {
loading: "メールを送信しています",
success: () => {
setIsSent(true);
return "メールを送信しました";
},
error: "メールの送信に失敗しました",
});
};
return (
<form onSubmit={handleSubmit(onSubmit)} className={vstack({ justifyContent: "center", gap: 3, width: 72 })}>
<label className={css({ width: "full" })}>
<span className={css({ fontWeight: "bold" })}>メールアドレス</span>
<input
type="email"
className={cx(basicFormStyle(), css({ width: "full" }))}
aria-invalid={errors.email ? "true" : "false"}
placeholder="[email protected]"
{...register("email", {
required: "メールアドレスを入力してください",
pattern: {
value: /.*@.*\.tsukuba\.ac\.jp$/,
message: "筑波大学のメールアドレスを入力してください",
},
})}
/>
{errors.email && <span className={basicErrorMessageStyle}>{errors.email.message}</span>}
{errors.root && <span className={basicErrorMessageStyle}>{errors.root.message}</span>}
</label>
<button
type="submit"
disabled={isSent}
className={cx(
buttonStyle({ visual: "solid", color: "purple" }),
css({
_disabled: {
opacity: 0.5,
cursor: "default",
"&:hover": { opacity: 0.5 },
},
}),
)}>
送信
</button>
{isSent && <p className={css({ textAlign: "center" })}>メールを送信しました。受信トレイをご確認ください</p>}
</form>
);
};

export const ResetPassword: React.FC = () => {
const setAuthMode = useSetAtom(authModeAtom);
return (
<div
className={center({
minHeight: "calc(100vh - token(spacing.20))",
})}>
<main
className={vstack({
boxShadow: "token(shadows.md)",
paddingY: 11,
paddingX: 20,
borderRadius: "token(xl)",
width: "fit-content",
verticalAlign: "center",
maxWidth: "90%",
})}>
<h1 className={css({ fontSize: "2xl", fontWeight: "bold", marginBottom: 8 })}>パスワードのリセット</h1>
<ResetPasswordForm />
<div className={css({ marginTop: 4, display: "flex", gap: 3.5 })}>
<Image src={Triangle} alt="" />
<button
onClick={() => setAuthMode("signIn")}
className={css({
textDecoration: "underline",
fontWeight: "bold",
cursor: "pointer",
})}>
サインイン
</button>
</div>
</main>
</div>
);
};
14 changes: 11 additions & 3 deletions src/common_components/auth/signin/SignInPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { css, cx } from "@styled-system/css";
import { basicErrorMessageStyle, basicFormStyle } from "@/common_components/formFields/styles";
import { Button } from "@/common_components/Button";
import { buttonStyle } from "@/recipes/button";

type SignInInput = { email: string; password: string };

Expand Down Expand Up @@ -88,9 +88,17 @@ export const SigninForm: React.FC = () => {
{errors.password && <span className={basicErrorMessageStyle}>{errors.password.message}</span>}
{errors.root && <span className={basicErrorMessageStyle}>{errors.root.message}</span>}
</div>
<Button type="submit" color="purple" className={css({ flexGrow: 0, alignSelf: "center" })}>
<button
type="submit"
className={cx(
buttonStyle({
color: "purple",
visual: "solid",
}),
css({ flexGrow: 0, alignSelf: "center" }),
)}>
送信
</Button>
</button>
</form>
</>
);
Expand Down
26 changes: 19 additions & 7 deletions src/common_components/auth/signin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Image from "next/image";
import Triangle from "@/assets/Triangle.svg?url";
import { authModeAtom } from "@/common_components/auth/AuthUI";
import { useSetAtom } from "jotai";
import { center } from "@styled-system/patterns";
import { center, vstack } from "@styled-system/patterns";

const SigninPage: NextPage = () => {
const setAuthMode = useSetAtom(authModeAtom);
Expand All @@ -18,20 +18,32 @@ const SigninPage: NextPage = () => {
minHeight: "calc(100vh - token(spacing.20))",
})}>
<div
className={css({
display: "flex",
flexDir: "column",
alignItems: "center",
className={vstack({
boxShadow: "token(shadows.md)",
paddingY: 11,
paddingX: 20,
borderRadius: "token(xl)",
width: "fit-content",
maxWidth: "90%",
})}>
<h1 className={css({ fontSize: "2xl", fontWeight: "bold", marginBottom: 8 })}>ログイン</h1>
<h1 className={css({ fontSize: "2xl", fontWeight: "bold", marginBottom: 8 })}>サインイン</h1>
<SigninForm />
<div className={css({ marginTop: 4, display: "flex", gap: 3.5 })}>
<div className={css({ display: "flex", gap: 3.5 })}>
<Image src={Triangle} alt="" />
<button
onClick={() => setAuthMode("resetPassword")}
className={css({
textDecoration: "underline",
fontWeight: "bold",
cursor: "pointer",
wordBreak: "auto-phrase",
})}>
パスワードを忘れた場合
<wbr />
はこちら
</button>
</div>
<div className={css({ display: "flex", gap: 3.5 })}>
<Image src={Triangle} alt="" />
<button
onClick={() => setAuthMode("signUp")}
Expand Down
4 changes: 2 additions & 2 deletions src/common_components/header/Header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export const Header: FC = () => {
: [
{
path: "/register",
name: "ログイン/新規登録",
name: "サインイン/新規登録",
} as MenuData,
];

Expand Down Expand Up @@ -291,7 +291,7 @@ export const Header: FC = () => {
borderX: "solid 1px token(colors.gray.200)",
display: { base: "none", lg: "block" },
})}>
ログイン/新規登録
サインイン/新規登録
</button>
</nav>
)}
Expand Down

0 comments on commit 9eb367d

Please sign in to comment.