From c68ea1f26f513e4a11dfa023eb4c59df8dcf77c1 Mon Sep 17 00:00:00 2001
From: Thanh Quy - wolf <524H0124@student.tdtu.edu.vn>
Date: Sun, 5 Apr 2026 16:16:48 +0700
Subject: [PATCH] Atomic for login page and register page
---
app/(main)/login/page.tsx | 157 +-----------------
app/(main)/payment/page.tsx | 1 +
app/(main)/register/page.tsx | 23 +--
components/atoms/buttons/Button.tsx | 19 ++-
components/atoms/buttons/Button.types.ts | 7 +-
components/atoms/errors/Error.types.ts | 6 +
components/atoms/errors/ErrorMessageLogin.tsx | 32 ++++
components/atoms/errors/index.ts | 4 +
components/atoms/inputs/Input.types.ts | 9 +
components/atoms/inputs/LoginInput.tsx | 56 +++++++
components/atoms/inputs/index.ts | 1 +
.../molecules/cards/PaymentSummaryCard.tsx | 8 +-
components/organisms/forms/LoginForm.tsx | 120 +++++++++++++
13 files changed, 256 insertions(+), 187 deletions(-)
create mode 100644 components/atoms/errors/Error.types.ts
create mode 100644 components/atoms/errors/ErrorMessageLogin.tsx
create mode 100644 components/atoms/errors/index.ts
create mode 100644 components/atoms/inputs/LoginInput.tsx
create mode 100644 components/organisms/forms/LoginForm.tsx
diff --git a/app/(main)/login/page.tsx b/app/(main)/login/page.tsx
index 7273a79..b801294 100644
--- a/app/(main)/login/page.tsx
+++ b/app/(main)/login/page.tsx
@@ -1,64 +1,10 @@
"use client";
-import { useAuth } from "@/lib/auth-context";
import { SHOP_INFO } from "@/lib/constants";
+import LoginForm from "@/components/organisms/forms/LoginForm";
import Image from "next/image";
-import Link from "next/link";
-import { useRouter } from "next/navigation";
-import { FormEvent, useState } from "react";
export default function LoginPage() {
- const router = useRouter();
- const { login } = useAuth();
-
- const [username, setUsername] = useState("");
- const [password, setPassword] = useState("");
- const [errors, setErrors] = useState({
- username: "",
- password: "",
- general: "",
- });
- const [showPassword, setShowPassword] = useState(false);
-
- const validate = (): boolean => {
- const newErrors = { username: "", password: "", general: "" };
- let isValid = true;
-
- if (!username.trim()) {
- newErrors.username = "Vui lòng nhập tên đăng nhập";
- isValid = false;
- }
-
- if (!password.trim()) {
- newErrors.password = "Vui lòng nhập mật khẩu";
- isValid = false;
- } else if (password.length < 4) {
- newErrors.password = "Mật khẩu phải có ít nhất 4 ký tự";
- isValid = false;
- }
-
- setErrors(newErrors);
- return isValid;
- };
-
- const handleSubmit = (e: FormEvent) => {
- e.preventDefault();
-
- if (!validate()) return;
-
- const success = login(username, password);
-
- if (success) {
- router.push("/");
- } else {
- setErrors({
- username: "",
- password: "",
- general: "Tên đăng nhập hoặc mật khẩu không đúng",
- });
- }
- };
-
return (
{/* Login Form Card */}
@@ -82,106 +28,9 @@ export default function LoginPage() {
Đăng nhập vào hệ thống
-
- {/* Error Message */}
- {errors.general && (
-
-
- {errors.general}
-
- )}
-
+
{/* Login Form */}
-
+
{/* Demo Credentials Info */}
diff --git a/app/(main)/payment/page.tsx b/app/(main)/payment/page.tsx
index febe020..f9377f4 100644
--- a/app/(main)/payment/page.tsx
+++ b/app/(main)/payment/page.tsx
@@ -104,6 +104,7 @@ export default function PaymentPage() {
onClick={() => removeFromCart(item.id)}
variant="danger"
size="md"
+ style="payment"
>
Xóa sản phẩm
diff --git a/app/(main)/register/page.tsx b/app/(main)/register/page.tsx
index 807f836..b48bec8 100644
--- a/app/(main)/register/page.tsx
+++ b/app/(main)/register/page.tsx
@@ -6,6 +6,7 @@ import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { FormEvent, useState } from "react";
+import Button from "@/components/atoms/buttons/Button";
// Static OTP for demo (in production, this would be sent via SMS)
const DEMO_OTP = "123456";
@@ -160,12 +161,9 @@ export default function RegisterPage() {
{/* Buttons */}
{/* Submit Button */}
-
{/* Back to Login */}
{/* Submit Button */}
-
+
Hoàn tất đăng ký
-
+
{/* Back Button */}
-
+
Thay đổi số điện thoại
-
+
{/* Resend OTP (disabled in demo) */}
diff --git a/components/atoms/buttons/Button.tsx b/components/atoms/buttons/Button.tsx
index d83b579..ab7c405 100644
--- a/components/atoms/buttons/Button.tsx
+++ b/components/atoms/buttons/Button.tsx
@@ -3,22 +3,21 @@
import type { ButtonProps } from "./Button.types";
export default function Button({
+ style = "base",
variant = "primary",
size = "md",
icon,
iconPosition = "left",
disabled = false,
children,
- className = "",
- payment = false,
...props
}: ButtonProps) {
- const baseStyles =
- "font-semibold rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center justify-center gap-1.5";
-// h-8 w-8
- const paymentStyles =
- "inline-flex cursor-pointer items-center justify-center gap-2 rounded-xl transition-colors";
-
+ const styles = {
+ base: "font-semibold rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center justify-center gap-1.5",
+ payment: "inline-flex cursor-pointer items-center justify-center gap-2 rounded-xl transition-colors",
+ login: "w-full cursor-pointer rounded-xl py-3 font-semibold transition-all duration-150",
+ }
+
const variants = {
primary:
"bg-(--color-primary) text-white hover:bg-(--color-primary-dark) active:scale-95",
@@ -26,6 +25,8 @@ export default function Button({
"border border-(--color-border) hover:bg-(--color-border-light) active:scale-95",
danger: "bg-red-500 text-white hover:bg-red-600 active:scale-95",
ghost: "bg-transparent hover:bg-(--color-border-light) active:scale-95",
+ primaryNoBorder: "border-none bg-(--color-primary) text-white hover:bg-(--color-primary-dark) active:scale-98",
+ bgWhite: "border-2 border-(--color-primary) bg-white text-(--color-primary) hover:bg-(--color-primary) hover:text-white active:scale-98",
};
const sizes = {
@@ -38,7 +39,7 @@ export default function Button({
return (
diff --git a/components/atoms/buttons/Button.types.ts b/components/atoms/buttons/Button.types.ts
index 476d8b0..cf24ac5 100644
--- a/components/atoms/buttons/Button.types.ts
+++ b/components/atoms/buttons/Button.types.ts
@@ -1,12 +1,11 @@
import { ButtonHTMLAttributes } from "react";
-export interface ButtonProps extends ButtonHTMLAttributes {
- variant?: "primary" | "secondary" | "danger" | "ghost";
+export interface ButtonProps extends Omit, 'style'> {
+ style?: "base" | "payment" | "login";
+ variant?: "primary" | "secondary" | "danger" | "ghost" | "primaryNoBorder" | "bgWhite";
size?: "sm" | "md" | "lg";
icon?: string; // FontAwesome class like "fa-solid fa-cart-plus"
iconPosition?: "left" | "right";
disabled?: boolean;
children: React.ReactNode;
- className?: string;
- payment?: boolean;
}
diff --git a/components/atoms/errors/Error.types.ts b/components/atoms/errors/Error.types.ts
new file mode 100644
index 0000000..ba8026b
--- /dev/null
+++ b/components/atoms/errors/Error.types.ts
@@ -0,0 +1,6 @@
+import { InputHTMLAttributes, TextareaHTMLAttributes } from "react";
+
+export interface ErrorMessageLoginProps {
+ message: string;
+ type?: string;
+ }
diff --git a/components/atoms/errors/ErrorMessageLogin.tsx b/components/atoms/errors/ErrorMessageLogin.tsx
new file mode 100644
index 0000000..a837fb1
--- /dev/null
+++ b/components/atoms/errors/ErrorMessageLogin.tsx
@@ -0,0 +1,32 @@
+import { ErrorMessageLoginProps } from "./Error.types";
+
+export default function ErrorMessageLogin({
+ message,
+ type = "primary",
+}: ErrorMessageLoginProps) {
+ function primaryType() {
+ return (
+
+
+ {message}
+
+ );
+ }
+
+ function secondaryType() {
+ return (
+
+
+ {message}
+
+ );
+ }
+
+ return (
+ type === "primary" ? primaryType() : secondaryType()
+ );
+}
diff --git a/components/atoms/errors/index.ts b/components/atoms/errors/index.ts
new file mode 100644
index 0000000..81c3cf8
--- /dev/null
+++ b/components/atoms/errors/index.ts
@@ -0,0 +1,4 @@
+export { default as ErrorMessageLogin } from "./ErrorMessageLogin";
+export type {
+ ErrorMessageLoginProps,
+} from "./Error.types";
diff --git a/components/atoms/inputs/Input.types.ts b/components/atoms/inputs/Input.types.ts
index 675212e..c062459 100644
--- a/components/atoms/inputs/Input.types.ts
+++ b/components/atoms/inputs/Input.types.ts
@@ -18,3 +18,12 @@ export interface SearchInputProps extends InputHTMLAttributes
onClear?: () => void;
className?: string;
}
+
+export interface LoginInputProps extends InputHTMLAttributes {
+ label: string;
+ type: string;
+ name: string;
+ value: string;
+ errors?: string;
+ onChange: (e: React.ChangeEvent) => void;
+}
diff --git a/components/atoms/inputs/LoginInput.tsx b/components/atoms/inputs/LoginInput.tsx
new file mode 100644
index 0000000..7984f03
--- /dev/null
+++ b/components/atoms/inputs/LoginInput.tsx
@@ -0,0 +1,56 @@
+import { useState } from "react";
+
+import { LoginInputProps } from "./Input.types";
+
+export default function LoginInput({
+ label,
+ type,
+ name,
+ value,
+ errors,
+ onChange,
+ ...restProps
+}: LoginInputProps) {
+ const [showPassword, setShowPassword] = useState(false);
+
+ function isPassword() {
+ if (type === "password") {
+ return (
+ setShowPassword(!showPassword)}
+ className="absolute top-1/2 right-4 -translate-y-1/2 text-(--color-text-muted) transition-colors hover:text-(--color-primary)"
+ aria-label={showPassword ? "Ẩn mật khẩu" : "Hiện mật khẩu"}
+ >
+
+
+ );
+ }
+ return "";
+ }
+
+ return (
+
+
+
+
+
+ {isPassword()}
+
+
+ );
+}
diff --git a/components/atoms/inputs/index.ts b/components/atoms/inputs/index.ts
index e4016df..b2b03aa 100644
--- a/components/atoms/inputs/index.ts
+++ b/components/atoms/inputs/index.ts
@@ -5,4 +5,5 @@ export type {
TextInputProps,
SearchInputProps,
TextareaProps,
+ LoginInputProps,
} from "./Input.types";
diff --git a/components/molecules/cards/PaymentSummaryCard.tsx b/components/molecules/cards/PaymentSummaryCard.tsx
index d0fdaf4..79214d0 100644
--- a/components/molecules/cards/PaymentSummaryCard.tsx
+++ b/components/molecules/cards/PaymentSummaryCard.tsx
@@ -36,7 +36,7 @@ export default function PaymentSummaryCard({
setIsReviewOpen(false)}
icon="fa-solid fa-arrow-rotate-left"
size="md"
diff --git a/components/organisms/forms/LoginForm.tsx b/components/organisms/forms/LoginForm.tsx
new file mode 100644
index 0000000..87899c1
--- /dev/null
+++ b/components/organisms/forms/LoginForm.tsx
@@ -0,0 +1,120 @@
+import ErrorMessageLogin from "@/components/atoms/errors/ErrorMessageLogin";
+import LoginInput from "@/components/atoms/inputs/LoginInput";
+import { useAuth } from "@/lib/auth-context";
+import Link from "next/link";
+import { useRouter } from "next/navigation";
+import { FormEvent, useState } from "react";
+import Button from "@/components/atoms/buttons/Button";
+
+export default function LoginForm() {
+ const router = useRouter();
+ const { login } = useAuth();
+
+ const [username, setUsername] = useState("");
+ const [password, setPassword] = useState("");
+ const [errors, setErrors] = useState({
+ username: "",
+ password: "",
+ general: "",
+ });
+
+ const validate = (): boolean => {
+ const newErrors = { username: "", password: "", general: "" };
+ let isValid = true;
+
+ if (!username.trim()) {
+ newErrors.username = "Vui lòng nhập tên đăng nhập";
+ isValid = false;
+ }
+
+ if (!password.trim()) {
+ newErrors.password = "Vui lòng nhập mật khẩu";
+ isValid = false;
+ } else if (password.length < 4) {
+ newErrors.password = "Mật khẩu phải có ít nhất 4 ký tự";
+ isValid = false;
+ }
+
+ setErrors(newErrors);
+ return isValid;
+ };
+
+ const handleSubmit = (e: FormEvent) => {
+ e.preventDefault();
+
+ if (!validate()) return;
+
+ const success = login(username, password);
+
+ if (success) {
+ router.push("/");
+ } else {
+ setErrors({
+ username: "",
+ password: "",
+ general: "Tên đăng nhập hoặc mật khẩu không đúng",
+ });
+ }
+ };
+
+ return (
+
+ {/* Error Message */}
+ {errors.general &&
}
+
+
+
+ );
+}