From 7bf0984698e43b9aa2dd879f65951318ed410390 Mon Sep 17 00:00:00 2001
From: Maze <167211895+mazeincoding@users.noreply.github.com>
Date: Sat, 31 Jan 2026 00:20:04 +0100
Subject: codebase overhaul (#697)
---
.../editor/dialogs/delete-project-dialog.tsx | 84 ++++++++
.../components/editor/dialogs/migration-dialog.tsx | 44 ++++
.../editor/dialogs/project-info-dialog.tsx | 83 ++++++++
.../editor/dialogs/rename-project-dialog.tsx | 72 +++++++
.../components/editor/dialogs/shortcuts-dialog.tsx | 229 +++++++++++++++++++++
5 files changed, 512 insertions(+)
create mode 100644 apps/web/src/components/editor/dialogs/delete-project-dialog.tsx
create mode 100644 apps/web/src/components/editor/dialogs/migration-dialog.tsx
create mode 100644 apps/web/src/components/editor/dialogs/project-info-dialog.tsx
create mode 100644 apps/web/src/components/editor/dialogs/rename-project-dialog.tsx
create mode 100644 apps/web/src/components/editor/dialogs/shortcuts-dialog.tsx
(limited to 'apps/web/src/components/editor/dialogs')
diff --git a/apps/web/src/components/editor/dialogs/delete-project-dialog.tsx b/apps/web/src/components/editor/dialogs/delete-project-dialog.tsx
new file mode 100644
index 00000000..0086384e
--- /dev/null
+++ b/apps/web/src/components/editor/dialogs/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 (
+
+ );
+}
diff --git a/apps/web/src/components/editor/dialogs/migration-dialog.tsx b/apps/web/src/components/editor/dialogs/migration-dialog.tsx
new file mode 100644
index 00000000..4c9550c2
--- /dev/null
+++ b/apps/web/src/components/editor/dialogs/migration-dialog.tsx
@@ -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 (
+
+ );
+}
diff --git a/apps/web/src/components/editor/dialogs/project-info-dialog.tsx b/apps/web/src/components/editor/dialogs/project-info-dialog.tsx
new file mode 100644
index 00000000..7b736b23
--- /dev/null
+++ b/apps/web/src/components/editor/dialogs/project-info-dialog.tsx
@@ -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 (
+
+ {label}
+ {value}
+
+ );
+}
+
+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 (
+
+ );
+}
diff --git a/apps/web/src/components/editor/dialogs/rename-project-dialog.tsx b/apps/web/src/components/editor/dialogs/rename-project-dialog.tsx
new file mode 100644
index 00000000..73024d71
--- /dev/null
+++ b/apps/web/src/components/editor/dialogs/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 (
+
+ );
+}
diff --git a/apps/web/src/components/editor/dialogs/shortcuts-dialog.tsx b/apps/web/src/components/editor/dialogs/shortcuts-dialog.tsx
new file mode 100644
index 00000000..565fe705
--- /dev/null
+++ b/apps/web/src/components/editor/dialogs/shortcuts-dialog.tsx
@@ -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(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 (
+
+ );
+}
+
+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 (
+
+
+ {shortcut.icon && (
+
{shortcut.icon}
+ )}
+
{shortcut.description}
+
+
+ {displayKeys.map((key: string, index: number) => (
+
+
+ {key.split("+").map((keyPart: string, partIndex: number) => {
+ const keyId = `${shortcut.id}-${index}-${partIndex}`;
+ return (
+ onStartRecording({ shortcut })}
+ >
+ {keyPart}
+
+ );
+ })}
+
+ {index < displayKeys.length - 1 && (
+
or
+ )}
+
+ ))}
+
+
+ );
+}
+
+function EditableShortcutKey({
+ children,
+ isRecording,
+ onStartRecording,
+}: {
+ children: React.ReactNode;
+ isRecording: boolean;
+ onStartRecording: () => void;
+}) {
+ const handleClick = (e: React.MouseEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ onStartRecording();
+ };
+
+ return (
+
+ );
+}
--
cgit v1.3.1