This commit is contained in:
2026-04-07 16:20:18 +07:00
parent 91f3b16634
commit e3065e1bf5
16 changed files with 442 additions and 195 deletions

View File

@@ -73,7 +73,7 @@ export default function SignInForm() {
console.log("Current User Data:", data);
if (data?.data) {
dispatch(setUserData(data.data));
// router.push("/profile");
router.push("/");
}
} else {
toast.error("Email hoặc mật khẩu không đúng.");

View File

@@ -0,0 +1,223 @@
"use client";
import React, { useState, useEffect } from "react";
import { useModal } from "../../hooks/useModal";
import { Modal } from "../ui/modal";
import Button from "../ui/button/Button";
import Input from "../form/input/InputField";
import Label from "../form/Label";
import { UserMetaCardProps } from "@/interface/user";
import { useRouter } from "next/navigation";
import { EyeCloseIcon, EyeIcon } from "@/icons";
import { apiChangePassword } from "@/service/auth";
import { toast } from "sonner";
export default function AccountDetails({ data }: { data: UserMetaCardProps }) {
const router = useRouter();
const { isOpen, openModal, closeModal } = useModal();
const [formValues, setFormValues] = useState({
old_password: "",
new_password: "",
confirm_password: "",
});
const [showOldPass, setShowOldPass] = useState(false);
const [error, setError] = useState("");
useEffect(() => {
if (!isOpen) {
setFormValues({
old_password: "",
new_password: "",
confirm_password: "",
});
setShowOldPass(false);
setError("");
}
}, [isOpen]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormValues((prev) => ({ ...prev, [name]: value }));
};
const handleSave = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
if (formValues.new_password !== formValues.confirm_password) {
setError("Mật khẩu mới và xác nhận mật khẩu không khớp.");
return;
}
if (formValues.new_password.length < 8) {
setError("Mật khẩu mới phải có ít nhất 8 ký tự.");
return;
}
try {
const userId = data?.data?.id;
if (userId) {
const payload = {
old_password: formValues.old_password,
new_password: formValues.new_password,
};
await apiChangePassword(userId, payload as any);
closeModal();
toast.success("Cập nhật thành công!");
router.refresh();
}
} catch (err: any) {
setError(
`${err?.response?.data?.message}, Cập nhật thất bại, vui lòng kiểm tra lại thông tin. Mật khẩu tối thiểu 8 ký tự, 1 in hoa, 1 số và 1 ký tự đặc biệt.` ||
"Failed to update password. Please check your current password.",
);
toast.error(err?.response?.data?.message || "Cập nhật thất bại, vui lòng kiểm tra lại thông tin!");
}
};
return (
<>
<div className="p-5 border border-gray-200 rounded-2xl dark:border-gray-800 lg:p-6">
<div className="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between">
<div>
<h4 className="text-lg font-semibold text-gray-800 dark:text-white/90 lg:mb-6">
Account Details
</h4>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 lg:gap-7 2xl:gap-x-32">
<div>
<p className="mb-2 text-xs text-gray-500 dark:text-gray-400">
Email Address
</p>
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
{data?.data?.email || "example@mail.com"}
</p>
</div>
<div>
<p className="mb-2 text-xs text-gray-500 dark:text-gray-400">
Password
</p>
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
</p>
</div>
</div>
</div>
<button
onClick={openModal}
className="flex items-center justify-center gap-2 rounded-full border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 lg:inline-flex lg:w-auto"
>
<svg
className="fill-current"
width="18"
height="18"
viewBox="0 0 18 18"
>
<path d="M15.0911 2.78206C14.2125 1.90338 12.7878 1.90338 11.9092 2.78206L4.57524 10.116C4.26682 10.4244 4.0547 10.8158 3.96468 11.2426L3.31231 14.3352C3.25997 14.5833 3.33653 14.841 3.51583 15.0203C3.69512 15.1996 3.95286 15.2761 4.20096 15.2238L7.29355 14.5714C7.72031 14.4814 8.11172 14.2693 8.42013 13.9609L15.7541 6.62695C16.6327 5.74827 16.6327 4.32365 15.7541 3.44497L15.0911 2.78206ZM12.9698 3.84272C13.2627 3.54982 13.7376 3.54982 14.0305 3.84272L14.6934 4.50563C14.9863 4.79852 14.9863 5.2734 14.6934 5.56629L14.044 6.21573L12.3204 4.49215L12.9698 3.84272ZM11.2597 5.55281L5.6359 11.1766C5.53309 11.2794 5.46238 11.4099 5.43238 11.5522L5.01758 13.5185L6.98394 13.1037C7.1262 13.0737 7.25666 13.003 7.35947 12.9002L12.9833 7.27639L11.2597 5.55281Z" />
</svg>
Edit
</button>
</div>
</div>
{/* Modal Chỉnh sửa */}
<Modal isOpen={isOpen} onClose={closeModal} className="max-w-[700px] m-4">
<div className="relative w-full p-4 overflow-y-auto bg-white no-scrollbar rounded-3xl dark:bg-gray-900 lg:p-11">
<div className="px-2 pr-14">
<h4 className="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90">
Update Password
</h4>
<p className="mb-6 text-sm text-gray-500 dark:text-gray-400">
For security, please enter your current password to set a new one.
</p>
</div>
<form className="flex flex-col" onSubmit={handleSave}>
<div className="px-2 overflow-y-auto custom-scrollbar">
<div className="grid grid-cols-1 gap-x-6 gap-y-5 lg:grid-cols-2">
{/* 1. Email (Disabled) */}
<div className="col-span-2">
<Label>Email Address</Label>
<Input
type="email"
defaultValue={data?.data?.email || ""}
disabled
className="bg-gray-100 dark:bg-gray-800 cursor-not-allowed"
/>
</div>
{/* 2. Current Password - CÓ NÚT HIỆN MẬT KHẨU */}
<div className="col-span-2 relative">
<Label>Current Password</Label>
<div className="relative">
<Input
type={showOldPass ? "text" : "password"}
name="old_password"
placeholder="Enter current password"
defaultValue={formValues.old_password}
onChange={handleChange}
/>
<button
type="button"
onClick={() => setShowOldPass(!showOldPass)}
className="absolute right-4 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
>
{showOldPass ? (
<EyeCloseIcon className="w-5 h-5" />
) : (
<EyeIcon className="w-5 h-5" />
)}
</button>
</div>
</div>
{/* 3. New Password - LUÔN ẨN */}
<div className="col-span-2 lg:col-span-1">
<Label>New Password</Label>
<Input
type="text"
name="new_password"
placeholder="Enter new password"
defaultValue={formValues.new_password}
onChange={handleChange}
/>
</div>
{/* 4. Confirm New Password - LUÔN ẨN */}
<div className="col-span-2 lg:col-span-1">
<Label>Confirm New Password</Label>
<Input
type="text"
name="confirm_password"
placeholder="Re-type new password"
defaultValue={formValues.confirm_password}
onChange={handleChange}
/>
</div>
</div>
{error && <p className="mt-4 text-sm text-red-500">{error}</p>}
</div>
<div className="flex items-center gap-3 px-2 mt-6 lg:justify-end">
<Button
size="sm"
variant="outline"
type="button"
onClick={closeModal}
>
Close
</Button>
<Button size="sm" type="submit">
Save Changes
</Button>
</div>
</form>
</div>
</Modal>
</>
);
}

View File

@@ -0,0 +1,73 @@
"use client";
import { useState } from "react";
import Lightbox from "yet-another-react-lightbox";
import Zoom from "yet-another-react-lightbox/plugins/zoom";
import Captions from "yet-another-react-lightbox/plugins/captions";
import "yet-another-react-lightbox/styles.css";
import "yet-another-react-lightbox/plugins/captions.css";
import { MediaDto } from "@/interface/media";
import { URL_MEDIA } from "../../../api";
export default function MediaCard({ data }: { data: MediaDto }) {
const [index, setIndex] = useState(-1);
const listMedia = data?.data || [];
const slides = listMedia.map((item) => ({
src: `${URL_MEDIA}${item.storage_key}`,
title: item.original_name,
description: `Size: ${(item.size / 1024).toFixed(2)} KB - Type: ${item.mime_type}`,
}));
return (
<div className="w-full">
<h3 className="mb-4 font-semibold">Media Assets</h3>
<div className="flex flex-row gap-4 overflow-x-auto pb-4 scrollbar-hide">
{listMedia.map((item, idx) => (
<div
key={item.id}
onClick={() => setIndex(idx)}
className="group relative min-w-[150px] h-[150px] cursor-pointer overflow-hidden rounded-lg border border-gray-200"
>
<img
src={`${URL_MEDIA}${item.storage_key}`}
alt={item.original_name}
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-110"
/>
<div className="absolute inset-0 flex items-end bg-black/40 p-2 opacity-0 transition-opacity duration-300 group-hover:opacity-100">
<p className="w-full truncate text-xs text-white">
{item.original_name}
</p>
</div>
</div>
))}
</div>
<Lightbox
index={index}
open={index >= 0}
close={() => setIndex(-1)}
slides={slides}
plugins={[Zoom, Captions]}
zoom={{
maxZoomPixelRatio: 10,
zoomInMultiplier: 2,
doubleTapDelay: 300,
doubleClickDelay: 300,
doubleClickMaxStops: 2,
keyboardMoveDistance: 50,
wheelZoomDistanceFactor: 100,
pinchZoomDistanceFactor: 100,
}}
animation={{ zoom: 200 }}
styles={{
root: {
zIndex: 999999999,
"--yarl__color_backdrop": "rgba(0, 0, 0, 0.85)",
},
}}
/>
</div>
);
}

View File

@@ -1,140 +0,0 @@
"use client";
import React, { useState } from "react";
import { useModal } from "../../hooks/useModal";
import { Modal } from "../ui/modal";
import Button from "../ui/button/Button";
import Input from "../form/input/InputField";
import Label from "../form/Label";
import { Profile } from "@/interface/user";
export default function UserAddressCard() {
const { isOpen, openModal, closeModal } = useModal();
const [formData, setFormData] = useState<Profile>({
display_name: "",
phone: "",
bio: "",
location: "",
website: "",
// Thêm các field khác nếu cần
});
useEffect(() => {
if (data?.data?.profile) {
setFormData({
display_name: data.data.profile.display_name || "",
phone: data.data.profile.phone || "",
bio: data.data.profile.bio || "",
location: data.data.profile.location || "",
website: data.data.profile.website || "",
});
}
}, [data, isOpen]);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData((prev) => ({
...prev,
[name]: value,
}));
};
return (
<>
<div className="p-5 border border-gray-200 rounded-2xl dark:border-gray-800 lg:p-6">
<div className="flex flex-col gap-6 lg:flex-row lg:items-start lg:justify-between">
<div>
<h4 className="text-lg font-semibold text-red-600 dark:text-white/90 lg:mb-6">
Account details
</h4>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 lg:gap-7 2xl:gap-x-32">
<div>
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
Username
</p>
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
United States
</p>
</div>
<div>
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
City/State
</p>
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
Phoenix, Arizona, United States.
</p>
</div>
</div>
</div>
<button
onClick={openModal}
className="flex w-full items-center justify-center gap-2 rounded-full border border-gray-300 bg-white px-4 py-3 text-sm font-medium text-red-600 shadow-theme-xs hover:bg-gray-50 hover:text-red-800 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-400 dark:hover:bg-white/[0.03] dark:hover:text-gray-200 lg:inline-flex lg:w-auto"
>
<svg
className="fill-current"
width="18"
height="18"
viewBox="0 0 18 18"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M15.0911 2.78206C14.2125 1.90338 12.7878 1.90338 11.9092 2.78206L4.57524 10.116C4.26682 10.4244 4.0547 10.8158 3.96468 11.2426L3.31231 14.3352C3.25997 14.5833 3.33653 14.841 3.51583 15.0203C3.69512 15.1996 3.95286 15.2761 4.20096 15.2238L7.29355 14.5714C7.72031 14.4814 8.11172 14.2693 8.42013 13.9609L15.7541 6.62695C16.6327 5.74827 16.6327 4.32365 15.7541 3.44497L15.0911 2.78206ZM12.9698 3.84272C13.2627 3.54982 13.7376 3.54982 14.0305 3.84272L14.6934 4.50563C14.9863 4.79852 14.9863 5.2734 14.6934 5.56629L14.044 6.21573L12.3204 4.49215L12.9698 3.84272ZM11.2597 5.55281L5.6359 11.1766C5.53309 11.2794 5.46238 11.4099 5.43238 11.5522L5.01758 13.5185L6.98394 13.1037C7.1262 13.0737 7.25666 13.003 7.35947 12.9002L12.9833 7.27639L11.2597 5.55281Z"
fill=""
/>
</svg>
Edit
</button>
</div>
</div>
<Modal isOpen={isOpen} onClose={closeModal} className="max-w-[700px] m-4">
<div className="relative w-full p-4 overflow-y-auto bg-white no-scrollbar rounded-3xl dark:bg-gray-900 lg:p-11">
<div className="px-2 pr-14">
<h4 className="mb-2 text-2xl font-semibold text-gray-800 dark:text-white/90">
Edit Address
</h4>
<p className="mb-6 text-sm text-gray-500 dark:text-gray-400 lg:mb-7">
Update your details to keep your profile up-to-date.
</p>
</div>
<form className="flex flex-col">
<div className="px-2 overflow-y-auto custom-scrollbar">
<div className="grid grid-cols-1 gap-x-6 gap-y-5 lg:grid-cols-2">
<div>
<Label>Country</Label>
<Input type="text" defaultValue="United States" />
</div>
<div>
<Label>City/State</Label>
<Input type="text" defaultValue="Arizona, United States." />
</div>
<div>
<Label>Postal Code</Label>
<Input type="text" defaultValue="ERT 2489" />
</div>
<div>
<Label>TAX ID</Label>
<Input type="text" defaultValue="AS4568384" />
</div>
</div>
</div>
<div className="flex items-center gap-3 px-2 mt-6 lg:justify-end">
<Button size="sm" variant="outline" onClick={closeModal}>
Close
</Button>
<Button size="sm" onClick={handleSave}>
Save Changes
</Button>
</div>
</form>
</div>
</Modal>
</>
);
}

View File

@@ -14,14 +14,13 @@ import Link from "next/link";
export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
const router = useRouter();
const { isOpen, openModal, closeModal } = useModal();
// 1. Khởi tạo state cho form
const [formData, setFormData] = useState<Profile>({
display_name: "",
phone: "",
bio: "",
location: "",
website: "",
// Thêm các field khác nếu cần
avatar_url: "",
});
useEffect(() => {
@@ -32,6 +31,7 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
bio: data.data.profile.bio || "",
location: data.data.profile.location || "",
website: data.data.profile.website || "",
avatar_url: data.data.profile.avatar_url || "",
});
}
}, [data, isOpen]);
@@ -65,6 +65,14 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
</h4>
<div className="grid grid-cols-1 gap-4 lg:grid-cols-2 lg:gap-7 2xl:gap-x-32">
{/* <div>
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
Avatar
</p>
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
{data.data?.profile?.avatar_url || "avatar_url"}
</p>
</div> */}
<div>
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
Full Name
@@ -164,6 +172,15 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
</h5>
<div className="grid grid-cols-1 gap-x-6 gap-y-5 lg:grid-cols-2">
<div className="col-span-2">
<Label>Avatar</Label>
<Input
type="text"
name="avatar_url"
defaultValue={formData.avatar_url}
onChange={handleChange}
/>
</div>
<div className="col-span-2">
<Label>Display Name</Label>
<Input

View File

@@ -1,20 +1,15 @@
"use client";
import React from "react";
import { useModal } from "../../hooks/useModal";
import { Modal } from "../ui/modal";
import Button from "../ui/button/Button";
import Input from "../form/input/InputField";
import Label from "../form/Label";
// import React from "react";
// import { useModal } from "../../hooks/useModal";
// import { Modal } from "../ui/modal";
// import Button from "../ui/button/Button";
// import Input from "../form/input/InputField";
// import Label from "../form/Label";
import Image from "next/image";
import { UserMetaCardProps } from "@/interface/user";
export default function UserMetaCard({ data }: { data: UserMetaCardProps }) {
const { isOpen, openModal, closeModal } = useModal();
const handleSave = () => {
console.log("Saving changes...");
closeModal();
};
console.log("UserMetaCard data:", data.data?.profile?.display_name);
// const { isOpen, openModal, closeModal } = useModal();
return (
<>
<div className="p-5 border border-gray-200 rounded-2xl dark:border-gray-800 lg:p-6">
@@ -33,6 +28,11 @@ export default function UserMetaCard({ data }: { data: UserMetaCardProps }) {
{data.data?.profile?.display_name || "Full Name"}
</h4>
<div className="flex flex-col items-center gap-1 text-center xl:flex-row xl:gap-3 xl:text-left">
<p className="text-sm text-blue-500 dark:text-gray-400">
{data.data?.roles?.map((role) => role.name).join(", ") ||
"No roles available"}
</p>
<div className="hidden h-3.5 w-px bg-gray-300 dark:bg-gray-700 xl:block"></div>
<p className="text-sm text-gray-500 dark:text-gray-400">
{data.data?.profile?.bio || "No bio available"}
</p>
@@ -40,11 +40,6 @@ export default function UserMetaCard({ data }: { data: UserMetaCardProps }) {
<p className="text-sm text-gray-500 dark:text-gray-400">
{data.data?.profile?.location || "user location"}
</p>
<div className="hidden h-3.5 w-px bg-gray-300 dark:bg-gray-700 xl:block"></div>
<p className="text-sm text-blue-500 dark:text-gray-400">
{data.data?.roles?.map((role) => role.name).join(", ") ||
"No roles available"}
</p>
</div>
</div>
{/* <div className="flex items-center order-2 gap-2 grow xl:order-3 xl:justify-end">