Sign in screen with token retrieval
This commit is contained in:
BIN
client/assets/SigninScreenBG.png
Normal file
BIN
client/assets/SigninScreenBG.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 MiB |
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
100
client/src/components/SignInModule.tsx
Normal file
100
client/src/components/SignInModule.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
18
client/src/icons.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
14
client/src/layouts/default.tsx
Normal file
14
client/src/layouts/default.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
36
client/src/pages/SignInPage.tsx
Normal file
36
client/src/pages/SignInPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user