create-surf-app 0.1.26-alpha.0 → 1.0.0-alpha.10
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/chunk-FDATV75D.js +87 -0
- package/dist/cli.js +1 -7
- package/dist/index.js +1 -1
- package/dist/templates/default/CLAUDE.md +20 -0
- package/dist/templates/default/backend/.env.example +2 -0
- package/dist/templates/default/backend/package.json +2 -3
- package/dist/templates/default/backend/scripts/check-env.js +40 -0
- package/dist/templates/default/backend/server.js +1 -1
- package/dist/templates/default/frontend/.env.example +3 -0
- package/dist/templates/default/frontend/package.json +2 -2
- package/dist/templates/default/frontend/scripts/check-env.cjs +51 -0
- package/dist/templates/default/frontend/src/vite-env.d.ts +0 -9
- package/dist/templates/default/frontend/vite.config.ts +6 -21
- package/dist/templates/default/package.json +11 -0
- package/dist/templates/nextjs/.env.example +3 -0
- package/dist/templates/nextjs/CLAUDE.md +93 -1
- package/dist/templates/nextjs/app/api/__sync-schema/route.ts +18 -0
- package/dist/templates/nextjs/app/api/cron/route.ts +31 -0
- package/dist/templates/nextjs/app/api/health/route.ts +1 -1
- package/dist/templates/nextjs/app/api/market/price/route.ts +5 -21
- package/dist/templates/nextjs/app/globals.css +1 -25
- package/dist/templates/nextjs/app/layout.tsx +16 -25
- package/dist/templates/nextjs/app/page.tsx +20 -60
- package/dist/templates/nextjs/app/providers.tsx +37 -0
- package/dist/templates/nextjs/components/ui/accordion.tsx +55 -0
- package/dist/templates/nextjs/components/ui/alert.tsx +59 -0
- package/dist/templates/nextjs/components/ui/aspect-ratio.tsx +5 -0
- package/dist/templates/nextjs/components/ui/avatar.tsx +48 -0
- package/dist/templates/nextjs/components/ui/badge.tsx +36 -0
- package/dist/templates/nextjs/components/ui/breadcrumb.tsx +115 -0
- package/dist/templates/nextjs/components/ui/button.tsx +57 -0
- package/dist/templates/nextjs/components/ui/calendar.tsx +211 -0
- package/dist/templates/nextjs/components/ui/card.tsx +76 -0
- package/dist/templates/nextjs/components/ui/carousel.tsx +262 -0
- package/dist/templates/nextjs/components/ui/checkbox.tsx +30 -0
- package/dist/templates/nextjs/components/ui/collapsible.tsx +9 -0
- package/dist/templates/nextjs/components/ui/command.tsx +153 -0
- package/dist/templates/nextjs/components/ui/context-menu.tsx +200 -0
- package/dist/templates/nextjs/components/ui/dialog.tsx +120 -0
- package/dist/templates/nextjs/components/ui/drawer.tsx +118 -0
- package/dist/templates/nextjs/components/ui/dropdown-menu.tsx +201 -0
- package/dist/templates/nextjs/components/ui/form.tsx +176 -0
- package/dist/templates/nextjs/components/ui/hover-card.tsx +29 -0
- package/dist/templates/nextjs/components/ui/input.tsx +22 -0
- package/dist/templates/nextjs/components/ui/label.tsx +26 -0
- package/dist/templates/nextjs/components/ui/menubar.tsx +256 -0
- package/dist/templates/nextjs/components/ui/navigation-menu.tsx +128 -0
- package/dist/templates/nextjs/components/ui/popover.tsx +33 -0
- package/dist/templates/nextjs/components/ui/progress.tsx +26 -0
- package/dist/templates/nextjs/components/ui/radio-group.tsx +42 -0
- package/dist/templates/nextjs/components/ui/resizable.tsx +43 -0
- package/dist/templates/nextjs/components/ui/scroll-area.tsx +46 -0
- package/dist/templates/nextjs/components/ui/select.tsx +157 -0
- package/dist/templates/nextjs/components/ui/separator.tsx +31 -0
- package/dist/templates/nextjs/components/ui/sheet.tsx +140 -0
- package/dist/templates/nextjs/components/ui/skeleton.tsx +15 -0
- package/dist/templates/nextjs/components/ui/slider.tsx +26 -0
- package/dist/templates/nextjs/components/ui/sonner.tsx +29 -0
- package/dist/templates/nextjs/components/ui/switch.tsx +29 -0
- package/dist/templates/nextjs/components/ui/table.tsx +120 -0
- package/dist/templates/nextjs/components/ui/tabs.tsx +53 -0
- package/dist/templates/nextjs/components/ui/textarea.tsx +22 -0
- package/dist/templates/nextjs/components/ui/toast.tsx +129 -0
- package/dist/templates/nextjs/components/ui/toaster.tsx +35 -0
- package/dist/templates/nextjs/components/ui/toggle-group.tsx +59 -0
- package/dist/templates/nextjs/components/ui/toggle.tsx +43 -0
- package/dist/templates/nextjs/components/ui/tooltip.tsx +30 -0
- package/dist/templates/nextjs/db/index.ts +8 -0
- package/dist/templates/nextjs/db/schema.ts +8 -0
- package/dist/templates/nextjs/eslint.config.mjs +14 -16
- package/dist/templates/nextjs/hooks/use-toast.ts +95 -0
- package/dist/templates/nextjs/instrumentation.ts +14 -0
- package/dist/templates/nextjs/lib/boot.ts +56 -0
- package/dist/templates/nextjs/lib/utils.ts +6 -0
- package/dist/templates/nextjs/next.config.ts +7 -3
- package/dist/templates/nextjs/package.json +68 -17
- package/dist/templates/nextjs/postcss.config.mjs +2 -2
- package/dist/templates/nextjs/scripts/check-env.js +52 -0
- package/dist/templates/nextjs/tsconfig.json +5 -10
- package/package.json +2 -2
- package/dist/chunk-E32T2IIS.js +0 -148
- package/dist/templates/nextjs/AGENTS.md +0 -10
- package/dist/templates/nextjs/README.md +0 -54
- package/dist/templates/nextjs/app/favicon.ico +0 -0
- package/dist/templates/nextjs/public/file.svg +0 -1
- package/dist/templates/nextjs/public/globe.svg +0 -1
- package/dist/templates/nextjs/public/next.svg +0 -1
- package/dist/templates/nextjs/public/vercel.svg +0 -1
- package/dist/templates/nextjs/public/window.svg +0 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
var VALID_TEMPLATES = ["vite", "nextjs"];
|
|
6
|
+
async function createSurfApp({
|
|
7
|
+
projectName = ".",
|
|
8
|
+
template: templateArg,
|
|
9
|
+
logger = console.log
|
|
10
|
+
} = {}) {
|
|
11
|
+
const root = path.resolve(projectName);
|
|
12
|
+
const name = path.basename(root);
|
|
13
|
+
const template = validateTemplate(templateArg);
|
|
14
|
+
const templateDir = resolveTemplateDir(template);
|
|
15
|
+
logger(`
|
|
16
|
+
Creating Surf app (${template}) in ${root}
|
|
17
|
+
`);
|
|
18
|
+
fs.mkdirSync(root, { recursive: true });
|
|
19
|
+
copyDir(templateDir, root, root, logger);
|
|
20
|
+
if (template === "nextjs") {
|
|
21
|
+
finalizePackageName(root, name);
|
|
22
|
+
logger(`
|
|
23
|
+
Done! Next steps:
|
|
24
|
+
|
|
25
|
+
cd ${name}
|
|
26
|
+
npm install
|
|
27
|
+
npm run dev
|
|
28
|
+
|
|
29
|
+
Open http://localhost:3000
|
|
30
|
+
`);
|
|
31
|
+
} else {
|
|
32
|
+
logger(`
|
|
33
|
+
Done! Next steps:
|
|
34
|
+
|
|
35
|
+
cd ${name}
|
|
36
|
+
npm install
|
|
37
|
+
npm run dev
|
|
38
|
+
|
|
39
|
+
Open the local URL printed by Vite
|
|
40
|
+
`);
|
|
41
|
+
}
|
|
42
|
+
return root;
|
|
43
|
+
}
|
|
44
|
+
function validateTemplate(template) {
|
|
45
|
+
if (!template) return "vite";
|
|
46
|
+
if (!VALID_TEMPLATES.includes(template)) {
|
|
47
|
+
throw new Error(`Unknown template: ${template}. Valid templates: ${VALID_TEMPLATES.join(", ")}`);
|
|
48
|
+
}
|
|
49
|
+
return template;
|
|
50
|
+
}
|
|
51
|
+
function resolveTemplateDir(template = "vite") {
|
|
52
|
+
const dirName = template === "vite" ? "default" : template;
|
|
53
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
54
|
+
const candidates = [
|
|
55
|
+
path.join(here, "templates", dirName),
|
|
56
|
+
path.join(here, "..", "templates", dirName)
|
|
57
|
+
];
|
|
58
|
+
for (const candidate of candidates) {
|
|
59
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
60
|
+
}
|
|
61
|
+
throw new Error(`Could not find ${template} template near ${here}`);
|
|
62
|
+
}
|
|
63
|
+
function copyDir(src, dest, root, logger) {
|
|
64
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
65
|
+
const srcPath = path.join(src, entry.name);
|
|
66
|
+
const destPath = path.join(dest, entry.name);
|
|
67
|
+
if (entry.isDirectory()) {
|
|
68
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
69
|
+
copyDir(srcPath, destPath, root, logger);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
73
|
+
fs.writeFileSync(destPath, fs.readFileSync(srcPath));
|
|
74
|
+
logger(` ${path.relative(root, destPath)}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function finalizePackageName(root, projectName) {
|
|
78
|
+
const pkgPath = path.join(root, "package.json");
|
|
79
|
+
if (!fs.existsSync(pkgPath)) return;
|
|
80
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
81
|
+
pkg.name = projectName.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "surf-app";
|
|
82
|
+
fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export {
|
|
86
|
+
createSurfApp
|
|
87
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
createSurfApp
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-FDATV75D.js";
|
|
5
5
|
|
|
6
6
|
// src/cli.ts
|
|
7
7
|
var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
8
|
-
"--frontend-port",
|
|
9
|
-
"--backend-port",
|
|
10
|
-
"--preview-base",
|
|
11
8
|
"--template"
|
|
12
9
|
]);
|
|
13
10
|
function getFlag(args, name) {
|
|
@@ -35,9 +32,6 @@ function parseCliArgs(args) {
|
|
|
35
32
|
}
|
|
36
33
|
return {
|
|
37
34
|
projectName: positionalArgs[0] || ".",
|
|
38
|
-
frontendPort: getFlag(args, "--frontend-port"),
|
|
39
|
-
backendPort: getFlag(args, "--backend-port"),
|
|
40
|
-
previewBase: getFlag(args, "--preview-base"),
|
|
41
35
|
template: getFlag(args, "--template")
|
|
42
36
|
};
|
|
43
37
|
}
|
package/dist/index.js
CHANGED
|
@@ -88,3 +88,23 @@ The agent can also call `POST /api/__sync-schema` explicitly after editing.
|
|
|
88
88
|
- Do not bypass your backend routes from the frontend
|
|
89
89
|
- Frontend packages are pre-installed - check `package.json` before installing
|
|
90
90
|
- Default to a dark theme unless the user explicitly asks for a different visual direction.
|
|
91
|
+
|
|
92
|
+
## Design
|
|
93
|
+
|
|
94
|
+
### Avoid AI-default patterns
|
|
95
|
+
- No colored icon boxes next to metrics (the blue-bg-with-icon KPI pattern)
|
|
96
|
+
- No gradient avatar circles with initials
|
|
97
|
+
- No "Built with React · Tailwind" footers
|
|
98
|
+
- No AI copywriting: "Elevate", "Seamless", "Unleash", "Delve", "Next-Gen"
|
|
99
|
+
- No "Oops!" error messages — be direct ("Failed to load. Try again.")
|
|
100
|
+
- No round placeholder numbers ($100.00) — use realistic data ($847.29)
|
|
101
|
+
- Sentence case on headings, not Title Case On Every Word
|
|
102
|
+
- Icons should aid scanning, not decorate — omit when the label is clear
|
|
103
|
+
|
|
104
|
+
### ECharts
|
|
105
|
+
- Flat style: show primary axis line, dashed split lines, transparent chart bg
|
|
106
|
+
- Custom tooltip formatter with dash indicators (12×2.5px bars, not default circle dots)
|
|
107
|
+
- Legend: type "plain", icon "roundRect", itemWidth 12, itemHeight 3
|
|
108
|
+
- Prefer timeframe tabs (7D/30D/90D/1Y/All) over dataZoom for time series
|
|
109
|
+
- Default to theme visualizer palette; override when semantics demand it (red/green for gain/loss, sequential scales for heatmaps)
|
|
110
|
+
- Dark mode: parameterize tooltip bg, axis colors, split line colors via resolvedTheme
|
|
@@ -2,11 +2,10 @@
|
|
|
2
2
|
"name": "backend",
|
|
3
3
|
"private": true,
|
|
4
4
|
"scripts": {
|
|
5
|
-
"
|
|
6
|
-
"dev": "node --watch server.js"
|
|
5
|
+
"dev": "node scripts/check-env.js node --watch server.js"
|
|
7
6
|
},
|
|
8
7
|
"dependencies": {
|
|
9
|
-
"@surf-ai/sdk": "0.
|
|
8
|
+
"@surf-ai/sdk": "1.0.0-alpha.10",
|
|
10
9
|
"express": "4.22.1"
|
|
11
10
|
}
|
|
12
11
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates required env vars before running a command.
|
|
3
|
+
* Loads .env if it exists (optional convenience), then checks vars.
|
|
4
|
+
*
|
|
5
|
+
* Dev/start: BACKEND_PORT, SURF_API_KEY
|
|
6
|
+
*/
|
|
7
|
+
const fs = require('node:fs')
|
|
8
|
+
const path = require('node:path')
|
|
9
|
+
const { execSync } = require('node:child_process')
|
|
10
|
+
|
|
11
|
+
// Load .env if it exists (convenience — env vars can come from anywhere)
|
|
12
|
+
const envPath = path.join(process.cwd(), '.env')
|
|
13
|
+
if (fs.existsSync(envPath)) {
|
|
14
|
+
for (const line of fs.readFileSync(envPath, 'utf8').split('\n')) {
|
|
15
|
+
const trimmed = line.trim()
|
|
16
|
+
if (!trimmed || trimmed.startsWith('#')) continue
|
|
17
|
+
const eq = trimmed.indexOf('=')
|
|
18
|
+
if (eq < 0) continue
|
|
19
|
+
const key = trimmed.slice(0, eq)
|
|
20
|
+
const val = trimmed.slice(eq + 1)
|
|
21
|
+
if (!process.env[key]) process.env[key] = val
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const args = process.argv.slice(2)
|
|
26
|
+
|
|
27
|
+
const required = ['BACKEND_PORT', 'SURF_API_KEY']
|
|
28
|
+
const missing = required.filter(k => !process.env[k])
|
|
29
|
+
|
|
30
|
+
if (missing.length > 0) {
|
|
31
|
+
console.error(`\n❌ Missing required env vars: ${missing.join(', ')}`)
|
|
32
|
+
console.error(` Set them in your environment or copy .env.example to .env\n`)
|
|
33
|
+
process.exit(1)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
execSync(args.join(' '), { stdio: 'inherit', env: process.env })
|
|
38
|
+
} catch (e) {
|
|
39
|
+
process.exit(e.status || 1)
|
|
40
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
const { createServer } = require('@surf-ai/sdk/server')
|
|
2
|
-
createServer(
|
|
2
|
+
createServer().start()
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
"version": "0.0.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"dev": "vite",
|
|
8
|
-
"build": "npm run build:client && npm run build:server",
|
|
7
|
+
"dev": "node scripts/check-env.cjs vite",
|
|
8
|
+
"build": "node scripts/check-env.cjs npm run build:client && npm run build:server",
|
|
9
9
|
"build:client": "vite build --outDir dist/client",
|
|
10
10
|
"build:server": "vite build --ssr src/entry-server.tsx --outDir dist/server",
|
|
11
11
|
"lint": "eslint .",
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates required env vars before running a command.
|
|
3
|
+
* Loads .env manually, checks vars, then execs the actual command.
|
|
4
|
+
*
|
|
5
|
+
* Build: BASE_PATH (defined, can be empty), BACKEND_PORT
|
|
6
|
+
* Dev: PORT, BACKEND_PORT, BASE_PATH
|
|
7
|
+
*/
|
|
8
|
+
const fs = require('node:fs')
|
|
9
|
+
const path = require('node:path')
|
|
10
|
+
const { execSync } = require('node:child_process')
|
|
11
|
+
|
|
12
|
+
// Load .env manually (Vite doesn't load non-VITE_ vars into process.env)
|
|
13
|
+
const envPath = path.join(process.cwd(), '.env')
|
|
14
|
+
if (fs.existsSync(envPath)) {
|
|
15
|
+
for (const line of fs.readFileSync(envPath, 'utf8').split('\n')) {
|
|
16
|
+
const trimmed = line.trim()
|
|
17
|
+
if (!trimmed || trimmed.startsWith('#')) continue
|
|
18
|
+
const eq = trimmed.indexOf('=')
|
|
19
|
+
if (eq < 0) continue
|
|
20
|
+
const key = trimmed.slice(0, eq)
|
|
21
|
+
const val = trimmed.slice(eq + 1)
|
|
22
|
+
if (!process.env[key]) process.env[key] = val
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const args = process.argv.slice(2)
|
|
27
|
+
const isBuild = args.some(a => a.includes('build'))
|
|
28
|
+
|
|
29
|
+
// Vars that must be non-empty
|
|
30
|
+
const requiredNonEmpty = isBuild
|
|
31
|
+
? ['BACKEND_PORT']
|
|
32
|
+
: ['PORT', 'BACKEND_PORT']
|
|
33
|
+
|
|
34
|
+
// Vars that must be defined (empty is ok — BASE_PATH="" means root)
|
|
35
|
+
const requiredDefined = ['BASE_PATH']
|
|
36
|
+
|
|
37
|
+
const missingNonEmpty = requiredNonEmpty.filter(k => !process.env[k])
|
|
38
|
+
const missingDefined = requiredDefined.filter(k => process.env[k] === undefined)
|
|
39
|
+
const missing = [...missingNonEmpty, ...missingDefined]
|
|
40
|
+
|
|
41
|
+
if (missing.length > 0) {
|
|
42
|
+
console.error(`\n❌ Missing required env vars: ${missing.join(', ')}`)
|
|
43
|
+
console.error(` Set them in your environment or copy .env.example to .env\n`)
|
|
44
|
+
process.exit(1)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
execSync(args.join(' '), { stdio: 'inherit', env: process.env })
|
|
49
|
+
} catch (e) {
|
|
50
|
+
process.exit(e.status || 1)
|
|
51
|
+
}
|
|
@@ -1,24 +1,12 @@
|
|
|
1
1
|
import path from 'path'
|
|
2
|
-
import { defineConfig
|
|
2
|
+
import { defineConfig } from 'vite'
|
|
3
3
|
import react from '@vitejs/plugin-react'
|
|
4
4
|
import tailwindcss from '@tailwindcss/vite'
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
env
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const value = env[name]
|
|
11
|
-
const port = Number.parseInt(value || '', 10)
|
|
12
|
-
if (!Number.isInteger(port)) {
|
|
13
|
-
throw new Error(`Missing required ${name} in frontend/.env`)
|
|
14
|
-
}
|
|
15
|
-
return port
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export default defineConfig(({ mode }) => {
|
|
19
|
-
const env = loadEnv(mode, process.cwd())
|
|
20
|
-
const backendPort = readRequiredPort(env, 'VITE_BACKEND_PORT')
|
|
21
|
-
const base = env.VITE_BASE || './'
|
|
6
|
+
export default defineConfig(() => {
|
|
7
|
+
const frontendPort = Number.parseInt(process.env.PORT || '', 10)
|
|
8
|
+
const backendPort = Number.parseInt(process.env.BACKEND_PORT || '', 10)
|
|
9
|
+
const base = process.env.BASE_PATH || './'
|
|
22
10
|
const hasAbsBase = base.startsWith('/')
|
|
23
11
|
const apiBasePrefix = hasAbsBase ? base.replace(/\/$/, '') : ''
|
|
24
12
|
|
|
@@ -33,12 +21,11 @@ export default defineConfig(({ mode }) => {
|
|
|
33
21
|
return {
|
|
34
22
|
plugins: [react(), tailwindcss()],
|
|
35
23
|
server: {
|
|
36
|
-
port: readRequiredPort(env, 'VITE_PORT'),
|
|
37
24
|
host: '0.0.0.0',
|
|
25
|
+
port: frontendPort || undefined,
|
|
38
26
|
proxy: {
|
|
39
27
|
[`${apiBasePrefix}/api`]: backendProxy,
|
|
40
28
|
},
|
|
41
|
-
// Keep the HMR socket under the preview base path.
|
|
42
29
|
hmr: {
|
|
43
30
|
path: 'ws/vite-hmr',
|
|
44
31
|
},
|
|
@@ -50,8 +37,6 @@ export default defineConfig(({ mode }) => {
|
|
|
50
37
|
dedupe: ['react', 'react-dom'],
|
|
51
38
|
preserveSymlinks: true,
|
|
52
39
|
},
|
|
53
|
-
// Pre-bundle the deps touched during the initial boot path so cold starts
|
|
54
|
-
// do not race Vite's lazy dependency optimizer.
|
|
55
40
|
optimizeDeps: {
|
|
56
41
|
include: [
|
|
57
42
|
'react',
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "surf-app",
|
|
3
|
+
"private": true,
|
|
4
|
+
"workspaces": ["backend", "frontend"],
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "concurrently \"npm run dev --workspace backend\" \"npm run dev --workspace frontend\""
|
|
7
|
+
},
|
|
8
|
+
"devDependencies": {
|
|
9
|
+
"concurrently": "^9.0.0"
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -1 +1,93 @@
|
|
|
1
|
-
|
|
1
|
+
# Project
|
|
2
|
+
|
|
3
|
+
Built with [Surf SDK](https://github.com/cyberconnecthq/urania/tree/main/packages/sdk) and Next.js App Router.
|
|
4
|
+
|
|
5
|
+
## Imports from @surf-ai/sdk
|
|
6
|
+
|
|
7
|
+
Use `@surf-ai/sdk/server` in API route handlers to talk to Surf data APIs.
|
|
8
|
+
Do not import `@surf-ai/sdk/server` in client components.
|
|
9
|
+
Use `fetch('/api/...')` in client components to call your own API routes.
|
|
10
|
+
|
|
11
|
+
**API Route Handler (`@surf-ai/sdk/server`):**
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { dataApi } from "@surf-ai/sdk/server";
|
|
15
|
+
const data = await dataApi.market.price({ symbol: "BTC" });
|
|
16
|
+
const holders = await dataApi.token.holders({
|
|
17
|
+
address: "0x...",
|
|
18
|
+
chain: "ethereum",
|
|
19
|
+
});
|
|
20
|
+
// Escape hatch for new endpoints:
|
|
21
|
+
const raw = await dataApi.get("newcategory/endpoint", { foo: "bar" });
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Structure
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
app/page.tsx - build your UI here
|
|
28
|
+
app/api/*/route.ts - add API route handlers
|
|
29
|
+
components/ - add components (use "use client" directive)
|
|
30
|
+
db/schema.ts - define database tables
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Built-in Endpoints
|
|
34
|
+
|
|
35
|
+
These are provided automatically - do NOT create routes for them:
|
|
36
|
+
|
|
37
|
+
| Endpoint | Method | Purpose |
|
|
38
|
+
| -------------------- | ------ | ---------------------------------------------- |
|
|
39
|
+
| `/api/health` | GET | Health check - `{ status: 'ok' }` |
|
|
40
|
+
| `/api/__sync-schema` | POST | Sync `db/schema.ts` tables to database |
|
|
41
|
+
| `/api/cron` | GET | List cron jobs |
|
|
42
|
+
| `/api/cron` | POST | Create a new cron task |
|
|
43
|
+
|
|
44
|
+
## Creating API Routes
|
|
45
|
+
|
|
46
|
+
Create Route Handler files in `app/api/`:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
// app/api/prices/route.ts
|
|
50
|
+
import { dataApi } from '@surf-ai/sdk/server'
|
|
51
|
+
|
|
52
|
+
export async function GET(request: Request) {
|
|
53
|
+
const { searchParams } = new URL(request.url)
|
|
54
|
+
const symbol = searchParams.get('symbol') || 'BTC'
|
|
55
|
+
const data = await dataApi.market.price({ symbol })
|
|
56
|
+
return Response.json(data)
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Database
|
|
61
|
+
|
|
62
|
+
Define tables in `db/schema.ts` using Drizzle ORM:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { pgTable, serial, text, timestamp } from "drizzle-orm/pg-core";
|
|
66
|
+
export const users = pgTable("users", {
|
|
67
|
+
id: serial("id").primaryKey(),
|
|
68
|
+
name: text("name").notNull(),
|
|
69
|
+
created_at: timestamp("created_at").defaultNow(),
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Tables are auto-synced on server start and when `db/schema.ts` changes in dev mode.
|
|
74
|
+
|
|
75
|
+
## Do NOT modify
|
|
76
|
+
|
|
77
|
+
- `instrumentation.ts` - server boot (schema sync, cron)
|
|
78
|
+
- `next.config.ts` - build/deploy config
|
|
79
|
+
- `db/index.ts` - database connection
|
|
80
|
+
- `lib/boot.ts` - infrastructure (schema sync, cron init)
|
|
81
|
+
- `app/layout.tsx` - root layout and providers
|
|
82
|
+
- `app/providers.tsx` - client-side providers (QueryClient, theme)
|
|
83
|
+
- `eslint.config.mjs` - lint rules
|
|
84
|
+
- `globals.css` - only imports, do not add styles here (use Tailwind classes)
|
|
85
|
+
|
|
86
|
+
## Rules
|
|
87
|
+
|
|
88
|
+
- Add `"use client"` at the top of all components you create
|
|
89
|
+
- Use `fetch('/api/...')` in client components to call API routes
|
|
90
|
+
- Use `@surf-ai/sdk/server` `dataApi` in route handlers when you need Surf data
|
|
91
|
+
- Do not bypass your API routes from client components
|
|
92
|
+
- Packages are pre-installed - check `package.json` before installing
|
|
93
|
+
- Default to a dark theme unless the user explicitly asks for a different visual direction
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import { syncSchema } from '@surf-ai/sdk/db'
|
|
3
|
+
|
|
4
|
+
export async function POST() {
|
|
5
|
+
try {
|
|
6
|
+
await syncSchema({
|
|
7
|
+
schemaPath: path.join(process.cwd(), 'db', 'schema.ts'),
|
|
8
|
+
retries: 2,
|
|
9
|
+
retryDelay: 1500,
|
|
10
|
+
})
|
|
11
|
+
return Response.json({ ok: true })
|
|
12
|
+
} catch (err) {
|
|
13
|
+
return Response.json(
|
|
14
|
+
{ ok: false, error: String(err) },
|
|
15
|
+
{ status: 500 }
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { randomUUID } from 'node:crypto'
|
|
4
|
+
|
|
5
|
+
const CRON_PATH = path.join(process.cwd(), 'cron.json')
|
|
6
|
+
|
|
7
|
+
function readCronConfig() {
|
|
8
|
+
if (!fs.existsSync(CRON_PATH)) return []
|
|
9
|
+
return JSON.parse(fs.readFileSync(CRON_PATH, 'utf-8'))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function writeCronConfig(config: unknown[]) {
|
|
13
|
+
fs.writeFileSync(CRON_PATH, JSON.stringify(config, null, 2))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function GET() {
|
|
17
|
+
return Response.json(readCronConfig())
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function POST(request: Request) {
|
|
21
|
+
const body = await request.json()
|
|
22
|
+
const config = readCronConfig()
|
|
23
|
+
const task = {
|
|
24
|
+
id: randomUUID(),
|
|
25
|
+
...body,
|
|
26
|
+
enabled: body.enabled ?? true,
|
|
27
|
+
}
|
|
28
|
+
config.push(task)
|
|
29
|
+
writeCronConfig(config)
|
|
30
|
+
return Response.json(task, { status: 201 })
|
|
31
|
+
}
|
|
@@ -1,25 +1,9 @@
|
|
|
1
|
-
import { dataApi } from
|
|
2
|
-
import { NextResponse } from "next/server";
|
|
3
|
-
|
|
4
|
-
const TIME_RANGES = ["1d", "7d", "14d", "30d", "90d", "180d", "365d", "max"] as const;
|
|
1
|
+
import { dataApi } from '@surf-ai/sdk/server'
|
|
5
2
|
|
|
6
3
|
export async function GET(request: Request) {
|
|
7
|
-
const { searchParams } = new URL(request.url)
|
|
8
|
-
const symbol = searchParams.get(
|
|
9
|
-
const rawTimeRange = searchParams.get("time_range");
|
|
10
|
-
const timeRange = TIME_RANGES.includes((rawTimeRange || "") as (typeof TIME_RANGES)[number])
|
|
11
|
-
? (rawTimeRange as (typeof TIME_RANGES)[number])
|
|
12
|
-
: "1d";
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
const data = await dataApi.market.price({
|
|
16
|
-
symbol,
|
|
17
|
-
time_range: timeRange,
|
|
18
|
-
});
|
|
4
|
+
const { searchParams } = new URL(request.url)
|
|
5
|
+
const symbol = searchParams.get('symbol') || 'BTC'
|
|
19
6
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
const message = error instanceof Error ? error.message : "Unknown error";
|
|
23
|
-
return NextResponse.json({ error: message }, { status: 500 });
|
|
24
|
-
}
|
|
7
|
+
const data = await dataApi.market.price({ symbol })
|
|
8
|
+
return Response.json(data)
|
|
25
9
|
}
|
|
@@ -1,26 +1,2 @@
|
|
|
1
1
|
@import "tailwindcss";
|
|
2
|
-
|
|
3
|
-
:root {
|
|
4
|
-
--background: #ffffff;
|
|
5
|
-
--foreground: #171717;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
@theme inline {
|
|
9
|
-
--color-background: var(--background);
|
|
10
|
-
--color-foreground: var(--foreground);
|
|
11
|
-
--font-sans: var(--font-geist-sans);
|
|
12
|
-
--font-mono: var(--font-geist-mono);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
@media (prefers-color-scheme: dark) {
|
|
16
|
-
:root {
|
|
17
|
-
--background: #0a0a0a;
|
|
18
|
-
--foreground: #ededed;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
body {
|
|
23
|
-
background: var(--background);
|
|
24
|
-
color: var(--foreground);
|
|
25
|
-
font-family: Arial, Helvetica, sans-serif;
|
|
26
|
-
}
|
|
2
|
+
@import "tw-animate-css";
|
|
@@ -1,33 +1,24 @@
|
|
|
1
|
-
import type { Metadata } from "next"
|
|
2
|
-
import {
|
|
3
|
-
import "./
|
|
4
|
-
|
|
5
|
-
const geistSans = Geist({
|
|
6
|
-
variable: "--font-geist-sans",
|
|
7
|
-
subsets: ["latin"],
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
const geistMono = Geist_Mono({
|
|
11
|
-
variable: "--font-geist-mono",
|
|
12
|
-
subsets: ["latin"],
|
|
13
|
-
});
|
|
1
|
+
import type { Metadata } from "next"
|
|
2
|
+
import { ThemeProvider } from "next-themes"
|
|
3
|
+
import { Providers } from "./providers"
|
|
4
|
+
import "./globals.css"
|
|
14
5
|
|
|
15
6
|
export const metadata: Metadata = {
|
|
16
|
-
title: "
|
|
17
|
-
|
|
18
|
-
};
|
|
7
|
+
title: "Surf App",
|
|
8
|
+
}
|
|
19
9
|
|
|
20
10
|
export default function RootLayout({
|
|
21
11
|
children,
|
|
22
|
-
}:
|
|
23
|
-
children: React.ReactNode
|
|
24
|
-
}
|
|
12
|
+
}: {
|
|
13
|
+
children: React.ReactNode
|
|
14
|
+
}) {
|
|
25
15
|
return (
|
|
26
|
-
<html
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
16
|
+
<html lang="en" suppressHydrationWarning>
|
|
17
|
+
<body>
|
|
18
|
+
<ThemeProvider attribute="class" defaultTheme="dark" enableSystem={false}>
|
|
19
|
+
<Providers>{children}</Providers>
|
|
20
|
+
</ThemeProvider>
|
|
21
|
+
</body>
|
|
31
22
|
</html>
|
|
32
|
-
)
|
|
23
|
+
)
|
|
33
24
|
}
|