summaryrefslogtreecommitdiff
path: root/apps/web/src/project/components
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/project/components')
-rw-r--r--apps/web/src/project/components/delete-project-dialog.tsx84
-rw-r--r--apps/web/src/project/components/migration-dialog.tsx44
-rw-r--r--apps/web/src/project/components/project-info-dialog.tsx81
-rw-r--r--apps/web/src/project/components/rename-project-dialog.tsx72
4 files changed, 281 insertions, 0 deletions
diff --git a/apps/web/src/project/components/delete-project-dialog.tsx b/apps/web/src/project/components/delete-project-dialog.tsx
new file mode 100644
index 00000000..0086384e
--- /dev/null
+++ b/apps/web/src/project/components/delete-project-dialog.tsx
@@ -0,0 +1,84 @@
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogBody,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+import { Label } from "@/components/ui/label";
+import { Input } from "@/components/ui/input";
+
+export function DeleteProjectDialog({
+ isOpen,
+ onOpenChange,
+ onConfirm,
+ projectNames,
+}: {
+ isOpen: boolean;
+ onOpenChange: (open: boolean) => void;
+ onConfirm: () => void;
+ projectNames: string[];
+}) {
+ const count = projectNames.length;
+ const isSingle = count === 1;
+ const singleName = isSingle ? projectNames[0] : null;
+
+ return (
+ <Dialog open={isOpen} onOpenChange={onOpenChange}>
+ <DialogContent
+ onOpenAutoFocus={(event) => {
+ event.preventDefault();
+ event.stopPropagation();
+ }}
+ >
+ <DialogHeader>
+ <DialogTitle>
+ {singleName ? (
+ <>
+ {"Delete '"}
+ <span className="inline-block max-w-[300px] truncate align-bottom">
+ {singleName}
+ </span>
+ {"'?"}
+ </>
+ ) : (
+ `Delete ${count} projects?`
+ )}
+ </DialogTitle>
+ </DialogHeader>
+ <DialogBody>
+ <Alert variant="destructive">
+ <AlertTitle>Warning</AlertTitle>
+ <AlertDescription>
+ This will permanently delete{" "}
+ {singleName ? `"${singleName}"` : `${count} projects`} and all
+ associated files.
+ </AlertDescription>
+ </Alert>
+ <div className="flex flex-col gap-3">
+ <Label className="text-xs font-semibold text-slate-500">
+ Type "DELETE" to confirm
+ </Label>
+ <Input
+ type="text"
+ placeholder="DELETE"
+ size="lg"
+ variant="destructive"
+ />
+ </div>
+ </DialogBody>
+ <DialogFooter>
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
+ Cancel
+ </Button>
+ <Button variant="destructive" onClick={onConfirm}>
+ Delete project
+ </Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ );
+}
diff --git a/apps/web/src/project/components/migration-dialog.tsx b/apps/web/src/project/components/migration-dialog.tsx
new file mode 100644
index 00000000..4aac26d5
--- /dev/null
+++ b/apps/web/src/project/components/migration-dialog.tsx
@@ -0,0 +1,44 @@
+"use client";
+
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { useEditor } from "@/editor/use-editor";
+import { Loader2 } from "lucide-react";
+
+export function MigrationDialog() {
+ const editor = useEditor();
+ const migrationState = editor.project.getMigrationState();
+
+ if (!migrationState.isMigrating) return null;
+
+ const title = migrationState.projectName
+ ? "Updating project"
+ : "Updating projects";
+ const description = migrationState.projectName
+ ? `Upgrading "${migrationState.projectName}" from v${migrationState.fromVersion} to v${migrationState.toVersion}`
+ : `Upgrading projects from v${migrationState.fromVersion} to v${migrationState.toVersion}`;
+
+ return (
+ <Dialog open={true}>
+ <DialogContent
+ className="sm:max-w-md"
+ onPointerDownOutside={(event) => event.preventDefault()}
+ onEscapeKeyDown={(event) => event.preventDefault()}
+ >
+ <DialogHeader>
+ <DialogTitle>{title}</DialogTitle>
+ <DialogDescription>{description}</DialogDescription>
+ </DialogHeader>
+
+ <div className="flex items-center justify-center py-4">
+ <Loader2 className="text-muted-foreground size-8 animate-spin" />
+ </div>
+ </DialogContent>
+ </Dialog>
+ );
+}
diff --git a/apps/web/src/project/components/project-info-dialog.tsx b/apps/web/src/project/components/project-info-dialog.tsx
new file mode 100644
index 00000000..f0ea364e
--- /dev/null
+++ b/apps/web/src/project/components/project-info-dialog.tsx
@@ -0,0 +1,81 @@
+import {
+ Dialog,
+ DialogBody,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import type { TProjectMetadata } from "@/project/types";
+import { formatDate } from "@/utils/date";
+import { formatTimecode, mediaTimeToSeconds } from "opencut-wasm";
+import { Button } from "@/components/ui/button";
+
+function InfoRow({
+ label,
+ value,
+}: {
+ label: string;
+ value: string | React.ReactNode;
+}) {
+ return (
+ <div className="flex justify-between items-center py-0 last:pb-0">
+ <span className="text-muted-foreground text-sm">{label}</span>
+ <span className="text-sm font-medium">{value}</span>
+ </div>
+ );
+}
+
+export function ProjectInfoDialog({
+ isOpen,
+ onOpenChange,
+ project,
+}: {
+ isOpen: boolean;
+ onOpenChange: (open: boolean) => void;
+ project: TProjectMetadata;
+}) {
+ const durationSeconds = mediaTimeToSeconds({ time: project.duration });
+ const durationFormatted =
+ project.duration > 0
+ ? (formatTimecode({ time: project.duration, format: durationSeconds >= 3600 ? "HH:MM:SS" : "MM:SS" }) ?? "")
+ : "0:00";
+
+ return (
+ <Dialog open={isOpen} onOpenChange={onOpenChange}>
+ <DialogContent onOpenAutoFocus={(event) => event.preventDefault()}>
+ <DialogHeader>
+ <DialogTitle className="truncate max-w-[350px]">
+ {project.name}
+ </DialogTitle>
+ </DialogHeader>
+
+ <DialogBody className="flex flex-col">
+ <InfoRow label="Duration" value={durationFormatted} />
+ <InfoRow
+ label="Created"
+ value={formatDate({ date: project.createdAt })}
+ />
+ <InfoRow
+ label="Modified"
+ value={formatDate({ date: project.updatedAt })}
+ />
+ <InfoRow
+ label="Project ID"
+ value={
+ <code className="text-xs bg-muted px-1.5 py-0.5 rounded">
+ {project.id.slice(0, 8)}
+ </code>
+ }
+ />
+ </DialogBody>
+ <DialogFooter>
+ <Button variant="outline" onClick={() => onOpenChange(false)}>
+ Close
+ </Button>
+ <Button onClick={() => onOpenChange(false)}>Done</Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ );
+}
diff --git a/apps/web/src/project/components/rename-project-dialog.tsx b/apps/web/src/project/components/rename-project-dialog.tsx
new file mode 100644
index 00000000..73024d71
--- /dev/null
+++ b/apps/web/src/project/components/rename-project-dialog.tsx
@@ -0,0 +1,72 @@
+import { Button } from "@/components/ui/button";
+import {
+ Dialog,
+ DialogBody,
+ DialogContent,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+} from "@/components/ui/dialog";
+import { Input } from "@/components/ui/input";
+import { useState } from "react";
+import { Label } from "@/components/ui/label";
+
+export function RenameProjectDialog({
+ isOpen,
+ onOpenChange,
+ onConfirm,
+ projectName,
+}: {
+ isOpen: boolean;
+ onOpenChange: (open: boolean) => void;
+ onConfirm: (newName: string) => void;
+ projectName: string;
+}) {
+ const [name, setName] = useState(projectName);
+
+ const handleOpenChange = (open: boolean) => {
+ if (open) {
+ setName(projectName);
+ }
+ onOpenChange(open);
+ };
+
+ return (
+ <Dialog open={isOpen} onOpenChange={handleOpenChange}>
+ <DialogContent>
+ <DialogHeader>
+ <DialogTitle>Rename project</DialogTitle>
+ </DialogHeader>
+
+ <DialogBody className="gap-3">
+ <Label>New name</Label>
+ <Input
+ value={name}
+ onChange={(e) => setName(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter") {
+ e.preventDefault();
+ onConfirm(name);
+ }
+ }}
+ placeholder="Enter a new name"
+ />
+ </DialogBody>
+
+ <DialogFooter>
+ <Button
+ variant="outline"
+ onClick={(e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ onOpenChange(false);
+ }}
+ >
+ Cancel
+ </Button>
+ <Button onClick={() => onConfirm(name)}>Rename</Button>
+ </DialogFooter>
+ </DialogContent>
+ </Dialog>
+ );
+}