diff --git a/src/App.tsx b/src/App.tsx
index 38e22b6..0ccf5bc 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -12,6 +12,7 @@ import { NewProjectModal, ProjectConfig } from './components/NewProjectModal';
import { ConsolePanel } from './components/ConsolePanel';
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from './components/ui/resizable';
import { useIsMobile } from './hooks/use-mobile';
+import { useKeyboardShortcuts } from './hooks/use-keyboard-shortcuts';
import { Tabs, TabsContent, TabsList, TabsTrigger } from './components/ui/tabs';
import { toast } from 'sonner';
import { EducationPanel } from './components/EducationPanel';
@@ -40,6 +41,63 @@ function App() {
initSentry();
}, []);
+ // Keyboard shortcuts
+ useKeyboardShortcuts([
+ {
+ key: 's',
+ meta: true, // Cmd on Mac
+ ctrl: true, // Ctrl on Windows/Linux
+ handler: () => {
+ toast.success('File saved automatically!');
+ captureEvent('keyboard_shortcut', { action: 'save' });
+ },
+ description: 'Save file',
+ },
+ {
+ key: 'p',
+ meta: true,
+ ctrl: true,
+ handler: () => {
+ // TODO: Implement file search modal
+ toast.info('File search coming soon! (Cmd/Ctrl+P)');
+ captureEvent('keyboard_shortcut', { action: 'file_search' });
+ },
+ description: 'Quick file search',
+ },
+ {
+ key: 'k',
+ meta: true,
+ ctrl: true,
+ handler: () => {
+ // TODO: Implement command palette
+ toast.info('Command palette coming soon! (Cmd/Ctrl+K)');
+ captureEvent('keyboard_shortcut', { action: 'command_palette' });
+ },
+ description: 'Command palette',
+ },
+ {
+ key: 'n',
+ meta: true,
+ ctrl: true,
+ handler: () => {
+ setShowNewProject(true);
+ captureEvent('keyboard_shortcut', { action: 'new_project' });
+ },
+ description: 'New project',
+ },
+ {
+ key: '/',
+ meta: true,
+ ctrl: true,
+ handler: () => {
+ // Monaco editor has built-in Cmd/Ctrl+F for find
+ toast.info('Use Cmd/Ctrl+F in the editor to find text');
+ captureEvent('keyboard_shortcut', { action: 'find' });
+ },
+ description: 'Find in editor',
+ },
+ ]);
+
const handleLoginSuccess = (user: { login: string; avatarUrl: string; email: string }) => {
setUser(user);
localStorage.setItem('aethex-user', JSON.stringify(user));
@@ -89,6 +147,40 @@ end)`,
const handleTemplateSelect = (templateCode: string) => {
setCode(templateCode);
setCurrentCode(templateCode);
+ // Update active file content
+ if (activeFileId) {
+ handleCodeChange(templateCode);
+ }
+ };
+
+ const handleCodeChange = (newCode: string) => {
+ setCurrentCode(newCode);
+ setCode(newCode);
+
+ // Update the file content in the files tree
+ if (activeFileId) {
+ setFiles((prev) => {
+ const updateFileContent = (nodes: FileNode[]): FileNode[] => {
+ return nodes.map((node) => {
+ if (node.id === activeFileId) {
+ return { ...node, content: newCode };
+ }
+ if (node.children) {
+ return { ...node, children: updateFileContent(node.children) };
+ }
+ return node;
+ });
+ };
+ return updateFileContent(prev || []);
+ });
+
+ // Also update in openFiles to keep tabs in sync
+ setOpenFiles((prev) =>
+ (prev || []).map((file) =>
+ file.id === activeFileId ? { ...file, content: newCode } : file
+ )
+ );
+ }
};
const handleFileSelect = (file: FileNode) => {
@@ -249,7 +341,7 @@ end)`,
onFileClose={handleFileClose}
/>
-
+
diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx
new file mode 100644
index 0000000..e2226cb
--- /dev/null
+++ b/src/components/ErrorBoundary.tsx
@@ -0,0 +1,107 @@
+import React, { Component, ErrorInfo, ReactNode } from 'react';
+import { Button } from './ui/button';
+import { Card } from './ui/card';
+import { AlertTriangle } from '@phosphor-icons/react';
+import { captureError } from '../lib/sentry';
+
+interface Props {
+ children: ReactNode;
+ fallback?: ReactNode;
+}
+
+interface State {
+ hasError: boolean;
+ error: Error | null;
+ errorInfo: ErrorInfo | null;
+}
+
+export class ErrorBoundary extends Component {
+ public state: State = {
+ hasError: false,
+ error: null,
+ errorInfo: null,
+ };
+
+ public static getDerivedStateFromError(error: Error): State {
+ return { hasError: true, error, errorInfo: null };
+ }
+
+ public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
+ console.error('Uncaught error:', error, errorInfo);
+
+ this.setState({
+ error,
+ errorInfo,
+ });
+
+ // Report to Sentry
+ if (typeof captureError === 'function') {
+ captureError(error, { extra: { errorInfo } });
+ }
+ }
+
+ private handleReset = () => {
+ this.setState({ hasError: false, error: null, errorInfo: null });
+ window.location.reload();
+ };
+
+ private handleResetWithoutReload = () => {
+ this.setState({ hasError: false, error: null, errorInfo: null });
+ };
+
+ public render() {
+ if (this.state.hasError) {
+ if (this.props.fallback) {
+ return this.props.fallback;
+ }
+
+ return (
+
+
+
+
+
+
Something went wrong
+
+ An unexpected error occurred in the application
+
+
+
+
+ {this.state.error && (
+
+
+ {this.state.error.toString()}
+
+ {this.state.errorInfo && (
+
+ {this.state.errorInfo.componentStack}
+
+ )}
+
+ )}
+
+
+
+
+
+
+
+ If this problem persists, please report it to the development team
+
+
+
+ );
+ }
+
+ return this.props.children;
+ }
+}
diff --git a/src/components/ui/loading-spinner.tsx b/src/components/ui/loading-spinner.tsx
new file mode 100644
index 0000000..a12dfcd
--- /dev/null
+++ b/src/components/ui/loading-spinner.tsx
@@ -0,0 +1,39 @@
+import { cn } from '@/lib/utils';
+
+interface LoadingSpinnerProps {
+ size?: 'sm' | 'md' | 'lg';
+ className?: string;
+}
+
+export function LoadingSpinner({ size = 'md', className }: LoadingSpinnerProps) {
+ const sizeClasses = {
+ sm: 'h-4 w-4 border-2',
+ md: 'h-8 w-8 border-2',
+ lg: 'h-12 w-12 border-3',
+ };
+
+ return (
+
+ Loading...
+
+ );
+}
+
+export function LoadingOverlay({ message = 'Loading...' }: { message?: string }) {
+ return (
+
+ );
+}
diff --git a/src/hooks/use-keyboard-shortcuts.ts b/src/hooks/use-keyboard-shortcuts.ts
new file mode 100644
index 0000000..c96964c
--- /dev/null
+++ b/src/hooks/use-keyboard-shortcuts.ts
@@ -0,0 +1,38 @@
+import { useEffect } from 'react';
+
+export interface KeyboardShortcut {
+ key: string;
+ ctrl?: boolean;
+ meta?: boolean;
+ shift?: boolean;
+ alt?: boolean;
+ handler: (e: KeyboardEvent) => void;
+ description?: string;
+}
+
+export function useKeyboardShortcuts(shortcuts: KeyboardShortcut[]) {
+ useEffect(() => {
+ if (typeof window === 'undefined') {
+ return;
+ }
+
+ const handleKeyDown = (e: KeyboardEvent) => {
+ for (const shortcut of shortcuts) {
+ const keyMatches = e.key.toLowerCase() === shortcut.key.toLowerCase();
+ const ctrlMatches = shortcut.ctrl === undefined || e.ctrlKey === shortcut.ctrl;
+ const metaMatches = shortcut.meta === undefined || e.metaKey === shortcut.meta;
+ const shiftMatches = shortcut.shift === undefined || e.shiftKey === shortcut.shift;
+ const altMatches = shortcut.alt === undefined || e.altKey === shortcut.alt;
+
+ if (keyMatches && ctrlMatches && metaMatches && shiftMatches && altMatches) {
+ e.preventDefault();
+ shortcut.handler(e);
+ break;
+ }
+ }
+ };
+
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [shortcuts]);
+}
diff --git a/src/main.tsx b/src/main.tsx
index 7821419..cd5b02d 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -1,16 +1,15 @@
import { createRoot } from 'react-dom/client'
-import { ErrorBoundary } from "react-error-boundary";
import "@github/spark/spark"
import App from './App'
-import { ErrorFallback } from './ErrorFallback'
+import { ErrorBoundary } from './components/ErrorBoundary'
import "./main.css"
import "./styles/theme.css"
import "./index.css"
createRoot(document.getElementById('root')!).render(
-
+
-
+
)