Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions apps/web/src/app/projects/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { Checkbox } from "@/components/ui/checkbox";
import { Input } from "@/components/ui/input";
import { Skeleton } from "@/components/ui/skeleton";
import { useEditor } from "@/hooks/use-editor";
import { useFileUpload } from "@/hooks/use-file-upload";
import { useProjectsStore } from "./store";
import type {
TProjectMetadata,
Expand Down Expand Up @@ -45,6 +46,8 @@ import {
Edit03Icon,
ArrowDown02Icon,
InformationCircleIcon,
Download01Icon,
Upload01Icon,
} from "@hugeicons/core-free-icons";
import { OcVideoIcon } from "@/components/icons";
import { Label } from "@/components/ui/label";
Expand Down Expand Up @@ -183,6 +186,7 @@ function ProjectsHeader() {

<div className="flex items-center gap-3 md:gap-4">
<SearchBar className="hidden md:block" />
<ImportProjectButton />
<NewProjectButton />
</div>
</div>
Expand Down Expand Up @@ -526,6 +530,48 @@ function NewProjectButton() {
);
}

function ImportProjectButton() {
const editor = useEditor();
const router = useRouter();

const { openFilePicker, fileInputProps } = useFileUpload({
accept: ".json,.opencut.json",
multiple: false,
onFilesSelected: async (files) => {
const file = files[0];
if (!file) return;

try {
const json = await file.text();
const projectId = await editor.project.importProjectFromJSON({ json });
if (projectId) {
router.push(`/editor/${projectId}`);
}
} catch (error) {
toast.error("Failed to read file", {
description:
error instanceof Error ? error.message : "Please try again",
});
}
},
});

return (
<>
<input {...fileInputProps} />
<Button
size="lg"
variant="outline"
className="flex px-5 md:px-6"
onClick={openFilePicker}
>
<HugeiconsIcon icon={Upload01Icon} className="size-4" />
<span className="text-sm font-medium hidden md:block">Import</span>
</Button>
</>
);
}

function ProjectItem({
project,
allProjectIds,
Expand Down Expand Up @@ -557,6 +603,9 @@ function ProjectItem({
};
const handleDeleteClick = () => setIsDeleteDialogOpen(true);
const handleInfoClick = () => setIsInfoDialogOpen(true);
const handleExportJSON = async () => {
await editor.project.exportProjectByIdAsJSON({ id: project.id });
};
const handleDeleteConfirm = async () => {
await deleteProjects({ editor, ids: [project.id] });
setIsDeleteDialogOpen(false);
Expand Down Expand Up @@ -676,6 +725,7 @@ function ProjectItem({
onDuplicateClick={handleDuplicate}
onDeleteClick={handleDeleteClick}
onInfoClick={handleInfoClick}
onExportJSONClick={handleExportJSON}
/>
)}
</div>
Expand Down Expand Up @@ -717,6 +767,7 @@ function ProjectItem({
onDuplicateClick={handleDuplicate}
onDeleteClick={handleDeleteClick}
onInfoClick={handleInfoClick}
onExportJSONClick={handleExportJSON}
/>
)}
</>
Expand All @@ -730,6 +781,7 @@ function ProjectItem({
onDuplicateClick={handleDuplicate}
onDeleteClick={handleDeleteClick}
onInfoClick={handleInfoClick}
onExportJSONClick={handleExportJSON}
/>
</ContextMenu>

Expand Down Expand Up @@ -764,11 +816,13 @@ function ProjectContextMenuContent({
onDuplicateClick,
onDeleteClick,
onInfoClick,
onExportJSONClick,
}: {
onRenameClick: () => void;
onDuplicateClick: () => void;
onDeleteClick: () => void;
onInfoClick: () => void;
onExportJSONClick: () => void;
}) {
return (
<ContextMenuContent>
Expand All @@ -784,6 +838,12 @@ function ProjectContextMenuContent({
>
Duplicate
</ContextMenuItem>
<ContextMenuItem
icon={<HugeiconsIcon icon={Download01Icon} />}
onClick={onExportJSONClick}
>
Export as JSON
</ContextMenuItem>
<ContextMenuItem
icon={<HugeiconsIcon icon={InformationCircleIcon} />}
onClick={onInfoClick}
Expand All @@ -810,6 +870,7 @@ function ProjectMenu({
onDuplicateClick,
onDeleteClick,
onInfoClick,
onExportJSONClick,
}: {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
Expand All @@ -818,6 +879,7 @@ function ProjectMenu({
onDuplicateClick: () => void;
onDeleteClick: () => void;
onInfoClick: () => void;
onExportJSONClick: () => void;
}) {
const handleMenuClick = ({
event,
Expand Down Expand Up @@ -860,6 +922,11 @@ function ProjectMenu({
onOpenChange(false);
};

const handleExportJSON = () => {
onExportJSONClick();
onOpenChange(false);
};

const isGrid = variant === "grid";

return (
Expand Down Expand Up @@ -902,6 +969,10 @@ function ProjectMenu({
<HugeiconsIcon icon={Copy01Icon} />
Duplicate
</DropdownMenuItem>
<DropdownMenuItem onClick={handleExportJSON}>
<HugeiconsIcon icon={Download01Icon} />
Export as JSON
</DropdownMenuItem>
<DropdownMenuItem onClick={handleInfoClick}>
<HugeiconsIcon icon={InformationCircleIcon} />
Info
Expand Down
9 changes: 8 additions & 1 deletion apps/web/src/components/editor/editor-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { ThemeToggle } from "../theme-toggle";
import { DEFAULT_LOGO_URL, SOCIAL_LINKS } from "@/constants/site-constants";
import { toast } from "sonner";
import { useEditor } from "@/hooks/use-editor";
import { CommandIcon, Logout05Icon } from "@hugeicons/core-free-icons";
import { CommandIcon, Download01Icon, Logout05Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";
import { ShortcutsDialog } from "./dialogs/shortcuts-dialog";
import Image from "next/image";
Expand Down Expand Up @@ -127,6 +127,13 @@ function ProjectDropdown() {
Exit project
</DropdownMenuItem>

<DropdownMenuItem
onClick={() => editor.project.exportProjectAsJSON()}
icon={<HugeiconsIcon icon={Download01Icon} />}
>
Export as JSON
</DropdownMenuItem>

<DropdownMenuItem
onClick={() => setOpenDialog("shortcuts")}
icon={<HugeiconsIcon icon={CommandIcon} />}
Expand Down
Loading