diff --git a/apps/web/src/app/projects/page.tsx b/apps/web/src/app/projects/page.tsx
index b855cefc..a86cf775 100644
--- a/apps/web/src/app/projects/page.tsx
+++ b/apps/web/src/app/projects/page.tsx
@@ -46,6 +46,13 @@ import {
} from "@hugeicons/core-free-icons";
import { OcVideoIcon } from "@opencut/ui/icons";
import { Label } from "@/components/ui/label";
+import {
+ ContextMenu,
+ ContextMenuContent,
+ ContextMenuItem,
+ ContextMenuSeparator,
+ ContextMenuTrigger,
+} from "@/components/ui/context-menu";
import {
DropdownMenu,
DropdownMenuCheckboxItem,
@@ -534,10 +541,25 @@ function ProjectItem({
const isSelected = selectedProjectIdSet.has(project.id);
const selectedProjectCount = selectedProjectIds.length;
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
+ const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
+ const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
+ const [isInfoDialogOpen, setIsInfoDialogOpen] = useState(false);
+ const editor = useEditor();
const durationLabel = formatProjectDuration({ duration: project.duration });
const isMultiSelect = selectedProjectCount > 1;
const isGridView = viewMode === "grid";
+ const handleRename = () => setIsRenameDialogOpen(true);
+ const handleDuplicate = async () => {
+ await duplicateProjects({ editor, ids: [project.id] });
+ };
+ const handleDeleteClick = () => setIsDeleteDialogOpen(true);
+ const handleInfoClick = () => setIsInfoDialogOpen(true);
+ const handleDeleteConfirm = async () => {
+ await deleteProjects({ editor, ids: [project.id] });
+ setIsDeleteDialogOpen(false);
+ };
+
const handleCheckboxChange = ({
checked,
shiftKey,
@@ -622,9 +644,8 @@ function ProjectItem({
const listContent = (
{}}
+ onCheckedChange={() => { }}
className="size-5 shrink-0"
/>
@@ -643,176 +664,71 @@ function ProjectItem({
{listRowContent}
- {!isMultiSelect && (
-
- )}
+ {!isMultiSelect && (
+
+ )}
);
- const cardContent = isGridView ? gridContent : listContent;
-
- if (!isGridView) {
- return {listContent}
;
- }
-
- return (
-
-
- {cardContent}
-
-
- {isGridView && (
- <>
-
event.preventDefault()}
- onClick={(event) => {
- handleCheckboxChange({
- checked: !isSelected,
- shiftKey: event.shiftKey,
- });
- }}
- onCheckedChange={() => {}}
- className={`absolute z-10 size-5 top-3 left-3 ${
- isSelected || isDropdownOpen
- ? "opacity-100"
- : "opacity-0 group-hover:opacity-100"
- }`}
- />
-
- {!isMultiSelect && (
-
- )}
- >
- )}
-
- );
-}
-
-function ProjectMenu({
- isOpen,
- onOpenChange,
- project,
- variant = "grid",
-}: {
- isOpen: boolean;
- onOpenChange: (open: boolean) => void;
- project: TProjectMetadata;
- variant?: "grid" | "list";
-}) {
- const editor = useEditor();
- const [isRenameDialogOpen, setIsRenameDialogOpen] = useState(false);
- const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
- const [isInfoDialogOpen, setIsInfoDialogOpen] = useState(false);
-
- const handleMenuClick = ({
- event,
- }: {
- event: MouseEvent;
- }) => {
- event.preventDefault();
- event.stopPropagation();
- };
-
- const handleMenuKeyDown = ({
- event,
- }: {
- event: KeyboardEvent;
- }) => {
- if (event.key !== "Enter" && event.key !== " ") {
- return;
- }
- event.preventDefault();
- event.stopPropagation();
- };
-
- const handleRename = () => {
- setIsRenameDialogOpen(true);
- onOpenChange(false);
- };
-
- const handleDuplicate = async () => {
- await duplicateProjects({ editor, ids: [project.id] });
- onOpenChange(false);
- };
-
- const handleDeleteClick = () => {
- setIsDeleteDialogOpen(true);
- onOpenChange(false);
- };
-
- const handleDeleteConfirm = async () => {
- await deleteProjects({ editor, ids: [project.id] });
- setIsDeleteDialogOpen(false);
- };
-
- const handleInfoClick = () => {
- setIsInfoDialogOpen(true);
- onOpenChange(false);
- };
-
- const isGrid = variant === "grid";
-
return (
<>
-
-
-
-
-
-
-
- Rename
-
-
-
- Duplicate
-
-
-
- Info
-
-
-
- Delete
-
-
-
+
+
+
+ {isGridView ? (
+ <>
+
+ {gridContent}
+
+
+
event.preventDefault()}
+ onClick={(event) => {
+ handleCheckboxChange({
+ checked: !isSelected,
+ shiftKey: event.shiftKey,
+ });
+ }}
+ onCheckedChange={() => {}}
+ className={`absolute z-10 size-5 top-3 left-3 ${
+ isSelected || isDropdownOpen
+ ? "opacity-100"
+ : "opacity-0 group-hover:opacity-100"
+ }`}
+ />
+
+ {!isMultiSelect && (
+
+ )}
+ >
+ ) : (
+ listContent
+ )}
+
+
+
+
void;
+ onDuplicateClick: () => void;
+ onDeleteClick: () => void;
+ onInfoClick: () => void;
+}) {
+ return (
+
+ }
+ onClick={onRenameClick}
+ >
+ Rename
+
+ }
+ onClick={onDuplicateClick}
+ >
+ Duplicate
+
+ }
+ onClick={onInfoClick}
+ >
+ Info
+
+
+ }
+ onClick={onDeleteClick}
+ >
+ Delete
+
+
+ );
+}
+
+function ProjectMenu({
+ isOpen,
+ onOpenChange,
+ variant = "grid",
+ onRenameClick,
+ onDuplicateClick,
+ onDeleteClick,
+ onInfoClick,
+}: {
+ isOpen: boolean;
+ onOpenChange: (open: boolean) => void;
+ variant?: "grid" | "list";
+ onRenameClick: () => void;
+ onDuplicateClick: () => void;
+ onDeleteClick: () => void;
+ onInfoClick: () => void;
+}) {
+ const handleMenuClick = ({
+ event,
+ }: {
+ event: MouseEvent;
+ }) => {
+ event.preventDefault();
+ event.stopPropagation();
+ };
+
+ const handleMenuKeyDown = ({
+ event,
+ }: {
+ event: KeyboardEvent;
+ }) => {
+ if (event.key !== "Enter" && event.key !== " ") {
+ return;
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ };
+
+ const handleRename = () => {
+ onRenameClick();
+ onOpenChange(false);
+ };
+
+ const handleDuplicate = () => {
+ onDuplicateClick();
+ onOpenChange(false);
+ };
+
+ const handleDeleteClick = () => {
+ onDeleteClick();
+ onOpenChange(false);
+ };
+
+ const handleInfoClick = () => {
+ onInfoClick();
+ onOpenChange(false);
+ };
+
+ const isGrid = variant === "grid";
+
+ return (
+
+
+
+
+
+
+
+ Rename
+
+
+
+ Duplicate
+
+
+
+ Info
+
+
+
+ Delete
+
+
+
+ );
+}
+
function ProjectsSkeleton() {
const skeletonIds = Array.from(
{ length: 24 },