Skip to content

Commit

Permalink
Merge branch 'feature/admin'
Browse files Browse the repository at this point in the history
  • Loading branch information
ad956 committed Jul 28, 2024
2 parents 1389a59 + c198fb1 commit 86a5971
Show file tree
Hide file tree
Showing 8 changed files with 338 additions and 169 deletions.
186 changes: 19 additions & 167 deletions app/(pages)/(auth)/admin-login/page.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,14 @@
"use client";

import OtpSection from "@components/OtpSection";
import { Input, Button, Link, Card, Image } from "@nextui-org/react";
import { AiOutlineEyeInvisible, AiTwotoneEye } from "react-icons/ai";
import { MdOutlineAlternateEmail, MdOutlineKey } from "react-icons/md";
import { ChangeEvent, useEffect, useRef, useState } from "react";
import toast, { Toaster } from "react-hot-toast";
import { loginAction } from "@lib/actions";
import FormValidator from "@utils/formValidator";
import { useState, useEffect } from "react";
import { Toaster } from "react-hot-toast";
import AnimatedBackground from "@components/AnimatedBackground";
import { AccessDenied, LoginForm, PinVerification } from "@admin/components";

export default function AdminLoginPage() {
const [formValidator] = useState(new FormValidator());

const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [isVisible, setIsVisible] = useState(false);
const [showOtp, setShowOtp] = useState(false);
const [userData, setUserData] = useState({ email: "", role: "", action: "" });
const [isPinVerified, setIsPinVerified] = useState(false);
const [isDesktop, setIsDesktop] = useState(true);

const [loginDisabled, setLoginDisabled] = useState(true);

useEffect(() => {
const checkDeviceType = () => {
const userAgent = navigator.userAgent.toLowerCase();
Expand All @@ -47,162 +35,26 @@ export default function AdminLoginPage() {
};
}, []);

function handleEmailChange(e: ChangeEvent<HTMLInputElement>) {
const error = FormValidator.validateEmail(e.target.value);
formValidator.setError("email", error);
setEmail(e.target.value);
}

function handlePasswordChange(e: ChangeEvent<HTMLInputElement>) {
const error = FormValidator.validatePassword(e.target.value);
formValidator.setError("password", error);
setPassword(e.target.value);
}

const toggleVisibility = () => setIsVisible(!isVisible);

useEffect(() => {
setLoginDisabled(formValidator.hasErrors() || !email || !password);
}, [email, password]);

async function handleFormSubmit(
e: React.FormEvent<HTMLFormElement>
): Promise<void> {
e.preventDefault();
const formData = new FormData(e.currentTarget);

try {
toast.loading("Please wait ...", { position: "bottom-center" });
const isValidUser = await loginAction(formData);
toast.dismiss();

if (isValidUser?.unauthorized) {
toast.error("Invalid email or password. Please try again.");
} else {
const userEmail = formData.get("email");
if (userEmail) {
setUserData({
email: userEmail.toString(),
role: "admin",
action: "Login",
});
toast.success("OTP successfully sent!", {
position: "bottom-center",
});
setShowOtp(true);
}
}
} catch (error) {
toast.error("An error occurred. Please try again.");
}
}

async function handleForgetPassword() {
if (!email) {
toast.error("Please enter a valid email address to continue.", {
position: "bottom-center",
duration: 2000,
});
}
}

if (!isDesktop) {
return (
<div className="min-h-screen flex items-center justify-center bg-gray-100 p-4">
<Card shadow="lg" className="w-full max-w-md p-6">
<h2 className="text-xl font-bold text-center text-red-600 mb-4">
Access Denied
</h2>
<p className="text-center">
This admin page is only accessible on desktop devices. Please use a
desktop computer to access this site.
</p>
</Card>
</div>
<AnimatedBackground>
<div className="min-h-screen flex items-center justify-center bg-gray-100 p-4">
<AccessDenied />
</div>
</AnimatedBackground>
);
}

return (
<div className="min-h-screen flex items-center justify-center bg-gray-100 p-4">
<Card shadow="lg" className="w-full max-w-md p-6">
<div className="flex items-center mb-6">
<Image
src="/icons/patient.svg"
alt="brand-logo"
width={40}
height={40}
/>
<h2 className="ml-2 text-xl font-bold">Patient Fitness Tracker</h2>
</div>
<div className="mb-6">
<h3 className="text-2xl font-bold">Welcome Admin!</h3>
<p className="text-sm text-gray-500">
Please enter your login details below
</p>
</div>
<form className="space-y-4" onSubmit={handleFormSubmit}>
<Input
name="email"
label="Email"
variant="bordered"
size="lg"
type="email"
placeholder="[email protected]"
startContent={<MdOutlineAlternateEmail />}
value={email}
onChange={handleEmailChange}
autoComplete="username"
isInvalid={!!formValidator.getError("email")}
errorMessage={formValidator.getError("email")}
/>
<input name="role" type="hidden" value="admin" />
<Input
name="password"
label="Password"
variant="bordered"
size="lg"
placeholder="Enter your password"
startContent={<MdOutlineKey />}
value={password}
onChange={handlePasswordChange}
endContent={
<button
className="focus:outline-none"
type="button"
onClick={toggleVisibility}
>
{isVisible ? (
<AiOutlineEyeInvisible className="text-2xl text-default-400" />
) : (
<AiTwotoneEye className="text-2xl text-default-400" />
)}
</button>
}
type={isVisible ? "text" : "password"}
autoComplete="current-password"
isInvalid={!!formValidator.getError("password")}
errorMessage={formValidator.getError("password")}
/>
<Link
href="#"
size="sm"
className="text-sm text-right block"
onClick={handleForgetPassword}
>
Forgot password?
</Link>
{showOtp && <OtpSection userData={userData} />}
<Button
type="submit"
size="lg"
className="w-full text-white self-center bg-[#161313]"
isDisabled={loginDisabled}
>
Sign in
</Button>
</form>
</Card>
<AnimatedBackground>
<div className="flex justify-center items-center min-h-screen w-full">
{!isPinVerified ? (
<PinVerification onVerified={() => setIsPinVerified(true)} />
) : (
<LoginForm />
)}
</div>
<Toaster />
</div>
</AnimatedBackground>
);
}
15 changes: 15 additions & 0 deletions app/(pages)/admin/components/AccessDenied/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Card } from "@nextui-org/react";

export default function AccessDenied() {
return (
<Card shadow="lg" className="w-full max-w-md p-6">
<h2 className="text-xl font-bold text-center text-red-600 mb-4">
Access Denied
</h2>
<p className="text-center">
This admin page is only accessible on desktop devices. Please use a
desktop computer to access this site.
</p>
</Card>
);
}
156 changes: 156 additions & 0 deletions app/(pages)/admin/components/LoginForm/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { useState, useEffect } from "react";
import { Input, Button, Link, Card, Image } from "@nextui-org/react";
import { AiOutlineEyeInvisible, AiTwotoneEye } from "react-icons/ai";
import { MdOutlineAlternateEmail, MdOutlineKey } from "react-icons/md";
import toast from "react-hot-toast";
import { loginAction } from "@lib/actions";
import OtpSection from "@components/OtpSection";
import FormValidator from "@utils/formValidator";

export default function LoginForm() {
const [formValidator] = useState(new FormValidator());
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [isVisible, setIsVisible] = useState(false);
const [showOtp, setShowOtp] = useState(false);
const [userData, setUserData] = useState({ email: "", role: "", action: "" });
const [loginDisabled, setLoginDisabled] = useState(true);

function handleEmailChange(e) {
const error = FormValidator.validateEmail(e.target.value);
formValidator.setError("email", error);
setEmail(e.target.value);
}

function handlePasswordChange(e) {
const error = FormValidator.validatePassword(e.target.value);
formValidator.setError("password", error);
setPassword(e.target.value);
}

const toggleVisibility = () => setIsVisible(!isVisible);

useEffect(() => {
setLoginDisabled(formValidator.hasErrors() || !email || !password);
}, [email, password]);

async function handleFormSubmit(e) {
e.preventDefault();
const formData = new FormData(e.currentTarget);

try {
toast.loading("Please wait ...", { position: "bottom-center" });
const isValidUser = await loginAction(formData);
toast.dismiss();

if (isValidUser?.unauthorized) {
toast.error("Invalid email or password. Please try again.");
} else {
const userEmail = formData.get("email");
if (userEmail) {
setUserData({
email: userEmail.toString(),
role: "admin",
action: "Login",
});
toast.success("OTP successfully sent!", {
position: "bottom-center",
});
setShowOtp(true);
}
}
} catch (error) {
toast.error("An error occurred. Please try again.");
}
}

async function handleForgetPassword() {
if (!email) {
toast.error("Please enter a valid email address to continue.", {
position: "bottom-center",
duration: 2000,
});
}
}

return (
<Card shadow="lg" className="h4/5 w-full max-w-md p-6">
<div className="flex items-center mb-6">
<Image
src="/icons/patient.svg"
alt="brand-logo"
width={40}
height={40}
/>
<h2 className="ml-2 text-xl font-bold">Patient Fitness Tracker</h2>
</div>
<div className="mb-6">
<h3 className="text-2xl font-bold">Welcome Admin!</h3>
<p className="text-sm text-gray-500">
Please enter your login details below
</p>
</div>
<form className="space-y-4" onSubmit={handleFormSubmit}>
<Input
name="email"
label="Email"
variant="bordered"
size="lg"
type="email"
placeholder="[email protected]"
startContent={<MdOutlineAlternateEmail />}
value={email}
onChange={handleEmailChange}
autoComplete="username"
isInvalid={!!formValidator.getError("email")}
errorMessage={formValidator.getError("email")}
/>
<input name="role" type="hidden" value="admin" />
<Input
name="password"
label="Password"
variant="bordered"
size="lg"
placeholder="Enter your password"
startContent={<MdOutlineKey />}
value={password}
onChange={handlePasswordChange}
endContent={
<button
className="focus:outline-none"
type="button"
onClick={toggleVisibility}
>
{isVisible ? (
<AiOutlineEyeInvisible className="text-2xl text-default-400" />
) : (
<AiTwotoneEye className="text-2xl text-default-400" />
)}
</button>
}
type={isVisible ? "text" : "password"}
autoComplete="current-password"
isInvalid={!!formValidator.getError("password")}
errorMessage={formValidator.getError("password")}
/>
<Link
href="#"
size="sm"
className="text-sm text-right block"
onClick={handleForgetPassword}
>
Forgot password?
</Link>
{showOtp && <OtpSection userData={userData} />}
<Button
type="submit"
size="lg"
className="w-full text-white self-center bg-[#161313]"
isDisabled={loginDisabled}
>
Sign in
</Button>
</form>
</Card>
);
}
Loading

0 comments on commit 86a5971

Please sign in to comment.