mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
Generated by Spark: We should have 4 levels - 1( normal webpage, hamburger menu, some sample content, 2) Normal user area, normal users can sign up and do normal user stuff, nothing destructive. The can browse their profile page and play with the comment box. 3) django style admin panel for admin user 4) god tier panel where the first 3 levels are designed, developed and tested. Intuitive ui, maybe a workflow system, lua lambdas, json data structures with GUI editor, Make it abstract enough that whole first 3 levels can be procedurally generated.
This commit is contained in:
111
PRD.md
111
PRD.md
@@ -1,74 +1,77 @@
|
||||
# Planning Guide
|
||||
|
||||
A visual drag-and-drop GUI builder that allows developers to create custom admin panels and applications by arranging components visually, with an integrated Monaco code editor for advanced customization and scripting. The system includes authentication to protect the builder interface.
|
||||
A meta-architecture system with 4 distinct levels: Level 1 (public-facing website with sample content), Level 2 (user area with profiles and comments), Level 3 (Django-style admin panel for data management), and Level 4 (god-tier builder where all previous levels can be designed, developed, and tested through visual workflows, GUI editors for JSON schemas, and Lua scripting).
|
||||
|
||||
**Experience Qualities**:
|
||||
1. **Visual** - Intuitive drag-and-drop interface where components can be placed, configured, and connected without writing code
|
||||
2. **Flexible** - Seamlessly switch between visual building and code editing with Monaco editor for advanced customization
|
||||
3. **Secure** - Login system protecting the admin builder with default credentials, ensuring only authorized users can modify the interface
|
||||
1. **Layered** - Clear separation between public, user, admin, and meta-builder levels with intuitive navigation between tiers
|
||||
2. **Generative** - Level 4 can procedurally generate entire applications for Levels 1-3 through declarative JSON schemas and visual workflows
|
||||
3. **Powerful** - Lua lambdas for custom logic, visual JSON schema editor, workflow system for complex processes, all through an intuitive GUI
|
||||
|
||||
**Complexity Level**: Complex Application (advanced functionality, likely with multiple views)
|
||||
This is a visual application builder with drag-and-drop UI composition, component property editors, Monaco code editor integration, authentication system, and dynamic rendering - requiring sophisticated state management, component catalog, and code execution environment.
|
||||
This is a 4-tier meta-application builder: a public website layer, authenticated user area, admin panel, and a god-tier visual builder that can procedurally generate all three previous layers using JSON schemas, workflow systems, and embedded Lua scripting.
|
||||
|
||||
## Essential Features
|
||||
|
||||
### Authentication System
|
||||
- **Functionality**: Login page with username/password authentication protecting the GUI builder
|
||||
- **Purpose**: Ensure only authorized users can access and modify the admin panel builder
|
||||
- **Trigger**: Application loads without valid session
|
||||
- **Progression**: Show login page → User enters credentials → Validate against stored credentials → Store session in KV → Redirect to builder
|
||||
- **Success criteria**: Invalid credentials show error; successful login persists session; logout clears session; default credentials (admin/admin) work initially
|
||||
### Level 1: Public Website
|
||||
- **Functionality**: Normal webpage with responsive hamburger menu, hero section, content areas, footer
|
||||
- **Purpose**: Public-facing content accessible to anyone without authentication
|
||||
- **Trigger**: User visits root URL without authentication
|
||||
- **Progression**: Load homepage → Browse content sections → Click hamburger menu → Navigate pages → View sample content
|
||||
- **Success criteria**: Responsive design works; hamburger menu collapses on mobile; content is readable; links work; no auth required
|
||||
|
||||
### Component Catalog
|
||||
- **Functionality**: Sidebar showing all available shadcn components (Button, Input, Card, Table, etc.) that can be dragged onto canvas
|
||||
- **Purpose**: Provide visual discovery and easy access to all UI building blocks
|
||||
- **Trigger**: Builder loads with authenticated session
|
||||
- **Progression**: Display component list → User searches/filters → Drag component → Drop on canvas → Component appears with default props
|
||||
- **Success criteria**: All shadcn components cataloged; search/filter works; drag preview shows; drop zones highlight; components render correctly
|
||||
### Level 2: User Area
|
||||
- **Functionality**: Authenticated user dashboard with profile page and comment system
|
||||
- **Purpose**: Allow normal users to sign up, manage their profile, and interact through comments
|
||||
- **Trigger**: User clicks "Sign Up" or "Login" from Level 1
|
||||
- **Progression**: Register account → Verify credentials → Access dashboard → Edit profile → Browse comment sections → Post/edit comments → View own history
|
||||
- **Success criteria**: Registration persists in KV; profile edits save; comments are CRUD-able; users can't access admin functions; profile picture upload works
|
||||
|
||||
### Drag-and-Drop Canvas
|
||||
- **Functionality**: Visual workspace where components can be dragged, dropped, positioned, and nested
|
||||
- **Purpose**: Enable intuitive visual composition of UI without code
|
||||
- **Trigger**: User drags component from catalog or rearranges existing components
|
||||
- **Progression**: Drag component → Hover over drop zones → Visual feedback shows valid targets → Drop → Component inserted → Selection updates
|
||||
- **Success criteria**: Smooth drag experience; clear drop zone indicators; nested layouts work; undo/redo functions; component selection/multi-select
|
||||
### Level 3: Django-Style Admin Panel
|
||||
- **Functionality**: Full data management interface with model list views, CRUD operations, filtering, search, and bulk actions
|
||||
- **Purpose**: Provide admin users comprehensive control over all data models and system configuration
|
||||
- **Trigger**: User with admin role logs in or selects "Admin" from navigation
|
||||
- **Progression**: Login as admin → View model dashboard → Select model → See filtered list view → Search/filter records → Click record → Edit form → Save changes → View audit trail
|
||||
- **Success criteria**: All models from schema rendered; inline editing; validation works; relations display correctly; permissions enforced; export to JSON/CSV
|
||||
|
||||
### Property Inspector
|
||||
- **Functionality**: Right sidebar showing editable properties for selected component (text, colors, sizes, variants, etc.)
|
||||
- **Purpose**: Configure component appearance and behavior without touching code
|
||||
- **Trigger**: Component selected on canvas
|
||||
- **Progression**: Select component → Inspector loads props → User edits values → Component updates live → Changes persist
|
||||
- **Success criteria**: All component props exposed; appropriate input types; live preview; validation feedback; reset to defaults
|
||||
### Level 4: God-Tier Builder
|
||||
- **Functionality**: Meta-builder with visual JSON schema editor, workflow designer, Lua lambda editor, component catalog, and live preview of Levels 1-3
|
||||
- **Purpose**: Allow god-level users to design, configure, and deploy entire applications for Levels 1-3 through declarative configuration
|
||||
- **Trigger**: User with god role accesses builder interface
|
||||
- **Progression**: Open builder → Design data schema in GUI → Create workflows visually → Write Lua handlers → Configure page templates → Preview generated app → Deploy configuration → Test all levels
|
||||
- **Success criteria**: JSON schema editor validates; workflow nodes connect; Lua syntax highlighting; live preview updates; can export/import configurations; changes propagate to all levels
|
||||
|
||||
### Monaco Code Editor
|
||||
- **Functionality**: Integrated Monaco editor (VS Code engine) for writing custom JavaScript/TypeScript logic
|
||||
- **Purpose**: Enable advanced customization beyond visual building - event handlers, data transformations, custom logic
|
||||
- **Trigger**: User clicks "Code" tab or "Edit Handler" in property inspector
|
||||
- **Progression**: Open code editor → View/edit component code → Syntax highlighting → Autocomplete → Save → Code executes in sandbox
|
||||
- **Success criteria**: Syntax highlighting works; autocomplete for available APIs; error detection; safe sandboxed execution; code persists
|
||||
### JSON Schema Editor (Level 4)
|
||||
- **Functionality**: Visual GUI for defining data models with fields, types, validation rules, relationships
|
||||
- **Purpose**: Declaratively define all data structures without writing JSON by hand
|
||||
- **Trigger**: User opens "Schema Designer" in Level 4
|
||||
- **Progression**: Create new model → Add fields via forms → Set field types/constraints → Define relations → Visualize schema graph → Validate → Generate Level 3 admin interface
|
||||
- **Success criteria**: All field types supported; visual relationship mapping; constraint validation; auto-generates CRUD interfaces; imports/exports valid JSON
|
||||
|
||||
### Safe Script Execution
|
||||
- **Functionality**: Sandboxed JavaScript execution environment for custom code with access to limited APIs (Spark KV, component methods, utility functions)
|
||||
- **Purpose**: Allow custom logic while preventing malicious code execution
|
||||
- **Trigger**: Custom code attached to component event handlers or lifecycle
|
||||
- **Progression**: User writes code → Parser validates → Execute in isolated context → Limited API access → Results render → Errors caught and displayed
|
||||
- **Success criteria**: Cannot access globals; cannot make arbitrary network requests; errors don't crash app; performance limits enforced
|
||||
### Workflow System (Level 4)
|
||||
- **Functionality**: Visual node-based workflow editor for defining business logic flows
|
||||
- **Purpose**: Create complex processes (approval flows, notifications, data transformations) without code
|
||||
- **Trigger**: User opens "Workflow Designer" in Level 4
|
||||
- **Progression**: Create workflow → Drag trigger node → Add action nodes → Connect with arrows → Configure conditions → Attach to data events → Test execution → Monitor runs
|
||||
- **Success criteria**: Nodes connect smoothly; execution order clear; can branch/merge; error handling; logs show execution path; integrates with Lua
|
||||
|
||||
### Layout System
|
||||
- **Functionality**: Pre-built layout components (Grid, Flex, Stack, Container) for organizing components spatially
|
||||
- **Purpose**: Enable responsive, structured layouts without CSS knowledge
|
||||
- **Trigger**: User drags layout component onto canvas
|
||||
- **Progression**: Drop layout → Configure columns/gaps → Drag children into layout → Auto-responsive → Preview mobile view
|
||||
- **Success criteria**: Flexbox/Grid layouts work; responsive breakpoints; gap/padding controls; nested layouts; alignment tools
|
||||
### Lua Lambda System (Level 4)
|
||||
- **Functionality**: Embedded Lua scripting environment for custom business logic with access to data context and utility functions
|
||||
- **Purpose**: Provide safe, sandboxed scripting for custom transformations and validations beyond declarative capabilities
|
||||
- **Trigger**: User adds "Lua Action" node in workflow or attaches script to model hook
|
||||
- **Progression**: Open Lua editor → Write function → Access data context → Call utility APIs → Test with sample data → Save → Execute on events
|
||||
- **Success criteria**: Syntax highlighting; autocomplete for context API; sandboxed execution; timeout protection; error messages clear; can transform JSON data
|
||||
|
||||
## Edge Case Handling
|
||||
- **Invalid Credentials**: Show clear error message with retry; rate limit login attempts after 5 failures
|
||||
- **Malicious Code in Editor**: Sandbox prevents DOM access, network requests, and infinite loops; timeout after 5 seconds
|
||||
- **Circular Component Nesting**: Detect and prevent infinite nesting; show warning when depth exceeds 10 levels
|
||||
- **Large Component Trees**: Virtualize component tree view; lazy load property panels; debounce updates
|
||||
- **Unsupported Component Props**: Gracefully ignore invalid props; show warnings in dev panel; provide prop documentation
|
||||
- **Lost Session**: Auto-save builder state every 10 seconds; restore on session loss; show "reconnecting" state
|
||||
- **Monaco Loading Failure**: Fallback to basic textarea editor; show notification about reduced functionality
|
||||
- **Invalid User Credentials**: Show clear error message; rate limit after 5 attempts; support password reset flow
|
||||
- **Unauthorized Access Attempts**: Redirect to appropriate level; log security events; show "access denied" message
|
||||
- **Circular Schema Relations**: Detect and prevent infinite loops in model relationships; warn user
|
||||
- **Invalid Lua Scripts**: Catch syntax errors; timeout after 3 seconds; sandbox prevents dangerous operations
|
||||
- **Malformed JSON Schemas**: Validate before save; highlight errors with line numbers; provide fix suggestions
|
||||
- **Workflow Infinite Loops**: Detect cycles; limit execution steps to 1000; show execution trace
|
||||
- **Large Comment Threads**: Paginate comments; lazy load older entries; virtualize long lists
|
||||
- **Schema Migration Conflicts**: Detect breaking changes; show migration preview; allow rollback
|
||||
- **Lost Sessions Across Levels**: Auto-save state; restore context; show reconnection indicator
|
||||
- **Monaco/Lua Library Load Failure**: Fallback to basic textarea; show degraded mode warning
|
||||
|
||||
## Design Direction
|
||||
The design should evoke creativity and power - a professional design tool that feels both approachable and capable. Think Figma meets VS Code: clean, modern, with clear visual hierarchy and purposeful spacing. The canvas should feel like a creative workspace, not a cluttered IDE.
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>GUI Builder - Visual Component Editor</title>
|
||||
<title>MetaBuilder - 4-Level Application Platform</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@500;600;700&family=IBM+Plex+Sans:wght@400;500&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||
|
||||
153
src/App.tsx
153
src/App.tsx
@@ -1,49 +1,140 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
import { Toaster } from '@/components/ui/sonner'
|
||||
import { Login } from '@/components/Login'
|
||||
import { Builder } from '@/components/Builder'
|
||||
import type { Session } from '@/lib/builder-types'
|
||||
import { UnifiedLogin } from '@/components/UnifiedLogin'
|
||||
import { Level1 } from '@/components/Level1'
|
||||
import { Level2 } from '@/components/Level2'
|
||||
import { Level3 } from '@/components/Level3'
|
||||
import { Level4 } from '@/components/Level4'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
const DEFAULT_USERNAME = 'admin'
|
||||
const DEFAULT_PASSWORD = 'admin'
|
||||
import { DEFAULT_USERS, DEFAULT_CREDENTIALS, canAccessLevel } from '@/lib/auth'
|
||||
import type { User, AppLevel } from '@/lib/level-types'
|
||||
|
||||
function App() {
|
||||
const [session, setSession] = useKV<Session>('builder_session', {
|
||||
authenticated: false,
|
||||
username: '',
|
||||
timestamp: 0,
|
||||
})
|
||||
const [users, setUsers] = useKV<User[]>('app_users', DEFAULT_USERS)
|
||||
const [currentUser, setCurrentUser] = useKV<User | null>('current_user', null)
|
||||
const [currentLevel, setCurrentLevel] = useState<AppLevel>(1)
|
||||
|
||||
if (!session) return null
|
||||
if (!users) return null
|
||||
|
||||
const handleLogin = (username: string, password: string) => {
|
||||
if (username === DEFAULT_USERNAME && password === DEFAULT_PASSWORD) {
|
||||
setSession({
|
||||
authenticated: true,
|
||||
username,
|
||||
timestamp: Date.now(),
|
||||
})
|
||||
toast.success('Welcome to GUI Builder!')
|
||||
} else {
|
||||
toast.error('Invalid credentials. Use admin/admin')
|
||||
const handleLogin = (credentials: { username: string; password: string }) => {
|
||||
const { username, password } = credentials
|
||||
|
||||
const storedPassword = DEFAULT_CREDENTIALS[username]
|
||||
if (!storedPassword || storedPassword !== password) {
|
||||
toast.error('Invalid credentials')
|
||||
return
|
||||
}
|
||||
|
||||
const user = users.find(u => u.username === username)
|
||||
if (!user) {
|
||||
toast.error('User not found')
|
||||
return
|
||||
}
|
||||
|
||||
setCurrentUser(user)
|
||||
|
||||
if (user.role === 'god') {
|
||||
setCurrentLevel(4)
|
||||
} else if (user.role === 'admin') {
|
||||
setCurrentLevel(3)
|
||||
} else {
|
||||
setCurrentLevel(2)
|
||||
}
|
||||
|
||||
toast.success(`Welcome, ${user.username}!`)
|
||||
}
|
||||
|
||||
const handleRegister = (username: string, email: string, password: string) => {
|
||||
if (users.some(u => u.username === username)) {
|
||||
toast.error('Username already exists')
|
||||
return
|
||||
}
|
||||
|
||||
if (users.some(u => u.email === email)) {
|
||||
toast.error('Email already registered')
|
||||
return
|
||||
}
|
||||
|
||||
const newUser: User = {
|
||||
id: `user_${Date.now()}`,
|
||||
username,
|
||||
email,
|
||||
role: 'user',
|
||||
createdAt: Date.now(),
|
||||
}
|
||||
|
||||
setUsers((current) => [...(current || []), newUser])
|
||||
setCurrentUser(newUser)
|
||||
setCurrentLevel(2)
|
||||
toast.success('Account created successfully!')
|
||||
}
|
||||
|
||||
const handleLogout = () => {
|
||||
setSession({
|
||||
authenticated: false,
|
||||
username: '',
|
||||
timestamp: 0,
|
||||
})
|
||||
setCurrentUser(null)
|
||||
setCurrentLevel(1)
|
||||
toast.info('Logged out successfully')
|
||||
}
|
||||
|
||||
if (!session.authenticated) {
|
||||
const handleNavigate = (level: AppLevel) => {
|
||||
if (currentUser && !canAccessLevel(currentUser.role, level)) {
|
||||
toast.error('Access denied. Insufficient permissions.')
|
||||
return
|
||||
}
|
||||
|
||||
if (level > 1 && !currentUser) {
|
||||
toast.info('Please sign in to access this area')
|
||||
return
|
||||
}
|
||||
|
||||
setCurrentLevel(level)
|
||||
}
|
||||
|
||||
const handlePreview = (level: AppLevel) => {
|
||||
setCurrentLevel(level)
|
||||
toast.info(`Previewing Level ${level}`)
|
||||
}
|
||||
|
||||
if (!currentUser) {
|
||||
return (
|
||||
<>
|
||||
<Login onLogin={handleLogin} />
|
||||
<Level1 onNavigate={handleNavigate} />
|
||||
<Toaster />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (currentLevel === 1) {
|
||||
return (
|
||||
<>
|
||||
<Level1 onNavigate={handleNavigate} />
|
||||
<Toaster />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (currentLevel === 2) {
|
||||
return (
|
||||
<>
|
||||
<Level2 user={currentUser} onLogout={handleLogout} onNavigate={handleNavigate} />
|
||||
<Toaster />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (currentLevel === 3 && canAccessLevel(currentUser.role, 3)) {
|
||||
return (
|
||||
<>
|
||||
<Level3 user={currentUser} onLogout={handleLogout} onNavigate={handleNavigate} />
|
||||
<Toaster />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
if (currentLevel === 4 && canAccessLevel(currentUser.role, 4)) {
|
||||
return (
|
||||
<>
|
||||
<Level4 user={currentUser} onLogout={handleLogout} onNavigate={handleNavigate} onPreview={handlePreview} />
|
||||
<Toaster />
|
||||
</>
|
||||
)
|
||||
@@ -51,7 +142,7 @@ function App() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Builder onLogout={handleLogout} />
|
||||
<UnifiedLogin onLogin={handleLogin} onRegister={handleRegister} />
|
||||
<Toaster />
|
||||
</>
|
||||
)
|
||||
|
||||
220
src/components/Level1.tsx
Normal file
220
src/components/Level1.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { List, X, User, ShieldCheck } from '@phosphor-icons/react'
|
||||
|
||||
interface Level1Props {
|
||||
onNavigate: (level: number) => void
|
||||
}
|
||||
|
||||
export function Level1({ onNavigate }: Level1Props) {
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-primary/5 via-background to-accent/5">
|
||||
<nav className="border-b border-border bg-card/50 backdrop-blur-sm sticky top-0 z-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-primary to-accent" />
|
||||
<span className="font-bold text-xl">MetaBuilder</span>
|
||||
</div>
|
||||
|
||||
<div className="hidden md:flex items-center gap-4">
|
||||
<a href="#features" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
|
||||
Features
|
||||
</a>
|
||||
<a href="#about" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
|
||||
About
|
||||
</a>
|
||||
<a href="#contact" className="text-sm text-muted-foreground hover:text-foreground transition-colors">
|
||||
Contact
|
||||
</a>
|
||||
<Button variant="outline" size="sm" onClick={() => onNavigate(2)}>
|
||||
<User className="mr-2" size={16} />
|
||||
Sign In
|
||||
</Button>
|
||||
<Button size="sm" onClick={() => onNavigate(4)}>
|
||||
<ShieldCheck className="mr-2" size={16} />
|
||||
Admin
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="md:hidden p-2 rounded-lg hover:bg-accent transition-colors"
|
||||
onClick={() => setMenuOpen(!menuOpen)}
|
||||
>
|
||||
{menuOpen ? <X size={24} /> : <List size={24} />}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{menuOpen && (
|
||||
<div className="md:hidden border-t border-border bg-card">
|
||||
<div className="px-4 py-3 space-y-3">
|
||||
<a href="#features" className="block text-sm text-muted-foreground hover:text-foreground">
|
||||
Features
|
||||
</a>
|
||||
<a href="#about" className="block text-sm text-muted-foreground hover:text-foreground">
|
||||
About
|
||||
</a>
|
||||
<a href="#contact" className="block text-sm text-muted-foreground hover:text-foreground">
|
||||
Contact
|
||||
</a>
|
||||
<Button variant="outline" size="sm" className="w-full" onClick={() => onNavigate(2)}>
|
||||
<User className="mr-2" size={16} />
|
||||
Sign In
|
||||
</Button>
|
||||
<Button size="sm" className="w-full" onClick={() => onNavigate(4)}>
|
||||
<ShieldCheck className="mr-2" size={16} />
|
||||
Admin
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
<section className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
|
||||
<div className="text-center space-y-6">
|
||||
<h1 className="text-5xl md:text-6xl font-bold bg-gradient-to-r from-primary via-accent to-primary bg-clip-text text-transparent">
|
||||
Build Anything, Visually
|
||||
</h1>
|
||||
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||
A 4-level meta-architecture for creating entire applications through visual workflows,
|
||||
schema editors, and embedded scripting. No code required.
|
||||
</p>
|
||||
<div className="flex gap-4 justify-center flex-wrap">
|
||||
<Button size="lg" onClick={() => onNavigate(2)}>
|
||||
Get Started
|
||||
</Button>
|
||||
<Button size="lg" variant="outline">
|
||||
Watch Demo
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="features" className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
|
||||
<h2 className="text-3xl font-bold text-center mb-12">Four Levels of Power</h2>
|
||||
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6">
|
||||
<Card className="border-2 hover:border-primary transition-colors">
|
||||
<CardHeader>
|
||||
<div className="w-12 h-12 rounded-lg bg-gradient-to-br from-blue-500 to-blue-600 flex items-center justify-center text-white font-bold text-xl mb-4">
|
||||
1
|
||||
</div>
|
||||
<CardTitle>Public Website</CardTitle>
|
||||
<CardDescription>
|
||||
Beautiful landing pages with responsive design and hamburger menus
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
Perfect for marketing sites, portfolios, and public-facing content
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-2 hover:border-primary transition-colors">
|
||||
<CardHeader>
|
||||
<div className="w-12 h-12 rounded-lg bg-gradient-to-br from-green-500 to-green-600 flex items-center justify-center text-white font-bold text-xl mb-4">
|
||||
2
|
||||
</div>
|
||||
<CardTitle>User Area</CardTitle>
|
||||
<CardDescription>
|
||||
Authenticated dashboards with profiles and comment systems
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
User registration, profile management, and interactive features
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-2 hover:border-primary transition-colors">
|
||||
<CardHeader>
|
||||
<div className="w-12 h-12 rounded-lg bg-gradient-to-br from-orange-500 to-orange-600 flex items-center justify-center text-white font-bold text-xl mb-4">
|
||||
3
|
||||
</div>
|
||||
<CardTitle>Admin Panel</CardTitle>
|
||||
<CardDescription>
|
||||
Django-style data management with CRUD operations and filters
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
Complete control over data models with list views and inline editing
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="border-2 hover:border-primary transition-colors">
|
||||
<CardHeader>
|
||||
<div className="w-12 h-12 rounded-lg bg-gradient-to-br from-purple-500 to-purple-600 flex items-center justify-center text-white font-bold text-xl mb-4">
|
||||
4
|
||||
</div>
|
||||
<CardTitle>God-Tier Builder</CardTitle>
|
||||
<CardDescription>
|
||||
Meta-builder with workflows, schemas, and Lua scripting
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm text-muted-foreground">
|
||||
Design and generate entire applications procedurally
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="about" className="bg-muted/30 py-20">
|
||||
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 text-center space-y-6">
|
||||
<h2 className="text-3xl font-bold">About MetaBuilder</h2>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
MetaBuilder is a revolutionary platform that lets you build entire application stacks
|
||||
through visual interfaces. From public websites to complex admin panels, everything
|
||||
is generated from declarative configurations, workflows, and embedded scripts.
|
||||
</p>
|
||||
<p className="text-lg text-muted-foreground">
|
||||
Whether you're a designer who wants to create without code, or a developer who wants
|
||||
to work at a higher level of abstraction, MetaBuilder adapts to your needs.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section id="contact" className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-20">
|
||||
<Card>
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-2xl">Get in Touch</CardTitle>
|
||||
<CardDescription>Have questions? We'd love to hear from you.</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
className="w-full px-3 py-2 border border-input rounded-md bg-background"
|
||||
placeholder="Your name"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
className="w-full px-3 py-2 border border-input rounded-md bg-background"
|
||||
placeholder="your@email.com"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">Message</label>
|
||||
<textarea
|
||||
className="w-full px-3 py-2 border border-input rounded-md bg-background"
|
||||
rows={4}
|
||||
placeholder="Your message..."
|
||||
/>
|
||||
</div>
|
||||
<Button className="w-full">Send Message</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</section>
|
||||
|
||||
<footer className="border-t border-border bg-muted/30 py-8 mt-20">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center text-sm text-muted-foreground">
|
||||
<p>© 2024 MetaBuilder. Built with the power of visual workflows and declarative schemas.</p>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
288
src/components/Level2.tsx
Normal file
288
src/components/Level2.tsx
Normal file
@@ -0,0 +1,288 @@
|
||||
import { useState } from 'react'
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
|
||||
import { User, ChatCircle, SignOut, House, Trash } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
import type { User as UserType, Comment } from '@/lib/level-types'
|
||||
|
||||
interface Level2Props {
|
||||
user: UserType
|
||||
onLogout: () => void
|
||||
onNavigate: (level: number) => void
|
||||
}
|
||||
|
||||
export function Level2({ user, onLogout, onNavigate }: Level2Props) {
|
||||
const [users, setUsers] = useKV<UserType[]>('app_users', [])
|
||||
const [comments, setComments] = useKV<Comment[]>('app_comments', [])
|
||||
const [newComment, setNewComment] = useState('')
|
||||
const [editingProfile, setEditingProfile] = useState(false)
|
||||
const [profileForm, setProfileForm] = useState({
|
||||
bio: user.bio || '',
|
||||
email: user.email,
|
||||
})
|
||||
|
||||
const currentUser = users?.find(u => u.id === user.id) || user
|
||||
|
||||
const handleProfileSave = () => {
|
||||
setUsers((current) =>
|
||||
current?.map(u =>
|
||||
u.id === user.id
|
||||
? { ...u, bio: profileForm.bio, email: profileForm.email }
|
||||
: u
|
||||
) || []
|
||||
)
|
||||
setEditingProfile(false)
|
||||
toast.success('Profile updated successfully')
|
||||
}
|
||||
|
||||
const handlePostComment = () => {
|
||||
if (!newComment.trim()) {
|
||||
toast.error('Comment cannot be empty')
|
||||
return
|
||||
}
|
||||
|
||||
const comment: Comment = {
|
||||
id: `comment_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
|
||||
userId: user.id,
|
||||
content: newComment,
|
||||
createdAt: Date.now(),
|
||||
}
|
||||
|
||||
setComments((current) => [...(current || []), comment])
|
||||
setNewComment('')
|
||||
toast.success('Comment posted')
|
||||
}
|
||||
|
||||
const handleDeleteComment = (commentId: string) => {
|
||||
setComments((current) => current?.filter(c => c.id !== commentId) || [])
|
||||
toast.success('Comment deleted')
|
||||
}
|
||||
|
||||
const userComments = comments?.filter(c => c.userId === user.id) || []
|
||||
const allComments = comments || []
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<nav className="border-b border-border bg-card sticky top-0 z-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-primary to-accent" />
|
||||
<span className="font-bold text-xl">MetaBuilder</span>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={() => onNavigate(1)}>
|
||||
<House className="mr-2" size={16} />
|
||||
Home
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-muted-foreground hidden sm:inline">
|
||||
{currentUser.username}
|
||||
</span>
|
||||
<Avatar className="w-8 h-8">
|
||||
<AvatarFallback>{currentUser.username[0].toUpperCase()}</AvatarFallback>
|
||||
</Avatar>
|
||||
<Button variant="ghost" size="sm" onClick={onLogout}>
|
||||
<SignOut size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<h1 className="text-3xl font-bold mb-8">User Dashboard</h1>
|
||||
|
||||
<Tabs defaultValue="profile" className="space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-2 max-w-md">
|
||||
<TabsTrigger value="profile">
|
||||
<User className="mr-2" size={16} />
|
||||
Profile
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="comments">
|
||||
<ChatCircle className="mr-2" size={16} />
|
||||
Comments
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="profile" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Profile Information</CardTitle>
|
||||
<CardDescription>Manage your account details</CardDescription>
|
||||
</div>
|
||||
{!editingProfile ? (
|
||||
<Button onClick={() => setEditingProfile(true)}>
|
||||
Edit Profile
|
||||
</Button>
|
||||
) : (
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={() => setEditingProfile(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleProfileSave}>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar className="w-20 h-20">
|
||||
<AvatarFallback className="text-2xl">
|
||||
{currentUser.username[0].toUpperCase()}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg">{currentUser.username}</h3>
|
||||
<p className="text-sm text-muted-foreground capitalize">{currentUser.role} Account</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Username</Label>
|
||||
<Input value={currentUser.username} disabled className="mt-2" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Email</Label>
|
||||
<Input
|
||||
type="email"
|
||||
value={editingProfile ? profileForm.email : currentUser.email}
|
||||
onChange={(e) => setProfileForm({ ...profileForm, email: e.target.value })}
|
||||
disabled={!editingProfile}
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Bio</Label>
|
||||
<Textarea
|
||||
value={editingProfile ? profileForm.bio : (currentUser.bio || '')}
|
||||
onChange={(e) => setProfileForm({ ...profileForm, bio: e.target.value })}
|
||||
disabled={!editingProfile}
|
||||
className="mt-2"
|
||||
rows={4}
|
||||
placeholder="Tell us about yourself..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Account Created</Label>
|
||||
<Input
|
||||
value={new Date(currentUser.createdAt).toLocaleDateString()}
|
||||
disabled
|
||||
className="mt-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="comments" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Post a Comment</CardTitle>
|
||||
<CardDescription>Share your thoughts with the community</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<Textarea
|
||||
value={newComment}
|
||||
onChange={(e) => setNewComment(e.target.value)}
|
||||
placeholder="Write your comment here..."
|
||||
rows={4}
|
||||
/>
|
||||
<Button onClick={handlePostComment}>Post Comment</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Your Comments ({userComments.length})</CardTitle>
|
||||
<CardDescription>Comments you've posted</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{userComments.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-8">
|
||||
You haven't posted any comments yet
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{userComments.map((comment) => (
|
||||
<div key={comment.id} className="border border-border rounded-lg p-4 space-y-2">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<p className="text-sm flex-1">{comment.content}</p>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteComment(comment.id)}
|
||||
>
|
||||
<Trash size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{new Date(comment.createdAt).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>All Comments ({allComments.length})</CardTitle>
|
||||
<CardDescription>Community discussion</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{allComments.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-8">
|
||||
No comments yet. Be the first to post!
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{allComments.map((comment) => {
|
||||
const commentUser = users?.find(u => u.id === comment.userId)
|
||||
return (
|
||||
<div key={comment.id} className="border border-border rounded-lg p-4 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="w-6 h-6">
|
||||
<AvatarFallback className="text-xs">
|
||||
{commentUser?.username[0].toUpperCase() || '?'}
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="text-sm font-medium">
|
||||
{commentUser?.username || 'Unknown User'}
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
· {new Date(comment.createdAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm">{comment.content}</p>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
333
src/components/Level3.tsx
Normal file
333
src/components/Level3.tsx
Normal file
@@ -0,0 +1,333 @@
|
||||
import { useState } from 'react'
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { SignOut, MagnifyingGlass, Plus, PencilSimple, Trash, Users, ChatCircle, House } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
import type { User as UserType, Comment } from '@/lib/level-types'
|
||||
import type { ModelSchema } from '@/lib/schema-types'
|
||||
|
||||
interface Level3Props {
|
||||
user: UserType
|
||||
onLogout: () => void
|
||||
onNavigate: (level: number) => void
|
||||
}
|
||||
|
||||
export function Level3({ user, onLogout, onNavigate }: Level3Props) {
|
||||
const [users, setUsers] = useKV<UserType[]>('app_users', [])
|
||||
const [comments, setComments] = useKV<Comment[]>('app_comments', [])
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [selectedModel, setSelectedModel] = useState<'users' | 'comments'>('users')
|
||||
const [editingItem, setEditingItem] = useState<any>(null)
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
|
||||
const allUsers = users || []
|
||||
const allComments = comments || []
|
||||
|
||||
const filteredUsers = allUsers.filter(u =>
|
||||
u.username.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
u.email.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
|
||||
const filteredComments = allComments.filter(c =>
|
||||
c.content.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
)
|
||||
|
||||
const handleDeleteUser = (userId: string) => {
|
||||
if (userId === user.id) {
|
||||
toast.error("You cannot delete your own account")
|
||||
return
|
||||
}
|
||||
setUsers((current) => current?.filter(u => u.id !== userId) || [])
|
||||
toast.success('User deleted')
|
||||
}
|
||||
|
||||
const handleDeleteComment = (commentId: string) => {
|
||||
setComments((current) => current?.filter(c => c.id !== commentId) || [])
|
||||
toast.success('Comment deleted')
|
||||
}
|
||||
|
||||
const handleEditUser = (editUser: UserType) => {
|
||||
setEditingItem(editUser)
|
||||
setDialogOpen(true)
|
||||
}
|
||||
|
||||
const handleSaveUser = () => {
|
||||
if (!editingItem) return
|
||||
|
||||
setUsers((current) =>
|
||||
current?.map(u => u.id === editingItem.id ? editingItem : u) || []
|
||||
)
|
||||
setDialogOpen(false)
|
||||
setEditingItem(null)
|
||||
toast.success('User updated')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<nav className="border-b border-border bg-sidebar sticky top-0 z-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-orange-500 to-orange-600" />
|
||||
<span className="font-bold text-xl text-sidebar-foreground">Admin Panel</span>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={() => onNavigate(1)} className="text-sidebar-foreground">
|
||||
<House className="mr-2" size={16} />
|
||||
Home
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary">{user.username}</Badge>
|
||||
<Button variant="ghost" size="sm" onClick={onLogout} className="text-sidebar-foreground">
|
||||
<SignOut size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold mb-2">Data Management</h1>
|
||||
<p className="text-muted-foreground">Manage all application data and users</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-3 mb-8">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Users</CardTitle>
|
||||
<Users className="text-muted-foreground" size={20} />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{allUsers.length}</div>
|
||||
<p className="text-xs text-muted-foreground">Registered accounts</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Comments</CardTitle>
|
||||
<ChatCircle className="text-muted-foreground" size={20} />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{allComments.length}</div>
|
||||
<p className="text-xs text-muted-foreground">Posted by users</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Admins</CardTitle>
|
||||
<Users className="text-muted-foreground" size={20} />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{allUsers.filter(u => u.role === 'admin' || u.role === 'god').length}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">Admin & god users</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<CardTitle>Models</CardTitle>
|
||||
<CardDescription>Browse and manage data models</CardDescription>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<div className="relative">
|
||||
<MagnifyingGlass className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" size={16} />
|
||||
<Input
|
||||
placeholder="Search..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="pl-9 w-64"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs value={selectedModel} onValueChange={(v) => setSelectedModel(v as any)}>
|
||||
<TabsList className="grid w-full grid-cols-2 max-w-md">
|
||||
<TabsTrigger value="users">
|
||||
<Users className="mr-2" size={16} />
|
||||
Users ({allUsers.length})
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="comments">
|
||||
<ChatCircle className="mr-2" size={16} />
|
||||
Comments ({allComments.length})
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="users" className="mt-6">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Username</TableHead>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead>Role</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredUsers.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={5} className="text-center text-muted-foreground">
|
||||
No users found
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredUsers.map((u) => (
|
||||
<TableRow key={u.id}>
|
||||
<TableCell className="font-medium">{u.username}</TableCell>
|
||||
<TableCell>{u.email}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={u.role === 'god' ? 'default' : u.role === 'admin' ? 'secondary' : 'outline'}>
|
||||
{u.role}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{new Date(u.createdAt).toLocaleDateString()}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEditUser(u)}
|
||||
>
|
||||
<PencilSimple size={16} />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteUser(u.id)}
|
||||
disabled={u.id === user.id}
|
||||
>
|
||||
<Trash size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="comments" className="mt-6">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>User</TableHead>
|
||||
<TableHead>Content</TableHead>
|
||||
<TableHead>Created</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filteredComments.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-center text-muted-foreground">
|
||||
No comments found
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
filteredComments.map((c) => {
|
||||
const commentUser = allUsers.find(u => u.id === c.userId)
|
||||
return (
|
||||
<TableRow key={c.id}>
|
||||
<TableCell className="font-medium">
|
||||
{commentUser?.username || 'Unknown'}
|
||||
</TableCell>
|
||||
<TableCell className="max-w-md truncate">{c.content}</TableCell>
|
||||
<TableCell>{new Date(c.createdAt).toLocaleDateString()}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteComment(c.id)}
|
||||
>
|
||||
<Trash size={16} />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Edit User</DialogTitle>
|
||||
<DialogDescription>Update user information</DialogDescription>
|
||||
</DialogHeader>
|
||||
{editingItem && (
|
||||
<div className="space-y-4 pt-4">
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">Username</label>
|
||||
<Input
|
||||
value={editingItem.username}
|
||||
onChange={(e) => setEditingItem({ ...editingItem, username: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">Email</label>
|
||||
<Input
|
||||
type="email"
|
||||
value={editingItem.email}
|
||||
onChange={(e) => setEditingItem({ ...editingItem, email: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-sm font-medium mb-2 block">Bio</label>
|
||||
<Input
|
||||
value={editingItem.bio || ''}
|
||||
onChange={(e) => setEditingItem({ ...editingItem, bio: e.target.value })}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button variant="outline" onClick={() => setDialogOpen(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSaveUser}>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
201
src/components/Level4.tsx
Normal file
201
src/components/Level4.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
import { useState } from 'react'
|
||||
import { useKV } from '@github/spark/hooks'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { SignOut, Database, Lightning, Code, Eye, House, Download, Upload } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
import { SchemaEditorLevel4 } from './SchemaEditorLevel4'
|
||||
import { WorkflowEditor } from './WorkflowEditor'
|
||||
import { LuaEditor } from './LuaEditor'
|
||||
import type { User as UserType, AppConfiguration } from '@/lib/level-types'
|
||||
import type { ModelSchema } from '@/lib/schema-types'
|
||||
|
||||
interface Level4Props {
|
||||
user: UserType
|
||||
onLogout: () => void
|
||||
onNavigate: (level: number) => void
|
||||
onPreview: (level: number) => void
|
||||
}
|
||||
|
||||
export function Level4({ user, onLogout, onNavigate, onPreview }: Level4Props) {
|
||||
const [appConfig, setAppConfig] = useKV<AppConfiguration>('app_configuration', {
|
||||
id: 'app_001',
|
||||
name: 'MetaBuilder App',
|
||||
schemas: [],
|
||||
workflows: [],
|
||||
luaScripts: [],
|
||||
pages: [],
|
||||
theme: {
|
||||
colors: {},
|
||||
fonts: {},
|
||||
},
|
||||
})
|
||||
|
||||
if (!appConfig) return null
|
||||
|
||||
const handleExportConfig = () => {
|
||||
const dataStr = JSON.stringify(appConfig, null, 2)
|
||||
const dataBlob = new Blob([dataStr], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(dataBlob)
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.download = 'app-config.json'
|
||||
link.click()
|
||||
toast.success('Configuration exported')
|
||||
}
|
||||
|
||||
const handleImportConfig = () => {
|
||||
const input = document.createElement('input')
|
||||
input.type = 'file'
|
||||
input.accept = 'application/json'
|
||||
input.onchange = async (e) => {
|
||||
const file = (e.target as HTMLInputElement).files?.[0]
|
||||
if (!file) return
|
||||
|
||||
const text = await file.text()
|
||||
try {
|
||||
const config = JSON.parse(text)
|
||||
setAppConfig(config)
|
||||
toast.success('Configuration imported')
|
||||
} catch (error) {
|
||||
toast.error('Invalid configuration file')
|
||||
}
|
||||
}
|
||||
input.click()
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-canvas">
|
||||
<nav className="border-b border-sidebar-border bg-sidebar sticky top-0 z-50">
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center h-16">
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-lg bg-gradient-to-br from-purple-500 to-purple-600" />
|
||||
<span className="font-bold text-xl text-sidebar-foreground">God-Tier Builder</span>
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" onClick={() => onNavigate(1)} className="text-sidebar-foreground">
|
||||
<House className="mr-2" size={16} />
|
||||
Home
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="hidden sm:flex gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => onPreview(1)}>
|
||||
<Eye className="mr-2" size={16} />
|
||||
Preview L1
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => onPreview(2)}>
|
||||
<Eye className="mr-2" size={16} />
|
||||
Preview L2
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={() => onPreview(3)}>
|
||||
<Eye className="mr-2" size={16} />
|
||||
Preview L3
|
||||
</Button>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={handleExportConfig}>
|
||||
<Download size={16} />
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" onClick={handleImportConfig}>
|
||||
<Upload size={16} />
|
||||
</Button>
|
||||
<Badge variant="secondary">{user.username}</Badge>
|
||||
<Button variant="ghost" size="sm" onClick={onLogout} className="text-sidebar-foreground">
|
||||
<SignOut size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-3xl font-bold mb-2">Application Builder</h1>
|
||||
<p className="text-muted-foreground">
|
||||
Design your application declaratively. Define schemas, create workflows, and write Lua scripts.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="schemas" className="space-y-6">
|
||||
<TabsList className="grid w-full grid-cols-3 max-w-2xl">
|
||||
<TabsTrigger value="schemas">
|
||||
<Database className="mr-2" size={16} />
|
||||
Data Schemas
|
||||
<Badge variant="secondary" className="ml-2">{appConfig.schemas.length}</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="workflows">
|
||||
<Lightning className="mr-2" size={16} />
|
||||
Workflows
|
||||
<Badge variant="secondary" className="ml-2">{appConfig.workflows.length}</Badge>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="lua">
|
||||
<Code className="mr-2" size={16} />
|
||||
Lua Scripts
|
||||
<Badge variant="secondary" className="ml-2">{appConfig.luaScripts.length}</Badge>
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="schemas" className="space-y-6">
|
||||
<SchemaEditorLevel4
|
||||
schemas={appConfig.schemas}
|
||||
onSchemasChange={(schemas) =>
|
||||
setAppConfig((current) => ({ ...current!, schemas }))
|
||||
}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="workflows" className="space-y-6">
|
||||
<WorkflowEditor
|
||||
workflows={appConfig.workflows}
|
||||
onWorkflowsChange={(workflows) =>
|
||||
setAppConfig((current) => ({ ...current!, workflows }))
|
||||
}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="lua" className="space-y-6">
|
||||
<LuaEditor
|
||||
scripts={appConfig.luaScripts}
|
||||
onScriptsChange={(luaScripts) =>
|
||||
setAppConfig((current) => ({ ...current!, luaScripts }))
|
||||
}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
<div className="mt-8 p-6 bg-gradient-to-r from-primary/10 to-accent/10 rounded-lg border-2 border-dashed border-primary/30">
|
||||
<h3 className="font-semibold mb-2">Configuration Summary</h3>
|
||||
<div className="grid gap-2 text-sm">
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Data Models:</span>
|
||||
<span className="font-medium">{appConfig.schemas.length}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Total Fields:</span>
|
||||
<span className="font-medium">
|
||||
{appConfig.schemas.reduce((acc, s) => acc + s.fields.length, 0)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Workflows:</span>
|
||||
<span className="font-medium">{appConfig.workflows.length}</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Workflow Nodes:</span>
|
||||
<span className="font-medium">
|
||||
{appConfig.workflows.reduce((acc, w) => acc + w.nodes.length, 0)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-muted-foreground">Lua Scripts:</span>
|
||||
<span className="font-medium">{appConfig.luaScripts.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
266
src/components/LuaEditor.tsx
Normal file
266
src/components/LuaEditor.tsx
Normal file
@@ -0,0 +1,266 @@
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { Plus, Trash, Play } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
import type { LuaScript } from '@/lib/level-types'
|
||||
|
||||
interface LuaEditorProps {
|
||||
scripts: LuaScript[]
|
||||
onScriptsChange: (scripts: LuaScript[]) => void
|
||||
}
|
||||
|
||||
export function LuaEditor({ scripts, onScriptsChange }: LuaEditorProps) {
|
||||
const [selectedScript, setSelectedScript] = useState<string | null>(
|
||||
scripts.length > 0 ? scripts[0].id : null
|
||||
)
|
||||
const [testOutput, setTestOutput] = useState<string>('')
|
||||
|
||||
const currentScript = scripts.find(s => s.id === selectedScript)
|
||||
|
||||
const handleAddScript = () => {
|
||||
const newScript: LuaScript = {
|
||||
id: `lua_${Date.now()}`,
|
||||
name: 'New Script',
|
||||
code: '-- Lua script\nfunction execute(context)\n -- Your code here\n return {success = true}\nend',
|
||||
parameters: [],
|
||||
}
|
||||
onScriptsChange([...scripts, newScript])
|
||||
setSelectedScript(newScript.id)
|
||||
toast.success('Script created')
|
||||
}
|
||||
|
||||
const handleDeleteScript = (scriptId: string) => {
|
||||
onScriptsChange(scripts.filter(s => s.id !== scriptId))
|
||||
if (selectedScript === scriptId) {
|
||||
setSelectedScript(scripts.length > 1 ? scripts[0].id : null)
|
||||
}
|
||||
toast.success('Script deleted')
|
||||
}
|
||||
|
||||
const handleUpdateScript = (updates: Partial<LuaScript>) => {
|
||||
if (!currentScript) return
|
||||
|
||||
onScriptsChange(
|
||||
scripts.map(s => s.id === selectedScript ? { ...s, ...updates } : s)
|
||||
)
|
||||
}
|
||||
|
||||
const handleTestScript = () => {
|
||||
if (!currentScript) return
|
||||
|
||||
setTestOutput('Lua execution simulation:\n\nScript: ' + currentScript.name + '\n\nNote: Actual Lua execution would require a Lua interpreter library.\nFor now, this is a mock test showing your script would execute.\n\nYour code:\n' + currentScript.code)
|
||||
toast.info('Script test simulation complete')
|
||||
}
|
||||
|
||||
const handleAddParameter = () => {
|
||||
if (!currentScript) return
|
||||
|
||||
const newParam = { name: `param${currentScript.parameters.length + 1}`, type: 'string' }
|
||||
handleUpdateScript({
|
||||
parameters: [...currentScript.parameters, newParam],
|
||||
})
|
||||
}
|
||||
|
||||
const handleDeleteParameter = (index: number) => {
|
||||
if (!currentScript) return
|
||||
|
||||
handleUpdateScript({
|
||||
parameters: currentScript.parameters.filter((_, i) => i !== index),
|
||||
})
|
||||
}
|
||||
|
||||
const handleUpdateParameter = (index: number, updates: { name?: string; type?: string }) => {
|
||||
if (!currentScript) return
|
||||
|
||||
handleUpdateScript({
|
||||
parameters: currentScript.parameters.map((p, i) =>
|
||||
i === index ? { ...p, ...updates } : p
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid md:grid-cols-3 gap-6 h-full">
|
||||
<Card className="md:col-span-1">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg">Lua Scripts</CardTitle>
|
||||
<Button size="sm" onClick={handleAddScript}>
|
||||
<Plus size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
<CardDescription>Custom logic scripts</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{scripts.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-4">
|
||||
No scripts yet. Create one to start.
|
||||
</p>
|
||||
) : (
|
||||
scripts.map((script) => (
|
||||
<div
|
||||
key={script.id}
|
||||
className={`flex items-center justify-between p-3 rounded-lg border cursor-pointer transition-colors ${
|
||||
selectedScript === script.id
|
||||
? 'bg-accent border-accent-foreground'
|
||||
: 'hover:bg-muted border-border'
|
||||
}`}
|
||||
onClick={() => setSelectedScript(script.id)}
|
||||
>
|
||||
<div>
|
||||
<div className="font-medium text-sm font-mono">{script.name}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{script.parameters.length} params
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDeleteScript(script.id)
|
||||
}}
|
||||
>
|
||||
<Trash size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="md:col-span-2">
|
||||
{!currentScript ? (
|
||||
<CardContent className="flex items-center justify-center h-full min-h-[400px]">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<p>Select or create a script to edit</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
) : (
|
||||
<>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Edit Script: {currentScript.name}</CardTitle>
|
||||
<CardDescription>Write custom Lua logic</CardDescription>
|
||||
</div>
|
||||
<Button onClick={handleTestScript}>
|
||||
<Play className="mr-2" size={16} />
|
||||
Test Script
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label>Script Name</Label>
|
||||
<Input
|
||||
value={currentScript.name}
|
||||
onChange={(e) => handleUpdateScript({ name: e.target.value })}
|
||||
placeholder="validate_user"
|
||||
className="font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Return Type</Label>
|
||||
<Input
|
||||
value={currentScript.returnType || ''}
|
||||
onChange={(e) => handleUpdateScript({ returnType: e.target.value })}
|
||||
placeholder="table, boolean, string..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Description</Label>
|
||||
<Input
|
||||
value={currentScript.description || ''}
|
||||
onChange={(e) => handleUpdateScript({ description: e.target.value })}
|
||||
placeholder="What this script does..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<Label>Parameters</Label>
|
||||
<Button size="sm" variant="outline" onClick={handleAddParameter}>
|
||||
<Plus className="mr-2" size={14} />
|
||||
Add Parameter
|
||||
</Button>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{currentScript.parameters.map((param, index) => (
|
||||
<div key={index} className="flex gap-2 items-center">
|
||||
<Input
|
||||
value={param.name}
|
||||
onChange={(e) => handleUpdateParameter(index, { name: e.target.value })}
|
||||
placeholder="paramName"
|
||||
className="flex-1 font-mono text-sm"
|
||||
/>
|
||||
<Input
|
||||
value={param.type}
|
||||
onChange={(e) => handleUpdateParameter(index, { type: e.target.value })}
|
||||
placeholder="string"
|
||||
className="w-32 text-sm"
|
||||
/>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteParameter(index)}
|
||||
>
|
||||
<Trash size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Lua Code</Label>
|
||||
<Textarea
|
||||
value={currentScript.code}
|
||||
onChange={(e) => handleUpdateScript({ code: e.target.value })}
|
||||
className="font-mono text-sm min-h-[300px]"
|
||||
placeholder="-- Lua script function execute(context) -- Your code here return {success = true} end"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Write Lua code. The context object provides access to data and utilities.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{testOutput && (
|
||||
<Card className="bg-muted/50">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-sm">Test Output</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre className="text-xs font-mono whitespace-pre-wrap">{testOutput}</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<div className="bg-muted/50 rounded-lg p-4 border border-dashed">
|
||||
<div className="space-y-2 text-xs text-muted-foreground">
|
||||
<p className="font-semibold text-foreground">Available in context:</p>
|
||||
<ul className="space-y-1 list-disc list-inside">
|
||||
<li><code className="font-mono">context.data</code> - Input data</li>
|
||||
<li><code className="font-mono">context.user</code> - Current user info</li>
|
||||
<li><code className="font-mono">context.kv</code> - Key-value storage</li>
|
||||
<li><code className="font-mono">context.log(msg)</code> - Logging function</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
330
src/components/SchemaEditorLevel4.tsx
Normal file
330
src/components/SchemaEditorLevel4.tsx
Normal file
@@ -0,0 +1,330 @@
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Plus, Trash } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
import type { ModelSchema, FieldSchema, FieldType } from '@/lib/schema-types'
|
||||
|
||||
interface SchemaEditorLevel4Props {
|
||||
schemas: ModelSchema[]
|
||||
onSchemasChange: (schemas: ModelSchema[]) => void
|
||||
}
|
||||
|
||||
export function SchemaEditorLevel4({ schemas, onSchemasChange }: SchemaEditorLevel4Props) {
|
||||
const [selectedModel, setSelectedModel] = useState<string | null>(
|
||||
schemas.length > 0 ? schemas[0].name : null
|
||||
)
|
||||
|
||||
const currentModel = schemas.find(s => s.name === selectedModel)
|
||||
|
||||
const handleAddModel = () => {
|
||||
const newModel: ModelSchema = {
|
||||
name: `Model_${Date.now()}`,
|
||||
label: 'New Model',
|
||||
fields: [],
|
||||
}
|
||||
onSchemasChange([...schemas, newModel])
|
||||
setSelectedModel(newModel.name)
|
||||
toast.success('Model created')
|
||||
}
|
||||
|
||||
const handleDeleteModel = (modelName: string) => {
|
||||
onSchemasChange(schemas.filter(s => s.name !== modelName))
|
||||
if (selectedModel === modelName) {
|
||||
setSelectedModel(schemas.length > 1 ? schemas[0].name : null)
|
||||
}
|
||||
toast.success('Model deleted')
|
||||
}
|
||||
|
||||
const handleUpdateModel = (updates: Partial<ModelSchema>) => {
|
||||
if (!currentModel) return
|
||||
|
||||
onSchemasChange(
|
||||
schemas.map(s => s.name === selectedModel ? { ...s, ...updates } : s)
|
||||
)
|
||||
}
|
||||
|
||||
const handleAddField = () => {
|
||||
if (!currentModel) return
|
||||
|
||||
const newField: FieldSchema = {
|
||||
name: `field_${Date.now()}`,
|
||||
type: 'string',
|
||||
label: 'New Field',
|
||||
required: false,
|
||||
editable: true,
|
||||
}
|
||||
|
||||
handleUpdateModel({
|
||||
fields: [...currentModel.fields, newField],
|
||||
})
|
||||
toast.success('Field added')
|
||||
}
|
||||
|
||||
const handleDeleteField = (fieldName: string) => {
|
||||
if (!currentModel) return
|
||||
|
||||
handleUpdateModel({
|
||||
fields: currentModel.fields.filter(f => f.name !== fieldName),
|
||||
})
|
||||
toast.success('Field deleted')
|
||||
}
|
||||
|
||||
const handleUpdateField = (fieldName: string, updates: Partial<FieldSchema>) => {
|
||||
if (!currentModel) return
|
||||
|
||||
handleUpdateModel({
|
||||
fields: currentModel.fields.map(f =>
|
||||
f.name === fieldName ? { ...f, ...updates } : f
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid md:grid-cols-3 gap-6 h-full">
|
||||
<Card className="md:col-span-1">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg">Models</CardTitle>
|
||||
<Button size="sm" onClick={handleAddModel}>
|
||||
<Plus size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
<CardDescription>Data model definitions</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{schemas.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-4">
|
||||
No models yet. Create one to start.
|
||||
</p>
|
||||
) : (
|
||||
schemas.map((schema) => (
|
||||
<div
|
||||
key={schema.name}
|
||||
className={`flex items-center justify-between p-3 rounded-lg border cursor-pointer transition-colors ${
|
||||
selectedModel === schema.name
|
||||
? 'bg-accent border-accent-foreground'
|
||||
: 'hover:bg-muted border-border'
|
||||
}`}
|
||||
onClick={() => setSelectedModel(schema.name)}
|
||||
>
|
||||
<div>
|
||||
<div className="font-medium text-sm">{schema.label || schema.name}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{schema.fields.length} fields
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDeleteModel(schema.name)
|
||||
}}
|
||||
>
|
||||
<Trash size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="md:col-span-2">
|
||||
{!currentModel ? (
|
||||
<CardContent className="flex items-center justify-center h-full min-h-[400px]">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<p>Select or create a model to edit</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
) : (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle>Edit Model: {currentModel.label}</CardTitle>
|
||||
<CardDescription>Configure model properties and fields</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label>Model Name (ID)</Label>
|
||||
<Input
|
||||
value={currentModel.name}
|
||||
onChange={(e) => handleUpdateModel({ name: e.target.value })}
|
||||
placeholder="user_model"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Display Label</Label>
|
||||
<Input
|
||||
value={currentModel.label || ''}
|
||||
onChange={(e) => handleUpdateModel({ label: e.target.value })}
|
||||
placeholder="User"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Plural Label</Label>
|
||||
<Input
|
||||
value={currentModel.labelPlural || ''}
|
||||
onChange={(e) => handleUpdateModel({ labelPlural: e.target.value })}
|
||||
placeholder="Users"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Icon Name</Label>
|
||||
<Input
|
||||
value={currentModel.icon || ''}
|
||||
onChange={(e) => handleUpdateModel({ icon: e.target.value })}
|
||||
placeholder="Users"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Label className="text-base">Fields</Label>
|
||||
<Button size="sm" onClick={handleAddField}>
|
||||
<Plus className="mr-2" size={16} />
|
||||
Add Field
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{currentModel.fields.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-8 border border-dashed rounded-lg">
|
||||
No fields yet. Add a field to start.
|
||||
</p>
|
||||
) : (
|
||||
currentModel.fields.map((field) => (
|
||||
<Card key={field.name} className="border-2">
|
||||
<CardContent className="pt-6 space-y-4">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="grid gap-4 md:grid-cols-2 flex-1">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">Field Name</Label>
|
||||
<Input
|
||||
value={field.name}
|
||||
onChange={(e) =>
|
||||
handleUpdateField(field.name, { name: e.target.value })
|
||||
}
|
||||
placeholder="email"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">Label</Label>
|
||||
<Input
|
||||
value={field.label || ''}
|
||||
onChange={(e) =>
|
||||
handleUpdateField(field.name, { label: e.target.value })
|
||||
}
|
||||
placeholder="Email Address"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">Type</Label>
|
||||
<Select
|
||||
value={field.type}
|
||||
onValueChange={(value) =>
|
||||
handleUpdateField(field.name, { type: value as FieldType })
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="string">String</SelectItem>
|
||||
<SelectItem value="text">Text</SelectItem>
|
||||
<SelectItem value="number">Number</SelectItem>
|
||||
<SelectItem value="boolean">Boolean</SelectItem>
|
||||
<SelectItem value="date">Date</SelectItem>
|
||||
<SelectItem value="datetime">DateTime</SelectItem>
|
||||
<SelectItem value="email">Email</SelectItem>
|
||||
<SelectItem value="url">URL</SelectItem>
|
||||
<SelectItem value="select">Select</SelectItem>
|
||||
<SelectItem value="relation">Relation</SelectItem>
|
||||
<SelectItem value="json">JSON</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">Default Value</Label>
|
||||
<Input
|
||||
value={field.default || ''}
|
||||
onChange={(e) =>
|
||||
handleUpdateField(field.name, { default: e.target.value })
|
||||
}
|
||||
placeholder="Default"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteField(field.name)}
|
||||
>
|
||||
<Trash size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-6">
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={field.required || false}
|
||||
onCheckedChange={(checked) =>
|
||||
handleUpdateField(field.name, { required: checked })
|
||||
}
|
||||
/>
|
||||
<Label className="text-xs">Required</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={field.unique || false}
|
||||
onCheckedChange={(checked) =>
|
||||
handleUpdateField(field.name, { unique: checked })
|
||||
}
|
||||
/>
|
||||
<Label className="text-xs">Unique</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={field.editable !== false}
|
||||
onCheckedChange={(checked) =>
|
||||
handleUpdateField(field.name, { editable: checked })
|
||||
}
|
||||
/>
|
||||
<Label className="text-xs">Editable</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
checked={field.searchable || false}
|
||||
onCheckedChange={(checked) =>
|
||||
handleUpdateField(field.name, { searchable: checked })
|
||||
}
|
||||
/>
|
||||
<Label className="text-xs">Searchable</Label>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
155
src/components/UnifiedLogin.tsx
Normal file
155
src/components/UnifiedLogin.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||
import { SignIn, UserPlus } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
interface UnifiedLoginProps {
|
||||
onLogin: (credentials: { username: string; password: string }) => void
|
||||
onRegister: (username: string, email: string, password: string) => void
|
||||
}
|
||||
|
||||
export function UnifiedLogin({ onLogin, onRegister }: UnifiedLoginProps) {
|
||||
const [loginForm, setLoginForm] = useState({ username: '', password: '' })
|
||||
const [registerForm, setRegisterForm] = useState({ username: '', email: '', password: '', confirmPassword: '' })
|
||||
|
||||
const handleLogin = () => {
|
||||
if (!loginForm.username || !loginForm.password) {
|
||||
toast.error('Please fill in all fields')
|
||||
return
|
||||
}
|
||||
|
||||
onLogin(loginForm)
|
||||
}
|
||||
|
||||
const handleRegister = () => {
|
||||
if (!registerForm.username || !registerForm.email || !registerForm.password) {
|
||||
toast.error('Please fill in all fields')
|
||||
return
|
||||
}
|
||||
|
||||
if (registerForm.password !== registerForm.confirmPassword) {
|
||||
toast.error('Passwords do not match')
|
||||
return
|
||||
}
|
||||
|
||||
if (registerForm.password.length < 6) {
|
||||
toast.error('Password must be at least 6 characters')
|
||||
return
|
||||
}
|
||||
|
||||
onRegister(registerForm.username, registerForm.email, registerForm.password)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-primary/5 via-background to-accent/5 flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="text-center">
|
||||
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-primary to-accent flex items-center justify-center">
|
||||
<div className="w-8 h-8 rounded-lg bg-white" />
|
||||
</div>
|
||||
<CardTitle className="text-2xl">MetaBuilder</CardTitle>
|
||||
<CardDescription>Sign in to access the platform</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Tabs defaultValue="login">
|
||||
<TabsList className="grid w-full grid-cols-2">
|
||||
<TabsTrigger value="login">
|
||||
<SignIn className="mr-2" size={16} />
|
||||
Login
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="register">
|
||||
<UserPlus className="mr-2" size={16} />
|
||||
Register
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="login" className="space-y-4 mt-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="login-username">Username</Label>
|
||||
<Input
|
||||
id="login-username"
|
||||
value={loginForm.username}
|
||||
onChange={(e) => setLoginForm({ ...loginForm, username: e.target.value })}
|
||||
placeholder="Enter username"
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="login-password">Password</Label>
|
||||
<Input
|
||||
id="login-password"
|
||||
type="password"
|
||||
value={loginForm.password}
|
||||
onChange={(e) => setLoginForm({ ...loginForm, password: e.target.value })}
|
||||
placeholder="Enter password"
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleLogin()}
|
||||
/>
|
||||
</div>
|
||||
<Button className="w-full" onClick={handleLogin}>
|
||||
<SignIn className="mr-2" size={16} />
|
||||
Sign In
|
||||
</Button>
|
||||
<div className="text-xs text-muted-foreground space-y-1 p-3 bg-muted rounded-lg">
|
||||
<p className="font-semibold">Test Credentials:</p>
|
||||
<p>• God: <code className="font-mono">god / god123</code></p>
|
||||
<p>• Admin: <code className="font-mono">admin / admin</code></p>
|
||||
<p>• User: <code className="font-mono">demo / demo</code></p>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="register" className="space-y-4 mt-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="register-username">Username</Label>
|
||||
<Input
|
||||
id="register-username"
|
||||
value={registerForm.username}
|
||||
onChange={(e) => setRegisterForm({ ...registerForm, username: e.target.value })}
|
||||
placeholder="Choose a username"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="register-email">Email</Label>
|
||||
<Input
|
||||
id="register-email"
|
||||
type="email"
|
||||
value={registerForm.email}
|
||||
onChange={(e) => setRegisterForm({ ...registerForm, email: e.target.value })}
|
||||
placeholder="your@email.com"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="register-password">Password</Label>
|
||||
<Input
|
||||
id="register-password"
|
||||
type="password"
|
||||
value={registerForm.password}
|
||||
onChange={(e) => setRegisterForm({ ...registerForm, password: e.target.value })}
|
||||
placeholder="Choose a password"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="register-confirm">Confirm Password</Label>
|
||||
<Input
|
||||
id="register-confirm"
|
||||
type="password"
|
||||
value={registerForm.confirmPassword}
|
||||
onChange={(e) => setRegisterForm({ ...registerForm, confirmPassword: e.target.value })}
|
||||
placeholder="Confirm your password"
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleRegister()}
|
||||
/>
|
||||
</div>
|
||||
<Button className="w-full" onClick={handleRegister}>
|
||||
<UserPlus className="mr-2" size={16} />
|
||||
Create Account
|
||||
</Button>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
328
src/components/WorkflowEditor.tsx
Normal file
328
src/components/WorkflowEditor.tsx
Normal file
@@ -0,0 +1,328 @@
|
||||
import { useState } from 'react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
import { Plus, Trash, Lightning, Code, GitBranch, ArrowRight } from '@phosphor-icons/react'
|
||||
import { toast } from 'sonner'
|
||||
import type { Workflow, WorkflowNode, WorkflowEdge } from '@/lib/level-types'
|
||||
|
||||
interface WorkflowEditorProps {
|
||||
workflows: Workflow[]
|
||||
onWorkflowsChange: (workflows: Workflow[]) => void
|
||||
}
|
||||
|
||||
export function WorkflowEditor({ workflows, onWorkflowsChange }: WorkflowEditorProps) {
|
||||
const [selectedWorkflow, setSelectedWorkflow] = useState<string | null>(
|
||||
workflows.length > 0 ? workflows[0].id : null
|
||||
)
|
||||
|
||||
const currentWorkflow = workflows.find(w => w.id === selectedWorkflow)
|
||||
|
||||
const handleAddWorkflow = () => {
|
||||
const newWorkflow: Workflow = {
|
||||
id: `workflow_${Date.now()}`,
|
||||
name: 'New Workflow',
|
||||
nodes: [],
|
||||
edges: [],
|
||||
enabled: true,
|
||||
}
|
||||
onWorkflowsChange([...workflows, newWorkflow])
|
||||
setSelectedWorkflow(newWorkflow.id)
|
||||
toast.success('Workflow created')
|
||||
}
|
||||
|
||||
const handleDeleteWorkflow = (workflowId: string) => {
|
||||
onWorkflowsChange(workflows.filter(w => w.id !== workflowId))
|
||||
if (selectedWorkflow === workflowId) {
|
||||
setSelectedWorkflow(workflows.length > 1 ? workflows[0].id : null)
|
||||
}
|
||||
toast.success('Workflow deleted')
|
||||
}
|
||||
|
||||
const handleUpdateWorkflow = (updates: Partial<Workflow>) => {
|
||||
if (!currentWorkflow) return
|
||||
|
||||
onWorkflowsChange(
|
||||
workflows.map(w => w.id === selectedWorkflow ? { ...w, ...updates } : w)
|
||||
)
|
||||
}
|
||||
|
||||
const handleAddNode = (type: WorkflowNode['type']) => {
|
||||
if (!currentWorkflow) return
|
||||
|
||||
const newNode: WorkflowNode = {
|
||||
id: `node_${Date.now()}`,
|
||||
type,
|
||||
label: `${type.charAt(0).toUpperCase() + type.slice(1)} Node`,
|
||||
config: {},
|
||||
position: { x: 100, y: currentWorkflow.nodes.length * 100 + 100 },
|
||||
}
|
||||
|
||||
handleUpdateWorkflow({
|
||||
nodes: [...currentWorkflow.nodes, newNode],
|
||||
})
|
||||
toast.success('Node added')
|
||||
}
|
||||
|
||||
const handleDeleteNode = (nodeId: string) => {
|
||||
if (!currentWorkflow) return
|
||||
|
||||
handleUpdateWorkflow({
|
||||
nodes: currentWorkflow.nodes.filter(n => n.id !== nodeId),
|
||||
edges: currentWorkflow.edges.filter(e => e.source !== nodeId && e.target !== nodeId),
|
||||
})
|
||||
toast.success('Node deleted')
|
||||
}
|
||||
|
||||
const handleUpdateNode = (nodeId: string, updates: Partial<WorkflowNode>) => {
|
||||
if (!currentWorkflow) return
|
||||
|
||||
handleUpdateWorkflow({
|
||||
nodes: currentWorkflow.nodes.map(n => n.id === nodeId ? { ...n, ...updates } : n),
|
||||
})
|
||||
}
|
||||
|
||||
const getNodeIcon = (type: WorkflowNode['type']) => {
|
||||
switch (type) {
|
||||
case 'trigger':
|
||||
return <Lightning size={16} />
|
||||
case 'action':
|
||||
return <ArrowRight size={16} />
|
||||
case 'condition':
|
||||
return <GitBranch size={16} />
|
||||
case 'lua':
|
||||
return <Code size={16} />
|
||||
case 'transform':
|
||||
return <ArrowRight size={16} />
|
||||
default:
|
||||
return <ArrowRight size={16} />
|
||||
}
|
||||
}
|
||||
|
||||
const getNodeColor = (type: WorkflowNode['type']) => {
|
||||
switch (type) {
|
||||
case 'trigger':
|
||||
return 'bg-green-500'
|
||||
case 'action':
|
||||
return 'bg-blue-500'
|
||||
case 'condition':
|
||||
return 'bg-yellow-500'
|
||||
case 'lua':
|
||||
return 'bg-purple-500'
|
||||
case 'transform':
|
||||
return 'bg-cyan-500'
|
||||
default:
|
||||
return 'bg-gray-500'
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid md:grid-cols-3 gap-6 h-full">
|
||||
<Card className="md:col-span-1">
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-lg">Workflows</CardTitle>
|
||||
<Button size="sm" onClick={handleAddWorkflow}>
|
||||
<Plus size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
<CardDescription>Automation workflows</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-2">
|
||||
{workflows.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-4">
|
||||
No workflows yet. Create one to start.
|
||||
</p>
|
||||
) : (
|
||||
workflows.map((workflow) => (
|
||||
<div
|
||||
key={workflow.id}
|
||||
className={`flex items-center justify-between p-3 rounded-lg border cursor-pointer transition-colors ${
|
||||
selectedWorkflow === workflow.id
|
||||
? 'bg-accent border-accent-foreground'
|
||||
: 'hover:bg-muted border-border'
|
||||
}`}
|
||||
onClick={() => setSelectedWorkflow(workflow.id)}
|
||||
>
|
||||
<div className="flex-1">
|
||||
<div className="font-medium text-sm">{workflow.name}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{workflow.nodes.length} nodes
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={workflow.enabled ? 'default' : 'secondary'} className="text-xs">
|
||||
{workflow.enabled ? 'On' : 'Off'}
|
||||
</Badge>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDeleteWorkflow(workflow.id)
|
||||
}}
|
||||
>
|
||||
<Trash size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card className="md:col-span-2">
|
||||
{!currentWorkflow ? (
|
||||
<CardContent className="flex items-center justify-center h-full min-h-[400px]">
|
||||
<div className="text-center text-muted-foreground">
|
||||
<p>Select or create a workflow to edit</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
) : (
|
||||
<>
|
||||
<CardHeader>
|
||||
<CardTitle>Edit Workflow: {currentWorkflow.name}</CardTitle>
|
||||
<CardDescription>Configure workflow nodes and connections</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label>Workflow Name</Label>
|
||||
<Input
|
||||
value={currentWorkflow.name}
|
||||
onChange={(e) => handleUpdateWorkflow({ name: e.target.value })}
|
||||
placeholder="My Workflow"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label>Description</Label>
|
||||
<Input
|
||||
value={currentWorkflow.description || ''}
|
||||
onChange={(e) => handleUpdateWorkflow({ description: e.target.value })}
|
||||
placeholder="What this workflow does..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<Label className="text-base">Nodes</Label>
|
||||
<div className="flex gap-2">
|
||||
<Button size="sm" variant="outline" onClick={() => handleAddNode('trigger')}>
|
||||
<Lightning className="mr-2" size={14} />
|
||||
Trigger
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleAddNode('action')}>
|
||||
<ArrowRight className="mr-2" size={14} />
|
||||
Action
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleAddNode('condition')}>
|
||||
<GitBranch className="mr-2" size={14} />
|
||||
Condition
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => handleAddNode('lua')}>
|
||||
<Code className="mr-2" size={14} />
|
||||
Lua
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
{currentWorkflow.nodes.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-8 border border-dashed rounded-lg">
|
||||
No nodes yet. Add nodes to build your workflow.
|
||||
</p>
|
||||
) : (
|
||||
currentWorkflow.nodes.map((node, index) => (
|
||||
<Card key={node.id} className="border-2">
|
||||
<CardContent className="pt-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className={`w-10 h-10 rounded-lg ${getNodeColor(node.type)} flex items-center justify-center text-white shrink-0`}>
|
||||
{getNodeIcon(node.type)}
|
||||
</div>
|
||||
<div className="flex-1 space-y-3">
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">Node Label</Label>
|
||||
<Input
|
||||
value={node.label}
|
||||
onChange={(e) =>
|
||||
handleUpdateNode(node.id, { label: e.target.value })
|
||||
}
|
||||
placeholder="Node name"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs">Node Type</Label>
|
||||
<Select
|
||||
value={node.type}
|
||||
onValueChange={(value) =>
|
||||
handleUpdateNode(node.id, { type: value as WorkflowNode['type'] })
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="trigger">Trigger</SelectItem>
|
||||
<SelectItem value="action">Action</SelectItem>
|
||||
<SelectItem value="condition">Condition</SelectItem>
|
||||
<SelectItem value="lua">Lua Script</SelectItem>
|
||||
<SelectItem value="transform">Transform</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Step {index + 1}
|
||||
</Badge>
|
||||
{index < currentWorkflow.nodes.length - 1 && (
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<ArrowRight size={12} />
|
||||
<span>Next</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleDeleteNode(node.id)}
|
||||
>
|
||||
<Trash size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{currentWorkflow.nodes.length > 0 && (
|
||||
<div className="bg-muted/50 rounded-lg p-4 border border-dashed">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Lightning size={16} />
|
||||
<span>Workflow execution: {currentWorkflow.nodes.map(n => n.label).join(' → ')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
55
src/lib/auth.ts
Normal file
55
src/lib/auth.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type { User, UserRole } from './level-types'
|
||||
|
||||
export const DEFAULT_USERS: User[] = [
|
||||
{
|
||||
id: 'user_god',
|
||||
username: 'god',
|
||||
email: 'god@builder.com',
|
||||
role: 'god',
|
||||
bio: 'System architect with full access to all levels',
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: 'user_admin',
|
||||
username: 'admin',
|
||||
email: 'admin@builder.com',
|
||||
role: 'admin',
|
||||
bio: 'Administrator with data management access',
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
{
|
||||
id: 'user_demo',
|
||||
username: 'demo',
|
||||
email: 'demo@builder.com',
|
||||
role: 'user',
|
||||
bio: 'Demo user account',
|
||||
createdAt: Date.now(),
|
||||
},
|
||||
]
|
||||
|
||||
export const DEFAULT_CREDENTIALS: Record<string, string> = {
|
||||
god: 'god123',
|
||||
admin: 'admin',
|
||||
demo: 'demo',
|
||||
}
|
||||
|
||||
export function canAccessLevel(userRole: UserRole, level: number): boolean {
|
||||
const roleHierarchy: Record<UserRole, number> = {
|
||||
public: 1,
|
||||
user: 2,
|
||||
admin: 3,
|
||||
god: 4,
|
||||
}
|
||||
|
||||
return roleHierarchy[userRole] >= level
|
||||
}
|
||||
|
||||
export function getRoleDisplayName(role: UserRole): string {
|
||||
const names: Record<UserRole, string> = {
|
||||
public: 'Public',
|
||||
user: 'User',
|
||||
admin: 'Administrator',
|
||||
god: 'System Architect',
|
||||
}
|
||||
return names[role]
|
||||
}
|
||||
78
src/lib/level-types.ts
Normal file
78
src/lib/level-types.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
export type UserRole = 'public' | 'user' | 'admin' | 'god'
|
||||
|
||||
export type AppLevel = 1 | 2 | 3 | 4
|
||||
|
||||
export interface User {
|
||||
id: string
|
||||
username: string
|
||||
email: string
|
||||
role: UserRole
|
||||
profilePicture?: string
|
||||
bio?: string
|
||||
createdAt: number
|
||||
}
|
||||
|
||||
export interface Comment {
|
||||
id: string
|
||||
userId: string
|
||||
content: string
|
||||
createdAt: number
|
||||
updatedAt?: number
|
||||
parentId?: string
|
||||
}
|
||||
|
||||
export interface WorkflowNode {
|
||||
id: string
|
||||
type: 'trigger' | 'action' | 'condition' | 'lua' | 'transform'
|
||||
label: string
|
||||
config: Record<string, any>
|
||||
position: { x: number; y: number }
|
||||
}
|
||||
|
||||
export interface WorkflowEdge {
|
||||
id: string
|
||||
source: string
|
||||
target: string
|
||||
label?: string
|
||||
}
|
||||
|
||||
export interface Workflow {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
nodes: WorkflowNode[]
|
||||
edges: WorkflowEdge[]
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
export interface LuaScript {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
code: string
|
||||
parameters: Array<{ name: string; type: string }>
|
||||
returnType?: string
|
||||
}
|
||||
|
||||
export interface PageConfig {
|
||||
id: string
|
||||
path: string
|
||||
title: string
|
||||
level: AppLevel
|
||||
componentTree: any[]
|
||||
requiresAuth: boolean
|
||||
requiredRole?: UserRole
|
||||
}
|
||||
|
||||
export interface AppConfiguration {
|
||||
id: string
|
||||
name: string
|
||||
schemas: any[]
|
||||
workflows: Workflow[]
|
||||
luaScripts: LuaScript[]
|
||||
pages: PageConfig[]
|
||||
theme: {
|
||||
colors: Record<string, string>
|
||||
fonts: Record<string, string>
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user