This commit is contained in:
2026-04-28 09:27:54 +07:00
commit 68d05da584
320 changed files with 26229 additions and 0 deletions

View File

@@ -0,0 +1,44 @@
import GridShape from "@/components/common/GridShape";
import ThemeTogglerTwo from "@/components/common/ThemeTogglerTwo";
import { ThemeProvider } from "@/context/ThemeContext";
import Image from "next/image";
import Link from "next/link";
import React from "react";
export default function AuthLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="relative p-6 bg-white z-1 dark:bg-gray-900 sm:p-0">
<ThemeProvider>
<div className="relative flex lg:flex-row w-full h-screen justify-center flex-col dark:bg-gray-900 sm:p-0">
{children}
{/* <div className="lg:w-1/2 w-full h-full bg-brand-950 dark:bg-white/5 lg:grid items-center hidden">
<div className="relative items-center justify-center flex z-1">
<GridShape />
<div className="flex flex-col items-center max-w-xs">
<Link href="/" className="block mb-4">
<Image
width={231}
height={48}
src="./images/logo/auth-logo.svg"
alt="Logo"
/>
</Link>
<p className="text-center text-gray-400 dark:text-white/60">
Free and Open-Source Tailwind CSS Admin Dashboard Template
</p>
</div>
</div>
</div> */}
<div className="fixed bottom-6 right-6 z-50 hidden sm:block">
<ThemeTogglerTwo />
</div>
</div>
</ThemeProvider>
</div>
);
}

View File

@@ -0,0 +1,239 @@
"use client";
import Input from "@/components/form/input/InputField";
import Label from "@/components/form/Label";
import { ChevronLeftIcon, EyeCloseIcon, EyeIcon } from "@/icons";
import { apiCreateOTP, apiVerifyOTP, apiResetPassword } from "@/service/auth";
import Link from "next/link";
import { useState } from "react";
import { toast } from "sonner";
export default function ResetPasswordForm() {
const [step, setStep] = useState(1);
const [email, setEmail] = useState("");
const [otp, setOtp] = useState("");
const [newPassword, setNewPassword] = useState("");
const [showPassword, setShowPassword] = useState(false);
const [errorMsg, setErrorMsg] = useState("");
const [loading, setLoading] = useState(false);
const isValidEmail = (email: string) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email);
};
const isValidPassword = (pass: string) => {
const passwordRegex = /^(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,}$/;
return passwordRegex.test(pass);
};
const handleSendOtp = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setErrorMsg("");
if (!email.trim()) {
setErrorMsg("Vui lòng nhập email.");
return;
}
if (!isValidEmail(email)) {
setErrorMsg("Email không đúng định dạng.");
return;
}
try {
setLoading(true);
await apiCreateOTP(email);
toast.success("Mã OTP đã được gửi đến email của bạn!");
setStep(2);
} catch (error) {
setErrorMsg("Lỗi khi gửi OTP. Vui lòng kiểm tra lại email.");
toast.error("Gửi OTP thất bại.");
} finally {
setLoading(false);
}
};
const handleResetPassword = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setErrorMsg("");
if (!otp.trim()) {
setErrorMsg("Vui lòng nhập mã OTP.");
return;
}
if (!isValidPassword(newPassword)) {
setErrorMsg("Mật khẩu chưa đủ điều kiện bảo mật.");
return;
}
try {
setLoading(true);
const verifyRes = await apiVerifyOTP(email, otp);
const tokenId = verifyRes?.data?.token_id;
if (!tokenId) {
throw new Error("OTP không hợp lệ hoặc đã hết hạn.");
}
const resetPayload = {
email: email,
new_password: newPassword,
token_id: tokenId,
};
await apiResetPassword(resetPayload);
toast.success("Đổi mật khẩu thành công!");
window.location.href = "/signin";
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Đổi mật khẩu thất bại.";
setErrorMsg(errorMessage);
console.error("Reset password error:", error);
toast.error("Vui lòng kiểm tra lại mã OTP.");
} finally {
setLoading(false);
}
};
return (
<div className="flex flex-col flex-1 w-full lg:w-1/2 overflow-y-auto no-scrollbar">
<div className="w-full max-w-md mx-auto mb-5 sm:pt-10">
<Link
href="/signin"
className="inline-flex items-center text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
>
<ChevronLeftIcon />
Back to Sign In
</Link>
</div>
<div className="flex flex-col justify-center flex-1 w-full max-w-md mx-auto">
<div>
<div className="mb-5 sm:mb-8">
<h1 className="mb-2 font-semibold text-gray-800 text-title-sm dark:text-white/90 sm:text-title-md">
{step === 1 ? "Forgot Password" : "Set New Password"}
</h1>
<p className="text-sm text-gray-500 dark:text-gray-400">
{step === 1
? "Enter your email address to receive an OTP code."
: `We sent an OTP to ${email}. Please enter it along with your new password.`}
</p>
</div>
{errorMsg && (
<div className="p-3 mb-4 text-sm rounded text-error-500 bg-error-50 dark:bg-error-500/10">
{errorMsg}
</div>
)}
{step === 1 && (
<form onSubmit={handleSendOtp}>
<div className="space-y-5">
<div>
<Label>
Email <span className="text-error-500">*</span>
</Label>
<Input
type="email"
name="email"
defaultValue={email}
onChange={(e) => {
setEmail(e.target.value);
setErrorMsg("");
}}
placeholder="Enter your registered email"
/>
</div>
<div>
<button
type="submit"
disabled={loading || !email.trim()}
className={`flex items-center justify-center w-full px-4 py-3 text-sm font-medium text-white transition rounded-lg shadow-theme-xs
${loading || !email.trim() ? "bg-gray-400 cursor-not-allowed" : "bg-brand-500 hover:bg-brand-600"}`}
>
{loading ? "Sending OTP..." : "Send Reset Code"}
</button>
</div>
</div>
</form>
)}
{step === 2 && (
<form onSubmit={handleResetPassword}>
<div className="space-y-5">
<div>
<Label>
OTP Code <span className="text-error-500">*</span>
</Label>
<Input
type="text"
name="otp"
defaultValue={otp}
onChange={(e) => {
setOtp(e.target.value);
setErrorMsg("");
}}
placeholder="Enter the 6-digit code"
/>
</div>
<div>
<Label>
New Password <span className="text-error-500">*</span>
</Label>
<div
className={`relative ${newPassword.length > 0 && !isValidPassword(newPassword) ? "border border-red-500 ring-1 ring-red-500 rounded-lg" : ""}`}
>
<Input
name="newPassword"
defaultValue={newPassword}
onChange={(e) => {
setNewPassword(e.target.value);
setErrorMsg("");
}}
placeholder="Min. 8 characters"
type={showPassword ? "text" : "password"}
/>
<span
onClick={() => setShowPassword(!showPassword)}
className="absolute z-30 -translate-y-1/2 cursor-pointer right-4 top-1/2"
>
{showPassword ? <EyeIcon /> : <EyeCloseIcon />}
</span>
</div>
<p
className={`mt-2 text-xs ${newPassword.length === 0 ? "text-gray-400" : isValidPassword(newPassword) ? "text-green-500" : "text-red-500"}`}
>
Mật khẩu phải chứa tối thiểu 8 tự, 1 chữ cái in hoa, 1 chữ số 1 tự đc biệt.
</p>
</div>
<div className="flex gap-3 pt-2">
<button
type="button"
onClick={() => setStep(1)}
className="flex items-center justify-center w-1/3 px-4 py-3 text-sm font-medium text-gray-700 transition bg-gray-200 rounded-lg hover:bg-gray-300 dark:bg-gray-800 dark:text-gray-300 dark:hover:bg-gray-700"
>
Back
</button>
<button
type="submit"
disabled={loading || !isValidPassword(newPassword)}
className={`flex items-center justify-center w-2/3 px-4 py-3 text-sm font-medium text-white transition rounded-lg shadow-theme-xs
${loading || !isValidPassword(newPassword) ? "bg-brand-400 opacity-70 cursor-not-allowed" : "bg-brand-500 hover:bg-brand-600"}`}
>
{loading ? "Resetting..." : "Reset Password"}
</button>
</div>
</div>
</form>
)}
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,11 @@
import SignInForm from "@/components/auth/SignInForm";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Next.js SignIn Page | TailAdmin - Next.js Dashboard Template",
description: "This is Next.js Signin Page TailAdmin Dashboard Template",
};
export default function SignIn() {
return <SignInForm />;
}

View File

@@ -0,0 +1,12 @@
import SignUpForm from "@/components/auth/SignUpForm";
import { Metadata } from "next";
export const metadata: Metadata = {
title: "Next.js SignUp Page | TailAdmin - Next.js Dashboard Template",
description: "This is Next.js SignUp Page TailAdmin Dashboard Template",
// other metadata
};
export default function SignUp() {
return <SignUpForm />;
}

View File

@@ -0,0 +1,54 @@
import GridShape from "@/components/common/GridShape";
import { Metadata } from "next";
import Image from "next/image";
import Link from "next/link";
import React from "react";
export const metadata: Metadata = {
title: "Next.js Error 404 | TailAdmin - Next.js Dashboard Template",
description:
"This is Next.js Error 404 page for TailAdmin - Next.js Tailwind CSS Admin Dashboard Template",
};
export default function Error404() {
return (
<div className="relative flex flex-col items-center justify-center min-h-screen p-6 overflow-hidden z-1">
<GridShape />
<div className="mx-auto w-full max-w-[242px] text-center sm:max-w-[472px]">
<h1 className="mb-8 font-bold text-gray-800 text-title-md dark:text-white/90 xl:text-title-2xl">
ERROR
</h1>
<Image
src="/images/error/404.svg"
alt="404"
className="dark:hidden"
width={472}
height={152}
/>
<Image
src="/images/error/404-dark.svg"
alt="404"
className="hidden dark:block"
width={472}
height={152}
/>
<p className="mt-10 mb-6 text-base text-gray-700 dark:text-gray-400 sm:text-lg">
We cant seem to find the page you are looking for!
</p>
<Link
href="/"
className="inline-flex items-center justify-center rounded-lg border border-gray-300 bg-white px-5 py-3.5 text-sm font-medium text-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200"
>
Back to Home Page
</Link>
</div>
{/* <!-- Footer --> */}
<p className="absolute text-sm text-center text-gray-500 -translate-x-1/2 bottom-6 left-1/2 dark:text-gray-400">
&copy; {new Date().getFullYear()} - TailAdmin
</p>
</div>
);
}

View File

@@ -0,0 +1,7 @@
export default function FullWidthPageLayout({
children,
}: {
children: React.ReactNode;
}) {
return <div>{children}</div>;
}