"use client"; import React, { useEffect, useMemo, useRef, useState } from "react"; import Image from "next/image"; import { useRouter } from "next/navigation"; import ComponentCard from "@/components/common/ComponentCard"; import { toast } from "sonner"; import { useModal } from "@/hooks/useModal"; import { Modal } from "@/components/ui/modal"; import Button from "@/components/ui/button/Button"; import Label from "@/components/form/Label"; import Badge from "@/components/ui/badge/Badge"; import { CreateProjectPayload, Project, ProjectMember } from "@/interface/project"; import { apiCreateProject, apiCreateProjectCommit, apiGetProjectCommits, getCurrentProject, } from "@/service/projectService"; import { normalizeEditorSnapshot, toApiEditorSnapshot } from "@/uhm/lib/editor/snapshot/editorSnapshot"; import type { EditorSnapshot, ProjectCommit } from "@/uhm/types/projects"; import StickyHeader from "@/components/ui/StickyHeader"; export type ProjectSortColumn = "created_at" | "updated_at" | "title"; function isRecord(value: unknown): value is Record { return !!value && typeof value === "object" && !Array.isArray(value); } function extractProjectCommitList(value: unknown): ProjectCommit[] { let rows: unknown[] = []; if (Array.isArray(value)) { rows = value; } else if (isRecord(value)) { if (Array.isArray(value.items)) { rows = value.items; } else if (Array.isArray(value.data)) { rows = value.data; } else if (isRecord(value.data) && Array.isArray(value.data.items)) { rows = value.data.items; } } return rows.filter((row): row is ProjectCommit => isRecord(row) && typeof row.id === "string"); } export default function ProjectsPage() { const router = useRouter(); const [projects, setProjects] = useState([]); const [isLoading, setIsLoading] = useState(true); const [isSubmitting, setIsSubmitting] = useState(false); 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: "", status: "PRIVATE", }); const importJsonInputRef = useRef(null); const [importSnapshot, setImportSnapshot] = useState( null, ); const [importSnapshotName, setImportSnapshotName] = useState( null, ); const fetchProjects = async () => { try { setIsLoading(true); const res = await getCurrentProject(); setProjects(res?.data?.items || res?.data || []); } catch (error) { console.error("Lỗi khi tải danh sách dự án:", error); toast.error("Không thể tải danh sách dự án. Vui lòng thử lại!"); } finally { setIsLoading(false); } }; useEffect(() => { fetchProjects(); }, []); // 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, // Đảm bảo value từ select được gán chính xác vào key status })); }; const handleCreateProject = async (e: React.FormEvent) => { e.preventDefault(); if (!formData.title.trim()) { 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; 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) { const snapshot = toApiEditorSnapshot(importSnapshot); await apiCreateProjectCommit(projectId, { edit_summary: `Init project from ${importSnapshotName || "JSON"}`, snapshot_json: snapshot, }); 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: "", 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:", error); toast.error("Có lỗi xảy ra khi tạo dự án."); } 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 = await apiGetProjectCommits(projectId); const commits = extractProjectCommitList(res); const head = commits.find((c) => 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(); }; const handleImportJsonFile = async (file: File | null) => { if (!file) return; try { const text = await file.text(); const raw = JSON.parse(text) as unknown; const normalized = normalizeEditorSnapshot(raw); if (!normalized) { toast.error("JSON snapshot không hợp lệ."); return; } setImportSnapshot(normalized); setImportSnapshotName(file.name); 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 handleSort = (column: ProjectSortColumn) => { if (sortBy === column) { setSortOrder(sortOrder === "asc" ? "desc" : "asc"); } else { setSortBy(column); setSortOrder("desc"); } }; const sortedProjects = [...projects].sort((a, b) => { const valA = String(a[sortBy] || ""); const valB = String(b[sortBy] || ""); if (valA < valB) return sortOrder === "asc" ? -1 : 1; if (valA > valB) return sortOrder === "asc" ? 1 : -1; return 0; }); const formatDate = (dateString: string | null | undefined) => { if (!dateString) return "-"; const date = new Date(dateString); if (isNaN(date.getTime())) return "-"; return date.toLocaleDateString("vi-VN", { day: "2-digit", month: "short", year: "numeric", hour: "2-digit", minute: "2-digit", }); }; const getStatusBadge = (status: string) => { switch (status) { case "PUBLIC": return ( PUBLIC ); case "PRIVATE": return ( PRIVATE ); case "ARCHIVE": return ( ARCHIVE ); default: return ( {status} ); } }; const SortButton = ({ column, label, }: { column: ProjectSortColumn; label: string; }) => { const isActive = sortBy === column; return ( ); }; const importLabel = useMemo(() => { if (!importSnapshotName) return "Chưa chọn JSON snapshot"; return `JSON: ${importSnapshotName}`; }, [importSnapshotName]); // const path =[ // {name: "Thư viện", href:"/user/library/"} // ] return (
{/* */}
+ Tạo dự án mới } >
{isLoading && (
)} {!isLoading && sortedProjects.length > 0 ? (
Trạng thái
Thành viên
Thao tác
{sortedProjects.map((project) => (

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 ) : (
{project.user?.display_name ?.charAt(0) ?.toUpperCase() || "U"}
)} {project.user?.display_name || "Unknown"}
{getStatusBadge(project.project_status)}
{project.members && project.members.length > 0 ? ( <> {project.members .slice(0, 4) .map((m: ProjectMember, index: number) => m.avatar_url ? ( {m.display_name} ) : (
{m.display_name ?.charAt(0) ?.toUpperCase() || "U"}
), )} {project.members.length > 4 && (
+{project.members.length - 4}
)} ) : ( )}
{formatDate(project.updated_at)}
Editor
Export JSON
))}
) : ( !isLoading && (

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

) )}

Tạo dự án mới

{importLabel}
handleImportJsonFile(e.target.files?.[0] || null) } />
); }