create-surf-app 0.1.26-alpha.0 → 1.0.0-alpha.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/chunk-NZOL5GFZ.js +131 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/templates/default/backend/package.json +3 -3
- package/dist/templates/default/backend/server.js +1 -1
- package/dist/templates/default/frontend/src/vite-env.d.ts +0 -9
- package/dist/templates/default/frontend/vite.config.ts +6 -5
- package/dist/templates/default/package.json +11 -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 +23 -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 +6 -3
- package/dist/templates/nextjs/package.json +69 -17
- package/dist/templates/nextjs/postcss.config.mjs +2 -2
- package/dist/templates/nextjs/scripts/check-env.js +50 -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,131 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import os from "os";
|
|
4
|
+
import path from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
var DEFAULT_BACKEND_PORT = "3001";
|
|
7
|
+
var VALID_TEMPLATES = ["vite", "nextjs"];
|
|
8
|
+
async function createSurfApp({
|
|
9
|
+
projectName = ".",
|
|
10
|
+
backendPort = process.env.BACKEND_PORT || DEFAULT_BACKEND_PORT,
|
|
11
|
+
previewBase = process.env.BASE_PATH,
|
|
12
|
+
template: templateArg,
|
|
13
|
+
logger = console.log
|
|
14
|
+
} = {}) {
|
|
15
|
+
const root = path.resolve(projectName);
|
|
16
|
+
const name = path.basename(root);
|
|
17
|
+
const template = validateTemplate(templateArg);
|
|
18
|
+
const validatedBackendPort = validatePort("backend", backendPort);
|
|
19
|
+
const templateDir = resolveTemplateDir(template);
|
|
20
|
+
logger(`
|
|
21
|
+
Creating Surf app (${template}) in ${root}
|
|
22
|
+
`);
|
|
23
|
+
fs.mkdirSync(root, { recursive: true });
|
|
24
|
+
copyDir(templateDir, root, root, logger);
|
|
25
|
+
if (template === "nextjs") {
|
|
26
|
+
writeNextjsEnvFile(root, validatedBackendPort, previewBase);
|
|
27
|
+
finalizePackageName(root, name);
|
|
28
|
+
logger(`
|
|
29
|
+
Done! Next steps:
|
|
30
|
+
|
|
31
|
+
cd ${name}
|
|
32
|
+
npm install
|
|
33
|
+
npm run dev
|
|
34
|
+
|
|
35
|
+
Open http://localhost:3000
|
|
36
|
+
`);
|
|
37
|
+
} else {
|
|
38
|
+
writeEnvFiles(root, validatedBackendPort, previewBase);
|
|
39
|
+
logger(`
|
|
40
|
+
Done! Next steps:
|
|
41
|
+
|
|
42
|
+
cd ${name}
|
|
43
|
+
cd backend && npm install && cd ..
|
|
44
|
+
cd frontend && npm install && cd ..
|
|
45
|
+
|
|
46
|
+
# Start backend
|
|
47
|
+
cd backend && npm run dev &
|
|
48
|
+
|
|
49
|
+
# Start frontend
|
|
50
|
+
cd frontend && npm run dev
|
|
51
|
+
|
|
52
|
+
Open the local URL printed by Vite
|
|
53
|
+
`);
|
|
54
|
+
}
|
|
55
|
+
return root;
|
|
56
|
+
}
|
|
57
|
+
function validateTemplate(template) {
|
|
58
|
+
if (!template) return "vite";
|
|
59
|
+
if (!VALID_TEMPLATES.includes(template)) {
|
|
60
|
+
throw new Error(`Unknown template: ${template}. Valid templates: ${VALID_TEMPLATES.join(", ")}`);
|
|
61
|
+
}
|
|
62
|
+
return template;
|
|
63
|
+
}
|
|
64
|
+
function resolveTemplateDir(template = "vite") {
|
|
65
|
+
const dirName = template === "vite" ? "default" : template;
|
|
66
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
67
|
+
const candidates = [
|
|
68
|
+
path.join(here, "templates", dirName),
|
|
69
|
+
path.join(here, "..", "templates", dirName)
|
|
70
|
+
];
|
|
71
|
+
for (const candidate of candidates) {
|
|
72
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
73
|
+
}
|
|
74
|
+
throw new Error(`Could not find ${template} template near ${here}`);
|
|
75
|
+
}
|
|
76
|
+
function copyDir(src, dest, root, logger) {
|
|
77
|
+
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
|
|
78
|
+
const srcPath = path.join(src, entry.name);
|
|
79
|
+
const destPath = path.join(dest, entry.name);
|
|
80
|
+
if (entry.isDirectory()) {
|
|
81
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
82
|
+
copyDir(srcPath, destPath, root, logger);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
86
|
+
fs.writeFileSync(destPath, fs.readFileSync(srcPath));
|
|
87
|
+
logger(` ${path.relative(root, destPath)}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function validatePort(label, value) {
|
|
91
|
+
const port = Number.parseInt(value, 10);
|
|
92
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
93
|
+
throw new Error(`Invalid ${label} port: ${value}`);
|
|
94
|
+
}
|
|
95
|
+
return String(port);
|
|
96
|
+
}
|
|
97
|
+
function finalizePackageName(root, projectName) {
|
|
98
|
+
const pkgPath = path.join(root, "package.json");
|
|
99
|
+
if (!fs.existsSync(pkgPath)) return;
|
|
100
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
101
|
+
pkg.name = projectName.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "surf-app";
|
|
102
|
+
fs.writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}${os.EOL}`);
|
|
103
|
+
}
|
|
104
|
+
function writeNextjsEnvFile(root, port, basePath) {
|
|
105
|
+
const envPath = path.join(root, ".env");
|
|
106
|
+
const envContent = [
|
|
107
|
+
`FRONTEND_PORT=${port}`,
|
|
108
|
+
`BASE_PATH=${basePath || ""}`,
|
|
109
|
+
"SURF_API_KEY="
|
|
110
|
+
].join(os.EOL);
|
|
111
|
+
fs.writeFileSync(envPath, `${envContent}${os.EOL}`);
|
|
112
|
+
}
|
|
113
|
+
function writeEnvFiles(root, backendPort, previewBase) {
|
|
114
|
+
const backendEnvPath = path.join(root, "backend", ".env");
|
|
115
|
+
const frontendEnvPath = path.join(root, "frontend", ".env");
|
|
116
|
+
const backendEnv = [
|
|
117
|
+
`BACKEND_PORT=${backendPort}`,
|
|
118
|
+
"SURF_API_KEY="
|
|
119
|
+
].join(os.EOL);
|
|
120
|
+
fs.writeFileSync(backendEnvPath, `${backendEnv}${os.EOL}`);
|
|
121
|
+
const frontendEnv = [
|
|
122
|
+
"FRONTEND_PORT=5173",
|
|
123
|
+
`BACKEND_PORT=${backendPort}`,
|
|
124
|
+
`BASE_PATH=${previewBase || ""}`
|
|
125
|
+
].join(os.EOL);
|
|
126
|
+
fs.writeFileSync(frontendEnvPath, `${frontendEnv}${os.EOL}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export {
|
|
130
|
+
createSurfApp
|
|
131
|
+
};
|
package/dist/cli.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"name": "backend",
|
|
3
3
|
"private": true,
|
|
4
4
|
"scripts": {
|
|
5
|
-
"start": "node server.js",
|
|
6
|
-
"dev": "node --watch server.js"
|
|
5
|
+
"start": "node --env-file=.env server.js",
|
|
6
|
+
"dev": "node --env-file=.env --watch server.js"
|
|
7
7
|
},
|
|
8
8
|
"dependencies": {
|
|
9
|
-
"@surf-ai/sdk": "0.1
|
|
9
|
+
"@surf-ai/sdk": "1.0.0-alpha.1",
|
|
10
10
|
"express": "4.22.1"
|
|
11
11
|
}
|
|
12
12
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
const { createServer } = require('@surf-ai/sdk/server')
|
|
2
|
-
createServer(
|
|
2
|
+
createServer().start()
|
|
@@ -5,7 +5,7 @@ import tailwindcss from '@tailwindcss/vite'
|
|
|
5
5
|
|
|
6
6
|
function readRequiredPort(
|
|
7
7
|
env: Record<string, string>,
|
|
8
|
-
name: '
|
|
8
|
+
name: 'BACKEND_PORT' | 'FRONTEND_PORT',
|
|
9
9
|
) {
|
|
10
10
|
const value = env[name]
|
|
11
11
|
const port = Number.parseInt(value || '', 10)
|
|
@@ -16,9 +16,10 @@ function readRequiredPort(
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
export default defineConfig(({ mode }) => {
|
|
19
|
-
const env = loadEnv(mode, process.cwd())
|
|
20
|
-
const
|
|
21
|
-
const
|
|
19
|
+
const env = loadEnv(mode, process.cwd(), '')
|
|
20
|
+
const frontendPort = readRequiredPort(env, 'FRONTEND_PORT')
|
|
21
|
+
const backendPort = readRequiredPort(env, 'BACKEND_PORT')
|
|
22
|
+
const base = env.BASE_PATH || './'
|
|
22
23
|
const hasAbsBase = base.startsWith('/')
|
|
23
24
|
const apiBasePrefix = hasAbsBase ? base.replace(/\/$/, '') : ''
|
|
24
25
|
|
|
@@ -33,8 +34,8 @@ export default defineConfig(({ mode }) => {
|
|
|
33
34
|
return {
|
|
34
35
|
plugins: [react(), tailwindcss()],
|
|
35
36
|
server: {
|
|
36
|
-
port: readRequiredPort(env, 'VITE_PORT'),
|
|
37
37
|
host: '0.0.0.0',
|
|
38
|
+
port: frontendPort,
|
|
38
39
|
proxy: {
|
|
39
40
|
[`${apiBasePrefix}/api`]: backendProxy,
|
|
40
41
|
},
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "surf-app",
|
|
3
|
+
"private": true,
|
|
4
|
+
"scripts": {
|
|
5
|
+
"dev": "concurrently \"npm run dev --prefix backend\" \"npm run dev --prefix frontend\"",
|
|
6
|
+
"install:all": "cd backend && npm install && cd ../frontend && npm install"
|
|
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
|
}
|
|
@@ -1,65 +1,25 @@
|
|
|
1
|
-
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
const SURF_LOGO_PATH = "M14.6875 13.333C15.0977 13.333 15.4859 13.5196 15.7422 13.8398L17.8721 16.502C17.9009 16.538 17.9308 16.5721 17.9619 16.6035C18.3091 16.9542 18.7529 17.3405 18.7529 17.834C18.7528 18.3399 18.3428 18.75 17.8369 18.75H17.5713C17.2123 18.75 16.8727 18.5869 16.6484 18.3066L15.4678 16.8311C14.761 15.9479 13.3369 16.4478 13.3369 17.5791V18.6494C13.3369 18.7062 13.2882 18.75 13.2314 18.75H11.3584C10.7127 18.75 10.1016 18.4564 9.69824 17.9521L8.80176 16.8311C8.09497 15.9477 6.66993 16.4478 6.66992 17.5791V18.6494C6.66991 18.7062 6.62124 18.75 6.56445 18.75H4.69141C4.04589 18.75 3.43548 18.4562 3.03223 17.9521L2.13477 16.8311C1.77885 16.3862 1.25293 15.931 1.25293 15.3613V13.4395C1.253 13.3822 1.29729 13.3331 1.35449 13.333C1.7646 13.3332 2.15295 13.5196 2.40918 13.8398L4.53809 16.502C5.24478 17.3853 6.66962 16.886 6.66992 15.7549V14.6836C6.67018 13.9379 7.27483 13.3332 8.02051 13.333C8.43065 13.333 8.81884 13.5197 9.0752 13.8398L11.2051 16.502C11.9097 17.3827 13.3282 16.8887 13.3369 15.7646V14.6836C13.3372 13.9378 13.9417 13.3331 14.6875 13.333ZM14.6875 6.66699C15.0978 6.66702 15.4859 6.85347 15.7422 7.17383L17.8721 9.83594C18.2276 10.2801 18.7526 10.7339 18.7529 11.3027V13.2266C18.7528 13.2833 18.7091 13.333 18.6523 13.333C18.2421 13.3329 17.8539 13.1465 17.5977 12.8262L15.4678 10.1641C14.7609 9.28113 13.3369 9.78089 13.3369 10.9121V11.9824C13.3369 12.7282 12.7321 13.3328 11.9863 13.333C11.5761 13.3329 11.187 13.1465 10.9307 12.8262L8.80176 10.1641C8.09497 9.28072 6.66993 9.78076 6.66992 10.9121V11.9824C6.66992 12.7284 6.06525 13.3329 5.31934 13.333C4.90908 13.3329 4.52094 13.1465 4.26465 12.8262L2.13477 10.1641C1.7788 9.71925 1.25293 9.26403 1.25293 8.69434V6.77344C1.253 6.71621 1.29728 6.6671 1.35449 6.66699C1.76461 6.66714 2.15295 6.85359 2.40918 7.17383L4.53809 9.83594C5.24487 10.7193 6.66989 10.2192 6.66992 9.08789V8.01758C6.66992 7.27168 7.27467 6.66722 8.02051 6.66699C8.43082 6.66699 8.81885 6.85345 9.0752 7.17383L11.2051 9.83594C11.9097 10.7163 13.3279 10.2223 13.3369 9.09863V8.01758C13.3369 7.27161 13.9416 6.6671 14.6875 6.66699ZM2.43555 1.25C2.79488 1.25 3.13499 1.41367 3.35938 1.69434L4.53809 3.16895C5.24485 4.05241 6.66991 3.55228 6.66992 2.4209V1.35059C6.66993 1.29391 6.71874 1.25017 6.77539 1.25H8.64844C9.29394 1.25 9.90438 1.5438 10.3076 2.04785L11.2051 3.16895C11.9097 4.04967 13.3282 3.55564 13.3369 2.43164V1.35059C13.3369 1.29391 13.3857 1.25017 13.4424 1.25H15.3154C15.9609 1.25 16.5714 1.5438 16.9746 2.04785L17.8721 3.16895C18.2275 3.61314 18.7526 4.06689 18.7529 4.63574V6.56055C18.7528 6.61728 18.7091 6.66699 18.6523 6.66699C18.2421 6.66688 17.8539 6.4795 17.5977 6.15918L15.4678 3.49805C14.761 2.61474 13.3371 3.11388 13.3369 4.24512V5.31543C13.3369 6.06126 12.7321 6.66673 11.9863 6.66699C11.5761 6.66693 11.187 6.47955 10.9307 6.15918L8.80176 3.49805C8.095 2.61474 6.67006 3.11388 6.66992 4.24512V5.31543C6.66992 6.06137 6.06525 6.66691 5.31934 6.66699C4.90908 6.66693 4.52094 6.47955 4.26465 6.15918L2.13477 3.49805C2.10558 3.46158 2.07543 3.42725 2.04395 3.39551C1.6966 3.04528 1.25293 2.6583 1.25293 2.16504C1.25317 1.65963 1.66348 1.25 2.16895 1.25H2.43555Z"
|
|
4
|
+
|
|
5
|
+
function SurfLogo() {
|
|
6
|
+
return (
|
|
7
|
+
<svg width="40" height="40" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
8
|
+
<path d={SURF_LOGO_PATH} fill="var(--fg-base, #212121)" />
|
|
9
|
+
</svg>
|
|
10
|
+
)
|
|
11
|
+
}
|
|
2
12
|
|
|
3
13
|
export default function Home() {
|
|
4
14
|
return (
|
|
5
|
-
<div className="flex flex-col
|
|
6
|
-
<
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
priority
|
|
14
|
-
/>
|
|
15
|
-
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
|
16
|
-
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
|
17
|
-
To get started, edit the page.tsx file.
|
|
18
|
-
</h1>
|
|
19
|
-
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
|
20
|
-
Looking for a starting point or more instructions? Head over to{" "}
|
|
21
|
-
<a
|
|
22
|
-
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
23
|
-
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
24
|
-
>
|
|
25
|
-
Templates
|
|
26
|
-
</a>{" "}
|
|
27
|
-
or the{" "}
|
|
28
|
-
<a
|
|
29
|
-
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
30
|
-
className="font-medium text-zinc-950 dark:text-zinc-50"
|
|
31
|
-
>
|
|
32
|
-
Learning
|
|
33
|
-
</a>{" "}
|
|
34
|
-
center.
|
|
35
|
-
</p>
|
|
36
|
-
</div>
|
|
37
|
-
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
|
38
|
-
<a
|
|
39
|
-
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
|
40
|
-
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
41
|
-
target="_blank"
|
|
42
|
-
rel="noopener noreferrer"
|
|
43
|
-
>
|
|
44
|
-
<Image
|
|
45
|
-
className="dark:invert"
|
|
46
|
-
src="/vercel.svg"
|
|
47
|
-
alt="Vercel logomark"
|
|
48
|
-
width={16}
|
|
49
|
-
height={16}
|
|
50
|
-
/>
|
|
51
|
-
Deploy Now
|
|
52
|
-
</a>
|
|
53
|
-
<a
|
|
54
|
-
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
|
55
|
-
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
|
56
|
-
target="_blank"
|
|
57
|
-
rel="noopener noreferrer"
|
|
58
|
-
>
|
|
59
|
-
Documentation
|
|
60
|
-
</a>
|
|
61
|
-
</div>
|
|
62
|
-
</main>
|
|
15
|
+
<div className="min-h-screen flex flex-col items-center justify-center gap-4 rounded-[14px]" style={{ background: 'var(--bg-chat-nav, #f4f4f4)', border: '1px solid var(--border-strong, rgba(42,42,42,0.08))' }}>
|
|
16
|
+
<SurfLogo />
|
|
17
|
+
<h1 className="font-black text-[36px] leading-[44px] text-[var(--fg-base,#212121)]">
|
|
18
|
+
GM, Builder<span style={{ color: 'var(--brand-100, #ff2882)' }}>.</span>
|
|
19
|
+
</h1>
|
|
20
|
+
<p className="text-xl leading-7 text-[var(--fg-subtle,#7a7a7a)]">
|
|
21
|
+
Start your crypto project here!
|
|
22
|
+
</p>
|
|
63
23
|
</div>
|
|
64
|
-
)
|
|
24
|
+
)
|
|
65
25
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
|
|
4
|
+
import { useState } from "react"
|
|
5
|
+
|
|
6
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
7
|
+
const [queryClient] = useState(
|
|
8
|
+
() =>
|
|
9
|
+
new QueryClient({
|
|
10
|
+
defaultOptions: {
|
|
11
|
+
queries: {
|
|
12
|
+
refetchOnWindowFocus: false,
|
|
13
|
+
retry: 3,
|
|
14
|
+
staleTime: 30 * 1000,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
22
|
+
)
|
|
23
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import * as AccordionPrimitive from "@radix-ui/react-accordion"
|
|
3
|
+
import { ChevronDown } from "lucide-react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "@/lib/utils"
|
|
6
|
+
|
|
7
|
+
const Accordion = AccordionPrimitive.Root
|
|
8
|
+
|
|
9
|
+
const AccordionItem = React.forwardRef<
|
|
10
|
+
React.ElementRef<typeof AccordionPrimitive.Item>,
|
|
11
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Item>
|
|
12
|
+
>(({ className, ...props }, ref) => (
|
|
13
|
+
<AccordionPrimitive.Item
|
|
14
|
+
ref={ref}
|
|
15
|
+
className={cn("border-b", className)}
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
))
|
|
19
|
+
AccordionItem.displayName = "AccordionItem"
|
|
20
|
+
|
|
21
|
+
const AccordionTrigger = React.forwardRef<
|
|
22
|
+
React.ElementRef<typeof AccordionPrimitive.Trigger>,
|
|
23
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Trigger>
|
|
24
|
+
>(({ className, children, ...props }, ref) => (
|
|
25
|
+
<AccordionPrimitive.Header className="flex">
|
|
26
|
+
<AccordionPrimitive.Trigger
|
|
27
|
+
ref={ref}
|
|
28
|
+
className={cn(
|
|
29
|
+
"flex flex-1 items-center justify-between py-4 text-sm font-medium transition-all hover:underline text-left [&[data-state=open]>svg]:rotate-180",
|
|
30
|
+
className
|
|
31
|
+
)}
|
|
32
|
+
{...props}
|
|
33
|
+
>
|
|
34
|
+
{children}
|
|
35
|
+
<ChevronDown className="h-4 w-4 shrink-0 text-muted-foreground transition-transform duration-200" />
|
|
36
|
+
</AccordionPrimitive.Trigger>
|
|
37
|
+
</AccordionPrimitive.Header>
|
|
38
|
+
))
|
|
39
|
+
AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName
|
|
40
|
+
|
|
41
|
+
const AccordionContent = React.forwardRef<
|
|
42
|
+
React.ElementRef<typeof AccordionPrimitive.Content>,
|
|
43
|
+
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
|
|
44
|
+
>(({ className, children, ...props }, ref) => (
|
|
45
|
+
<AccordionPrimitive.Content
|
|
46
|
+
ref={ref}
|
|
47
|
+
className="overflow-hidden text-sm data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
|
|
48
|
+
{...props}
|
|
49
|
+
>
|
|
50
|
+
<div className={cn("pb-4 pt-0", className)}>{children}</div>
|
|
51
|
+
</AccordionPrimitive.Content>
|
|
52
|
+
))
|
|
53
|
+
AccordionContent.displayName = AccordionPrimitive.Content.displayName
|
|
54
|
+
|
|
55
|
+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }
|