deleted: app/ide/page.tsx
This commit is contained in:
parent
a8b2ffc3fe
commit
524f64315d
70 changed files with 5527 additions and 1725 deletions
15
.env.example
15
.env.example
|
|
@ -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
1
.gitignore
vendored
|
|
@ -95,3 +95,4 @@ pids
|
||||||
.devcontainer/
|
.devcontainer/
|
||||||
|
|
||||||
.spark-workbench-id
|
.spark-workbench-id
|
||||||
|
.env.local
|
||||||
|
|
|
||||||
18
app/App.tsx
18
app/App.tsx
|
|
@ -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';
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
import { DashboardPage } from "../../src/components/aethex/dashboard-page";
|
|
||||||
export default function Page() {
|
|
||||||
return <DashboardPage />;
|
|
||||||
}
|
|
||||||
154
app/globals.css
154
app/globals.css
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
import App from "../../src/App";
|
|
||||||
export default function Page() {
|
|
||||||
return <App />;
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
|
||||||
|
|
@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
61
app/main.css
61
app/main.css
|
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
import App from "../src/App";
|
|
||||||
|
|
||||||
export default function Page() {
|
|
||||||
return <App />;
|
|
||||||
}
|
|
||||||
|
|
@ -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... */
|
|
||||||
|
|
@ -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
19
middleware.ts
Normal 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
774
package-lock.json
generated
|
|
@ -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",
|
||||||
|
|
|
||||||
14
package.json
14
package.json
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
17
src/App.tsx
17
src/App.tsx
|
|
@ -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} />}
|
||||||
|
|
|
||||||
66
src/app/auth/auth-code-error/page.tsx
Normal file
66
src/app/auth/auth-code-error/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
src/app/auth/callback/route.ts
Normal file
21
src/app/auth/callback/route.ts
Normal 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
169
src/app/auth/login/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
230
src/app/auth/signup/page.tsx
Normal file
230
src/app/auth/signup/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
472
src/app/page.tsx
472
src/app/page.tsx
|
|
@ -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
249
src/app/profile/page.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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[];
|
||||||
|
|
|
||||||
|
|
@ -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 && (
|
||||||
|
|
|
||||||
|
|
@ -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}
|
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
211
src/components/aethex/export-modal.tsx
Normal file
211
src/components/aethex/export-modal.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
219
src/components/aethex/git-panel.tsx
Normal file
219
src/components/aethex/git-panel.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
320
src/components/aethex/settings-panel.tsx
Normal file
320
src/components/aethex/settings-panel.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
182
src/components/aethex/status-bar.tsx
Normal file
182
src/components/aethex/status-bar.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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={
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -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
95
src/hooks/use-projects.ts
Normal 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
136
src/hooks/use-supabase.ts
Normal 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 };
|
||||||
|
}
|
||||||
18
src/lib/supabase/client.ts
Normal file
18
src/lib/supabase/client.ts
Normal 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;
|
||||||
|
}
|
||||||
36
src/lib/supabase/middleware.ts
Normal file
36
src/lib/supabase/middleware.ts
Normal 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;
|
||||||
|
}
|
||||||
28
src/lib/supabase/server.ts
Normal file
28
src/lib/supabase/server.ts
Normal 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
143
src/lib/supabase/types.ts
Normal 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';
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
export * from "./utils";
|
export * from "../utils";
|
||||||
1
src/types/lucide-react.d.ts
vendored
1
src/types/lucide-react.d.ts
vendored
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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 });
|
||||||
|
}
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
|
||||||
240
supabase/migrations/001_initial_schema.sql
Normal file
240
supabase/migrations/001_initial_schema.sql
Normal 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();
|
||||||
|
|
@ -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")],
|
|
||||||
};
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue