@alstar/studio 0.0.0-beta.1 → 0.0.0-beta.11
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/api/api-key.ts +69 -0
- package/api/auth.ts +66 -0
- package/api/backup.ts +40 -0
- package/api/block.ts +131 -66
- package/api/index.ts +19 -1
- package/api/mcp.ts +50 -0
- package/components/AdminPanel.ts +87 -0
- package/components/Backup.ts +13 -0
- package/components/BlockFieldRenderer.ts +125 -0
- package/components/BlockRenderer.ts +22 -0
- package/components/Entries.ts +20 -12
- package/components/Entry.ts +13 -21
- package/components/FieldRenderer.ts +35 -0
- package/components/Render.ts +46 -0
- package/components/Settings.ts +104 -0
- package/components/SiteLayout.ts +61 -0
- package/components/Users.ts +46 -0
- package/components/fields/Markdown.ts +44 -0
- package/components/fields/Slug.ts +113 -0
- package/components/fields/Text.ts +42 -0
- package/components/fields/index.ts +7 -0
- package/components/icons.ts +136 -7
- package/index.ts +94 -34
- package/package.json +10 -7
- package/pages/entry/[id].ts +15 -0
- package/pages/error.ts +14 -0
- package/{components → pages}/index.ts +7 -4
- package/pages/login.ts +21 -0
- package/pages/register.ts +33 -0
- package/pages/settings.ts +8 -0
- package/public/studio/css/admin-panel.css +103 -0
- package/public/studio/css/blocks-field.css +53 -0
- package/public/studio/css/settings.css +28 -0
- package/public/studio/js/markdown-editor.js +34 -0
- package/public/studio/js/sortable-list.js +50 -0
- package/public/studio/main.css +166 -0
- package/public/studio/main.js +21 -0
- package/queries/block-2.ts +339 -0
- package/queries/block-with-children.ts +74 -0
- package/queries/block.ts +289 -0
- package/queries/db-types.ts +15 -0
- package/queries/getBlockTrees-2.ts +71 -0
- package/queries/getBlockTrees.ts +316 -0
- package/queries/getBlocks.ts +214 -0
- package/queries/index.ts +2 -98
- package/queries/structure-types.ts +97 -0
- package/readme.md +205 -0
- package/schema.sql +18 -0
- package/schemas.ts +23 -52
- package/types.ts +144 -5
- package/utils/auth.ts +54 -0
- package/utils/buildBlocksTree.ts +4 -4
- package/utils/create-hash.ts +9 -0
- package/utils/define.ts +39 -0
- package/utils/file-based-router.ts +11 -2
- package/utils/get-config.ts +8 -9
- package/utils/get-or-create-row.ts +41 -0
- package/utils/html.ts +247 -0
- package/utils/startup-log.ts +19 -0
- package/components/AdminPanel/AdminPanel.css +0 -59
- package/components/AdminPanel/AdminPanel.ts +0 -57
- package/components/Block.ts +0 -116
- package/components/Entry.css +0 -7
- package/components/Field.ts +0 -164
- package/components/Fields.ts +0 -43
- package/components/layout.ts +0 -53
- package/public/main.css +0 -92
- package/public/main.js +0 -43
- /package/public/{favicon.svg → studio/favicon.svg} +0 -0
package/api/api-key.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type HttpBindings } from '@hono/node-server'
|
|
2
|
+
import { ServerSentEventGenerator } from '@starfederation/datastar-sdk'
|
|
3
|
+
import { Hono } from 'hono'
|
|
4
|
+
import { streamSSE } from 'hono/streaming'
|
|
5
|
+
import { db } from '@alstar/db'
|
|
6
|
+
import crypto from 'node:crypto'
|
|
7
|
+
|
|
8
|
+
import { stripNewlines } from '../utils/strip-newlines.ts'
|
|
9
|
+
import { sql } from '../utils/sql.ts'
|
|
10
|
+
import Settings from '../components/Settings.ts'
|
|
11
|
+
import { createHash } from '../utils/create-hash.ts'
|
|
12
|
+
|
|
13
|
+
const app = new Hono<{ Bindings: HttpBindings }>()
|
|
14
|
+
|
|
15
|
+
app.post('/api-key', async (c) => {
|
|
16
|
+
return streamSSE(c, async (stream) => {
|
|
17
|
+
const formData = await c.req.formData()
|
|
18
|
+
const data = Object.fromEntries(formData.entries())
|
|
19
|
+
|
|
20
|
+
if (!data) return
|
|
21
|
+
|
|
22
|
+
const apiKey = crypto.randomUUID()
|
|
23
|
+
|
|
24
|
+
const hash = createHash(apiKey)
|
|
25
|
+
|
|
26
|
+
const xs = (length: number) => '*'.repeat(length)
|
|
27
|
+
|
|
28
|
+
db.insertInto('api_keys', {
|
|
29
|
+
name: data.name?.toString(),
|
|
30
|
+
value: hash,
|
|
31
|
+
hint: `${apiKey.substring(0, 8)}-${xs(4)}-${xs(4)}-${xs(4)}-${xs(12)}`,
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
await stream.writeSSE({
|
|
35
|
+
event: 'datastar-patch-signals',
|
|
36
|
+
data: `signals { apiKey: '${apiKey}', name: '' }`,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
await stream.writeSSE({
|
|
40
|
+
event: 'datastar-patch-elements',
|
|
41
|
+
data: `elements ${stripNewlines(Settings())}`,
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
app.delete('/api-key', async (c) => {
|
|
47
|
+
return streamSSE(c, async (stream) => {
|
|
48
|
+
const formData = await c.req.formData()
|
|
49
|
+
|
|
50
|
+
const value = formData.get('value')?.toString()
|
|
51
|
+
|
|
52
|
+
if (!value) return
|
|
53
|
+
|
|
54
|
+
db.database
|
|
55
|
+
.prepare(sql`
|
|
56
|
+
delete from api_keys
|
|
57
|
+
where
|
|
58
|
+
value = ?
|
|
59
|
+
`)
|
|
60
|
+
.run(value)
|
|
61
|
+
|
|
62
|
+
await stream.writeSSE({
|
|
63
|
+
event: 'datastar-patch-elements',
|
|
64
|
+
data: `elements ${stripNewlines(Settings())}`,
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
export const apiKeyRoutes = app
|
package/api/auth.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { type HttpBindings } from '@hono/node-server'
|
|
2
|
+
import { Hono } from 'hono'
|
|
3
|
+
import { createHash } from '../utils/create-hash.ts'
|
|
4
|
+
import { db } from '@alstar/db'
|
|
5
|
+
import { streamSSE } from 'hono/streaming'
|
|
6
|
+
import { sql } from '../utils/sql.ts'
|
|
7
|
+
import { setCookie } from 'hono/cookie'
|
|
8
|
+
|
|
9
|
+
const app = new Hono<{ Bindings: HttpBindings }>()
|
|
10
|
+
|
|
11
|
+
app.post('/register', async (c) => {
|
|
12
|
+
return streamSSE(c, async (stream) => {
|
|
13
|
+
const formData = await c.req.formData()
|
|
14
|
+
const data = Object.fromEntries(formData.entries())
|
|
15
|
+
|
|
16
|
+
if (!data || !data.email || !data.password) return
|
|
17
|
+
|
|
18
|
+
const hash = createHash(data.password.toString())
|
|
19
|
+
|
|
20
|
+
db.insertInto('users', {
|
|
21
|
+
email: data.email?.toString(),
|
|
22
|
+
hash: hash,
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
await stream.writeSSE({
|
|
26
|
+
event: 'datastar-patch-signals',
|
|
27
|
+
data: `signals { status: 200, message: 'User "${data.email}" created successfully' }`,
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
app.post('/login', async (c) => {
|
|
33
|
+
const formData = await c.req.formData()
|
|
34
|
+
const data = Object.fromEntries(formData.entries())
|
|
35
|
+
|
|
36
|
+
if (!data || !data.email || !data.password) return
|
|
37
|
+
|
|
38
|
+
const user = db.database
|
|
39
|
+
.prepare(sql`
|
|
40
|
+
select
|
|
41
|
+
*
|
|
42
|
+
from
|
|
43
|
+
users
|
|
44
|
+
where
|
|
45
|
+
email = ?
|
|
46
|
+
`)
|
|
47
|
+
.get(data.email.toString())
|
|
48
|
+
|
|
49
|
+
if (!user) {
|
|
50
|
+
return c.json({ status: 404, message: 'No user with that email' })
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const passwordHash = createHash(data.password.toString())
|
|
54
|
+
|
|
55
|
+
if (passwordHash !== user.hash) {
|
|
56
|
+
return c.json({ status: 401, message: 'Wrong password' })
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
setCookie(c, 'login', 'yes')
|
|
60
|
+
|
|
61
|
+
return c.redirect('/studio')
|
|
62
|
+
|
|
63
|
+
// return c.json({ status: 200, message: 'Logged in!' })
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
export const authRoutes = app
|
package/api/backup.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fsp from 'node:fs/promises'
|
|
2
|
+
import { backup } from 'node:sqlite'
|
|
3
|
+
|
|
4
|
+
import { Hono } from 'hono'
|
|
5
|
+
import { type HttpBindings } from '@hono/node-server'
|
|
6
|
+
import { streamSSE } from 'hono/streaming'
|
|
7
|
+
import { db } from '@alstar/db'
|
|
8
|
+
|
|
9
|
+
const app = new Hono<{ Bindings: HttpBindings }>()
|
|
10
|
+
|
|
11
|
+
app.post('/backup', async (c) => {
|
|
12
|
+
const date = new Date()
|
|
13
|
+
const name = `./backups/backup-${date.toISOString()}.db`
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
fsp.mkdir('./backups', { recursive: true })
|
|
17
|
+
|
|
18
|
+
await backup(db.database, name)
|
|
19
|
+
|
|
20
|
+
console.log('Backup')
|
|
21
|
+
|
|
22
|
+
return streamSSE(c, async (stream) => {
|
|
23
|
+
await stream.writeSSE({
|
|
24
|
+
event: 'datastar-patch-signals',
|
|
25
|
+
data: `signals { status: 200, message: '${name} created' }`,
|
|
26
|
+
})
|
|
27
|
+
})
|
|
28
|
+
} catch (error) {
|
|
29
|
+
console.log(error)
|
|
30
|
+
|
|
31
|
+
return streamSSE(c, async (stream) => {
|
|
32
|
+
await stream.writeSSE({
|
|
33
|
+
event: 'datastar-patch-signals',
|
|
34
|
+
data: `signals { status: 500, message: '${name} failed' }`,
|
|
35
|
+
})
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
export const backupRoutes = app
|
package/api/block.ts
CHANGED
|
@@ -1,97 +1,162 @@
|
|
|
1
1
|
import { type HttpBindings } from '@hono/node-server'
|
|
2
|
-
|
|
2
|
+
import { ServerSentEventGenerator } from '@starfederation/datastar-sdk'
|
|
3
3
|
import { Hono } from 'hono'
|
|
4
4
|
import { streamSSE } from 'hono/streaming'
|
|
5
5
|
import { stripNewlines } from '../utils/strip-newlines.ts'
|
|
6
6
|
import { sql } from '../utils/sql.ts'
|
|
7
7
|
import { type Structure } from '../types.ts'
|
|
8
8
|
import { db } from '@alstar/db'
|
|
9
|
-
import Entries from '../components/Entries.ts'
|
|
10
9
|
import Entry from '../components/Entry.ts'
|
|
10
|
+
import {
|
|
11
|
+
blockWithChildren,
|
|
12
|
+
deleteBlockWithChildren,
|
|
13
|
+
} from '../queries/block-with-children.ts'
|
|
14
|
+
import AdminPanel from '../components/AdminPanel.ts'
|
|
15
|
+
import { query } from '../queries/index.ts'
|
|
16
|
+
import { studioStructure } from '../index.ts'
|
|
11
17
|
|
|
12
|
-
|
|
13
|
-
const app = new Hono<{ Bindings: HttpBindings }>()
|
|
18
|
+
const app = new Hono<{ Bindings: HttpBindings }>()
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
app.post('/block', async (c) => {
|
|
21
|
+
return streamSSE(c, async (stream) => {
|
|
22
|
+
const formData = await c.req.formData()
|
|
23
|
+
const data = Object.fromEntries(formData.entries())
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
const row = studioStructure[data.name?.toString()]
|
|
21
26
|
|
|
22
|
-
|
|
27
|
+
if (!row) return
|
|
23
28
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
+
db.insertInto('blocks', {
|
|
30
|
+
name: data.name?.toString(),
|
|
31
|
+
label: row.label,
|
|
32
|
+
type: row.type,
|
|
33
|
+
})
|
|
29
34
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
})
|
|
35
|
+
await stream.writeSSE({
|
|
36
|
+
event: 'datastar-patch-elements',
|
|
37
|
+
data: `elements ${stripNewlines(AdminPanel())}`,
|
|
34
38
|
})
|
|
35
39
|
})
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
app.post('/new-block', async (c) => {
|
|
43
|
+
return streamSSE(c, async (stream) => {
|
|
44
|
+
const formData = await c.req.formData()
|
|
45
|
+
const data = Object.fromEntries(formData)
|
|
46
|
+
|
|
47
|
+
db.insertInto('blocks', {
|
|
48
|
+
type: data.type.toString(),
|
|
49
|
+
name: data.name.toString(),
|
|
50
|
+
label: data.label.toString(),
|
|
51
|
+
parent_id: data.parent_id.toString(),
|
|
52
|
+
sort_order: data.sort_order.toString(),
|
|
53
|
+
})
|
|
36
54
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
55
|
+
await stream.writeSSE({
|
|
56
|
+
event: 'datastar-patch-elements',
|
|
57
|
+
data: `elements ${stripNewlines(Entry({ entryId: parseInt(data.entry_id.toString()) }))}`,
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
app.patch('/block', async (c) => {
|
|
63
|
+
return streamSSE(c, async (stream) => {
|
|
64
|
+
const formData = await c.req.formData()
|
|
65
|
+
|
|
66
|
+
const id = formData.get('id')?.toString()
|
|
67
|
+
const value = formData.get('value')?.toString()
|
|
68
|
+
const name = formData.get('name')?.toString()
|
|
69
|
+
const entryId = formData.get('entryId')?.toString()
|
|
70
|
+
const parentId = formData.get('parentId')?.toString()
|
|
71
|
+
// const sortOrder = formData.get('sort_order')?.toString()
|
|
72
|
+
|
|
73
|
+
if (!id || !value) return
|
|
74
|
+
|
|
75
|
+
const transaction = db.database.prepare(sql`
|
|
76
|
+
update blocks
|
|
77
|
+
set
|
|
78
|
+
value = ?
|
|
79
|
+
where
|
|
80
|
+
id = ?;
|
|
81
|
+
`)
|
|
82
|
+
|
|
83
|
+
transaction.run(value, id)
|
|
84
|
+
|
|
85
|
+
if (entryId === parentId && name?.toString() === 'title') {
|
|
86
|
+
const rootBlock = query.block({
|
|
87
|
+
id: parentId?.toString() || null,
|
|
88
|
+
})
|
|
43
89
|
|
|
44
|
-
|
|
45
|
-
|
|
90
|
+
if (rootBlock.type !== 'single') {
|
|
91
|
+
await stream.writeSSE({
|
|
92
|
+
event: 'datastar-patch-elements',
|
|
93
|
+
data: `elements <a href="/studio/entry/${entryId}" id="block_link_${entryId}">${value}</a>`,
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
})
|
|
46
99
|
|
|
47
|
-
|
|
100
|
+
app.patch('/value', async (c) => {
|
|
101
|
+
const body = await c.req.json()
|
|
48
102
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
103
|
+
const transaction = db.database.prepare(sql`
|
|
104
|
+
update blocks
|
|
105
|
+
set
|
|
106
|
+
value = ?
|
|
107
|
+
where
|
|
108
|
+
id = ?;
|
|
109
|
+
`)
|
|
110
|
+
|
|
111
|
+
transaction.run(body.value, body.id)
|
|
112
|
+
|
|
113
|
+
return c.json({ status: 200, message: 'success' })
|
|
114
|
+
})
|
|
56
115
|
|
|
116
|
+
app.delete('/block', async (c) => {
|
|
117
|
+
return streamSSE(c, async (stream) => {
|
|
118
|
+
const formData = await c.req.formData()
|
|
119
|
+
|
|
120
|
+
const id = formData.get('id')?.toString()
|
|
121
|
+
const entryId = formData.get('entry_id')?.toString()
|
|
122
|
+
|
|
123
|
+
if (!id) return
|
|
124
|
+
|
|
125
|
+
const transaction = db.database.prepare(deleteBlockWithChildren)
|
|
126
|
+
|
|
127
|
+
transaction.all(id)
|
|
128
|
+
|
|
129
|
+
if (entryId) {
|
|
57
130
|
await stream.writeSSE({
|
|
58
131
|
event: 'datastar-patch-elements',
|
|
59
|
-
data: `elements ${stripNewlines(Entry({ entryId:
|
|
132
|
+
data: `elements ${stripNewlines(Entry({ entryId: parseInt(entryId.toString()) }))}`,
|
|
60
133
|
})
|
|
61
|
-
}
|
|
134
|
+
} else {
|
|
135
|
+
await stream.writeSSE({
|
|
136
|
+
event: 'datastar-patch-elements',
|
|
137
|
+
data: `elements ${stripNewlines(AdminPanel())}`,
|
|
138
|
+
})
|
|
139
|
+
}
|
|
62
140
|
})
|
|
141
|
+
})
|
|
63
142
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const id = formData.get('id')?.toString()
|
|
69
|
-
const value = formData.get('value')?.toString()
|
|
70
|
-
const entryId = formData.get('entryId')?.toString()
|
|
71
|
-
const parentId = formData.get('parentId')?.toString()
|
|
72
|
-
const sortOrder = formData.get('sort_order')?.toString()
|
|
143
|
+
app.post('/sort-order', async (c) => {
|
|
144
|
+
const id = c.req.query('id')
|
|
145
|
+
const sortOrder = c.req.query('sort-order')
|
|
73
146
|
|
|
74
|
-
|
|
147
|
+
if (!id || !sortOrder) return
|
|
75
148
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
`)
|
|
149
|
+
const transaction = db.database.prepare(sql`
|
|
150
|
+
update blocks
|
|
151
|
+
set
|
|
152
|
+
sort_order = ?
|
|
153
|
+
where
|
|
154
|
+
id = ?
|
|
155
|
+
`)
|
|
84
156
|
|
|
85
|
-
|
|
157
|
+
transaction.run(sortOrder, id)
|
|
86
158
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
event: 'datastar-patch-elements',
|
|
90
|
-
data: `elements <a href="/admin/entry/${entryId}" id="block_link_${entryId}">${value}</a>`,
|
|
91
|
-
})
|
|
92
|
-
}
|
|
93
|
-
})
|
|
94
|
-
})
|
|
159
|
+
return c.json({ status: 200, message: 'success' })
|
|
160
|
+
})
|
|
95
161
|
|
|
96
|
-
|
|
97
|
-
}
|
|
162
|
+
export const blockRoutes = app
|
package/api/index.ts
CHANGED
|
@@ -1,2 +1,20 @@
|
|
|
1
|
-
|
|
1
|
+
import { Hono } from 'hono'
|
|
2
2
|
|
|
3
|
+
import { blockRoutes } from './block.ts'
|
|
4
|
+
import { apiKeyRoutes } from './api-key.ts'
|
|
5
|
+
import { backupRoutes } from './backup.ts'
|
|
6
|
+
import { authRoutes } from './auth.ts'
|
|
7
|
+
import { fieldRoutes } from '../components/fields/index.ts'
|
|
8
|
+
|
|
9
|
+
const routes = new Hono()
|
|
10
|
+
|
|
11
|
+
routes.route('/', blockRoutes)
|
|
12
|
+
routes.route('/', apiKeyRoutes)
|
|
13
|
+
routes.route('/', backupRoutes)
|
|
14
|
+
routes.route('/auth', authRoutes)
|
|
15
|
+
|
|
16
|
+
fieldRoutes.forEach((fieldRoute) => {
|
|
17
|
+
routes.route('/field', fieldRoute)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export const apiRoutes = routes
|
package/api/mcp.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type HttpBindings } from '@hono/node-server'
|
|
2
|
+
import { Hono } from 'hono'
|
|
3
|
+
import { sql } from '../utils/sql.ts'
|
|
4
|
+
import { db } from '@alstar/db'
|
|
5
|
+
import { bearerAuth } from 'hono/bearer-auth'
|
|
6
|
+
import crypto from 'node:crypto'
|
|
7
|
+
|
|
8
|
+
const app = new Hono<{ Bindings: HttpBindings }>()
|
|
9
|
+
|
|
10
|
+
app.use(
|
|
11
|
+
'/*',
|
|
12
|
+
bearerAuth({
|
|
13
|
+
verifyToken: async (token, c) => {
|
|
14
|
+
const hash = crypto.createHash('sha256')
|
|
15
|
+
|
|
16
|
+
hash.update(token)
|
|
17
|
+
|
|
18
|
+
const digest = hash.digest().toString('base64')
|
|
19
|
+
|
|
20
|
+
const exists = db.database
|
|
21
|
+
.prepare(sql`
|
|
22
|
+
select
|
|
23
|
+
value
|
|
24
|
+
from
|
|
25
|
+
api_keys
|
|
26
|
+
where
|
|
27
|
+
value = ?
|
|
28
|
+
`)
|
|
29
|
+
.get(digest)
|
|
30
|
+
|
|
31
|
+
return !!exists
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
app.get('/entry', async (c) => {
|
|
37
|
+
return c.json({
|
|
38
|
+
status: 'success',
|
|
39
|
+
message: 'Response from MCP server!',
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
app.post('/entry', async (c) => {
|
|
44
|
+
return c.json({
|
|
45
|
+
status: 'success',
|
|
46
|
+
message: 'New entry created!',
|
|
47
|
+
})
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
export const mcpRoutes = app
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { html } from 'hono/html'
|
|
2
|
+
import { logo } from './icons.ts'
|
|
3
|
+
import Entries from './Entries.ts'
|
|
4
|
+
import * as icons from './icons.ts'
|
|
5
|
+
import { studioStructure } from '../index.ts'
|
|
6
|
+
import { getOrCreateRow } from '../utils/get-or-create-row.ts'
|
|
7
|
+
|
|
8
|
+
export default () => {
|
|
9
|
+
const entries = Object.entries(studioStructure)
|
|
10
|
+
|
|
11
|
+
return html`
|
|
12
|
+
<div class="admin-panel" id="admin_panel">
|
|
13
|
+
<h1>
|
|
14
|
+
<a href="/studio" aria-label="Go to dashboard"> ${logo} </a>
|
|
15
|
+
</h1>
|
|
16
|
+
|
|
17
|
+
<aside style="width: 100%;">
|
|
18
|
+
${entries.map(([name, block]) => {
|
|
19
|
+
if (block.type === 'single') {
|
|
20
|
+
const data = getOrCreateRow({ parentId: null, name, field: block })
|
|
21
|
+
|
|
22
|
+
return html`
|
|
23
|
+
<section id="entries">
|
|
24
|
+
<ul>
|
|
25
|
+
<li>
|
|
26
|
+
<a
|
|
27
|
+
href="/studio/entry/${data.id}"
|
|
28
|
+
id="block_link_${data.id}"
|
|
29
|
+
>
|
|
30
|
+
${block.label}
|
|
31
|
+
</a>
|
|
32
|
+
</li>
|
|
33
|
+
</ul>
|
|
34
|
+
</section>
|
|
35
|
+
`
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return html`
|
|
39
|
+
<form
|
|
40
|
+
data-on-submit="@post('/studio/api/block', { contentType: 'form' })"
|
|
41
|
+
style="display: flex; align-items: center; gap: 1rem;"
|
|
42
|
+
>
|
|
43
|
+
<input type="hidden" name="name" value="${name}" />
|
|
44
|
+
<button
|
|
45
|
+
class="ghost"
|
|
46
|
+
style="padding: 10px; margin: 0 -13px; display: flex;"
|
|
47
|
+
data-tooltip="New ${block.label}"
|
|
48
|
+
data-placement="right"
|
|
49
|
+
>
|
|
50
|
+
${icons.newDocument}
|
|
51
|
+
</button>
|
|
52
|
+
<p style="user-select: none;"><small>${block.label}</small></p>
|
|
53
|
+
</form>
|
|
54
|
+
|
|
55
|
+
${Entries({ name })}
|
|
56
|
+
`
|
|
57
|
+
})}
|
|
58
|
+
</aside>
|
|
59
|
+
|
|
60
|
+
<footer>
|
|
61
|
+
<a
|
|
62
|
+
role="button"
|
|
63
|
+
href="/studio/settings"
|
|
64
|
+
class="ghost"
|
|
65
|
+
style="padding: 10px; margin: 0 -13px; display: flex;"
|
|
66
|
+
data-tooltip="Settings"
|
|
67
|
+
data-placement="right"
|
|
68
|
+
aria-label="Settings"
|
|
69
|
+
>
|
|
70
|
+
${icons.cog}
|
|
71
|
+
</a>
|
|
72
|
+
|
|
73
|
+
<a
|
|
74
|
+
role="button"
|
|
75
|
+
href="/"
|
|
76
|
+
class="ghost"
|
|
77
|
+
style="padding: 10px; margin: 0 -13px; display: flex;"
|
|
78
|
+
data-tooltip="Leave Studio"
|
|
79
|
+
data-placement="right"
|
|
80
|
+
aria-label="Leave"
|
|
81
|
+
>
|
|
82
|
+
${icons.leave}
|
|
83
|
+
</a>
|
|
84
|
+
</footer>
|
|
85
|
+
</div>
|
|
86
|
+
`
|
|
87
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { html } from 'hono/html'
|
|
2
|
+
|
|
3
|
+
export default () => {
|
|
4
|
+
return html`<article data-signals="{ status: null, message: '' }">
|
|
5
|
+
<header>Backup</header>
|
|
6
|
+
<form
|
|
7
|
+
data-on-submit="@post('/studio/api/backup', { contentType: 'form' })"
|
|
8
|
+
>
|
|
9
|
+
<button type="submit">Backup database</button>
|
|
10
|
+
<p data-style-color="$status === 200 ? 'green' : 'red'" data-text="$message || ' '"></p>
|
|
11
|
+
</form>
|
|
12
|
+
</article>`
|
|
13
|
+
}
|