@thinksoftai/cli 1.4.0 → 1.5.0
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.
- package/dist/commands/frontend.d.ts +13 -0
- package/dist/commands/frontend.js +1038 -0
- package/dist/commands/frontend.js.map +1 -0
- package/dist/index.js +24 -7
- package/dist/index.js.map +1 -1
- package/dist/utils/aiContext.js +62 -44
- package/dist/utils/aiContext.js.map +1 -1
- package/dist/utils/api.d.ts +6 -0
- package/dist/utils/api.js +34 -0
- package/dist/utils/api.js.map +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,1038 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Frontend command - Scaffold frontend projects for external AI tools
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* thinksoft frontend --init # Scaffold new frontend
|
|
7
|
+
* thinksoft frontend --init --app ID # Scaffold for specific app
|
|
8
|
+
*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
42
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
43
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
44
|
+
};
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.frontend = frontend;
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
50
|
+
const ora_1 = __importDefault(require("ora"));
|
|
51
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
52
|
+
const config = __importStar(require("../utils/config"));
|
|
53
|
+
const api = __importStar(require("../utils/api"));
|
|
54
|
+
async function frontend(options) {
|
|
55
|
+
if (!options.init) {
|
|
56
|
+
console.log(chalk_1.default.yellow('\nUsage: thinksoft frontend --init [--app <appId>]'));
|
|
57
|
+
console.log(chalk_1.default.gray(' Scaffold a new frontend project\n'));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (!config.isLoggedIn()) {
|
|
61
|
+
console.log(chalk_1.default.red('\n✗ Not logged in'));
|
|
62
|
+
console.log(chalk_1.default.gray(' Run: thinksoft login\n'));
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
let appId = options.app;
|
|
66
|
+
// If no app specified, let user select
|
|
67
|
+
if (!appId) {
|
|
68
|
+
const appsResult = await api.listApps();
|
|
69
|
+
if (appsResult.error || !appsResult.apps?.length) {
|
|
70
|
+
console.log(chalk_1.default.red('\n✗ No apps found'));
|
|
71
|
+
console.log(chalk_1.default.gray(' Run: thinksoft create "Your app description"\n'));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const { selectedApp } = await inquirer_1.default.prompt([{
|
|
75
|
+
type: 'list',
|
|
76
|
+
name: 'selectedApp',
|
|
77
|
+
message: 'Select an app:',
|
|
78
|
+
choices: appsResult.apps.map((a) => ({
|
|
79
|
+
name: `${a.name} (${a.short_id})`,
|
|
80
|
+
value: a.short_id
|
|
81
|
+
}))
|
|
82
|
+
}]);
|
|
83
|
+
appId = selectedApp;
|
|
84
|
+
}
|
|
85
|
+
console.log();
|
|
86
|
+
console.log(chalk_1.default.cyan('┌─────────────────────────────────────────────────────────────┐'));
|
|
87
|
+
console.log(chalk_1.default.cyan('│') + chalk_1.default.white.bold(' ThinkSoft Frontend ') + chalk_1.default.cyan('│'));
|
|
88
|
+
console.log(chalk_1.default.cyan('│') + chalk_1.default.gray(` App: ${appId}`.padEnd(61)) + chalk_1.default.cyan('│'));
|
|
89
|
+
console.log(chalk_1.default.cyan('└─────────────────────────────────────────────────────────────┘'));
|
|
90
|
+
console.log();
|
|
91
|
+
// Fetch app info and schema
|
|
92
|
+
const spinner = (0, ora_1.default)('Fetching app schema...').start();
|
|
93
|
+
const appInfo = await fetchAppInfo(appId);
|
|
94
|
+
if (!appInfo) {
|
|
95
|
+
spinner.fail('Failed to fetch app info');
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
spinner.succeed(`Found ${appInfo.tables.length} tables`);
|
|
99
|
+
console.log();
|
|
100
|
+
// Ask for project name
|
|
101
|
+
const { projectName } = await inquirer_1.default.prompt([{
|
|
102
|
+
type: 'input',
|
|
103
|
+
name: 'projectName',
|
|
104
|
+
message: 'Project folder name:',
|
|
105
|
+
default: `${appInfo.name.toLowerCase().replace(/\s+/g, '-')}-frontend`
|
|
106
|
+
}]);
|
|
107
|
+
// Scaffold the project
|
|
108
|
+
console.log();
|
|
109
|
+
await scaffoldFrontend(appId, appInfo, projectName);
|
|
110
|
+
// Show next steps
|
|
111
|
+
showNextSteps(projectName);
|
|
112
|
+
}
|
|
113
|
+
async function fetchAppInfo(appId) {
|
|
114
|
+
try {
|
|
115
|
+
// Get app details
|
|
116
|
+
const appResult = await api.getApp(appId);
|
|
117
|
+
if (appResult.error)
|
|
118
|
+
return null;
|
|
119
|
+
// Get tables/schema
|
|
120
|
+
const tablesResult = await api.getTables(appId);
|
|
121
|
+
const tables = [];
|
|
122
|
+
if (tablesResult.tables) {
|
|
123
|
+
for (const table of tablesResult.tables) {
|
|
124
|
+
const columns = (table.columns || []).map((col) => ({
|
|
125
|
+
name: col.name,
|
|
126
|
+
type: col.type || 'text',
|
|
127
|
+
required: col.required || false
|
|
128
|
+
}));
|
|
129
|
+
tables.push({ name: table.name, columns });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
name: appResult.app?.name || appId,
|
|
134
|
+
short_id: appId,
|
|
135
|
+
description: appResult.app?.description || appResult.app?.prompt || undefined,
|
|
136
|
+
tables
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
async function scaffoldFrontend(appId, appInfo, projectName) {
|
|
144
|
+
const projectDir = path.join(process.cwd(), projectName);
|
|
145
|
+
console.log(chalk_1.default.cyan(`📦 Scaffolding: ${chalk_1.default.white.bold(projectName)}/`));
|
|
146
|
+
console.log();
|
|
147
|
+
// Create project directory
|
|
148
|
+
if (!fs.existsSync(projectDir)) {
|
|
149
|
+
fs.mkdirSync(projectDir, { recursive: true });
|
|
150
|
+
}
|
|
151
|
+
// Create package.json
|
|
152
|
+
const packageJson = {
|
|
153
|
+
name: projectName,
|
|
154
|
+
version: '0.1.0',
|
|
155
|
+
private: true,
|
|
156
|
+
type: 'module',
|
|
157
|
+
scripts: {
|
|
158
|
+
dev: 'vite',
|
|
159
|
+
build: 'tsc && vite build',
|
|
160
|
+
preview: 'vite preview',
|
|
161
|
+
deploy: `thinksoft deploy --app ${appId}`
|
|
162
|
+
},
|
|
163
|
+
dependencies: {
|
|
164
|
+
'@thinksoftai/sdk': '^1.0.0',
|
|
165
|
+
'react': '^18.2.0',
|
|
166
|
+
'react-dom': '^18.2.0',
|
|
167
|
+
'react-router-dom': '^6.20.0',
|
|
168
|
+
'lucide-react': '^0.294.0'
|
|
169
|
+
},
|
|
170
|
+
devDependencies: {
|
|
171
|
+
'@types/react': '^18.2.0',
|
|
172
|
+
'@types/react-dom': '^18.2.0',
|
|
173
|
+
'@vitejs/plugin-react': '^4.2.0',
|
|
174
|
+
'autoprefixer': '^10.4.16',
|
|
175
|
+
'postcss': '^8.4.32',
|
|
176
|
+
'tailwindcss': '^3.3.6',
|
|
177
|
+
'typescript': '^5.3.0',
|
|
178
|
+
'vite': '^5.0.0'
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
writeFile(projectDir, 'package.json', JSON.stringify(packageJson, null, 2));
|
|
182
|
+
// Create thinksoft.json
|
|
183
|
+
const thinksoftJson = {
|
|
184
|
+
appId,
|
|
185
|
+
name: appInfo.name,
|
|
186
|
+
framework: 'react',
|
|
187
|
+
outputDir: 'dist'
|
|
188
|
+
};
|
|
189
|
+
writeFile(projectDir, 'thinksoft.json', JSON.stringify(thinksoftJson, null, 2));
|
|
190
|
+
// Create vite.config.ts
|
|
191
|
+
writeFile(projectDir, 'vite.config.ts', `import { defineConfig } from 'vite'
|
|
192
|
+
import react from '@vitejs/plugin-react'
|
|
193
|
+
import path from 'path'
|
|
194
|
+
|
|
195
|
+
export default defineConfig({
|
|
196
|
+
plugins: [react()],
|
|
197
|
+
resolve: {
|
|
198
|
+
alias: {
|
|
199
|
+
'@': path.resolve(__dirname, './src'),
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
})
|
|
203
|
+
`);
|
|
204
|
+
// Create tsconfig.json
|
|
205
|
+
const tsConfig = {
|
|
206
|
+
compilerOptions: {
|
|
207
|
+
target: 'ES2020',
|
|
208
|
+
useDefineForClassFields: true,
|
|
209
|
+
lib: ['ES2020', 'DOM', 'DOM.Iterable'],
|
|
210
|
+
module: 'ESNext',
|
|
211
|
+
skipLibCheck: true,
|
|
212
|
+
moduleResolution: 'bundler',
|
|
213
|
+
allowImportingTsExtensions: true,
|
|
214
|
+
resolveJsonModule: true,
|
|
215
|
+
isolatedModules: true,
|
|
216
|
+
noEmit: true,
|
|
217
|
+
jsx: 'react-jsx',
|
|
218
|
+
strict: true,
|
|
219
|
+
noUnusedLocals: true,
|
|
220
|
+
noUnusedParameters: true,
|
|
221
|
+
noFallthroughCasesInSwitch: true,
|
|
222
|
+
baseUrl: '.',
|
|
223
|
+
paths: { '@/*': ['./src/*'] }
|
|
224
|
+
},
|
|
225
|
+
include: ['src'],
|
|
226
|
+
references: [{ path: './tsconfig.node.json' }]
|
|
227
|
+
};
|
|
228
|
+
writeFile(projectDir, 'tsconfig.json', JSON.stringify(tsConfig, null, 2));
|
|
229
|
+
// Create tsconfig.node.json
|
|
230
|
+
writeFile(projectDir, 'tsconfig.node.json', JSON.stringify({
|
|
231
|
+
compilerOptions: {
|
|
232
|
+
composite: true,
|
|
233
|
+
skipLibCheck: true,
|
|
234
|
+
module: 'ESNext',
|
|
235
|
+
moduleResolution: 'bundler',
|
|
236
|
+
allowSyntheticDefaultImports: true
|
|
237
|
+
},
|
|
238
|
+
include: ['vite.config.ts']
|
|
239
|
+
}, null, 2));
|
|
240
|
+
// Create tailwind.config.js
|
|
241
|
+
writeFile(projectDir, 'tailwind.config.js', `/** @type {import('tailwindcss').Config} */
|
|
242
|
+
export default {
|
|
243
|
+
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
|
|
244
|
+
theme: { extend: {} },
|
|
245
|
+
plugins: [],
|
|
246
|
+
}
|
|
247
|
+
`);
|
|
248
|
+
// Create postcss.config.js
|
|
249
|
+
writeFile(projectDir, 'postcss.config.js', `export default {
|
|
250
|
+
plugins: {
|
|
251
|
+
tailwindcss: {},
|
|
252
|
+
autoprefixer: {},
|
|
253
|
+
},
|
|
254
|
+
}
|
|
255
|
+
`);
|
|
256
|
+
// Create index.html
|
|
257
|
+
writeFile(projectDir, 'index.html', `<!DOCTYPE html>
|
|
258
|
+
<html lang="en">
|
|
259
|
+
<head>
|
|
260
|
+
<meta charset="UTF-8" />
|
|
261
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
262
|
+
<title>${appInfo.name}</title>
|
|
263
|
+
</head>
|
|
264
|
+
<body>
|
|
265
|
+
<div id="root"></div>
|
|
266
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
267
|
+
</body>
|
|
268
|
+
</html>
|
|
269
|
+
`);
|
|
270
|
+
// Create src directory structure
|
|
271
|
+
const srcDir = path.join(projectDir, 'src');
|
|
272
|
+
const libDir = path.join(srcDir, 'lib');
|
|
273
|
+
const contextDir = path.join(srcDir, 'context');
|
|
274
|
+
const componentsDir = path.join(srcDir, 'components');
|
|
275
|
+
const pagesDir = path.join(srcDir, 'pages');
|
|
276
|
+
fs.mkdirSync(libDir, { recursive: true });
|
|
277
|
+
fs.mkdirSync(contextDir, { recursive: true });
|
|
278
|
+
fs.mkdirSync(componentsDir, { recursive: true });
|
|
279
|
+
fs.mkdirSync(pagesDir, { recursive: true });
|
|
280
|
+
// Create src/main.tsx
|
|
281
|
+
writeFile(srcDir, 'main.tsx', `import React from 'react'
|
|
282
|
+
import ReactDOM from 'react-dom/client'
|
|
283
|
+
import { BrowserRouter } from 'react-router-dom'
|
|
284
|
+
import App from './App'
|
|
285
|
+
import './index.css'
|
|
286
|
+
|
|
287
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
288
|
+
<React.StrictMode>
|
|
289
|
+
<BrowserRouter>
|
|
290
|
+
<App />
|
|
291
|
+
</BrowserRouter>
|
|
292
|
+
</React.StrictMode>,
|
|
293
|
+
)
|
|
294
|
+
`);
|
|
295
|
+
// Create src/index.css
|
|
296
|
+
writeFile(srcDir, 'index.css', `@tailwind base;
|
|
297
|
+
@tailwind components;
|
|
298
|
+
@tailwind utilities;
|
|
299
|
+
`);
|
|
300
|
+
// Create src/lib/client.ts
|
|
301
|
+
writeFile(libDir, 'client.ts', `import { ThinkSoft } from '@thinksoftai/sdk'
|
|
302
|
+
|
|
303
|
+
export const client = new ThinkSoft({ appId: '${appId}' })
|
|
304
|
+
`);
|
|
305
|
+
// Create src/context/AuthContext.tsx
|
|
306
|
+
writeFile(contextDir, 'AuthContext.tsx', `import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react'
|
|
307
|
+
import { client } from '../lib/client'
|
|
308
|
+
|
|
309
|
+
type AuthContextType = {
|
|
310
|
+
isAuthenticated: boolean
|
|
311
|
+
user: { email: string } | null
|
|
312
|
+
login: (email: string, otp: string) => Promise<boolean>
|
|
313
|
+
sendOTP: (email: string) => Promise<boolean>
|
|
314
|
+
logout: () => void
|
|
315
|
+
showLoginModal: boolean
|
|
316
|
+
setShowLoginModal: (show: boolean) => void
|
|
317
|
+
requireAuth: (callback: () => void) => void
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const AuthContext = createContext<AuthContextType | null>(null)
|
|
321
|
+
|
|
322
|
+
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
|
323
|
+
const [isAuthenticated, setIsAuthenticated] = useState(false)
|
|
324
|
+
const [user, setUser] = useState<{ email: string } | null>(null)
|
|
325
|
+
const [showLoginModal, setShowLoginModal] = useState(false)
|
|
326
|
+
const [pendingAction, setPendingAction] = useState<(() => void) | null>(null)
|
|
327
|
+
|
|
328
|
+
useEffect(() => {
|
|
329
|
+
const authed = client.auth.isAuthenticated()
|
|
330
|
+
setIsAuthenticated(authed)
|
|
331
|
+
if (authed) {
|
|
332
|
+
setUser({ email: localStorage.getItem('ts_user_email') || '' })
|
|
333
|
+
}
|
|
334
|
+
}, [])
|
|
335
|
+
|
|
336
|
+
const sendOTP = async (email: string): Promise<boolean> => {
|
|
337
|
+
try {
|
|
338
|
+
await client.auth.sendOTP(email)
|
|
339
|
+
localStorage.setItem('ts_user_email', email)
|
|
340
|
+
return true
|
|
341
|
+
} catch {
|
|
342
|
+
return false
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const login = async (email: string, otp: string): Promise<boolean> => {
|
|
347
|
+
try {
|
|
348
|
+
await client.auth.verifyOTP(email, otp)
|
|
349
|
+
setIsAuthenticated(true)
|
|
350
|
+
setUser({ email })
|
|
351
|
+
setShowLoginModal(false)
|
|
352
|
+
if (pendingAction) {
|
|
353
|
+
pendingAction()
|
|
354
|
+
setPendingAction(null)
|
|
355
|
+
}
|
|
356
|
+
return true
|
|
357
|
+
} catch {
|
|
358
|
+
return false
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
const logout = () => {
|
|
363
|
+
client.auth.logout()
|
|
364
|
+
setIsAuthenticated(false)
|
|
365
|
+
setUser(null)
|
|
366
|
+
localStorage.removeItem('ts_user_email')
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const requireAuth = (callback: () => void) => {
|
|
370
|
+
if (isAuthenticated) {
|
|
371
|
+
callback()
|
|
372
|
+
} else {
|
|
373
|
+
setPendingAction(() => callback)
|
|
374
|
+
setShowLoginModal(true)
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
return (
|
|
379
|
+
<AuthContext.Provider value={{
|
|
380
|
+
isAuthenticated, user, login, sendOTP, logout,
|
|
381
|
+
showLoginModal, setShowLoginModal, requireAuth
|
|
382
|
+
}}>
|
|
383
|
+
{children}
|
|
384
|
+
</AuthContext.Provider>
|
|
385
|
+
)
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
export const useAuth = () => {
|
|
389
|
+
const ctx = useContext(AuthContext)
|
|
390
|
+
if (!ctx) throw new Error('useAuth must be used within AuthProvider')
|
|
391
|
+
return ctx
|
|
392
|
+
}
|
|
393
|
+
`);
|
|
394
|
+
// Create src/components/LoginModal.tsx
|
|
395
|
+
writeFile(componentsDir, 'LoginModal.tsx', `import React, { useState } from 'react'
|
|
396
|
+
import { useAuth } from '../context/AuthContext'
|
|
397
|
+
import { X } from 'lucide-react'
|
|
398
|
+
|
|
399
|
+
const LoginModal: React.FC = () => {
|
|
400
|
+
const { sendOTP, login, showLoginModal, setShowLoginModal } = useAuth()
|
|
401
|
+
const [email, setEmail] = useState('')
|
|
402
|
+
const [otp, setOtp] = useState('')
|
|
403
|
+
const [step, setStep] = useState<'email' | 'otp'>('email')
|
|
404
|
+
const [loading, setLoading] = useState(false)
|
|
405
|
+
const [error, setError] = useState('')
|
|
406
|
+
|
|
407
|
+
if (!showLoginModal) return null
|
|
408
|
+
|
|
409
|
+
const handleSendOTP = async (e: React.FormEvent) => {
|
|
410
|
+
e.preventDefault()
|
|
411
|
+
setLoading(true)
|
|
412
|
+
setError('')
|
|
413
|
+
const ok = await sendOTP(email)
|
|
414
|
+
setLoading(false)
|
|
415
|
+
if (ok) setStep('otp')
|
|
416
|
+
else setError('Failed to send OTP')
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const handleVerify = async (e: React.FormEvent) => {
|
|
420
|
+
e.preventDefault()
|
|
421
|
+
setLoading(true)
|
|
422
|
+
setError('')
|
|
423
|
+
const ok = await login(email, otp)
|
|
424
|
+
setLoading(false)
|
|
425
|
+
if (!ok) setError('Invalid OTP')
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const handleClose = () => {
|
|
429
|
+
setShowLoginModal(false)
|
|
430
|
+
setStep('email')
|
|
431
|
+
setError('')
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return (
|
|
435
|
+
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
|
436
|
+
<div className="bg-white rounded-xl p-6 w-full max-w-md relative">
|
|
437
|
+
<button onClick={handleClose} className="absolute top-4 right-4 text-gray-500 hover:text-gray-700">
|
|
438
|
+
<X size={20} />
|
|
439
|
+
</button>
|
|
440
|
+
<h2 className="text-xl font-bold mb-4">Login Required</h2>
|
|
441
|
+
<p className="text-sm text-gray-600 mb-4">Please login to perform this action.</p>
|
|
442
|
+
{error && <p className="text-red-500 text-sm mb-4">{error}</p>}
|
|
443
|
+
{step === 'email' ? (
|
|
444
|
+
<form onSubmit={handleSendOTP}>
|
|
445
|
+
<input
|
|
446
|
+
type="email"
|
|
447
|
+
value={email}
|
|
448
|
+
onChange={e => setEmail(e.target.value)}
|
|
449
|
+
placeholder="Email"
|
|
450
|
+
className="w-full p-3 border rounded mb-4"
|
|
451
|
+
required
|
|
452
|
+
/>
|
|
453
|
+
<button
|
|
454
|
+
type="submit"
|
|
455
|
+
disabled={loading}
|
|
456
|
+
className="w-full bg-blue-600 text-white p-3 rounded hover:bg-blue-700 disabled:opacity-50"
|
|
457
|
+
>
|
|
458
|
+
{loading ? 'Sending...' : 'Send OTP'}
|
|
459
|
+
</button>
|
|
460
|
+
</form>
|
|
461
|
+
) : (
|
|
462
|
+
<form onSubmit={handleVerify}>
|
|
463
|
+
<p className="text-sm text-gray-600 mb-4">OTP sent to {email}</p>
|
|
464
|
+
<input
|
|
465
|
+
type="text"
|
|
466
|
+
value={otp}
|
|
467
|
+
onChange={e => setOtp(e.target.value)}
|
|
468
|
+
placeholder="Enter OTP"
|
|
469
|
+
className="w-full p-3 border rounded mb-4"
|
|
470
|
+
required
|
|
471
|
+
/>
|
|
472
|
+
<button
|
|
473
|
+
type="submit"
|
|
474
|
+
disabled={loading}
|
|
475
|
+
className="w-full bg-blue-600 text-white p-3 rounded hover:bg-blue-700 disabled:opacity-50"
|
|
476
|
+
>
|
|
477
|
+
{loading ? 'Verifying...' : 'Verify'}
|
|
478
|
+
</button>
|
|
479
|
+
<button
|
|
480
|
+
type="button"
|
|
481
|
+
onClick={() => setStep('email')}
|
|
482
|
+
className="w-full mt-2 text-gray-600 text-sm"
|
|
483
|
+
>
|
|
484
|
+
Back
|
|
485
|
+
</button>
|
|
486
|
+
</form>
|
|
487
|
+
)}
|
|
488
|
+
</div>
|
|
489
|
+
</div>
|
|
490
|
+
)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
export default LoginModal
|
|
494
|
+
`);
|
|
495
|
+
// Create src/App.tsx (minimal shell)
|
|
496
|
+
writeFile(srcDir, 'App.tsx', `import React from 'react'
|
|
497
|
+
import { Routes, Route } from 'react-router-dom'
|
|
498
|
+
import { AuthProvider } from './context/AuthContext'
|
|
499
|
+
import LoginModal from './components/LoginModal'
|
|
500
|
+
|
|
501
|
+
const App: React.FC = () => {
|
|
502
|
+
return (
|
|
503
|
+
<AuthProvider>
|
|
504
|
+
<div className="min-h-screen bg-gray-50">
|
|
505
|
+
<main className="p-6">
|
|
506
|
+
<Routes>
|
|
507
|
+
<Route path="/" element={<HomePage />} />
|
|
508
|
+
{/* Add your routes here */}
|
|
509
|
+
</Routes>
|
|
510
|
+
</main>
|
|
511
|
+
<LoginModal />
|
|
512
|
+
</div>
|
|
513
|
+
</AuthProvider>
|
|
514
|
+
)
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Placeholder home page - replace with your own
|
|
518
|
+
const HomePage: React.FC = () => {
|
|
519
|
+
return (
|
|
520
|
+
<div className="max-w-4xl mx-auto">
|
|
521
|
+
<h1 className="text-3xl font-bold text-gray-900 mb-4">${appInfo.name}</h1>
|
|
522
|
+
<p className="text-gray-600 mb-8">Your frontend is ready. Start building!</p>
|
|
523
|
+
|
|
524
|
+
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
525
|
+
<h2 className="text-lg font-semibold mb-4">Getting Started</h2>
|
|
526
|
+
<ol className="list-decimal list-inside space-y-2 text-gray-700">
|
|
527
|
+
<li>Read <code className="bg-gray-100 px-2 py-1 rounded text-sm">CONTEXT.md</code> for backend schema</li>
|
|
528
|
+
<li>Create pages in <code className="bg-gray-100 px-2 py-1 rounded text-sm">src/pages/</code></li>
|
|
529
|
+
<li>Add routes to <code className="bg-gray-100 px-2 py-1 rounded text-sm">src/App.tsx</code></li>
|
|
530
|
+
<li>Use Cursor or Claude to VIBE CODE your pages</li>
|
|
531
|
+
</ol>
|
|
532
|
+
</div>
|
|
533
|
+
</div>
|
|
534
|
+
)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export default App
|
|
538
|
+
`);
|
|
539
|
+
// Create .gitignore
|
|
540
|
+
writeFile(projectDir, '.gitignore', `node_modules
|
|
541
|
+
dist
|
|
542
|
+
.env
|
|
543
|
+
.env.local
|
|
544
|
+
*.log
|
|
545
|
+
`);
|
|
546
|
+
// Create CONTEXT.md for AI tools
|
|
547
|
+
const contextMd = generateContextMd(appId, appInfo);
|
|
548
|
+
writeFile(projectDir, 'CONTEXT.md', contextMd);
|
|
549
|
+
// Create .cursorrules
|
|
550
|
+
const cursorrules = generateCursorrules(appId, appInfo);
|
|
551
|
+
writeFile(projectDir, '.cursorrules', cursorrules);
|
|
552
|
+
console.log();
|
|
553
|
+
console.log(chalk_1.default.green('✅ Frontend scaffolded successfully!'));
|
|
554
|
+
}
|
|
555
|
+
function writeFile(dir, filename, content) {
|
|
556
|
+
const filePath = path.join(dir, filename);
|
|
557
|
+
fs.writeFileSync(filePath, content);
|
|
558
|
+
console.log(chalk_1.default.gray(` ${filename}`));
|
|
559
|
+
}
|
|
560
|
+
function generateContextMd(appId, appInfo) {
|
|
561
|
+
// Generate table schema section
|
|
562
|
+
const tablesSection = appInfo.tables.length > 0
|
|
563
|
+
? appInfo.tables.map(table => {
|
|
564
|
+
const columnsTable = table.columns.length > 0
|
|
565
|
+
? table.columns.map(col => `| ${col.name} | ${col.type} | ${col.required ? 'Yes' : 'No'} |`).join('\n')
|
|
566
|
+
: '| (no columns defined) | - | - |';
|
|
567
|
+
return `### \`${table.name}\`
|
|
568
|
+
|
|
569
|
+
| Column | Type | Required |
|
|
570
|
+
|--------|------|----------|
|
|
571
|
+
${columnsTable}`;
|
|
572
|
+
}).join('\n\n')
|
|
573
|
+
: '*No tables defined yet. Create tables in the ThinkSoft dashboard.*';
|
|
574
|
+
// Generate SDK examples with actual table names
|
|
575
|
+
const exampleTable = appInfo.tables[0]?.name || 'items';
|
|
576
|
+
const exampleColumns = appInfo.tables[0]?.columns || [];
|
|
577
|
+
const exampleField = exampleColumns[0]?.name || 'name';
|
|
578
|
+
// Build app info table with optional description
|
|
579
|
+
const appInfoRows = [
|
|
580
|
+
`| App ID | \`${appId}\` |`,
|
|
581
|
+
`| App Name | ${appInfo.name} |`
|
|
582
|
+
];
|
|
583
|
+
if (appInfo.description) {
|
|
584
|
+
appInfoRows.push(`| Description | ${appInfo.description} |`);
|
|
585
|
+
}
|
|
586
|
+
appInfoRows.push(`| Tables | ${appInfo.tables.length} |`);
|
|
587
|
+
return `# ${appInfo.name} - ThinkSoft Backend Context
|
|
588
|
+
|
|
589
|
+
> This file provides context for AI-assisted development (Cursor, Claude, Copilot, etc.)
|
|
590
|
+
> Read this to understand the backend schema and SDK usage.
|
|
591
|
+
|
|
592
|
+
## App Information
|
|
593
|
+
|
|
594
|
+
| Property | Value |
|
|
595
|
+
|----------|-------|
|
|
596
|
+
${appInfoRows.join('\n')}
|
|
597
|
+
|
|
598
|
+
---
|
|
599
|
+
|
|
600
|
+
## Database Schema
|
|
601
|
+
|
|
602
|
+
${tablesSection}
|
|
603
|
+
|
|
604
|
+
---
|
|
605
|
+
|
|
606
|
+
## ThinkSoft SDK Reference
|
|
607
|
+
|
|
608
|
+
The SDK client is pre-configured in \`src/lib/client.ts\`.
|
|
609
|
+
|
|
610
|
+
### Import
|
|
611
|
+
|
|
612
|
+
\`\`\`typescript
|
|
613
|
+
import { client } from '../lib/client'
|
|
614
|
+
\`\`\`
|
|
615
|
+
|
|
616
|
+
### List Records
|
|
617
|
+
|
|
618
|
+
\`\`\`typescript
|
|
619
|
+
// Get all records
|
|
620
|
+
const { data } = await client.${exampleTable}.list()
|
|
621
|
+
|
|
622
|
+
// With filtering
|
|
623
|
+
const { data } = await client.${exampleTable}.list({
|
|
624
|
+
filter: { status: 'active' }
|
|
625
|
+
})
|
|
626
|
+
|
|
627
|
+
// With sorting (field:asc or field:desc)
|
|
628
|
+
const { data } = await client.${exampleTable}.list({
|
|
629
|
+
sort: 'created_at:desc'
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
// With pagination
|
|
633
|
+
const { data } = await client.${exampleTable}.list({
|
|
634
|
+
limit: 10,
|
|
635
|
+
offset: 0
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
// Combined
|
|
639
|
+
const { data } = await client.${exampleTable}.list({
|
|
640
|
+
filter: { status: 'active' },
|
|
641
|
+
sort: 'created_at:desc',
|
|
642
|
+
limit: 20
|
|
643
|
+
})
|
|
644
|
+
\`\`\`
|
|
645
|
+
|
|
646
|
+
### Get Single Record
|
|
647
|
+
|
|
648
|
+
\`\`\`typescript
|
|
649
|
+
const record = await client.${exampleTable}.get('record-id')
|
|
650
|
+
\`\`\`
|
|
651
|
+
|
|
652
|
+
### Create Record
|
|
653
|
+
|
|
654
|
+
\`\`\`typescript
|
|
655
|
+
const newRecord = await client.${exampleTable}.create({
|
|
656
|
+
${exampleField}: 'value'
|
|
657
|
+
// add other fields...
|
|
658
|
+
})
|
|
659
|
+
\`\`\`
|
|
660
|
+
|
|
661
|
+
### Update Record
|
|
662
|
+
|
|
663
|
+
\`\`\`typescript
|
|
664
|
+
await client.${exampleTable}.update('record-id', {
|
|
665
|
+
${exampleField}: 'new value'
|
|
666
|
+
})
|
|
667
|
+
\`\`\`
|
|
668
|
+
|
|
669
|
+
### Delete Record
|
|
670
|
+
|
|
671
|
+
\`\`\`typescript
|
|
672
|
+
await client.${exampleTable}.delete('record-id')
|
|
673
|
+
\`\`\`
|
|
674
|
+
|
|
675
|
+
---
|
|
676
|
+
|
|
677
|
+
## Schema Management (Create Tables & Columns)
|
|
678
|
+
|
|
679
|
+
The SDK can create and modify tables/columns programmatically.
|
|
680
|
+
|
|
681
|
+
### Create a New Table
|
|
682
|
+
|
|
683
|
+
\`\`\`typescript
|
|
684
|
+
await client.schema.createTable({
|
|
685
|
+
name: 'Reviews',
|
|
686
|
+
icon: '⭐',
|
|
687
|
+
columns: [
|
|
688
|
+
{ name: 'rating', type: 'number', required: true },
|
|
689
|
+
{ name: 'comment', type: 'textarea' },
|
|
690
|
+
{ name: 'author', type: 'text' }
|
|
691
|
+
]
|
|
692
|
+
})
|
|
693
|
+
\`\`\`
|
|
694
|
+
|
|
695
|
+
### Add Column to Existing Table
|
|
696
|
+
|
|
697
|
+
\`\`\`typescript
|
|
698
|
+
await client.schema.addColumn('${exampleTable}', {
|
|
699
|
+
name: 'priority',
|
|
700
|
+
type: 'select',
|
|
701
|
+
options: ['low', 'medium', 'high']
|
|
702
|
+
})
|
|
703
|
+
\`\`\`
|
|
704
|
+
|
|
705
|
+
### Update Column
|
|
706
|
+
|
|
707
|
+
\`\`\`typescript
|
|
708
|
+
await client.schema.updateColumn('${exampleTable}', 'priority', {
|
|
709
|
+
options: ['low', 'medium', 'high', 'urgent']
|
|
710
|
+
})
|
|
711
|
+
\`\`\`
|
|
712
|
+
|
|
713
|
+
### Delete Column
|
|
714
|
+
|
|
715
|
+
\`\`\`typescript
|
|
716
|
+
await client.schema.deleteColumn('${exampleTable}', 'old_field')
|
|
717
|
+
\`\`\`
|
|
718
|
+
|
|
719
|
+
### Delete Table
|
|
720
|
+
|
|
721
|
+
\`\`\`typescript
|
|
722
|
+
await client.schema.deleteTable('old_table')
|
|
723
|
+
\`\`\`
|
|
724
|
+
|
|
725
|
+
### Column Types
|
|
726
|
+
|
|
727
|
+
| Type | Description |
|
|
728
|
+
|------|-------------|
|
|
729
|
+
| text | Single line text |
|
|
730
|
+
| email | Email address |
|
|
731
|
+
| phone | Phone number |
|
|
732
|
+
| number | Numeric value |
|
|
733
|
+
| date | Date picker |
|
|
734
|
+
| url | URL/link |
|
|
735
|
+
| textarea | Multi-line text |
|
|
736
|
+
| richtext | Rich text editor |
|
|
737
|
+
| select | Single select dropdown |
|
|
738
|
+
| multiselect | Multi-select |
|
|
739
|
+
| radio | Radio buttons |
|
|
740
|
+
| checkbox | Checkbox |
|
|
741
|
+
| file | File upload |
|
|
742
|
+
| reference | Link to another table |
|
|
743
|
+
|
|
744
|
+
---
|
|
745
|
+
|
|
746
|
+
## Authentication Pattern (Auth-on-Action)
|
|
747
|
+
|
|
748
|
+
Users can **browse freely without login**. Authentication is only required for write operations (create, update, delete).
|
|
749
|
+
|
|
750
|
+
### Using requireAuth()
|
|
751
|
+
|
|
752
|
+
\`\`\`typescript
|
|
753
|
+
import { useAuth } from '../context/AuthContext'
|
|
754
|
+
import { client } from '../lib/client'
|
|
755
|
+
|
|
756
|
+
const MyComponent = () => {
|
|
757
|
+
const { requireAuth, isAuthenticated, user } = useAuth()
|
|
758
|
+
const [items, setItems] = useState([])
|
|
759
|
+
|
|
760
|
+
// READ: No auth required
|
|
761
|
+
useEffect(() => {
|
|
762
|
+
client.${exampleTable}.list().then(({ data }) => setItems(data))
|
|
763
|
+
}, [])
|
|
764
|
+
|
|
765
|
+
// WRITE: Wrap with requireAuth - shows login modal if needed
|
|
766
|
+
const handleCreate = () => {
|
|
767
|
+
requireAuth(async () => {
|
|
768
|
+
await client.${exampleTable}.create({ ${exampleField}: 'New Item' })
|
|
769
|
+
// Refresh data after create
|
|
770
|
+
const { data } = await client.${exampleTable}.list()
|
|
771
|
+
setItems(data)
|
|
772
|
+
})
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
const handleUpdate = (id: string) => {
|
|
776
|
+
requireAuth(async () => {
|
|
777
|
+
await client.${exampleTable}.update(id, { ${exampleField}: 'Updated' })
|
|
778
|
+
})
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
const handleDelete = (id: string) => {
|
|
782
|
+
requireAuth(async () => {
|
|
783
|
+
await client.${exampleTable}.delete(id)
|
|
784
|
+
setItems(items.filter(item => item.id !== id))
|
|
785
|
+
})
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
return (
|
|
789
|
+
<div>
|
|
790
|
+
{isAuthenticated && <p>Logged in as {user?.email}</p>}
|
|
791
|
+
<button onClick={handleCreate}>Add Item</button>
|
|
792
|
+
{items.map(item => (
|
|
793
|
+
<div key={item.id}>
|
|
794
|
+
{item.${exampleField}}
|
|
795
|
+
<button onClick={() => handleDelete(item.id)}>Delete</button>
|
|
796
|
+
</div>
|
|
797
|
+
))}
|
|
798
|
+
</div>
|
|
799
|
+
)
|
|
800
|
+
}
|
|
801
|
+
\`\`\`
|
|
802
|
+
|
|
803
|
+
---
|
|
804
|
+
|
|
805
|
+
## Project Structure
|
|
806
|
+
|
|
807
|
+
\`\`\`
|
|
808
|
+
src/
|
|
809
|
+
├── lib/
|
|
810
|
+
│ └── client.ts # ThinkSoft SDK client (configured)
|
|
811
|
+
├── context/
|
|
812
|
+
│ └── AuthContext.tsx # Auth state + requireAuth()
|
|
813
|
+
├── components/
|
|
814
|
+
│ └── LoginModal.tsx # Login modal (implemented)
|
|
815
|
+
├── pages/ # Your pages go here
|
|
816
|
+
│ └── (create your pages)
|
|
817
|
+
├── App.tsx # Router setup
|
|
818
|
+
├── main.tsx # Entry point
|
|
819
|
+
└── index.css # Tailwind CSS
|
|
820
|
+
\`\`\`
|
|
821
|
+
|
|
822
|
+
---
|
|
823
|
+
|
|
824
|
+
## Tech Stack
|
|
825
|
+
|
|
826
|
+
- **React 18** + TypeScript
|
|
827
|
+
- **Vite** - Build tool
|
|
828
|
+
- **Tailwind CSS** - Styling
|
|
829
|
+
- **React Router v6** - Routing
|
|
830
|
+
- **Lucide React** - Icons
|
|
831
|
+
- **@thinksoftai/sdk** - Backend SDK
|
|
832
|
+
|
|
833
|
+
---
|
|
834
|
+
|
|
835
|
+
## Development Commands
|
|
836
|
+
|
|
837
|
+
\`\`\`bash
|
|
838
|
+
npm install # Install dependencies
|
|
839
|
+
npm run dev # Start dev server (http://localhost:5173)
|
|
840
|
+
npm run build # Build for production
|
|
841
|
+
npm run deploy # Deploy to ThinkSoft
|
|
842
|
+
\`\`\`
|
|
843
|
+
|
|
844
|
+
---
|
|
845
|
+
|
|
846
|
+
## Example: Complete Page
|
|
847
|
+
|
|
848
|
+
\`\`\`typescript
|
|
849
|
+
// src/pages/${exampleTable.charAt(0).toUpperCase() + exampleTable.slice(1)}Page.tsx
|
|
850
|
+
import React, { useEffect, useState } from 'react'
|
|
851
|
+
import { client } from '../lib/client'
|
|
852
|
+
import { useAuth } from '../context/AuthContext'
|
|
853
|
+
import { Plus, Trash2 } from 'lucide-react'
|
|
854
|
+
|
|
855
|
+
interface ${exampleTable.charAt(0).toUpperCase() + exampleTable.slice(1)}Item {
|
|
856
|
+
id: string
|
|
857
|
+
${exampleField}: string
|
|
858
|
+
created_at: string
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const ${exampleTable.charAt(0).toUpperCase() + exampleTable.slice(1)}Page: React.FC = () => {
|
|
862
|
+
const [items, setItems] = useState<${exampleTable.charAt(0).toUpperCase() + exampleTable.slice(1)}Item[]>([])
|
|
863
|
+
const [loading, setLoading] = useState(true)
|
|
864
|
+
const { requireAuth } = useAuth()
|
|
865
|
+
|
|
866
|
+
useEffect(() => {
|
|
867
|
+
loadItems()
|
|
868
|
+
}, [])
|
|
869
|
+
|
|
870
|
+
const loadItems = async () => {
|
|
871
|
+
setLoading(true)
|
|
872
|
+
const { data } = await client.${exampleTable}.list({ sort: 'created_at:desc' })
|
|
873
|
+
setItems(data || [])
|
|
874
|
+
setLoading(false)
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
const handleAdd = () => {
|
|
878
|
+
requireAuth(async () => {
|
|
879
|
+
await client.${exampleTable}.create({ ${exampleField}: 'New Item' })
|
|
880
|
+
loadItems()
|
|
881
|
+
})
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
const handleDelete = (id: string) => {
|
|
885
|
+
requireAuth(async () => {
|
|
886
|
+
await client.${exampleTable}.delete(id)
|
|
887
|
+
setItems(items.filter(item => item.id !== id))
|
|
888
|
+
})
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
if (loading) {
|
|
892
|
+
return <div className="p-6">Loading...</div>
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
return (
|
|
896
|
+
<div className="max-w-4xl mx-auto p-6">
|
|
897
|
+
<div className="flex justify-between items-center mb-6">
|
|
898
|
+
<h1 className="text-2xl font-bold">${exampleTable.charAt(0).toUpperCase() + exampleTable.slice(1)}</h1>
|
|
899
|
+
<button
|
|
900
|
+
onClick={handleAdd}
|
|
901
|
+
className="flex items-center gap-2 bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700"
|
|
902
|
+
>
|
|
903
|
+
<Plus size={20} />
|
|
904
|
+
Add New
|
|
905
|
+
</button>
|
|
906
|
+
</div>
|
|
907
|
+
|
|
908
|
+
<div className="bg-white rounded-xl border border-gray-200">
|
|
909
|
+
{items.length === 0 ? (
|
|
910
|
+
<div className="p-8 text-center text-gray-500">
|
|
911
|
+
No items yet. Click "Add New" to create one.
|
|
912
|
+
</div>
|
|
913
|
+
) : (
|
|
914
|
+
<ul className="divide-y divide-gray-200">
|
|
915
|
+
{items.map(item => (
|
|
916
|
+
<li key={item.id} className="flex items-center justify-between p-4">
|
|
917
|
+
<span>{item.${exampleField}}</span>
|
|
918
|
+
<button
|
|
919
|
+
onClick={() => handleDelete(item.id)}
|
|
920
|
+
className="text-red-500 hover:text-red-700"
|
|
921
|
+
>
|
|
922
|
+
<Trash2 size={18} />
|
|
923
|
+
</button>
|
|
924
|
+
</li>
|
|
925
|
+
))}
|
|
926
|
+
</ul>
|
|
927
|
+
)}
|
|
928
|
+
</div>
|
|
929
|
+
</div>
|
|
930
|
+
)
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
export default ${exampleTable.charAt(0).toUpperCase() + exampleTable.slice(1)}Page
|
|
934
|
+
\`\`\`
|
|
935
|
+
|
|
936
|
+
Then add to App.tsx:
|
|
937
|
+
\`\`\`typescript
|
|
938
|
+
import ${exampleTable.charAt(0).toUpperCase() + exampleTable.slice(1)}Page from './pages/${exampleTable.charAt(0).toUpperCase() + exampleTable.slice(1)}Page'
|
|
939
|
+
|
|
940
|
+
// In Routes:
|
|
941
|
+
<Route path="/${exampleTable}" element={<${exampleTable.charAt(0).toUpperCase() + exampleTable.slice(1)}Page />} />
|
|
942
|
+
\`\`\`
|
|
943
|
+
`;
|
|
944
|
+
}
|
|
945
|
+
function generateCursorrules(appId, appInfo) {
|
|
946
|
+
const tableNames = appInfo.tables.map(t => t.name).join(', ') || 'No tables yet';
|
|
947
|
+
const exampleTable = appInfo.tables[0]?.name || 'items';
|
|
948
|
+
return `# Cursor Rules for ${appInfo.name}
|
|
949
|
+
|
|
950
|
+
You are building a React frontend for a ThinkSoft backend app.
|
|
951
|
+
|
|
952
|
+
## App Context
|
|
953
|
+
- App ID: \`${appId}\`
|
|
954
|
+
- Tables: ${tableNames}
|
|
955
|
+
- See CONTEXT.md for full schema details
|
|
956
|
+
|
|
957
|
+
## Key Files (Already Configured)
|
|
958
|
+
- \`src/lib/client.ts\` - SDK client
|
|
959
|
+
- \`src/context/AuthContext.tsx\` - Auth with requireAuth()
|
|
960
|
+
- \`src/components/LoginModal.tsx\` - Login modal
|
|
961
|
+
|
|
962
|
+
## Code Patterns
|
|
963
|
+
|
|
964
|
+
### SDK Usage
|
|
965
|
+
\`\`\`typescript
|
|
966
|
+
import { client } from '../lib/client'
|
|
967
|
+
|
|
968
|
+
// CRUD operations
|
|
969
|
+
const { data } = await client.${exampleTable}.list()
|
|
970
|
+
const { data } = await client.${exampleTable}.list({ filter: { status: 'active' }, sort: 'created_at:desc' })
|
|
971
|
+
const item = await client.${exampleTable}.get(id)
|
|
972
|
+
await client.${exampleTable}.create({ ... })
|
|
973
|
+
await client.${exampleTable}.update(id, { ... })
|
|
974
|
+
await client.${exampleTable}.delete(id)
|
|
975
|
+
|
|
976
|
+
// Schema management (create tables/columns)
|
|
977
|
+
await client.schema.createTable({ name: 'Reviews', columns: [...] })
|
|
978
|
+
await client.schema.addColumn('${exampleTable}', { name: 'priority', type: 'select', options: ['low', 'high'] })
|
|
979
|
+
\`\`\`
|
|
980
|
+
|
|
981
|
+
### Auth-on-Action Pattern
|
|
982
|
+
\`\`\`typescript
|
|
983
|
+
import { useAuth } from '../context/AuthContext'
|
|
984
|
+
|
|
985
|
+
const { requireAuth, isAuthenticated, user } = useAuth()
|
|
986
|
+
|
|
987
|
+
// Read = no auth needed
|
|
988
|
+
const { data } = await client.${exampleTable}.list()
|
|
989
|
+
|
|
990
|
+
// Write = wrap with requireAuth
|
|
991
|
+
const handleSave = () => {
|
|
992
|
+
requireAuth(async () => {
|
|
993
|
+
await client.${exampleTable}.create({ ... })
|
|
994
|
+
})
|
|
995
|
+
}
|
|
996
|
+
\`\`\`
|
|
997
|
+
|
|
998
|
+
## Rules
|
|
999
|
+
|
|
1000
|
+
1. Use TypeScript with proper types
|
|
1001
|
+
2. Use Tailwind CSS for all styling
|
|
1002
|
+
3. Use Lucide React for icons
|
|
1003
|
+
4. Use default exports for pages and components
|
|
1004
|
+
5. Import client from '../lib/client' (never create new ThinkSoft instance)
|
|
1005
|
+
6. Import useAuth from '../context/AuthContext'
|
|
1006
|
+
7. Wrap all create/update/delete with requireAuth()
|
|
1007
|
+
8. Read operations don't need auth
|
|
1008
|
+
|
|
1009
|
+
## Do NOT
|
|
1010
|
+
- Create new ThinkSoft() instances
|
|
1011
|
+
- Add BrowserRouter (already in main.tsx)
|
|
1012
|
+
- Generate package.json or config files
|
|
1013
|
+
- Use inline styles
|
|
1014
|
+
- Block users from reading data
|
|
1015
|
+
- Use named exports for page components
|
|
1016
|
+
`;
|
|
1017
|
+
}
|
|
1018
|
+
function showNextSteps(projectName) {
|
|
1019
|
+
console.log();
|
|
1020
|
+
console.log(chalk_1.default.cyan('┌─────────────────────────────────────────────────────────────┐'));
|
|
1021
|
+
console.log(chalk_1.default.cyan('│') + chalk_1.default.white.bold(' Next Steps ') + chalk_1.default.cyan('│'));
|
|
1022
|
+
console.log(chalk_1.default.cyan('└─────────────────────────────────────────────────────────────┘'));
|
|
1023
|
+
console.log();
|
|
1024
|
+
console.log(chalk_1.default.white(` 1. cd ${projectName}`));
|
|
1025
|
+
console.log(chalk_1.default.white(' 2. npm install'));
|
|
1026
|
+
console.log(chalk_1.default.white(' 3. npm run dev'));
|
|
1027
|
+
console.log();
|
|
1028
|
+
console.log(chalk_1.default.gray(' Build pages with VIBE CODE:'));
|
|
1029
|
+
console.log(chalk_1.default.white(' 4. Open in Cursor or VS Code'));
|
|
1030
|
+
console.log(chalk_1.default.white(' 5. Read CONTEXT.md for thinksoft backend app'));
|
|
1031
|
+
console.log(chalk_1.default.white(' 6. VIBE CODE Frontend App'));
|
|
1032
|
+
console.log();
|
|
1033
|
+
console.log(chalk_1.default.gray(' Deploy:'));
|
|
1034
|
+
console.log(chalk_1.default.white(' 7. npm run build'));
|
|
1035
|
+
console.log(chalk_1.default.white(' 8. npm run deploy'));
|
|
1036
|
+
console.log();
|
|
1037
|
+
}
|
|
1038
|
+
//# sourceMappingURL=frontend.js.map
|