361 lines
No EOL
12 KiB
TypeScript
361 lines
No EOL
12 KiB
TypeScript
import React, { useState } from 'react';
|
|
import { Toaster } from './components/ui/sonner';
|
|
import { CodeEditor } from './components/CodeEditor';
|
|
import { AIChat } from './components/AIChat';
|
|
import { Toolbar } from './components/Toolbar';
|
|
import { TemplatesDrawer } from './components/TemplatesDrawer';
|
|
import { WelcomeDialog } from './components/WelcomeDialog';
|
|
import { FileTree, FileNode } from './components/FileTree';
|
|
import { FileTabs } from './components/FileTabs';
|
|
import { PreviewModal } from './components/PreviewModal';
|
|
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 { Tabs, TabsContent, TabsList, TabsTrigger } from './components/ui/tabs';
|
|
import { toast } from 'sonner';
|
|
import { EducationPanel } from './components/EducationPanel';
|
|
import { ExtraTabs } from './components/ui/tabs-extra';
|
|
import { PassportLogin } from './components/PassportLogin';
|
|
import { Button } from './components/ui/button';
|
|
import { initPostHog, captureEvent } from './lib/posthog';
|
|
import { initSentry, captureError } from './lib/sentry';
|
|
|
|
function App() {
|
|
const [currentCode, setCurrentCode] = useState('');
|
|
const [showTemplates, setShowTemplates] = useState(false);
|
|
const [showPreview, setShowPreview] = useState(false);
|
|
const [showNewProject, setShowNewProject] = useState(false);
|
|
const [code, setCode] = useState('');
|
|
const isMobile = useIsMobile();
|
|
|
|
const [showPassportLogin, setShowPassportLogin] = useState(false);
|
|
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;
|
|
});
|
|
|
|
React.useEffect(() => {
|
|
initPostHog();
|
|
initSentry();
|
|
}, []);
|
|
|
|
const handleLoginSuccess = (user: { login: string; avatarUrl: string; email: string }) => {
|
|
setUser(user);
|
|
localStorage.setItem('aethex-user', JSON.stringify(user));
|
|
captureEvent('login', { user });
|
|
};
|
|
|
|
const handleSignOut = () => {
|
|
setUser(null);
|
|
localStorage.removeItem('aethex-user');
|
|
};
|
|
|
|
const [files, setFiles] = useState<FileNode[]>([
|
|
{
|
|
id: 'root',
|
|
name: 'src',
|
|
type: 'folder',
|
|
children: [
|
|
{
|
|
id: 'file-1',
|
|
name: 'script.lua',
|
|
type: 'file',
|
|
content: `-- Welcome to AeThex Studio!
|
|
-- Write your Roblox Lua code here
|
|
|
|
local Players = game:GetService("Players")
|
|
|
|
Players.PlayerAdded:Connect(function(player)
|
|
print(player.Name .. " joined the game!")
|
|
|
|
local leaderstats = Instance.new("Folder")
|
|
leaderstats.Name = "leaderstats"
|
|
leaderstats.Parent = player
|
|
|
|
local coins = Instance.new("IntValue")
|
|
coins.Name = "Coins"
|
|
coins.Value = 0
|
|
coins.Parent = leaderstats
|
|
end)`,
|
|
},
|
|
],
|
|
},
|
|
]);
|
|
|
|
const [openFiles, setOpenFiles] = useState<FileNode[]>([]);
|
|
const [activeFileId, setActiveFileId] = useState<string>('file-1');
|
|
|
|
const handleTemplateSelect = (templateCode: string) => {
|
|
setCode(templateCode);
|
|
setCurrentCode(templateCode);
|
|
};
|
|
|
|
const handleFileSelect = (file: FileNode) => {
|
|
if (file.type === 'file') {
|
|
setActiveFileId(file.id);
|
|
if (!(openFiles || []).find((f) => f.id === file.id)) {
|
|
setOpenFiles((prev) => [...(prev || []), file]);
|
|
}
|
|
setCode(file.content || '');
|
|
setCurrentCode(file.content || '');
|
|
}
|
|
captureEvent('file_select', { fileId: file.id });
|
|
};
|
|
|
|
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',
|
|
};
|
|
|
|
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}`);
|
|
};
|
|
|
|
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 || []);
|
|
});
|
|
};
|
|
|
|
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 || []);
|
|
});
|
|
|
|
setOpenFiles((prev) => (prev || []).filter((f) => f.id !== id));
|
|
if (activeFileId === id) {
|
|
setActiveFileId((openFiles || [])[0]?.id || '');
|
|
}
|
|
captureEvent('file_delete', { id });
|
|
};
|
|
|
|
const handleFileClose = (id: string) => {
|
|
setOpenFiles((prev) => (prev || []).filter((f) => f.id !== id));
|
|
if (activeFileId === id) {
|
|
const remaining = (openFiles || []).filter((f) => f.id !== id);
|
|
setActiveFileId(remaining[0]?.id || '');
|
|
}
|
|
};
|
|
|
|
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!")`,
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
setFiles(projectFiles);
|
|
setOpenFiles([]);
|
|
setActiveFileId('');
|
|
};
|
|
|
|
// Example user stub for profile
|
|
const demoUser = user || {
|
|
login: 'demo-user',
|
|
avatarUrl: 'https://avatars.githubusercontent.com/u/1?v=4',
|
|
email: 'demo@aethex.com',
|
|
};
|
|
return (
|
|
<div className="h-screen flex flex-col bg-background text-foreground">
|
|
<Toolbar
|
|
code={currentCode}
|
|
onTemplatesClick={() => setShowTemplates(true)}
|
|
onPreviewClick={() => setShowPreview(true)}
|
|
onNewProjectClick={() => setShowNewProject(true)}
|
|
/>
|
|
|
|
<div className="flex-1 overflow-hidden flex flex-col">
|
|
{isMobile ? (
|
|
<Tabs defaultValue="editor" className="h-full flex flex-col">
|
|
<TabsList className="w-full rounded-none border-b border-border">
|
|
<TabsTrigger value="files" className="flex-1">Files</TabsTrigger>
|
|
<TabsTrigger value="editor" className="flex-1">Editor</TabsTrigger>
|
|
<TabsTrigger value="ai" className="flex-1">AI</TabsTrigger>
|
|
<TabsTrigger value="education" className="flex-1">Learn</TabsTrigger>
|
|
</TabsList>
|
|
<TabsContent value="files" className="flex-1 m-0">
|
|
<FileTree
|
|
files={files || []}
|
|
onFileSelect={handleFileSelect}
|
|
onFileCreate={handleFileCreate}
|
|
onFileRename={handleFileRename}
|
|
onFileDelete={handleFileDelete}
|
|
selectedFileId={activeFileId}
|
|
/>
|
|
</TabsContent>
|
|
<TabsContent value="editor" className="flex-1 m-0 flex flex-col">
|
|
<FileTabs
|
|
openFiles={openFiles || []}
|
|
activeFileId={activeFileId}
|
|
onFileSelect={handleFileSelect}
|
|
onFileClose={handleFileClose}
|
|
/>
|
|
<div className="flex-1">
|
|
<CodeEditor onCodeChange={setCurrentCode} />
|
|
</div>
|
|
</TabsContent>
|
|
<TabsContent value="ai" className="flex-1 m-0">
|
|
<AIChat currentCode={currentCode} />
|
|
</TabsContent>
|
|
<TabsContent value="education" className="flex-1 m-0">
|
|
<EducationPanel />
|
|
</TabsContent>
|
|
</Tabs>
|
|
) : (
|
|
<>
|
|
<div className="flex-1 overflow-hidden">
|
|
<ResizablePanelGroup direction="horizontal">
|
|
<ResizablePanel defaultSize={15} minSize={10} maxSize={25}>
|
|
<FileTree
|
|
files={files || []}
|
|
onFileSelect={handleFileSelect}
|
|
onFileCreate={handleFileCreate}
|
|
onFileRename={handleFileRename}
|
|
onFileDelete={handleFileDelete}
|
|
selectedFileId={activeFileId}
|
|
/>
|
|
</ResizablePanel>
|
|
|
|
<ResizableHandle className="w-1 bg-border hover:bg-accent transition-colors" />
|
|
|
|
<ResizablePanel defaultSize={55} minSize={30}>
|
|
<div className="h-full flex flex-col">
|
|
<FileTabs
|
|
openFiles={openFiles || []}
|
|
activeFileId={activeFileId}
|
|
onFileSelect={handleFileSelect}
|
|
onFileClose={handleFileClose}
|
|
/>
|
|
<div className="flex-1">
|
|
<CodeEditor onCodeChange={setCurrentCode} />
|
|
</div>
|
|
</div>
|
|
</ResizablePanel>
|
|
|
|
<ResizableHandle className="w-1 bg-border hover:bg-accent transition-colors" />
|
|
|
|
<ResizablePanel defaultSize={20} minSize={15}>
|
|
<EducationPanel />
|
|
</ResizablePanel>
|
|
|
|
<ResizablePanel defaultSize={15} minSize={10}>
|
|
<AIChat currentCode={currentCode} />
|
|
</ResizablePanel>
|
|
</ResizablePanelGroup>
|
|
</div>
|
|
<ConsolePanel />
|
|
</>
|
|
)}
|
|
{/* Unified feature tabs for all major panels */}
|
|
<div className="w-full border-t border-border mt-4">
|
|
<ExtraTabs user={demoUser} />
|
|
</div>
|
|
</div>
|
|
|
|
{showTemplates && (
|
|
<TemplatesDrawer
|
|
onSelectTemplate={handleTemplateSelect}
|
|
onClose={() => setShowTemplates(false)}
|
|
/>
|
|
)}
|
|
|
|
<PreviewModal
|
|
open={showPreview}
|
|
onClose={() => setShowPreview(false)}
|
|
code={currentCode}
|
|
/>
|
|
|
|
<NewProjectModal
|
|
open={showNewProject}
|
|
onClose={() => setShowNewProject(false)}
|
|
onCreateProject={handleCreateProject}
|
|
/>
|
|
|
|
<WelcomeDialog />
|
|
{!user && (
|
|
<Button
|
|
variant="secondary"
|
|
className="fixed top-4 right-4 z-50"
|
|
onClick={() => setShowPassportLogin(true)}
|
|
>
|
|
Sign In
|
|
</Button>
|
|
)}
|
|
{user && (
|
|
<div className="fixed top-4 right-4 z-50 flex items-center gap-2 bg-card px-3 py-1 rounded shadow">
|
|
<img src={user.avatarUrl} alt={user.login} className="h-6 w-6 rounded-full" />
|
|
<span className="text-xs font-medium">{user.login}</span>
|
|
<Button variant="ghost" size="sm" onClick={handleSignOut}>
|
|
Sign Out
|
|
</Button>
|
|
</div>
|
|
)}
|
|
<PassportLogin
|
|
open={showPassportLogin}
|
|
onClose={() => setShowPassportLogin(false)}
|
|
onLoginSuccess={handleLoginSuccess}
|
|
/>
|
|
<Toaster position="bottom-right" theme="dark" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default App; |