deleted: app/ide/page.tsx

This commit is contained in:
Anderson 2026-02-05 10:50:56 +00:00 committed by GitHub
parent a8b2ffc3fe
commit 524f64315d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
70 changed files with 5527 additions and 1725 deletions

View file

@ -1,6 +1,17 @@
# AeThex Studio Environment Variables # AeThex Studio Environment Variables
# ===========================================
# SUPABASE (Required)
# ===========================================
# Get these from: https://supabase.com/dashboard/project/_/settings/api
NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key-here
# Service role key - KEEP SECRET, never expose to client
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key-here
# ===========================================
# Claude API Configuration # Claude API Configuration
# ===========================================
# Get your API key from: https://console.anthropic.com/ # Get your API key from: https://console.anthropic.com/
# Required for cross-platform code translation feature # Required for cross-platform code translation feature
VITE_CLAUDE_API_KEY=sk-ant-api03-your-api-key-here VITE_CLAUDE_API_KEY=sk-ant-api03-your-api-key-here
@ -8,9 +19,13 @@ VITE_CLAUDE_API_KEY=sk-ant-api03-your-api-key-here
# Optional: Override Claude model (default: claude-3-5-sonnet-20241022) # Optional: Override Claude model (default: claude-3-5-sonnet-20241022)
# VITE_CLAUDE_MODEL=claude-3-5-sonnet-20241022 # VITE_CLAUDE_MODEL=claude-3-5-sonnet-20241022
# ===========================================
# PostHog Analytics (Optional) # PostHog Analytics (Optional)
# ===========================================
# VITE_POSTHOG_KEY=your-posthog-key # VITE_POSTHOG_KEY=your-posthog-key
# VITE_POSTHOG_HOST=https://app.posthog.com # VITE_POSTHOG_HOST=https://app.posthog.com
# ===========================================
# Sentry Error Tracking (Optional) # Sentry Error Tracking (Optional)
# ===========================================
# VITE_SENTRY_DSN=your-sentry-dsn # VITE_SENTRY_DSN=your-sentry-dsn

1
.gitignore vendored
View file

@ -95,3 +95,4 @@ pids
.devcontainer/ .devcontainer/
.spark-workbench-id .spark-workbench-id
.env.local

View file

@ -1,18 +0,0 @@
import { useState } from 'react';
import { Toaster } from '../src/components/ui/sonner';
import { CodeEditor } from '../src/components/CodeEditor';
import { AIChat } from '../src/components/AIChat';
import { Toolbar } from '../src/components/Toolbar';
import { TemplatesDrawer } from '../src/components/TemplatesDrawer';
import { FileTree, FileNode } from '../src/components/FileTree';
import { FileTabs } from '../src/components/FileTabs';
import { PreviewModal } from '../src/components/PreviewModal';
// Removed named imports for WelcomeDialog and NewProjectModal. Use lazy-loaded versions from src/App.tsx.
import { ConsolePanel } from '../src/components/ConsolePanel';
import { ResizablePanelGroup, ResizablePanel, ResizableHandle } from '../src/components/ui/resizable';
import { useIsMobile } from '../src/hooks/use-mobile';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../src/components/ui/tabs';
import { toast } from 'sonner';
export { default } from '../src/App';

View file

@ -1,4 +0,0 @@
import { DashboardPage } from "../../src/components/aethex/dashboard-page";
export default function Page() {
return <DashboardPage />;
}

View file

@ -1,154 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
/* Default Dark Theme */
:root, .theme-dark {
--background: #0a0a0f;
--surface: #1a1a1f;
--primary: #8b5cf6;
--primary-light: #a78bfa;
--primary-dark: #7c3aed;
--secondary: #ec4899;
--accent: #06b6d4;
--border: #2a2a2f;
--foreground: #ffffff;
--muted: #6b7280;
}
/* Light Theme */
.theme-light {
--background: #ffffff;
--surface: #f9fafb;
--primary: #7c3aed;
--primary-light: #8b5cf6;
--primary-dark: #6d28d9;
--secondary: #db2777;
--accent: #0891b2;
--border: #e5e7eb;
--foreground: #111827;
--muted: #6b7280;
}
/* Synthwave Theme */
.theme-synthwave {
--background: #2b213a;
--surface: #241b2f;
--primary: #ff6ac1;
--primary-light: #ff8ad8;
--primary-dark: #ff4aaa;
--secondary: #9d72ff;
--accent: #72f1b8;
--border: #495495;
--foreground: #f8f8f2;
--muted: #a599e9;
}
/* Forest Theme */
.theme-forest {
--background: #0d1b1e;
--surface: #1a2f33;
--primary: #2dd4bf;
--primary-light: #5eead4;
--primary-dark: #14b8a6;
--secondary: #34d399;
--accent: #a7f3d0;
--border: #234e52;
--foreground: #ecfdf5;
--muted: #6ee7b7;
}
/* Ocean Theme */
.theme-ocean {
--background: #0c1821;
--surface: #1b2838;
--primary: #3b82f6;
--primary-light: #60a5fa;
--primary-dark: #2563eb;
--secondary: #06b6d4;
--accent: #38bdf8;
--border: #1e3a5f;
--foreground: #dbeafe;
--muted: #7dd3fc;
}
* {
border-color: var(--border);
}
body {
background-color: var(--background);
color: var(--foreground);
font-family: var(--font-inter), 'Inter', sans-serif;
}
code, pre {
font-family: var(--font-jetbrains-mono), 'JetBrains Mono', monospace;
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-track {
background: #1a1a1f;
}
::-webkit-scrollbar-thumb {
background: #2a2a2f;
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: #3a3a3f;
}
/* Animation classes */
.animate-slide-in {
animation: slideIn 0.2s ease-out;
}
.animate-fade-in {
animation: fadeIn 0.2s ease-in-out;
}
@keyframes slideIn {
from {
transform: translateY(-10px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* Monaco Editor theme overrides */
.monaco-editor .margin {
background-color: #0a0a0f !important;
}
.monaco-editor {
background-color: #0a0a0f !important;
}

View file

@ -1,4 +0,0 @@
import App from "../../src/App";
export default function Page() {
return <App />;
}

View file

@ -1,16 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>AeThex Studio - Roblox Lua Editor</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<link href="/app/main.css" rel="stylesheet" />
</head>
<body>
<div id="root"></div>
<script type="module" src="/app/App.tsx"></script>
</body>
</html>

View file

@ -1,35 +0,0 @@
import type { Metadata } from "next";
import Toaster from "../src/components/ui/toaster";
import "./globals.css";
export const metadata: Metadata = {
title: "AeThex Studio",
description: "The Next-Generation Cross-Platform IDE",
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" className="dark">
<head>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link
rel="preconnect"
href="https://fonts.gstatic.com"
crossOrigin="anonymous"
/>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&family=Source+Code+Pro:wght@400&family=Space+Grotesk:wght@400;700&display=swap"
rel="stylesheet"
/>
</head>
<body className="font-body antialiased">
{children}
<Toaster />
</body>
</html>
);
}

View file

@ -1,61 +0,0 @@
@import url('https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600;700&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
* {
border-color: var(--border);
}
body {
font-family: 'Inter', sans-serif;
background:
radial-gradient(circle at 20% 50%, oklch(0.20 0.08 265 / 0.3) 0%, transparent 50%),
radial-gradient(circle at 80% 80%, oklch(0.20 0.08 150 / 0.2) 0%, transparent 50%),
oklch(0.15 0.02 265);
background-attachment: fixed;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Space Grotesk', sans-serif;
}
code, pre {
font-family: 'JetBrains Mono', monospace;
}
}
@layer components {
.btn-accent-hover {
transition: all 0.2s ease;
}
.btn-accent-hover:hover {
transform: scale(1.02);
filter: brightness(1.1);
}
.btn-accent-hover:active {
transform: scale(0.98);
}
}
:root {
--background: oklch(0.15 0.02 265);
--foreground: oklch(0.85 0.03 265);
--card: oklch(0.20 0.03 265);
--card-foreground: oklch(0.85 0.03 265);
--popover: oklch(0.20 0.03 265);
--popover-foreground: oklch(0.85 0.03 265);
--primary: oklch(0.45 0.20 265);
--primary-foreground: oklch(0.98 0 0);
--secondary: oklch(0.25 0.04 265);
--secondary-foreground: oklch(0.85 0.03 265);
--muted: oklch(0.22 0.03 265);
--muted-foreground: oklch(0.55 0.03 265);
--accent: oklch(0.75 0.20 150);
--accent-foreground: oklch(0.15 0.02 265);
--destructive: oklch(0.55 0.22 25);
--destructive-foreground: oklch(0.98 0 0);
--border: oklch(0.30 0.04 265);
--input: oklch(0.30 0.04 265);
--ring: oklch(0.75 0.20 150);
--radius: 0.5rem;
}

View file

@ -1,5 +0,0 @@
import App from "../src/App";
export default function Page() {
return <App />;
}

View file

@ -1,865 +0,0 @@
/* Tablet region: 1015x768px - global UI sizing */
@media (min-width: 900px) and (max-width: 1100px) and (min-height: 700px) and (max-height: 800px) {
.ide-container.grid-layout {
max-width: 1015px;
max-height: 768px;
margin: 0 auto;
grid-template-rows: 60px 1fr 120px;
grid-template-columns: 90px 1fr 220px;
grid-template-areas:
"title title title"
"sidebar editor right"
"bottom bottom bottom";
border-radius: 20px;
box-shadow: 0 2px 24px 0 rgba(0,0,0,0.20);
background: #101014;
padding: 12px;
overflow: hidden;
}
.grid-title {
font-size: 1.25em;
padding: 16px 20px;
}
.grid-sidebar {
min-width: 90px;
max-width: 120px;
font-size: 1.18em;
padding: 16px 0;
border-radius: 14px;
box-shadow: 0 1px 10px 0 rgba(0,0,0,0.12);
}
.grid-right {
min-width: 180px;
max-width: 220px;
font-size: 1.15em;
padding: 16px 0;
border-radius: 14px;
box-shadow: 0 1px 10px 0 rgba(0,0,0,0.12);
}
.grid-editor {
min-width: 0;
padding: 0 3vw;
font-size: 1.22em;
border-radius: 16px;
box-shadow: 0 1px 10px 0 rgba(0,0,0,0.12);
background: #181818;
}
.grid-bottom {
min-height: 120px;
max-height: 180px;
font-size: 1.15em;
padding: 24px 2vw 56px 2vw;
border-radius: 14px;
box-shadow: 0 1px 10px 0 rgba(0,0,0,0.12);
background: #15151a;
box-sizing: border-box;
}
.title-bar {
padding: 16px 20px;
font-size: 1.25em;
border-radius: 14px;
box-shadow: 0 1px 10px 0 rgba(0,0,0,0.12);
background: #18181c;
}
.editor-tab, .file-item, .bottom-tab {
min-height: 52px;
font-size: 1.22em;
padding: 0 20px;
border-radius: 10px;
margin-bottom: 8px;
background: #222226;
box-shadow: 0 1px 6px 0 rgba(0,0,0,0.10);
touch-action: manipulation;
}
.bottom-collapse-btn {
font-size: 1.6em !important;
min-width: 56px;
min-height: 56px;
padding: 0 14px;
border-radius: 10px;
touch-action: manipulation;
}
.network-viz.docked {
max-width: 440px;
padding: 18px 20px;
margin: 12px auto;
font-size: 1.15em;
border-radius: 14px;
box-shadow: 0 1px 10px 0 rgba(0,0,0,0.12);
}
}
/* Responsive: docked status panel inside bottom panel for tablet/mobile */
.network-viz.docked {
margin: 16px auto;
padding: 16px 18px;
border-radius: 14px;
background: #18181c;
box-shadow: 0 1px 8px 0 rgba(0,0,0,0.10);
max-width: 420px;
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.network-viz-header {
font-size: 1.1em;
font-weight: 600;
margin-bottom: 8px;
color: #e0e0e0;
}
.network-nodes {
display: flex;
flex-direction: row;
gap: 18px;
}
.network-node {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
}
.node-dot {
width: 18px;
height: 18px;
border-radius: 50%;
margin-bottom: 4px;
}
.node-dot.foundation { background: #ff0000; }
.node-dot.corporation { background: #0066ff; }
.node-dot.labs { background: #ffa500; }
.node-label {
font-size: 1em;
font-weight: 500;
}
.node-status {
font-size: 0.95em;
color: #b0b0b0;
text-align: center;
}
@media (max-width: 1024px) {
.network-viz.docked {
max-width: 98vw;
padding: 12px 8px;
margin: 8px auto;
font-size: 1.08em;
}
.network-nodes {
gap: 10px;
}
.network-node {
gap: 2px;
}
.node-dot {
width: 16px;
height: 16px;
}
}
@media (min-width: 768px) and (max-width: 1024px) {
.ide-container.grid-layout {
grid-template-rows: 56px 1fr 120px;
grid-template-columns: 72px 1fr 180px;
grid-template-areas:
"title title title"
"sidebar editor right"
"bottom bottom bottom";
border-radius: 18px;
box-shadow: 0 2px 16px 0 rgba(0,0,0,0.18);
background: #101014;
padding: 8px;
}
.grid-sidebar {
min-width: 56px;
max-width: 90px;
font-size: 1.15em;
padding: 12px 0;
border-right: none;
background: #18181c;
border-radius: 12px;
box-shadow: 0 1px 8px 0 rgba(0,0,0,0.10);
}
.grid-right {
min-width: 100px;
max-width: 180px;
font-size: 1.1em;
border-left: none;
background: #18181c;
border-radius: 12px;
box-shadow: 0 1px 8px 0 rgba(0,0,0,0.10);
}
.grid-editor {
min-width: 0;
padding: 0 3vw;
font-size: 1.18em;
touch-action: manipulation;
border-radius: 14px;
box-shadow: 0 1px 8px 0 rgba(0,0,0,0.10);
background: #181818;
}
.grid-bottom {
min-height: 80px;
max-height: 160px;
font-size: 1.1em;
padding: 16px 2vw 40px 2vw;
border-radius: 12px;
box-shadow: 0 1px 8px 0 rgba(0,0,0,0.10);
background: #15151a;
box-sizing: border-box;
}
@media (max-width: 1024px) {
.grid-bottom {
padding-bottom: 56px;
padding-top: 24px;
min-height: 100px;
max-height: 180px;
}
}
.title-bar {
padding: 12px 12px;
font-size: 1.18em;
border-radius: 12px;
box-shadow: 0 1px 8px 0 rgba(0,0,0,0.10);
background: #18181c;
}
.editor-tab, .file-item, .bottom-tab {
min-height: 48px;
font-size: 1.18em;
padding: 0 16px;
touch-action: manipulation;
border-radius: 8px;
margin-bottom: 6px;
background: #222226;
box-shadow: 0 1px 4px 0 rgba(0,0,0,0.08);
}
}
@media (max-width: 768px) {
.ide-container.grid-layout {
grid-template-rows: 56px 1fr 100px;
grid-template-columns: 1fr;
grid-template-areas:
"title"
"editor"
"bottom";
width: 100vw;
height: 100vh;
overflow: hidden;
border-radius: 0;
box-shadow: none;
background: #101014;
padding: 0;
}
.grid-sidebar,
.grid-right {
display: none !important;
}
.grid-editor {
grid-area: editor;
width: 100vw;
height: calc(100vh - 56px - 100px);
min-width: 0;
min-height: 0;
overflow: auto;
font-size: 1.22em;
padding: 0 4vw;
touch-action: manipulation;
border-radius: 0;
box-shadow: none;
background: #181818;
}
.grid-bottom {
min-height: 64px;
max-height: 120px;
width: 100vw;
overflow-y: auto;
font-size: 1.15em;
padding: 0 2vw;
border-radius: 0;
box-shadow: none;
background: #15151a;
}
.title-bar {
width: 100vw;
min-width: 0;
padding: 12px 12px;
font-size: 1.22em;
border-radius: 0;
box-shadow: none;
background: #18181c;
}
.editor-tab, .file-item, .bottom-tab {
min-height: 48px;
font-size: 1.22em;
padding: 0 16px;
touch-action: manipulation;
border-radius: 0;
margin-bottom: 8px;
background: #222226;
box-shadow: none;
}
.grid-bottom.collapsed {
display: none !important;
}
}
/* Tablet-specific layout: 768px to 1024px */
@media (min-width: 768px) and (max-width: 1024px) {
.ide-container.grid-layout {
grid-template-rows: 48px 1fr 120px;
grid-template-columns: 80px 1fr 220px;
grid-template-areas:
"title title title"
"sidebar editor right"
"bottom bottom bottom";
width: 100vw;
height: 100vh;
overflow: hidden;
}
.grid-sidebar {
min-width: 60px;
max-width: 100px;
font-size: 1.1em;
padding: 8px 0;
border-right: 1px solid #222;
}
.grid-right {
min-width: 120px;
max-width: 220px;
font-size: 1em;
border-left: 1px solid #222;
}
.grid-editor {
min-width: 0;
padding: 0 2vw;
font-size: 1.1em;
.bottom-tab {
display: inline-block;
padding: 14px 28px;
font-size: 1.18em;
border-radius: 10px 10px 0 0;
background: #222226;
color: #e0e0e0;
margin-right: 12px;
cursor: pointer;
transition: background 0.2s;
min-width: 56px;
min-height: 48px;
touch-action: manipulation;
}
.bottom-tab.active {
background: #333344;
color: #fff;
}
.bottom-collapse-btn {
font-size: 1.35em !important;
min-width: 44px;
min-height: 44px;
padding: 0 8px;
border-radius: 8px;
touch-action: manipulation;
}
@media (max-width: 1024px) {
.bottom-tab {
font-size: 1.22em;
min-width: 64px;
min-height: 52px;
padding: 16px 32px;
}
.bottom-collapse-btn {
font-size: 1.5em !important;
min-width: 52px;
min-height: 52px;
padding: 0 12px;
}
}
padding: 0 12px;
touch-action: manipulation;
}
}
/* Mobile layout: <768px */
@media (max-width: 768px) {
.ide-container.grid-layout {
grid-template-rows: 48px 1fr 120px;
grid-template-columns: 1fr;
grid-template-areas:
"title"
"editor"
"bottom";
width: 100vw;
height: 100vh;
overflow: hidden;
}
.grid-sidebar,
.grid-right {
display: none !important;
}
.grid-editor {
grid-area: editor;
width: 100vw;
height: calc(100vh - 48px - 120px);
min-width: 0;
min-height: 0;
overflow: auto;
font-size: 1.15em;
padding: 0 2vw;
touch-action: manipulation;
}
.grid-bottom {
min-height: 80px;
max-height: 160px;
width: 100vw;
overflow-y: auto;
font-size: 1.1em;
padding: 0 1vw;
}
.title-bar {
width: 100vw;
min-width: 0;
padding: 8px 8px;
font-size: 1.15em;
}
.editor-tab, .file-item, .bottom-tab {
min-height: 44px;
font-size: 1.15em;
padding: 0 12px;
touch-action: manipulation;
}
.grid-bottom.collapsed {
display: none !important;
}
}
/* AeThex Studio Mockup Theme */
@import url('https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@300;400;500;700&family=JetBrains+Mono:wght@400;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body, .ide-container {
font-family: 'JetBrains Mono', 'Roboto Mono', monospace;
background: #0a0a0a;
color: #e0e0e0;
height: 100vh;
}
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.15),
rgba(0, 0, 0, 0.15) 1px,
transparent 1px,
transparent 2px
);
pointer-events: none;
z-index: 1000;
}
.ide-container {
display: flex;
flex-direction: column;
height: 100vh;
width: 100vw;
overflow: hidden;
}
.title-bar {
background: #0d0d0d;
border-bottom: 2px solid #1a1a1a;
padding: 8px 16px;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
}
.title-bar::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(90deg, #ff0000 33%, #0066ff 33%, #0066ff 66%, #ffa500 66%);
}
.title-left {
display: flex;
align-items: center;
gap: 20px;
}
.logo-small {
font-size: 1.2em;
font-weight: 700;
letter-spacing: 3px;
background: linear-gradient(90deg, #ff0000, #0066ff, #ffa500);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.project-name {
color: #666;
font-size: 0.9em;
}
.project-name span {
color: #0066ff;
font-weight: 700;
}
.title-right {
display: flex;
gap: 15px;
font-size: 0.85em;
}
.status-indicator {
display: flex;
align-items: center;
gap: 8px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
animation: pulse 2s infinite;
}
.status-dot.foundation { background: #ff0000; }
.status-dot.corporation { background: #0066ff; }
.status-dot.labs { background: #ffa500; }
@keyframes pulse {
0%, 100% { opacity: 1; box-shadow: 0 0 8px currentColor; }
50% { opacity: 0.6; box-shadow: 0 0 4px currentColor; }
}
.main-content {
display: flex;
flex: 1;
overflow: hidden;
}
.sidebar {
width: 250px;
background: #0d0d0d;
border-right: 1px solid #1a1a1a;
display: flex;
flex-direction: column;
}
.sidebar-section {
border-bottom: 1px solid #1a1a1a;
}
.sidebar-header {
padding: 12px 16px;
font-size: 0.75em;
text-transform: uppercase;
letter-spacing: 2px;
color: #666;
display: flex;
align-items: center;
gap: 8px;
border-left: 3px solid;
}
.sidebar-header.foundation { border-color: #ff0000; }
.sidebar-header.corporation { border-color: #0066ff; }
.sidebar-header.labs { border-color: #ffa500; }
.file-tree {
padding: 8px 0;
}
.file-item {
padding: 6px 16px 6px 24px;
font-size: 0.85em;
cursor: pointer;
transition: background 0.2s;
display: flex;
align-items: center;
gap: 8px;
}
.file-item:hover {
background: #1a1a1a;
}
.file-item.active {
background: #1a1a1a;
border-left: 2px solid #0066ff;
}
.file-icon {
color: #666;
}
.editor-area {
flex: 1;
display: flex;
flex-direction: column;
background: #0f0f0f;
}
.editor-tabs {
background: #0d0d0d;
border-bottom: 1px solid #1a1a1a;
display: flex;
padding: 0;
}
.editor-tab {
padding: 10px 20px;
font-size: 0.85em;
background: #0d0d0d;
border-right: 1px solid #1a1a1a;
cursor: pointer;
transition: background 0.2s;
display: flex;
align-items: center;
gap: 8px;
}
.editor-tab:hover {
background: #1a1a1a;
}
.editor-tab.active {
background: #0f0f0f;
border-bottom: 2px solid #0066ff;
}
.editor-content {
flex: 1;
overflow: auto;
padding: 20px;
}
.code-line {
display: flex;
font-size: 0.9em;
line-height: 1.6;
font-family: 'JetBrains Mono', monospace;
}
.line-number {
color: #333;
width: 40px;
text-align: right;
padding-right: 20px;
user-select: none;
}
.line-content {
flex: 1;
}
.keyword { color: #ff0000; font-weight: 700; }
.function { color: #0066ff; }
.comment { color: #ffa500; font-style: italic; }
.string { color: #00ff88; }
.number { color: #ff6b9d; }
.variable { color: #e0e0e0; }
.operator { color: #999; }
.right-panel {
width: 320px;
background: #0d0d0d;
border-left: 1px solid #1a1a1a;
display: flex;
flex-direction: column;
}
.panel-header {
padding: 12px 16px;
font-size: 0.75em;
text-transform: uppercase;
letter-spacing: 2px;
border-bottom: 1px solid #1a1a1a;
display: flex;
align-items: center;
justify-content: space-between;
}
.panel-content {
flex: 1;
overflow: auto;
padding: 16px;
}
.copilot-message {
margin-bottom: 16px;
padding: 12px;
background: #1a1a1a;
border-left: 3px solid;
font-size: 0.85em;
line-height: 1.6;
}
.copilot-message.labs { border-color: #ffa500; }
.copilot-message.foundation { border-color: #ff0000; }
.copilot-message.corporation { border-color: #0066ff; }
.copilot-label {
font-size: 0.75em;
text-transform: uppercase;
letter-spacing: 1px;
margin-bottom: 8px;
font-weight: 700;
}
.copilot-label.labs { color: #ffa500; }
.copilot-label.foundation { color: #ff0000; }
.copilot-label.corporation { color: #0066ff; }
.bottom-panel {
height: 200px;
background: #0d0d0d;
border-top: 1px solid #1a1a1a;
display: flex;
flex-direction: column;
}
.bottom-tabs-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 8px;
}
.bottom-tabs {
display: flex;
gap: 0;
border-bottom: 1px solid #1a1a1a;
}
.bottom-tab {
padding: 8px 16px;
font-size: 0.8em;
text-transform: uppercase;
letter-spacing: 1px;
cursor: pointer;
border-right: 1px solid #1a1a1a;
transition: background 0.2s;
}
.bottom-tab:hover {
background: #1a1a1a;
}
.bottom-tab.active {
background: #1a1a1a;
border-bottom: 2px solid #0066ff;
}
.bottom-content {
flex: 1;
overflow: auto;
padding: 12px;
}
.terminal-output {
flex: 1;
overflow: auto;
padding: 12px;
font-size: 0.85em;
line-height: 1.6;
}
.terminal-line {
margin: 2px 0;
}
.terminal-line.foundation { color: #ff0000; }
.terminal-line.corporation { color: #0066ff; }
.terminal-line.labs { color: #ffa500; }
.terminal-line.success { color: #00ff00; }
.terminal-line.error { color: #ff0000; }
.network-viz {
position: fixed;
bottom: 220px;
right: 20px;
width: 300px;
background: rgba(13, 13, 13, 0.95);
border: 1px solid #1a1a1a;
padding: 16px;
font-size: 0.75em;
z-index: 100;
}
.network-viz-header {
text-transform: uppercase;
letter-spacing: 2px;
margin-bottom: 12px;
color: #666;
}
.network-node {
display: flex;
align-items: center;
gap: 12px;
margin: 8px 0;
padding: 8px;
background: #0f0f0f;
}
.node-dot {
width: 12px;
height: 12px;
border-radius: 50%;
animation: pulse 2s infinite;
}
.node-dot.foundation { background: #ff0000; }
.node-dot.corporation { background: #0066ff; }
.node-dot.labs { background: #ffa500; }
.node-info {
flex: 1;
}
.node-label {
font-weight: 700;
margin-bottom: 2px;
}
.node-status {
color: #666;
font-size: 0.9em;
}
.grid-title {
grid-area: title;
z-index: 2;
}
.grid-sidebar {
grid-area: sidebar;
min-width: 180px;
max-width: 260px;
overflow-y: auto;
border-right: 2px solid #181818;
background: #111;
z-index: 1;
}
.grid-editor {
grid-area: editor;
overflow: auto;
background: #181818;
}
.grid-right {
grid-area: right;
min-width: 220px;
max-width: 400px;
border-left: 2px solid #181818;
background: #15151a;
overflow-y: auto;
}
.grid-bottom {
grid-area: bottom;
border-top: 2px solid #181818;
background: #101014;
min-height: 120px;
max-height: 220px;
overflow-y: auto;
}
.grid-network {
position: fixed;
right: 24px;
bottom: 24px;
z-index: 10;
}
.title-bar {
background: #0d0d0d;
border-bottom: 2px solid #1a1a1a;
padding: 8px 16px;
display: flex;
align-items: center;
justify-content: space-between;
position: relative;
}
.title-bar::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 100%;
height: 2px;
background: linear-gradient(90deg, #ff0000 33%, #0066ff 33%, #0066ff 66%, #ffa500 66%);
}
.title-left {
display: flex;
align-items: center;
gap: 20px;
}
.logo-small {
font-size: 1.2em;
font-weight: 700;
letter-spacing: 3px;
}
/* ...continue copying the rest of the CSS from the mockup as needed... */

View file

@ -11,7 +11,7 @@ import { Switch } from '@/components/ui/switch';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Progress } from '@/components/ui/progress'; import { Progress } from '@/components/ui/progress';
import { useAppStore } from '@/store/app-store'; import { useAppStore } from '@/store/app-store';
import { templates } from '@/lib/templates'; import { templates, Template } from '../lib/templates';
import { getPlatformIcon } from '@/lib/utils'; import { getPlatformIcon } from '@/lib/utils';
export function NewProjectModal() { export function NewProjectModal() {

19
middleware.ts Normal file
View file

@ -0,0 +1,19 @@
import { type NextRequest } from 'next/server';
import { updateSession } from '@/lib/supabase/middleware';
export async function middleware(request: NextRequest) {
return await updateSession(request);
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
* - public folder
*/
'/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
],
};

774
package-lock.json generated
View file

@ -18,10 +18,13 @@
"@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-collapsible": "^1.1.11", "@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.2", "@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-menubar": "^1.1.6", "@radix-ui/react-menubar": "^1.1.6",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-progress": "^1.1.2", "@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-radio-group": "^1.2.3", "@radix-ui/react-radio-group": "^1.2.3",
@ -33,29 +36,40 @@
"@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toast": "^1.2.6", "@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-tooltip": "^1.1.8",
"@sentry/browser": "^10.38.0",
"@supabase/ssr": "^0.8.0",
"@supabase/supabase-js": "^2.93.3",
"@tailwindcss/typography": "^0.5.13", "@tailwindcss/typography": "^0.5.13",
"autoprefixer": "^10.4.23", "autoprefixer": "^10.4.23",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"firebase": "^11.9.1", "firebase": "^11.9.1",
"genkit": "^1.20.0", "genkit": "^1.20.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.475.0", "lucide-react": "^0.475.0",
"marked": "^12.0.2", "marked": "^12.0.2",
"next": "15.5.9", "next": "15.5.9",
"next-themes": "^0.4.6",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"posthog-js": "^1.337.0",
"react": "^19.2.1", "react": "^19.2.1",
"react-day-picker": "^9.11.3", "react-day-picker": "^9.11.3",
"react-dom": "^19.2.1", "react-dom": "^19.2.1",
"react-hook-form": "^7.54.2", "react-hook-form": "^7.54.2",
"react-resizable-panels": "^4.5.9",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"recharts": "^2.15.1", "recharts": "^2.15.1",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.0.1", "tailwind-merge": "^3.0.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2",
"zod": "^3.24.2", "zod": "^3.24.2",
"zustand": "^5.0.10" "zustand": "^5.0.10"
}, },
@ -3608,6 +3622,18 @@
"node": ">=8.0.0" "node": ">=8.0.0"
} }
}, },
"node_modules/@opentelemetry/api-logs": {
"version": "0.208.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.208.0.tgz",
"integrity": "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api": "^1.3.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@opentelemetry/auto-instrumentations-node": { "node_modules/@opentelemetry/auto-instrumentations-node": {
"version": "0.49.2", "version": "0.49.2",
"resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.49.2.tgz", "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.49.2.tgz",
@ -3826,6 +3852,40 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@opentelemetry/exporter-logs-otlp-http": {
"version": "0.208.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-logs-otlp-http/-/exporter-logs-otlp-http-0.208.0.tgz",
"integrity": "sha512-jOv40Bs9jy9bZVLo/i8FwUiuCvbjWDI+ZW13wimJm4LjnlwJxGgB+N/VWOZUTpM+ah/awXeQqKdNlpLf2EjvYg==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api-logs": "0.208.0",
"@opentelemetry/core": "2.2.0",
"@opentelemetry/otlp-exporter-base": "0.208.0",
"@opentelemetry/otlp-transformer": "0.208.0",
"@opentelemetry/sdk-logs": "0.208.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": "^1.3.0"
}
},
"node_modules/@opentelemetry/exporter-logs-otlp-http/node_modules/@opentelemetry/core": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz",
"integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.0.0 <1.10.0"
}
},
"node_modules/@opentelemetry/exporter-trace-otlp-grpc": { "node_modules/@opentelemetry/exporter-trace-otlp-grpc": {
"version": "0.52.1", "version": "0.52.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.52.1.tgz", "resolved": "https://registry.npmjs.org/@opentelemetry/exporter-trace-otlp-grpc/-/exporter-trace-otlp-grpc-0.52.1.tgz",
@ -5265,6 +5325,37 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@opentelemetry/otlp-exporter-base": {
"version": "0.208.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-exporter-base/-/otlp-exporter-base-0.208.0.tgz",
"integrity": "sha512-gMd39gIfVb2OgxldxUtOwGJYSH8P1kVFFlJLuut32L6KgUC4gl1dMhn+YC2mGn0bDOiQYSk/uHOdSjuKp58vvA==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.2.0",
"@opentelemetry/otlp-transformer": "0.208.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": "^1.3.0"
}
},
"node_modules/@opentelemetry/otlp-exporter-base/node_modules/@opentelemetry/core": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz",
"integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.0.0 <1.10.0"
}
},
"node_modules/@opentelemetry/otlp-grpc-exporter-base": { "node_modules/@opentelemetry/otlp-grpc-exporter-base": {
"version": "0.52.1", "version": "0.52.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.52.1.tgz", "resolved": "https://registry.npmjs.org/@opentelemetry/otlp-grpc-exporter-base/-/otlp-grpc-exporter-base-0.52.1.tgz",
@ -5349,6 +5440,91 @@
"@opentelemetry/api": ">=1.4.0 <1.10.0" "@opentelemetry/api": ">=1.4.0 <1.10.0"
} }
}, },
"node_modules/@opentelemetry/otlp-transformer": {
"version": "0.208.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/otlp-transformer/-/otlp-transformer-0.208.0.tgz",
"integrity": "sha512-DCFPY8C6lAQHUNkzcNT9R+qYExvsk6C5Bto2pbNxgicpcSWbe2WHShLxkOxIdNcBiYPdVHv/e7vH7K6TI+C+fQ==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api-logs": "0.208.0",
"@opentelemetry/core": "2.2.0",
"@opentelemetry/resources": "2.2.0",
"@opentelemetry/sdk-logs": "0.208.0",
"@opentelemetry/sdk-metrics": "2.2.0",
"@opentelemetry/sdk-trace-base": "2.2.0",
"protobufjs": "^7.3.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": "^1.3.0"
}
},
"node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/core": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz",
"integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.0.0 <1.10.0"
}
},
"node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/resources": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.2.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-metrics": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-2.2.0.tgz",
"integrity": "sha512-G5KYP6+VJMZzpGipQw7Giif48h6SGQ2PFKEYCybeXJsOCB4fp8azqMAAzE5lnnHK3ZVwYQrgmFbsUJO/zOnwGw==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.2.0",
"@opentelemetry/resources": "2.2.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.9.0 <1.10.0"
}
},
"node_modules/@opentelemetry/otlp-transformer/node_modules/@opentelemetry/sdk-trace-base": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.2.0.tgz",
"integrity": "sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.2.0",
"@opentelemetry/resources": "2.2.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/@opentelemetry/propagation-utils": { "node_modules/@opentelemetry/propagation-utils": {
"version": "0.30.16", "version": "0.30.16",
"resolved": "https://registry.npmjs.org/@opentelemetry/propagation-utils/-/propagation-utils-0.30.16.tgz", "resolved": "https://registry.npmjs.org/@opentelemetry/propagation-utils/-/propagation-utils-0.30.16.tgz",
@ -5747,6 +5923,54 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@opentelemetry/sdk-logs": {
"version": "0.208.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-logs/-/sdk-logs-0.208.0.tgz",
"integrity": "sha512-QlAyL1jRpOeaqx7/leG1vJMp84g0xKP6gJmfELBpnI4O/9xPX+Hu5m1POk9Kl+veNkyth5t19hRlN6tNY1sjbA==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/api-logs": "0.208.0",
"@opentelemetry/core": "2.2.0",
"@opentelemetry/resources": "2.2.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.4.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/core": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.2.0.tgz",
"integrity": "sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.0.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-logs/node_modules/@opentelemetry/resources": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.2.0.tgz",
"integrity": "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.2.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/@opentelemetry/sdk-metrics": { "node_modules/@opentelemetry/sdk-metrics": {
"version": "1.25.1", "version": "1.25.1",
"resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.25.1.tgz", "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-metrics/-/sdk-metrics-1.25.1.tgz",
@ -5892,7 +6116,6 @@
"node_modules/@opentelemetry/semantic-conventions": { "node_modules/@opentelemetry/semantic-conventions": {
"version": "1.39.0", "version": "1.39.0",
"license": "Apache-2.0", "license": "Apache-2.0",
"optional": true,
"engines": { "engines": {
"node": ">=14" "node": ">=14"
} }
@ -5960,6 +6183,21 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@posthog/core": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.18.0.tgz",
"integrity": "sha512-irPbrcopCT0LCRgGM4V8jFuMNCFos6EM4QFf5KA2sHQFC/6pGaHZYoyHcjRUDUKFw4vmpLlmGEXA5ah8x5K4LQ==",
"license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.6"
}
},
"node_modules/@posthog/types": {
"version": "1.337.0",
"resolved": "https://registry.npmjs.org/@posthog/types/-/types-1.337.0.tgz",
"integrity": "sha512-R7J5BIeulNbjmUNfc8FICRa57K1IizSpJBRI6IuJvRFnm3eeczWOw6DKH0NCHXHZiE3XzVcUrJUOKaKXBcdQxQ==",
"license": "MIT"
},
"node_modules/@protobufjs/aspromise": { "node_modules/@protobufjs/aspromise": {
"version": "1.1.2", "version": "1.1.2",
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
@ -6295,6 +6533,34 @@
} }
} }
}, },
"node_modules/@radix-ui/react-context-menu": {
"version": "2.2.16",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.16.tgz",
"integrity": "sha512-O8morBEW+HsVG28gYDZPTrT9UUovQUlJue5YO836tiTJhuIWBm/zQHc7j388sHWtdH/xUZurK9olD2+pcqx5ww==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-menu": "2.1.16",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dialog": { "node_modules/@radix-ui/react-dialog": {
"version": "1.1.15", "version": "1.1.15",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.15.tgz",
@ -6454,6 +6720,37 @@
} }
} }
}, },
"node_modules/@radix-ui/react-hover-card": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.15.tgz",
"integrity": "sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-dismissable-layer": "1.1.11",
"@radix-ui/react-popper": "1.2.8",
"@radix-ui/react-portal": "1.1.9",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-id": { "node_modules/@radix-ui/react-id": {
"version": "1.1.1", "version": "1.1.1",
"license": "MIT", "license": "MIT",
@ -6604,6 +6901,42 @@
} }
} }
}, },
"node_modules/@radix-ui/react-navigation-menu": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz",
"integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-collection": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.11",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-layout-effect": "1.1.1",
"@radix-ui/react-use-previous": "1.1.1",
"@radix-ui/react-visually-hidden": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popover": { "node_modules/@radix-ui/react-popover": {
"version": "1.1.15", "version": "1.1.15",
"license": "MIT", "license": "MIT",
@ -7159,6 +7492,60 @@
} }
} }
}, },
"node_modules/@radix-ui/react-toggle": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.10.tgz",
"integrity": "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-toggle-group": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.11.tgz",
"integrity": "sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-roving-focus": "1.1.11",
"@radix-ui/react-toggle": "1.1.10",
"@radix-ui/react-use-controllable-state": "1.2.2"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-tooltip": { "node_modules/@radix-ui/react-tooltip": {
"version": "1.2.8", "version": "1.2.8",
"license": "MIT", "license": "MIT",
@ -7376,6 +7763,81 @@
"integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==", "integrity": "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@sentry-internal/browser-utils": {
"version": "10.38.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.38.0.tgz",
"integrity": "sha512-UOJtYmdcxHCcV0NPfXFff/a95iXl/E0EhuQ1y0uE0BuZDMupWSF5t2BgC4HaE5Aw3RTjDF3XkSHWoIF6ohy7eA==",
"license": "MIT",
"dependencies": {
"@sentry/core": "10.38.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry-internal/feedback": {
"version": "10.38.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.38.0.tgz",
"integrity": "sha512-JXneg9zRftyfy1Fyfc39bBlF/Qd8g4UDublFFkVvdc1S6JQPlK+P6q22DKz3Pc8w3ySby+xlIq/eTu9Pzqi4KA==",
"license": "MIT",
"dependencies": {
"@sentry/core": "10.38.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry-internal/replay": {
"version": "10.38.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.38.0.tgz",
"integrity": "sha512-YWIkL6/dnaiQyFiZXJ/nN+NXGv/15z45ia86bE/TMq01CubX/DUOilgsFz0pk2v/pg3tp/U2MskLO9Hz0cnqeg==",
"license": "MIT",
"dependencies": {
"@sentry-internal/browser-utils": "10.38.0",
"@sentry/core": "10.38.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry-internal/replay-canvas": {
"version": "10.38.0",
"resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.38.0.tgz",
"integrity": "sha512-OXWM9jEqNYh4VTvrMu7v+z1anz+QKQ/fZXIZdsO7JTT2lGNZe58UUMeoq386M+Saxen8F9SUH7yTORy/8KI5qw==",
"license": "MIT",
"dependencies": {
"@sentry-internal/replay": "10.38.0",
"@sentry/core": "10.38.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry/browser": {
"version": "10.38.0",
"resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.38.0.tgz",
"integrity": "sha512-3phzp1YX4wcQr9mocGWKbjv0jwtuoDBv7+Y6Yfrys/kwyaL84mDLjjQhRf4gL5SX7JdYkhBp4WaiNlR0UC4kTA==",
"license": "MIT",
"dependencies": {
"@sentry-internal/browser-utils": "10.38.0",
"@sentry-internal/feedback": "10.38.0",
"@sentry-internal/replay": "10.38.0",
"@sentry-internal/replay-canvas": "10.38.0",
"@sentry/core": "10.38.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@sentry/core": {
"version": "10.38.0",
"resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.38.0.tgz",
"integrity": "sha512-1pubWDZE5y5HZEPMAZERP4fVl2NH3Ihp1A+vMoVkb3Qc66Diqj1WierAnStlZP7tCx0TBa0dK85GTW/ZFYyB9g==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"node_modules/@so-ric/colorspace": { "node_modules/@so-ric/colorspace": {
"version": "1.1.6", "version": "1.1.6",
"resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz",
@ -7393,6 +7855,112 @@
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@supabase/auth-js": {
"version": "2.93.3",
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.93.3.tgz",
"integrity": "sha512-JdnkHZPKexVGSNONtu89RHU4bxz3X9kxx+f5ZnR5osoCIX+vs/MckwWRPZEybAEvlJXt5xjomDb3IB876QCxWQ==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/functions-js": {
"version": "2.93.3",
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.93.3.tgz",
"integrity": "sha512-qWO0gHNDm/5jRjROv/nv9L6sYabCWS1kzorOLUv3kqCwRvEJLYZga93ppJPrZwOgoZfXmJzvpjY8fODA4HQfBw==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/postgrest-js": {
"version": "2.93.3",
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.93.3.tgz",
"integrity": "sha512-+iJ96g94skO2e4clsRSmEXg22NUOjh9BziapsJSAvnB1grOBf/BA8vGtCHjNOA+Z6lvKXL1jwBqcL9+fS1W/Lg==",
"license": "MIT",
"dependencies": {
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/realtime-js": {
"version": "2.93.3",
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.93.3.tgz",
"integrity": "sha512-gnYpcFzwy8IkezRP4CDbT5I8jOsiOjrWrqTY1B+7jIriXsnpifmlM6RRjLBm9oD7OwPG0/WksniGPdKW67sXOA==",
"license": "MIT",
"dependencies": {
"@types/phoenix": "^1.6.6",
"@types/ws": "^8.18.1",
"tslib": "2.8.1",
"ws": "^8.18.2"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/ssr": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@supabase/ssr/-/ssr-0.8.0.tgz",
"integrity": "sha512-/PKk8kNFSs8QvvJ2vOww1mF5/c5W8y42duYtXvkOSe+yZKRgTTZywYG2l41pjhNomqESZCpZtXuWmYjFRMV+dw==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.2"
},
"peerDependencies": {
"@supabase/supabase-js": "^2.76.1"
}
},
"node_modules/@supabase/ssr/node_modules/cookie": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/@supabase/storage-js": {
"version": "2.93.3",
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.93.3.tgz",
"integrity": "sha512-cw4qXiLrx3apglDM02Tx/w/stvFlrkKocC6vCvuFAz3JtVEl1zH8MUfDQDTH59kJAQVaVdbewrMWSoBob7REnA==",
"license": "MIT",
"dependencies": {
"iceberg-js": "^0.8.1",
"tslib": "2.8.1"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@supabase/supabase-js": {
"version": "2.93.3",
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.93.3.tgz",
"integrity": "sha512-paUqEqdBI9ztr/4bbMoCgeJ6M8ZTm2fpfjSOlzarPuzYveKFM20ZfDZqUpi9CFfYagYj5Iv3m3ztUjaI9/tM1w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@supabase/auth-js": "2.93.3",
"@supabase/functions-js": "2.93.3",
"@supabase/postgrest-js": "2.93.3",
"@supabase/realtime-js": "2.93.3",
"@supabase/storage-js": "2.93.3"
},
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/@swc/helpers": { "node_modules/@swc/helpers": {
"version": "0.5.15", "version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@ -7635,6 +8203,12 @@
"@types/pg": "*" "@types/pg": "*"
} }
}, },
"node_modules/@types/phoenix": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz",
"integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==",
"license": "MIT"
},
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "19.2.9", "version": "19.2.9",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz",
@ -7741,6 +8315,15 @@
"integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/ws": {
"version": "8.18.1",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/yauzl": { "node_modules/@types/yauzl": {
"version": "2.10.3", "version": "2.10.3",
"resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz",
@ -8521,6 +9104,22 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/cmdk": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cmdk/-/cmdk-1.1.1.tgz",
"integrity": "sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "^1.1.1",
"@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-id": "^1.1.0",
"@radix-ui/react-primitive": "^2.0.2"
},
"peerDependencies": {
"react": "^18 || ^19 || ^19.0.0-rc",
"react-dom": "^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/color": { "node_modules/color": {
"version": "5.0.3", "version": "5.0.3",
"resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz", "resolved": "https://registry.npmjs.org/color/-/color-5.0.3.tgz",
@ -8698,6 +9297,17 @@
"integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/core-js": {
"version": "3.48.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz",
"integrity": "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ==",
"hasInstallScript": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/cors": { "node_modules/cors": {
"version": "2.8.6", "version": "2.8.6",
"resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz",
@ -9637,6 +10247,12 @@
"node": "^12.20 || >= 14.13" "node": "^12.20 || >= 14.13"
} }
}, },
"node_modules/fflate": {
"version": "0.4.8",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz",
"integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==",
"license": "MIT"
},
"node_modules/fill-range": { "node_modules/fill-range": {
"version": "7.1.1", "version": "7.1.1",
"license": "MIT", "license": "MIT",
@ -10637,6 +11253,15 @@
"node": ">= 14" "node": ">= 14"
} }
}, },
"node_modules/iceberg-js": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
"integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
"license": "MIT",
"engines": {
"node": ">=20.0.0"
}
},
"node_modules/iconv-lite": { "node_modules/iconv-lite": {
"version": "0.4.24", "version": "0.4.24",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
@ -10700,6 +11325,16 @@
"version": "2.0.4", "version": "2.0.4",
"license": "ISC" "license": "ISC"
}, },
"node_modules/input-otp": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz",
"integrity": "sha512-l3jWwYNvrEa6NTCt7BECfCm48GvwuZzkoeG3gBL2w4CHeOXW3eKFmf9UNYkNfYc3mxMrthMnxjIE07MT0zLBQA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/internmap": { "node_modules/internmap": {
"version": "2.0.3", "version": "2.0.3",
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
@ -11696,6 +12331,16 @@
} }
} }
}, },
"node_modules/next-themes": {
"version": "0.4.6",
"resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz",
"integrity": "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/next/node_modules/postcss": { "node_modules/next/node_modules/postcss": {
"version": "8.4.31", "version": "8.4.31",
"funding": [ "funding": [
@ -12360,6 +13005,83 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/posthog-js": {
"version": "1.337.0",
"resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.337.0.tgz",
"integrity": "sha512-wtzPoMGlCAJGgfjOIjimFRj+8SH4Aojfhc2+s8HdL54ivdQGK24JvwQoz12bKssvDSpFiBPwX1xunkt0f7mpcg==",
"license": "SEE LICENSE IN LICENSE",
"dependencies": {
"@opentelemetry/api": "^1.9.0",
"@opentelemetry/api-logs": "^0.208.0",
"@opentelemetry/exporter-logs-otlp-http": "^0.208.0",
"@opentelemetry/resources": "^2.2.0",
"@opentelemetry/sdk-logs": "^0.208.0",
"@posthog/core": "1.18.0",
"@posthog/types": "1.337.0",
"core-js": "^3.38.1",
"dompurify": "^3.3.1",
"fflate": "^0.4.8",
"preact": "^10.28.2",
"query-selector-shadow-dom": "^1.0.1",
"web-vitals": "^5.1.0"
}
},
"node_modules/posthog-js/node_modules/@opentelemetry/core": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.5.0.tgz",
"integrity": "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.0.0 <1.10.0"
}
},
"node_modules/posthog-js/node_modules/@opentelemetry/resources": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.5.0.tgz",
"integrity": "sha512-F8W52ApePshpoSrfsSk1H2yJn9aKjCrbpQF1M9Qii0GHzbfVeFUB+rc3X4aggyZD8x9Gu3Slua+s6krmq6Dt8g==",
"license": "Apache-2.0",
"dependencies": {
"@opentelemetry/core": "2.5.0",
"@opentelemetry/semantic-conventions": "^1.29.0"
},
"engines": {
"node": "^18.19.0 || >=20.6.0"
},
"peerDependencies": {
"@opentelemetry/api": ">=1.3.0 <1.10.0"
}
},
"node_modules/posthog-js/node_modules/dompurify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.1.tgz",
"integrity": "sha512-qkdCKzLNtrgPFP1Vo+98FRzJnBRGe4ffyCea9IwHB1fyxPOeNTHpLKYGd4Uk9xvNoH0ZoOjwZxNptyMwqrId1Q==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
},
"node_modules/posthog-js/node_modules/web-vitals": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.1.0.tgz",
"integrity": "sha512-ArI3kx5jI0atlTtmV0fWU3fjpLmq/nD3Zr1iFFlJLaqa5wLBkUSzINwBPySCX/8jRyjlmy1Volw1kz1g9XE4Jg==",
"license": "Apache-2.0"
},
"node_modules/preact": {
"version": "10.28.3",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.28.3.tgz",
"integrity": "sha512-tCmoRkPQLpBeWzpmbhryairGnhW9tKV6c6gr/w+RhoRoKEJwsjzipwp//1oCpGPOchvSLaAPlpcJi9MwMmoPyA==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/prismjs": { "node_modules/prismjs": {
"version": "1.30.0", "version": "1.30.0",
"resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz",
@ -12512,6 +13234,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/query-selector-shadow-dom": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/query-selector-shadow-dom/-/query-selector-shadow-dom-1.0.1.tgz",
"integrity": "sha512-lT5yCqEBgfoMYpf3F2xQRK7zEr1rhIIZuceDK6+xRkJQ4NMbHTwXqk4NkwDwQMNqXgG9r9fyHnzwNVs6zV5KRw==",
"license": "MIT"
},
"node_modules/queue-microtask": { "node_modules/queue-microtask": {
"version": "1.2.3", "version": "1.2.3",
"funding": [ "funding": [
@ -12674,6 +13402,16 @@
} }
} }
}, },
"node_modules/react-resizable-panels": {
"version": "4.5.9",
"resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-4.5.9.tgz",
"integrity": "sha512-7l0w2TxYq032F2o6PnfxDbDEKzi1lohDw3BKx4OBkh6uu7uh+Gj1C0Ubpv0/fOO2bRvo+IIQMOoFE0l2LgpeAg==",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
}
},
"node_modules/react-smooth": { "node_modules/react-smooth": {
"version": "4.0.4", "version": "4.0.4",
"resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz",
@ -14136,6 +14874,19 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/vaul": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vaul/-/vaul-1.1.2.tgz",
"integrity": "sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-dialog": "^1.1.1"
},
"peerDependencies": {
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/victory-vendor": { "node_modules/victory-vendor": {
"version": "36.9.2", "version": "36.9.2",
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz",
@ -14374,6 +15125,27 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/ws": {
"version": "8.19.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xdg-basedir": { "node_modules/xdg-basedir": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz",

View file

@ -22,10 +22,13 @@
"@radix-ui/react-avatar": "^1.1.3", "@radix-ui/react-avatar": "^1.1.3",
"@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-checkbox": "^1.1.4",
"@radix-ui/react-collapsible": "^1.1.11", "@radix-ui/react-collapsible": "^1.1.11",
"@radix-ui/react-context-menu": "^2.2.16",
"@radix-ui/react-dialog": "^1.1.6", "@radix-ui/react-dialog": "^1.1.6",
"@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-dropdown-menu": "^2.1.6",
"@radix-ui/react-hover-card": "^1.1.15",
"@radix-ui/react-label": "^2.1.2", "@radix-ui/react-label": "^2.1.2",
"@radix-ui/react-menubar": "^1.1.6", "@radix-ui/react-menubar": "^1.1.6",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-popover": "^1.1.6", "@radix-ui/react-popover": "^1.1.6",
"@radix-ui/react-progress": "^1.1.2", "@radix-ui/react-progress": "^1.1.2",
"@radix-ui/react-radio-group": "^1.2.3", "@radix-ui/react-radio-group": "^1.2.3",
@ -37,29 +40,40 @@
"@radix-ui/react-switch": "^1.1.3", "@radix-ui/react-switch": "^1.1.3",
"@radix-ui/react-tabs": "^1.1.3", "@radix-ui/react-tabs": "^1.1.3",
"@radix-ui/react-toast": "^1.2.6", "@radix-ui/react-toast": "^1.2.6",
"@radix-ui/react-toggle": "^1.1.10",
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.1.8", "@radix-ui/react-tooltip": "^1.1.8",
"@sentry/browser": "^10.38.0",
"@supabase/ssr": "^0.8.0",
"@supabase/supabase-js": "^2.93.3",
"@tailwindcss/typography": "^0.5.13", "@tailwindcss/typography": "^0.5.13",
"autoprefixer": "^10.4.23", "autoprefixer": "^10.4.23",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"dotenv": "^16.5.0", "dotenv": "^16.5.0",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"firebase": "^11.9.1", "firebase": "^11.9.1",
"genkit": "^1.20.0", "genkit": "^1.20.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.475.0", "lucide-react": "^0.475.0",
"marked": "^12.0.2", "marked": "^12.0.2",
"next": "15.5.9", "next": "15.5.9",
"next-themes": "^0.4.6",
"patch-package": "^8.0.0", "patch-package": "^8.0.0",
"posthog-js": "^1.337.0",
"react": "^19.2.1", "react": "^19.2.1",
"react-day-picker": "^9.11.3", "react-day-picker": "^9.11.3",
"react-dom": "^19.2.1", "react-dom": "^19.2.1",
"react-hook-form": "^7.54.2", "react-hook-form": "^7.54.2",
"react-resizable-panels": "^4.5.9",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"recharts": "^2.15.1", "recharts": "^2.15.1",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"tailwind-merge": "^3.0.1", "tailwind-merge": "^3.0.1",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"vaul": "^1.1.2",
"zod": "^3.24.2", "zod": "^3.24.2",
"zustand": "^5.0.10" "zustand": "^5.0.10"
}, },

View file

@ -1,3 +1,4 @@
'use client';
// ...existing code... // ...existing code...
import React, { useState, lazy, Suspense } from 'react'; import React, { useState, lazy, Suspense } from 'react';
import { FileTree } from '../components/FileTree'; import { FileTree } from '../components/FileTree';
@ -232,7 +233,15 @@ function App() {
{/* Main Editor/Preview Split */} {/* Main Editor/Preview Split */}
<div className="flex flex-1 overflow-hidden"> <div className="flex flex-1 overflow-hidden">
<div className="flex-1 flex flex-col"> <div className="flex-1 flex flex-col">
<Toolbar code={code} onTemplatesClick={() => setShowTemplates(true)} /*...existing code...*/ /> <Toolbar
code={code}
onTemplatesClick={() => setShowTemplates(true)}
onPreviewClick={() => setShowPreview(true)}
onNewProjectClick={() => setShowNewProject(true)}
currentPlatform={currentPlatform}
onPlatformChange={setCurrentPlatform}
onTranslateClick={() => setShowTranslation(true)}
/>
<div className="flex-1 flex flex-col"> <div className="flex-1 flex flex-col">
{activeFileId ? ( {activeFileId ? (
<CodeEditor /> <CodeEditor />
@ -327,14 +336,14 @@ function App() {
)} )}
{/* Modals and Drawers */} {/* Modals and Drawers */}
<Suspense fallback={<div className="fixed inset-0 flex items-center justify-center bg-background/80 z-50">Loading</div>}> <Suspense fallback={<div className="fixed inset-0 flex items-center justify-center bg-background/80 z-50">Loading</div>}>
{showTemplates && <TemplatesDrawer open={showTemplates} onSelect={handleTemplateSelect} onClose={() => setShowTemplates(false)} />} {showTemplates && <TemplatesDrawer onSelectTemplate={handleTemplateSelect} onClose={() => setShowTemplates(false)} currentPlatform={currentPlatform} />}
{showPreview && <PreviewModal open={showPreview} code={currentCode} onClose={() => setShowPreview(false)} />} {showPreview && <PreviewModal open={showPreview} code={currentCode} onClose={() => setShowPreview(false)} />}
{showNewProject && <NewProjectModal open={showNewProject} onClose={() => setShowNewProject(false)} />} {showNewProject && <NewProjectModal open={showNewProject} onClose={() => setShowNewProject(false)} onCreateProject={() => {}} />}
</Suspense> </Suspense>
</div> </div>
<Suspense fallback={<div className="fixed inset-0 flex items-center justify-center bg-background/80 z-50">Loading</div>}> <Suspense fallback={<div className="fixed inset-0 flex items-center justify-center bg-background/80 z-50">Loading</div>}>
{showTemplates && <TemplatesDrawer open={showTemplates} onSelect={handleTemplateSelect} onClose={() => setShowTemplates(false)} />} {showTemplates && <TemplatesDrawer onSelectTemplate={handleTemplateSelect} onClose={() => setShowTemplates(false)} currentPlatform={currentPlatform} />}
{showPreview && <PreviewModal open={showPreview} code={currentCode} onClose={() => setShowPreview(false)} />} {showPreview && <PreviewModal open={showPreview} code={currentCode} onClose={() => setShowPreview(false)} />}
{showNewProject && <NewProjectModal open={showNewProject} onClose={() => setShowNewProject(false)} onCreateProject={() => {}} />} {showNewProject && <NewProjectModal open={showNewProject} onClose={() => setShowNewProject(false)} onCreateProject={() => {}} />}
{showTranslation && <TranslationPanel isOpen={showTranslation} onClose={() => setShowTranslation(false)} currentCode={currentCode} currentPlatform={currentPlatform} />} {showTranslation && <TranslationPanel isOpen={showTranslation} onClose={() => setShowTranslation(false)} currentCode={currentCode} currentPlatform={currentPlatform} />}

View file

@ -0,0 +1,66 @@
"use client";
import Link from "next/link";
export default function AuthCodeErrorPage() {
return (
<div className="min-h-screen bg-[#0a0a0a] flex items-center justify-center px-4">
<div className="max-w-md w-full text-center">
<div className="w-16 h-16 mx-auto mb-6 rounded-full bg-red-500/20 flex items-center justify-center">
<svg
className="w-8 h-8 text-red-500"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</div>
<h1 className="text-2xl font-bold text-white mb-2">
Authentication Error
</h1>
<p className="text-[#888] mb-8">
Something went wrong during sign in. This could happen if:
</p>
<ul className="text-left text-[#888] mb-8 space-y-2">
<li className="flex items-start gap-2">
<span className="text-red-500"></span>
<span>The sign in link expired or was already used</span>
</li>
<li className="flex items-start gap-2">
<span className="text-red-500"></span>
<span>You denied access to your account</span>
</li>
<li className="flex items-start gap-2">
<span className="text-red-500"></span>
<span>There was a network error during authentication</span>
</li>
</ul>
<div className="space-y-3">
<Link
href="/auth/login"
className="block w-full py-3 px-4 bg-gradient-to-r from-purple-600 to-blue-600 text-white font-medium rounded-lg hover:opacity-90 transition"
>
Try Again
</Link>
<Link
href="/"
className="block w-full py-3 px-4 bg-[#1a1a1a] text-[#888] font-medium rounded-lg hover:bg-[#222] transition"
>
Go Home
</Link>
</div>
</div>
</div>
);
}

View file

@ -0,0 +1,21 @@
import { createClient } from '@/lib/supabase/server';
import { NextResponse } from 'next/server';
export async function GET(request: Request) {
const { searchParams, origin } = new URL(request.url);
const code = searchParams.get('code');
// Default redirect to /ide after successful OAuth login
const next = searchParams.get('next') ?? '/ide';
if (code) {
const supabase = await createClient();
const { error } = await supabase.auth.exchangeCodeForSession(code);
if (!error) {
return NextResponse.redirect(`${origin}${next}`);
}
console.error('OAuth callback error:', error.message);
}
// Return the user to an error page with instructions
return NextResponse.redirect(`${origin}/auth/auth-code-error`);
}

169
src/app/auth/login/page.tsx Normal file
View file

@ -0,0 +1,169 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useSupabaseAuth } from "@/hooks/use-supabase";
export default function LoginPage() {
const router = useRouter();
const { signIn, signInWithOAuth } = useSupabaseAuth();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
setLoading(true);
const { error } = await signIn(email, password);
if (error) {
setError(error.message);
setLoading(false);
} else {
router.push("/ide");
}
};
const handleOAuth = async (provider: "github" | "google" | "discord") => {
setError("");
const { error } = await signInWithOAuth(provider);
if (error) {
setError(error.message);
}
};
return (
<main className="min-h-screen bg-[#0a0a0a] flex items-center justify-center px-4">
<div className="w-full max-w-md">
{/* Logo */}
<div className="flex justify-center mb-8">
<Link href="/" className="flex items-center gap-2">
<div className="h-10 w-10 rounded-xl bg-gradient-to-br from-purple-500 to-blue-600 flex items-center justify-center">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5">
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
</svg>
</div>
<span className="text-2xl font-bold text-white">AeThex Studio</span>
</Link>
</div>
{/* Card */}
<div className="bg-[#141414] border border-[#222] rounded-2xl p-8">
<h1 className="text-2xl font-semibold text-white text-center mb-2">Welcome back</h1>
<p className="text-[#888] text-center mb-8">Sign in to your AeThex account</p>
{error && (
<div className="bg-red-500/10 border border-red-500/20 text-red-400 px-4 py-3 rounded-lg mb-6 text-sm">
{error}
</div>
)}
{/* OAuth Buttons */}
<div className="space-y-3 mb-6">
<button
onClick={() => handleOAuth("github")}
className="w-full flex items-center justify-center gap-3 bg-[#1b1b1b] hover:bg-[#222] border border-[#333] text-white py-3 px-4 rounded-lg transition-colors"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
Continue with GitHub
</button>
<button
onClick={() => handleOAuth("google")}
className="w-full flex items-center justify-center gap-3 bg-[#1b1b1b] hover:bg-[#222] border border-[#333] text-white py-3 px-4 rounded-lg transition-colors"
>
<svg width="20" height="20" viewBox="0 0 24 24">
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
</svg>
Continue with Google
</button>
<button
onClick={() => handleOAuth("discord")}
className="w-full flex items-center justify-center gap-3 bg-[#1b1b1b] hover:bg-[#222] border border-[#333] text-white py-3 px-4 rounded-lg transition-colors"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="#5865F2">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
</svg>
Continue with Discord
</button>
</div>
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-[#333]"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-4 bg-[#141414] text-[#666]">or continue with email</span>
</div>
</div>
{/* Email Form */}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium text-[#888] mb-2">
Email
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full bg-[#1b1b1b] border border-[#333] rounded-lg px-4 py-3 text-white placeholder-[#666] focus:outline-none focus:border-purple-500 transition-colors"
placeholder="you@example.com"
required
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-[#888] mb-2">
Password
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full bg-[#1b1b1b] border border-[#333] rounded-lg px-4 py-3 text-white placeholder-[#666] focus:outline-none focus:border-purple-500 transition-colors"
placeholder="••••••••"
required
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full bg-white hover:bg-[#e5e5e5] text-[#0a0a0a] font-medium py-3 px-4 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? "Signing in..." : "Sign in"}
</button>
</form>
<p className="mt-6 text-center text-[#888] text-sm">
Don't have an account?{" "}
<Link href="/auth/signup" className="text-purple-400 hover:text-purple-300 transition-colors">
Sign up
</Link>
</p>
</div>
{/* Ecosystem Links */}
<div className="mt-8 text-center">
<p className="text-[#666] text-xs mb-3">Part of the AeThex ecosystem</p>
<div className="flex items-center justify-center gap-4 text-sm">
<a href="https://aethex.dev" className="text-[#888] hover:text-white transition-colors">aethex.dev</a>
<span className="text-[#444]"></span>
<a href="https://aethex.foundation" className="text-[#888] hover:text-white transition-colors">aethex.foundation</a>
<span className="text-[#444]"></span>
<a href="https://aethex.studio" className="text-purple-400 hover:text-purple-300 transition-colors">aethex.studio</a>
</div>
</div>
</div>
</main>
);
}

View file

@ -0,0 +1,230 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useSupabaseAuth } from "@/hooks/use-supabase";
export default function SignUpPage() {
const router = useRouter();
const { signUp, signInWithOAuth } = useSupabaseAuth();
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [error, setError] = useState("");
const [loading, setLoading] = useState(false);
const [success, setSuccess] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError("");
if (password !== confirmPassword) {
setError("Passwords do not match");
return;
}
if (password.length < 8) {
setError("Password must be at least 8 characters");
return;
}
setLoading(true);
const { error } = await signUp(email, password);
if (error) {
setError(error.message);
setLoading(false);
} else {
setSuccess(true);
}
};
const handleOAuth = async (provider: "github" | "google" | "discord") => {
setError("");
const { error } = await signInWithOAuth(provider);
if (error) {
setError(error.message);
}
};
if (success) {
return (
<main className="min-h-screen bg-[#0a0a0a] flex items-center justify-center px-4">
<div className="w-full max-w-md text-center">
<div className="bg-[#141414] border border-[#222] rounded-2xl p-8">
<div className="h-16 w-16 rounded-full bg-green-500/10 flex items-center justify-center mx-auto mb-6">
<svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="2">
<polyline points="20 6 9 17 4 12" />
</svg>
</div>
<h1 className="text-2xl font-semibold text-white mb-2">Check your email</h1>
<p className="text-[#888] mb-6">
We sent a confirmation link to <span className="text-white">{email}</span>
</p>
<Link
href="/auth/login"
className="inline-block bg-white hover:bg-[#e5e5e5] text-[#0a0a0a] font-medium py-3 px-6 rounded-lg transition-colors"
>
Back to login
</Link>
</div>
</div>
</main>
);
}
return (
<main className="min-h-screen bg-[#0a0a0a] flex items-center justify-center px-4">
<div className="w-full max-w-md">
{/* Logo */}
<div className="flex justify-center mb-8">
<Link href="/" className="flex items-center gap-2">
<div className="h-10 w-10 rounded-xl bg-gradient-to-br from-purple-500 to-blue-600 flex items-center justify-center">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5">
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
</svg>
</div>
<span className="text-2xl font-bold text-white">AeThex Studio</span>
</Link>
</div>
{/* Card */}
<div className="bg-[#141414] border border-[#222] rounded-2xl p-8">
<h1 className="text-2xl font-semibold text-white text-center mb-2">Create your account</h1>
<p className="text-[#888] text-center mb-8">Join the AeThex ecosystem</p>
{error && (
<div className="bg-red-500/10 border border-red-500/20 text-red-400 px-4 py-3 rounded-lg mb-6 text-sm">
{error}
</div>
)}
{/* OAuth Buttons */}
<div className="space-y-3 mb-6">
<button
onClick={() => handleOAuth("github")}
className="w-full flex items-center justify-center gap-3 bg-[#1b1b1b] hover:bg-[#222] border border-[#333] text-white py-3 px-4 rounded-lg transition-colors"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
Continue with GitHub
</button>
<button
onClick={() => handleOAuth("google")}
className="w-full flex items-center justify-center gap-3 bg-[#1b1b1b] hover:bg-[#222] border border-[#333] text-white py-3 px-4 rounded-lg transition-colors"
>
<svg width="20" height="20" viewBox="0 0 24 24">
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
</svg>
Continue with Google
</button>
<button
onClick={() => handleOAuth("discord")}
className="w-full flex items-center justify-center gap-3 bg-[#1b1b1b] hover:bg-[#222] border border-[#333] text-white py-3 px-4 rounded-lg transition-colors"
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="#5865F2">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
</svg>
Continue with Discord
</button>
</div>
<div className="relative my-6">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t border-[#333]"></div>
</div>
<div className="relative flex justify-center text-sm">
<span className="px-4 bg-[#141414] text-[#666]">or continue with email</span>
</div>
</div>
{/* Email Form */}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="email" className="block text-sm font-medium text-[#888] mb-2">
Email
</label>
<input
id="email"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full bg-[#1b1b1b] border border-[#333] rounded-lg px-4 py-3 text-white placeholder-[#666] focus:outline-none focus:border-purple-500 transition-colors"
placeholder="you@example.com"
required
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-[#888] mb-2">
Password
</label>
<input
id="password"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full bg-[#1b1b1b] border border-[#333] rounded-lg px-4 py-3 text-white placeholder-[#666] focus:outline-none focus:border-purple-500 transition-colors"
placeholder="••••••••"
required
/>
</div>
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-[#888] mb-2">
Confirm Password
</label>
<input
id="confirmPassword"
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="w-full bg-[#1b1b1b] border border-[#333] rounded-lg px-4 py-3 text-white placeholder-[#666] focus:outline-none focus:border-purple-500 transition-colors"
placeholder="••••••••"
required
/>
</div>
<button
type="submit"
disabled={loading}
className="w-full bg-white hover:bg-[#e5e5e5] text-[#0a0a0a] font-medium py-3 px-4 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? "Creating account..." : "Create account"}
</button>
</form>
<p className="mt-6 text-center text-[#888] text-sm">
Already have an account?{" "}
<Link href="/auth/login" className="text-purple-400 hover:text-purple-300 transition-colors">
Sign in
</Link>
</p>
</div>
{/* Terms */}
<p className="mt-6 text-center text-[#666] text-xs">
By creating an account, you agree to our{" "}
<Link href="/terms" className="text-[#888] hover:text-white transition-colors">Terms of Service</Link>
{" "}and{" "}
<Link href="/privacy" className="text-[#888] hover:text-white transition-colors">Privacy Policy</Link>
</p>
{/* Ecosystem Links */}
<div className="mt-6 text-center">
<p className="text-[#666] text-xs mb-3">Part of the AeThex ecosystem</p>
<div className="flex items-center justify-center gap-4 text-sm">
<a href="https://aethex.dev" className="text-[#888] hover:text-white transition-colors">aethex.dev</a>
<span className="text-[#444]"></span>
<a href="https://aethex.foundation" className="text-[#888] hover:text-white transition-colors">aethex.foundation</a>
<span className="text-[#444]"></span>
<a href="https://aethex.studio" className="text-purple-400 hover:text-purple-300 transition-colors">aethex.studio</a>
</div>
</div>
</div>
</main>
);
}

View file

@ -10,14 +10,136 @@
--card-foreground: 0 0% 3.9%; --card-foreground: 0 0% 3.9%;
--popover: 0 0% 100%; --popover: 0 0% 100%;
--popover-foreground: 0 0% 3.9%; --popover-foreground: 0 0% 3.9%;
--primary: 278 52% 49%; --primary: 262 83% 58%;
--primary-foreground: 0 0% 98%; --primary-foreground: 0 0% 98%;
--secondary: 277 100% 25%; --secondary: 240 4.8% 95.9%;
--secondary-foreground: 0 0% 98%; --secondary-foreground: 240 5.9% 10%;
--muted: 0 0% 96.1%; --muted: 240 4.8% 95.9%;
--muted-foreground: 0 0% 45.1%; --muted-foreground: 240 3.8% 46.1%;
--accent: 180 100% 25%; --accent: 240 4.8% 95.9%;
--accent-foreground: 0 0% 9%; --accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%; --destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%; --destructive-foreground: 0 0% 98%;
--border: 0 0% 89.8%; --border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 262 83% 58%;
--radius: 0.5rem;
}
.dark {
--background: 240 6% 6%;
--foreground: 0 0% 95%;
--card: 240 6% 8%;
--card-foreground: 0 0% 95%;
--popover: 240 6% 8%;
--popover-foreground: 0 0% 95%;
--primary: 262 83% 58%;
--primary-foreground: 0 0% 98%;
--secondary: 240 5% 12%;
--secondary-foreground: 0 0% 95%;
--muted: 240 5% 14%;
--muted-foreground: 240 5% 55%;
--accent: 240 5% 14%;
--accent-foreground: 0 0% 95%;
--destructive: 0 62.8% 45%;
--destructive-foreground: 0 0% 98%;
--border: 240 4% 16%;
--input: 240 4% 16%;
--ring: 262 83% 58%;
}
* {
border-color: hsl(var(--border));
}
html, body {
height: 100%;
margin: 0;
padding: 0;
}
body {
background-color: hsl(var(--background));
color: hsl(var(--foreground));
font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
@layer utilities {
.text-balance {
text-wrap: balance;
}
}
/* Custom scrollbar - minimal and modern */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.1);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.2);
}
::-webkit-scrollbar-corner {
background: transparent;
}
/* IDE-specific scrollbar */
.ide-scroll::-webkit-scrollbar {
width: 10px;
}
.ide-scroll::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.08);
border: 2px solid transparent;
background-clip: padding-box;
border-radius: 5px;
}
.ide-scroll::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.15);
border: 2px solid transparent;
background-clip: padding-box;
}
/* Smooth transitions */
* {
transition-property: background-color, border-color, color, fill, stroke, opacity, box-shadow, transform;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
/* Selection styling */
::selection {
background: rgba(139, 92, 246, 0.3);
color: white;
}
/* Focus rings */
:focus-visible {
outline: 2px solid rgba(139, 92, 246, 0.5);
outline-offset: 2px;
}
/* Hide scrollbar utility */
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}

View file

@ -1,4 +1,4 @@
import { AethexStudio } from "@/components/aethex/aethex-studio"; import { AethexStudio } from "@/src/components/aethex/aethex-studio";
export default function IdePage() { export default function IdePage() {
return ( return (

View file

@ -22,11 +22,11 @@ export default function RootLayout({
crossOrigin="anonymous" crossOrigin="anonymous"
/> />
<link <link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&family=Source+Code+Pro:wght@400&family=Space+Grotesk:wght@400;700&display=swap" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap"
rel="stylesheet" rel="stylesheet"
/> />
</head> </head>
<body className="font-body antialiased"> <body className="font-sans antialiased">
{children} {children}
<Toaster /> <Toaster />
</body> </body>

View file

@ -1,34 +1,458 @@
"use client";
import { useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useSupabaseAuth, useProfile } from "@/hooks/use-supabase";
export default function Page() { export default function Page() {
const router = useRouter();
const { user, loading } = useSupabaseAuth();
const { profile } = useProfile();
const [prompt, setPrompt] = useState("");
const handlePromptSubmit = () => {
if (prompt.trim()) {
sessionStorage.setItem("aethex_prompt", prompt);
router.push("/ide");
}
};
return ( return (
<main className="min-h-screen flex flex-col items-center justify-center bg-gradient-to-br from-gray-950 to-gray-900 text-white"> <main className="min-h-screen bg-[#0a0a0a]">
<div className="max-w-xl w-full px-6 py-12 rounded-xl shadow-2xl bg-black/70 border border-gray-800 flex flex-col items-center"> {/* Fixed Navbar */}
<h1 className="text-4xl font-bold mb-4 tracking-tight">Welcome to AeThex Studio</h1> <nav className="fixed top-0 left-0 right-0 z-50 bg-[#0a0a0a]/90 backdrop-blur-md border-b border-[#1a1a1a]">
<p className="mb-6 text-lg text-gray-300 text-center"> <div className="flex h-14 items-center justify-between px-4 md:px-10 max-w-6xl mx-auto">
The next-generation cross-platform IDE for creators, educators, and teams.<br /> <div className="flex items-center gap-8">
Download or launch AeThex Studio below. <Link href="/" className="flex items-center gap-2">
</p> <div className="h-8 w-8 rounded-lg bg-gradient-to-br from-purple-500 to-blue-600 flex items-center justify-center">
<div className="flex flex-col gap-4 w-full items-center"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5">
<a <polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
href="/ide" </svg>
className="w-full py-3 px-6 rounded-lg bg-blue-600 hover:bg-blue-700 text-white font-semibold text-lg text-center transition"
>
Launch Web IDE
</a>
<a
href="https://github.com/AeThex-LABS/aethex-studio/releases/latest"
target="_blank"
rel="noopener noreferrer"
className="w-full py-3 px-6 rounded-lg bg-gray-800 hover:bg-gray-700 text-gray-100 font-semibold text-lg text-center border border-gray-700 transition"
>
Download Desktop App
</a>
</div> </div>
<p className="mt-8 text-xs text-gray-500 text-center"> <span className="text-xl font-bold text-white">AeThex Studio</span>
Open source on <a href="https://github.com/AeThex-LABS/aethex-studio" className="underline hover:text-blue-400">GitHub</a> </Link>
<div className="hidden md:flex items-center gap-6">
<a href="#features" className="text-sm font-medium text-[#666] hover:text-white transition-colors">Features</a>
<a href="#platforms" className="text-sm font-medium text-[#666] hover:text-white transition-colors">Platforms</a>
<a href="#pricing" className="text-sm font-medium text-[#666] hover:text-white transition-colors">Pricing</a>
<a href="https://github.com/AeThex-LABS/aethex-studio" target="_blank" rel="noopener noreferrer" className="text-sm font-medium text-[#666] hover:text-white transition-colors">GitHub</a>
</div>
</div>
<div className="flex items-center gap-4">
{!loading && (
<>
{user ? (
<>
<Link href="/profile" className="flex items-center gap-2 text-sm text-[#666] hover:text-white transition-colors">
<div className="h-7 w-7 rounded-full bg-gradient-to-br from-purple-500 to-blue-600 flex items-center justify-center text-xs font-medium text-white">
{profile?.username?.[0]?.toUpperCase() || user.email?.[0]?.toUpperCase() || "?"}
</div>
</Link>
<Link href="/ide" className="rounded-full bg-white px-5 py-2 text-sm font-medium text-[#0a0a0a] transition-all hover:bg-[#e5e5e5]">
Launch IDE
</Link>
</>
) : (
<>
<Link href="/auth/login" className="text-sm font-medium text-[#666] hover:text-white transition-colors">Sign in</Link>
<Link href="/auth/signup" className="rounded-full bg-white px-5 py-2 text-sm font-medium text-[#0a0a0a] transition-all hover:bg-[#e5e5e5]">Get started</Link>
</>
)}
</>
)}
</div>
</div>
</nav>
{/* Hero Section - Prompt First */}
<section className="relative flex flex-col items-center px-6 pt-32 pb-20 md:pt-44 md:pb-28 overflow-hidden">
<div className="pointer-events-none absolute inset-0" style={{
background: "radial-gradient(ellipse 80% 50% at 50% -20%, rgba(120, 119, 198, 0.3), transparent)"
}} />
<h1 className="relative max-w-4xl text-center text-4xl font-bold tracking-tight text-white md:text-6xl lg:text-7xl">
Build games for{" "}
<span className="bg-gradient-to-r from-purple-400 via-pink-400 to-blue-400 bg-clip-text text-transparent">
every platform
</span>
</h1>
<p className="relative mt-6 max-w-2xl text-center text-lg leading-relaxed text-[#888] md:text-xl">
The AI-powered cross-platform game IDE. Write once, deploy to Roblox, UEFN, Spatial, and the Web.
</p>
{/* Prompt Input Box */}
<div className="relative mt-10 w-full max-w-2xl px-4">
<div className="bg-[#111] border border-[#222] rounded-2xl p-4 shadow-2xl shadow-purple-500/5">
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handlePromptSubmit();
}
}}
placeholder="Describe your game... e.g. 'Create a tower defense game with wave spawning'"
className="w-full min-h-[60px] bg-transparent text-white text-base resize-none outline-none placeholder-[#555]"
rows={2}
/>
<div className="flex items-center justify-between mt-3 pt-3 border-t border-[#222]">
<div className="flex items-center gap-2">
<button className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-[#1a1a1a] text-[#666] text-sm hover:bg-[#222] hover:text-white transition-colors">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="m16 6-8.414 8.586a2 2 0 0 0 2.829 2.829l8.414-8.586a4 4 0 1 0-5.657-5.657l-8.379 8.551a6 6 0 1 0 8.485 8.485l8.379-8.551" /></svg>
Attach
</button>
<button className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-[#1a1a1a] text-[#666] text-sm hover:bg-[#222] hover:text-white transition-colors">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect width="18" height="7" x="3" y="3" rx="1"/><rect width="9" height="7" x="3" y="14" rx="1"/><rect width="5" height="7" x="16" y="14" rx="1"/></svg>
Template
</button>
</div>
<button
onClick={handlePromptSubmit}
disabled={!prompt.trim()}
className="flex items-center justify-center w-10 h-10 rounded-full bg-white text-[#0a0a0a] disabled:bg-[#333] disabled:text-[#666] disabled:cursor-not-allowed transition-colors hover:bg-[#e5e5e5]"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>
</button>
</div>
</div>
</div>
{/* Platform badges */}
<div className="relative mt-12 flex flex-wrap items-center justify-center gap-3">
{[
{ name: "Roblox", color: "#e11d48" },
{ name: "UEFN", color: "#8b5cf6" },
{ name: "Spatial", color: "#10b981" },
{ name: "Web", color: "#3b82f6" },
].map((platform) => (
<div key={platform.name} className="flex items-center gap-2 rounded-full border border-[#222] bg-[#111] px-4 py-2">
<div className="h-2 w-2 rounded-full" style={{ backgroundColor: platform.color }} />
<span className="text-sm font-medium text-white">{platform.name}</span>
</div>
))}
</div>
</section>
{/* Stats Banner */}
<section className="border-y border-[#1a1a1a] bg-[#0d0d0d]">
<div className="mx-auto flex max-w-5xl flex-col items-center justify-center gap-8 px-6 py-10 md:flex-row md:gap-16">
<div className="text-center">
<p className="text-3xl font-bold text-white">4</p>
<p className="mt-1 text-sm text-[#666]">Platforms supported</p>
</div>
<div className="hidden h-8 w-px bg-[#222] md:block" />
<div className="text-center">
<p className="text-3xl font-bold text-white">AI-Powered</p>
<p className="mt-1 text-sm text-[#666]">Code translation</p>
</div>
<div className="hidden h-8 w-px bg-[#222] md:block" />
<div className="text-center">
<p className="text-3xl font-bold text-white">Open Source</p>
<p className="mt-1 text-sm text-[#666]">MIT Licensed</p>
</div>
</div>
</section>
{/* Why AeThex vs Others */}
<section className="px-6 py-20 md:py-28">
<div className="mx-auto max-w-6xl">
<div className="text-center mb-16">
<p className="text-sm font-medium uppercase tracking-widest text-purple-400">Why AeThex?</p>
<h2 className="mt-4 text-3xl font-bold tracking-tight text-white md:text-5xl">
One IDE. Every platform.
</h2>
<p className="mx-auto mt-4 max-w-2xl text-lg text-[#888]">
While others lock you into one platform, AeThex lets you build for all of them
</p> </p>
</div> </div>
<div className="grid md:grid-cols-2 gap-6">
<div className="rounded-2xl border-2 border-purple-500/50 bg-gradient-to-b from-purple-500/10 to-transparent p-8">
<div className="flex items-center gap-3 mb-6">
<div className="h-10 w-10 rounded-lg bg-gradient-to-br from-purple-500 to-blue-600 flex items-center justify-center">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5">
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
</svg>
</div>
<span className="text-xl font-bold text-white">AeThex Studio</span>
</div>
<ul className="space-y-4">
{[
"Roblox + UEFN + Spatial + Web",
"Full browser-based IDE",
"AI translates between platforms",
"Write once, deploy everywhere",
"Visual UI builder for all platforms",
"Open source (MIT license)",
].map((item, i) => (
<li key={i} className="flex items-center gap-3 text-white">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="2.5"><polyline points="20 6 9 17 4 12"/></svg>
{item}
</li>
))}
</ul>
</div>
<div className="rounded-2xl border border-[#222] bg-[#111] p-8">
<div className="flex items-center gap-3 mb-6">
<div className="h-10 w-10 rounded-lg bg-[#222] flex items-center justify-center text-[#666]">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><circle cx="12" cy="12" r="10"/></svg>
</div>
<span className="text-xl font-bold text-[#666]">Single-platform tools</span>
</div>
<ul className="space-y-4">
{[
"Roblox only OR UEFN only",
"Generates code snippets",
"No cross-platform translation",
"Locked to one ecosystem",
"Platform-specific UI code",
"Proprietary / closed source",
].map((item, i) => (
<li key={i} className="flex items-center gap-3 text-[#666]">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#ef4444" strokeWidth="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
{item}
</li>
))}
</ul>
</div>
</div>
</div>
</section>
{/* Features Grid */}
<section id="features" className="border-t border-[#1a1a1a] bg-[#0d0d0d] px-6 py-20 md:py-28">
<div className="mx-auto max-w-6xl">
<div className="text-center mb-16">
<p className="text-sm font-medium uppercase tracking-widest text-[#666]">Features</p>
<h2 className="mt-4 text-3xl font-bold tracking-tight text-white md:text-5xl">
Everything you need to ship games
</h2>
</div>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
{[
{ title: "AI Code Generation", desc: "Describe what you want in plain English. Get production-ready code instantly.", icon: "code" },
{ title: "Cross-Platform Translation", desc: "Write in Luau, TypeScript, or C#. Deploy to any supported platform.", icon: "translate" },
{ title: "Visual UI Builder", desc: "Design game UIs visually. Export to platform-native components.", icon: "ui" },
{ title: "Live Preview", desc: "See changes across all platforms in real-time. Instant feedback loop.", icon: "preview" },
{ title: "Studio Sync", desc: "Sync directly to Roblox Studio, UEFN, and more via plugin.", icon: "sync" },
{ title: "Asset Library", desc: "Thousands of free assets, sounds, and scripts ready to use.", icon: "assets" },
].map((feature, i) => (
<div key={i} className="group rounded-2xl border border-[#1a1a1a] bg-[#0a0a0a] p-6 transition-all hover:border-[#333] hover:bg-[#111]">
<div className="mb-4 flex h-12 w-12 items-center justify-center rounded-xl bg-[#1a1a1a] text-white transition-all group-hover:bg-purple-500">
{feature.icon === "code" && <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>}
{feature.icon === "translate" && <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="m5 8 6 6"/><path d="m4 14 6-6 2-3"/><path d="M2 5h12"/><path d="M7 2h1"/><path d="m22 22-5-10-5 10"/><path d="M14 18h6"/></svg>}
{feature.icon === "ui" && <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/></svg>}
{feature.icon === "preview" && <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/></svg>}
{feature.icon === "sync" && <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="M4.9 19.1C1 15.2 1 8.8 4.9 4.9"/><path d="M7.8 16.2a5 5 0 0 1 0-8.5"/><circle cx="12" cy="12" r="2"/><path d="M16.2 7.8a5 5 0 0 1 0 8.5"/><path d="M19.1 4.9C23 8.8 23 15.1 19.1 19"/></svg>}
{feature.icon === "assets" && <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><path d="m21.44 11.05-9.19 9.19a6 6 0 0 1-8.49-8.49l8.57-8.57A4 4 0 1 1 18 8.84l-8.59 8.57a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>}
</div>
<h3 className="text-lg font-semibold text-white">{feature.title}</h3>
<p className="mt-2 text-sm text-[#888] leading-relaxed">{feature.desc}</p>
</div>
))}
</div>
</div>
</section>
{/* Platforms */}
<section id="platforms" className="px-6 py-20 md:py-28">
<div className="mx-auto max-w-6xl">
<div className="text-center mb-16">
<p className="text-sm font-medium uppercase tracking-widest text-[#666]">Platforms</p>
<h2 className="mt-4 text-3xl font-bold tracking-tight text-white md:text-5xl">
Deploy everywhere
</h2>
</div>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
{[
{ name: "Roblox", color: "#e11d48", lang: "Luau", users: "70M+ daily players" },
{ name: "UEFN", color: "#8b5cf6", lang: "Verse", users: "Fortnite Creative" },
{ name: "Spatial", color: "#10b981", lang: "C#", users: "Metaverse platform" },
{ name: "Web", color: "#3b82f6", lang: "TypeScript", users: "HTML5 Canvas" },
].map((platform) => (
<div key={platform.name} className="rounded-2xl border border-[#1a1a1a] bg-[#0d0d0d] p-6 text-center hover:border-[#333] transition-colors">
<div className="mx-auto mb-4 h-16 w-16 rounded-2xl flex items-center justify-center" style={{ backgroundColor: platform.color + "15" }}>
<div className="h-8 w-8 rounded-lg" style={{ backgroundColor: platform.color }} />
</div>
<h3 className="text-xl font-bold text-white">{platform.name}</h3>
<p className="mt-1 text-sm text-purple-400 font-medium">{platform.lang}</p>
<p className="mt-2 text-xs text-[#666]">{platform.users}</p>
</div>
))}
</div>
</div>
</section>
{/* Pricing */}
<section id="pricing" className="border-t border-[#1a1a1a] bg-[#0d0d0d] px-6 py-20 md:py-28">
<div className="mx-auto max-w-6xl">
<div className="text-center mb-16">
<p className="text-sm font-medium uppercase tracking-widest text-[#666]">Pricing</p>
<h2 className="mt-4 text-3xl font-bold tracking-tight text-white md:text-5xl">
Simple, transparent pricing
</h2>
<p className="mx-auto mt-4 max-w-xl text-lg text-[#888]">
Start free. Upgrade when you need more power.
</p>
</div>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">
<div className="rounded-2xl border border-[#1a1a1a] bg-[#0a0a0a] p-6">
<h3 className="text-xl font-bold text-white">Free</h3>
<p className="mt-2 text-sm text-[#666] h-10">Get started with AeThex</p>
<p className="mt-4"><span className="text-4xl font-bold text-white">$0</span><span className="text-[#666] ml-1">/mo</span></p>
<Link href="/auth/signup" className="mt-6 block w-full rounded-lg border border-[#333] py-2.5 text-center text-sm font-medium text-white hover:bg-[#1a1a1a] transition-colors">
Get Started
</Link>
<ul className="mt-6 space-y-3">
{["50 AI generations/month", "2 projects", "Community support", "All 4 platforms"].map((item, i) => (
<li key={i} className="flex items-start gap-2 text-sm text-[#888]">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="2" className="mt-0.5 shrink-0"><polyline points="20 6 9 17 4 12"/></svg>
{item}
</li>
))}
</ul>
</div>
<div className="rounded-2xl border-2 border-purple-500 bg-gradient-to-b from-purple-500/10 to-[#0a0a0a] p-6 relative">
<div className="absolute -top-3 left-1/2 -translate-x-1/2 px-3 py-1 bg-purple-500 rounded-full text-xs font-medium text-white">Popular</div>
<h3 className="text-xl font-bold text-white">Pro</h3>
<p className="mt-2 text-sm text-[#666] h-10">For serious game developers</p>
<p className="mt-4"><span className="text-4xl font-bold text-white">$15</span><span className="text-[#666] ml-1">/mo</span></p>
<Link href="/auth/signup" className="mt-6 block w-full rounded-lg bg-purple-500 py-2.5 text-center text-sm font-medium text-white hover:bg-purple-600 transition-colors">
Get Started
</Link>
<ul className="mt-6 space-y-3">
{["500 AI generations/month", "Unlimited projects", "Priority support", "Advanced AI models", "Team collaboration"].map((item, i) => (
<li key={i} className="flex items-start gap-2 text-sm text-[#888]">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="2" className="mt-0.5 shrink-0"><polyline points="20 6 9 17 4 12"/></svg>
{item}
</li>
))}
</ul>
</div>
<div className="rounded-2xl border border-[#1a1a1a] bg-[#0a0a0a] p-6">
<h3 className="text-xl font-bold text-white">Team</h3>
<p className="mt-2 text-sm text-[#666] h-10">For studios and teams</p>
<p className="mt-4"><span className="text-4xl font-bold text-white">$40</span><span className="text-[#666] ml-1">/mo</span></p>
<Link href="/auth/signup" className="mt-6 block w-full rounded-lg border border-[#333] py-2.5 text-center text-sm font-medium text-white hover:bg-[#1a1a1a] transition-colors">
Get Started
</Link>
<ul className="mt-6 space-y-3">
{["2000 AI generations/month", "Shared team workspace", "Admin controls", "Custom templates", "Dedicated support"].map((item, i) => (
<li key={i} className="flex items-start gap-2 text-sm text-[#888]">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="2" className="mt-0.5 shrink-0"><polyline points="20 6 9 17 4 12"/></svg>
{item}
</li>
))}
</ul>
</div>
<div className="rounded-2xl border border-[#1a1a1a] bg-[#0a0a0a] p-6">
<h3 className="text-xl font-bold text-white">Enterprise</h3>
<p className="mt-2 text-sm text-[#666] h-10">Custom solutions at scale</p>
<p className="mt-4"><span className="text-4xl font-bold text-white">Custom</span></p>
<a href="mailto:enterprise@aethex.studio" className="mt-6 block w-full rounded-lg border border-[#333] py-2.5 text-center text-sm font-medium text-white hover:bg-[#1a1a1a] transition-colors">
Contact Sales
</a>
<ul className="mt-6 space-y-3">
{["Unlimited generations", "SLA guarantee", "On-premise option", "Custom integrations", "24/7 support"].map((item, i) => (
<li key={i} className="flex items-start gap-2 text-sm text-[#888]">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#22c55e" strokeWidth="2" className="mt-0.5 shrink-0"><polyline points="20 6 9 17 4 12"/></svg>
{item}
</li>
))}
</ul>
</div>
</div>
</div>
</section>
{/* Final CTA */}
<section className="px-6 py-20 md:py-28">
<div className="mx-auto max-w-4xl text-center">
<h2 className="text-3xl font-bold tracking-tight text-white md:text-5xl">
Ready to build cross-platform games?
</h2>
<p className="mx-auto mt-4 max-w-xl text-lg text-[#888]">
Join thousands of developers building the next generation of games.
</p>
<div className="mt-10 flex flex-col sm:flex-row items-center justify-center gap-4">
<Link href="/ide" className="flex items-center gap-2 rounded-full bg-white px-8 py-3 text-base font-medium text-[#0a0a0a] transition-all hover:bg-[#e5e5e5]">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
Launch IDE
</Link>
<a href="https://github.com/AeThex-LABS/aethex-studio" target="_blank" rel="noopener noreferrer" className="flex items-center gap-2 rounded-full border border-[#333] px-8 py-3 text-base font-medium text-white transition-all hover:bg-[#1a1a1a]">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
Star on GitHub
</a>
</div>
</div>
</section>
{/* Footer */}
<footer className="border-t border-[#1a1a1a] bg-[#0a0a0a]">
<div className="mx-auto max-w-6xl px-6 py-12">
<div className="grid gap-8 md:grid-cols-4">
<div className="md:col-span-1">
<div className="flex items-center gap-2 mb-4">
<div className="h-8 w-8 rounded-lg bg-gradient-to-br from-purple-500 to-blue-600 flex items-center justify-center">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5">
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
</svg>
</div>
<span className="text-lg font-bold text-white">AeThex</span>
</div>
<p className="text-sm text-[#666]">The cross-platform game development IDE.</p>
</div>
<div>
<h4 className="text-sm font-semibold text-white mb-4">Product</h4>
<ul className="space-y-2 text-sm">
<li><Link href="/ide" className="text-[#666] hover:text-white transition-colors">IDE</Link></li>
<li><a href="#features" className="text-[#666] hover:text-white transition-colors">Features</a></li>
<li><a href="#pricing" className="text-[#666] hover:text-white transition-colors">Pricing</a></li>
</ul>
</div>
<div>
<h4 className="text-sm font-semibold text-white mb-4">Ecosystem</h4>
<ul className="space-y-2 text-sm">
<li><a href="https://aethex.dev" className="text-[#666] hover:text-white transition-colors">aethex.dev</a></li>
<li><a href="https://aethex.foundation" className="text-[#666] hover:text-white transition-colors">aethex.foundation</a></li>
<li><a href="https://aethex.studio" className="text-purple-400 hover:text-purple-300 transition-colors">aethex.studio</a></li>
</ul>
</div>
<div>
<h4 className="text-sm font-semibold text-white mb-4">Legal</h4>
<ul className="space-y-2 text-sm">
<li><Link href="/terms" className="text-[#666] hover:text-white transition-colors">Terms</Link></li>
<li><Link href="/privacy" className="text-[#666] hover:text-white transition-colors">Privacy</Link></li>
</ul>
</div>
</div>
<div className="mt-12 pt-8 border-t border-[#1a1a1a] flex flex-col md:flex-row items-center justify-between gap-4">
<p className="text-xs text-[#444]">© 2026 AeThex Labs. All rights reserved.</p>
<div className="flex items-center gap-4">
<a href="https://github.com/AeThex-LABS" target="_blank" rel="noopener noreferrer" className="text-[#666] hover:text-white transition-colors">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>
</a>
<a href="https://discord.gg/aethex" target="_blank" rel="noopener noreferrer" className="text-[#666] hover:text-white transition-colors">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028c.462-.63.874-1.295 1.226-1.994a.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/></svg>
</a>
<a href="https://x.com/aethexlabs" target="_blank" rel="noopener noreferrer" className="text-[#666] hover:text-white transition-colors">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
</a>
</div>
</div>
</div>
</footer>
</main> </main>
); );
} }

249
src/app/profile/page.tsx Normal file
View file

@ -0,0 +1,249 @@
"use client";
import { useEffect, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useSupabaseAuth, useProfile } from "@/hooks/use-supabase";
import { getSupabase } from "@/lib/supabase/client";
export default function ProfilePage() {
const router = useRouter();
const { user, loading: authLoading, signOut } = useSupabaseAuth();
const { profile, loading: profileLoading } = useProfile();
const [username, setUsername] = useState("");
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState("");
useEffect(() => {
if (!authLoading && !user) {
router.push("/auth/login");
}
}, [user, authLoading, router]);
useEffect(() => {
if (profile?.username) {
setUsername(profile.username);
}
}, [profile]);
const handleSave = async () => {
if (!user) return;
setSaving(true);
setMessage("");
const supabase = getSupabase();
const { error } = await supabase
.from("profiles")
.update({ username, updated_at: new Date().toISOString() })
.eq("id", user.id);
if (error) {
setMessage("Error saving profile: " + error.message);
} else {
setMessage("Profile saved successfully!");
}
setSaving(false);
};
const handleSignOut = async () => {
await signOut();
router.push("/");
};
if (authLoading || profileLoading) {
return (
<main className="min-h-screen bg-[#0a0a0a] flex items-center justify-center">
<div className="animate-spin h-8 w-8 border-2 border-purple-500 border-t-transparent rounded-full"></div>
</main>
);
}
if (!user) {
return null;
}
const subscriptionColors: Record<string, string> = {
free: "bg-gray-500",
studio: "bg-blue-500",
pro: "bg-purple-500",
enterprise: "bg-gradient-to-r from-purple-500 to-blue-500",
};
return (
<main className="min-h-screen bg-[#0a0a0a]">
{/* Header */}
<nav className="border-b border-[#222] bg-[#141414]">
<div className="max-w-4xl mx-auto px-4 h-14 flex items-center justify-between">
<Link href="/" className="flex items-center gap-2">
<div className="h-8 w-8 rounded-lg bg-gradient-to-br from-purple-500 to-blue-600 flex items-center justify-center">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="white" strokeWidth="2.5">
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
</svg>
</div>
<span className="text-lg font-bold text-white">AeThex Studio</span>
</Link>
<div className="flex items-center gap-4">
<Link href="/ide" className="text-sm text-[#888] hover:text-white transition-colors">
Open IDE
</Link>
<button
onClick={handleSignOut}
className="text-sm text-[#888] hover:text-white transition-colors"
>
Sign out
</button>
</div>
</div>
</nav>
<div className="max-w-4xl mx-auto px-4 py-12">
<h1 className="text-3xl font-bold text-white mb-8">Profile</h1>
{message && (
<div className={`px-4 py-3 rounded-lg mb-6 text-sm ${
message.includes("Error")
? "bg-red-500/10 border border-red-500/20 text-red-400"
: "bg-green-500/10 border border-green-500/20 text-green-400"
}`}>
{message}
</div>
)}
<div className="grid gap-6 md:grid-cols-2">
{/* Profile Card */}
<div className="bg-[#141414] border border-[#222] rounded-2xl p-6">
<div className="flex items-start gap-4 mb-6">
<div className="h-16 w-16 rounded-full bg-gradient-to-br from-purple-500 to-blue-600 flex items-center justify-center text-2xl font-bold text-white">
{username?.[0]?.toUpperCase() || user.email?.[0]?.toUpperCase() || "?"}
</div>
<div className="flex-1">
<h2 className="text-xl font-semibold text-white">
{username || "Set your username"}
</h2>
<p className="text-[#888] text-sm">{user.email}</p>
<div className="mt-2">
<span className={`inline-block px-2 py-1 rounded text-xs font-medium text-white ${subscriptionColors[profile?.subscription_tier || "free"]}`}>
{(profile?.subscription_tier || "free").toUpperCase()}
</span>
</div>
</div>
</div>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-[#888] mb-2">
Username
</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="w-full bg-[#1b1b1b] border border-[#333] rounded-lg px-4 py-3 text-white placeholder-[#666] focus:outline-none focus:border-purple-500 transition-colors"
placeholder="Enter username"
/>
</div>
<div>
<label className="block text-sm font-medium text-[#888] mb-2">
Email
</label>
<input
type="email"
value={user.email || ""}
disabled
className="w-full bg-[#1b1b1b] border border-[#333] rounded-lg px-4 py-3 text-[#666] cursor-not-allowed"
/>
</div>
<button
onClick={handleSave}
disabled={saving}
className="w-full bg-white hover:bg-[#e5e5e5] text-[#0a0a0a] font-medium py-3 px-4 rounded-lg transition-colors disabled:opacity-50"
>
{saving ? "Saving..." : "Save changes"}
</button>
</div>
</div>
{/* Stats Card */}
<div className="bg-[#141414] border border-[#222] rounded-2xl p-6">
<h3 className="text-lg font-semibold text-white mb-6">Usage</h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<span className="text-[#888]">Translations used</span>
<span className="text-white font-medium">{profile?.translation_count || 0}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-[#888]">Account created</span>
<span className="text-white font-medium">
{profile?.created_at ? new Date(profile.created_at).toLocaleDateString() : "—"}
</span>
</div>
<div className="flex items-center justify-between">
<span className="text-[#888]">Subscription</span>
<span className="text-white font-medium capitalize">{profile?.subscription_tier || "Free"}</span>
</div>
</div>
{profile?.subscription_tier === "free" && (
<div className="mt-6 p-4 bg-purple-500/10 border border-purple-500/20 rounded-lg">
<p className="text-purple-400 text-sm mb-3">Upgrade to Pro for unlimited translations</p>
<Link
href="/pricing"
className="inline-block bg-purple-500 hover:bg-purple-600 text-white font-medium py-2 px-4 rounded-lg text-sm transition-colors"
>
View plans
</Link>
</div>
)}
</div>
{/* Connected Accounts */}
<div className="bg-[#141414] border border-[#222] rounded-2xl p-6 md:col-span-2">
<h3 className="text-lg font-semibold text-white mb-6">Connected accounts</h3>
<div className="grid gap-4 sm:grid-cols-3">
<div className="flex items-center gap-3 p-4 bg-[#1b1b1b] rounded-lg border border-[#333]">
<svg width="24" height="24" viewBox="0 0 24 24" fill="white">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
<div>
<p className="text-white font-medium">GitHub</p>
<p className="text-[#666] text-xs">Not connected</p>
</div>
</div>
<div className="flex items-center gap-3 p-4 bg-[#1b1b1b] rounded-lg border border-[#333]">
<svg width="24" height="24" viewBox="0 0 24 24">
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
</svg>
<div>
<p className="text-white font-medium">Google</p>
<p className="text-[#666] text-xs">Not connected</p>
</div>
</div>
<div className="flex items-center gap-3 p-4 bg-[#1b1b1b] rounded-lg border border-[#333]">
<svg width="24" height="24" viewBox="0 0 24 24" fill="#5865F2">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z"/>
</svg>
<div>
<p className="text-white font-medium">Discord</p>
<p className="text-[#666] text-xs">Not connected</p>
</div>
</div>
</div>
</div>
</div>
{/* Ecosystem Links */}
<div className="mt-12 text-center border-t border-[#222] pt-8">
<p className="text-[#666] text-xs mb-3">Your AeThex account works across</p>
<div className="flex items-center justify-center gap-6 text-sm">
<a href="https://aethex.dev" className="text-[#888] hover:text-white transition-colors">aethex.dev</a>
<a href="https://aethex.foundation" className="text-[#888] hover:text-white transition-colors">aethex.foundation</a>
<a href="https://aethex.studio" className="text-purple-400 hover:text-purple-300 transition-colors">aethex.studio</a>
</div>
</div>
</div>
</main>
);
}

View file

@ -1,17 +1,14 @@
"use client"; "use client";
import { useState } from "react"; import { useState, useMemo } from "react";
import { import { toast } from "sonner";
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "../ui/resizable";
import { Navbar } from "./navbar"; import { Navbar } from "./navbar";
import FileNavigator from "./file-navigator"; import FileNavigator from "./file-navigator";
import { MainView } from "./main-view"; import { MainView } from "./main-view";
import { BottomPanel } from "./bottom-panel"; import { BottomPanel } from "./bottom-panel";
import AiAssistant from "./ai-assistant"; import AiAssistant from "./ai-assistant";
import { StatusBar } from "./status-bar";
import { import {
initialFileTree, initialFileTree,
File, File,
@ -20,12 +17,83 @@ import {
} from "../../lib/aethex-data"; } from "../../lib/aethex-data";
import { NewProjectModal } from "./new-project-modal"; import { NewProjectModal } from "./new-project-modal";
import { ScriptTemplate, getTemplatesForPlatform } from "../../lib/templates"; import { ScriptTemplate, getTemplatesForPlatform } from "../../lib/templates";
import { cn } from "@/lib/utils";
import { CommandPalette, createDefaultCommands } from "../CommandPalette";
import { SettingsPanel } from "./settings-panel";
import { GitPanel } from "./git-panel";
import { ExportModal } from "./export-modal";
export function AethexStudio() { export function AethexStudio() {
const [openFiles, setOpenFiles] = useState<File[]>([]); const [openFiles, setOpenFiles] = useState<File[]>([]);
const [activeTab, setActiveTab] = useState<string>(openFiles[0]?.id || ""); const [activeTab, setActiveTab] = useState<string>(openFiles[0]?.id || "");
const [fileTree, setFileTree] = useState<FolderNode>(initialFileTree); const [fileTree, setFileTree] = useState<FolderNode>(initialFileTree);
const [isNewProjectModalOpen, setIsNewProjectModalOpen] = useState(false); const [isNewProjectModalOpen, setIsNewProjectModalOpen] = useState(false);
const [isTemplatesOpen, setIsTemplatesOpen] = useState(false);
const [sidebarOpen, setSidebarOpen] = useState(true);
const [aiOpen, setAiOpen] = useState(true);
const [bottomPanelOpen, setBottomPanelOpen] = useState(true);
const [searchOpen, setSearchOpen] = useState(false);
const [settingsOpen, setSettingsOpen] = useState(false);
const [gitOpen, setGitOpen] = useState(false);
const [exportOpen, setExportOpen] = useState(false);
const [currentPlatform, setCurrentPlatform] = useState<"roblox" | "uefn" | "spatial" | "web">("roblox");
const handleQuickNewFile = () => {
const timestamp = Date.now();
const newFile: File = {
id: `untitled-${timestamp}`,
name: `untitled-${openFiles.length + 1}.lua`,
language: "lua",
content: `-- New Lua Script\n-- Created with AeThex Studio\n\nlocal module = {}\n\nfunction module.init()\n print("Hello, World!")\nend\n\nreturn module\n`,
};
setOpenFiles((prev) => [...prev, newFile]);
setActiveTab(newFile.id);
};
const handleTemplateSelect = (template: ScriptTemplate) => {
const timestamp = Date.now();
const ext = template.platform === 'web' ? 'ts' : 'lua';
const newFile: File = {
id: `${template.name.toLowerCase().replace(/\s+/g, '-')}-${timestamp}`,
name: `${template.name.replace(/\s+/g, '')}.${ext}`,
language: template.platform === 'web' ? 'typescript' : 'lua',
content: template.code,
};
setOpenFiles((prev) => [...prev, newFile]);
setActiveTab(newFile.id);
setIsTemplatesOpen(false);
};
const handleRun = () => {
// Show a toast/log in the console panel
console.log("[AeThex] Running project...");
toast.success("Project started!", {
description: "Check the Console panel for output",
});
};
const handleSave = () => {
const activeFile = openFiles.find(f => f.id === activeTab);
if (activeFile) {
// In a real app, this would save to backend/filesystem
console.log("[AeThex] Saving file:", activeFile.name);
toast.success(`Saved "${activeFile.name}"`);
} else {
toast.error("No file open to save");
}
};
const handleSettings = () => {
setSettingsOpen(true);
};
const handleGit = () => {
setGitOpen(true);
};
const handleExport = () => {
setExportOpen(true);
};
const handleOpenFile = (file: File) => { const handleOpenFile = (file: File) => {
if (!openFiles.find((f) => f.id === file.id)) { if (!openFiles.find((f) => f.id === file.id)) {
@ -57,14 +125,232 @@ export function AethexStudio() {
setOpenFiles([...openFiles, newFile]); setOpenFiles([...openFiles, newFile]);
}; };
const handleCreateFile = (name: string) => {
const ext = name.split('.').pop()?.toLowerCase();
let language = 'text';
if (ext === 'lua' || ext === 'luau') language = 'lua';
else if (ext === 'ts' || ext === 'tsx') language = 'typescript';
else if (ext === 'js' || ext === 'jsx') language = 'javascript';
else if (ext === 'json') language = 'json';
const newFileNode: FileNode = {
type: 'file',
name,
language,
content: '',
};
// Add to file tree root (src folder)
setFileTree(prev => {
const srcFolder = prev.children.find(c => c.type === 'folder' && c.name === 'src') as FolderNode | undefined;
if (srcFolder) {
return {
...prev,
children: prev.children.map(child =>
child === srcFolder
? { ...srcFolder, children: [...srcFolder.children, newFileNode] }
: child
)
};
}
return {
...prev,
children: [...prev.children, newFileNode]
};
});
};
const handleCreateFolder = (name: string) => {
const newFolderNode: FolderNode = {
type: 'folder',
name,
children: [],
};
// Add to file tree root (src folder)
setFileTree(prev => {
const srcFolder = prev.children.find(c => c.type === 'folder' && c.name === 'src') as FolderNode | undefined;
if (srcFolder) {
return {
...prev,
children: prev.children.map(child =>
child === srcFolder
? { ...srcFolder, children: [...srcFolder.children, newFolderNode] }
: child
)
};
}
return {
...prev,
children: [...prev.children, newFolderNode]
};
});
};
// Command palette commands
const commands = useMemo(() => createDefaultCommands({
onNewProject: () => setIsNewProjectModalOpen(true),
onTemplates: () => setIsTemplatesOpen(true),
onPreview: handleRun,
onExport: () => {
const activeFile = openFiles.find(f => f.id === activeTab);
if (activeFile) {
const blob = new Blob([activeFile.content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = activeFile.name;
a.click();
URL.revokeObjectURL(url);
toast.success(`Exported "${activeFile.name}"`);
} else {
toast.error("No file open to export");
}
},
onCopy: () => {
const activeFile = openFiles.find(f => f.id === activeTab);
if (activeFile) {
navigator.clipboard.writeText(activeFile.content);
toast.success("Code copied to clipboard");
} else {
toast.error("No file open to copy");
}
},
}), [openFiles, activeTab]);
return ( return (
<div className="aethex-studio"> <div className="flex h-full flex-col bg-[#0d0d10] text-foreground">
<Navbar /> {/* Top navbar */}
<FileNavigator fileTree={fileTree} onOpenFile={handleOpenFile} /> <Navbar
<MainView /> onToggleSidebar={() => setSidebarOpen(!sidebarOpen)}
onToggleAI={() => setAiOpen(!aiOpen)}
onToggleBottomPanel={() => setBottomPanelOpen(!bottomPanelOpen)}
onNewFile={handleQuickNewFile}
onSearch={() => setSearchOpen(true)}
onRun={handleRun}
onSave={handleSave}
onSettings={handleSettings}
onGit={handleGit}
onExport={handleExport}
sidebarOpen={sidebarOpen}
aiOpen={aiOpen}
bottomPanelOpen={bottomPanelOpen}
/>
{/* Main content area */}
<div className="flex-1 flex overflow-hidden">
{/* Left sidebar - File Navigator */}
{sidebarOpen && (
<div className="w-[220px] flex-shrink-0 bg-[#12121a] border-r border-white/5 overflow-hidden">
<FileNavigator
fileTree={fileTree}
onOpenFile={handleOpenFile}
onCreateFile={handleCreateFile}
onCreateFolder={handleCreateFolder}
/>
</div>
)}
{/* Center - Editor + Bottom Panel */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* Editor area */}
<div className={cn("flex-1 overflow-hidden", bottomPanelOpen ? "h-[70%]" : "h-full")}>
<MainView
openFiles={openFiles}
activeFileId={activeTab}
onFileSelect={setActiveTab}
onFileClose={handleCloseFile}
onNewFile={handleQuickNewFile}
onOpenTemplates={() => setIsTemplatesOpen(true)}
/>
</div>
{/* Bottom panel */}
{bottomPanelOpen && (
<div className="h-[200px] flex-shrink-0 bg-[#12121a] border-t border-white/5">
<BottomPanel /> <BottomPanel />
</div>
)}
</div>
{/* Right sidebar - AI Assistant */}
{aiOpen && (
<div className="w-[320px] flex-shrink-0 bg-[#12121a] border-l border-white/5 overflow-hidden">
<AiAssistant /> <AiAssistant />
<NewProjectModal onCreate={handleCreateProject} onClose={() => setIsNewProjectModalOpen(false)} /> </div>
)}
</div>
{/* Status bar */}
<StatusBar platform="roblox" language="Lua" />
{/* Command Palette */}
<CommandPalette
open={searchOpen}
onClose={() => setSearchOpen(false)}
commands={commands}
/>
{/* Modals */}
{isNewProjectModalOpen && (
<NewProjectModal
onCreate={handleCreateProject}
onClose={() => setIsNewProjectModalOpen(false)}
/>
)}
{/* Templates Modal */}
{isTemplatesOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm">
<div className="bg-[#12121a] border border-white/10 rounded-xl w-full max-w-2xl max-h-[80vh] overflow-hidden shadow-2xl">
<div className="flex items-center justify-between px-4 py-3 border-b border-white/10">
<h2 className="text-lg font-semibold">Choose a Template</h2>
<button
onClick={() => setIsTemplatesOpen(false)}
className="text-muted-foreground hover:text-foreground transition-colors"
>
</button>
</div>
<div className="p-4 overflow-y-auto max-h-[60vh]">
<div className="grid grid-cols-2 gap-3">
{getTemplatesForPlatform('roblox').map((template) => (
<button
key={template.name}
onClick={() => handleTemplateSelect(template)}
className="text-left p-3 rounded-lg border border-white/5 bg-white/[0.02] hover:bg-white/5 hover:border-white/10 transition-all group"
>
<div className="flex items-center gap-2 mb-1">
<div className="h-6 w-6 rounded bg-red-500/20 flex items-center justify-center">
<span className="text-xs">🎮</span>
</div>
<span className="font-medium text-sm group-hover:text-foreground">{template.name}</span>
</div>
<p className="text-xs text-muted-foreground line-clamp-2">{template.description}</p>
<div className="mt-2 flex items-center gap-2">
<span className="text-[10px] px-1.5 py-0.5 rounded bg-red-500/20 text-red-400">Roblox</span>
<span className="text-[10px] text-muted-foreground">{template.category}</span>
</div>
</button>
))}
</div>
</div>
</div>
</div>
)}
{/* Settings Panel */}
<SettingsPanel open={settingsOpen} onClose={() => setSettingsOpen(false)} />
{/* Git Panel */}
<GitPanel open={gitOpen} onClose={() => setGitOpen(false)} />
{/* Export Modal */}
<ExportModal
open={exportOpen}
onClose={() => setExportOpen(false)}
platform={currentPlatform}
/>
</div> </div>
); );
} }

View file

@ -1,5 +1,6 @@
"use client"; "use client";
import { useState, useRef, useEffect } from "react";
import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
@ -21,25 +22,220 @@ import {
MessageSquarePlus, MessageSquarePlus,
FlaskConical, FlaskConical,
BookText, BookText,
User,
Zap,
RefreshCw,
} from "lucide-react"; } from "lucide-react";
import { AethexLogo } from "./icons";
import { useState, useRef, useEffect, memo } from "react";
import { aiHelpFromPrompt } from "@/ai/flows/ai-help-from-prompt";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { marked } from "marked";
import SyntaxHighlighter from "react-syntax-highlighter";
import { atomOneDark } from "react-syntax-highlighter/dist/esm/styles/hljs";
import { useToast } from "@/hooks/use-toast";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
interface Message {
id: string;
role: "user" | "assistant";
content: string;
timestamp: Date;
}
const suggestedPrompts = [
{ icon: Code, text: "Generate a Roblox spawn system" },
{ icon: FlaskConical, text: "Debug my current code" },
{ icon: BookText, text: "Explain cross-platform patterns" },
{ icon: Sparkles, text: "Optimize my game loop" },
];
const AiAssistant = () => { const AiAssistant = () => {
return <aside>AiAssistant (stub)</aside>; const [messages, setMessages] = useState<Message[]>([]);
const [input, setInput] = useState("");
const [isLoading, setIsLoading] = useState(false);
const [model, setModel] = useState("claude-3.5");
const scrollRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (scrollRef.current) {
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
}
}, [messages]);
const handleSend = async () => {
if (!input.trim() || isLoading) return;
const userMessage: Message = {
id: Date.now().toString(),
role: "user",
content: input,
timestamp: new Date(),
};
setMessages((prev) => [...prev, userMessage]);
setInput("");
setIsLoading(true);
// Simulate AI response
setTimeout(() => {
const aiMessage: Message = {
id: (Date.now() + 1).toString(),
role: "assistant",
content: `I'll help you with that! Here's a solution for "${input.slice(0, 50)}..."
\`\`\`lua
-- Example code snippet
local function example()
print("Hello from AeThex AI!")
end
\`\`\`
Would you like me to explain this further or make any modifications?`,
timestamp: new Date(),
};
setMessages((prev) => [...prev, aiMessage]);
setIsLoading(false);
}, 1500);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSend();
}
};
return (
<div className="flex h-full flex-col">
{/* Header */}
<div className="flex items-center justify-between border-b border-white/5 px-3 py-2">
<div className="flex items-center gap-2">
<div className="h-6 w-6 rounded-lg bg-gradient-to-br from-purple-500 via-blue-500 to-cyan-400 flex items-center justify-center shadow-lg shadow-purple-500/20">
<Zap className="h-3 w-3 text-white" />
</div>
<span className="text-sm font-semibold bg-gradient-to-r from-purple-400 to-blue-400 bg-clip-text text-transparent">AI Assistant</span>
</div>
<div className="flex items-center gap-1">
<Select value={model} onValueChange={setModel}>
<SelectTrigger className="h-6 w-24 text-[10px] bg-white/5 border-white/10">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="claude-3.5">Claude 3.5</SelectItem>
<SelectItem value="gpt-4">GPT-4</SelectItem>
<SelectItem value="gemini">Gemini Pro</SelectItem>
</SelectContent>
</Select>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-muted-foreground hover:text-foreground hover:bg-white/5"
onClick={() => setMessages([])}
title="New conversation"
>
<MessageSquarePlus className="h-3.5 w-3.5" />
</Button>
</div>
</div>
{/* Messages */}
<ScrollArea className="flex-1 p-3" ref={scrollRef}>
{messages.length === 0 ? (
<div className="flex flex-col items-center justify-center h-full text-center py-8">
<div className="h-14 w-14 rounded-2xl bg-gradient-to-br from-purple-500/20 via-blue-500/20 to-cyan-500/20 flex items-center justify-center mb-4 border border-white/5">
<Bot className="h-7 w-7 text-purple-400" />
</div>
<h3 className="font-semibold text-foreground mb-1">How can I help?</h3>
<p className="text-xs text-muted-foreground mb-6 max-w-48">
Ask anything about your code, get suggestions, or generate new scripts.
</p>
<div className="grid grid-cols-1 gap-2 w-full">
{suggestedPrompts.map((prompt, i) => (
<button
key={i}
onClick={() => setInput(prompt.text)}
className="flex items-center gap-2.5 rounded-lg border border-white/5 bg-white/[0.02] p-2.5 text-xs text-muted-foreground hover:bg-white/5 hover:text-foreground hover:border-white/10 transition-all text-left group"
>
<div className="h-6 w-6 rounded-md bg-white/5 flex items-center justify-center group-hover:bg-white/10 transition-colors">
<prompt.icon className="h-3 w-3" />
</div>
<span className="truncate">{prompt.text}</span>
</button>
))}
</div>
</div>
) : (
<div className="space-y-4">
{messages.map((message) => (
<div
key={message.id}
className={cn(
"flex gap-3",
message.role === "user" && "flex-row-reverse"
)}
>
<Avatar className="h-7 w-7 shrink-0">
<AvatarFallback
className={cn(
"text-xs",
message.role === "assistant"
? "bg-gradient-to-br from-blue-500 to-purple-600 text-white"
: "bg-accent"
)}
>
{message.role === "assistant" ? (
<Bot className="h-3.5 w-3.5" />
) : (
<User className="h-3.5 w-3.5" />
)}
</AvatarFallback>
</Avatar>
<div
className={cn(
"rounded-lg px-3 py-2 text-sm max-w-[85%]",
message.role === "assistant"
? "bg-accent/50"
: "bg-blue-600 text-white"
)}
>
<p className="whitespace-pre-wrap">{message.content}</p>
</div>
</div>
))}
{isLoading && (
<div className="flex gap-3">
<Avatar className="h-7 w-7 shrink-0">
<AvatarFallback className="bg-gradient-to-br from-blue-500 to-purple-600 text-white text-xs">
<Bot className="h-3.5 w-3.5" />
</AvatarFallback>
</Avatar>
<div className="rounded-lg bg-accent/50 px-3 py-2 text-sm">
<Loader2 className="h-4 w-4 animate-spin" />
</div>
</div>
)}
</div>
)}
</ScrollArea>
{/* Input */}
<div className="border-t border-white/5 p-3">
<div className="relative">
<Textarea
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Ask anything about your code..."
className="min-h-[70px] resize-none pr-10 text-sm bg-white/5 border-white/5 placeholder:text-muted-foreground/50 focus:border-purple-500/50 focus:bg-white/10"
/>
<Button
size="icon"
className="absolute bottom-2 right-2 h-7 w-7 bg-gradient-to-r from-purple-600 to-blue-600 hover:from-purple-500 hover:to-blue-500 border-0"
onClick={handleSend}
disabled={!input.trim() || isLoading}
>
<Send className="h-3 w-3" />
</Button>
</div>
<p className="mt-2 text-[10px] text-muted-foreground/50 text-center">
AI responses may not always be accurate. Verify important code.
</p>
</div>
</div>
);
}; };
export default AiAssistant; export default AiAssistant;

View file

@ -1,62 +1,204 @@
"use client"; "use client";
import { useState } from "react";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { consoleLogs } from "@/lib/aethex-data"; import { ScrollArea } from "@/components/ui/scroll-area";
import { ChevronRight, HardDrive } from "lucide-react"; import { Button } from "@/components/ui/button";
import { consoleLogs as initialLogs } from "@/lib/aethex-data";
import {
ChevronRight,
Terminal as TerminalIcon,
AlertCircle,
AlertTriangle,
Info,
Trash2,
Filter,
Download,
Maximize2,
X
} from "lucide-react";
import { cn } from "@/lib/utils";
export function BottomPanel() { export function BottomPanel() {
const [activeTab, setActiveTab] = useState("console");
const [logs, setLogs] = useState(initialLogs);
const [filterType, setFilterType] = useState<string | null>(null);
const [isMaximized, setIsMaximized] = useState(false);
const handleClearLogs = () => {
setLogs([]);
};
const handleFilter = () => {
// Cycle through filter types: null -> info -> warn -> error -> null
if (filterType === null) setFilterType("info");
else if (filterType === "info") setFilterType("warn");
else if (filterType === "warn") setFilterType("error");
else setFilterType(null);
};
const filteredLogs = filterType ? logs.filter(log => log.type === filterType) : logs;
return ( return (
<div className="flex h-full flex-col"> <div className="flex h-full flex-col">
<Tabs defaultValue="console" className="flex h-full flex-col"> {/* Tab bar with actions */}
<TabsList className="mx-2 mt-2 self-start rounded-md"> <div className="flex items-center justify-between border-b border-white/5 px-2">
<TabsTrigger value="console">Console</TabsTrigger> <div className="flex items-center">
<TabsTrigger value="terminal">Terminal</TabsTrigger> <button
</TabsList> onClick={() => setActiveTab("console")}
<TabsContent value="console" className="flex-1 overflow-auto p-4 text-xs"> className={cn(
<div className="font-code"> "flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium transition-colors border-b-2 -mb-px",
{consoleLogs.map((log, index) => ( activeTab === "console"
? "text-foreground border-purple-500"
: "text-muted-foreground border-transparent hover:text-foreground"
)}
>
<Info className="h-3 w-3" />
Console
<span className="ml-1.5 bg-white/10 px-1.5 py-0.5 rounded text-[10px]">3</span>
</button>
<button
onClick={() => setActiveTab("terminal")}
className={cn(
"flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium transition-colors border-b-2 -mb-px",
activeTab === "terminal"
? "text-foreground border-purple-500"
: "text-muted-foreground border-transparent hover:text-foreground"
)}
>
<TerminalIcon className="h-3 w-3" />
Terminal
</button>
<button
onClick={() => setActiveTab("problems")}
className={cn(
"flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium transition-colors border-b-2 -mb-px",
activeTab === "problems"
? "text-foreground border-purple-500"
: "text-muted-foreground border-transparent hover:text-foreground"
)}
>
<AlertCircle className="h-3 w-3" />
Problems
<span className="ml-1.5 bg-yellow-500/20 text-yellow-400 px-1.5 py-0.5 rounded text-[10px]">2</span>
</button>
</div>
<div className="flex items-center gap-0.5">
<Button
variant="ghost"
size="icon"
className={cn(
"h-6 w-6 hover:bg-white/5",
filterType ? "text-purple-400" : "text-muted-foreground hover:text-foreground"
)}
onClick={handleFilter}
title={filterType ? `Filtering: ${filterType}` : "Filter logs"}
>
<Filter className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-muted-foreground hover:text-foreground hover:bg-white/5"
onClick={handleClearLogs}
title="Clear console"
>
<Trash2 className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-muted-foreground hover:text-foreground hover:bg-white/5"
onClick={() => setIsMaximized(!isMaximized)}
title={isMaximized ? "Restore" : "Maximize"}
>
<Maximize2 className="h-3 w-3" />
</Button>
</div>
</div>
{/* Console content */}
{activeTab === "console" && (
<ScrollArea className="flex-1">
<div className="p-2 font-mono text-xs space-y-0.5">
{filteredLogs.length === 0 ? (
<div className="flex items-center justify-center h-20 text-muted-foreground">
{logs.length === 0 ? "No console output" : `No ${filterType} logs`}
</div>
) : (
filteredLogs.map((log, index) => (
<div <div
key={index} key={index}
className={`flex items-start gap-2 border-b border-border/50 py-1 ${ className={cn(
log.type === "error" "flex items-start gap-2 px-2 py-1 rounded hover:bg-white/5 transition-colors",
? "text-destructive" log.type === "error" && "bg-red-500/5",
: log.type === "warn" log.type === "warn" && "bg-yellow-500/5"
? "text-yellow-400" )}
: "text-muted-foreground"
}`}
> >
<span className="w-20 shrink-0 text-foreground/50"> <span className="text-muted-foreground/50 w-16 shrink-0 tabular-nums">
{log.timestamp} {log.timestamp}
</span> </span>
<span <span
className={`w-12 shrink-0 font-bold ${ className={cn(
log.platform === "Roblox" "w-14 shrink-0 font-medium",
? "text-red-500" log.platform === "Roblox" && "text-red-400",
: log.platform === "Web" log.platform === "Web" && "text-blue-400",
? "text-blue-500" log.platform === "Mobile" && "text-emerald-400"
: "text-green-500" )}
}`}
> >
[{log.platform}] [{log.platform}]
</span> </span>
<p className="flex-1 whitespace-pre-wrap">{log.message}</p> {log.type === "error" && <AlertCircle className="h-3 w-3 text-red-400 shrink-0 mt-0.5" />}
{log.type === "warn" && <AlertTriangle className="h-3 w-3 text-yellow-400 shrink-0 mt-0.5" />}
<p className={cn(
"flex-1",
log.type === "error" && "text-red-400",
log.type === "warn" && "text-yellow-400",
log.type === "log" && "text-foreground/80"
)}>
{log.message}
</p>
</div> </div>
))} )))}
</div> </div>
</TabsContent> </ScrollArea>
<TabsContent value="terminal" className="h-full"> )}
<div className="flex h-full flex-col bg-background p-4 font-code text-xs">
<p>AeThex Terminal</p> {/* Terminal content */}
<p>Copyright (c) 2024. All rights reserved.</p> {activeTab === "terminal" && (
<div className="mt-4 flex items-center gap-2"> <div className="flex-1 bg-[#0a0a0c] p-3 font-mono text-xs">
<HardDrive className="h-3 w-3 text-accent" /> <div className="text-emerald-400 mb-2">
<span className="text-accent">~/aethex-project</span> <span className="text-purple-400"></span> ~/aethex-project <span className="text-blue-400">git:(main)</span>
<ChevronRight className="h-3 w-3" /> </div>
<span className="flex-1"></span> <div className="flex items-center gap-1 text-muted-foreground">
<span className="text-muted-foreground/50">$</span>
<span className="animate-pulse"></span>
</div> </div>
</div> </div>
</TabsContent> )}
</Tabs>
{/* Problems content */}
{activeTab === "problems" && (
<ScrollArea className="flex-1">
<div className="p-2 space-y-1">
<div className="flex items-start gap-2 px-2 py-1.5 rounded bg-yellow-500/5 hover:bg-yellow-500/10 transition-colors">
<AlertTriangle className="h-3.5 w-3.5 text-yellow-400 shrink-0 mt-0.5" />
<div className="flex-1 min-w-0">
<p className="text-xs text-yellow-400">Unused variable 'tempData'</p>
<p className="text-[10px] text-muted-foreground">src/scripts/main.lua:24</p>
</div>
</div>
<div className="flex items-start gap-2 px-2 py-1.5 rounded bg-yellow-500/5 hover:bg-yellow-500/10 transition-colors">
<AlertTriangle className="h-3.5 w-3.5 text-yellow-400 shrink-0 mt-0.5" />
<div className="flex-1 min-w-0">
<p className="text-xs text-yellow-400">Consider using 'local' for function declaration</p>
<p className="text-[10px] text-muted-foreground">src/scripts/player.lua:8</p>
</div>
</div>
</div>
</ScrollArea>
)}
</div> </div>
); );
} }

View file

@ -5,7 +5,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { X } from "lucide-react"; import { X } from "lucide-react";
import { ScrollArea } from "@/components/ui/scroll-area"; import { ScrollArea } from "@/components/ui/scroll-area";
import type { OpenFileType } from "./aethex-studio"; import type { File as OpenFileType } from "@/lib/aethex-data";
type CodeEditorProps = { type CodeEditorProps = {
openFiles: OpenFileType[]; openFiles: OpenFileType[];

View file

@ -66,7 +66,7 @@ export function CrossPlatformView() {
const mobileViewport = PlaceHolderImages.find((p) => p.id === "mobile-vp"); const mobileViewport = PlaceHolderImages.find((p) => p.id === "mobile-vp");
return ( return (
<ResizablePanelGroup direction="vertical" className="h-full w-full"> <ResizablePanelGroup orientation="vertical" className="h-full w-full">
<ResizablePanel defaultSize={50} minSize={30}> <ResizablePanel defaultSize={50} minSize={30}>
<div className="grid h-full grid-cols-3 gap-2 p-2"> <div className="grid h-full grid-cols-3 gap-2 p-2">
{robloxViewport && ( {robloxViewport && (

View file

@ -7,8 +7,6 @@ import { WorkspaceCard } from "./workspace-card";
import { WorkspaceCardSkeleton } from "./workspace-card-skeleton"; import { WorkspaceCardSkeleton } from "./workspace-card-skeleton";
import { workspaces as initialWorkspaces } from "@/lib/workspaces"; import { workspaces as initialWorkspaces } from "@/lib/workspaces";
import { NewProjectModal } from "./new-project-modal"; import { NewProjectModal } from "./new-project-modal";
import { ProjectTemplate } from "@/lib/templates";
import { NewProjectFormValues } from "./new-project-modal";
import { MobileIcon, RobloxIcon, WebIcon } from "./icons"; import { MobileIcon, RobloxIcon, WebIcon } from "./icons";
type Workspace = typeof initialWorkspaces[0]; type Workspace = typeof initialWorkspaces[0];
@ -26,21 +24,14 @@ export function DashboardPage() {
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, []); }, []);
const handleCreateProject = ( const handleCreateProject = (name: string) => {
template: ProjectTemplate,
config: NewProjectFormValues
) => {
// This is a mock implementation. In a real app, this would involve // This is a mock implementation. In a real app, this would involve
// an API call to create a new project in the backend. // an API call to create a new project in the backend.
const newWorkspace: Workspace = { const newWorkspace: Workspace = {
id: `proj-${Date.now()}`, id: `proj-${Date.now()}`,
name: config.projectName, name,
lastModified: "Just now", lastModified: "Just now",
platforms: config.platforms.map((p) => { platforms: ["roblox"],
if (p === "roblox") return RobloxIcon;
if (p === "web") return WebIcon;
return MobileIcon;
}),
thumbnailUrlId: "workspace-thumb-4", thumbnailUrlId: "workspace-thumb-4",
thumbnailImageHint: "futuristic city", thumbnailImageHint: "futuristic city",
}; };
@ -105,11 +96,12 @@ export function DashboardPage() {
<main>{renderContent()}</main> <main>{renderContent()}</main>
</div> </div>
</div> </div>
{isNewProjectModalOpen && (
<NewProjectModal <NewProjectModal
isOpen={isNewProjectModalOpen} onCreate={handleCreateProject}
onClose={() => setIsNewProjectModalOpen(false)} onClose={() => setIsNewProjectModalOpen(false)}
onCreateProject={handleCreateProject}
/> />
)}
</> </>
); );
} }

View file

@ -0,0 +1,211 @@
"use client";
import { useState } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Checkbox } from "@/components/ui/checkbox";
import { Progress } from "@/components/ui/progress";
import {
Download,
Folder,
FileCode,
Package,
Check,
Loader2,
} from "lucide-react";
import { cn } from "@/lib/utils";
interface ExportModalProps {
open: boolean;
onClose: () => void;
platform: "roblox" | "uefn" | "spatial" | "web";
}
const exportFormats = {
roblox: [
{ id: "rbxlx", name: "Roblox Place (.rbxlx)", description: "Full place file with all assets" },
{ id: "rbxmx", name: "Roblox Model (.rbxmx)", description: "Model file for importing" },
{ id: "lua", name: "Lua Scripts (.lua)", description: "Export scripts only" },
],
uefn: [
{ id: "uproject", name: "UEFN Project", description: "Full Unreal project structure" },
{ id: "verse", name: "Verse Scripts", description: "Export Verse code only" },
{ id: "pak", name: "Package (.pak)", description: "Compiled game package" },
],
spatial: [
{ id: "spatial", name: "Spatial Package", description: "Ready for Spatial.io upload" },
{ id: "unity", name: "Unity Export", description: "Unity asset format" },
{ id: "gltf", name: "glTF Models", description: "3D models only" },
],
web: [
{ id: "html", name: "Static HTML", description: "Single HTML file with embedded code" },
{ id: "zip", name: "Project ZIP", description: "Full project archive" },
{ id: "npm", name: "NPM Package", description: "Ready for npm publish" },
],
};
const platformColors = {
roblox: "text-red-400",
uefn: "text-purple-400",
spatial: "text-emerald-400",
web: "text-blue-400",
};
export function ExportModal({ open, onClose, platform }: ExportModalProps) {
const [selectedFormat, setSelectedFormat] = useState(exportFormats[platform][0].id);
const [exportName, setExportName] = useState("my-project");
const [includeAssets, setIncludeAssets] = useState(true);
const [minify, setMinify] = useState(false);
const [isExporting, setIsExporting] = useState(false);
const [exportProgress, setExportProgress] = useState(0);
const [exportComplete, setExportComplete] = useState(false);
const handleExport = async () => {
setIsExporting(true);
setExportProgress(0);
setExportComplete(false);
// Simulate export progress
for (let i = 0; i <= 100; i += 10) {
await new Promise(resolve => setTimeout(resolve, 200));
setExportProgress(i);
}
setIsExporting(false);
setExportComplete(true);
// Trigger download
const content = `-- Exported from AeThex Studio\n-- Platform: ${platform}\n-- Format: ${selectedFormat}\n\nprint("Hello from AeThex!")`;
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `${exportName}.${selectedFormat === 'lua' ? 'lua' : 'zip'}`;
a.click();
URL.revokeObjectURL(url);
};
const resetAndClose = () => {
setExportComplete(false);
setExportProgress(0);
onClose();
};
return (
<Dialog open={open} onOpenChange={resetAndClose}>
<DialogContent className="max-w-lg p-0 gap-0 bg-[#12121a] border-white/10">
<DialogHeader className="px-6 py-4 border-b border-white/10">
<DialogTitle className="flex items-center gap-2 text-lg">
<Download className="h-5 w-5" />
Export Project
</DialogTitle>
</DialogHeader>
<div className="p-6 space-y-6">
{/* Platform indicator */}
<div className={cn("flex items-center gap-2 text-sm", platformColors[platform])}>
<Package className="h-4 w-4" />
<span className="capitalize font-medium">{platform} Export</span>
</div>
{/* Export name */}
<div className="space-y-2">
<Label>Export Name</Label>
<Input
value={exportName}
onChange={(e) => setExportName(e.target.value)}
placeholder="my-project"
className="bg-white/5 border-white/10"
/>
</div>
{/* Format selection */}
<div className="space-y-3">
<Label>Export Format</Label>
<RadioGroup value={selectedFormat} onValueChange={setSelectedFormat}>
{exportFormats[platform].map((format) => (
<div
key={format.id}
className={cn(
"flex items-start gap-3 p-3 rounded-lg border transition-colors cursor-pointer",
selectedFormat === format.id
? "border-purple-500 bg-purple-500/10"
: "border-white/10 hover:border-white/20"
)}
onClick={() => setSelectedFormat(format.id)}
>
<RadioGroupItem value={format.id} className="mt-0.5" />
<div>
<div className="font-medium text-sm">{format.name}</div>
<div className="text-xs text-muted-foreground">{format.description}</div>
</div>
</div>
))}
</RadioGroup>
</div>
{/* Options */}
<div className="space-y-3">
<Label>Options</Label>
<div className="space-y-2">
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={includeAssets}
onCheckedChange={(v) => setIncludeAssets(!!v)}
/>
<span className="text-sm">Include assets (images, sounds, models)</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<Checkbox
checked={minify}
onCheckedChange={(v) => setMinify(!!v)}
/>
<span className="text-sm">Minify code for production</span>
</label>
</div>
</div>
{/* Progress */}
{(isExporting || exportComplete) && (
<div className="space-y-2">
<div className="flex items-center justify-between text-sm">
<span>{exportComplete ? "Export complete!" : "Exporting..."}</span>
<span>{exportProgress}%</span>
</div>
<Progress value={exportProgress} className="h-2" />
</div>
)}
</div>
<div className="flex items-center justify-end gap-2 px-6 py-4 border-t border-white/10">
<Button variant="ghost" onClick={resetAndClose}>Cancel</Button>
<Button
onClick={handleExport}
disabled={isExporting || !exportName.trim()}
className="bg-purple-600 hover:bg-purple-500 gap-2"
>
{isExporting ? (
<>
<Loader2 className="h-4 w-4 animate-spin" />
Exporting...
</>
) : exportComplete ? (
<>
<Check className="h-4 w-4" />
Downloaded
</>
) : (
<>
<Download className="h-4 w-4" />
Export
</>
)}
</Button>
</div>
</DialogContent>
</Dialog>
);
}

View file

@ -1,12 +1,23 @@
"use client"; "use client";
import React from "react"; import React, { useState } from "react";
import { import {
File, File,
Folder, Folder,
FolderOpen,
ChevronRight, ChevronRight,
ChevronDown,
FolderPlus, FolderPlus,
FilePlus, FilePlus,
Search,
MoreHorizontal,
Code,
FileCode,
FileJson,
FileText,
Image,
X,
Check,
} from "lucide-react"; } from "lucide-react";
import { import {
Collapsible, Collapsible,
@ -15,17 +26,300 @@ import {
} from "@/components/ui/collapsible"; } from "@/components/ui/collapsible";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import { FileNode, FolderNode, File as OpenFileType } from "@/lib/aethex-data"; import { FileNode, FolderNode, File as OpenFileType } from "@/lib/aethex-data";
import { generateFileContent } from "@/lib/templates"; import { cn } from "@/lib/utils";
type FileNavigatorProps = { type FileNavigatorProps = {
onOpenFile: (file: OpenFileType) => void; onOpenFile: (file: OpenFileType) => void;
fileTree: FolderNode; fileTree: FolderNode;
onCreateFile?: (name: string, parentPath?: string) => void;
onCreateFolder?: (name: string, parentPath?: string) => void;
}; };
const FileNavigator: React.FC<FileNavigatorProps> = () => { const getFileIcon = (filename: string) => {
return <nav>FileNavigator (stub)</nav>; const ext = filename.split('.').pop()?.toLowerCase();
switch (ext) {
case 'lua':
case 'luau':
return <FileCode className="h-4 w-4 text-blue-400" />;
case 'ts':
case 'tsx':
return <FileCode className="h-4 w-4 text-blue-500" />;
case 'js':
case 'jsx':
return <FileCode className="h-4 w-4 text-yellow-400" />;
case 'json':
return <FileJson className="h-4 w-4 text-yellow-500" />;
case 'md':
return <FileText className="h-4 w-4 text-gray-400" />;
case 'png':
case 'jpg':
case 'svg':
return <Image className="h-4 w-4 text-purple-400" />;
default:
return <File className="h-4 w-4 text-muted-foreground" />;
}
};
interface TreeItemProps {
node: FileNode | FolderNode;
depth: number;
onOpenFile: (file: OpenFileType) => void;
}
function TreeItem({ node, depth, onOpenFile }: TreeItemProps) {
const [isOpen, setIsOpen] = useState(depth < 2);
const isFolder = node.type === 'folder';
if (isFolder) {
const folderNode = node as FolderNode;
return (
<Collapsible open={isOpen} onOpenChange={setIsOpen}>
<CollapsibleTrigger asChild>
<button
className={cn(
"flex w-full items-center gap-1.5 rounded-sm px-2 py-1 text-sm hover:bg-accent/50 transition-colors",
"text-muted-foreground hover:text-foreground"
)}
style={{ paddingLeft: `${depth * 12 + 8}px` }}
>
{isOpen ? (
<ChevronDown className="h-3.5 w-3.5 shrink-0" />
) : (
<ChevronRight className="h-3.5 w-3.5 shrink-0" />
)}
{isOpen ? (
<FolderOpen className="h-4 w-4 text-amber-400 shrink-0" />
) : (
<Folder className="h-4 w-4 text-amber-400 shrink-0" />
)}
<span className="truncate">{folderNode.name}</span>
</button>
</CollapsibleTrigger>
<CollapsibleContent>
{folderNode.children.map((child, index) => (
<TreeItem
key={child.name + index}
node={child}
depth={depth + 1}
onOpenFile={onOpenFile}
/>
))}
</CollapsibleContent>
</Collapsible>
);
}
const fileNode = node as FileNode;
return (
<button
className={cn(
"flex w-full items-center gap-1.5 rounded-sm px-2 py-1 text-sm hover:bg-accent/50 transition-colors",
"text-muted-foreground hover:text-foreground"
)}
style={{ paddingLeft: `${depth * 12 + 24}px` }}
onClick={() =>
onOpenFile({
id: fileNode.name,
name: fileNode.name,
language: fileNode.language || "text",
content: fileNode.content || "",
})
}
>
{getFileIcon(fileNode.name)}
<span className="truncate">{fileNode.name}</span>
</button>
);
}
const FileNavigator: React.FC<FileNavigatorProps> = ({ fileTree, onOpenFile, onCreateFile, onCreateFolder }) => {
const [searchQuery, setSearchQuery] = useState("");
const [isCreatingFile, setIsCreatingFile] = useState(false);
const [isCreatingFolder, setIsCreatingFolder] = useState(false);
const [newItemName, setNewItemName] = useState("");
const handleCreateFile = () => {
if (newItemName.trim()) {
if (onCreateFile) {
onCreateFile(newItemName.trim());
}
// Also immediately open the new file
const ext = newItemName.split('.').pop()?.toLowerCase();
let language = 'text';
if (ext === 'lua' || ext === 'luau') language = 'lua';
else if (ext === 'ts' || ext === 'tsx') language = 'typescript';
else if (ext === 'js' || ext === 'jsx') language = 'javascript';
else if (ext === 'json') language = 'json';
else if (ext === 'md') language = 'markdown';
onOpenFile({
id: newItemName.trim() + '-' + Date.now(),
name: newItemName.trim(),
language,
content: getDefaultContent(newItemName.trim()),
});
setNewItemName("");
setIsCreatingFile(false);
}
};
const handleCreateFolder = () => {
if (newItemName.trim() && onCreateFolder) {
onCreateFolder(newItemName.trim());
setNewItemName("");
setIsCreatingFolder(false);
}
};
const getDefaultContent = (filename: string): string => {
const ext = filename.split('.').pop()?.toLowerCase();
switch (ext) {
case 'lua':
case 'luau':
return `-- ${filename}\n-- Created with AeThex Studio\n\nlocal module = {}\n\nfunction module.init()\n print("Hello from ${filename}!")\nend\n\nreturn module\n`;
case 'ts':
case 'tsx':
return `// ${filename}\n// Created with AeThex Studio\n\nexport function init() {\n console.log("Hello from ${filename}!");\n}\n`;
case 'js':
case 'jsx':
return `// ${filename}\n// Created with AeThex Studio\n\nfunction init() {\n console.log("Hello from ${filename}!");\n}\n\nmodule.exports = { init };\n`;
case 'json':
return `{\n "name": "${filename.replace('.json', '')}",\n "version": "1.0.0"\n}\n`;
case 'md':
return `# ${filename.replace('.md', '')}\n\nCreated with AeThex Studio.\n`;
default:
return `// ${filename}\n`;
}
};
const cancelCreate = () => {
setIsCreatingFile(false);
setIsCreatingFolder(false);
setNewItemName("");
};
return (
<div className="flex h-full flex-col">
{/* Header */}
<div className="flex items-center justify-between px-3 py-2.5 border-b border-white/5">
<div className="flex items-center gap-2">
<div className="h-5 w-5 rounded bg-gradient-to-br from-amber-500/20 to-orange-500/20 flex items-center justify-center">
<Folder className="h-3 w-3 text-amber-400" />
</div>
<span className="text-xs font-medium text-foreground">
my-project
</span>
</div>
<div className="flex items-center gap-0.5">
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-muted-foreground hover:text-foreground hover:bg-white/5"
onClick={() => {
setIsCreatingFile(true);
setIsCreatingFolder(false);
setNewItemName("");
}}
title="New File"
>
<FilePlus className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-muted-foreground hover:text-foreground hover:bg-white/5"
onClick={() => {
setIsCreatingFolder(true);
setIsCreatingFile(false);
setNewItemName("");
}}
title="New Folder"
>
<FolderPlus className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-6 w-6 text-muted-foreground hover:text-foreground hover:bg-white/5">
<MoreHorizontal className="h-3.5 w-3.5" />
</Button>
</div>
</div>
{/* New File/Folder Input */}
{(isCreatingFile || isCreatingFolder) && (
<div className="border-b border-white/5 p-2 bg-white/[0.02]">
<div className="flex items-center gap-1">
{isCreatingFile ? (
<File className="h-4 w-4 text-blue-400 shrink-0" />
) : (
<Folder className="h-4 w-4 text-amber-400 shrink-0" />
)}
<Input
autoFocus
placeholder={isCreatingFile ? "filename.lua" : "folder-name"}
value={newItemName}
onChange={(e) => setNewItemName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
isCreatingFile ? handleCreateFile() : handleCreateFolder();
} else if (e.key === 'Escape') {
cancelCreate();
}
}}
className="h-6 text-xs flex-1 bg-white/5 border-white/10 focus:border-purple-500/50"
/>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-emerald-400 hover:text-emerald-300 hover:bg-emerald-500/10"
onClick={isCreatingFile ? handleCreateFile : handleCreateFolder}
>
<Check className="h-3.5 w-3.5" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-red-400 hover:text-red-300 hover:bg-red-500/10"
onClick={cancelCreate}
>
<X className="h-3.5 w-3.5" />
</Button>
</div>
<p className="text-[10px] text-muted-foreground/70 mt-1 ml-5">
Enter to create · Escape to cancel
</p>
</div>
)}
{/* Search */}
<div className="px-2 py-1.5">
<div className="relative">
<Search className="absolute left-2.5 top-1/2 h-3 w-3 -translate-y-1/2 text-muted-foreground/50" />
<Input
placeholder="Search files..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="h-7 pl-7 text-xs bg-white/5 border-white/5 placeholder:text-muted-foreground/40 focus:border-purple-500/50 focus:bg-white/10"
/>
</div>
</div>
{/* File Tree */}
<ScrollArea className="flex-1">
<div className="pb-4 px-1">
<TreeItem node={fileTree} depth={0} onOpenFile={onOpenFile} />
</div>
</ScrollArea>
{/* Footer */}
<div className="border-t border-white/5 px-3 py-2">
<div className="text-[10px] text-muted-foreground/60 flex items-center gap-1.5">
<div className="h-1.5 w-1.5 rounded-full bg-emerald-400" />
<span>Connected to workspace</span>
</div>
</div>
</div>
);
}; };
export default FileNavigator; export default FileNavigator;

View file

@ -0,0 +1,219 @@
"use client";
import { useState } from "react";
import { toast } from "sonner";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
GitBranch,
GitCommit,
GitPullRequest,
GitMerge,
Plus,
Check,
X,
RefreshCw,
Upload,
Download,
FileCode,
FilePlus,
FileMinus,
FileEdit,
} from "lucide-react";
import { cn } from "@/lib/utils";
interface GitPanelProps {
open: boolean;
onClose: () => void;
}
const mockChanges = [
{ file: "src/main.lua", status: "modified" as const },
{ file: "src/utils/helpers.lua", status: "added" as const },
{ file: "src/old-module.lua", status: "deleted" as const },
];
const mockBranches = [
{ name: "main", current: true },
{ name: "feature/new-ui", current: false },
{ name: "fix/player-spawn", current: false },
];
const mockCommits = [
{ hash: "a1b2c3d", message: "Add player spawn system", author: "You", time: "2 hours ago" },
{ hash: "e4f5g6h", message: "Initial project setup", author: "You", time: "5 hours ago" },
{ hash: "i7j8k9l", message: "Configure build system", author: "You", time: "1 day ago" },
];
export function GitPanel({ open, onClose }: GitPanelProps) {
const [commitMessage, setCommitMessage] = useState("");
const [selectedBranch, setSelectedBranch] = useState("main");
const [stagedFiles, setStagedFiles] = useState<string[]>([]);
const toggleStage = (file: string) => {
setStagedFiles(prev =>
prev.includes(file) ? prev.filter(f => f !== file) : [...prev, file]
);
};
const stageAll = () => {
setStagedFiles(mockChanges.map(c => c.file));
};
const handleCommit = () => {
if (commitMessage.trim() && stagedFiles.length > 0) {
toast.success(`Committed ${stagedFiles.length} file${stagedFiles.length > 1 ? 's' : ''}`, {
description: commitMessage,
});
setCommitMessage("");
setStagedFiles([]);
}
};
const getStatusIcon = (status: "modified" | "added" | "deleted") => {
switch (status) {
case "modified":
return <FileEdit className="h-4 w-4 text-yellow-400" />;
case "added":
return <FilePlus className="h-4 w-4 text-emerald-400" />;
case "deleted":
return <FileMinus className="h-4 w-4 text-red-400" />;
}
};
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-2xl h-[600px] p-0 gap-0 bg-[#12121a] border-white/10">
<DialogHeader className="px-6 py-4 border-b border-white/10">
<DialogTitle className="flex items-center gap-2 text-lg">
<GitBranch className="h-5 w-5" />
Source Control
</DialogTitle>
</DialogHeader>
<div className="flex flex-1 overflow-hidden">
{/* Main content */}
<div className="flex-1 flex flex-col">
{/* Branch selector */}
<div className="px-4 py-3 border-b border-white/10 flex items-center justify-between">
<div className="flex items-center gap-2">
<GitBranch className="h-4 w-4 text-muted-foreground" />
<select
value={selectedBranch}
onChange={(e) => setSelectedBranch(e.target.value)}
className="bg-transparent text-sm font-medium focus:outline-none cursor-pointer"
>
{mockBranches.map(b => (
<option key={b.name} value={b.name}>{b.name}</option>
))}
</select>
</div>
<div className="flex items-center gap-1">
<Button variant="ghost" size="icon" className="h-7 w-7" title="Create branch">
<Plus className="h-4 w-4" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7" title="Refresh">
<RefreshCw className="h-4 w-4" />
</Button>
</div>
</div>
{/* Changes */}
<div className="flex-1 overflow-hidden flex flex-col">
<div className="px-4 py-2 flex items-center justify-between">
<span className="text-xs font-medium text-muted-foreground uppercase">Changes ({mockChanges.length})</span>
<Button variant="ghost" size="sm" className="h-6 text-xs" onClick={stageAll}>
Stage All
</Button>
</div>
<ScrollArea className="flex-1 px-4">
<div className="space-y-1">
{mockChanges.map((change) => (
<div
key={change.file}
className={cn(
"flex items-center gap-2 px-2 py-1.5 rounded text-sm hover:bg-white/5 cursor-pointer transition-colors",
stagedFiles.includes(change.file) && "bg-purple-500/10"
)}
onClick={() => toggleStage(change.file)}
>
<div className={cn(
"h-4 w-4 rounded border flex items-center justify-center transition-colors",
stagedFiles.includes(change.file)
? "bg-purple-500 border-purple-500"
: "border-white/20"
)}>
{stagedFiles.includes(change.file) && <Check className="h-3 w-3 text-white" />}
</div>
{getStatusIcon(change.status)}
<span className="flex-1 truncate">{change.file}</span>
</div>
))}
</div>
</ScrollArea>
{/* Commit message */}
<div className="p-4 border-t border-white/10">
<Input
placeholder="Commit message..."
value={commitMessage}
onChange={(e) => setCommitMessage(e.target.value)}
className="mb-2 bg-white/5 border-white/10"
/>
<div className="flex gap-2">
<Button
className="flex-1 bg-purple-600 hover:bg-purple-500"
disabled={!commitMessage.trim() || stagedFiles.length === 0}
onClick={handleCommit}
>
<GitCommit className="h-4 w-4 mr-2" />
Commit ({stagedFiles.length})
</Button>
</div>
</div>
</div>
</div>
{/* Sidebar - Recent commits */}
<div className="w-64 border-l border-white/10 flex flex-col">
<div className="px-4 py-3 border-b border-white/10">
<span className="text-xs font-medium text-muted-foreground uppercase">Recent Commits</span>
</div>
<ScrollArea className="flex-1 p-2">
<div className="space-y-2">
{mockCommits.map((commit) => (
<div
key={commit.hash}
className="p-2 rounded hover:bg-white/5 cursor-pointer transition-colors"
>
<div className="flex items-center gap-2 mb-1">
<GitCommit className="h-3 w-3 text-muted-foreground" />
<code className="text-xs text-purple-400">{commit.hash}</code>
</div>
<p className="text-sm truncate">{commit.message}</p>
<p className="text-xs text-muted-foreground">{commit.time}</p>
</div>
))}
</div>
</ScrollArea>
{/* Push/Pull */}
<div className="p-2 border-t border-white/10 space-y-2">
<Button variant="outline" className="w-full h-8 text-xs gap-2">
<Download className="h-3 w-3" />
Pull
</Button>
<Button variant="outline" className="w-full h-8 text-xs gap-2">
<Upload className="h-3 w-3" />
Push
</Button>
</div>
</div>
</div>
</DialogContent>
</Dialog>
);
}

View file

@ -1 +1,212 @@
export function MainView() { return <main>Main View (stub)</main>; } "use client";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
X,
Circle,
FileCode,
Sparkles,
Play,
Bug,
Braces,
ChevronRight,
Zap,
FileJson,
FileText,
} from "lucide-react";
import { cn } from "@/lib/utils";
interface OpenFile {
id: string;
name: string;
language: string;
content: string;
isDirty?: boolean;
}
interface MainViewProps {
openFiles?: OpenFile[];
activeFileId?: string;
onFileSelect?: (fileId: string) => void;
onFileClose?: (fileId: string) => void;
onNewFile?: () => void;
onOpenTemplates?: () => void;
}
const getFileIcon = (filename: string, size = "h-3.5 w-3.5") => {
const ext = filename.split('.').pop()?.toLowerCase();
switch (ext) {
case 'lua':
case 'luau':
return <FileCode className={cn(size, "text-blue-400")} />;
case 'ts':
case 'tsx':
return <FileCode className={cn(size, "text-blue-500")} />;
case 'js':
case 'jsx':
return <FileCode className={cn(size, "text-yellow-400")} />;
case 'json':
return <FileJson className={cn(size, "text-yellow-500")} />;
default:
return <FileText className={cn(size, "text-muted-foreground")} />;
}
};
export function MainView({
openFiles = [],
activeFileId,
onFileSelect,
onFileClose,
onNewFile,
onOpenTemplates,
}: MainViewProps) {
const activeFile = openFiles.find((f) => f.id === activeFileId);
if (openFiles.length === 0) {
return (
<div className="h-full w-full bg-[#0e0e12] flex items-center justify-center">
<div className="flex flex-col items-center gap-4 text-center max-w-sm">
<div className="h-12 w-12 rounded-xl bg-gradient-to-br from-purple-500/20 to-blue-500/20 flex items-center justify-center border border-white/10">
<Zap className="h-6 w-6 text-purple-400" />
</div>
<div>
<h2 className="text-lg font-semibold text-foreground mb-1">No files open</h2>
<p className="text-xs text-muted-foreground">Open a file from the sidebar or create a new one</p>
</div>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
className="h-8 text-xs gap-2 bg-white/5 border-white/10 hover:bg-white/10"
onClick={onNewFile}
>
<FileCode className="h-3.5 w-3.5" />
New File
</Button>
<Button
size="sm"
variant="outline"
className="h-8 text-xs gap-2 bg-white/5 border-white/10 hover:bg-white/10"
onClick={onOpenTemplates}
>
<Sparkles className="h-3.5 w-3.5" />
Template
</Button>
</div>
</div>
</div>
);
}
return (
<div className="flex h-full flex-col bg-[#0d0d10]">
{/* Tab bar */}
<div className="flex items-center border-b border-white/5 bg-[#12121a]">
<div className="flex-1 overflow-x-auto scrollbar-hide">
<div className="flex">
{openFiles.map((file) => (
<div
key={file.id}
onClick={() => onFileSelect?.(file.id)}
role="tab"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
onFileSelect?.(file.id);
}
}}
className={cn(
"group flex items-center gap-2 px-3 py-1.5 text-xs transition-all relative cursor-pointer",
"hover:bg-white/5",
activeFileId === file.id
? "bg-[#0d0d10] text-foreground"
: "text-muted-foreground"
)}
>
{activeFileId === file.id && (
<div className="absolute top-0 left-0 right-0 h-[2px] bg-gradient-to-r from-purple-500 to-blue-500" />
)}
{getFileIcon(file.name)}
<span className="truncate max-w-32">{file.name}</span>
{file.isDirty && (
<Circle className="h-1.5 w-1.5 fill-current text-blue-400" />
)}
<button
onClick={(e) => {
e.stopPropagation();
onFileClose?.(file.id);
}}
className="ml-1 rounded p-0.5 opacity-0 hover:bg-white/10 group-hover:opacity-100 transition-opacity"
>
<X className="h-3 w-3" />
</button>
</div>
))}
</div>
</div>
<div className="flex items-center gap-0.5 px-2 border-l border-white/5">
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-muted-foreground hover:text-foreground hover:bg-white/5"
onClick={() => toast.success("Running script...", { description: "Output will appear in the Console panel" })}
title="Run Script"
>
<Play className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-muted-foreground hover:text-foreground hover:bg-white/5"
onClick={() => toast.info("Debug mode enabled", { description: "Click line numbers to set breakpoints" })}
title="Debug Mode"
>
<Bug className="h-3 w-3" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-muted-foreground hover:text-foreground hover:bg-white/5"
onClick={() => toast.success("Code formatted")}
title="Format Code"
>
<Braces className="h-3 w-3" />
</Button>
</div>
</div>
{/* Breadcrumb */}
{activeFile && (
<div className="flex items-center gap-1 px-4 py-1.5 text-[11px] text-muted-foreground border-b border-white/5 bg-[#0d0d10]">
<span className="hover:text-foreground cursor-pointer">src</span>
<ChevronRight className="h-3 w-3" />
<span className="hover:text-foreground cursor-pointer">scripts</span>
<ChevronRight className="h-3 w-3" />
<span className="text-foreground">{activeFile.name}</span>
</div>
)}
{/* Editor area */}
{activeFile && (
<ScrollArea className="flex-1">
<div className="relative min-h-full">
{/* Line numbers gutter */}
<div className="absolute left-0 top-0 w-14 border-r border-white/5 bg-[#0a0a0c] text-right pr-4 pt-3 text-[11px] text-muted-foreground/30 select-none font-mono">
{activeFile.content.split("\n").map((_, i) => (
<div key={i} className="leading-6 hover:text-muted-foreground transition-colors">
{i + 1}
</div>
))}
</div>
{/* Code content */}
<pre className="pl-20 pt-3 pr-4 pb-8 text-[13px] font-mono leading-6 text-foreground/90">
<code>{activeFile.content}</code>
</pre>
</div>
</ScrollArea>
)}
</div>
);
}

View file

@ -1 +1,243 @@
export function Navbar() { return <nav>Navbar (stub)</nav>; } "use client";
import { useState } from "react";
import { toast } from "sonner";
import {
Menu,
Play,
Save,
Undo,
Redo,
Settings,
Search,
Terminal,
PanelLeft,
Download,
Upload,
GitBranch,
Users,
Zap,
Plus,
FolderOpen,
} from "lucide-react";
import { AethexLogo } from "./icons";
import { Button } from "@/components/ui/button";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
const platformColors = {
roblox: "bg-red-500",
uefn: "bg-purple-500",
spatial: "bg-emerald-500",
web: "bg-blue-500",
};
interface NavbarProps {
onToggleSidebar?: () => void;
onToggleAI?: () => void;
onToggleBottomPanel?: () => void;
onNewFile?: () => void;
onSearch?: () => void;
onRun?: () => void;
onSave?: () => void;
onSettings?: () => void;
onGit?: () => void;
onExport?: () => void;
sidebarOpen?: boolean;
aiOpen?: boolean;
bottomPanelOpen?: boolean;
}
export function Navbar({
onToggleSidebar,
onToggleAI,
onToggleBottomPanel,
onNewFile,
onSearch,
onRun,
onSave,
onSettings,
onGit,
onExport,
sidebarOpen = true,
aiOpen = true,
bottomPanelOpen = true,
}: NavbarProps) {
const [platform, setPlatform] = useState<keyof typeof platformColors>("roblox");
const handleCopy = () => {
const selection = window.getSelection()?.toString();
if (selection) {
navigator.clipboard.writeText(selection);
}
};
const handlePaste = async () => {
const text = await navigator.clipboard.readText();
// Note: Paste would need to be implemented in the editor component
console.log("Paste:", text);
};
return (
<nav className="flex h-11 items-center justify-between border-b border-white/5 bg-[#0d0d10] px-2">
{/* Left section */}
<div className="flex items-center gap-1">
<Button
variant="ghost"
size="icon"
className="h-7 w-7 text-muted-foreground hover:text-foreground hover:bg-white/5"
onClick={onToggleSidebar}
>
<PanelLeft className={cn("h-4 w-4 transition-transform", !sidebarOpen && "rotate-180")} />
</Button>
<div className="flex items-center gap-2 px-2">
<div className="h-6 w-6 rounded-lg bg-gradient-to-br from-purple-500 via-blue-500 to-cyan-400 flex items-center justify-center shadow-lg shadow-purple-500/20">
<AethexLogo className="h-4 w-4 text-white" />
</div>
<span className="font-semibold text-sm bg-gradient-to-r from-white to-white/70 bg-clip-text text-transparent">AeThex</span>
</div>
<div className="h-4 w-px bg-white/10 mx-1" />
{/* File Menu */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-6 px-2 text-xs text-muted-foreground hover:text-foreground hover:bg-white/5">
File
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48">
<DropdownMenuItem onClick={onNewFile}>
<Plus className="mr-2 h-4 w-4" />
New File
<span className="ml-auto text-xs text-muted-foreground">N</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => toast.info("Use the sidebar to navigate project files")}>
<FolderOpen className="mr-2 h-4 w-4" />
Open...
<span className="ml-auto text-xs text-muted-foreground">O</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onSave}>
<Save className="mr-2 h-4 w-4" />
Save
<span className="ml-auto text-xs text-muted-foreground">S</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={onSave}>Save As...</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onExport}>
<Download className="mr-2 h-4 w-4" />
Export Project...
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-6 px-2 text-xs text-muted-foreground hover:text-foreground hover:bg-white/5">
Edit
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48">
<DropdownMenuItem onClick={() => document.execCommand('undo')}>
<Undo className="mr-2 h-4 w-4" />
Undo
<span className="ml-auto text-xs text-muted-foreground">Z</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => document.execCommand('redo')}>
<Redo className="mr-2 h-4 w-4" />
Redo
<span className="ml-auto text-xs text-muted-foreground">Z</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={() => document.execCommand('cut')}>Cut</DropdownMenuItem>
<DropdownMenuItem onClick={handleCopy}>Copy</DropdownMenuItem>
<DropdownMenuItem onClick={handlePaste}>Paste</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-6 px-2 text-xs text-muted-foreground hover:text-foreground hover:bg-white/5">
View
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-48">
<DropdownMenuItem onClick={onSearch}>Command Palette</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={onToggleSidebar}>Explorer</DropdownMenuItem>
<DropdownMenuItem onClick={onSearch}>Search</DropdownMenuItem>
<DropdownMenuItem onClick={onToggleBottomPanel}>Terminal</DropdownMenuItem>
<DropdownMenuItem onClick={onToggleAI}>AI Assistant</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
{/* Center section - Platform selector */}
<div className="flex items-center gap-0.5 rounded-full bg-white/5 p-0.5 border border-white/5">
{(Object.keys(platformColors) as Array<keyof typeof platformColors>).map((p) => (
<button
key={p}
className={cn(
"flex items-center gap-1.5 h-6 px-3 text-[11px] font-medium capitalize rounded-full transition-all",
platform === p
? "bg-white/10 text-white shadow-sm"
: "text-muted-foreground hover:text-foreground"
)}
onClick={() => setPlatform(p)}
>
<div className={cn(
"h-1.5 w-1.5 rounded-full transition-all",
platformColors[p],
platform === p && "shadow-lg"
)} />
{p}
</button>
))}
</div>
{/* Right section */}
<div className="flex items-center gap-0.5">
<Button variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground hover:text-foreground hover:bg-white/5" onClick={onSearch}>
<Search className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground hover:text-foreground hover:bg-white/5" onClick={onGit}>
<GitBranch className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground hover:text-foreground hover:bg-white/5" onClick={() => toast.info("1 user online", { description: "Invite others to collaborate in real-time" })}>
<Users className="h-3.5 w-3.5" />
</Button>
<div className="h-4 w-px bg-white/10 mx-1" />
<Button size="sm" className="h-7 gap-1.5 px-3 text-xs bg-gradient-to-r from-emerald-600 to-emerald-500 hover:from-emerald-500 hover:to-emerald-400 shadow-lg shadow-emerald-500/20 border-0" onClick={onRun}>
<Play className="h-3 w-3" />
Run
</Button>
<Button
variant="ghost"
size="icon"
className={cn("h-7 w-7 hover:bg-white/5", aiOpen ? "text-purple-400" : "text-muted-foreground")}
onClick={onToggleAI}
>
<Zap className="h-3.5 w-3.5" />
</Button>
<Button variant="ghost" size="icon" className="h-7 w-7 text-muted-foreground hover:text-foreground hover:bg-white/5" onClick={onSettings}>
<Settings className="h-3.5 w-3.5" />
</Button>
</div>
</nav>
);
}

View file

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import { X } from 'lucide-react';
interface NewProjectModalProps { interface NewProjectModalProps {
onCreate: (name: string) => void; onCreate: (name: string) => void;
@ -8,17 +9,66 @@ interface NewProjectModalProps {
export const NewProjectModal: React.FC<NewProjectModalProps> = ({ onCreate, onClose }) => { export const NewProjectModal: React.FC<NewProjectModalProps> = ({ onCreate, onClose }) => {
const [name, setName] = React.useState(''); const [name, setName] = React.useState('');
const handleCreate = () => {
if (name.trim()) {
onCreate(name.trim());
setName('');
onClose();
}
};
return ( return (
<div className="new-project-modal"> <div className="fixed inset-0 z-50 flex items-center justify-center">
<h2>New Project</h2> {/* Backdrop */}
<div
className="absolute inset-0 bg-black/60 backdrop-blur-sm"
onClick={onClose}
/>
{/* Modal */}
<div className="relative bg-[#1a1a1a] border border-[#333] rounded-xl shadow-2xl w-full max-w-md mx-4 p-6">
<button
onClick={onClose}
className="absolute top-4 right-4 text-[#666] hover:text-white transition-colors"
>
<X className="h-5 w-5" />
</button>
<h2 className="text-xl font-semibold text-white mb-4">New Project</h2>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-[#888] mb-2">
Project Name
</label>
<input <input
type="text" type="text"
value={name} value={name}
onChange={e => setName(e.target.value)} onChange={e => setName(e.target.value)}
placeholder="Project Name" onKeyDown={e => e.key === 'Enter' && handleCreate()}
placeholder="my-awesome-game"
autoFocus
className="w-full bg-[#0a0a0a] border border-[#333] rounded-lg px-4 py-3 text-white placeholder-[#666] focus:outline-none focus:border-purple-500 transition-colors"
/> />
<button onClick={() => onCreate(name)} disabled={!name}>Create</button> </div>
<button onClick={onClose}>Cancel</button>
<div className="flex gap-3 pt-2">
<button
onClick={onClose}
className="flex-1 px-4 py-2.5 rounded-lg border border-[#333] text-[#888] hover:text-white hover:border-[#444] transition-colors"
>
Cancel
</button>
<button
onClick={handleCreate}
disabled={!name.trim()}
className="flex-1 px-4 py-2.5 rounded-lg bg-purple-600 hover:bg-purple-500 text-white font-medium disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Create
</button>
</div>
</div>
</div>
</div> </div>
); );
}; };

View file

@ -0,0 +1,320 @@
"use client";
import { useState } from "react";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Slider } from "@/components/ui/slider";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Settings,
Palette,
Code,
Keyboard,
Zap,
Cloud,
User,
Monitor,
} from "lucide-react";
import { cn } from "@/lib/utils";
interface SettingsPanelProps {
open: boolean;
onClose: () => void;
}
export function SettingsPanel({ open, onClose }: SettingsPanelProps) {
const [settings, setSettings] = useState({
theme: "dark",
fontSize: 14,
tabSize: 2,
wordWrap: true,
autoSave: true,
autoSaveDelay: 1000,
minimap: true,
lineNumbers: true,
bracketMatching: true,
aiModel: "claude-3.5",
aiAutoComplete: true,
syncEnabled: true,
});
const updateSetting = (key: string, value: any) => {
setSettings(prev => ({ ...prev, [key]: value }));
};
return (
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className="max-w-3xl h-[600px] p-0 gap-0 bg-[#12121a] border-white/10">
<DialogHeader className="px-6 py-4 border-b border-white/10">
<DialogTitle className="flex items-center gap-2 text-lg">
<Settings className="h-5 w-5" />
Settings
</DialogTitle>
</DialogHeader>
<div className="flex flex-1 overflow-hidden">
{/* Sidebar */}
<div className="w-48 border-r border-white/10 p-2">
<Tabs defaultValue="editor" orientation="vertical" className="h-full">
<TabsList className="flex flex-col h-auto bg-transparent gap-1">
<TabsTrigger value="editor" className="w-full justify-start gap-2 data-[state=active]:bg-white/10">
<Code className="h-4 w-4" />
Editor
</TabsTrigger>
<TabsTrigger value="appearance" className="w-full justify-start gap-2 data-[state=active]:bg-white/10">
<Palette className="h-4 w-4" />
Appearance
</TabsTrigger>
<TabsTrigger value="ai" className="w-full justify-start gap-2 data-[state=active]:bg-white/10">
<Zap className="h-4 w-4" />
AI Settings
</TabsTrigger>
<TabsTrigger value="sync" className="w-full justify-start gap-2 data-[state=active]:bg-white/10">
<Cloud className="h-4 w-4" />
Sync & Cloud
</TabsTrigger>
<TabsTrigger value="keybindings" className="w-full justify-start gap-2 data-[state=active]:bg-white/10">
<Keyboard className="h-4 w-4" />
Keybindings
</TabsTrigger>
</TabsList>
</Tabs>
</div>
{/* Content */}
<ScrollArea className="flex-1 p-6">
<Tabs defaultValue="editor">
<TabsContent value="editor" className="mt-0 space-y-6">
<div>
<h3 className="text-sm font-medium mb-4">Editor Settings</h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label>Font Size</Label>
<p className="text-xs text-muted-foreground">Size of text in the editor</p>
</div>
<div className="flex items-center gap-3">
<Slider
value={[settings.fontSize]}
onValueChange={([v]) => updateSetting("fontSize", v)}
min={10}
max={24}
step={1}
className="w-32"
/>
<span className="text-sm w-8">{settings.fontSize}px</span>
</div>
</div>
<div className="flex items-center justify-between">
<div>
<Label>Tab Size</Label>
<p className="text-xs text-muted-foreground">Number of spaces per tab</p>
</div>
<Select value={String(settings.tabSize)} onValueChange={(v) => updateSetting("tabSize", Number(v))}>
<SelectTrigger className="w-20">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="2">2</SelectItem>
<SelectItem value="4">4</SelectItem>
<SelectItem value="8">8</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between">
<div>
<Label>Word Wrap</Label>
<p className="text-xs text-muted-foreground">Wrap long lines</p>
</div>
<Switch
checked={settings.wordWrap}
onCheckedChange={(v) => updateSetting("wordWrap", v)}
/>
</div>
<div className="flex items-center justify-between">
<div>
<Label>Auto Save</Label>
<p className="text-xs text-muted-foreground">Automatically save files</p>
</div>
<Switch
checked={settings.autoSave}
onCheckedChange={(v) => updateSetting("autoSave", v)}
/>
</div>
<div className="flex items-center justify-between">
<div>
<Label>Minimap</Label>
<p className="text-xs text-muted-foreground">Show code overview minimap</p>
</div>
<Switch
checked={settings.minimap}
onCheckedChange={(v) => updateSetting("minimap", v)}
/>
</div>
<div className="flex items-center justify-between">
<div>
<Label>Line Numbers</Label>
<p className="text-xs text-muted-foreground">Show line numbers in gutter</p>
</div>
<Switch
checked={settings.lineNumbers}
onCheckedChange={(v) => updateSetting("lineNumbers", v)}
/>
</div>
<div className="flex items-center justify-between">
<div>
<Label>Bracket Matching</Label>
<p className="text-xs text-muted-foreground">Highlight matching brackets</p>
</div>
<Switch
checked={settings.bracketMatching}
onCheckedChange={(v) => updateSetting("bracketMatching", v)}
/>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="appearance" className="mt-0 space-y-6">
<div>
<h3 className="text-sm font-medium mb-4">Appearance</h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label>Theme</Label>
<p className="text-xs text-muted-foreground">Color scheme for the IDE</p>
</div>
<Select value={settings.theme} onValueChange={(v) => updateSetting("theme", v)}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="dark">Dark</SelectItem>
<SelectItem value="light">Light</SelectItem>
<SelectItem value="midnight">Midnight</SelectItem>
<SelectItem value="purple">Purple Haze</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="ai" className="mt-0 space-y-6">
<div>
<h3 className="text-sm font-medium mb-4">AI Settings</h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label>AI Model</Label>
<p className="text-xs text-muted-foreground">Model for code assistance</p>
</div>
<Select value={settings.aiModel} onValueChange={(v) => updateSetting("aiModel", v)}>
<SelectTrigger className="w-32">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="claude-3.5">Claude 3.5</SelectItem>
<SelectItem value="gpt-4">GPT-4</SelectItem>
<SelectItem value="gemini">Gemini Pro</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center justify-between">
<div>
<Label>AI Auto-Complete</Label>
<p className="text-xs text-muted-foreground">Enable AI code suggestions</p>
</div>
<Switch
checked={settings.aiAutoComplete}
onCheckedChange={(v) => updateSetting("aiAutoComplete", v)}
/>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="sync" className="mt-0 space-y-6">
<div>
<h3 className="text-sm font-medium mb-4">Sync & Cloud</h3>
<div className="space-y-4">
<div className="flex items-center justify-between">
<div>
<Label>Cloud Sync</Label>
<p className="text-xs text-muted-foreground">Sync projects to cloud</p>
</div>
<Switch
checked={settings.syncEnabled}
onCheckedChange={(v) => updateSetting("syncEnabled", v)}
/>
</div>
<div className="p-4 rounded-lg bg-white/5 border border-white/10">
<div className="flex items-center gap-3 mb-2">
<Cloud className="h-5 w-5 text-emerald-400" />
<span className="font-medium">Connected</span>
</div>
<p className="text-xs text-muted-foreground">Last synced: Just now</p>
</div>
</div>
</div>
</TabsContent>
<TabsContent value="keybindings" className="mt-0 space-y-6">
<div>
<h3 className="text-sm font-medium mb-4">Keyboard Shortcuts</h3>
<div className="space-y-2">
{[
{ action: "Save File", keys: "⌘S" },
{ action: "New File", keys: "⌘N" },
{ action: "Open File", keys: "⌘O" },
{ action: "Command Palette", keys: "⌘K" },
{ action: "Toggle Sidebar", keys: "⌘B" },
{ action: "Toggle Terminal", keys: "⌘`" },
{ action: "Find", keys: "⌘F" },
{ action: "Find & Replace", keys: "⌘H" },
{ action: "Run Project", keys: "⌘⏎" },
].map((shortcut) => (
<div key={shortcut.action} className="flex items-center justify-between py-2 px-3 rounded hover:bg-white/5">
<span className="text-sm">{shortcut.action}</span>
<kbd className="px-2 py-1 rounded bg-white/10 text-xs font-mono">{shortcut.keys}</kbd>
</div>
))}
</div>
</div>
</TabsContent>
</Tabs>
</ScrollArea>
</div>
<div className="flex items-center justify-end gap-2 px-6 py-4 border-t border-white/10">
<Button variant="ghost" onClick={onClose}>Cancel</Button>
<Button onClick={onClose} className="bg-purple-600 hover:bg-purple-500">Save Changes</Button>
</div>
</DialogContent>
</Dialog>
);
}

View file

@ -0,0 +1,182 @@
"use client";
import { useState, useEffect } from "react";
import { toast } from "sonner";
import {
GitBranch,
AlertCircle,
AlertTriangle,
CheckCircle2,
Wifi,
WifiOff,
Cloud,
CloudOff,
Cpu,
Bell,
Zap,
} from "lucide-react";
import { cn } from "@/lib/utils";
interface StatusBarProps {
platform?: "roblox" | "uefn" | "spatial" | "web";
fileName?: string;
cursorPosition?: { line: number; column: number };
language?: string;
}
const platformConfig = {
roblox: { color: "bg-red-500", label: "Roblox Studio" },
uefn: { color: "bg-purple-500", label: "UEFN" },
spatial: { color: "bg-emerald-500", label: "Spatial" },
web: { color: "bg-blue-500", label: "Web" },
};
export function StatusBar({
platform = "roblox",
fileName,
cursorPosition = { line: 1, column: 1 },
language = "Lua",
}: StatusBarProps) {
const [isConnected, setIsConnected] = useState(true);
const [errors, setErrors] = useState(0);
const [warnings, setWarnings] = useState(2);
const [syncStatus, setSyncStatus] = useState<"synced" | "syncing" | "offline">("synced");
// Simulated connection status
useEffect(() => {
const interval = setInterval(() => {
setIsConnected(Math.random() > 0.1);
}, 5000);
return () => clearInterval(interval);
}, []);
const config = platformConfig[platform];
return (
<footer className="flex h-6 items-center justify-between border-t border-border/40 bg-[#1a1a1f] text-[11px] text-muted-foreground select-none">
{/* Left side */}
<div className="flex items-center h-full">
{/* Platform indicator */}
<button
className={cn(
"flex items-center gap-1.5 px-2.5 h-full transition-colors hover:bg-white/5",
config.color.replace("bg-", "text-").replace("-500", "-400")
)}
onClick={() => toast.info(`${config.label} connected`)}
>
<div className={cn("h-2 w-2 rounded-full", config.color)} />
<span className="font-medium">{config.label}</span>
</button>
{/* Branch */}
<button
className="flex items-center gap-1.5 px-2.5 h-full hover:bg-white/5 transition-colors"
onClick={() => toast.info("main", { description: "No uncommitted changes" })}
>
<GitBranch className="h-3.5 w-3.5" />
<span>main</span>
</button>
{/* Sync status */}
<button
className="flex items-center gap-1.5 px-2.5 h-full hover:bg-white/5 transition-colors"
onClick={() => setSyncStatus(syncStatus === "synced" ? "syncing" : syncStatus === "syncing" ? "offline" : "synced")}
>
{syncStatus === "synced" ? (
<>
<Cloud className="h-3.5 w-3.5 text-emerald-400" />
<span className="text-emerald-400">Synced</span>
</>
) : syncStatus === "syncing" ? (
<>
<Cloud className="h-3.5 w-3.5 text-blue-400 animate-pulse" />
<span className="text-blue-400">Syncing...</span>
</>
) : (
<>
<CloudOff className="h-3.5 w-3.5 text-yellow-400" />
<span className="text-yellow-400">Offline</span>
</>
)}
</button>
{/* Errors & Warnings */}
<button
className="flex items-center gap-3 px-2.5 h-full hover:bg-white/5 transition-colors"
onClick={() => toast.info(`${errors} errors, ${warnings} warnings`)}
>
<span className="flex items-center gap-1">
<AlertCircle className={cn("h-3.5 w-3.5", errors > 0 ? "text-red-400" : "text-muted-foreground")} />
<span className={errors > 0 ? "text-red-400" : ""}>{errors}</span>
</span>
<span className="flex items-center gap-1">
<AlertTriangle className={cn("h-3.5 w-3.5", warnings > 0 ? "text-yellow-400" : "text-muted-foreground")} />
<span className={warnings > 0 ? "text-yellow-400" : ""}>{warnings}</span>
</span>
</button>
</div>
{/* Right side */}
<div className="flex items-center h-full">
{/* Cursor position */}
<button
className="px-2.5 h-full hover:bg-white/5 transition-colors"
onClick={() => toast.info(`Line ${cursorPosition.line}, Column ${cursorPosition.column}`)}
>
Ln {cursorPosition.line}, Col {cursorPosition.column}
</button>
{/* Language */}
<button
className="px-2.5 h-full hover:bg-white/5 transition-colors"
onClick={() => toast.info(`Language: ${language}`)}
>
{language}
</button>
{/* Encoding */}
<button
className="px-2.5 h-full hover:bg-white/5 transition-colors"
onClick={() => toast.info("Encoding: UTF-8")}
>
UTF-8
</button>
{/* Line endings */}
<button
className="px-2.5 h-full hover:bg-white/5 transition-colors"
onClick={() => toast.info("Line Endings: LF (Unix)")}
>
LF
</button>
{/* Connection status */}
<button
className="flex items-center gap-1.5 px-2.5 h-full hover:bg-white/5 transition-colors"
onClick={() => setIsConnected(!isConnected)}
title={isConnected ? "Connected" : "Disconnected"}
>
{isConnected ? (
<Wifi className="h-3.5 w-3.5 text-emerald-400" />
) : (
<WifiOff className="h-3.5 w-3.5 text-red-400" />
)}
</button>
{/* AI Status */}
<button
className="flex items-center gap-1.5 px-2.5 h-full hover:bg-white/5 transition-colors text-purple-400"
onClick={() => toast.info("AI Ready", { description: "Model: Claude 3.5" })}
>
<Zap className="h-3.5 w-3.5" />
<span>AI Ready</span>
</button>
{/* Notifications */}
<button className="flex items-center gap-1.5 px-2.5 h-full hover:bg-white/5 transition-colors">
<Bell className="h-3.5 w-3.5" />
</button>
</div>
</footer>
);
}

View file

@ -1,6 +1,6 @@
import { ComponentProps } from "react" import { ComponentProps } from "react"
import * as AccordionPrimitive from "@radix-ui/react-accordion" import * as AccordionPrimitive from "@radix-ui/react-accordion"
import ChevronDownIcon from "lucide-react/dist/esm/icons/chevron-down" import { ChevronDown as ChevronDownIcon } from "lucide-react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"

View file

@ -1,7 +1,6 @@
import { ComponentProps } from "react" import { ComponentProps } from "react"
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot"
import ChevronRight from "lucide-react/dist/esm/icons/chevron-right" import { ChevronRight, MoreHorizontal } from "lucide-react"
import MoreHorizontal from "lucide-react/dist/esm/icons/more-horizontal"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"

View file

@ -1,6 +1,5 @@
import { ComponentProps } from "react" import { ComponentProps } from "react"
import ChevronLeft from "lucide-react/dist/esm/icons/chevron-left" import { ChevronLeft, ChevronRight } from "lucide-react"
import ChevronRight from "lucide-react/dist/esm/icons/chevron-right"
import { DayPicker } from "react-day-picker" import { DayPicker } from "react-day-picker"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@ -60,10 +59,14 @@ function Calendar({
}} }}
components={{ components={{
PreviousMonthButton: ({ className, ...props }) => ( PreviousMonthButton: ({ className, ...props }) => (
<ChevronLeft className={cn("size-4", className)} {...props} /> <button type="button" className={cn("size-7 bg-transparent p-0 opacity-50 hover:opacity-100", className)} {...props}>
<ChevronLeft className="size-4" />
</button>
), ),
NextMonthButton: ({ className, ...props }) => ( NextMonthButton: ({ className, ...props }) => (
<ChevronRight className={cn("size-4", className)} {...props} /> <button type="button" className={cn("size-7 bg-transparent p-0 opacity-50 hover:opacity-100", className)} {...props}>
<ChevronRight className="size-4" />
</button>
), ),
}} }}
{...props} {...props}

View file

@ -4,8 +4,7 @@ import { ComponentProps, createContext, useCallback, useContext, useEffect, useS
import useEmblaCarousel, { import useEmblaCarousel, {
type UseEmblaCarouselType, type UseEmblaCarouselType,
} from "embla-carousel-react" } from "embla-carousel-react"
import ArrowLeft from "lucide-react/dist/esm/icons/arrow-left" import { ArrowLeft, ArrowRight } from "lucide-react"
import ArrowRight from "lucide-react/dist/esm/icons/arrow-right"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button" import { Button } from "@/components/ui/button"

View file

@ -2,7 +2,7 @@
import { ComponentProps } from "react" import { ComponentProps } from "react"
import { Command as CommandPrimitive } from "cmdk" import { Command as CommandPrimitive } from "cmdk"
import SearchIcon from "lucide-react/dist/esm/icons/search" import { Search as SearchIcon } from "lucide-react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { import {

View file

@ -1,253 +1,185 @@
"use client" "use client"
import { ComponentProps } from "react" import * as React from "react"
import { import * as ContextMenuPrimitive from "@radix-ui/react-context-menu"
Root as ContextMenuRoot, import { Check, ChevronRight, Circle } from "lucide-react"
Trigger as ContextMenuTrigger,
Content as ContextMenuContent,
Item as ContextMenuItem,
Separator as ContextMenuSeparator,
CheckboxItem as ContextMenuCheckboxItem,
RadioGroup as ContextMenuRadioGroup,
RadioItem as ContextMenuRadioItem,
Sub as ContextMenuSub,
SubTrigger as ContextMenuSubTrigger,
SubContent as ContextMenuSubContent,
Label as ContextMenuLabel,
Group as ContextMenuGroup,
} from "@radix-ui/react-context-menu"
import CheckIcon from "lucide-react/dist/esm/icons/check";
import ChevronRightIcon from "lucide-react/dist/esm/icons/chevron-right"
import CircleIcon from "lucide-react/dist/esm/icons/circle"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
function ContextMenu({ const ContextMenu = ContextMenuPrimitive.Root
...props
}: ComponentProps<typeof ContextMenuRoot>) {
return <ContextMenuRoot data-slot="context-menu" {...props} />
}
function ContextMenuTrigger({ const ContextMenuTrigger = ContextMenuPrimitive.Trigger
...props
}: ComponentProps<typeof ContextMenuTrigger>) {
return (
<ContextMenuTrigger data-slot="context-menu-trigger" {...props} />
)
}
function ContextMenuGroup({ const ContextMenuGroup = ContextMenuPrimitive.Group
...props
}: ComponentProps<typeof ContextMenuGroup>) {
return (
<ContextMenuGroup data-slot="context-menu-group" {...props} />
)
}
function ContextMenuPortal({ const ContextMenuPortal = ContextMenuPrimitive.Portal
...props
}: ComponentProps<typeof ContextMenuPortal>) {
return (
<ContextMenuPortal data-slot="context-menu-portal" {...props} />
)
}
function ContextMenuSub({ const ContextMenuSub = ContextMenuPrimitive.Sub
...props
}: ComponentProps<typeof ContextMenuSub>) {
return <ContextMenuSub data-slot="context-menu-sub" {...props} />
}
function ContextMenuRadioGroup({ const ContextMenuRadioGroup = ContextMenuPrimitive.RadioGroup
...props
}: ComponentProps<typeof ContextMenuRadioGroup>) {
return (
<ContextMenuRadioGroup
data-slot="context-menu-radio-group"
{...props}
/>
)
}
function ContextMenuSubTrigger({ const ContextMenuSubTrigger = React.forwardRef<
className, React.ElementRef<typeof ContextMenuPrimitive.SubTrigger>,
inset, React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubTrigger> & {
children,
...props
}: ComponentProps<typeof ContextMenuSubTrigger> & {
inset?: boolean inset?: boolean
}) { }
return ( >(({ className, inset, children, ...props }, ref) => (
<ContextMenuSubTrigger <ContextMenuPrimitive.SubTrigger
data-slot="context-menu-sub-trigger" ref={ref}
data-inset={inset}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground flex cursor-default items-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className className
)} )}
{...props} {...props}
> >
{children} {children}
<ChevronRightIcon className="ml-auto" /> <ChevronRight className="ml-auto h-4 w-4" />
</ContextMenuSubTrigger> </ContextMenuPrimitive.SubTrigger>
) ))
} ContextMenuSubTrigger.displayName = ContextMenuPrimitive.SubTrigger.displayName
function ContextMenuSubContent({ const ContextMenuSubContent = React.forwardRef<
className, React.ElementRef<typeof ContextMenuPrimitive.SubContent>,
...props React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.SubContent>
}: ComponentProps<typeof ContextMenuSubContent>) { >(({ className, ...props }, ref) => (
return ( <ContextMenuPrimitive.SubContent
<SubContent ref={ref}
data-slot="context-menu-sub-content"
className={cn( className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg", "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className className
) )}
{...props} {...props}
/> />
) ))
} ContextMenuSubContent.displayName = ContextMenuPrimitive.SubContent.displayName
function ContextMenuContent({ const ContextMenuContent = React.forwardRef<
className, React.ElementRef<typeof ContextMenuPrimitive.Content>,
...props React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Content>
}: ComponentProps<typeof ContextMenuContent>) { >(({ className, ...props }, ref) => (
return ( <ContextMenuPrimitive.Portal>
<ContextMenuPortal> <ContextMenuPrimitive.Content
<Content ref={ref}
data-slot="context-menu-content"
className={cn( className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-context-menu-content-available-height) min-w-[8rem] origin-(--radix-context-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md", "z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className className
) )}
{...props} {...props}
/> />
</ContextMenuPortal> </ContextMenuPrimitive.Portal>
) ))
} ContextMenuContent.displayName = ContextMenuPrimitive.Content.displayName
function ContextMenuItem({ const ContextMenuItem = React.forwardRef<
className, React.ElementRef<typeof ContextMenuPrimitive.Item>,
inset, React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Item> & {
variant = "default",
...props
}: ComponentProps<typeof Item> & {
inset?: boolean inset?: boolean
variant?: "default" | "destructive" }
}) { >(({ className, inset, ...props }, ref) => (
return ( <ContextMenuPrimitive.Item
<Item ref={ref}
data-slot="context-menu-item"
data-inset={inset}
data-variant={variant}
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className className
) )}
{...props} {...props}
/> />
) ))
} ContextMenuItem.displayName = ContextMenuPrimitive.Item.displayName
function ContextMenuCheckboxItem({ const ContextMenuCheckboxItem = React.forwardRef<
className, React.ElementRef<typeof ContextMenuPrimitive.CheckboxItem>,
children, React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.CheckboxItem>
checked, >(({ className, children, checked, ...props }, ref) => (
...props <ContextMenuPrimitive.CheckboxItem
}: ComponentProps<typeof CheckboxItem>) { ref={ref}
return (
<CheckboxItem
data-slot="context-menu-checkbox-item"
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className className
) )}
checked={checked} checked={checked}
{...props} {...props}
> >
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<span> <ContextMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" /> <Check className="h-4 w-4" />
</span> </ContextMenuPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</CheckboxItem> </ContextMenuPrimitive.CheckboxItem>
) ))
} ContextMenuCheckboxItem.displayName =
ContextMenuPrimitive.CheckboxItem.displayName
function ContextMenuRadioItem({ const ContextMenuRadioItem = React.forwardRef<
className, React.ElementRef<typeof ContextMenuPrimitive.RadioItem>,
children, React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.RadioItem>
...props >(({ className, children, ...props }, ref) => (
}: ComponentProps<typeof RadioItem>) { <ContextMenuPrimitive.RadioItem
return ( ref={ref}
<RadioItem
data-slot="context-menu-radio-item"
className={cn( className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", "relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className className
) )}
{...props} {...props}
> >
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"> <span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<span> <ContextMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" /> <Circle className="h-2 w-2 fill-current" />
</span> </ContextMenuPrimitive.ItemIndicator>
</span> </span>
{children} {children}
</RadioItem> </ContextMenuPrimitive.RadioItem>
) ))
} ContextMenuRadioItem.displayName = ContextMenuPrimitive.RadioItem.displayName
function ContextMenuLabel({ const ContextMenuLabel = React.forwardRef<
className, React.ElementRef<typeof ContextMenuPrimitive.Label>,
inset, React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Label> & {
...props
}: ComponentProps<typeof Label> & {
inset?: boolean inset?: boolean
}) { }
return ( >(({ className, inset, ...props }, ref) => (
<Label <ContextMenuPrimitive.Label
data-slot="context-menu-label" ref={ref}
data-inset={inset}
className={cn( className={cn(
"text-foreground px-2 py-1.5 text-sm font-medium data-[inset]:pl-8", "px-2 py-1.5 text-sm font-semibold text-foreground",
inset && "pl-8",
className className
) )}
{...props} {...props}
/> />
) ))
} ContextMenuLabel.displayName = ContextMenuPrimitive.Label.displayName
function ContextMenuSeparator({ const ContextMenuSeparator = React.forwardRef<
className, React.ElementRef<typeof ContextMenuPrimitive.Separator>,
...props React.ComponentPropsWithoutRef<typeof ContextMenuPrimitive.Separator>
}: ComponentProps<typeof Separator>) { >(({ className, ...props }, ref) => (
return ( <ContextMenuPrimitive.Separator
<Separator ref={ref}
data-slot="context-menu-separator" className={cn("-mx-1 my-1 h-px bg-border", className)}
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props} {...props}
/> />
) ))
} ContextMenuSeparator.displayName = ContextMenuPrimitive.Separator.displayName
function ContextMenuShortcut({ const ContextMenuShortcut = ({
className, className,
...props ...props
}: ComponentProps<"span">) { }: React.HTMLAttributes<HTMLSpanElement>) => {
return ( return (
<span <span
data-slot="context-menu-shortcut"
className={cn( className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest", "ml-auto text-xs tracking-widest text-muted-foreground",
className className
)} )}
{...props} {...props}
/> />
) )
} }
ContextMenuShortcut.displayName = "ContextMenuShortcut"
export { export {
ContextMenu, ContextMenu,

View file

@ -2,9 +2,7 @@
import { ComponentProps } from "react" import { ComponentProps } from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import CheckIcon from "lucide-react/dist/esm/icons/check" import { Check as CheckIcon, ChevronRight as ChevronRightIcon, Circle as CircleIcon } from "lucide-react"
import ChevronRightIcon from "lucide-react/dist/esm/icons/chevron-right"
import CircleIcon from "lucide-react/dist/esm/icons/circle"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"

View file

@ -2,7 +2,7 @@
import { ComponentProps, useContext } from "react" import { ComponentProps, useContext } from "react"
import { OTPInput, OTPInputContext } from "input-otp" import { OTPInput, OTPInputContext } from "input-otp"
import MinusIcon from "lucide-react/dist/esm/icons/minus" import { Minus as MinusIcon } from "lucide-react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"

View file

@ -1,8 +1,6 @@
import { ComponentProps } from "react" import { ComponentProps } from "react"
import * as MenubarPrimitive from "@radix-ui/react-menubar" import * as MenubarPrimitive from "@radix-ui/react-menubar"
import CheckIcon from "lucide-react/dist/esm/icons/check" import { Check as CheckIcon, ChevronRight as ChevronRightIcon, Circle as CircleIcon } from "lucide-react"
import ChevronRightIcon from "lucide-react/dist/esm/icons/chevron-right"
import CircleIcon from "lucide-react/dist/esm/icons/circle"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"

View file

@ -1,7 +1,7 @@
import { ComponentProps } from "react" import { ComponentProps } from "react"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu" import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority" import { cva } from "class-variance-authority"
import ChevronDownIcon from "lucide-react/dist/esm/icons/chevron-down" import { ChevronDown as ChevronDownIcon } from "lucide-react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"

View file

@ -1,7 +1,5 @@
import { ComponentProps } from "react" import { ComponentProps } from "react"
import ChevronLeftIcon from "lucide-react/dist/esm/icons/chevron-left" import { ChevronLeft as ChevronLeftIcon, ChevronRight as ChevronRightIcon, MoreHorizontal as MoreHorizontalIcon } from "lucide-react"
import ChevronRightIcon from "lucide-react/dist/esm/icons/chevron-right"
import MoreHorizontalIcon from "lucide-react/dist/esm/icons/more-horizontal"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button" import { Button, buttonVariants } from "@/components/ui/button"

View file

@ -2,7 +2,7 @@
import { ComponentProps } from "react" import { ComponentProps } from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group" import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import CircleIcon from "lucide-react/dist/esm/icons/circle" import { Circle as CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"

View file

@ -1,5 +1,4 @@
import { ComponentProps } from "react" import { ComponentProps } from "react"
import GripVerticalIcon from "lucide-react/dist/esm/icons/grip-vertical"
import * as ResizablePrimitive from "react-resizable-panels" import * as ResizablePrimitive from "react-resizable-panels"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
@ -12,7 +11,7 @@ function ResizablePanelGroup({
<ResizablePrimitive.Group <ResizablePrimitive.Group
data-slot="resizable-panel-group" data-slot="resizable-panel-group"
className={cn( className={cn(
"flex h-full w-full data-[panel-group-direction=vertical]:flex-col", "flex h-full w-full data-[panel-group-orientation=vertical]:flex-col",
className className
)} )}
{...props} {...props}
@ -37,17 +36,11 @@ function ResizableHandle({
<ResizablePrimitive.Separator <ResizablePrimitive.Separator
data-slot="resizable-handle" data-slot="resizable-handle"
className={cn( className={cn(
"bg-border focus-visible:ring-ring relative flex w-px items-center justify-center after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-offset-1 focus-visible:outline-hidden data-[panel-group-direction=vertical]:h-px data-[panel-group-direction=vertical]:w-full data-[panel-group-direction=vertical]:after:left-0 data-[panel-group-direction=vertical]:after:h-1 data-[panel-group-direction=vertical]:after:w-full data-[panel-group-direction=vertical]:after:-translate-y-1/2 data-[panel-group-direction=vertical]:after:translate-x-0 [&[data-panel-group-direction=vertical]>div]:rotate-90", "relative flex w-px cursor-col-resize items-center justify-center bg-white/5 hover:bg-purple-500/50 active:bg-purple-500 transition-colors focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-hidden data-[panel-group-orientation=vertical]:h-px data-[panel-group-orientation=vertical]:w-full data-[panel-group-orientation=vertical]:cursor-row-resize",
className className
)} )}
{...props} {...props}
> />
{withHandle && (
<div className="bg-border z-10 flex h-4 w-3 items-center justify-center rounded-xs border">
<GripVerticalIcon className="size-2.5" />
</div>
)}
</ResizablePrimitive.Separator>
) )
} }

View file

@ -3,7 +3,7 @@
import { CSSProperties, ComponentProps, createContext, useCallback, useContext, useEffect, useMemo, useState } from "react" import { CSSProperties, ComponentProps, createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"
import { Slot } from "@radix-ui/react-slot" import { Slot } from "@radix-ui/react-slot"
import { VariantProps, cva } from "class-variance-authority" import { VariantProps, cva } from "class-variance-authority"
import PanelLeftIcon from "lucide-react/dist/esm/icons/panel-left" import { PanelLeft as PanelLeftIcon } from "lucide-react"
import { useIsMobile } from "@/hooks/use-mobile" import { useIsMobile } from "@/hooks/use-mobile"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"

View file

@ -1,13 +1,13 @@
import { useTheme } from "next-themes" import { useTheme } from "next-themes"
import { CSSProperties } from "react" import { CSSProperties, ComponentProps } from "react"
import { Toaster as Sonner, ToasterProps } from "sonner" import { Toaster as Sonner } from "sonner"
const Toaster = ({ ...props }: ToasterProps) => { const Toaster = ({ ...props }: ComponentProps<typeof Sonner>) => {
const { theme = "system" } = useTheme() const { theme = "system" } = useTheme()
return ( return (
<Sonner <Sonner
theme={theme as ToasterProps["theme"]} theme={theme as "light" | "dark" | "system"}
className="toaster group" className="toaster group"
style={ style={
{ {

View file

@ -39,7 +39,7 @@ export function ExtraTabs({ user }: { user: any }) {
<TabsContent value="profile"><UserProfile user={user} /></TabsContent> <TabsContent value="profile"><UserProfile user={user} /></TabsContent>
<TabsContent value="uefn"><UEFNPanel /></TabsContent> <TabsContent value="uefn"><UEFNPanel /></TabsContent>
<TabsContent value="preview"><GamePreviewPanel /></TabsContent> <TabsContent value="preview"><GamePreviewPanel /></TabsContent>
<TabsContent value="translation"><TranslationPanel /></TabsContent> <TabsContent value="translation"><TranslationPanel isOpen={true} onClose={() => {}} currentCode="" currentPlatform="roblox" /></TabsContent>
<TabsContent value="spatial"><SpatialPanel /></TabsContent> <TabsContent value="spatial"><SpatialPanel /></TabsContent>
<TabsContent value="assets"><AssetLibraryPanel /></TabsContent> <TabsContent value="assets"><AssetLibraryPanel /></TabsContent>
<TabsContent value="teacher"><TeacherDashboard /></TabsContent> <TabsContent value="teacher"><TeacherDashboard /></TabsContent>

95
src/hooks/use-projects.ts Normal file
View file

@ -0,0 +1,95 @@
'use client';
import { useEffect, useState } from 'react';
import { getSupabase } from '@/lib/supabase/client';
import { useSupabaseAuth } from './use-supabase';
import type { Database } from '@/lib/supabase/types';
type Project = Database['public']['Tables']['projects']['Row'];
type ProjectInsert = Database['public']['Tables']['projects']['Insert'];
export function useProjects() {
const { user } = useSupabaseAuth();
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!user) {
setProjects([]);
setLoading(false);
return;
}
const fetchProjects = async () => {
const supabase = getSupabase();
const { data, error } = await supabase
.from('projects')
.select('*')
.eq('user_id', user.id)
.order('updated_at', { ascending: false });
if (error) {
console.error('Error fetching projects:', error);
} else {
setProjects(data ?? []);
}
setLoading(false);
};
fetchProjects();
}, [user]);
const createProject = async (project: Omit<ProjectInsert, 'user_id'>) => {
if (!user) return { error: new Error('Not authenticated') };
const supabase = getSupabase();
const { data, error } = await supabase
.from('projects')
.insert({ ...project, user_id: user.id })
.select()
.single();
if (!error && data) {
setProjects((prev) => [data, ...prev]);
}
return { data, error };
};
const updateProject = async (id: string, updates: Partial<Project>) => {
const supabase = getSupabase();
const { data, error } = await supabase
.from('projects')
.update({ ...updates, updated_at: new Date().toISOString() })
.eq('id', id)
.select()
.single();
if (!error && data) {
setProjects((prev) =>
prev.map((p) => (p.id === id ? data : p))
);
}
return { data, error };
};
const deleteProject = async (id: string) => {
const supabase = getSupabase();
const { error } = await supabase.from('projects').delete().eq('id', id);
if (!error) {
setProjects((prev) => prev.filter((p) => p.id !== id));
}
return { error };
};
return {
projects,
loading,
createProject,
updateProject,
deleteProject,
};
}

136
src/hooks/use-supabase.ts Normal file
View file

@ -0,0 +1,136 @@
'use client';
import { useEffect, useState } from 'react';
import { User, Session, AuthChangeEvent } from '@supabase/supabase-js';
import { getSupabase } from '@/lib/supabase/client';
import type { Database } from '@/lib/supabase/types';
type Profile = Database['public']['Tables']['profiles']['Row'];
export function useSupabaseAuth() {
const [user, setUser] = useState<User | null>(null);
const [session, setSession] = useState<Session | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
const supabase = getSupabase();
// Get initial session
const getInitialSession = async () => {
const { data } = await supabase.auth.getSession();
setSession(data.session);
setUser(data.session?.user ?? null);
setLoading(false);
};
getInitialSession();
// Listen for auth changes
const {
data: { subscription },
} = supabase.auth.onAuthStateChange((_event: AuthChangeEvent, session: Session | null) => {
setSession(session);
setUser(session?.user ?? null);
setLoading(false);
});
return () => subscription.unsubscribe();
}, []);
const signIn = async (email: string, password: string) => {
const supabase = getSupabase();
const { data, error } = await supabase.auth.signInWithPassword({
email,
password,
});
return { data, error };
};
const signUp = async (email: string, password: string) => {
const supabase = getSupabase();
const { data, error } = await supabase.auth.signUp({
email,
password,
});
return { data, error };
};
const signInWithOAuth = async (provider: 'github' | 'google' | 'discord') => {
const supabase = getSupabase();
const { data, error } = await supabase.auth.signInWithOAuth({
provider,
options: {
redirectTo: `${window.location.origin}/auth/callback`,
},
});
return { data, error };
};
const signOut = async () => {
const supabase = getSupabase();
const { error } = await supabase.auth.signOut();
return { error };
};
return {
user,
session,
loading,
signIn,
signUp,
signInWithOAuth,
signOut,
};
}
export function useProfile() {
const { user } = useSupabaseAuth();
const [profile, setProfile] = useState<Profile | null>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!user) {
setProfile(null);
setLoading(false);
return;
}
const fetchProfile = async () => {
const supabase = getSupabase();
const { data, error } = await supabase
.from('profiles')
.select('*')
.eq('id', user.id)
.single();
if (error) {
console.error('Error fetching profile:', error);
} else {
setProfile(data);
}
setLoading(false);
};
fetchProfile();
}, [user]);
const updateProfile = async (updates: Partial<Profile>) => {
if (!user) return { error: new Error('Not authenticated') };
const supabase = getSupabase();
const { data, error } = await supabase
.from('profiles')
.update({ ...updates, updated_at: new Date().toISOString() })
.eq('id', user.id)
.select()
.single();
if (!error && data) {
setProfile(data);
}
return { data, error };
};
return { profile, loading, updateProfile };
}

View file

@ -0,0 +1,18 @@
import { createBrowserClient } from '@supabase/ssr';
export function createClient() {
return createBrowserClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
);
}
// Singleton instance for client-side usage
let supabaseInstance: ReturnType<typeof createBrowserClient> | null = null;
export function getSupabase() {
if (!supabaseInstance) {
supabaseInstance = createClient();
}
return supabaseInstance;
}

View file

@ -0,0 +1,36 @@
import { createServerClient } from '@supabase/ssr';
import { NextResponse, type NextRequest } from 'next/server';
export async function updateSession(request: NextRequest) {
let supabaseResponse = NextResponse.next({
request,
});
const supabase = createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return request.cookies.getAll();
},
setAll(cookiesToSet) {
cookiesToSet.forEach(({ name, value }) =>
request.cookies.set(name, value)
);
supabaseResponse = NextResponse.next({
request,
});
cookiesToSet.forEach(({ name, value, options }) =>
supabaseResponse.cookies.set(name, value, options)
);
},
},
}
);
// Refresh session if expired
await supabase.auth.getUser();
return supabaseResponse;
}

View file

@ -0,0 +1,28 @@
import { createServerClient, type CookieOptions } from '@supabase/ssr';
import { cookies } from 'next/headers';
export async function createClient() {
const cookieStore = await cookies();
return createServerClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
getAll() {
return cookieStore.getAll();
},
setAll(cookiesToSet) {
try {
cookiesToSet.forEach(({ name, value, options }) =>
cookieStore.set(name, value, options)
);
} catch {
// The `setAll` method was called from a Server Component.
// This can be ignored if you have middleware refreshing user sessions.
}
},
},
}
);
}

143
src/lib/supabase/types.ts Normal file
View file

@ -0,0 +1,143 @@
// Database types - Generate with: npx supabase gen types typescript --project-id YOUR_PROJECT_ID > src/lib/supabase/types.ts
export type Json =
| string
| number
| boolean
| null
| { [key: string]: Json | undefined }
| Json[];
export interface Database {
public: {
Tables: {
profiles: {
Row: {
id: string;
username: string | null;
avatar_url: string | null;
created_at: string;
updated_at: string;
subscription_tier: 'free' | 'studio' | 'pro' | 'enterprise';
translation_count: number;
};
Insert: {
id: string;
username?: string | null;
avatar_url?: string | null;
created_at?: string;
updated_at?: string;
subscription_tier?: 'free' | 'studio' | 'pro' | 'enterprise';
translation_count?: number;
};
Update: {
id?: string;
username?: string | null;
avatar_url?: string | null;
created_at?: string;
updated_at?: string;
subscription_tier?: 'free' | 'studio' | 'pro' | 'enterprise';
translation_count?: number;
};
};
projects: {
Row: {
id: string;
user_id: string;
name: string;
description: string | null;
platforms: string[];
created_at: string;
updated_at: string;
is_public: boolean;
};
Insert: {
id?: string;
user_id: string;
name: string;
description?: string | null;
platforms?: string[];
created_at?: string;
updated_at?: string;
is_public?: boolean;
};
Update: {
id?: string;
user_id?: string;
name?: string;
description?: string | null;
platforms?: string[];
created_at?: string;
updated_at?: string;
is_public?: boolean;
};
};
files: {
Row: {
id: string;
project_id: string;
name: string;
path: string;
content: string | null;
language: string;
created_at: string;
updated_at: string;
};
Insert: {
id?: string;
project_id: string;
name: string;
path: string;
content?: string | null;
language: string;
created_at?: string;
updated_at?: string;
};
Update: {
id?: string;
project_id?: string;
name?: string;
path?: string;
content?: string | null;
language?: string;
created_at?: string;
updated_at?: string;
};
};
translations: {
Row: {
id: string;
user_id: string;
source_platform: string;
target_platform: string;
source_code: string;
translated_code: string;
created_at: string;
};
Insert: {
id?: string;
user_id: string;
source_platform: string;
target_platform: string;
source_code: string;
translated_code: string;
created_at?: string;
};
Update: {
id?: string;
user_id?: string;
source_platform?: string;
target_platform?: string;
source_code?: string;
translated_code?: string;
created_at?: string;
};
};
};
Views: Record<string, never>;
Functions: Record<string, never>;
Enums: {
subscription_tier: 'free' | 'studio' | 'pro' | 'enterprise';
};
};
}

View file

@ -2,6 +2,7 @@ import { PlatformId } from './platforms';
import { uefnTemplates } from './templates-uefn'; import { uefnTemplates } from './templates-uefn';
import { spatialTemplates } from './templates-spatial'; import { spatialTemplates } from './templates-spatial';
export interface ScriptTemplate {
id: string; id: string;
name: string; name: string;
description: string; description: string;

View file

@ -77,7 +77,7 @@ Example:
/** /**
* Platform-specific translation rules * Platform-specific translation rules
*/ */
const platformTranslationRules: Record<string, Record<string, string[]>> = { const platformTranslationRules: Record<string, string[]> = {
'roblox-to-uefn': [ 'roblox-to-uefn': [
'game:GetService() → Use Verse imports', 'game:GetService() → Use Verse imports',
'Instance.new() → object{} syntax in Verse', 'Instance.new() → object{} syntax in Verse',
@ -145,7 +145,7 @@ async function translateWithClaudeAPI(
): Promise<TranslationResult> { ): Promise<TranslationResult> {
try { try {
// Check if API key is configured // Check if API key is configured
const apiKey = import.meta.env.VITE_CLAUDE_API_KEY || process.env.NEXT_PUBLIC_CLAUDE_API_KEY; const apiKey = process.env.NEXT_PUBLIC_CLAUDE_API_KEY;
if (!apiKey) { if (!apiKey) {
console.warn('Claude API key not configured, using mock translation'); console.warn('Claude API key not configured, using mock translation');

View file

@ -1 +1 @@
export * from "./utils"; export * from "../utils";

View file

@ -17,7 +17,6 @@ declare module 'lucide-react/dist/esm/icons/circle' {
const CircleIcon: React.FC<React.SVGProps<SVGSVGElement>>; const CircleIcon: React.FC<React.SVGProps<SVGSVGElement>>;
export default CircleIcon; export default CircleIcon;
} }
}
declare module 'lucide-react/dist/esm/icons/chevron-left' { declare module 'lucide-react/dist/esm/icons/chevron-left' {
const ChevronLeft: any; const ChevronLeft: any;
export default ChevronLeft; export default ChevronLeft;

View file

@ -32,6 +32,7 @@ interface EditorStore {
updateFileContent: (fileId: string, content: string) => void; updateFileContent: (fileId: string, content: string) => void;
saveFile: (fileId: string) => void; saveFile: (fileId: string) => void;
saveAllFiles: () => void; saveAllFiles: () => void;
moveFile: (fileId: string, targetFolderId: string) => void;
} }
export const useEditorStore = create<EditorStore>((set, get) => ({ export const useEditorStore = create<EditorStore>((set, get) => ({
@ -188,4 +189,47 @@ export const useEditorStore = create<EditorStore>((set, get) => ({
}); });
}, 500); }, 500);
}, },
moveFile: (fileId: string, targetFolderId: string) => {
const { files } = get();
// Helper to find and remove a file from tree
const findAndRemove = (nodes: FileNode[], id: string): { nodes: FileNode[]; removed: FileNode | null } => {
let removed: FileNode | null = null;
const newNodes = nodes.filter(n => {
if (n.id === id) {
removed = n;
return false;
}
return true;
}).map(n => {
if (n.children && !removed) {
const result = findAndRemove(n.children, id);
removed = result.removed;
return { ...n, children: result.nodes };
}
return n;
});
return { nodes: newNodes, removed };
};
// Helper to add file to target folder
const addToFolder = (nodes: FileNode[], folderId: string, file: FileNode): FileNode[] => {
return nodes.map(n => {
if (n.id === folderId && n.type === 'folder') {
return { ...n, children: [...(n.children || []), file] };
}
if (n.children) {
return { ...n, children: addToFolder(n.children, folderId, file) };
}
return n;
});
};
const { nodes: afterRemove, removed } = findAndRemove(files, fileId);
if (removed) {
const newFiles = addToFolder(afterRemove, targetFolderId, removed);
set({ files: newFiles });
}
},
})); }));

View file

@ -0,0 +1,240 @@
-- AeThex Studio Initial Database Schema
-- Run this in Supabase SQL Editor to set up your database
-- Enable RLS (Row Level Security)
alter default privileges revoke execute on functions from public;
-- Create subscription tier enum
create type subscription_tier as enum ('free', 'studio', 'pro', 'enterprise');
-- ============================================
-- PROFILES TABLE
-- ============================================
create table public.profiles (
id uuid references auth.users on delete cascade primary key,
username text unique,
avatar_url text,
created_at timestamptz default now() not null,
updated_at timestamptz default now() not null,
subscription_tier subscription_tier default 'free' not null,
translation_count integer default 0 not null
);
-- Enable RLS
alter table public.profiles enable row level security;
-- Policies
create policy "Users can view their own profile"
on public.profiles for select
using (auth.uid() = id);
create policy "Users can update their own profile"
on public.profiles for update
using (auth.uid() = id);
-- Auto-create profile on signup
create function public.handle_new_user()
returns trigger
language plpgsql
security definer set search_path = ''
as $$
begin
insert into public.profiles (id, username, avatar_url)
values (
new.id,
new.raw_user_meta_data ->> 'username',
new.raw_user_meta_data ->> 'avatar_url'
);
return new;
end;
$$;
create trigger on_auth_user_created
after insert on auth.users
for each row execute procedure public.handle_new_user();
-- ============================================
-- PROJECTS TABLE
-- ============================================
create table public.projects (
id uuid default gen_random_uuid() primary key,
user_id uuid references public.profiles(id) on delete cascade not null,
name text not null,
description text,
platforms text[] default '{}' not null,
created_at timestamptz default now() not null,
updated_at timestamptz default now() not null,
is_public boolean default false not null
);
-- Enable RLS
alter table public.projects enable row level security;
-- Policies
create policy "Users can view their own projects"
on public.projects for select
using (auth.uid() = user_id);
create policy "Users can view public projects"
on public.projects for select
using (is_public = true);
create policy "Users can insert their own projects"
on public.projects for insert
with check (auth.uid() = user_id);
create policy "Users can update their own projects"
on public.projects for update
using (auth.uid() = user_id);
create policy "Users can delete their own projects"
on public.projects for delete
using (auth.uid() = user_id);
-- Indexes
create index projects_user_id_idx on public.projects(user_id);
create index projects_is_public_idx on public.projects(is_public) where is_public = true;
-- ============================================
-- FILES TABLE
-- ============================================
create table public.files (
id uuid default gen_random_uuid() primary key,
project_id uuid references public.projects(id) on delete cascade not null,
name text not null,
path text not null,
content text,
language text not null,
created_at timestamptz default now() not null,
updated_at timestamptz default now() not null,
unique(project_id, path)
);
-- Enable RLS
alter table public.files enable row level security;
-- Policies
create policy "Users can view files in their projects"
on public.files for select
using (
exists (
select 1 from public.projects
where projects.id = files.project_id
and projects.user_id = auth.uid()
)
);
create policy "Users can view files in public projects"
on public.files for select
using (
exists (
select 1 from public.projects
where projects.id = files.project_id
and projects.is_public = true
)
);
create policy "Users can insert files to their projects"
on public.files for insert
with check (
exists (
select 1 from public.projects
where projects.id = files.project_id
and projects.user_id = auth.uid()
)
);
create policy "Users can update files in their projects"
on public.files for update
using (
exists (
select 1 from public.projects
where projects.id = files.project_id
and projects.user_id = auth.uid()
)
);
create policy "Users can delete files from their projects"
on public.files for delete
using (
exists (
select 1 from public.projects
where projects.id = files.project_id
and projects.user_id = auth.uid()
)
);
-- Indexes
create index files_project_id_idx on public.files(project_id);
-- ============================================
-- TRANSLATIONS TABLE
-- ============================================
create table public.translations (
id uuid default gen_random_uuid() primary key,
user_id uuid references public.profiles(id) on delete cascade not null,
source_platform text not null,
target_platform text not null,
source_code text not null,
translated_code text not null,
created_at timestamptz default now() not null
);
-- Enable RLS
alter table public.translations enable row level security;
-- Policies
create policy "Users can view their own translations"
on public.translations for select
using (auth.uid() = user_id);
create policy "Users can insert their own translations"
on public.translations for insert
with check (auth.uid() = user_id);
-- Indexes
create index translations_user_id_idx on public.translations(user_id);
create index translations_created_at_idx on public.translations(created_at desc);
-- ============================================
-- HELPER FUNCTIONS
-- ============================================
-- Increment translation count for user
create function public.increment_translation_count(user_uuid uuid)
returns void
language plpgsql
security definer
as $$
begin
update public.profiles
set translation_count = translation_count + 1,
updated_at = now()
where id = user_uuid;
end;
$$;
-- Updated at trigger function
create function public.handle_updated_at()
returns trigger
language plpgsql
as $$
begin
new.updated_at = now();
return new;
end;
$$;
-- Apply updated_at triggers
create trigger set_profiles_updated_at
before update on public.profiles
for each row execute procedure public.handle_updated_at();
create trigger set_projects_updated_at
before update on public.projects
for each row execute procedure public.handle_updated_at();
create trigger set_files_updated_at
before update on public.files
for each row execute procedure public.handle_updated_at();

View file

@ -1,153 +0,0 @@
import fs from "fs";
/** @type {import('tailwindcss').Config} */
let theme = {};
try {
const themePath = "./theme.json";
if (fs.existsSync(themePath)) {
theme = JSON.parse(fs.readFileSync(themePath, "utf-8"));
}
} catch (err) {
// Silently fall back to empty theme object if custom theme cannot be loaded
theme = {};
}
const defaultTheme = {
container: {
center: true,
padding: "2rem",
},
extend: {
screens: {
coarse: { raw: "(pointer: coarse)" },
fine: { raw: "(pointer: fine)" },
pwa: { raw: "(display-mode: standalone)" },
},
colors: {
neutral: {
1: "var(--color-neutral-1)",
2: "var(--color-neutral-2)",
3: "var(--color-neutral-3)",
4: "var(--color-neutral-4)",
5: "var(--color-neutral-5)",
6: "var(--color-neutral-6)",
7: "var(--color-neutral-7)",
8: "var(--color-neutral-8)",
9: "var(--color-neutral-9)",
10: "var(--color-neutral-10)",
11: "var(--color-neutral-11)",
12: "var(--color-neutral-12)",
a1: "var(--color-neutral-a1)",
a2: "var(--color-neutral-a2)",
a3: "var(--color-neutral-a3)",
a4: "var(--color-neutral-a4)",
a5: "var(--color-neutral-a5)",
a6: "var(--color-neutral-a6)",
a7: "var(--color-neutral-a7)",
a8: "var(--color-neutral-a8)",
a9: "var(--color-neutral-a9)",
a10: "var(--color-neutral-a10)",
a11: "var(--color-neutral-a11)",
a12: "var(--color-neutral-a12)",
contrast: "var(--color-neutral-contrast)",
},
accent: {
1: "var(--color-accent-1)",
2: "var(--color-accent-2)",
3: "var(--color-accent-3)",
4: "var(--color-accent-4)",
5: "var(--color-accent-5)",
6: "var(--color-accent-6)",
7: "var(--color-accent-7)",
8: "var(--color-accent-8)",
9: "var(--color-accent-9)",
10: "var(--color-accent-10)",
11: "var(--color-accent-11)",
12: "var(--color-accent-12)",
contrast: "var(--color-accent-contrast)",
},
"accent-secondary": {
1: "var(--color-accent-secondary-1)",
2: "var(--color-accent-secondary-2)",
3: "var(--color-accent-secondary-3)",
4: "var(--color-accent-secondary-4)",
5: "var(--color-accent-secondary-5)",
6: "var(--color-accent-secondary-6)",
7: "var(--color-accent-secondary-7)",
8: "var(--color-accent-secondary-8)",
9: "var(--color-accent-secondary-9)",
10: "var(--color-accent-secondary-10)",
11: "var(--color-accent-secondary-11)",
12: "var(--color-accent-secondary-12)",
contrast: "var(--color-accent-secondary-contrast)",
},
fg: {
DEFAULT: "var(--color-fg)",
secondary: "var(--color-fg-secondary)",
},
bg: {
DEFAULT: "var(--color-bg)",
inset: "var(--color-bg-inset)",
overlay: "var(--color-bg-overlay)",
},
"focus-ring": "var(--color-focus-ring)",
},
borderRadius: {
sm: "var(--radius-sm)",
md: "var(--radius-md)",
lg: "var(--radius-lg)",
xl: "var(--radius-xl)",
"2xl": "var(--radius-2xl)",
full: "var(--radius-full)",
},
},
spacing: {
px: "var(--size-px)",
0: "var(--size-0)",
0.5: "var(--size-0-5)",
1: "var(--size-1)",
1.5: "var(--size-1-5)",
2: "var(--size-2)",
2.5: "var(--size-2-5)",
3: "var(--size-3)",
3.5: "var(--size-3-5)",
4: "var(--size-4)",
5: "var(--size-5)",
6: "var(--size-6)",
7: "var(--size-7)",
8: "var(--size-8)",
9: "var(--size-9)",
10: "var(--size-10)",
11: "var(--size-11)",
12: "var(--size-12)",
14: "var(--size-14)",
16: "var(--size-16)",
20: "var(--size-20)",
24: "var(--size-24)",
28: "var(--size-28)",
32: "var(--size-32)",
36: "var(--size-36)",
40: "var(--size-40)",
44: "var(--size-44)",
48: "var(--size-48)",
52: "var(--size-52)",
56: "var(--size-56)",
60: "var(--size-60)",
64: "var(--size-64)",
72: "var(--size-72)",
80: "var(--size-80)",
96: "var(--size-96)",
},
darkMode: ["selector", '[data-appearance="dark"]'],
}
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
"./app/**/*.{js,ts,jsx,tsx}",
],
theme: { ...defaultTheme, ...theme },
plugins: [require("tailwindcss-animate")],
};

View file

@ -6,6 +6,7 @@ const config: Config = {
"./pages/**/*.{js,ts,jsx,tsx,mdx}", "./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}", "./app/**/*.{js,ts,jsx,tsx,mdx}",
"./src/**/*.{js,ts,jsx,tsx,mdx}",
], ],
theme: { theme: {
extend: { extend: {

View file

@ -20,9 +20,9 @@
} }
], ],
"paths": { "paths": {
"@/*": ["./*"] "@/*": ["./src/*", "./*"]
} }
}, },
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"] "exclude": ["node_modules", "**/*.test.ts", "**/*.test.tsx", "**/test/**", "**/__tests__/**", "vitest.config.ts", "vite.config.ts"]
} }