big update new layout
Build and Release / release (push) Successful in 35s

This commit is contained in:
2026-05-19 18:00:19 +07:00
parent e7c8322c63
commit f5514b8fb5
46 changed files with 1032 additions and 702 deletions
+1 -1
View File
@@ -13,7 +13,7 @@ interface BreadcrumbProps {
const PageBreadcrumb: React.FC<BreadcrumbProps> = ({ pageTitle, paths }) => {
return (
<div className="flex flex-wrap items-center justify-between gap-3 mb-6">
<div className="flex flex-wrap items-center justify-between gap-3 mb-6 sticky top-0 z-40 bg-linear-to-b from-gray-50 via-gray-50/60 to-transparent backdrop-blur-xs pb-8 pt-8">
<h2
className="text-xl font-semibold text-gray-800 dark:text-white/90"
x-text="pageName"
+69
View File
@@ -0,0 +1,69 @@
"use client";
import Link from "next/link";
import React from "react";
interface BreadcrumbPath {
name: string;
href: string;
}
interface StickyHeaderProps {
header: string;
paths?: BreadcrumbPath[];
}
export default function StickyHeader({ header, paths }: StickyHeaderProps) {
return (
<div className="sticky top-0 z-40 bg-gradient-to-b from-gray-50 via-gray-50/60 to-transparent pb-8 pt-8 backdrop-blur-xs dark:from-zinc-950 dark:via-zinc-950/60">
<div className="flex justify-between items-end">
<h1 className="text-3xl font-bold tracking-tight text-gray-900 dark:text-white">
{header}
</h1>
{paths && paths.length > 0 && (
<nav className="mb-3">
<ol className="flex flex-wrap items-center gap-1.5 text-sm">
<li>
<Link
className="inline-flex items-center gap-1.5 text-gray-500 transition-colors hover:text-gray-800 dark:text-gray-400 dark:hover:text-white/90"
href="/"
>
Home
</Link>
</li>
<span className="text-gray-400 dark:text-zinc-600">
<svg width="12" height="12" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg" className="stroke-current" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M6.0765 12.667L10.2432 8.50033L6.0765 4.33366" />
</svg>
</span>
{paths.map((path, index) => (
<React.Fragment key={index}>
<li>
<Link
className="inline-flex items-center gap-1.5 text-gray-500 transition-colors hover:text-gray-800 dark:text-gray-400 dark:hover:text-white/90"
href={path.href}
>
{path.name}
</Link>
</li>
<span className="text-gray-400 dark:text-zinc-600">
<svg width="12" height="12" viewBox="0 0 17 16" fill="none" xmlns="http://www.w3.org/2000/svg" className="stroke-current" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
<path d="M6.0765 12.667L10.2432 8.50033L6.0765 4.33366" />
</svg>
</span>
</React.Fragment>
))}
<li className="font-medium text-gray-800 dark:text-white/90 truncate max-w-[200px]">
{header}
</li>
</ol>
</nav>
)}
</div>
</div>
);
}
+1
View File
@@ -95,6 +95,7 @@ export default function ChatbotWidget({
<div className="fixed bottom-8 right-8 z-50">
{!isOpen && (
<button
name="AI chat"
onClick={() => setIsOpen(true)}
className="w-14 h-14 bg-brand-500 hover:bg-brand-600 text-white rounded-full flex items-center justify-center shadow-[0_4px_14px_rgba(0,0,0,0.25)] transition-transform hover:scale-105"
>
+10 -17
View File
@@ -7,7 +7,7 @@ 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 { EyeCloseIcon, EyeIcon, LockIcon, MailIcon } from "@/icons";
import { apiChangePassword } from "@/service/auth";
import { toast } from "sonner";
@@ -89,26 +89,19 @@ export default function AccountDetails({ data }: { data: UserMetaCardProps }) {
formValues.new_password !== formValues.confirm_password;
return (
<>
<div className="p-5 border border-red-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 className="border-t border-red-200 py-6">
<div className="flex flex-col gap-6">
<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">
<div className="">
<div className="flex items-center gap-2">
<MailIcon className="leading-normal text-gray-500"/>
<p className="text-sm font-medium text-gray-600 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">
<div className="flex items-center gap-3 mt-2">
<LockIcon className="leading-normal text-gray-500"/>
<p className="text-sm font-medium text-gray-600 dark:text-white/90">
</p>
</div>
@@ -117,15 +117,12 @@ export default function ApplicationList({
return (
<div className="rounded-2xl border border-gray-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-900/50">
<div className="mb-8 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between border-b border-gray-100 pb-5 dark:border-zinc-800">
<h3 className="text-xl font-bold text-gray-800 dark:text-white/90">
Hồ {" "}
<h3 className="text-xl font-bold text-gray-800 dark:text-white/90 items-center flex">
Hồ
<span className="ml-2 text-sm font-normal text-gray-500">
({applications.length} tệp)
</span>
</h3>
{/* <div className="text-sm text-gray-500">
Cập nhật lần cuối: {new Date().toLocaleDateString("vi-VN")}
</div> */}
</div>
{/* CHỈ SỬA DÒNG NÀY: Tăng số lượng cột (grid-cols) để các thẻ nhỏ lại */}
+5 -8
View File
@@ -13,7 +13,6 @@ import { deleteMedia } from "@/service/mediaService";
import { INITIAL_LIMIT } from "../../../constant";
import { MediaItem } from "../tables/MediaTable";
export default function MediaLibrary({
data,
onRefresh,
@@ -256,7 +255,6 @@ export default function MediaLibrary({
);
};
return (
<div className="rounded-2xl border border-gray-200 bg-white p-6 dark:border-zinc-800 dark:bg-zinc-900/50">
<div className="mb-6 flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
@@ -271,7 +269,7 @@ export default function MediaLibrary({
{selectedIds.length > 0 && (
<button
onClick={handleDeleteSelected}
className="flex items-center gap-1 rounded-lg bg-red-50 px-4 py-2 text-sm font-medium text-red-600 transition-colors hover:bg-red-100 dark:bg-red-500/10 dark:hover:bg-red-500/20"
className="flex items-center gap-1 h-10 rounded-4xl bg-red-50 px-4 py-2 text-sm font-medium text-red-600 transition-colors hover:bg-red-100 dark:bg-red-500/10 dark:hover:bg-red-500/20"
>
<svg
className="h-4 w-4"
@@ -286,13 +284,13 @@ export default function MediaLibrary({
d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"
/>
</svg>
Xóa ({selectedIds.length})
Xóa {selectedIds.length}
</button>
)}
<button
onClick={toggleSelectionMode}
className={`rounded-lg px-4 py-2 text-sm font-medium transition-all w-17.5 h-10 ${
className={`rounded-4xl px-4 py-2 text-sm font-medium transition-all w-17.5 h-10 ${
isSelectionMode
? "bg-blue-500 text-white shadow-md hover:bg-blue-600"
: "border border-gray-200 bg-gray-50 text-gray-700 hover:bg-gray-100 dark:border-zinc-700 dark:bg-zinc-800 dark:text-gray-200 dark:hover:bg-zinc-700"
@@ -319,7 +317,7 @@ export default function MediaLibrary({
<div className="mt-6 flex justify-center">
<button
onClick={() => setShowAllImages(!showAllImages)}
className="rounded-lg border border-gray-200 bg-gray-50 px-5 py-2 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 dark:border-zinc-700 dark:bg-zinc-800 dark:text-gray-300"
className="rounded-4xl border border-gray-200 bg-gray-50 px-5 py-2 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 dark:border-zinc-700 dark:bg-zinc-800 dark:text-gray-300"
>
{showAllImages
? "Thu gọn"
@@ -336,7 +334,6 @@ export default function MediaLibrary({
Tài liệu ({documentFiles.length})
</h4>
<div className="grid grid-cols-3 gap-3 sm:grid-cols-4 md:grid-cols-5 lg:grid-cols-6 xl:grid-cols-8">
{displayedDocs.map((item, idx) =>
renderItemCard(item, false, idx),
)}
@@ -345,7 +342,7 @@ export default function MediaLibrary({
<div className="mt-6 flex justify-center">
<button
onClick={() => setShowAllDocs(!showAllDocs)}
className="rounded-lg border border-gray-200 bg-gray-50 px-5 py-2 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 dark:border-zinc-700 dark:bg-zinc-800 dark:text-gray-300"
className="rounded-4xl border border-gray-200 bg-gray-50 px-5 py-2 text-sm font-medium text-gray-600 transition-colors hover:bg-gray-100 dark:border-zinc-700 dark:bg-zinc-800 dark:text-gray-300"
>
{showAllDocs
? "Thu gọn"
+56 -82
View File
@@ -1,6 +1,5 @@
"use client";
import React, { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { useModal } from "../../hooks/useModal";
import { Modal } from "../ui/modal";
import Button from "../ui/button/Button";
@@ -11,10 +10,9 @@ import { apiUpdateUser } from "@/service/userService";
import { toast } from "sonner";
import Link from "next/link";
import { AxiosError } from "axios";
import { LinkIcon, LocationIcon, MailIcon, PhoneIcon } from "@/icons";
export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
const router = useRouter();
const { isOpen, openModal, closeModal } = useModal();
const [formData, setFormData] = useState<Profile>({
display_name: "",
@@ -65,7 +63,10 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
window.location.reload();
}, 1000);
} catch (error) {
const axiosError = error as AxiosError<{ status: boolean; message: string }>;
const axiosError = error as AxiosError<{
status: boolean;
message: string;
}>;
const serverResponse = axiosError.response?.data;
if (serverResponse && serverResponse.status === false) {
@@ -82,88 +83,15 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
console.error("Lỗi chi tiết:", axiosError);
}
};
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">
Personal Information
</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">
Full Name
</p>
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
{data.data?.profile?.display_name || "Full Name"}
</p>
</div>
<div>
<p className="mb-2 text-xs leading-normal 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 || "Email address"}
</p>
</div>
{data.data?.profile?.phone && (
<div>
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
Phone
</p>
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
{data.data?.profile?.phone || "+XXX XXX XXX"}
</p>
</div>
)}
{data.data?.profile?.bio && (
<div>
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
Bio
</p>
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
{data.data?.profile?.bio || "No bio available"}
</p>
</div>
)}
{data.data?.profile?.location && (
<div>
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
Address
</p>
<p className="text-sm font-medium text-gray-800 dark:text-white/90">
{data.data?.profile?.location || "No location available"}
</p>
</div>
)}
{data.data?.profile?.website && (
<div>
<p className="mb-2 text-xs leading-normal text-gray-500 dark:text-gray-400">
Website
</p>
<Link
href={data.data?.profile?.website || "#"}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-medium text-gray-800 dark:text-white/90 hover:text-blue-500 dark:hover:text-blue-400"
>
{data.data?.profile?.website || "No website"}
</Link>
</div>
)}
</div>
</div>
<div className="py-6">
<div className="flex flex-col gap-6">
{data.openEdit && (
<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-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 lg:inline-flex lg:w-auto"
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-gray-700 shadow-theme-xs hover:bg-gray-50 hover:text-gray-600 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"
@@ -181,19 +109,65 @@ export default function UserInfoCard({ data }: { data: UserMetaCardProps }) {
Edit
</button>
)}
<div>
<div className=" gap-4 lg:grid-cols-2 lg:gap-7 2xl:gap-x-32">
<div className="flex items-center gap-2 py-2">
<MailIcon className="leading-normal text-gray-500" />
<p className="text-sm font-medium text-gray-600 ">
{data.data?.email || "Email address"}
</p>
</div>
{data.data?.profile?.phone && (
<div className="flex items-center gap-2 py-2">
<PhoneIcon className="w-5 h-5 opacity-60" />
<p className="text-sm font-medium text-gray-600 ">
{data.data?.profile?.phone || "+XXX XXX XXX"}
</p>
</div>
)}
{data.data?.profile?.location && (
<div className="flex items-center gap-2 py-2">
<LocationIcon className="w-5 h-5 opacity-60" />
<p className="text-sm font-medium text-gray-600 ">
{data.data?.profile?.location || "No location available"}
</p>
</div>
)}
{data.data?.profile?.website && (
<div className="flex items-center gap-2 py-2">
<LinkIcon className="w-5 h-5 opacity-60" />
<Link
href={data.data?.profile?.website || "#"}
target="_blank"
rel="noopener noreferrer"
className="text-sm font-medium text-gray-600 hover:text-blue-500 dark:hover:text-blue-400"
>
{data.data?.profile?.website || "No website"}
</Link>
</div>
)}
</div>
</div>
</div>
<Modal isOpen={isOpen} onClose={closeModal} className="max-w-[700px] m-4">
<div className="no-scrollbar relative w-full max-w-[700px] overflow-y-auto rounded-3xl bg-white p-4 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">
<h4 className="mb-2 text-2xl font-semibold text-gray-600 ">
Edit Personal Information
</h4>
</div>
<form className="flex flex-col" onSubmit={handleSave}>
<div className="custom-scrollbar h-[450px] overflow-y-auto px-2 pb-3">
<div className="mt-7">
<h5 className="mb-5 text-lg font-medium text-gray-800 dark:text-white/90 lg:mb-6">
<h5 className="mb-5 text-lg font-medium text-gray-600 lg:mb-6">
Personal Information
</h5>
+17 -20
View File
@@ -72,16 +72,16 @@ export default function UserMetaCard({ data }: { data: UserMetaCardProps }) {
}
};
return (
<div className="p-5 border border-gray-200 rounded-2xl dark:border-gray-800 lg:p-6">
<div className="flex flex-col gap-5 xl:flex-row xl:items-center xl:justify-between">
<div className="flex flex-col items-center w-full gap-6 xl:flex-row">
<div className="">
<div className="flex flex-col gap-5 ">
<div className="flex flex-col items-center w-full gap-6">
<div
onClick={handleAvatarClick}
className="relative w-20 h-20 overflow-hidden border border-gray-200 rounded-full cursor-pointer dark:border-gray-800 group shrink-0"
className="relative w-[296px] h-[296px] overflow-hidden border border-gray-200 rounded-full cursor-pointer dark:border-gray-800 group shrink-0"
>
<Image
width={80}
height={80}
width={296}
height={296}
src={previewImage}
alt="avatar"
className="object-cover w-full h-full"
@@ -142,26 +142,23 @@ export default function UserMetaCard({ data }: { data: UserMetaCardProps }) {
</div>
<div className="order-3 xl:order-2">
<h4 className="mb-2 text-lg font-semibold text-center text-gray-800 dark:text-white/90 xl:text-left">
{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>
{data.data?.profile?.bio && (
<>
<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 max-w-[450px] truncate">
{data.data.profile.bio}
</p>
</>
)}
</div>
<span className="text-sm text-gray-500 dark:text-gray-400">
ID: {data.data?.id}
</span>
<h4 className="mb-2 text-2xl font-semibold text-gray-700 text-left">
{data.data?.profile?.display_name || "Full Name"}
</h4>
{data.data?.profile?.bio && (
<p className="text-sm text-gray-500">
{data.data.profile.bio}
</p>
)}
<p className="text-sm text-gray-500 dark:text-gray-400">
ID: <span>{data.data?.id}</span>
</p>
</div>
</div>
</div>