Improve error handling across components

- Add try-catch blocks for localStorage operations (user state, login, signout)
- Add validation and error handling for file operations (create, rename, delete, move)
- Add error handling for project creation with validation
- Improve error logging with Sentry context in AIChat
- Add loading state and error handling for Monaco editor
- Add user-friendly error messages via toast notifications
- Add console.error logging for debugging
- Validate inputs (empty names, etc.) before processing
This commit is contained in:
Claude 2026-01-17 22:28:47 +00:00
parent ebd535f106
commit d43e0a3a27
No known key found for this signature in database
5 changed files with 754 additions and 144 deletions

268
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,268 @@
# Contributing to AeThex Studio
Thank you for your interest in contributing to AeThex Studio! We welcome contributions from the community.
## Getting Started
1. **Fork the repository** on GitHub
2. **Clone your fork** locally:
```bash
git clone https://github.com/YOUR_USERNAME/aethex-studio.git
cd aethex-studio
```
3. **Install dependencies**:
```bash
npm install
```
4. **Create a branch** for your feature:
```bash
git checkout -b feature/my-amazing-feature
```
## Development Workflow
### Running the Development Server
```bash
npm run dev
```
Visit `http://localhost:3000` to see your changes in real-time.
### Code Style
- We use **TypeScript** for type safety
- Follow existing code patterns and conventions
- Use **ESLint** to check your code:
```bash
npm run lint
```
### Testing
Before submitting a PR, ensure all tests pass:
```bash
# Run all tests
npm test
# Run tests in watch mode during development
npm run test:watch
# Check test coverage
npm run test:coverage
```
#### Writing Tests
- Place tests in `src/components/__tests__/` or `src/hooks/__tests__/`
- Use descriptive test names
- Test both success and error cases
- Example:
```typescript
describe('MyComponent', () => {
it('should render correctly', () => {
// Your test here
});
it('should handle errors gracefully', () => {
// Your test here
});
});
```
## Types of Contributions
### 🐛 Bug Fixes
1. Check if the bug is already reported in [Issues](https://github.com/AeThex-LABS/aethex-studio/issues)
2. If not, create a new issue with:
- Clear description
- Steps to reproduce
- Expected vs actual behavior
- Screenshots (if applicable)
3. Create a fix and submit a PR
### ✨ New Features
1. **Discuss first** - Open an issue to discuss the feature before implementing
2. Wait for approval from maintainers
3. Implement the feature
4. Add tests
5. Update documentation
6. Submit a PR
### 📝 Documentation
- Fix typos
- Improve clarity
- Add examples
- Update outdated information
### 🎨 UI/UX Improvements
- Follow the existing design system
- Ensure responsive design (mobile + desktop)
- Test accessibility
- Add screenshots to PR
## Pull Request Process
### Before Submitting
- [ ] Code builds without errors (`npm run build`)
- [ ] All tests pass (`npm test`)
- [ ] Linting passes (`npm run lint`)
- [ ] Code is well-commented
- [ ] Documentation is updated
- [ ] Commit messages are clear and descriptive
### Commit Messages
Use clear, descriptive commit messages:
```bash
# Good
git commit -m "Add drag-and-drop support for file tree"
git commit -m "Fix search highlighting bug in SearchPanel"
# Bad
git commit -m "Update stuff"
git commit -m "Fix bug"
```
### Submitting
1. Push your branch to your fork
2. Open a PR against the `main` branch
3. Fill out the PR template
4. Wait for review
### PR Template
```markdown
## Description
Brief description of what this PR does
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring
## Testing
Describe how you tested this change
## Screenshots (if applicable)
Add screenshots here
## Checklist
- [ ] Tests pass
- [ ] Linting passes
- [ ] Documentation updated
- [ ] Responsive on mobile
```
## Code Organization
### File Structure
```
src/
├── components/ # React components
│ ├── ui/ # Reusable UI components
│ ├── __tests__/ # Component tests
│ └── ComponentName.tsx
├── hooks/ # Custom React hooks
│ ├── __tests__/ # Hook tests
│ └── use-hook-name.ts
├── lib/ # Utilities and helpers
│ └── helpers.ts
└── App.tsx # Main application
```
### Naming Conventions
- **Components**: PascalCase (`FileTree.tsx`, `AIChat.tsx`)
- **Hooks**: camelCase with `use` prefix (`use-theme.ts`, `use-keyboard-shortcuts.ts`)
- **Utilities**: camelCase (`helpers.ts`, `validators.ts`)
- **Tests**: Match component name with `.test.tsx` or `.test.ts`
## Adding New Features
### Adding a New Template
1. Edit `src/lib/templates.ts`
2. Add your template to the `templates` array:
```typescript
{
id: 'your-template-id',
name: 'Template Name',
description: 'Brief description',
category: 'beginner' | 'gameplay' | 'ui' | 'tools' | 'advanced',
code: `-- Your Lua code here`,
}
```
### Adding a New Component
1. Create the component file in `src/components/`
2. Write the component with TypeScript types
3. Add tests in `src/components/__tests__/`
4. Export from the component file
5. Import and use in `App.tsx` or parent component
### Adding a Keyboard Shortcut
1. Edit `src/App.tsx`
2. Add to the `useKeyboardShortcuts` array:
```typescript
{
key: 'x',
meta: true,
ctrl: true,
handler: () => {
// Your action here
},
description: 'Description of shortcut',
}
```
3. Update README.md keyboard shortcuts table
## Performance Guidelines
- Use React.lazy() for code splitting
- Memoize expensive computations with useMemo()
- Use useCallback() for function props
- Optimize re-renders with React.memo()
- Lazy load images and heavy components
## Accessibility Guidelines
- Add proper ARIA labels
- Ensure keyboard navigation works
- Test with screen readers
- Maintain good color contrast
- Add focus indicators
## Questions?
- Open an issue for questions
- Join our community discussions
- Check existing issues and PRs
## Code of Conduct
- Be respectful and inclusive
- Welcome newcomers
- Give constructive feedback
- Focus on the code, not the person
## License
By contributing, you agree that your contributions will be licensed under the MIT License.
---
Thank you for contributing to AeThex Studio! 🎉

270
README.md
View file

@ -1,23 +1,261 @@
# ✨ Welcome to Your Spark Template!
You've just launched your brand-new Spark Template Codespace — everythings fired up and ready for you to explore, build, and create with Spark!
# AeThex Studio
This template is your blank canvas. It comes with a minimal setup to help you get started quickly with Spark development.
A powerful, browser-based IDE specifically designed for Roblox Lua development with AI assistance, modern tooling, and an intuitive interface.
🚀 What's Inside?
- A clean, minimal Spark environment
- Pre-configured for local development
- Ready to scale with your ideas
🧠 What Can You Do?
![AeThex Studio](https://img.shields.io/badge/version-1.0.0-blue.svg) ![License](https://img.shields.io/badge/license-MIT-green.svg) ![Next.js](https://img.shields.io/badge/Next.js-14.2-black.svg) ![React](https://img.shields.io/badge/React-18.3-blue.svg)
Right now, this is just a starting point — the perfect place to begin building and testing your Spark applications.
## ✨ Features
🧹 Just Exploring?
No problem! If you were just checking things out and dont need to keep this code:
### 🎨 **Modern Code Editor**
- **Monaco Editor** - The same editor that powers VS Code
- **Lua syntax highlighting** with autocomplete
- **Real-time code validation** and linting
- **Multi-file editing** with tab management
- **File tree navigation** with drag-and-drop organization
- Simply delete your Spark.
- Everything will be cleaned up — no traces left behind.
### 🤖 **AI-Powered Assistant**
- Built-in AI chat for code help and debugging
- Context-aware suggestions
- Code explanation and documentation
- Roblox API knowledge
📄 License For Spark Template Resources
### 📁 **Project Management**
- **File Tree** - Organize your scripts into folders
- **Drag-and-drop** - Rearrange files easily
- **Quick file search** (Cmd/Ctrl+P) - Find files instantly
- **Search in files** (Cmd/Ctrl+Shift+F) - Global text search
The Spark Template files and resources from GitHub are licensed under the terms of the MIT license, Copyright GitHub, Inc.
### 🎯 **Productivity Features**
- **25+ Code Templates** - Ready-made scripts for common tasks
- Beginner templates (Hello World, Touch Detectors, etc.)
- Gameplay systems (DataStore, Teams, Combat, etc.)
- UI components (GUIs, Timers, etc.)
- Advanced features (Pathfinding, Inventory, etc.)
- **Command Palette** (Cmd/Ctrl+K) - Quick access to all commands
- **Keyboard Shortcuts** - Professional IDE shortcuts
- **Code Preview** - Test your scripts instantly
### 🎨 **Customization**
- **5 Beautiful Themes**:
- **Dark** - Classic dark theme for comfortable coding
- **Light** - Clean light theme for bright environments
- **Synthwave** - Retro neon aesthetic
- **Forest** - Calming green tones
- **Ocean** - Deep blue theme
- **Persistent preferences** - Your settings are saved
### 📱 **Mobile Responsive**
- Optimized layouts for phones and tablets
- Touch-friendly controls
- Hamburger menu for mobile
- Collapsible panels
### 🚀 **Developer Experience**
- **Code splitting** for fast loading
- **Error boundaries** with graceful error handling
- **Loading states** with spinners
- **Toast notifications** for user feedback
- **Testing infrastructure** with Vitest
## 🎮 Perfect For
- **Roblox Developers** - Build game scripts faster
- **Students & Learners** - Learn Lua with templates and AI help
- **Rapid Prototyping** - Quickly test game ideas
- **Web-Based Development** - Code anywhere, no installation needed
## ⌨️ Keyboard Shortcuts
| Shortcut | Action |
|----------|--------|
| `Cmd/Ctrl + S` | Save file (auto-save enabled) |
| `Cmd/Ctrl + P` | Quick file search |
| `Cmd/Ctrl + K` | Command palette |
| `Cmd/Ctrl + N` | New project |
| `Cmd/Ctrl + F` | Find in editor |
| `Cmd/Ctrl + Shift + F` | Search in all files |
## 🚀 Getting Started
### Prerequisites
- Node.js 18+
- npm or yarn
### Installation
```bash
# Clone the repository
git clone https://github.com/AeThex-LABS/aethex-studio.git
# Navigate to the project directory
cd aethex-studio
# Install dependencies
npm install
# Start the development server
npm run dev
```
Visit `http://localhost:3000` to see the application.
### Building for Production
```bash
# Build the application
npm run build
# Start the production server
npm start
```
## 📖 Usage Guide
### Creating Your First Script
1. Click **"New File"** in the file tree
2. Choose a template or start from scratch
3. Write your Lua code in the Monaco editor
4. Click **"Preview"** to test
5. **Copy** or **Export** your script
### Using Templates
1. Click the **Templates** button in the toolbar
2. Browse categories: Beginner, Gameplay, UI, Tools, Advanced
3. Click a template to load it into your editor
4. Customize the code for your needs
### AI Assistant
1. Open the **AI Chat** panel (right side on desktop)
2. Ask questions about:
- Roblox scripting
- Code debugging
- API usage
- Best practices
3. Get instant, context-aware answers
### Organizing Files
- **Create folders** - Right-click in file tree
- **Drag and drop** - Move files between folders
- **Rename** - Click the menu (⋯) next to a file
- **Delete** - Use the menu to remove files
### Searching
- **Quick search** - `Cmd/Ctrl+P` to find files by name
- **Global search** - `Cmd/Ctrl+Shift+F` to search text across all files
- **In-editor search** - `Cmd/Ctrl+F` to find text in current file
## 🛠️ Tech Stack
- **Next.js 14** - React framework
- **React 18** - UI library
- **TypeScript** - Type safety
- **Monaco Editor** - Code editor
- **Tailwind CSS** - Styling
- **Radix UI** - Component primitives
- **Phosphor Icons** - Icon library
- **Vitest** - Testing framework
- **PostHog** - Analytics (optional)
- **Sentry** - Error tracking (optional)
## 🧪 Running Tests
```bash
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with UI
npm run test:ui
# Generate coverage report
npm run test:coverage
```
## 📂 Project Structure
```
aethex-studio/
├── src/
│ ├── components/ # React components
│ │ ├── ui/ # Reusable UI components
│ │ ├── CodeEditor.tsx
│ │ ├── FileTree.tsx
│ │ ├── AIChat.tsx
│ │ └── ...
│ ├── hooks/ # Custom React hooks
│ ├── lib/ # Utility functions
│ │ ├── templates.ts # Code templates
│ │ └── ...
│ └── App.tsx # Main application
├── app/ # Next.js app directory
│ └── globals.css # Global styles
├── public/ # Static assets
└── tests/ # Test files
```
## 🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
1. Fork the repository
2. Create your feature branch (`git checkout -b feature/AmazingFeature`)
3. Commit your changes (`git commit -m 'Add some AmazingFeature'`)
4. Push to the branch (`git push origin feature/AmazingFeature`)
5. Open a Pull Request
## 📝 Code Templates
AeThex Studio includes 25+ production-ready templates:
**Beginner:**
- Hello World, Player Join Handler, Part Touch Detector, etc.
**Gameplay:**
- DataStore System, Teleport Part, Team System, Combat System, etc.
**UI:**
- GUI Buttons, Proximity Prompts, Countdown Timers, etc.
**Tools:**
- Give Tool, Sound Manager, Admin Commands, Chat Commands, etc.
**Advanced:**
- Round System, Inventory System, Pathfinding NPC, Shop System, etc.
## 🐛 Bug Reports
Found a bug? Please open an issue on GitHub with:
- Description of the bug
- Steps to reproduce
- Expected vs actual behavior
- Screenshots (if applicable)
## 📜 License
This project is licensed under the MIT License - see the LICENSE file for details.
## 🙏 Acknowledgments
- **Monaco Editor** - For the powerful code editor
- **Roblox** - For the game platform
- **Radix UI** - For accessible component primitives
- **Vercel** - For Next.js framework
## 📧 Contact
- **Website**: [aethex.com](https://aethex.com)
- **GitHub**: [@AeThex-LABS](https://github.com/AeThex-LABS)
- **Issues**: [GitHub Issues](https://github.com/AeThex-LABS/aethex-studio/issues)
---
**Built with ❤️ by the AeThex team**
Happy coding! 🎮✨

View file

@ -42,13 +42,23 @@ function App() {
const [showPassportLogin, setShowPassportLogin] = useState(false);
const [consoleCollapsed, setConsoleCollapsed] = useState(isMobile);
const [user, setUser] = useState<{ login: string; avatarUrl: string; email: string } | null>(() => {
const stored = typeof window !== 'undefined' ? localStorage.getItem('aethex-user') : null;
return stored ? JSON.parse(stored) : null;
try {
const stored = typeof window !== 'undefined' ? localStorage.getItem('aethex-user') : null;
return stored ? JSON.parse(stored) : null;
} catch (error) {
console.error('Failed to load user from localStorage:', error);
captureError(error as Error, { context: 'user_state_initialization' });
return null;
}
});
React.useEffect(() => {
initPostHog();
initSentry();
try {
initPostHog();
initSentry();
} catch (error) {
console.error('Failed to initialize analytics:', error);
}
}, []);
// Keyboard shortcuts
@ -118,14 +128,28 @@ function App() {
]);
const handleLoginSuccess = (user: { login: string; avatarUrl: string; email: string }) => {
setUser(user);
localStorage.setItem('aethex-user', JSON.stringify(user));
captureEvent('login', { user });
try {
setUser(user);
localStorage.setItem('aethex-user', JSON.stringify(user));
captureEvent('login', { user });
toast.success('Successfully signed in!');
} catch (error) {
console.error('Failed to save user session:', error);
captureError(error as Error, { context: 'login_success' });
toast.error('Failed to save session. Please try again.');
}
};
const handleSignOut = () => {
setUser(null);
localStorage.removeItem('aethex-user');
try {
setUser(null);
localStorage.removeItem('aethex-user');
toast.success('Signed out successfully');
} catch (error) {
console.error('Failed to sign out:', error);
captureError(error as Error, { context: 'sign_out' });
toast.error('Failed to sign out. Please try again.');
}
};
const [files, setFiles] = useState<FileNode[]>([
@ -215,121 +239,158 @@ end)`,
};
const handleFileCreate = (name: string, parentId?: string) => {
const newFile: FileNode = {
id: `file-${Date.now()}`,
name: name.endsWith('.lua') ? name : `${name}.lua`,
type: 'file',
content: '-- New file\n',
};
try {
if (!name || name.trim() === '') {
toast.error('File name cannot be empty');
return;
}
setFiles((prev) => {
const addToFolder = (nodes: FileNode[]): FileNode[] => {
return nodes.map((node) => {
if (node.id === 'root' && !parentId) {
return {
...node,
children: [...(node.children || []), newFile],
};
}
if (node.id === parentId && node.type === 'folder') {
return {
...node,
children: [...(node.children || []), newFile],
};
}
if (node.children) {
return { ...node, children: addToFolder(node.children) };
}
return node;
});
const newFile: FileNode = {
id: `file-${Date.now()}`,
name: name.endsWith('.lua') ? name : `${name}.lua`,
type: 'file',
content: '-- New file\n',
};
return addToFolder(prev || []);
});
captureEvent('file_create', { name, parentId });
toast.success(`Created ${newFile.name}`);
setFiles((prev) => {
const addToFolder = (nodes: FileNode[]): FileNode[] => {
return nodes.map((node) => {
if (node.id === 'root' && !parentId) {
return {
...node,
children: [...(node.children || []), newFile],
};
}
if (node.id === parentId && node.type === 'folder') {
return {
...node,
children: [...(node.children || []), newFile],
};
}
if (node.children) {
return { ...node, children: addToFolder(node.children) };
}
return node;
});
};
return addToFolder(prev || []);
});
captureEvent('file_create', { name, parentId });
toast.success(`Created ${newFile.name}`);
} catch (error) {
console.error('Failed to create file:', error);
captureError(error as Error, { context: 'file_create', name, parentId });
toast.error('Failed to create file. Please try again.');
}
};
const handleFileRename = (id: string, newName: string) => {
setFiles((prev) => {
const rename = (nodes: FileNode[]): FileNode[] => {
return nodes.map((node) => {
if (node.id === id) {
return { ...node, name: newName };
}
if (node.children) {
return { ...node, children: rename(node.children) };
}
return node;
});
};
return rename(prev || []);
});
try {
if (!newName || newName.trim() === '') {
toast.error('File name cannot be empty');
return;
}
setFiles((prev) => {
const rename = (nodes: FileNode[]): FileNode[] => {
return nodes.map((node) => {
if (node.id === id) {
return { ...node, name: newName };
}
if (node.children) {
return { ...node, children: rename(node.children) };
}
return node;
});
};
return rename(prev || []);
});
captureEvent('file_rename', { id, newName });
} catch (error) {
console.error('Failed to rename file:', error);
captureError(error as Error, { context: 'file_rename', id, newName });
toast.error('Failed to rename file. Please try again.');
}
};
const handleFileDelete = (id: string) => {
setFiles((prev) => {
const deleteNode = (nodes: FileNode[]): FileNode[] => {
return nodes.filter((node) => {
if (node.id === id) return false;
if (node.children) {
node.children = deleteNode(node.children);
}
return true;
});
};
return deleteNode(prev || []);
});
try {
setFiles((prev) => {
const deleteNode = (nodes: FileNode[]): FileNode[] => {
return nodes.filter((node) => {
if (node.id === id) return false;
if (node.children) {
node.children = deleteNode(node.children);
}
return true;
});
};
return deleteNode(prev || []);
});
setOpenFiles((prev) => (prev || []).filter((f) => f.id !== id));
if (activeFileId === id) {
setActiveFileId((openFiles || [])[0]?.id || '');
setOpenFiles((prev) => (prev || []).filter((f) => f.id !== id));
if (activeFileId === id) {
setActiveFileId((openFiles || [])[0]?.id || '');
}
captureEvent('file_delete', { id });
} catch (error) {
console.error('Failed to delete file:', error);
captureError(error as Error, { context: 'file_delete', id });
toast.error('Failed to delete file. Please try again.');
}
captureEvent('file_delete', { id });
};
const handleFileMove = (fileId: string, targetParentId: string) => {
setFiles((prev) => {
let movedNode: FileNode | null = null;
try {
setFiles((prev) => {
let movedNode: FileNode | null = null;
// First, find and remove the node from its current location
const removeNode = (nodes: FileNode[]): FileNode[] => {
return nodes.filter((node) => {
if (node.id === fileId) {
movedNode = node;
return false;
}
if (node.children) {
node.children = removeNode(node.children);
}
return true;
});
};
// First, find and remove the node from its current location
const removeNode = (nodes: FileNode[]): FileNode[] => {
return nodes.filter((node) => {
if (node.id === fileId) {
movedNode = node;
return false;
}
if (node.children) {
node.children = removeNode(node.children);
}
return true;
});
};
// Then, add the node to the target folder
const addToTarget = (nodes: FileNode[]): FileNode[] => {
return nodes.map((node) => {
if (node.id === targetParentId && node.type === 'folder') {
return {
...node,
children: [...(node.children || []), movedNode!],
};
}
if (node.children) {
return { ...node, children: addToTarget(node.children) };
}
return node;
});
};
// Then, add the node to the target folder
const addToTarget = (nodes: FileNode[]): FileNode[] => {
return nodes.map((node) => {
if (node.id === targetParentId && node.type === 'folder') {
return {
...node,
children: [...(node.children || []), movedNode!],
};
}
if (node.children) {
return { ...node, children: addToTarget(node.children) };
}
return node;
});
};
const withoutMoved = removeNode(prev || []);
if (movedNode) {
return addToTarget(withoutMoved);
}
return prev || [];
});
const withoutMoved = removeNode(prev || []);
if (movedNode) {
return addToTarget(withoutMoved);
}
return prev || [];
});
captureEvent('file_move', { fileId, targetParentId });
captureEvent('file_move', { fileId, targetParentId });
} catch (error) {
console.error('Failed to move file:', error);
captureError(error as Error, { context: 'file_move', fileId, targetParentId });
toast.error('Failed to move file. Please try again.');
}
};
const handleFileClose = (id: string) => {
@ -341,25 +402,39 @@ end)`,
};
const handleCreateProject = (config: ProjectConfig) => {
const projectFiles: FileNode[] = [
{
id: 'root',
name: config.name,
type: 'folder',
children: [
{
id: `file-${Date.now()}`,
name: 'main.lua',
type: 'file',
content: `-- ${config.name}\n-- Template: ${config.template}\n\nprint("Project initialized!")`,
},
],
},
];
try {
if (!config.name || config.name.trim() === '') {
toast.error('Project name cannot be empty');
return;
}
setFiles(projectFiles);
setOpenFiles([]);
setActiveFileId('');
const projectFiles: FileNode[] = [
{
id: 'root',
name: config.name,
type: 'folder',
children: [
{
id: `file-${Date.now()}`,
name: 'main.lua',
type: 'file',
content: `-- ${config.name}\n-- Template: ${config.template}\n\nprint("Project initialized!")`,
},
],
},
];
setFiles(projectFiles);
setOpenFiles([]);
setActiveFileId('');
captureEvent('project_create', { name: config.name, template: config.template });
toast.success(`Project "${config.name}" created successfully!`);
} catch (error) {
console.error('Failed to create project:', error);
captureError(error as Error, { context: 'project_create', config });
toast.error('Failed to create project. Please try again.');
}
};
// Example user stub for profile

View file

@ -4,6 +4,7 @@ import { ScrollArea } from '@/components/ui/scroll-area';
import { Textarea } from '@/components/ui/textarea';
import { Sparkle, PaperPlaneRight } from '@phosphor-icons/react';
import { toast } from 'sonner';
import { captureError } from '@/lib/sentry';
interface Message {
role: 'user' | 'assistant';
@ -53,8 +54,10 @@ export function AIChat({ currentCode }: AIChatProps) {
setMessages((prev) => [...prev, { role: 'assistant', content: response }]);
}
} catch (error) {
console.error('AI chat error:', error);
captureError(error as Error, { context: 'ai_chat', userMessage, codeLength: currentCode.length });
toast.error('Failed to get AI response. Please try again.');
setMessages((prev) => [...prev, { role: 'assistant', content: 'Sorry, I encountered an error. Please try asking again.' }]);
setMessages((prev) => [...prev, { role: 'assistant', content: 'Sorry, I encountered an error. Please try asking again or check your connection.' }]);
} finally {
setIsLoading(false);
}

View file

@ -1,6 +1,8 @@
import Editor from '@monaco-editor/react';
import { useKV } from '@github/spark/hooks';
import { useEffect } from 'react';
import { LoadingSpinner } from './ui/loading-spinner';
import { toast } from 'sonner';
interface CodeEditorProps {
onCodeChange?: (code: string) => void;
@ -27,13 +29,31 @@ end)
`);
useEffect(() => {
if (onCodeChange && code) {
onCodeChange(code);
try {
if (onCodeChange && code) {
onCodeChange(code);
}
} catch (error) {
console.error('Failed to update code:', error);
}
}, [code, onCodeChange]);
const handleEditorChange = (value: string | undefined) => {
setCode(value || '');
try {
setCode(value || '');
} catch (error) {
console.error('Failed to save code:', error);
toast.error('Failed to save changes. Please try again.');
}
};
const handleEditorMount = () => {
console.log('Monaco editor mounted successfully');
};
const handleEditorError = (error: Error) => {
console.error('Monaco editor error:', error);
toast.error('Editor failed to load. Please refresh the page.');
};
return (
@ -45,6 +65,12 @@ end)
theme="vs-dark"
value={code}
onChange={handleEditorChange}
onMount={handleEditorMount}
loading={
<div className="h-full flex items-center justify-center">
<LoadingSpinner />
</div>
}
options={{
minimap: { enabled: typeof window !== 'undefined' && window.innerWidth >= 768 },
fontSize: 13,