ai-chatbot/components/home/lovart/lovart-projects-section.tsx
2026-07-10 18:09:49 +08:00

121 lines
5.0 KiB
TypeScript

import { ChevronRight, Loader2, Trash2 } from "lucide-react";
import type { Ref } from "react";
import type { CanvasProject } from "./types";
import { formatRelativeTime } from "./utils";
type LovartProjectsSectionProps = {
deletingProjectId: string | null;
hasMoreProjects: boolean;
isProjectsOnlyView: boolean;
onCreateProject: () => void;
onDeleteProject: (projectId: string, projectTitle?: string) => void;
onOpenProject: (projectId: string) => void;
onPrepareCreate: () => void;
onPrepareProject: (projectId: string) => void;
onViewAllProjects: () => void;
projects: CanvasProject[];
projectSectionRef: Ref<HTMLElement>;
};
export function LovartProjectsSection({
deletingProjectId,
hasMoreProjects,
isProjectsOnlyView,
onCreateProject,
onDeleteProject,
onOpenProject,
onPrepareCreate,
onPrepareProject,
onViewAllProjects,
projects,
projectSectionRef,
}: LovartProjectsSectionProps) {
return (
<section
ref={projectSectionRef}
className={isProjectsOnlyView ? "mt-5" : "mt-[112px]"}
>
<div className="mb-6 flex items-center justify-between">
<h2 className="text-[20px] font-semibold leading-none text-[#1f1f1f] dark:text-white">
{isProjectsOnlyView ? "所有项目" : "最近项目"}
</h2>
{!isProjectsOnlyView && hasMoreProjects ? (
<button
type="button"
className="inline-flex items-center gap-1 text-[13px] font-normal leading-none text-[#7d7d7d] transition-colors duration-150 hover:text-[#1f1f1f] dark:text-white/50 dark:hover:text-white"
onClick={onViewAllProjects}
>
查看全部
<ChevronRight className="h-3.5 w-3.5" strokeWidth={1.8} />
</button>
) : null}
</div>
<div className="grid grid-cols-1 items-start gap-x-4 gap-y-7 md:grid-cols-2 lg:grid-cols-5">
<button
type="button"
onClick={onCreateProject}
onFocus={onPrepareCreate}
onPointerEnter={onPrepareCreate}
className="group block w-full cursor-pointer appearance-none bg-transparent p-0 text-left"
>
<div className="flex aspect-[16/9] items-center justify-center rounded-[8px] border border-dashed border-[#d8d8d8] bg-[#f4f4f4] transition-colors duration-150 group-hover:border-[#c8c8c8] group-hover:bg-[#f0f0f0] dark:border-white/15 dark:bg-[#18191d]">
<span className="text-[26px] font-light leading-none text-[#8e8e8e]">+</span>
</div>
<div className="mt-3 text-[14px] font-normal leading-none text-[#202020] dark:text-white">
新建项目
</div>
</button>
{projects.map((item) => (
<div
key={item.id}
className="group relative text-left"
>
<button
type="button"
onClick={() => onOpenProject(item.id)}
onFocus={() => onPrepareProject(item.id)}
onPointerEnter={() => onPrepareProject(item.id)}
className="block w-full cursor-pointer text-left"
>
<div className="aspect-[16/9] overflow-hidden rounded-[8px] border border-[#dddddd] bg-[#f4f4f4] dark:border-white/10 dark:bg-[#1b1c20]">
{item.thumbnail ? (
<img
src={item.thumbnail}
alt={item.title}
loading="lazy"
decoding="async"
className="h-full w-full object-cover transition-transform duration-150 ease-out will-change-transform group-hover:scale-[1.018]"
/>
) : (
<div className="h-full w-full" />
)}
</div>
<div className="mt-3 truncate text-[14px] font-normal leading-none text-[#202020] dark:text-white">
{item.title || "Untitled"}
</div>
<div className="mt-2 text-[11px] font-normal leading-none text-[#858585] dark:text-white/42">
{formatRelativeTime(item.updatedAt || item.createdAt)}
</div>
</button>
<button
type="button"
onClick={() => onDeleteProject(item.id, item.title)}
disabled={deletingProjectId === item.id}
className="absolute right-3 top-3 z-20 inline-flex h-8 w-8 items-center justify-center rounded-full border border-black/10 bg-white/90 text-black/45 opacity-0 shadow-sm transition hover:border-[#ffb4b4] hover:text-[#ef4444] group-hover:opacity-100 disabled:cursor-not-allowed disabled:opacity-80 dark:border-white/10 dark:bg-[#18181c]/90 dark:text-white/45"
aria-label={`删除${item.title || "未命名画布"}`}
>
{deletingProjectId === item.id ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Trash2 className="h-3.5 w-3.5" />
)}
</button>
</div>
))}
</div>
</section>
);
}