Sign in screen with token retrieval

This commit is contained in:
2024-06-24 22:36:33 +08:00
parent 5eeaade098
commit 94a62f43b2
10 changed files with 191 additions and 8 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 MiB

View File

@@ -1,12 +1,14 @@
import { Route, Routes } from "react-router-dom"; import { Route, Routes } from "react-router-dom";
import HomePage from "./pages/HomePage"; import HomePage from "./pages/HomePage";
import SignUpPage from "./pages/SignUpPage"; import SignUpPage from "./pages/SignUpPage";
import SignInPage from "./pages/SignInPage";
function App() { function App() {
return ( return (
<Routes> <Routes>
<Route element={<HomePage />} path="/" /> <Route element={<HomePage />} path="/" />
<Route element={<SignUpPage />} path="/signup" /> <Route element={<SignUpPage />} path="/signup" />
<Route element={<SignInPage />} path="/signin" />
</Routes> </Routes>
); );
} }

View File

@@ -0,0 +1,100 @@
import { Button, Link } from "@nextui-org/react";
import { Formik, Form } from "formik";
import * as Yup from "yup";
import axios from "axios";
import config from "../config";
import NextUIFormikInput from "./NextUIFormikInput";
import { useNavigate } from "react-router-dom";
import { ChevronLeftIcon } from "../icons";
const validationSchema = Yup.object({
email: Yup.string()
.trim()
.lowercase()
.min(5)
.max(69)
.email("Invalid email format")
.required("Email is required"),
password: Yup.string()
.trim()
.max(69, "Password must be at most 69 characters")
.matches(
/^(?=.*[a-zA-Z])(?=.*[0-9]).{1,}$/,
"Password contains only letters and numbers"
)
.required("Password is required"),
});
export default function SignInModule() {
const navigate = useNavigate();
const initialValues = {
email: "",
password: "",
};
const handleSubmit = async (values: any) => {
try {
const response = await axios.post(
config.serverAddress + "/users/login",
values
);
console.log(response.data.accessToken);
} catch (error) {
console.error("Error logging in:", error);
}
};
return (
<div className="flex flex-col gap-16">
<div className="flex flex-col gap-1">
<p className="text-4xl font-bold">Sign In</p>
<p className="text-2xl">to ecoconnect</p>
</div>
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
onSubmit={handleSubmit}
>
{({ isValid, dirty }) => (
<Form className="flex flex-col gap-4">
<NextUIFormikInput
label="Email"
name="email"
type="email"
placeholder="johndoe@email.com"
labelPlacement="outside"
/>
<NextUIFormikInput
label="Password"
name="password"
type="password"
placeholder=" "
labelPlacement="outside"
/>
<Button
type="submit"
color="primary"
size="lg"
className="mx-auto"
isIconOnly
isDisabled={!isValid || !dirty}
>
<ChevronLeftIcon />
</Button>
</Form>
)}
</Formik>
<div className="w-full flex flex-col gap-2 *:mx-auto">
<p>New here?</p>
<Link
onPress={() => {
navigate("/signup");
}}
>
Sign up
</Link>
</div>
</div>
);
}

View File

@@ -4,6 +4,7 @@ import * as Yup from "yup";
import axios from "axios"; import axios from "axios";
import config from "../config"; import config from "../config";
import NextUIFormikInput from "./NextUIFormikInput"; import NextUIFormikInput from "./NextUIFormikInput";
import { useNavigate } from "react-router-dom";
const validationSchema = Yup.object({ const validationSchema = Yup.object({
firstName: Yup.string() firstName: Yup.string()
@@ -44,6 +45,8 @@ const validationSchema = Yup.object({
}); });
export default function SignUpModule() { export default function SignUpModule() {
const navigate = useNavigate();
const initialValues = { const initialValues = {
firstName: "", firstName: "",
lastName: "", lastName: "",
@@ -66,7 +69,7 @@ export default function SignUpModule() {
}; };
return ( return (
<div> <div className="flex flex-col gap-16">
<Formik <Formik
initialValues={initialValues} initialValues={initialValues}
validationSchema={validationSchema} validationSchema={validationSchema}
@@ -140,6 +143,16 @@ export default function SignUpModule() {
</Form> </Form>
)} )}
</Formik> </Formik>
<div className="w-full flex flex-col gap-2 *:mx-auto">
<p>Already here before?</p>
<Link
onPress={() => {
navigate("/signin");
}}
>
Sign in
</Link>
</div>
</div> </div>
); );
} }

View File

@@ -3,7 +3,7 @@ import { Link } from "@nextui-org/react";
export default function SingaporeAgencyStrip() { export default function SingaporeAgencyStrip() {
return ( return (
<div className="h-8 bg-neutral-200 text-black relative"> <div className="h-8 bg-neutral-200 text-black relative">
<div className="flex flex-row gap-2 *:my-auto pl-16 h-full text-sm"> <div className="mx-auto w-full flex flex-row gap-2 *:my-auto h-full text-sm max-w-7xl pl-4">
<img src="../assets/Merlion.svg" alt="Merlion Icon" className="w-6" /> <img src="../assets/Merlion.svg" alt="Merlion Icon" className="w-6" />
<p>A Singapore Government Agency Website</p> <p>A Singapore Government Agency Website</p>
<Link size="sm">How to identify</Link> <Link size="sm">How to identify</Link>

18
client/src/icons.tsx Normal file
View File

@@ -0,0 +1,18 @@
export const ChevronLeftIcon = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="size-6"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="m8.25 4.5 7.5 7.5-7.5 7.5"
/>
</svg>
);
};

View File

@@ -0,0 +1,14 @@
import SingaporeAgencyStrip from "../components/SingaporeAgencyStrip";
export default function DefaultLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="relative flex flex-col h-screen">
<SingaporeAgencyStrip />
<main className=" flex-grow">{children}</main>
</div>
);
}

View File

@@ -1,10 +1,11 @@
import { Button } from "@nextui-org/react"; import { Button } from "@nextui-org/react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import DefaultLayout from "../layouts/default";
export default function HomePage() { export default function HomePage() {
const navigate = useNavigate(); const navigate = useNavigate();
return ( return (
<div> <DefaultLayout>
<p>Home</p> <p>Home</p>
<Button <Button
onPress={() => { onPress={() => {
@@ -13,6 +14,6 @@ export default function HomePage() {
> >
Sign up! Sign up!
</Button> </Button>
</div> </DefaultLayout>
); );
} }

View File

@@ -0,0 +1,36 @@
import SignInModule from "../components/SignInModule";
import DefaultLayout from "../layouts/default";
export default function SignInPage() {
return (
<DefaultLayout>
<div className="flex flex-col h-full">
<div className="flex flex-row h-full">
<div className="w-3/5 relative">
<div className="absolute inset-0">
<div className="w-full h-full relative">
<img
src="../assets/SigninScreenBG.png"
alt="HDB flat"
className="w-full h-full object-cover -z-10"
/>
<div className="absolute inset-0 z-10 flex flex-col justify-center">
<div className="w-full text-right text-white flex flex-col gap-6 p-16">
<p className="text-7xl font-semibold">Welcome back!</p>
<p className="text-3xl">
Good to have you here again. Tell us who you are, and wel
will let you in.
</p>
</div>
</div>
</div>
</div>
</div>
<div className="w-2/5 p-8">
<SignInModule />
</div>
</div>
</div>
</DefaultLayout>
);
}

View File

@@ -1,11 +1,10 @@
import SignUpModule from "../components/SignUpModule"; import SignUpModule from "../components/SignUpModule";
import SingaporeAgencyStrip from "../components/SingaporeAgencyStrip"; import DefaultLayout from "../layouts/default";
export default function SignUpPage() { export default function SignUpPage() {
return ( return (
<div className="absolute inset-0"> <DefaultLayout>
<div className="flex flex-col h-full"> <div className="flex flex-col h-full">
<SingaporeAgencyStrip></SingaporeAgencyStrip>
<div className="flex flex-row h-full"> <div className="flex flex-row h-full">
<div className="w-3/5 relative"> <div className="w-3/5 relative">
<div className="absolute inset-0"> <div className="absolute inset-0">
@@ -32,6 +31,6 @@ export default function SignUpPage() {
</div> </div>
</div> </div>
</div> </div>
</div> </DefaultLayout>
); );
} }