:global(.container) {
+ align-self: center;
+ grid-row: 2 span / 3;
+ }
+ }
+}
diff --git a/frontend/src/components/PasswordReset/index.tsx b/frontend/src/components/PasswordReset/index.tsx
new file mode 100644
index 0000000..242c1ca
--- /dev/null
+++ b/frontend/src/components/PasswordReset/index.tsx
@@ -0,0 +1,198 @@
+import { IconX } from "@tabler/icons-react";
+import clsx from "clsx";
+import { useEffect, useState } from "react";
+import { useErrorBoundary } from "react-error-boundary";
+import { useForm } from "react-hook-form";
+import { Link, useSearch } from "wouter";
+import ActivityOverlay from "~/components/ActivityOverlay";
+import FormTitle from "~/components/FormTitle";
+import Layout from "~/components/Layout";
+import Title from "~/components/Title";
+import useUser from "~/hooks/useUser";
+import { api } from "~/lib/api";
+import { setErrors } from "~/lib/form";
+import styles from "./PasswordReset.module.css";
+
+type State = {
+ isAlreadySignedIn: boolean;
+ isPasswordUpdated: boolean;
+ isTokenValid: boolean;
+};
+
+type Form = {
+ passwordResetToken: string;
+ userPassword: string;
+ userPassword2: string;
+};
+
+export default function PasswordReset() {
+ const { showBoundary } = useErrorBoundary();
+ const search = useSearch();
+ const user = useUser();
+ const token = new URLSearchParams(search).get("t");
+ const [state, setState] = useState
({ isAlreadySignedIn: false, isPasswordUpdated: false, isTokenValid: false });
+ const { clearErrors, formState: { errors, isSubmitting }, handleSubmit, register, reset, setError } = useForm