"use client"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useRef, useState, type ReactNode, } from "react"; import { ProjectActionsDropdown, type ProjectActionKey, } from "@/components/ProjectActionsDropdown"; import { CanvasTemplatePreviewModal, type CanvasTemplatePreviewData, } from "@/components/CanvasTemplatePreviewModal"; import { ArrowIcon, CloseIcon, DefaultAvatar, LeftOutlinedIcon, SearchIcon, StartCreation, } from "@/components/icons"; import { authFetch } from "@/utils/authFetch"; import { useI18n, type TranslationKey } from "@/i18n"; import { createCanvasWorkflow, deleteCanvasWorkflow, getCanvasWorkflowDetail, listCanvasWorkflows, type CanvasWorkflowListItem, type CanvasWorkflowPageInfo, updateCanvasWorkflow, uploadCanvasWorkflowCover, } from "@/lib/canvasWorkflowApi"; import { Form, Input, Modal, Tooltip, message } from "antd"; type Translate = (key: TranslationKey, values?: Record) => string; type ProjectData = { id: number; title: string; date: string; image: string; uploadProgress?: number; }; type ShowData = { id: number; title: string; subtitle: string; image: string; avatar: string; vip?: boolean; data: string; }; type TemplateCategoryData = { id: number | null; name: string; }; type CanvasTemplateCategoryItem = { id: number; name: string; sort: number; deleted: boolean; }; type CanvasTemplateItem = { id: number; categoryId: number; type: number; coverUrl: string; properties: string; name: string; desp: string; deleted: boolean; categoryName: string; userInfo: { avatar: string; code: string; id: number; name: string; } }; type ListResponse = { data?: { pageInfo?: CanvasWorkflowPageInfo; list?: T[]; }; status?: string; }; type HomeProjectsContentProps = { heroSection: ReactNode; }; const DEFAULT_PROJECT_PAGE_INFO: CanvasWorkflowPageInfo = { page: 1, pageSize: 4, pageCount: 1, total: 0, }; const ALL_PROJECTS_LOAD_MORE_OFFSET = 260; const SHOWS_PAGE_SIZE = 20; const SHOWS_LOAD_MORE_OFFSET = 260; function formatProjectTime(value?: string): string { if (!value) return "-"; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return date.toLocaleDateString(); } function mapWorkflowToProject(item: CanvasWorkflowListItem): ProjectData { return { id: item.id, title: item.title?.trim() || item.code?.trim() || `#${item.id}`, date: formatProjectTime(item.updateTime || item.createTime), image: item.coverUrl || "", }; } function buildEmptyWorkflowProperties(title: string): string { return JSON.stringify({ name: title, nodes: [], edges: [], edgeStyle: "curved", }); } function ProjectCard({ id, title, date, image, uploadProgress, t, onOpen, onAction, }: ProjectData & { t: Translate; onOpen: () => void; onAction: (id: number, action: ProjectActionKey) => void; }) { const isUploadingCover = typeof uploadProgress === "number"; return (

{date}

onAction(id, action)} />
); } function ShowCard({ id, title, subtitle, image, avatar, vip, data, onOpen }: ShowData & { onOpen: (data: ShowData) => void }) { return (
onOpen({ id, title, subtitle, image, avatar, vip, data })}>
{avatar ? :
}

{title}

{/* {vip ? ( VIP ) : null} */}

{subtitle || '-'}

); } function ProjectSectionGrid({ projects, isLoadingProjects, isCreating, t, showOverflowCue = false, pageInfo, onPrevPage, onNextPage, onStartCreation, onProjectAction, }: { projects: ProjectData[]; isLoadingProjects: boolean; isCreating: boolean; t: Translate; showOverflowCue?: boolean; pageInfo?: CanvasWorkflowPageInfo; onPrevPage?: () => void; onNextPage?: () => void; onStartCreation: () => void; onProjectAction: (id: number, action: ProjectActionKey) => void; }) { const router = useRouter(); const canGoPrev = !!pageInfo && pageInfo.page > 1; const canGoNext = !!pageInfo && pageInfo.page < pageInfo.pageCount; const uniqueProjects = projects.filter( (project, index, list) => list.findIndex((item) => item.id === project.id) === index ); const [firstProject, ...remainingProjects] = uniqueProjects; return (
{isLoadingProjects && projects.length === 0 ? (
{t("home.loading")}
) : null} {firstProject ? (
{pageInfo ? ( <>
) : null} router.push(`/canvas?workflowId=${firstProject.id}`)} onAction={onProjectAction} />
) : null} {remainingProjects.map((project) => ( router.push(`/canvas?workflowId=${project.id}`)} onAction={onProjectAction} /> ))} {showOverflowCue && pageInfo && ( <>
)}
); } function ProjectActionModals({ t, isActionLoading, renameOpen, renameTitle, onRenameTitleChange, onCancelRename, onConfirmRename, deleteTitle, onCancelDelete, onConfirmDelete, }: { t: Translate; isActionLoading: boolean; renameOpen: boolean; renameTitle: string; onRenameTitleChange: (value: string) => void; onCancelRename: () => void; onConfirmRename: () => void; deleteTitle: string | null; onCancelDelete: () => void; onConfirmDelete: () => void; }) { const modalClassName = "[&_.ant-modal-content]:!rounded-[9px] [&_.ant-modal-content]:!p-[30px] [&_.ant-modal-header]:!mb-0 [&_.ant-modal-body]:!pt-2 [&_.ant-modal-footer]:!mt-6 [&_.ant-modal-footer]:!flex [&_.ant-modal-footer]:!justify-end [&_.ant-modal-footer]:!gap-2.5 [&_.ant-modal-footer_.ant-btn+_.ant-btn]:!ml-0 [&_.ant-modal-close]:!right-2.5 [&_.ant-modal-close]:!top-2.5 [&_.ant-modal-close]:!h-5 [&_.ant-modal-close]:!w-5 [&_.ant-modal-close]:!text-[#999] hover:[&_.ant-modal-close]:!text-[#666]"; const modalTitleClassName = "text-[24px] font-medium leading-[22px] text-[#333]"; const cancelButtonClassName = "!h-[37px] !w-[109px] !rounded-[24px] !border-0 !bg-[#f0f0f0] !p-0 !text-[16px] !font-medium !text-[#666] !shadow-none hover:!bg-[#e6e6e6] hover:!text-[#666]"; const confirmButtonClassName = "!h-[37px] !w-[109px] !rounded-[24px] !border-0 !bg-[#5c3bc7] !p-0 !text-[16px] !font-medium !text-white !shadow-none hover:!bg-[#6b4bd4]"; const deleteConfirmButtonClassName = "!h-[37px] !w-[109px] !rounded-[24px] !border-0 !bg-[#b7301e] !p-0 !text-[16px] !font-medium !text-white !shadow-none hover:!bg-[#a62b1b]"; return ( <> {t("home.renameProject")}} open={renameOpen} closeIcon={} centered width={491} className={modalClassName} confirmLoading={isActionLoading} okText={t("home.confirm")} cancelText={t("home.cancelEsc")} okButtonProps={{ className: confirmButtonClassName, disabled: !renameTitle.trim(), }} cancelButtonProps={{ className: cancelButtonClassName }} onOk={onConfirmRename} onCancel={onCancelRename} destroyOnHidden >
{t("home.projectName")}} className="!mb-0" > onRenameTitleChange(event.target.value)} placeholder={t("home.projectNamePlaceholder")} onPressEnter={onConfirmRename} autoFocus className="!h-[37px] !rounded-[9px] !border-[#e6e6e6] !text-[14px] !shadow-none hover:!border-[#d6ceef] focus:!border-[#5c3bc7]" />
{t("home.confirmDelete")}} open={deleteTitle !== null} closeIcon={} centered width={491} className={modalClassName} confirmLoading={isActionLoading} okText={t("home.confirm")} okButtonProps={{ danger: true, className: deleteConfirmButtonClassName, }} cancelText={t("home.cancelEsc")} cancelButtonProps={{ className: cancelButtonClassName }} onOk={onConfirmDelete} onCancel={onCancelDelete} destroyOnHidden >

{t("home.deleteCannotRecover")}

); } function useProjectSectionState(pageSize: number, t: Translate, initialPage = 1) { const router = useRouter(); const coverInputRef = useRef(null); const coverProjectIdRef = useRef(null); const coverProgressTimerRef = useRef(null); const [projects, setProjects] = useState([]); const [page, setPage] = useState(initialPage); const [pageInfo, setPageInfo] = useState({ ...DEFAULT_PROJECT_PAGE_INFO, page: initialPage, pageSize, }); const [isLoadingProjects, setIsLoadingProjects] = useState(false); const [isCreating, setIsCreating] = useState(false); const [actionProjectId, setActionProjectId] = useState(null); const [renamePayload, setRenamePayload] = useState<{ id: number; title: string; desp: string; properties: string; coverUrl: string; } | null>(null); const [renameTitle, setRenameTitle] = useState(""); const [coverProjectId, setCoverProjectId] = useState(null); const [deletePayload, setDeletePayload] = useState<{ id: number; title: string; } | null>(null); const stopCoverProgressTimer = useCallback(() => { if (coverProgressTimerRef.current) { window.clearInterval(coverProgressTimerRef.current); coverProgressTimerRef.current = null; } }, []); const setProjectCoverProgress = useCallback((id: number, progress?: number) => { setProjects((currentProjects) => currentProjects.map((project) => project.id === id ? { ...project, uploadProgress: progress } : project ) ); }, []); const startFakeCoverProgress = useCallback((id: number) => { stopCoverProgressTimer(); setProjectCoverProgress(id, 1); coverProgressTimerRef.current = window.setInterval(() => { setProjects((currentProjects) => currentProjects.map((project) => { if (project.id !== id || typeof project.uploadProgress !== "number") { return project; } const nextProgress = Math.min( 90, project.uploadProgress + Math.max(1, Math.round((90 - project.uploadProgress) * 0.18)) ); return { ...project, uploadProgress: nextProgress }; }) ); }, 300); }, [setProjectCoverProgress, stopCoverProgressTimer]); const finishCoverProgress = useCallback((id: number, coverUrl: string) => { stopCoverProgressTimer(); setProjects((currentProjects) => currentProjects.map((project) => project.id === id ? { ...project, image: coverUrl, uploadProgress: 100 } : project ) ); window.setTimeout(() => setProjectCoverProgress(id, undefined), 450); }, [setProjectCoverProgress, stopCoverProgressTimer]); const loadProjects = useCallback(async (nextPage: number, options?: { append?: boolean }) => { setIsLoadingProjects(true); try { const data = await listCanvasWorkflows({ page: nextPage, pageSize }); const nextProjects = data.list .filter((item) => !item.deleted) .map((item) => mapWorkflowToProject(item)); setProjects((currentProjects) => options?.append ? [...currentProjects, ...nextProjects] : nextProjects ); setPageInfo(data.pageInfo); } catch { if (!options?.append) { setProjects([]); } setPageInfo({ ...DEFAULT_PROJECT_PAGE_INFO, page: nextPage, pageSize, }); } finally { setIsLoadingProjects(false); } }, [pageSize]); useEffect(() => { void loadProjects(page); return undefined; }, [loadProjects, page]); useEffect(() => { return () => { stopCoverProgressTimer(); }; }, [stopCoverProgressTimer]); const handleStartCreation = async () => { if (isCreating) { message.warning(t("home.createInProgress")); return; } setIsCreating(true); const title = t("home.projectFallback"); try { const result = await createCanvasWorkflow({ title, desp: "", properties: buildEmptyWorkflowProperties(title), }); const workflowId = (result.data as any)?.id; router.push(workflowId ? `/canvas?workflowId=${workflowId}` : "/canvas"); message.success(t("home.createSuccess")); } catch (error) { message.error(error instanceof Error ? error.message : t("home.createFailed")); setIsCreating(false); } }; const buildFullUpdatePayload = async (id: number) => { const detail = await getCanvasWorkflowDetail(id); return { id, title: detail.title ?? "", desp: detail.desp ?? "", properties: detail.properties ?? "", coverUrl: detail.coverUrl ?? "", }; }; const handleProjectAction = async (id: number, action: ProjectActionKey) => { if (actionProjectId !== null) return; setActionProjectId(id); try { if (action === "rename") { const payload = await buildFullUpdatePayload(id); setRenamePayload(payload); setRenameTitle(payload.title); return; } if (action === "change-cover") { coverProjectIdRef.current = id; setCoverProjectId(id); coverInputRef.current?.click(); return; } if (action === "duplicate") { const detail = await getCanvasWorkflowDetail(id); const title = detail.title ? `${detail.title}${t("home.copySuffix")}` : t("home.projectCopyFallback"); await createCanvasWorkflow({ title, desp: detail.desp ?? "", properties: detail.properties ?? "", coverUrl: detail.coverUrl ?? "", }); message.success(t("home.copyCreated")); await loadProjects(page); // const workflowId = (result.data as any)?.id; // if (workflowId) { // router.push(`/canvas?workflowId=${workflowId}`); // } return; } if (action === "delete") { const detail = await getCanvasWorkflowDetail(id); setDeletePayload({ id, title: detail.title ?? detail.code ?? `#${id}`, }); } } catch (error) { message.error(error instanceof Error ? error.message : t("home.actionFailed")); } finally { setActionProjectId(null); } }; const closeRenameModal = () => { setRenamePayload(null); setRenameTitle(""); }; const confirmRenameProject = async () => { if (!renamePayload || actionProjectId !== null) return; setActionProjectId(renamePayload.id); try { await updateCanvasWorkflow({ ...renamePayload, title: renameTitle, }); message.success(t("home.renameSuccess")); setProjects((currentProjects) => currentProjects.map((project) => project.id === renamePayload.id ? { ...project, title: renameTitle.trim() || project.title } : project ) ); closeRenameModal(); } catch (error) { message.error(error instanceof Error ? error.message : t("home.renameFailed")); } finally { setActionProjectId(null); } }; const closeDeleteModal = () => { setDeletePayload(null); }; const closeCoverModal = () => { coverProjectIdRef.current = null; setCoverProjectId(null); }; const handleCoverFileChange = async (file: File | undefined) => { const selectedCoverProjectId = coverProjectIdRef.current ?? coverProjectId; if (!file || selectedCoverProjectId === null || actionProjectId !== null) return; if (!file.type.startsWith("image/")) { message.error(t("home.selectImageFile")); closeCoverModal(); return; } setActionProjectId(selectedCoverProjectId); startFakeCoverProgress(selectedCoverProjectId); try { const coverPayload = await buildFullUpdatePayload(selectedCoverProjectId); const uploadedCoverUrl = await uploadCanvasWorkflowCover(file); await updateCanvasWorkflow({ ...coverPayload, coverUrl: uploadedCoverUrl, }); message.success(t("home.coverUpdated")); finishCoverProgress(selectedCoverProjectId, uploadedCoverUrl); closeCoverModal(); } catch (error) { stopCoverProgressTimer(); setProjectCoverProgress(selectedCoverProjectId, undefined); message.error(error instanceof Error ? error.message : t("home.coverUpdateFailed")); } finally { setActionProjectId(null); if (coverInputRef.current) { coverInputRef.current.value = ""; } } }; const confirmDeleteProject = async () => { if (!deletePayload || actionProjectId !== null) return; setActionProjectId(deletePayload.id); try { await deleteCanvasWorkflow(deletePayload.id); message.success(t("home.deleteSuccess")); closeDeleteModal(); await loadProjects(page); } catch (error) { message.error(error instanceof Error ? error.message : t("home.deleteFailed")); } finally { setActionProjectId(null); } }; return { projects, pageInfo, isLoadingProjects, isCreating, isActionLoading: actionProjectId !== null, renamePayload, renameTitle, setRenameTitle, closeRenameModal, confirmRenameProject, coverInputRef, closeCoverModal, handleCoverFileChange, deletePayload, closeDeleteModal, confirmDeleteProject, handleStartCreation, handleProjectAction, goPrevPage: () => setPage((current) => Math.max(1, current - 1)), goNextPage: () => setPage((current) => Math.min(pageInfo.pageCount, current + 1)), }; } function useAllProjectsState(t: Translate) { const pageSize = 20; const router = useRouter(); const coverInputRef = useRef(null); const coverProjectIdRef = useRef(null); const coverProgressTimerRef = useRef(null); const loadingMorePageRef = useRef(null); const [projects, setProjects] = useState([]); const [page, setPage] = useState(1); const [pageInfo, setPageInfo] = useState({ ...DEFAULT_PROJECT_PAGE_INFO, page: 1, pageSize, }); const [isLoadingProjects, setIsLoadingProjects] = useState(false); const [isCreating, setIsCreating] = useState(false); const [actionProjectId, setActionProjectId] = useState(null); const [renamePayload, setRenamePayload] = useState<{ id: number; title: string; desp: string; properties: string; coverUrl: string; } | null>(null); const [renameTitle, setRenameTitle] = useState(""); const [coverProjectId, setCoverProjectId] = useState(null); const [deletePayload, setDeletePayload] = useState<{ id: number; title: string; } | null>(null); const stopCoverProgressTimer = useCallback(() => { if (coverProgressTimerRef.current) { window.clearInterval(coverProgressTimerRef.current); coverProgressTimerRef.current = null; } }, []); const setProjectCoverProgress = useCallback((id: number, progress?: number) => { setProjects((currentProjects) => currentProjects.map((project) => project.id === id ? { ...project, uploadProgress: progress } : project ) ); }, []); const startFakeCoverProgress = useCallback((id: number) => { stopCoverProgressTimer(); setProjectCoverProgress(id, 1); coverProgressTimerRef.current = window.setInterval(() => { setProjects((currentProjects) => currentProjects.map((project) => { if (project.id !== id || typeof project.uploadProgress !== "number") return project; const nextProgress = Math.min( 90, project.uploadProgress + Math.max(1, Math.round((90 - project.uploadProgress) * 0.18)) ); return { ...project, uploadProgress: nextProgress }; }) ); }, 300); }, [setProjectCoverProgress, stopCoverProgressTimer]); const finishCoverProgress = useCallback((id: number, coverUrl: string) => { stopCoverProgressTimer(); setProjects((currentProjects) => currentProjects.map((project) => project.id === id ? { ...project, image: coverUrl, uploadProgress: 100 } : project ) ); window.setTimeout(() => setProjectCoverProgress(id, undefined), 450); }, [setProjectCoverProgress, stopCoverProgressTimer]); const loadProjectsPage = useCallback(async ( nextPage: number, options?: { append?: boolean } ) => { setIsLoadingProjects(true); try { const data = await listCanvasWorkflows({ page: nextPage, pageSize }); const nextProjects = data.list .filter((item) => !item.deleted) .map((item) => mapWorkflowToProject(item)); setProjects((currentProjects) => { if (!options?.append) return nextProjects; const existingIds = new Set(currentProjects.map((project) => project.id)); const uniqueNextProjects = nextProjects.filter((project) => !existingIds.has(project.id)); return [...currentProjects, ...uniqueNextProjects]; }); setPageInfo(data.pageInfo); setPage(nextPage); } catch { if (!options?.append) { setProjects([]); } setPageInfo({ ...DEFAULT_PROJECT_PAGE_INFO, page: nextPage, pageSize, }); } finally { setIsLoadingProjects(false); } }, []); useEffect(() => { void loadProjectsPage(1); }, [loadProjectsPage]); useEffect(() => { return () => { stopCoverProgressTimer(); }; }, [stopCoverProgressTimer]); const handleStartCreation = async () => { if (isCreating) { message.warning(t("home.createInProgress")); return; } setIsCreating(true); const title = t("home.projectFallback"); try { const result = await createCanvasWorkflow({ title, desp: "", properties: buildEmptyWorkflowProperties(title), }); const workflowId = (result.data as any)?.id; router.push(workflowId ? `/canvas?workflowId=${workflowId}` : "/canvas"); message.success(t("home.createSuccess")); } catch (error) { message.error(error instanceof Error ? error.message : t("home.createFailed")); setIsCreating(false); } }; const buildFullUpdatePayload = async (id: number) => { const detail = await getCanvasWorkflowDetail(id); return { id, title: detail.title ?? "", desp: detail.desp ?? "", properties: detail.properties ?? "", coverUrl: detail.coverUrl ?? "", }; }; const handleProjectAction = async (id: number, action: ProjectActionKey) => { if (actionProjectId !== null) return; setActionProjectId(id); try { if (action === "rename") { const payload = await buildFullUpdatePayload(id); setRenamePayload(payload); setRenameTitle(payload.title); return; } if (action === "change-cover") { coverProjectIdRef.current = id; setCoverProjectId(id); coverInputRef.current?.click(); return; } if (action === "duplicate") { const detail = await getCanvasWorkflowDetail(id); const title = detail.title ? `${detail.title}${t("home.copySuffix")}` : t("home.projectCopyFallback"); await createCanvasWorkflow({ title, desp: detail.desp ?? "", properties: detail.properties ?? "", coverUrl: detail.coverUrl ?? "", }); message.success(t("home.copyCreated")); await loadProjectsPage(1); return; } if (action === "delete") { const detail = await getCanvasWorkflowDetail(id); setDeletePayload({ id, title: detail.title ?? detail.code ?? `#${id}`, }); } } catch (error) { message.error(error instanceof Error ? error.message : t("home.actionFailed")); } finally { setActionProjectId(null); } }; const closeRenameModal = () => { setRenamePayload(null); setRenameTitle(""); }; const confirmRenameProject = async () => { if (!renamePayload || actionProjectId !== null) return; setActionProjectId(renamePayload.id); try { await updateCanvasWorkflow({ ...renamePayload, title: renameTitle, }); message.success(t("home.renameSuccess")); setProjects((currentProjects) => currentProjects.map((project) => project.id === renamePayload.id ? { ...project, title: renameTitle.trim() || project.title } : project ) ); closeRenameModal(); } catch (error) { message.error(error instanceof Error ? error.message : t("home.renameFailed")); } finally { setActionProjectId(null); } }; const closeDeleteModal = () => { setDeletePayload(null); }; const closeCoverModal = () => { coverProjectIdRef.current = null; setCoverProjectId(null); }; const handleCoverFileChange = async (file: File | undefined) => { const selectedCoverProjectId = coverProjectIdRef.current ?? coverProjectId; if (!file || selectedCoverProjectId === null || actionProjectId !== null) return; if (!file.type.startsWith("image/")) { message.error(t("home.selectImageFile")); closeCoverModal(); return; } setActionProjectId(selectedCoverProjectId); startFakeCoverProgress(selectedCoverProjectId); try { const coverPayload = await buildFullUpdatePayload(selectedCoverProjectId); const uploadedCoverUrl = await uploadCanvasWorkflowCover(file); await updateCanvasWorkflow({ ...coverPayload, coverUrl: uploadedCoverUrl, }); message.success(t("home.coverUpdated")); finishCoverProgress(selectedCoverProjectId, uploadedCoverUrl); closeCoverModal(); } catch (error) { stopCoverProgressTimer(); setProjectCoverProgress(selectedCoverProjectId, undefined); message.error(error instanceof Error ? error.message : t("home.coverUpdateFailed")); } finally { setActionProjectId(null); if (coverInputRef.current) { coverInputRef.current.value = ""; } } }; const confirmDeleteProject = async () => { if (!deletePayload || actionProjectId !== null) return; setActionProjectId(deletePayload.id); try { await deleteCanvasWorkflow(deletePayload.id); message.success(t("home.deleteSuccess")); closeDeleteModal(); await loadProjectsPage(1); } catch (error) { message.error(error instanceof Error ? error.message : t("home.deleteFailed")); } finally { setActionProjectId(null); } }; const loadNextPage = useCallback(() => { const nextPage = page + 1; if (isLoadingProjects || nextPage > pageInfo.pageCount) return; if (loadingMorePageRef.current === nextPage) return; loadingMorePageRef.current = nextPage; void loadProjectsPage(nextPage, { append: true }).finally(() => { loadingMorePageRef.current = null; }); }, [isLoadingProjects, loadProjectsPage, page, pageInfo.pageCount]); return { projects, pageInfo, isLoadingProjects, isCreating, isActionLoading: actionProjectId !== null, renamePayload, renameTitle, setRenameTitle, closeRenameModal, confirmRenameProject, coverInputRef, closeCoverModal, handleCoverFileChange, deletePayload, closeDeleteModal, confirmDeleteProject, handleStartCreation, handleProjectAction, loadNextPage, }; } function RecentProjectsSection({ className = "", onAllProjectsClick, }: { className?: string; onAllProjectsClick: () => void; }) { const { t } = useI18n(); const { projects, pageInfo, isLoadingProjects, isCreating, isActionLoading, renamePayload, renameTitle, setRenameTitle, closeRenameModal, confirmRenameProject, coverInputRef, closeCoverModal, handleCoverFileChange, deletePayload, closeDeleteModal, confirmDeleteProject, handleStartCreation, handleProjectAction, goPrevPage, goNextPage, } = useProjectSectionState(4, t); return (

{t("home.recentProjects")}

{ const file = event.target.files?.[0]; if (!file) { closeCoverModal(); return; } void handleCoverFileChange(file); }} />
); } function AllProjectsSection({ onBackClick }: { onBackClick: () => void }) { const { t } = useI18n(); const [stickyTop, setStickyTop] = useState(90); const { projects, pageInfo, isLoadingProjects, isCreating, isActionLoading, renamePayload, renameTitle, setRenameTitle, closeRenameModal, confirmRenameProject, coverInputRef, closeCoverModal, handleCoverFileChange, deletePayload, closeDeleteModal, confirmDeleteProject, handleStartCreation, handleProjectAction, loadNextPage, } = useAllProjectsState(t); const canLoadMoreProjects = pageInfo.page < pageInfo.pageCount; useEffect(() => { const updateStickyTop = () => { const header = document.querySelector("header"); setStickyTop(header?.getBoundingClientRect().height ?? 0); }; updateStickyTop(); window.addEventListener("resize", updateStickyTop); return () => { window.removeEventListener("resize", updateStickyTop); }; }, []); useEffect(() => { const handleScroll = () => { if (!canLoadMoreProjects || isLoadingProjects) return; const distanceToBottom = document.documentElement.scrollHeight - window.innerHeight - window.scrollY; if (distanceToBottom <= ALL_PROJECTS_LOAD_MORE_OFFSET) { loadNextPage(); } }; window.addEventListener("scroll", handleScroll, { passive: true }); handleScroll(); return () => { window.removeEventListener("scroll", handleScroll); }; }, [canLoadMoreProjects, isLoadingProjects, loadNextPage]); return (

{t("home.allProjects")}

{isLoadingProjects && projects.length > 0 ? t("home.loading") : canLoadMoreProjects ? t("home.loadMorePull") : projects.length > 0 ? t("home.noMoreProjects") : null}
{ const file = event.target.files?.[0]; if (!file) { closeCoverModal(); return; } void handleCoverFileChange(file); }} />
); } function mapTemplateToShow(item: CanvasTemplateItem, t: Translate): ShowData { return { id: item.id, title: item.name || '', subtitle: item.desp || '', image: item.coverUrl || '', avatar: item?.userInfo?.avatar || '', vip: item.type === 1, data: item.properties || '', }; } function ShowsSection() { const { t } = useI18n(); const [activeTab, setActiveTab] = useState(0); const [categoryList, setCategoryList] = useState([ { id: null, name: t("home.allCategory") }, ]); const [dataList, setDataList] = useState([]); const [searchQuery, setSearchQuery] = useState(""); const [submittedSearchQuery, setSubmittedSearchQuery] = useState(""); const selectedCategoryId = categoryList[activeTab]?.id ?? null; const [previewShow, setPreviewShow] = useState(null); const [isCopyingShow, setIsCopyingShow] = useState(false); const [isLoadingShows, setIsLoadingShows] = useState(false); const [showsPage, setShowsPage] = useState(1); const [showsPageInfo, setShowsPageInfo] = useState({ ...DEFAULT_PROJECT_PAGE_INFO, page: 1, pageSize: SHOWS_PAGE_SIZE, }); const [stickyTop, setStickyTop] = useState(90); const loadingMoreShowsPageRef = useRef(null); const router = useRouter(); useEffect(() => { setCategoryList((currentCategories) => currentCategories.map((category) => category.id === null ? { ...category, name: t("home.allCategory") } : category ) ); }, [t]); useEffect(() => { let canceled = false; async function loadCategories() { try { const params = new URLSearchParams({ keyword: "" }); const response = await authFetch(`/api/canvas-templates/categories?${params.toString()}`); if (!response.ok) return; const result = (await response.json()) as ListResponse; const list = result.data?.list; if (!Array.isArray(list)) return; const categories = list .filter((item) => !item.deleted) .sort((a, b) => a.sort - b.sort) .map((item) => ({ id: item.id, name: item.name })) .filter((item) => item.name); if (!canceled && categories.length > 0) { setCategoryList([{ id: null, name: t("home.allCategory") }, ...categories]); } } catch { // Keep fallback category. } } loadCategories(); return () => { canceled = true; }; }, []); useEffect(() => { const updateStickyTop = () => { const header = document.querySelector("header"); setStickyTop(header?.getBoundingClientRect().height ?? 0); }; updateStickyTop(); window.addEventListener("resize", updateStickyTop); return () => { window.removeEventListener("resize", updateStickyTop); }; }, []); const loadShowsPage = useCallback(async ( nextPage: number, options?: { append?: boolean; signal?: AbortSignal } ) => { setIsLoadingShows(true); try { const params = new URLSearchParams({ keyword: submittedSearchQuery, categoryId: selectedCategoryId ? String(selectedCategoryId) : "", type: "", userId: "", page: String(nextPage), pageSize: String(SHOWS_PAGE_SIZE), }); const response = await authFetch(`/api/canvas-templates?${params.toString()}`, { signal: options?.signal, }); if (!response.ok) { if (!options?.append) { setDataList([]); } return; } const result = (await response.json()) as ListResponse; const list = result.data?.list; if (!Array.isArray(list)) { if (!options?.append) { setDataList([]); } return; } const nextShows = list .filter((item) => !item.deleted) .map((item) => mapTemplateToShow(item, t)); setDataList((currentShows) => { if (!options?.append) return nextShows; const existingIds = new Set(currentShows.map((show) => show.id)); return [...currentShows, ...nextShows.filter((show) => !existingIds.has(show.id))]; }); setShowsPageInfo( result.data?.pageInfo ?? { page: nextPage, pageSize: SHOWS_PAGE_SIZE, pageCount: nextShows.length < SHOWS_PAGE_SIZE ? nextPage : nextPage + 1, total: nextShows.length, } ); setShowsPage(nextPage); } catch { if (!options?.signal?.aborted && !options?.append) { setDataList([]); } } finally { if (!options?.signal?.aborted) { setIsLoadingShows(false); } } }, [submittedSearchQuery, selectedCategoryId, t]); useEffect(() => { const controller = new AbortController(); setShowsPage(1); setShowsPageInfo({ ...DEFAULT_PROJECT_PAGE_INFO, page: 1, pageSize: SHOWS_PAGE_SIZE, }); void loadShowsPage(1, { signal: controller.signal }); return () => { controller.abort(); loadingMoreShowsPageRef.current = null; }; }, [loadShowsPage]); const canLoadMoreShows = showsPage < showsPageInfo.pageCount; const loadNextShowsPage = useCallback(() => { const nextPage = showsPage + 1; if (isLoadingShows || nextPage > showsPageInfo.pageCount) return; if (loadingMoreShowsPageRef.current === nextPage) return; loadingMoreShowsPageRef.current = nextPage; void loadShowsPage(nextPage, { append: true }).finally(() => { loadingMoreShowsPageRef.current = null; }); }, [isLoadingShows, loadShowsPage, showsPage, showsPageInfo.pageCount]); const handleSubmitSearch = useCallback(() => { setSubmittedSearchQuery(searchQuery.trim()); }, [searchQuery]); const handleClearSearch = useCallback(() => { setSearchQuery(""); setSubmittedSearchQuery(""); }, []); useEffect(() => { const handleScroll = () => { if (!canLoadMoreShows || isLoadingShows) return; const distanceToBottom = document.documentElement.scrollHeight - window.innerHeight - window.scrollY; if (distanceToBottom <= SHOWS_LOAD_MORE_OFFSET) { loadNextShowsPage(); } }; window.addEventListener("scroll", handleScroll, { passive: true }); handleScroll(); return () => { window.removeEventListener("scroll", handleScroll); }; }, [canLoadMoreShows, isLoadingShows, loadNextShowsPage]); const handleCopyShow = useCallback(async (data: CanvasTemplatePreviewData) => { if (isCopyingShow) return; setIsCopyingShow(true); const title = data.title || t("home.projectFallback"); try { const result = await createCanvasWorkflow({ title, desp: data.subtitle || "", properties: data.data || buildEmptyWorkflowProperties(title), coverUrl: data.image || "", }); const workflowId = (result.data as any)?.id; router.push(workflowId ? `/canvas?workflowId=${workflowId}` : "/canvas"); } catch (error) { message.error(error instanceof Error ? error.message : t("home.createFailed")); setIsCopyingShow(false); } }, [isCopyingShow, router, t]); return (

{t("home.tvShow")}

{categoryList.map((tab, index) => ( ))}
setSearchQuery(e.target.value)} onKeyDown={(event) => { if (event.key === "Enter") { handleSubmitSearch(); } }} placeholder={t("home.searchPlaceholder")} className="min-w-0 flex-1 bg-transparent text-[14px] outline-none placeholder:text-[#999]" /> {searchQuery ? ( ) : null}
{dataList.map((show) => ( ))}
{isLoadingShows && dataList.length > 0 ? t("home.loading") : canLoadMoreShows ? t("home.loadMorePullUp") : dataList.length > 0 ? t("home.noMoreContent") : isLoadingShows ? t("home.loading") : null}
setPreviewShow(null)} onCopy={handleCopyShow} />
); } export function HomeProjectsContent({ heroSection, }: HomeProjectsContentProps) { const [isShowAll, setIsShowAll] = useState(false); return (
{isShowAll ? ( setIsShowAll(false)} /> ) : ( <> {heroSection} setIsShowAll(true)} /> )}
); }