0x-lang 0.1.19 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,153 @@
1
+ // 0x LSP — Hover Information
2
+ import { MarkupKind } from 'vscode-languageserver';
3
+ import { parse } from '../parser.js';
4
+ // Keyword documentation map
5
+ const KEYWORD_DOCS = {
6
+ page: 'Page component declaration. Entry point for a route.',
7
+ component: 'Reusable component declaration.',
8
+ app: 'Top-level application declaration. Wraps all pages.',
9
+ state: 'Reactive state variable. Compiles to `useState` (React), `ref` (Vue), `$state` (Svelte).',
10
+ derived: 'Computed value. Compiles to `useMemo` (React), `computed` (Vue), `$derived` (Svelte).',
11
+ prop: 'Component property (input from parent).',
12
+ fn: 'Function declaration.',
13
+ layout: 'Flex/grid layout container. Use `row`, `col`, or `grid`.',
14
+ row: 'Horizontal flex layout (shorthand for `layout row`).',
15
+ col: 'Vertical flex layout (shorthand for `layout col`).',
16
+ grid: 'CSS grid layout.',
17
+ stack: 'Stack layout (overlapping children).',
18
+ text: 'Text element. Renders a `<p>`, `<span>`, or heading tag.',
19
+ button: 'Button element with click handler.',
20
+ input: 'Input field with two-way binding.',
21
+ image: 'Image element with src and alt.',
22
+ link: 'Anchor/link element.',
23
+ toggle: 'Toggle switch / checkbox.',
24
+ select: 'Dropdown select element.',
25
+ table: 'Data table with columns.',
26
+ chart: 'Chart visualization (bar, line, pie, etc.).',
27
+ modal: 'Modal dialog overlay.',
28
+ toast: 'Toast notification.',
29
+ nav: 'Navigation bar component.',
30
+ form: 'Form with fields, validation, and submit handler.',
31
+ model: 'Data model declaration with typed fields.',
32
+ auth: 'Authentication configuration (login, signup, logout, guard).',
33
+ route: 'Route declaration for navigation.',
34
+ role: 'Role-based access control.',
35
+ style: 'Scoped style block for custom CSS properties.',
36
+ import: 'Import external JavaScript module.',
37
+ use: 'Import a 0x component from another file.',
38
+ api: 'API endpoint binding (GET, POST, PUT, DELETE).',
39
+ store: 'Global store variable (shared across components).',
40
+ data: 'Data fetching declaration.',
41
+ watch: 'Watch a state variable for changes and run side effects.',
42
+ check: 'Runtime assertion / validation contract.',
43
+ 'on mount': 'Lifecycle hook: runs after component mounts.',
44
+ 'on destroy': 'Lifecycle hook: runs before component unmounts.',
45
+ if: 'Conditional rendering block.',
46
+ for: 'List rendering — iterates over a collection.',
47
+ show: 'Conditionally show an element (CSS display toggle).',
48
+ hide: 'Conditionally hide an element.',
49
+ emit: 'Emit a custom event to parent component.',
50
+ animate: 'Animation declaration (enter, exit, transition).',
51
+ seo: 'SEO metadata (title, description, og tags).',
52
+ a11y: 'Accessibility attributes.',
53
+ hero: 'Hero section UI pattern.',
54
+ features: 'Features grid section.',
55
+ pricing: 'Pricing table section.',
56
+ faq: 'FAQ accordion section.',
57
+ footer: 'Footer section.',
58
+ admin: 'Admin dashboard scaffold.',
59
+ deploy: 'Deployment configuration.',
60
+ env: 'Environment variable declaration.',
61
+ docker: 'Docker container configuration.',
62
+ realtime: 'Real-time data subscription (WebSocket).',
63
+ crud: 'CRUD scaffold — generates list, create, edit, delete views.',
64
+ upload: 'File upload component.',
65
+ search: 'Search input with filtering.',
66
+ responsive: 'Responsive breakpoint wrapper.',
67
+ async: 'Async function modifier.',
68
+ };
69
+ function getWordAtPosition(source, position) {
70
+ const lines = source.split('\n');
71
+ const line = lines[position.line] || '';
72
+ const before = line.substring(0, position.character);
73
+ const after = line.substring(position.character);
74
+ const wordBefore = before.match(/[\w]+$/) || [''];
75
+ const wordAfter = after.match(/^[\w]+/) || [''];
76
+ return wordBefore[0] + wordAfter[0];
77
+ }
78
+ export function getHoverInfo(source, position) {
79
+ const word = getWordAtPosition(source, position);
80
+ if (!word)
81
+ return null;
82
+ // Check for "on mount" / "on destroy" compound keywords
83
+ const lines = source.split('\n');
84
+ const line = lines[position.line] || '';
85
+ if (word === 'on' || word === 'mount' || word === 'destroy') {
86
+ if (/\bon\s+mount\b/.test(line)) {
87
+ const doc = KEYWORD_DOCS['on mount'];
88
+ if (doc) {
89
+ return { contents: { kind: MarkupKind.Markdown, value: `**on mount**\n\n${doc}` } };
90
+ }
91
+ }
92
+ if (/\bon\s+destroy\b/.test(line)) {
93
+ const doc = KEYWORD_DOCS['on destroy'];
94
+ if (doc) {
95
+ return { contents: { kind: MarkupKind.Markdown, value: `**on destroy**\n\n${doc}` } };
96
+ }
97
+ }
98
+ }
99
+ // Check keyword docs
100
+ const doc = KEYWORD_DOCS[word];
101
+ if (doc) {
102
+ return { contents: { kind: MarkupKind.Markdown, value: `**${word}**\n\n${doc}` } };
103
+ }
104
+ // Try to find the identifier in parsed AST
105
+ try {
106
+ const ast = parse(source);
107
+ for (const node of ast) {
108
+ if (node.type === 'Page' || node.type === 'Component' || node.type === 'App') {
109
+ for (const child of node.body) {
110
+ if (child.type === 'StateDecl' && child.name === word) {
111
+ const typeStr = child.valueType ? `: ${JSON.stringify(child.valueType)}` : '';
112
+ return {
113
+ contents: {
114
+ kind: MarkupKind.Markdown,
115
+ value: `**state** \`${word}\`${typeStr}\n\nReactive state variable declared in \`${node.name}\`.`,
116
+ },
117
+ };
118
+ }
119
+ if (child.type === 'DerivedDecl' && child.name === word) {
120
+ return {
121
+ contents: {
122
+ kind: MarkupKind.Markdown,
123
+ value: `**derived** \`${word}\`\n\nComputed value declared in \`${node.name}\`.`,
124
+ },
125
+ };
126
+ }
127
+ if (child.type === 'PropDecl' && child.name === word) {
128
+ return {
129
+ contents: {
130
+ kind: MarkupKind.Markdown,
131
+ value: `**prop** \`${word}\`\n\nComponent property declared in \`${node.name}\`.`,
132
+ },
133
+ };
134
+ }
135
+ if (child.type === 'FnDecl' && child.name === word) {
136
+ const params = child.params.map(p => p.name).join(', ');
137
+ return {
138
+ contents: {
139
+ kind: MarkupKind.Markdown,
140
+ value: `**fn** \`${word}(${params})\`\n\nFunction declared in \`${node.name}\`.`,
141
+ },
142
+ };
143
+ }
144
+ }
145
+ }
146
+ }
147
+ }
148
+ catch {
149
+ // Parse failed — no hover for identifiers
150
+ }
151
+ return null;
152
+ }
153
+ //# sourceMappingURL=hover.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"hover.js","sourceRoot":"","sources":["../../src/lsp/hover.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,70 @@
1
+ // 0x Language Server — Main entry point
2
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
3
+ // @ts-ignore -- need node-specific createConnection for stdio transport
4
+ import { createConnection, ProposedFeatures } from 'vscode-languageserver/node';
5
+ import { TextDocuments, TextDocumentSyncKind, } from 'vscode-languageserver';
6
+ import { TextDocument } from 'vscode-languageserver-textdocument';
7
+ import { getDiagnostics } from './diagnostics.js';
8
+ import { getCompletions } from './completion.js';
9
+ import { getHoverInfo } from './hover.js';
10
+ import { getDefinition } from './definition.js';
11
+ import { getDocumentSymbols } from './symbols.js';
12
+ // Create connection and document manager
13
+ const connection = createConnection(ProposedFeatures.all);
14
+ const documents = new TextDocuments(TextDocument);
15
+ // Initialize
16
+ connection.onInitialize((_params) => {
17
+ return {
18
+ capabilities: {
19
+ textDocumentSync: TextDocumentSyncKind.Full,
20
+ completionProvider: {
21
+ resolveProvider: false,
22
+ triggerCharacters: [' ', ':'],
23
+ },
24
+ hoverProvider: true,
25
+ definitionProvider: true,
26
+ documentSymbolProvider: true,
27
+ },
28
+ };
29
+ });
30
+ // Diagnostics — run on every content change
31
+ documents.onDidChangeContent((change) => {
32
+ const source = change.document.getText();
33
+ const diagnostics = getDiagnostics(source);
34
+ connection.sendDiagnostics({
35
+ uri: change.document.uri,
36
+ diagnostics,
37
+ });
38
+ });
39
+ // Completions
40
+ connection.onCompletion((params) => {
41
+ const doc = documents.get(params.textDocument.uri);
42
+ if (!doc)
43
+ return [];
44
+ return getCompletions(doc.getText(), params.position);
45
+ });
46
+ // Hover
47
+ connection.onHover((params) => {
48
+ const doc = documents.get(params.textDocument.uri);
49
+ if (!doc)
50
+ return null;
51
+ return getHoverInfo(doc.getText(), params.position);
52
+ });
53
+ // Go to Definition
54
+ connection.onDefinition((params) => {
55
+ const doc = documents.get(params.textDocument.uri);
56
+ if (!doc)
57
+ return null;
58
+ return getDefinition(doc.getText(), params.position, params.textDocument.uri);
59
+ });
60
+ // Document Symbols
61
+ connection.onDocumentSymbol((params) => {
62
+ const doc = documents.get(params.textDocument.uri);
63
+ if (!doc)
64
+ return [];
65
+ return getDocumentSymbols(doc.getText());
66
+ });
67
+ // Start listening
68
+ documents.listen(connection);
69
+ connection.listen();
70
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/lsp/server.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { DocumentSymbol } from 'vscode-languageserver';
2
+ export declare function getDocumentSymbols(source: string): DocumentSymbol[];
@@ -0,0 +1,74 @@
1
+ // 0x LSP — Document Symbols
2
+ import { SymbolKind } from 'vscode-languageserver';
3
+ import { parse } from '../parser.js';
4
+ function locToRange(loc) {
5
+ const line = Math.max(0, loc.line - 1);
6
+ const char = Math.max(0, loc.column - 1);
7
+ return {
8
+ start: { line, character: char },
9
+ end: { line, character: char + 1 },
10
+ };
11
+ }
12
+ function makeSymbol(name, kind, loc, detail, children) {
13
+ const range = locToRange(loc);
14
+ return {
15
+ name,
16
+ kind,
17
+ range,
18
+ selectionRange: range,
19
+ detail,
20
+ children,
21
+ };
22
+ }
23
+ export function getDocumentSymbols(source) {
24
+ const symbols = [];
25
+ try {
26
+ const ast = parse(source);
27
+ for (const node of ast) {
28
+ if (node.type === 'Page' || node.type === 'Component' || node.type === 'App') {
29
+ const children = [];
30
+ for (const child of node.body) {
31
+ switch (child.type) {
32
+ case 'StateDecl':
33
+ children.push(makeSymbol(child.name, SymbolKind.Variable, child.loc, 'state'));
34
+ break;
35
+ case 'DerivedDecl':
36
+ children.push(makeSymbol(child.name, SymbolKind.Variable, child.loc, 'derived'));
37
+ break;
38
+ case 'PropDecl':
39
+ children.push(makeSymbol(child.name, SymbolKind.Property, child.loc, 'prop'));
40
+ break;
41
+ case 'FnDecl':
42
+ children.push(makeSymbol(child.name, SymbolKind.Function, child.loc, 'fn'));
43
+ break;
44
+ case 'OnMount':
45
+ children.push(makeSymbol('on mount', SymbolKind.Event, child.loc));
46
+ break;
47
+ case 'OnDestroy':
48
+ children.push(makeSymbol('on destroy', SymbolKind.Event, child.loc));
49
+ break;
50
+ case 'StyleDecl':
51
+ children.push(makeSymbol('style', SymbolKind.Object, child.loc));
52
+ break;
53
+ }
54
+ }
55
+ symbols.push(makeSymbol(node.name, SymbolKind.Class, node.loc, node.type, children));
56
+ }
57
+ else if (node.type === 'Model') {
58
+ symbols.push(makeSymbol(node.name, SymbolKind.Struct, node.loc, 'model'));
59
+ }
60
+ else if (node.type === 'RouteDecl') {
61
+ symbols.push(makeSymbol(node.path, SymbolKind.Module, node.loc, 'route'));
62
+ }
63
+ else if (node.type === 'RoleDecl') {
64
+ const roleName = node.roles.map(r => r.name).join(', ');
65
+ symbols.push(makeSymbol(roleName, SymbolKind.Enum, node.loc, 'role'));
66
+ }
67
+ }
68
+ }
69
+ catch {
70
+ // Parse failed — return empty symbols
71
+ }
72
+ return symbols;
73
+ }
74
+ //# sourceMappingURL=symbols.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"symbols.js","sourceRoot":"","sources":["../../src/lsp/symbols.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,114 @@
1
+ // Generated by 0x
2
+ import React, { useEffect, useMemo, useState } from 'react';
3
+
4
+ export default function Admin() {
5
+ const [users, setUsers] = useState([]);
6
+ const [activeTab, setActiveTab] = useState('users');
7
+ const [searchQuery, setSearchQuery] = useState('');
8
+ const [loading, setLoading] = useState(true);
9
+ const filteredUsers = useMemo(() => setUsers(prev => prev.filter(u => u.name.includes(searchQuery) || u.email.includes(searchQuery))), [users, searchQuery]);
10
+ const totalUsers = useMemo(() => users.length, [users]);
11
+ const activeUsers = useMemo(() => setUsers(prev => prev.filter(u => u.active)).length, [users]);
12
+ const adminCount = useMemo(() => setUsers(prev => prev.filter(u => u.role == 'admin')).length, [users]);
13
+ const getUsers = async (params) => {
14
+ const res = await fetch('/api/users' + (params ? '?' + new URLSearchParams(params).toString() : ''), { method: 'GET' });
15
+ return res.json();
16
+ };
17
+ const deleteUserApi = async (params) => {
18
+ const res = await fetch('/api/users/{id}' + (params ? '?' + new URLSearchParams(params).toString() : ''), { method: 'DELETE' });
19
+ return res.json();
20
+ };
21
+ const updateUser = async (params) => {
22
+ const res = await fetch('/api/users/{id}' + (params ? '?' + new URLSearchParams(params).toString() : ''), { method: 'PUT' });
23
+ return res.json();
24
+ };
25
+ useEffect(() => {
26
+ setUsers(await getUsers());
27
+ setLoading(false);
28
+ }, []);
29
+ const deleteUser = (id) => {
30
+ await deleteUserApi({ id: id });
31
+ setUsers(setUsers(prev => prev.filter(u => u.id != id)));
32
+ };
33
+ const toggleStatus = (id) => {
34
+ user = users.find(u => u.id == id);
35
+ if (user) {
36
+ user.active = !user.active;
37
+ await updateUser({ id: id, active: user.active });
38
+ }
39
+ };
40
+
41
+ return (
42
+ <div style={{ display: 'flex', flexDirection: 'row', height: '100vh' }}>
43
+ {/* Sidebar */}
44
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px', padding: '24px', backgroundColor: ''#2c3e50'' }}>
45
+ <span style={{ fontSize: '24px', fontWeight: 'bold', color: 'white' }}>Admin Panel</span>
46
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
47
+ <button onClick={() => setActiveTab('users')} className="{ activeTab: activeTab, ==: ==, users: users, ?: ?, primary: 'ghost' }">Users</button>
48
+ <button onClick={() => setActiveTab('settings')} className="{ activeTab: activeTab, ==: ==, settings: settings, ?: ?, primary: 'ghost' }">Settings</button>
49
+ <button onClick={() => setActiveTab('logs')} className="{ activeTab: activeTab, ==: ==, logs: logs, ?: ?, primary: 'ghost' }">Logs</button>
50
+ </div>
51
+ </div>
52
+ {/* Main Content */}
53
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '24px', padding: '32px', flexGrow: 1 }}>
54
+ {activeTab == 'users' && (
55
+ {/* Stats Bar */}
56
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px' }}>
57
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '4px', padding: '24px', borderRadius: '12px', boxShadow: '0 4px 6px rgba(0,0,0,0.1)', backgroundColor: 'white' }}>
58
+ <span style={{ fontSize: '40px', fontWeight: 'bold', color: ''#3498db'' }}>{totalUsers}</span>
59
+ <span style={{ fontSize: '14px', color: ''#666'' }}>Total Users</span>
60
+ </div>
61
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '4px', padding: '24px', borderRadius: '12px', boxShadow: '0 4px 6px rgba(0,0,0,0.1)', backgroundColor: 'white' }}>
62
+ <span style={{ fontSize: '40px', fontWeight: 'bold', color: ''#27ae60'' }}>{activeUsers}</span>
63
+ <span style={{ fontSize: '14px', color: ''#666'' }}>Active Users</span>
64
+ </div>
65
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '4px', padding: '24px', borderRadius: '12px', boxShadow: '0 4px 6px rgba(0,0,0,0.1)', backgroundColor: 'white' }}>
66
+ <span style={{ fontSize: '40px', fontWeight: 'bold', color: ''#9b59b6'' }}>{adminCount}</span>
67
+ <span style={{ fontSize: '14px', color: ''#666'' }}>Admins</span>
68
+ </div>
69
+ </div>
70
+ {/* Search */}
71
+ <input value={searchQuery} onChange={e => setSearchQuery(e.target.value)} placeholder="Search users..." />
72
+ {/* User Table */}
73
+ {loading ? (
74
+ <span style={{ textAlign: 'center' }}>Loading...</span>
75
+ ) : (
76
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '4px' }}>
77
+ {/* Table Header */}
78
+ <div style={{ display: 'flex', flexDirection: 'row', gap: '16px', padding: '12px', backgroundColor: ''#f8f9fa'', borderRadius: '8px' }}>
79
+ <span style={{ fontWeight: 'bold' }}>Name</span>
80
+ <span style={{ fontWeight: 'bold' }}>Email</span>
81
+ <span style={{ fontWeight: 'bold' }}>Role</span>
82
+ <span style={{ fontWeight: 'bold' }}>Status</span>
83
+ <span style={{ fontWeight: 'bold' }}>Actions</span>
84
+ </div>
85
+ {filteredUsers.map((user) => (
86
+ <div style={{ display: 'flex', flexDirection: 'row', gap: '16px', padding: '12px', alignItems: 'center', backgroundColor: 'white', boxShadow: '0 1px 2px rgba(0,0,0,0.1)', borderRadius: '4px' }}>
87
+ <span>{user.name}</span>
88
+ <span style={{ color: ''#666'' }}>{user.email}</span>
89
+ <span style={{ fontSize: '14px' }}>{user.role}</span>
90
+ <span style={{ fontSize: '14px', color: '{ user: user, .: ., active: active, ?: ?, green: '#999' }' }}>{user.active ? 'Active' : 'Inactive'}</span>
91
+ <div style={{ display: 'flex', flexDirection: 'row', gap: '4px' }}>
92
+ <button onClick={() => toggleStatus(user.id)}>{user.active ? 'Disable' : 'Enable'}</button>
93
+ <button onClick={() => deleteUser(user.id)} className="danger">Delete</button>
94
+ </div>
95
+ </div>
96
+ ))}
97
+ </div>
98
+ {filteredUsers.length == 0 && (
99
+ <span style={{ color: ''#999'', textAlign: 'center' }}>No users found</span>
100
+ )}
101
+ )}
102
+ )}
103
+ {activeTab == 'settings' && (
104
+ <span style={{ fontSize: '32px', fontWeight: 'bold' }}>Settings</span>
105
+ <span style={{ color: ''#666'' }}>Settings panel coming soon.</span>
106
+ )}
107
+ {activeTab == 'logs' && (
108
+ <span style={{ fontSize: '32px', fontWeight: 'bold' }}>Activity Logs</span>
109
+ <span style={{ color: ''#666'' }}>Logs panel coming soon.</span>
110
+ )}
111
+ </div>
112
+ </div>
113
+ );
114
+ }
@@ -0,0 +1,80 @@
1
+ // Generated by 0x
2
+ import React, { useEffect, useMemo, useState } from 'react';
3
+
4
+ export default function Blog() {
5
+ const [posts, setPosts] = useState([]);
6
+ const [newTitle, setNewTitle] = useState('');
7
+ const [newContent, setNewContent] = useState('');
8
+ const [loading, setLoading] = useState(true);
9
+ const postCount = useMemo(() => posts.length, [posts]);
10
+ const getPosts = async (params) => {
11
+ const res = await fetch('/api/posts' + (params ? '?' + new URLSearchParams(params).toString() : ''), { method: 'GET' });
12
+ return res.json();
13
+ };
14
+ const createPost = async (params) => {
15
+ const res = await fetch('/api/posts' + (params ? '?' + new URLSearchParams(params).toString() : ''), { method: 'POST' });
16
+ return res.json();
17
+ };
18
+ useEffect(() => {
19
+ setPosts(await getPosts());
20
+ setLoading(false);
21
+ }, []);
22
+ const addPost = () => {
23
+ if (newTitle.trim() != '' && newContent.trim() != '') {
24
+ result = await createPost({ title: newTitle, content: newContent });
25
+ setPosts(prev => [...prev, result]);
26
+ setNewTitle('');
27
+ setNewContent('');
28
+ }
29
+ };
30
+ const deletePost = (id) => {
31
+ setPosts(setPosts(prev => prev.filter(p => p.id != id)));
32
+ };
33
+
34
+ return (
35
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '24px', padding: '32px', maxWidth: '800px', margin: 'auto' }}>
36
+ {/* Header */}
37
+ <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
38
+ <span style={{ fontSize: '40px', fontWeight: 'bold' }}>Blog</span>
39
+ <span style={{ fontSize: '20px', color: ''#666'' }}>{postCount} posts</span>
40
+ </div>
41
+ {/* New Post Form */}
42
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '12px', padding: '24px', borderRadius: '12px', boxShadow: '0 4px 6px rgba(0,0,0,0.1)', backgroundColor: 'white' }}>
43
+ <span style={{ fontSize: '24px', fontWeight: 'bold' }}>New Post</span>
44
+ <input value={newTitle} onChange={e => setNewTitle(e.target.value)} placeholder="Post title..." />
45
+ <input value={newContent} onChange={e => setNewContent(e.target.value)} placeholder="Write your content..." />
46
+ <button onClick={() => addPost()} className="primary">Publish</button>
47
+ </div>
48
+ {/* Post List */}
49
+ {loading ? (
50
+ <span style={{ textAlign: 'center' }}>Loading...</span>
51
+ ) : (
52
+ {posts.length == 0 ? (
53
+ <span style={{ color: ''#999'', textAlign: 'center' }}>No posts yet</span>
54
+ ) : (
55
+ {posts.map((post) => (
56
+ <PostCard {...post} />
57
+ ))}
58
+ )}
59
+ )}
60
+ </div>
61
+ );
62
+ }
63
+
64
+ // Generated by 0x
65
+ import React from 'react';
66
+
67
+ function PostCard({ post }) {
68
+
69
+ return (
70
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px', padding: '24px', borderRadius: '12px', boxShadow: '0 1px 2px rgba(0,0,0,0.1)', backgroundColor: 'white' }}>
71
+ <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', alignItems: 'center' }}>
72
+ <span style={{ fontSize: '24px', fontWeight: 'bold' }}>{post.title}</span>
73
+ <span style={{ fontSize: '14px', color: ''#999'' }}>{post.date}</span>
74
+ </div>
75
+ <span style={{ color: ''#444'' }}>{post.content}</span>
76
+ </div>
77
+ );
78
+ }
79
+
80
+ export { PostCard };
@@ -0,0 +1,98 @@
1
+ // Generated by 0x
2
+ import React, { useEffect, useMemo, useState } from 'react';
3
+
4
+ export default function CRM() {
5
+ const [customers, setCustomers] = useState([]);
6
+ const [searchQuery, setSearchQuery] = useState('');
7
+ const [showAddModal, setShowAddModal] = useState(false);
8
+ const [newName, setNewName] = useState('');
9
+ const [newEmail, setNewEmail] = useState('');
10
+ const [loading, setLoading] = useState(true);
11
+ const [stats, setStats] = useState([]);
12
+ const filtered = useMemo(() => setCustomers(prev => prev.filter(c => c.name.includes(searchQuery) || c.email.includes(searchQuery))), [customers, searchQuery]);
13
+ const totalCount = useMemo(() => customers.length, [customers]);
14
+ const activeCount = useMemo(() => setCustomers(prev => prev.filter(c => c.status == 'active')).length, [customers]);
15
+ const getCustomers = async (params) => {
16
+ const res = await fetch('/api/customers' + (params ? '?' + new URLSearchParams(params).toString() : ''), { method: 'GET' });
17
+ return res.json();
18
+ };
19
+ const createCustomer = async (params) => {
20
+ const res = await fetch('/api/customers' + (params ? '?' + new URLSearchParams(params).toString() : ''), { method: 'POST' });
21
+ return res.json();
22
+ };
23
+ useEffect(() => {
24
+ setCustomers(await getCustomers());
25
+ setLoading(false);
26
+ }, []);
27
+ const addCustomer = () => {
28
+ if (newName.trim() != '' && newEmail.trim() != '') {
29
+ result = await createCustomer({ name: newName, email: newEmail });
30
+ setCustomers(prev => [...prev, result]);
31
+ setNewName('');
32
+ setNewEmail('');
33
+ setShowAddModal(false);
34
+ }
35
+ };
36
+ const removeCustomer = (id) => {
37
+ setCustomers(setCustomers(prev => prev.filter(c => c.id != id)));
38
+ };
39
+
40
+ return (
41
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '24px', padding: '32px' }}>
42
+ {/* Header */}
43
+ <span style={{ fontSize: '40px', fontWeight: 'bold' }}>CRM Dashboard</span>
44
+ {/* Stats */}
45
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px' }}>
46
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '4px', alignItems: 'center', padding: '24px', borderRadius: '12px', boxShadow: '0 4px 6px rgba(0,0,0,0.1)', backgroundColor: 'white' }}>
47
+ <span style={{ fontSize: '40px', fontWeight: 'bold', color: ''#333'' }}>{totalCount}</span>
48
+ <span style={{ fontSize: '14px', color: ''#666'' }}>Total Customers</span>
49
+ </div>
50
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '4px', alignItems: 'center', padding: '24px', borderRadius: '12px', boxShadow: '0 4px 6px rgba(0,0,0,0.1)', backgroundColor: 'white' }}>
51
+ <span style={{ fontSize: '40px', fontWeight: 'bold', color: ''#27ae60'' }}>{activeCount}</span>
52
+ <span style={{ fontSize: '14px', color: ''#666'' }}>Active</span>
53
+ </div>
54
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '4px', alignItems: 'center', padding: '24px', borderRadius: '12px', boxShadow: '0 4px 6px rgba(0,0,0,0.1)', backgroundColor: 'white' }}>
55
+ <span style={{ fontSize: '40px', fontWeight: 'bold', color: ''#999'' }}>{totalCount - activeCount}</span>
56
+ <span style={{ fontSize: '14px', color: ''#666'' }}>Inactive</span>
57
+ </div>
58
+ </div>
59
+ {/* Search & Actions */}
60
+ <div style={{ display: 'flex', flexDirection: 'row', gap: '12px', alignItems: 'center' }}>
61
+ <input value={searchQuery} onChange={e => setSearchQuery(e.target.value)} placeholder="Search customers..." />
62
+ <button onClick={() => setShowAddModal(true)} className="primary">Add Customer</button>
63
+ </div>
64
+ {/* Add Form */}
65
+ {showAddModal && (
66
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '12px', padding: '24px', borderRadius: '12px', boxShadow: '0 4px 6px rgba(0,0,0,0.1)', backgroundColor: 'white' }}>
67
+ <span style={{ fontSize: '24px', fontWeight: 'bold' }}>New Customer</span>
68
+ <input value={newName} onChange={e => setNewName(e.target.value)} placeholder="Customer name..." />
69
+ <input value={newEmail} onChange={e => setNewEmail(e.target.value)} placeholder="Email address..." />
70
+ <div style={{ display: 'flex', flexDirection: 'row', gap: '8px' }}>
71
+ <button onClick={() => addCustomer()} className="primary">Save</button>
72
+ <button onClick={() => setShowAddModal(false)}>Cancel</button>
73
+ </div>
74
+ </div>
75
+ )}
76
+ {/* Customer List */}
77
+ {loading ? (
78
+ <span style={{ textAlign: 'center' }}>Loading...</span>
79
+ ) : (
80
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
81
+ {filtered.map((customer) => (
82
+ <div style={{ display: 'flex', flexDirection: 'row', gap: '16px', padding: '16px', borderRadius: '8px', backgroundColor: 'white', boxShadow: '0 1px 2px rgba(0,0,0,0.1)', alignItems: 'center' }}>
83
+ <div style={{ display: 'flex', flexDirection: 'column', flexGrow: 1 }}>
84
+ <span style={{ fontSize: '20px', fontWeight: 'bold' }}>{customer.name}</span>
85
+ <span style={{ fontSize: '14px', color: ''#666'' }}>{customer.email}</span>
86
+ </div>
87
+ <span style={{ fontSize: '14px', color: '{ customer: customer, .: ., status: status, ==: ==, active: active, ?: ?, green: '#999' }' }}>{customer.status}</span>
88
+ <button onClick={() => removeCustomer(customer.id)} className="danger">Remove</button>
89
+ </div>
90
+ ))}
91
+ </div>
92
+ {filtered.length == 0 && (
93
+ <span style={{ color: ''#999'', textAlign: 'center' }}>No customers found</span>
94
+ )}
95
+ )}
96
+ </div>
97
+ );
98
+ }