This commit is contained in:
Maze Winther
2025-12-25 21:15:36 +01:00
parent 2da84e7132
commit d5ca991501
102 changed files with 16028 additions and 6675 deletions
+265 -378
View File
@@ -1,25 +1,44 @@
"use client";
import { useEffect, useRef } from "react";
import { useParams, useRouter } from "next/navigation";
import { useParams } from "next/navigation";
import {
ResizablePanelGroup,
ResizablePanel,
ResizableHandle,
} from "@/components/ui/resizable";
import { MediaPanel } from "@/components/editor/media-panel";
import { AssetsPanel } from "@/components/editor/assets-panel";
import { PropertiesPanel } from "@/components/editor/properties-panel";
import { Timeline } from "@/components/editor/timeline";
import { PreviewPanel } from "@/components/editor/preview-panel";
import { EditorHeader } from "@/components/editor/editor-header";
import { usePanelStore } from "@/stores/panel-store";
import { useProjectStore } from "@/stores/project-store";
import { EditorProvider } from "@/components/providers/editor-provider";
import { usePlaybackControls } from "@/hooks/use-playback-controls";
import { Onboarding } from "@/components/editor/onboarding";
import { useProjectInitialization } from "@/hooks/use-project-initialization";
export default function Editor() {
const params = useParams();
const projectId = params.project_id as string;
useProjectInitialization({ projectId });
return (
<EditorProvider>
<div className="bg-background flex h-screen w-screen flex-col overflow-hidden">
<EditorHeader />
<div className="min-h-0 min-w-0 flex-1">
<EditorLayout />
</div>
<Onboarding />
</div>
</EditorProvider>
);
}
function EditorLayout({}: {}) {
const {
activePreset,
resetCounter,
toolsPanel,
previewPanel,
mainContent,
@@ -29,269 +48,54 @@ export default function Editor() {
setMainContent,
setTimeline,
propertiesPanel,
setPropertiesPanel,
activePreset,
resetCounter,
setPropertiesPanel,
} = usePanelStore();
const {
activeProject,
loadProject,
createNewProject,
isInvalidProjectId,
markProjectIdAsInvalid,
} = useProjectStore();
const params = useParams();
const router = useRouter();
const projectId = params.project_id as string;
const handledProjectIds = useRef<Set<string>>(new Set());
const isInitializingRef = useRef<boolean>(false);
return activePreset === "media" ? (
<ResizablePanelGroup
key={`media-${activePreset}-${resetCounter}`}
direction="horizontal"
className="h-full w-full gap-[0.18rem] px-3 pb-3"
>
<ResizablePanel
defaultSize={toolsPanel}
minSize={15}
maxSize={40}
onResize={setToolsPanel}
className="min-w-0 rounded-sm"
>
<AssetsPanel />
</ResizablePanel>
usePlaybackControls();
<ResizableHandle withHandle />
useEffect(() => {
let isCancelled = false;
const initProject = async () => {
if (!projectId) {
return;
}
// Prevent duplicate initialization
if (isInitializingRef.current) {
return;
}
// Check if project is already loaded
if (activeProject?.id === projectId) {
return;
}
// Check global invalid tracking first (most important for preventing duplicates)
if (isInvalidProjectId(projectId)) {
return;
}
// Check if we've already handled this project ID locally
if (handledProjectIds.current.has(projectId)) {
return;
}
// Mark as initializing to prevent race conditions
isInitializingRef.current = true;
handledProjectIds.current.add(projectId);
try {
await loadProject(projectId);
// Check if component was unmounted during async operation
if (isCancelled) {
return;
}
// Project loaded successfully
isInitializingRef.current = false;
} catch (error) {
// Check if component was unmounted during async operation
if (isCancelled) {
return;
}
// More specific error handling - only create new project for actual "not found" errors
const isProjectNotFound =
error instanceof Error &&
(error.message.includes("not found") ||
error.message.includes("does not exist") ||
error.message.includes("Project not found"));
if (isProjectNotFound) {
// Mark this project ID as invalid globally BEFORE creating project
markProjectIdAsInvalid(projectId);
try {
const newProjectId = await createNewProject("Untitled Project");
// Check again if component was unmounted
if (isCancelled) {
return;
}
router.replace(`/editor/${newProjectId}`);
} catch (createError) {
console.error("Failed to create new project:", createError);
}
} else {
// For other errors (storage issues, corruption, etc.), don't create new project
console.error(
"Project loading failed with recoverable error:",
error,
);
// Remove from handled set so user can retry
handledProjectIds.current.delete(projectId);
}
isInitializingRef.current = false;
}
};
initProject();
// Cleanup function to cancel async operations
return () => {
isCancelled = true;
isInitializingRef.current = false;
};
}, [
projectId,
loadProject,
createNewProject,
router,
isInvalidProjectId,
markProjectIdAsInvalid,
]);
return (
<EditorProvider>
<div className="bg-background flex h-screen w-screen flex-col overflow-hidden">
<EditorHeader />
<div className="min-h-0 min-w-0 flex-1">
{activePreset === "media" ? (
<ResizablePanel
defaultSize={100 - toolsPanel}
minSize={60}
className="min-h-0 min-w-0"
>
<ResizablePanelGroup
direction="vertical"
className="h-full w-full gap-[0.18rem]"
>
<ResizablePanel
defaultSize={mainContent}
minSize={30}
maxSize={85}
onResize={setMainContent}
className="min-h-0"
>
<ResizablePanelGroup
key={`media-${activePreset}-${resetCounter}`}
direction="horizontal"
className="h-full w-full gap-[0.18rem] px-3 pb-3"
className="h-full w-full gap-[0.19rem]"
>
<ResizablePanel
defaultSize={toolsPanel}
minSize={15}
maxSize={40}
onResize={setToolsPanel}
className="min-w-0 rounded-sm"
>
<MediaPanel />
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={100 - toolsPanel}
minSize={60}
className="min-h-0 min-w-0"
>
<ResizablePanelGroup
direction="vertical"
className="h-full w-full gap-[0.18rem]"
>
<ResizablePanel
defaultSize={mainContent}
minSize={30}
maxSize={85}
onResize={setMainContent}
className="min-h-0"
>
<ResizablePanelGroup
direction="horizontal"
className="h-full w-full gap-[0.19rem]"
>
<ResizablePanel
defaultSize={previewPanel}
minSize={30}
onResize={setPreviewPanel}
className="min-h-0 min-w-0 flex-1"
>
<PreviewPanel />
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={propertiesPanel}
minSize={15}
maxSize={40}
onResize={setPropertiesPanel}
className="min-w-0"
>
<PropertiesPanel />
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={timeline}
minSize={15}
maxSize={70}
onResize={setTimeline}
className="min-h-0"
>
<Timeline />
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
</ResizablePanelGroup>
) : activePreset === "inspector" ? (
<ResizablePanelGroup
key={`inspector-${activePreset}-${resetCounter}`}
direction="horizontal"
className="h-full w-full gap-[0.18rem] px-3 pb-3"
>
<ResizablePanel
defaultSize={100 - propertiesPanel}
defaultSize={previewPanel}
minSize={30}
onResize={(size) => setPropertiesPanel(100 - size)}
className="min-h-0 min-w-0"
onResize={setPreviewPanel}
className="min-h-0 min-w-0 flex-1"
>
<ResizablePanelGroup
direction="vertical"
className="h-full w-full gap-[0.18rem]"
>
<ResizablePanel
defaultSize={mainContent}
minSize={30}
maxSize={85}
onResize={setMainContent}
className="min-h-0"
>
<ResizablePanelGroup
direction="horizontal"
className="h-full w-full gap-[0.19rem]"
>
<ResizablePanel
defaultSize={toolsPanel}
minSize={15}
maxSize={40}
onResize={setToolsPanel}
className="min-w-0 rounded-sm"
>
<MediaPanel />
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={previewPanel}
minSize={30}
onResize={setPreviewPanel}
className="min-h-0 min-w-0 flex-1"
>
<PreviewPanel />
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={timeline}
minSize={15}
maxSize={70}
onResize={setTimeline}
className="min-h-0"
>
<Timeline />
</ResizablePanel>
</ResizablePanelGroup>
<PreviewPanel />
</ResizablePanel>
<ResizableHandle withHandle />
@@ -301,74 +105,62 @@ export default function Editor() {
minSize={15}
maxSize={40}
onResize={setPropertiesPanel}
className="min-h-0 min-w-0"
className="min-w-0"
>
<PropertiesPanel />
</ResizablePanel>
</ResizablePanelGroup>
) : activePreset === "vertical-preview" ? (
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={timeline}
minSize={15}
maxSize={70}
onResize={setTimeline}
className="min-h-0"
>
<Timeline />
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
</ResizablePanelGroup>
) : activePreset === "inspector" ? (
<ResizablePanelGroup
key={`inspector-${activePreset}-${resetCounter}`}
direction="horizontal"
className="h-full w-full gap-[0.18rem] px-3 pb-3"
>
<ResizablePanel
defaultSize={100 - propertiesPanel}
minSize={30}
onResize={(size) => setPropertiesPanel(100 - size)}
className="min-h-0 min-w-0"
>
<ResizablePanelGroup
direction="vertical"
className="h-full w-full gap-[0.18rem]"
>
<ResizablePanel
defaultSize={mainContent}
minSize={30}
maxSize={85}
onResize={setMainContent}
className="min-h-0"
>
<ResizablePanelGroup
key={`vertical-preview-${activePreset}-${resetCounter}`}
direction="horizontal"
className="h-full w-full gap-[0.18rem] px-3 pb-3"
className="h-full w-full gap-[0.19rem]"
>
<ResizablePanel
defaultSize={100 - previewPanel}
minSize={30}
onResize={(size) => setPreviewPanel(100 - size)}
className="min-h-0 min-w-0"
defaultSize={toolsPanel}
minSize={15}
maxSize={40}
onResize={setToolsPanel}
className="min-w-0 rounded-sm"
>
<ResizablePanelGroup
direction="vertical"
className="h-full w-full gap-[0.18rem]"
>
<ResizablePanel
defaultSize={mainContent}
minSize={30}
maxSize={85}
onResize={setMainContent}
className="min-h-0"
>
<ResizablePanelGroup
direction="horizontal"
className="h-full w-full gap-[0.19rem]"
>
<ResizablePanel
defaultSize={toolsPanel}
minSize={15}
maxSize={40}
onResize={setToolsPanel}
className="min-w-0 rounded-sm"
>
<MediaPanel />
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={propertiesPanel}
minSize={15}
maxSize={40}
onResize={setPropertiesPanel}
className="min-w-0"
>
<PropertiesPanel />
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={timeline}
minSize={15}
maxSize={70}
onResize={setTimeline}
className="min-h-0"
>
<Timeline />
</ResizablePanel>
</ResizablePanelGroup>
<AssetsPanel />
</ResizablePanel>
<ResizableHandle withHandle />
@@ -377,83 +169,178 @@ export default function Editor() {
defaultSize={previewPanel}
minSize={30}
onResize={setPreviewPanel}
className="min-h-0 min-w-0"
className="min-h-0 min-w-0 flex-1"
>
<PreviewPanel />
</ResizablePanel>
</ResizablePanelGroup>
) : (
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={timeline}
minSize={15}
maxSize={70}
onResize={setTimeline}
className="min-h-0"
>
<Timeline />
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={propertiesPanel}
minSize={15}
maxSize={40}
onResize={setPropertiesPanel}
className="min-h-0 min-w-0"
>
<PropertiesPanel />
</ResizablePanel>
</ResizablePanelGroup>
) : activePreset === "vertical-preview" ? (
<ResizablePanelGroup
key={`vertical-preview-${activePreset}-${resetCounter}`}
direction="horizontal"
className="h-full w-full gap-[0.18rem] px-3 pb-3"
>
<ResizablePanel
defaultSize={100 - previewPanel}
minSize={30}
onResize={(size) => setPreviewPanel(100 - size)}
className="min-h-0 min-w-0"
>
<ResizablePanelGroup
direction="vertical"
className="h-full w-full gap-[0.18rem]"
>
<ResizablePanel
defaultSize={mainContent}
minSize={30}
maxSize={85}
onResize={setMainContent}
className="min-h-0"
>
<ResizablePanelGroup
key={`default-${activePreset}-${resetCounter}`}
direction="vertical"
className="h-full w-full gap-[0.18rem]"
direction="horizontal"
className="h-full w-full gap-[0.19rem]"
>
<ResizablePanel
defaultSize={mainContent}
minSize={30}
maxSize={85}
onResize={setMainContent}
className="min-h-0"
defaultSize={toolsPanel}
minSize={15}
maxSize={40}
onResize={setToolsPanel}
className="min-w-0 rounded-sm"
>
{/* Main content area */}
<ResizablePanelGroup
direction="horizontal"
className="h-full w-full gap-[0.19rem] px-3"
>
{/* Tools Panel */}
<ResizablePanel
defaultSize={toolsPanel}
minSize={15}
maxSize={40}
onResize={setToolsPanel}
className="min-w-0 rounded-sm"
>
<MediaPanel />
</ResizablePanel>
<ResizableHandle withHandle />
{/* Preview Area */}
<ResizablePanel
defaultSize={previewPanel}
minSize={30}
onResize={setPreviewPanel}
className="min-h-0 min-w-0 flex-1"
>
<PreviewPanel />
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={propertiesPanel}
minSize={15}
maxSize={40}
onResize={setPropertiesPanel}
className="min-w-0 rounded-sm"
>
<PropertiesPanel />
</ResizablePanel>
</ResizablePanelGroup>
<AssetsPanel />
</ResizablePanel>
<ResizableHandle withHandle />
{/* Timeline */}
<ResizablePanel
defaultSize={timeline}
defaultSize={propertiesPanel}
minSize={15}
maxSize={70}
onResize={setTimeline}
className="min-h-0 px-3 pb-3"
maxSize={40}
onResize={setPropertiesPanel}
className="min-w-0"
>
<Timeline />
<PropertiesPanel />
</ResizablePanel>
</ResizablePanelGroup>
)}
</div>
<Onboarding />
</div>
</EditorProvider>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={timeline}
minSize={15}
maxSize={70}
onResize={setTimeline}
className="min-h-0"
>
<Timeline />
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={previewPanel}
minSize={30}
onResize={setPreviewPanel}
className="min-h-0 min-w-0"
>
<PreviewPanel />
</ResizablePanel>
</ResizablePanelGroup>
) : (
<ResizablePanelGroup
key={`default-${activePreset}-${resetCounter}`}
direction="vertical"
className="h-full w-full gap-[0.18rem]"
>
<ResizablePanel
defaultSize={mainContent}
minSize={30}
maxSize={85}
onResize={setMainContent}
className="min-h-0"
>
<ResizablePanelGroup
direction="horizontal"
className="h-full w-full gap-[0.19rem] px-3"
>
<ResizablePanel
defaultSize={toolsPanel}
minSize={15}
maxSize={40}
onResize={setToolsPanel}
className="min-w-0 rounded-sm"
>
<AssetsPanel />
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={previewPanel}
minSize={30}
onResize={setPreviewPanel}
className="min-h-0 min-w-0 flex-1"
>
<PreviewPanel />
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={propertiesPanel}
minSize={15}
maxSize={40}
onResize={setPropertiesPanel}
className="min-w-0 rounded-sm"
>
<PropertiesPanel />
</ResizablePanel>
</ResizablePanelGroup>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel
defaultSize={timeline}
minSize={15}
maxSize={70}
onResize={setTimeline}
className="min-h-0 px-3 pb-3"
>
<Timeline />
</ResizablePanel>
</ResizablePanelGroup>
);
}
+48 -49
View File
@@ -53,15 +53,16 @@ export default function ProjectsPage() {
Record<string, string | null>
>({});
const [_loadingThumbnails, setLoadingThumbnails] = useState<Set<string>>(
new Set()
new Set(),
);
const [isSelectionMode, setIsSelectionMode] = useState(false);
const [selectedProjects, setSelectedProjects] = useState<Set<string>>(
new Set()
new Set(),
);
const [isBulkDeleteDialogOpen, setIsBulkDeleteDialogOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
const [sortOption, setSortOption] = useState("createdAt-desc");
const { getProjectThumbnail } = useTimelineStore();
const router = useRouter();
const getProjectThumbnail = useCallback(
@@ -73,9 +74,7 @@ export default function ProjectsPage() {
setLoadingThumbnails((prev) => new Set(prev).add(projectId));
try {
const thumbnail = await useTimelineStore
.getState()
.getProjectThumbnail(projectId);
const thumbnail = await getProjectThumbnail(projectId);
setThumbnailCache((prev) => ({ ...prev, [projectId]: thumbnail }));
return thumbnail;
} finally {
@@ -86,7 +85,7 @@ export default function ProjectsPage() {
});
}
},
[]
[],
);
const handleCreateProject = async () => {
@@ -120,7 +119,7 @@ export default function ProjectsPage() {
const handleBulkDelete = async () => {
await Promise.all(
Array.from(selectedProjects).map((projectId) => deleteProject(projectId))
Array.from(selectedProjects).map((projectId) => deleteProject(projectId)),
);
setSelectedProjects(new Set());
setIsSelectionMode(false);
@@ -136,11 +135,11 @@ export default function ProjectsPage() {
selectedProjects.size > 0 && selectedProjects.size < sortedProjects.length;
return (
<div className="min-h-screen bg-background">
<div className="pt-6 px-6 flex items-center justify-between w-full h-16">
<div className="bg-background min-h-screen">
<div className="flex h-16 w-full items-center justify-between px-6 pt-6">
<Link
href="/"
className="flex items-center gap-1 hover:text-muted-foreground transition-colors"
className="hover:text-muted-foreground flex items-center gap-1 transition-colors"
>
<ChevronLeft className="size-5! shrink-0" />
<span className="text-sm font-medium">Back</span>
@@ -172,17 +171,17 @@ export default function ProjectsPage() {
)}
</div>
</div>
<main className="max-w-6xl mx-auto px-6 pt-6 pb-6">
<main className="mx-auto max-w-6xl px-6 pb-6 pt-6">
<div className="mb-8 flex items-center justify-between">
<div className="flex flex-col gap-3">
<h1 className="text-2xl md:text-3xl font-bold tracking-tight">
<h1 className="text-2xl font-bold tracking-tight md:text-3xl">
Your Projects
</h1>
<p className="text-muted-foreground">
{savedProjects.length}{" "}
{savedProjects.length === 1 ? "project" : "projects"}
{isSelectionMode && selectedProjects.size > 0 && (
<span className="ml-2 text-primary">
<span className="text-primary ml-2">
{selectedProjects.size} selected
</span>
)}
@@ -221,7 +220,7 @@ export default function ProjectsPage() {
</div>
<div className="mb-4 flex items-center justify-between gap-4">
<div className="flex-1 max-w-72">
<div className="max-w-72 flex-1">
<Input
placeholder="Search projects..."
value={searchQuery}
@@ -237,7 +236,7 @@ export default function ProjectsPage() {
<Button
size="icon"
variant="secondary"
className="justify-center items-center w-9 h-9"
className="h-9 w-9 items-center justify-center"
>
<ArrowDown01
strokeWidth={1.5}
@@ -253,7 +252,7 @@ export default function ProjectsPage() {
setSortOption(
sortOption.endsWith("asc")
? "createdAt-desc"
: "createdAt-asc"
: "createdAt-asc",
);
} else {
setSortOption("createdAt-asc");
@@ -270,7 +269,7 @@ export default function ProjectsPage() {
setSortOption(
sortOption.endsWith("asc")
? "name-desc"
: "name-asc"
: "name-asc",
);
} else {
setSortOption("name-asc");
@@ -305,32 +304,32 @@ export default function ProjectsPage() {
handleSelectAll(!allSelected);
}
}}
className="w-full hover:cursor-pointer gap-2 mb-6 p-4 bg-muted/30 rounded-lg border items-center flex"
className="bg-muted/30 mb-6 flex w-full items-center gap-2 rounded-lg border p-4 hover:cursor-pointer"
tabIndex={0}
>
<Checkbox checked={someSelected ? "indeterminate" : allSelected} />
<span className="text-sm font-medium">
{allSelected ? "Deselect All" : "Select All"}
</span>
<span className="text-sm text-muted-foreground">
<span className="text-muted-foreground text-sm">
({selectedProjects.size} of {sortedProjects.length} selected)
</span>
</button>
)}
{isLoading || !isInitialized ? (
<div className="grid grid-cols-1 xs:grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-6">
<div className="xs:grid-cols-2 grid grid-cols-1 gap-6 sm:grid-cols-3 lg:grid-cols-4">
{Array.from({ length: 8 }, (_, index) => (
<div
key={`skeleton-${index}-${Date.now()}`}
className="overflow-hidden bg-background border-none p-0"
className="bg-background overflow-hidden border-none p-0"
>
<Skeleton className="aspect-square w-full bg-muted/50" />
<div className="px-0 pt-5 flex flex-col gap-1">
<Skeleton className="h-4 w-3/4 bg-muted/50" />
<Skeleton className="bg-muted/50 aspect-square w-full" />
<div className="flex flex-col gap-1 px-0 pt-5">
<Skeleton className="bg-muted/50 h-4 w-3/4" />
<div className="flex items-center gap-1.5">
<Skeleton className="h-4 w-4 bg-muted/50" />
<Skeleton className="h-4 w-24 bg-muted/50" />
<Skeleton className="bg-muted/50 h-4 w-4" />
<Skeleton className="bg-muted/50 h-4 w-24" />
</div>
</div>
</div>
@@ -344,7 +343,7 @@ export default function ProjectsPage() {
onClearSearch={() => setSearchQuery("")}
/>
) : (
<div className="grid grid-cols-1 xs:grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-6">
<div className="xs:grid-cols-2 grid grid-cols-1 gap-6 sm:grid-cols-3 lg:grid-cols-4">
{sortedProjects.map((project) => (
<ProjectCard
key={project.id}
@@ -442,25 +441,25 @@ function ProjectCard({
const cardContent = (
<Card
className={`overflow-hidden bg-background border-none p-0 transition-all ${
isSelectionMode && isSelected ? "ring-2 ring-primary" : ""
className={`bg-background overflow-hidden border-none p-0 transition-all ${
isSelectionMode && isSelected ? "ring-primary ring-2" : ""
}`}
>
<div
className={`relative aspect-square bg-muted transition-opacity ${
className={`bg-muted relative aspect-square transition-opacity ${
isDropdownOpen ? "opacity-65" : "opacity-100 group-hover:opacity-65"
}`}
>
{isSelectionMode && (
<div className="absolute top-3 left-3 z-10">
<div className="w-5 h-5 rounded-full bg-background/80 backdrop-blur-xs border flex items-center justify-center">
<div className="absolute left-3 top-3 z-10">
<div className="bg-background/80 backdrop-blur-xs flex h-5 w-5 items-center justify-center rounded-full border">
<Checkbox
checked={isSelected}
onCheckedChange={(checked) =>
onSelect?.(project.id, checked as boolean)
}
onClick={(e) => e.stopPropagation()}
className="w-4 h-4"
className="h-4 w-4"
/>
</div>
</div>
@@ -468,8 +467,8 @@ function ProjectCard({
<div className="absolute inset-0">
{isLoadingThumbnail ? (
<div className="w-full h-full bg-muted/50 flex items-center justify-center">
<Loader2 className="h-12 w-12 text-muted-foreground animate-spin" />
<div className="bg-muted/50 flex h-full w-full items-center justify-center">
<Loader2 className="text-muted-foreground h-12 w-12 animate-spin" />
</div>
) : dynamicThumbnail ? (
<Image
@@ -479,16 +478,16 @@ function ProjectCard({
className="object-cover"
/>
) : (
<div className="w-full h-full bg-muted/50 flex items-center justify-center">
<Video className="h-12 w-12 shrink-0 text-muted-foreground" />
<div className="bg-muted/50 flex h-full w-full items-center justify-center">
<Video className="text-muted-foreground h-12 w-12 shrink-0" />
</div>
)}
</div>
</div>
<CardContent className="px-0 pt-5 flex flex-col gap-1">
<CardContent className="flex flex-col gap-1 px-0 pt-5">
<div className="flex items-start justify-between">
<h3 className="font-medium text-sm leading-snug group-hover:text-foreground/90 transition-colors line-clamp-2">
<h3 className="group-hover:text-foreground/90 line-clamp-2 text-sm font-medium leading-snug transition-colors">
{project.name}
</h3>
{!isSelectionMode && (
@@ -500,7 +499,7 @@ function ProjectCard({
<Button
variant="text"
size="sm"
className={`size-6 p-0 transition-all shrink-0 ml-2 ${
className={`ml-2 size-6 shrink-0 p-0 transition-all ${
isDropdownOpen
? "opacity-100"
: "opacity-0 group-hover:opacity-100"
@@ -554,7 +553,7 @@ function ProjectCard({
</div>
<div className="space-y-1">
<div className="flex items-center gap-1.5 text-sm text-muted-foreground">
<div className="text-muted-foreground flex items-center gap-1.5 text-sm">
<Calendar className="size-4!" />
<span>Created {formatDate(project.createdAt)}</span>
</div>
@@ -570,12 +569,12 @@ function ProjectCard({
type="button"
onClick={handleCardClick}
onKeyDown={handleCardKeyDown}
className="block group cursor-pointer w-full text-left"
className="group block w-full cursor-pointer text-left"
>
{cardContent}
</button>
) : (
<Link href={`/editor/${project.id}`} className="block group">
<Link href={`/editor/${project.id}`} className="group block">
{cardContent}
</Link>
)}
@@ -606,10 +605,10 @@ function CreateButton({ onClick }: { onClick?: () => void }) {
function NoProjects({ onCreateProject }: { onCreateProject: () => void }) {
return (
<div className="flex flex-col items-center justify-center py-16 text-center">
<div className="w-16 h-16 rounded-full bg-muted/30 flex items-center justify-center mb-4">
<Video className="h-8 w-8 text-muted-foreground" />
<div className="bg-muted/30 mb-4 flex h-16 w-16 items-center justify-center rounded-full">
<Video className="text-muted-foreground h-8 w-8" />
</div>
<h3 className="text-lg font-medium mb-2">No projects yet</h3>
<h3 className="mb-2 text-lg font-medium">No projects yet</h3>
<p className="text-muted-foreground mb-6 max-w-md">
Start creating your first video project. Import media, edit, and export
professional videos.
@@ -631,10 +630,10 @@ function NoResults({
}) {
return (
<div className="flex flex-col items-center justify-center py-16 text-center">
<div className="w-16 h-16 rounded-full bg-muted/30 flex items-center justify-center mb-4">
<Search className="h-8 w-8 text-muted-foreground" />
<div className="bg-muted/30 mb-4 flex h-16 w-16 items-center justify-center rounded-full">
<Search className="text-muted-foreground h-8 w-8" />
</div>
<h3 className="text-lg font-medium mb-2">No results found</h3>
<h3 className="mb-2 text-lg font-medium">No results found</h3>
<p className="text-muted-foreground mb-6 max-w-md">
Your search for "{searchQuery}" did not return any results.
</p>