Add major feature improvements and developer experience enhancements
New Features: ✅ File Content Syncing - Code changes now persist to file tree (App.tsx) - Added handleCodeChange() to update file content in real-time - Syncs changes to both files state and openFiles tabs - Templates now properly update active file content ✅ Keyboard Shortcuts System (use-keyboard-shortcuts.ts) - Cmd/Ctrl+S - Save file notification - Cmd/Ctrl+P - Quick file search (placeholder) - Cmd/Ctrl+K - Command palette (placeholder) - Cmd/Ctrl+N - New project modal - Cmd/Ctrl+/ - Find in editor hint - Cross-platform support (Mac/Windows/Linux) - Integrated with PostHog analytics ✅ Enhanced Error Boundary (ErrorBoundary.tsx) - Better error UI with stack traces - Sentry integration for error reporting - Reload and retry options - User-friendly error messages - Replaced react-error-boundary with custom implementation ✅ Loading States Infrastructure (loading-spinner.tsx) - Reusable LoadingSpinner component (sm/md/lg sizes) - LoadingOverlay for full-screen loading - Accessible with ARIA labels - Ready for async operation improvements Developer Experience: - All keyboard shortcuts tracked via PostHog - Better error debugging with component stack traces - Auto-save functionality foundation This commit significantly improves core functionality and sets foundation for: - File search modal - Command palette - Enhanced async operation handling
This commit is contained in:
parent
281faf1395
commit
1b1466f4ec
5 changed files with 280 additions and 5 deletions
94
src/App.tsx
94
src/App.tsx
|
|
@ -12,6 +12,7 @@ import { NewProjectModal, ProjectConfig } from './components/NewProjectModal';
|
||||||
import { ConsolePanel } from './components/ConsolePanel';
|
import { ConsolePanel } from './components/ConsolePanel';
|
||||||
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from './components/ui/resizable';
|
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from './components/ui/resizable';
|
||||||
import { useIsMobile } from './hooks/use-mobile';
|
import { useIsMobile } from './hooks/use-mobile';
|
||||||
|
import { useKeyboardShortcuts } from './hooks/use-keyboard-shortcuts';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from './components/ui/tabs';
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from './components/ui/tabs';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { EducationPanel } from './components/EducationPanel';
|
import { EducationPanel } from './components/EducationPanel';
|
||||||
|
|
@ -40,6 +41,63 @@ function App() {
|
||||||
initSentry();
|
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 }) => {
|
const handleLoginSuccess = (user: { login: string; avatarUrl: string; email: string }) => {
|
||||||
setUser(user);
|
setUser(user);
|
||||||
localStorage.setItem('aethex-user', JSON.stringify(user));
|
localStorage.setItem('aethex-user', JSON.stringify(user));
|
||||||
|
|
@ -89,6 +147,40 @@ end)`,
|
||||||
const handleTemplateSelect = (templateCode: string) => {
|
const handleTemplateSelect = (templateCode: string) => {
|
||||||
setCode(templateCode);
|
setCode(templateCode);
|
||||||
setCurrentCode(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) => {
|
const handleFileSelect = (file: FileNode) => {
|
||||||
|
|
@ -249,7 +341,7 @@ end)`,
|
||||||
onFileClose={handleFileClose}
|
onFileClose={handleFileClose}
|
||||||
/>
|
/>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<CodeEditor onCodeChange={setCurrentCode} />
|
<CodeEditor onCodeChange={handleCodeChange} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="ai" className="flex-1 m-0">
|
<TabsContent value="ai" className="flex-1 m-0">
|
||||||
|
|
|
||||||
107
src/components/ErrorBoundary.tsx
Normal file
107
src/components/ErrorBoundary.tsx
Normal file
|
|
@ -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<Props, State> {
|
||||||
|
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 (
|
||||||
|
<div className="h-screen w-screen flex items-center justify-center bg-background p-4">
|
||||||
|
<Card className="max-w-2xl w-full p-8">
|
||||||
|
<div className="flex items-center gap-4 mb-6">
|
||||||
|
<AlertTriangle className="text-destructive" size={48} weight="fill" />
|
||||||
|
<div>
|
||||||
|
<h1 className="text-2xl font-bold">Something went wrong</h1>
|
||||||
|
<p className="text-muted-foreground mt-1">
|
||||||
|
An unexpected error occurred in the application
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{this.state.error && (
|
||||||
|
<div className="bg-muted p-4 rounded-lg mb-6 overflow-auto max-h-64">
|
||||||
|
<p className="font-mono text-sm text-destructive font-semibold mb-2">
|
||||||
|
{this.state.error.toString()}
|
||||||
|
</p>
|
||||||
|
{this.state.errorInfo && (
|
||||||
|
<pre className="font-mono text-xs text-muted-foreground whitespace-pre-wrap">
|
||||||
|
{this.state.errorInfo.componentStack}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<Button onClick={this.handleReset} className="flex-1">
|
||||||
|
Reload Application
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={this.handleResetWithoutReload}
|
||||||
|
variant="outline"
|
||||||
|
className="flex-1"
|
||||||
|
>
|
||||||
|
Try Again
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-xs text-muted-foreground mt-4 text-center">
|
||||||
|
If this problem persists, please report it to the development team
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/components/ui/loading-spinner.tsx
Normal file
39
src/components/ui/loading-spinner.tsx
Normal file
|
|
@ -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 (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
'animate-spin rounded-full border-accent border-t-transparent',
|
||||||
|
sizeClasses[size],
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
role="status"
|
||||||
|
aria-label="Loading"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LoadingOverlay({ message = 'Loading...' }: { message?: string }) {
|
||||||
|
return (
|
||||||
|
<div className="absolute inset-0 bg-background/80 backdrop-blur-sm flex items-center justify-center z-50">
|
||||||
|
<div className="flex flex-col items-center gap-4">
|
||||||
|
<LoadingSpinner size="lg" />
|
||||||
|
<p className="text-sm text-muted-foreground">{message}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
38
src/hooks/use-keyboard-shortcuts.ts
Normal file
38
src/hooks/use-keyboard-shortcuts.ts
Normal file
|
|
@ -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]);
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
import { ErrorBoundary } from "react-error-boundary";
|
|
||||||
import "@github/spark/spark"
|
import "@github/spark/spark"
|
||||||
|
|
||||||
import App from './App'
|
import App from './App'
|
||||||
import { ErrorFallback } from './ErrorFallback'
|
import { ErrorBoundary } from './components/ErrorBoundary'
|
||||||
|
|
||||||
import "./main.css"
|
import "./main.css"
|
||||||
import "./styles/theme.css"
|
import "./styles/theme.css"
|
||||||
import "./index.css"
|
import "./index.css"
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
<ErrorBoundary>
|
||||||
<App />
|
<App />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue