organize dialogs

This commit is contained in:
Maze Winther
2026-01-27 17:02:48 +01:00
parent 0a7dc295f8
commit 94d4ee0c8c
8 changed files with 15 additions and 15 deletions
@@ -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>
);
}
@@ -0,0 +1,44 @@
"use client";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { useEditor } from "@/hooks/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>
);
}
@@ -0,0 +1,83 @@
import {
Dialog,
DialogBody,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import type { TProjectMetadata } from "@/types/project";
import { formatDate } from "@/utils/date";
import { formatTimeCode } from "@/lib/time";
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 durationFormatted =
project.duration > 0
? formatTimeCode({
timeInSeconds: project.duration,
format: project.duration >= 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>
);
}
@@ -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>
);
}
@@ -0,0 +1,229 @@
"use client";
import { useEffect, useState } from "react";
import { toast } from "sonner";
import {
type KeyboardShortcut,
useKeyboardShortcutsHelp,
} from "@/hooks/use-keyboard-shortcuts-help";
import { useKeybindingsStore } from "@/stores/keybindings-store";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogBody,
DialogContent,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
export function ShortcutsDialog({
isOpen,
onOpenChange,
}: {
isOpen: boolean;
onOpenChange: (open: boolean) => void;
}) {
const [recordingShortcut, setRecordingShortcut] =
useState<KeyboardShortcut | null>(null);
const {
updateKeybinding,
removeKeybinding,
getKeybindingString,
validateKeybinding,
getKeybindingsForAction,
setIsRecording,
resetToDefaults,
isRecording,
} = useKeybindingsStore();
const { shortcuts } = useKeyboardShortcutsHelp();
const categories = Array.from(new Set(shortcuts.map((s) => s.category)));
useEffect(() => {
if (!isRecording || !recordingShortcut) return;
const handleKeyDown = (e: KeyboardEvent) => {
e.preventDefault();
e.stopPropagation();
const keyString = getKeybindingString(e);
if (keyString) {
const conflict = validateKeybinding(
keyString,
recordingShortcut.action,
);
if (conflict) {
toast.error(
`Key "${keyString}" is already bound to "${conflict.existingAction}"`,
);
setRecordingShortcut(null);
return;
}
const oldKeys = getKeybindingsForAction(recordingShortcut.action);
for (const key of oldKeys) {
removeKeybinding(key);
}
updateKeybinding(keyString, recordingShortcut.action);
setIsRecording(false);
setRecordingShortcut(null);
}
};
const handleClickOutside = () => {
setRecordingShortcut(null);
setIsRecording(false);
};
document.addEventListener("keydown", handleKeyDown);
document.addEventListener("click", handleClickOutside);
return () => {
document.removeEventListener("keydown", handleKeyDown);
document.removeEventListener("click", handleClickOutside);
};
}, [
recordingShortcut,
getKeybindingString,
updateKeybinding,
removeKeybinding,
validateKeybinding,
getKeybindingsForAction,
setIsRecording,
isRecording,
]);
const handleStartRecording = (shortcut: KeyboardShortcut) => {
setRecordingShortcut(shortcut);
setIsRecording(true);
};
return (
<Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="flex max-h-[80vh] max-w-2xl flex-col p-0">
<DialogHeader>
<DialogTitle>Keyboard shortcuts</DialogTitle>
</DialogHeader>
<DialogBody className="scrollbar-thin flex-grow overflow-y-auto">
<div className="flex flex-col gap-6">
{categories.map((category) => (
<div key={category} className="flex flex-col gap-1">
<h3 className="text-muted-foreground text-xs font-medium tracking-wide uppercase">
{category}
</h3>
<div className="flex flex-col gap-1">
{shortcuts
.filter((shortcut) => shortcut.category === category)
.map((shortcut) => (
<ShortcutItem
key={shortcut.action}
shortcut={shortcut}
isRecording={
shortcut.action === recordingShortcut?.action
}
onStartRecording={() => handleStartRecording(shortcut)}
/>
))}
</div>
</div>
))}
</div>
</DialogBody>
<DialogFooter>
<Button variant="destructive" onClick={resetToDefaults}>
Reset to default
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
function ShortcutItem({
shortcut,
isRecording,
onStartRecording,
}: {
shortcut: KeyboardShortcut;
isRecording: boolean;
onStartRecording: (params: { shortcut: KeyboardShortcut }) => void;
}) {
const displayKeys = shortcut.keys.filter((key: string) => {
if (
key.includes("Cmd") &&
shortcut.keys.includes(key.replace("Cmd", "Ctrl"))
)
return false;
return true;
});
return (
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
{shortcut.icon && (
<div className="text-muted-foreground">{shortcut.icon}</div>
)}
<span className="text-sm">{shortcut.description}</span>
</div>
<div className="flex items-center gap-2">
{displayKeys.map((key: string, index: number) => (
<div key={key} className="flex items-center gap-2">
<div className="flex items-center gap-1">
{key.split("+").map((keyPart: string, partIndex: number) => {
const keyId = `${shortcut.id}-${index}-${partIndex}`;
return (
<EditableShortcutKey
key={keyId}
isRecording={isRecording}
onStartRecording={() => onStartRecording({ shortcut })}
>
{keyPart}
</EditableShortcutKey>
);
})}
</div>
{index < displayKeys.length - 1 && (
<span className="text-muted-foreground text-xs">or</span>
)}
</div>
))}
</div>
</div>
);
}
function EditableShortcutKey({
children,
isRecording,
onStartRecording,
}: {
children: React.ReactNode;
isRecording: boolean;
onStartRecording: () => void;
}) {
const handleClick = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
onStartRecording();
};
return (
<Button
variant="outline"
size="sm"
onClick={handleClick}
title={
isRecording ? "Press any key combination..." : "Click to edit shortcut"
}
>
{children}
</Button>
);
}