Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion RestroHub-FrontEnd/src/index.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import { GoogleOAuthProvider } from '@react-oauth/google';
import App from './App.jsx';

// ============================================
Expand All @@ -10,6 +11,8 @@ const root = ReactDOM.createRoot(document.getElementById('root'));

root.render(
<React.StrictMode>
<App />
<GoogleOAuthProvider clientId={import.meta.env.VITE_GOOGLE_CLIENT_ID || ''}>
<App />
</GoogleOAuthProvider>
</React.StrictMode>
);
320 changes: 306 additions & 14 deletions RestroHub-FrontEnd/src/pages/public/ForgotPassword.jsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,312 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { useState } from "react";
import { Link } from "react-router-dom";
import { useFormik } from "formik";
import * as Yup from "yup";
import toast from "react-hot-toast";
import api from "@services/common/api";
import { useTheme } from "@context/ThemeContext";

const validationSchema = Yup.object({
email: Yup.string()
.transform((value) => value?.trim())
.email("Enter a valid email address")
.required("Email is required"),
});

const EmailIcon = () => (
<svg
className="fill-current text-gray-400"
width="22"
height="22"
viewBox="0 0 22 22"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M19.25 3.3H2.75C1.58 3.3.59 4.26.59 5.47v11.14c0 1.17.96 2.17 2.16 2.17h16.5c1.17 0 2.17-.96 2.17-2.17V5.43c0-1.17-1-2.13-2.17-2.13Zm0 1.55h.21L11 10.22 2.55 4.88h.2c.07 0 .13 0 .2.03h16.3Zm0 12.3H2.75c-.34 0-.62-.28-.62-.62V6.36L10.28 11.52c.2.14.44.2.68.2s.48-.07.68-.2L19.78 6.36v10.57c.07.34-.2.62-.53.62Z" />
</svg>
);

const SpinnerIcon = () => (
<svg
className="h-5 w-5 animate-spin text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 0 1 8-8V0C5.37 0 0 5.37 0 12h4Zm2 5.29A7.96 7.96 0 0 1 4 12H0c0 3.04 1.14 5.82 3 7.94l3-2.65Z"
/>
</svg>
);

const Illustration = () => (
<svg
width="350"
height="350"
viewBox="0 0 350 350"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<ellipse cx="175" cy="232" rx="112" ry="30" fill="#E2E8F0" />
<rect x="112" y="118" width="126" height="104" rx="18" fill="#EFF6FF" />
<rect
x="112"
y="118"
width="126"
height="104"
rx="18"
stroke="#3B82F6"
strokeWidth="3"
/>
<path
d="M140 118V92c0-20 16-36 36-36s36 16 36 36v26"
stroke="#3B82F6"
strokeWidth="8"
strokeLinecap="round"
/>
<circle cx="175" cy="166" r="14" fill="#3B82F6" />
<path
d="M175 180v22"
stroke="#3B82F6"
strokeWidth="8"
strokeLinecap="round"
/>
<path
d="M88 252h174"
stroke="#93C5FD"
strokeWidth="4"
strokeLinecap="round"
/>
<circle cx="76" cy="118" r="7" fill="#FBBF24" />
<circle cx="278" cy="96" r="9" fill="#93C5FD" />
<circle cx="286" cy="232" r="6" fill="#FBBF24" />
</svg>
);

const ForgotPassword = () => {
const { isDark, toggle } = useTheme();
const [isLoading, setIsLoading] = useState(false);
const [submittedEmail, setSubmittedEmail] = useState("");
const [submitError, setSubmitError] = useState("");

const formik = useFormik({
initialValues: { email: "" },
validationSchema,
onSubmit: async ({ email }) => {
setIsLoading(true);
setSubmitError("");
const normalizedEmail = email.trim();

try {
const res = await api.post("/public/api/v1/auth/forgot-password", {
email: normalizedEmail,
});

if (res.data?.success === false) {
throw new Error(res.data?.message || "Unable to send reset instructions.");
}

const message =
res.data?.message ||
"Password reset instructions have been sent to your email.";

setSubmittedEmail(normalizedEmail);
toast.success(message);
} catch (err) {
const message =
err.response?.data?.message ||
err.message ||
"Unable to send reset instructions. Please try again.";

setSubmitError(message);
toast.error(
message
);
} finally {
setIsLoading(false);
}
},
});

const inputClass = (field) =>
`w-full rounded-lg border ${
formik.touched[field] && formik.errors[field]
? "border-red-500 focus:ring-red-500"
: "border-gray-300 focus:ring-blue-500 dark:border-gray-600"
} bg-transparent py-4 pl-6 pr-12 text-gray-800 placeholder-gray-400 outline-none transition focus:border-transparent focus:ring-2 dark:bg-gray-800 dark:text-white dark:placeholder-gray-500`;

return (
<div className="flex min-h-screen items-center justify-center bg-gray-100 dark:bg-gray-900 px-4">
<div className="max-w-md w-full bg-white dark:bg-gray-800 p-8 rounded-2xl shadow-xl text-center border border-gray-200 dark:border-gray-700">
<h2 className="text-3xl font-bold text-gray-900 dark:text-white mb-4">Forgot Password?</h2>
<p className="text-gray-600 dark:text-gray-400 mb-8">
The password recovery feature is currently under development. Please contact support or try again later!
</p>
<Link
to="/login"
className="inline-block bg-blue-600 hover:bg-blue-700 text-white font-medium py-3 px-6 rounded-lg transition"
>
Back to Login
</Link>
<div className="flex min-h-screen items-center justify-center bg-gray-100 px-4 py-10 dark:bg-gray-900 sm:px-6 lg:px-8">
<div className="w-full max-w-[1200px] overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-xl dark:border-gray-700 dark:bg-gray-800">
<div className="flex flex-wrap">
<div className="hidden w-full items-center justify-center bg-gradient-to-br from-blue-600 via-blue-700 to-indigo-600 p-12 xl:flex xl:w-1/2">
<div className="text-center">
<Link to="/" className="mb-6 inline-block">
<span className="text-4xl font-extrabold tracking-tight text-white">
Restroly
</span>
</Link>

<p className="mx-auto mb-10 max-w-sm text-lg leading-relaxed text-blue-100">
Reset your password and get back to managing your restaurant.
</p>

<Illustration />
</div>
</div>

<div className="w-full xl:w-1/2">
<div className="w-full px-6 py-12 sm:px-14 lg:px-20 xl:py-20">
<div className="mb-4 flex justify-end">
<button
type="button"
onClick={toggle}
aria-label="Toggle dark mode"
className="rounded-lg p-2 text-slate-500 transition-colors hover:bg-slate-100 dark:text-slate-400 dark:hover:bg-gray-700"
>
{isDark ? (
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364-6.364l-.707.707M6.343 17.657l-.707.707M17.657 17.657l-.707-.707M6.343 6.343l-.707-.707M16 12a4 4 0 11-8 0 4 4 0 018 0z"
/>
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z"
/>
</svg>
)}
</button>
</div>

<div className="mb-8 flex items-center justify-center xl:hidden">
<span className="text-2xl font-bold text-blue-600">
Restroly
</span>
</div>

<p className="mb-1 text-sm font-medium text-gray-500 dark:text-gray-400">
Forgot your password?
</p>
<h2 className="mb-4 text-2xl font-bold text-gray-900 dark:text-white sm:text-3xl">
Reset Password
</h2>
<p className="mb-8 text-sm leading-6 text-gray-500 dark:text-gray-400">
Enter your registered email address and we will send password
reset instructions.
</p>

{submittedEmail ? (
<div className="rounded-lg border border-green-200 bg-green-50 p-5 text-sm text-green-800 dark:border-green-900/60 dark:bg-green-900/20 dark:text-green-200">
<p className="font-medium">Check your inbox</p>
<p className="mt-2">
If an account exists for {submittedEmail}, reset
instructions will arrive shortly.
</p>
<button
type="button"
onClick={() => setSubmittedEmail("")}
className="mt-4 font-medium text-blue-600 hover:underline dark:text-blue-400"
>
Try another email
</button>
</div>
) : (
<form onSubmit={formik.handleSubmit} noValidate>
{submitError && (
<div className="mb-5 rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-700 dark:border-red-900/60 dark:bg-red-900/20 dark:text-red-200">
{submitError}
</div>
)}

<div className="mb-6">
<label
htmlFor="email"
className="mb-2 block text-sm font-medium text-gray-700 dark:text-gray-300"
>
Email Address
</label>
<div className="relative">
<input
id="email"
name="email"
type="email"
autoComplete="email"
placeholder="Enter your email address"
disabled={isLoading}
value={formik.values.email}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
className={inputClass("email")}
/>
<span className="pointer-events-none absolute right-4 top-1/2 -translate-y-1/2">
<EmailIcon />
</span>
</div>
{formik.touched.email && formik.errors.email && (
<p className="mt-1.5 text-xs text-red-500">
{formik.errors.email}
</p>
)}
</div>

<button
type="submit"
disabled={isLoading}
className="mb-5 flex w-full items-center justify-center gap-2 rounded-lg bg-blue-600 px-6 py-4 text-base font-medium text-white transition hover:bg-blue-700 focus:outline-none focus:ring-4 focus:ring-blue-300 disabled:cursor-not-allowed disabled:opacity-60 dark:bg-blue-500 dark:hover:bg-blue-600 dark:focus:ring-blue-800"
>
{isLoading ? (
<>
<SpinnerIcon />
Sending Instructions...
</>
) : (
"Send Reset Instructions"
)}
</button>
</form>
)}

<p className="mt-8 text-center text-sm text-gray-500 dark:text-gray-400">
Remember your password?{" "}
<Link
to="/login"
className="font-medium text-blue-600 hover:underline dark:text-blue-400"
>
Sign In
</Link>
</p>
</div>
</div>
</div>
</div>
</div>
);
Expand Down