diff --git a/public/font/SF-Pro-Display/SF-Pro-Display-Black.otf b/public/font/SF-Pro-Display/SF-Pro-Display-Black.otf new file mode 100755 index 0000000..8463d9b Binary files /dev/null and b/public/font/SF-Pro-Display/SF-Pro-Display-Black.otf differ diff --git a/public/font/SF-Pro-Display/SF-Pro-Display-BlackItalic.otf b/public/font/SF-Pro-Display/SF-Pro-Display-BlackItalic.otf new file mode 100755 index 0000000..edab1dd Binary files /dev/null and b/public/font/SF-Pro-Display/SF-Pro-Display-BlackItalic.otf differ diff --git a/public/font/SF-Pro-Display/SF-Pro-Display-Bold.otf b/public/font/SF-Pro-Display/SF-Pro-Display-Bold.otf new file mode 100755 index 0000000..28fa5a4 Binary files /dev/null and b/public/font/SF-Pro-Display/SF-Pro-Display-Bold.otf differ diff --git a/public/font/SF-Pro-Display/SF-Pro-Display-BoldItalic.otf b/public/font/SF-Pro-Display/SF-Pro-Display-BoldItalic.otf new file mode 100755 index 0000000..5f0f7f2 Binary files /dev/null and b/public/font/SF-Pro-Display/SF-Pro-Display-BoldItalic.otf differ diff --git a/public/font/SF-Pro-Display/SF-Pro-Display-Heavy.otf b/public/font/SF-Pro-Display/SF-Pro-Display-Heavy.otf new file mode 100755 index 0000000..4719416 Binary files /dev/null and b/public/font/SF-Pro-Display/SF-Pro-Display-Heavy.otf differ diff --git a/public/font/SF-Pro-Display/SF-Pro-Display-HeavyItalic.otf b/public/font/SF-Pro-Display/SF-Pro-Display-HeavyItalic.otf new file mode 100755 index 0000000..35275fe Binary files /dev/null and b/public/font/SF-Pro-Display/SF-Pro-Display-HeavyItalic.otf differ diff --git a/public/font/SF-Pro-Display/SF-Pro-Display-Light.otf b/public/font/SF-Pro-Display/SF-Pro-Display-Light.otf new file mode 100755 index 0000000..42ef1f1 Binary files /dev/null and b/public/font/SF-Pro-Display/SF-Pro-Display-Light.otf differ diff --git a/public/font/SF-Pro-Display/SF-Pro-Display-LightItalic.otf b/public/font/SF-Pro-Display/SF-Pro-Display-LightItalic.otf new file mode 100755 index 0000000..b70d379 Binary files /dev/null and b/public/font/SF-Pro-Display/SF-Pro-Display-LightItalic.otf differ diff --git a/public/font/SF-Pro-Display/SF-Pro-Display-Medium.otf b/public/font/SF-Pro-Display/SF-Pro-Display-Medium.otf new file mode 100755 index 0000000..668ba74 Binary files /dev/null and b/public/font/SF-Pro-Display/SF-Pro-Display-Medium.otf differ diff --git a/public/font/SF-Pro-Display/SF-Pro-Display-MediumItalic.otf b/public/font/SF-Pro-Display/SF-Pro-Display-MediumItalic.otf new file mode 100755 index 0000000..229cec0 Binary files /dev/null and b/public/font/SF-Pro-Display/SF-Pro-Display-MediumItalic.otf differ diff --git a/public/font/SF-Pro-Display/SF-Pro-Display-Regular.otf b/public/font/SF-Pro-Display/SF-Pro-Display-Regular.otf new file mode 100755 index 0000000..7042365 Binary files /dev/null and b/public/font/SF-Pro-Display/SF-Pro-Display-Regular.otf differ diff --git a/public/font/SF-Pro-Display/SF-Pro-Display-RegularItalic.otf b/public/font/SF-Pro-Display/SF-Pro-Display-RegularItalic.otf new file mode 100755 index 0000000..f62d31e Binary files /dev/null and b/public/font/SF-Pro-Display/SF-Pro-Display-RegularItalic.otf differ diff --git a/public/font/SF-Pro-Display/SF-Pro-Display-Semibold.otf b/public/font/SF-Pro-Display/SF-Pro-Display-Semibold.otf new file mode 100755 index 0000000..081b59b Binary files /dev/null and b/public/font/SF-Pro-Display/SF-Pro-Display-Semibold.otf differ diff --git a/public/font/SF-Pro-Display/SF-Pro-Display-SemiboldItalic.otf b/public/font/SF-Pro-Display/SF-Pro-Display-SemiboldItalic.otf new file mode 100755 index 0000000..04467cb Binary files /dev/null and b/public/font/SF-Pro-Display/SF-Pro-Display-SemiboldItalic.otf differ diff --git a/public/font/SF-Pro-Display/SF-Pro-Display-Thin.otf b/public/font/SF-Pro-Display/SF-Pro-Display-Thin.otf new file mode 100755 index 0000000..647830a Binary files /dev/null and b/public/font/SF-Pro-Display/SF-Pro-Display-Thin.otf differ diff --git a/public/font/SF-Pro-Display/SF-Pro-Display-ThinItalic.otf b/public/font/SF-Pro-Display/SF-Pro-Display-ThinItalic.otf new file mode 100755 index 0000000..2568d5f Binary files /dev/null and b/public/font/SF-Pro-Display/SF-Pro-Display-ThinItalic.otf differ diff --git a/public/font/SF-Pro-Display/SF-Pro-Display-Ultralight.otf b/public/font/SF-Pro-Display/SF-Pro-Display-Ultralight.otf new file mode 100755 index 0000000..c6cf452 Binary files /dev/null and b/public/font/SF-Pro-Display/SF-Pro-Display-Ultralight.otf differ diff --git a/public/font/SF-Pro-Display/SF-Pro-Display-UltralightItalic.otf b/public/font/SF-Pro-Display/SF-Pro-Display-UltralightItalic.otf new file mode 100755 index 0000000..8e76159 Binary files /dev/null and b/public/font/SF-Pro-Display/SF-Pro-Display-UltralightItalic.otf differ diff --git a/src/app/fonts.css b/src/app/fonts.css new file mode 100644 index 0000000..e2fb99a --- /dev/null +++ b/src/app/fonts.css @@ -0,0 +1,33 @@ + + +@font-face { + font-family: 'SF Pro Display'; + src: url('/font/SF-Pro-Display/SF-Pro-Display-Regular.otf') format('otf'); + font-weight: 400; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'SF Pro Display'; + src: url('/font/SF-Pro-Display/SF-Pro-Display-Medium.otf') format('otf'); + font-weight: 500; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'SF Pro Display'; + src: url('/font/SF-Pro-Display/SF-Pro-Display-Semibold.otf') format('otf'); + font-weight: 600; + font-style: normal; + font-display: swap; +} + +@font-face { + font-family: 'SF Pro Display'; + src: url('/font/SF-Pro-Display/SF-Pro-Display-Bold.otf') format('otf'); + font-weight: 700; + font-style: normal; + font-display: swap; +} \ No newline at end of file diff --git a/src/app/globals.css b/src/app/globals.css index b801797..440e0e0 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,11 +1,13 @@ @import 'tailwindcss'; +@import './fonts.css'; + @custom-variant dark (&:is(.dark *)); @theme { --font-*: initial; --font-outfit: Outfit, sans-serif; - --font-inter: "Inter", ui-sans-serif, system-ui, sans-serif; + --font-inter: "SF Pro Display", ui-sans-serif, system-ui, sans-serif; --breakpoint-*: initial; --breakpoint-2xsm: 375px; diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 4d7540d..347cd73 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,15 +1,35 @@ -import { Inter } from 'next/font/google'; +import localFont from 'next/font/local'; import './globals.css'; import "flatpickr/dist/flatpickr.css"; import { SidebarProvider } from '@/context/SidebarContext'; import { ThemeProvider } from '@/context/ThemeContext'; import { Toaster } from 'sonner'; import StoreProvider from '@/store/StoreProvider'; - -const inter = Inter({ - subsets: ["latin"], -}); - + +const sfPro = localFont({ + src: [ + { + path: '../../public/font/SF-Pro-Display/SF-Pro-Display-Regular.otf', + weight: '400', + style: 'normal', + }, + { + path: '../../public/font/SF-Pro-Display/SF-Pro-Display-Medium.otf', + weight: '500', + style: 'normal', + }, + { + path: '../../public/font/SF-Pro-Display/SF-Pro-Display-Semibold.otf', + weight: '600', + style: 'normal', + }, + { + path: '../../public/font/SF-Pro-Display/SF-Pro-Display-Bold.otf', + weight: '700', + style: 'normal', + }, + ], + }) export default function RootLayout({ children, }: Readonly<{ @@ -17,7 +37,7 @@ export default function RootLayout({ }>) { return ( - + {children} diff --git a/src/app/user/account/applications/page.tsx b/src/app/user/account/applications/page.tsx index 501ee5a..bdf73f3 100644 --- a/src/app/user/account/applications/page.tsx +++ b/src/app/user/account/applications/page.tsx @@ -17,6 +17,7 @@ import "yet-another-react-lightbox/plugins/captions.css"; import Image from "next/image"; import { URL_MEDIA } from "../../../../../api"; import { MediaItem } from "@/components/tables/MediaTable"; +import StickyHeader from "@/components/ui/StickyHeader"; export default function ApplicationDetailPage() { const application = useSelector( @@ -132,26 +133,14 @@ export default function ApplicationDetailPage() { }); }; - // console.log("Application Detail:", application); + const path =[ + {name: "Thư viện", href:"/user/library/"} + ] return ( -
-
-
-

- Chi tiết đơn đăng ký -

-
- -
- - {application.status} - -
-
- + <> + +
+
+ ); } diff --git a/src/app/user/account/page.tsx b/src/app/user/account/page.tsx index 8b2c08a..425d95c 100644 --- a/src/app/user/account/page.tsx +++ b/src/app/user/account/page.tsx @@ -1,5 +1,6 @@ "use client"; +import StickyHeader from "@/components/ui/StickyHeader"; import AccountDetails from "@/components/user-profile/AccountDetails"; import UserInfoCard from "@/components/user-profile/UserInfoCard"; import UserMetaCard from "@/components/user-profile/UserMetaCard"; @@ -31,11 +32,9 @@ export default function Profile() { return (
-
-

- Profile -

-
+ +
+
diff --git a/src/app/user/layout.tsx b/src/app/user/layout.tsx index cbca4bb..dc26440 100644 --- a/src/app/user/layout.tsx +++ b/src/app/user/layout.tsx @@ -9,7 +9,6 @@ import { setUserData } from "@/store/features/userSlice"; import React, { useEffect } from "react"; import { useDispatch } from "react-redux"; import ChatbotWidget from "@/components/ui/chat/ChatbotWidget"; - export default function AdminLayout({ children, }: { @@ -31,7 +30,6 @@ export default function AdminLayout({ }, []) - // Dynamic class for main content margin based on sidebar state const mainContentMargin = isMobileOpen ? "ml-0" : isExpanded || isHovered @@ -40,17 +38,13 @@ export default function AdminLayout({ return (
- {/* Sidebar and Backdrop */} - {/* Main Content Area */}
- {/* Header */} - - {/* Page Content */} -
{children}
+ {/* */} +
{children}
diff --git a/src/app/user/library/page.tsx b/src/app/user/library/page.tsx index c8ff121..1ac2d8b 100644 --- a/src/app/user/library/page.tsx +++ b/src/app/user/library/page.tsx @@ -7,6 +7,7 @@ import { apiGetCurrentUserApplications, apiGetCurrentUserMedia } from "@/service import MediaLibrary from "@/components/user-profile/Media"; import { Application } from "@/interface/historian"; import Loading from "@/app/loading"; +import StickyHeader from "@/components/ui/StickyHeader"; export default function LibraryPage() { const [mediaData, setMediaData] = useState(null); @@ -37,34 +38,33 @@ export default function LibraryPage() { const hasNoData = (!mediaData?.data || mediaData.data.length === 0) && applications.length === 0; return ( -
-
-

- Thư viện -

+
+ + + +
+ {hasNoData ? ( +
+

+ Chưa có nội dung nào trong thư viện +

+
+ ) : ( +
+ {(mediaData?.data?.length ?? 0) > 0 && ( +
+ +
+ )} + + {applications.length > 0 && ( +
+ +
+ )} +
+ )}
- - {hasNoData ? ( -
-

- Chưa có nội dung nào trong thư viện -

-
- ) : ( -
- {(mediaData?.data?.length ?? 0) > 0 && ( -
- -
- )} - - {applications.length > 0 && ( -
- -
- )} -
- )}
); } \ No newline at end of file diff --git a/src/app/user/page.tsx b/src/app/user/page.tsx index 65843b4..fcf0981 100644 --- a/src/app/user/page.tsx +++ b/src/app/user/page.tsx @@ -9,7 +9,7 @@ import DemographicCard from "@/components/ecommerce/DemographicCard"; export const metadata: Metadata = { title: - "Admin Dashboard", + "Home Page", description: "This is Dashboard Home for History Web", }; diff --git a/src/app/user/projects/[id]/page.tsx b/src/app/user/projects/[id]/page.tsx index 810f077..880d2ce 100644 --- a/src/app/user/projects/[id]/page.tsx +++ b/src/app/user/projects/[id]/page.tsx @@ -11,6 +11,8 @@ import PageBreadcrumb from "@/components/common/PageBreadCrumb"; import { apiAddProjectMember, apiChangeProjectOwner, apiDeleteProject, apiGetProjectDetail, apiRemoveProjectMember, apiUpdateProject, apiUpdateProjectMemberRole } from "@/service/projectService"; import Loading from "@/app/loading"; import Button from "@/components/ui/button/Button"; +import StickyHeader from "@/components/ui/StickyHeader"; +import { PencilIcon } from "@/icons"; type TabType = "overview" | "members" | "settings"; @@ -216,12 +218,12 @@ export default function ProjectDetailsPage() { ); // console.log(project) + const path =[ + {name: "Quản lý dự án", href:"/user/projects/"} + ] return (
- +
@@ -308,8 +310,9 @@ export default function ProjectDetailsPage() { ))}
-
diff --git a/src/app/user/projects/page.tsx b/src/app/user/projects/page.tsx index 279c908..152403e 100644 --- a/src/app/user/projects/page.tsx +++ b/src/app/user/projects/page.tsx @@ -12,9 +12,15 @@ import Button from "@/components/ui/button/Button"; import Label from "@/components/form/Label"; import Badge from "@/components/ui/badge/Badge"; import { CreateProjectPayload, Project } from "@/interface/project"; -import { apiCreateProject, apiCreateProjectCommit, apiGetProjectCommits, getCurrentProject } from "@/service/projectService"; +import { + apiCreateProject, + apiCreateProjectCommit, + apiGetProjectCommits, + getCurrentProject, +} from "@/service/projectService"; import { normalizeEditorSnapshot } from "@/uhm/lib/editor/snapshot/editorSnapshot"; import type { EditorSnapshot } from "@/uhm/types/projects"; +import StickyHeader from "@/components/ui/StickyHeader"; export type ProjectSortColumn = "created_at" | "updated_at" | "title"; @@ -23,16 +29,26 @@ export default function ProjectsPage() { const [projects, setProjects] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isSubmitting, setIsSubmitting] = useState(false); - const [isExportingProjectId, setIsExportingProjectId] = useState(null); + const [isExportingProjectId, setIsExportingProjectId] = useState< + string | null + >(null); const [sortBy, setSortBy] = useState("updated_at"); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc"); const { isOpen, openModal, closeModal } = useModal(); - const [formData, setFormData] = useState({ title: "", description: "", project_status: "PRIVATE" }); + const [formData, setFormData] = useState({ + title: "", + description: "", + status: "PRIVATE", + }); const importJsonInputRef = useRef(null); - const [importSnapshot, setImportSnapshot] = useState(null); - const [importSnapshotName, setImportSnapshotName] = useState(null); + const [importSnapshot, setImportSnapshot] = useState( + null, + ); + const [importSnapshotName, setImportSnapshotName] = useState( + null, + ); const fetchProjects = async () => { try { @@ -51,9 +67,17 @@ export default function ProjectsPage() { fetchProjects(); }, []); - const handleChange = (e: React.ChangeEvent) => { + // 1. Cập nhật lại hàm handleChange để đảm bảo bắt giá trị chính xác từ thẻ select + const handleChange = ( + e: React.ChangeEvent< + HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement + >, + ) => { const { name, value } = e.target; - setFormData((prev) => ({ ...prev, [name]: value })); + setFormData((prev) => ({ + ...prev, + [name]: value, // Đảm bảo value từ select được gán chính xác vào key status + })); }; const handleCreateProject = async (e: React.FormEvent) => { @@ -62,17 +86,38 @@ export default function ProjectsPage() { toast.warning("Vui lòng nhập tên dự án!"); return; } + try { setIsSubmitting(true); + // Bước 1: Luôn tạo project trước const created = await apiCreateProject(formData); const projectId = created?.data?.id; - toast.success("Tạo dự án mới thành công!"); + + if (!projectId) { + toast.error("Tạo dự án thất bại: không nhận được ID dự án."); + setIsSubmitting(false); // Dừng sớm nếu không có ID + return; + } + + // Bước 2: Nếu có snapshot, tạo commit ban đầu từ JSON + if (importSnapshot) { + await apiCreateProjectCommit(projectId, { + edit_summary: `Init project from ${importSnapshotName || "JSON"}`, + snapshot_json: importSnapshot as any, + } as any); + toast.success("Tạo dự án từ JSON thành công!"); + } else { + toast.success("Tạo dự án mới thành công!"); + } + + // Bước 3: Dọn dẹp state và chuyển hướng closeModal(); - setFormData({ title: "", description: "", project_status: "PRIVATE" }); + setFormData({ title: "", description: "", status: "PRIVATE" }); setImportSnapshot(null); setImportSnapshotName(null); - fetchProjects(); - if (projectId) router.push(`/editor/${projectId}`); + if (importJsonInputRef.current) importJsonInputRef.current.value = ""; + fetchProjects(); + router.push(`/editor/${projectId}`); } catch (error) { console.error("Lỗi tạo dự án:", error); toast.error("Có lỗi xảy ra khi tạo dự án."); @@ -81,6 +126,48 @@ export default function ProjectsPage() { } }; + const handleExportHeadSnapshot = async (project: Project) => { + const projectId = String(project.id || "").trim(); + if (!projectId) return; + const headCommitId = project.latest_commit_id + ? String(project.latest_commit_id) + : ""; + if (!headCommitId) { + toast.warning("Dự án chưa có head commit để export."); + return; + } + setIsExportingProjectId(projectId); + try { + const res: any = await apiGetProjectCommits(projectId); + const rawList = res?.data?.items ?? res?.data ?? res?.items ?? []; + const commits = Array.isArray(rawList) ? rawList : []; + const head = + commits.find((c: any) => String(c?.id || "") === headCommitId) || null; + const snapshot = head?.snapshot_json ?? null; + if (!snapshot) { + toast.error("Không tìm thấy snapshot_json của head commit."); + return; + } + const blob = new Blob([JSON.stringify(snapshot, null, 2)], { + type: "application/json;charset=utf-8", + }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `project-${projectId}-head-${headCommitId}.json`; + document.body.appendChild(a); + a.click(); + a.remove(); + URL.revokeObjectURL(url); + toast.success("Đã export JSON snapshot."); + } catch (err) { + console.error("Export snapshot failed", err); + toast.error("Export thất bại."); + } finally { + setIsExportingProjectId(null); + } + }; + const handlePickImportJson = () => { importJsonInputRef.current?.click(); }; @@ -97,88 +184,13 @@ export default function ProjectsPage() { } setImportSnapshot(normalized); setImportSnapshotName(file.name); - toast.success("Đã nạp JSON snapshot. Bấm 'Tạo với JSON' để khởi tạo dự án."); + toast.success("Đã nạp JSON snapshot. Bấm 'Tạo dự án' để hoàn tất."); } catch (err) { console.error("Import JSON failed", err); toast.error("Không đọc được file JSON."); } }; - const handleCreateProjectWithJson = async () => { - if (!formData.title.trim()) { - toast.warning("Vui lòng nhập tên dự án!"); - return; - } - if (!importSnapshot) { - toast.warning("Chưa chọn JSON snapshot."); - handlePickImportJson(); - return; - } - try { - setIsSubmitting(true); - const created = await apiCreateProject(formData); - const projectId = created?.data?.id; - if (!projectId) { - toast.error("Tạo dự án thất bại: thiếu project id."); - return; - } - await apiCreateProjectCommit(projectId, { - edit_summary: "Init project from JSON", - snapshot_json: importSnapshot as any, - } as any); - toast.success("Tạo dự án (kèm JSON) thành công!"); - closeModal(); - setFormData({ title: "", description: "", project_status: "PRIVATE" }); - setImportSnapshot(null); - setImportSnapshotName(null); - if (importJsonInputRef.current) importJsonInputRef.current.value = ""; - fetchProjects(); - router.push(`/editor/${projectId}`); - } catch (error) { - console.error("Lỗi tạo dự án với JSON:", error); - toast.error("Có lỗi xảy ra khi tạo dự án với JSON."); - } finally { - setIsSubmitting(false); - } - }; - - const handleExportHeadSnapshot = async (project: Project) => { - const projectId = String(project.id || "").trim(); - if (!projectId) return; - const headCommitId = project.latest_commit_id ? String(project.latest_commit_id) : ""; - if (!headCommitId) { - toast.warning("Dự án chưa có head commit để export."); - return; - } - setIsExportingProjectId(projectId); - try { - const res: any = await apiGetProjectCommits(projectId); - const rawList = res?.data?.items ?? res?.data ?? res?.items ?? []; - const commits = Array.isArray(rawList) ? rawList : []; - const head = commits.find((c: any) => String(c?.id || "") === headCommitId) || null; - const snapshot = head?.snapshot_json ?? null; - if (!snapshot) { - toast.error("Không tìm thấy snapshot_json của head commit."); - return; - } - const blob = new Blob([JSON.stringify(snapshot, null, 2)], { type: "application/json;charset=utf-8" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `project-${projectId}-head-${headCommitId}.json`; - document.body.appendChild(a); - a.click(); - a.remove(); - URL.revokeObjectURL(url); - toast.success("Đã export JSON snapshot."); - } catch (err) { - console.error("Export snapshot failed", err); - toast.error("Export thất bại."); - } finally { - setIsExportingProjectId(null); - } - }; - const handleSort = (column: ProjectSortColumn) => { if (sortBy === column) { setSortOrder(sortOrder === "asc" ? "desc" : "asc"); @@ -191,7 +203,7 @@ export default function ProjectsPage() { const sortedProjects = [...projects].sort((a: any, b: any) => { let valA = a[sortBy]; let valB = b[sortBy]; - + if (!valA) valA = ""; if (!valB) valB = ""; @@ -216,23 +228,47 @@ export default function ProjectsPage() { const getStatusBadge = (status: string) => { switch (status) { case "PUBLIC": - return PUBLIC; + return ( + + PUBLIC + + ); case "PRIVATE": - return PRIVATE; + return ( + + PRIVATE + + ); case "ARCHIVE": - return ARCHIVE; + return ( + + ARCHIVE + + ); default: - return {status}; + return ( + + {status} + + ); } }; - const SortButton = ({ column, label }: { column: ProjectSortColumn; label: string }) => { + const SortButton = ({ + column, + label, + }: { + column: ProjectSortColumn; + label: string; + }) => { const isActive = sortBy === column; return ( } @@ -273,12 +316,18 @@ export default function ProjectsPage() {
-
Trạng thái
-
Thành viên
+
+ Trạng thái +
+
+ Thành viên +
-
Thao tác
+
+ Thao tác +
@@ -290,49 +339,83 @@ export default function ProjectsPage() {

router.push(`/user/projects/${project.id}`)} + onClick={() => + router.push(`/user/projects/${project.id}`) + } className="font-semibold text-blue-600 dark:text-[#58a6ff] truncate cursor-pointer hover:underline" > {project.title}

-
{project.user?.avatar_url ? ( - avatar + avatar ) : (
- {project.user?.display_name?.charAt(0)?.toUpperCase() || "U"} + {project.user?.display_name + ?.charAt(0) + ?.toUpperCase() || "U"}
)} - {project.user?.display_name || "Unknown"} + + {project.user?.display_name || "Unknown"} +
- +
{getStatusBadge(project.project_status)}
- +
{project.members && project.members.length > 0 ? ( <> - {project.members.slice(0, 4).map((m: any, index: number) => - m.avatar_url ? ( - {m.display_name} - ) : ( -
- {m.display_name?.charAt(0)?.toUpperCase() || "U"} -
- ) - )} + {project.members + .slice(0, 4) + .map((m: any, index: number) => + m.avatar_url ? ( + {m.display_name} + ) : ( +
+ + {m.display_name + ?.charAt(0) + ?.toUpperCase() || "U"} + +
+ ), + )} {project.members.length > 4 && ( -
- +{project.members.length - 4} +
+ + +{project.members.length - 4} +
)} @@ -346,16 +429,29 @@ export default function ProjectsPage() { {formatDate(project.updated_at)}
-
+
@@ -368,31 +464,51 @@ export default function ProjectsPage() { size="sm" variant="outline" className="!p-0 w-9 h-9 flex items-center justify-center" - disabled={isExportingProjectId === String(project.id)} + disabled={ + isExportingProjectId === String(project.id) + } onClick={() => handleExportHeadSnapshot(project)} > - - + + Export JSON
-
))}
- ) : !isLoading && ( -
-

Bạn chưa có dự án nào.

- -
+ ) : ( + !isLoading && ( +
+

+ Bạn chưa có dự án nào. +

+ +
+ ) )}
@@ -400,10 +516,14 @@ export default function ProjectsPage() {
-

Tạo dự án mới

+

+ Tạo dự án mới +

- +
@@ -441,7 +576,12 @@ export default function ProjectsPage() {
-
@@ -453,22 +593,31 @@ export default function ProjectsPage() { type="file" accept="application/json" className="hidden" - onChange={(e) => handleImportJsonFile(e.target.files?.[0] || null)} + onChange={(e) => + handleImportJsonFile(e.target.files?.[0] || null) + } />
- -
diff --git a/src/app/user/role-upgrade/page.tsx b/src/app/user/role-upgrade/page.tsx index bc589ce..63cd322 100644 --- a/src/app/user/role-upgrade/page.tsx +++ b/src/app/user/role-upgrade/page.tsx @@ -19,6 +19,7 @@ import { toast } from "sonner"; import { newId } from "@/uhm/lib/utils/id"; import Swal from "sweetalert2"; import { PresignedUrlResponse } from "@/interface/media"; +import StickyHeader from "@/components/ui/StickyHeader"; type PendingFile = { id: string; @@ -197,42 +198,41 @@ export default function RoleUpgrade() { }; return ( -
- - -
-
- - +
+ +
+
+
+ + +
+ + {showPreview ? "Preview Mode" : "Edit Mode"} +
- - {showPreview ? "Preview Mode" : "Edit Mode"} - -
- -
-
- {!showPreview ? ( - - ) : ( -
- {content ? ( -