@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
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { query } from '../queries/index.ts'
|
|
2
|
+
import type { BlocksFieldDefStructure } from '../types.ts'
|
|
3
|
+
import { BlockInstance } from '../utils/define.ts'
|
|
4
|
+
import { getOrCreateRow } from '../utils/get-or-create-row.ts'
|
|
5
|
+
import { html } from '../utils/html.ts'
|
|
6
|
+
import * as icons from './icons.ts'
|
|
7
|
+
import Render from './Render.ts'
|
|
8
|
+
|
|
9
|
+
export default (props: {
|
|
10
|
+
entryId: number
|
|
11
|
+
parentId: number
|
|
12
|
+
name: string
|
|
13
|
+
structure: BlocksFieldDefStructure
|
|
14
|
+
id?: number
|
|
15
|
+
sortOrder?: number
|
|
16
|
+
}) => {
|
|
17
|
+
const { entryId, parentId, name, structure, id, sortOrder = 0 } = props
|
|
18
|
+
|
|
19
|
+
const data = getOrCreateRow({
|
|
20
|
+
parentId,
|
|
21
|
+
name,
|
|
22
|
+
field: structure,
|
|
23
|
+
sortOrder,
|
|
24
|
+
id,
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
if (!data) return html`<p>No block</p>`
|
|
28
|
+
|
|
29
|
+
const entries = Object.entries(structure.children)
|
|
30
|
+
|
|
31
|
+
const rows = query.blocks({ parent_id: data.id })
|
|
32
|
+
|
|
33
|
+
return html`
|
|
34
|
+
<div class="blocks-field">
|
|
35
|
+
<header>
|
|
36
|
+
<p>${structure.label}</p>
|
|
37
|
+
|
|
38
|
+
<details class="dropdown">
|
|
39
|
+
<summary>Add</summary>
|
|
40
|
+
<ul>
|
|
41
|
+
${entries.map(([name, block]) => {
|
|
42
|
+
return html`
|
|
43
|
+
<li>
|
|
44
|
+
<form
|
|
45
|
+
data-on-submit="@post('/studio/api/new-block', { contentType: 'form' })"
|
|
46
|
+
>
|
|
47
|
+
<button type="submit" class="ghost">${block.label}</button>
|
|
48
|
+
<input type="hidden" name="type" value="${block.type}" />
|
|
49
|
+
<input type="hidden" name="name" value="${name}" />
|
|
50
|
+
<input type="hidden" name="label" value="${block.label}" />
|
|
51
|
+
<input type="hidden" name="parent_id" value="${data.id}" />
|
|
52
|
+
<input type="hidden" name="entry_id" value="${entryId}" />
|
|
53
|
+
<input
|
|
54
|
+
type="hidden"
|
|
55
|
+
name="sort_order"
|
|
56
|
+
value="${rows.length}"
|
|
57
|
+
/>
|
|
58
|
+
</form>
|
|
59
|
+
</li>
|
|
60
|
+
`
|
|
61
|
+
})}
|
|
62
|
+
</ul>
|
|
63
|
+
</details>
|
|
64
|
+
</header>
|
|
65
|
+
|
|
66
|
+
<hr style="margin-top: 0;" />
|
|
67
|
+
|
|
68
|
+
<sortable-list data-id="${data.id}">
|
|
69
|
+
${rows.map((row, idx) => {
|
|
70
|
+
const [name, struct] =
|
|
71
|
+
entries.find(([name]) => name === row.name) || []
|
|
72
|
+
|
|
73
|
+
if (!name || !struct) return html`<p>No name</p>`
|
|
74
|
+
|
|
75
|
+
return html`
|
|
76
|
+
<article data-id="${row.id}">
|
|
77
|
+
<header>
|
|
78
|
+
${struct.label}
|
|
79
|
+
|
|
80
|
+
<aside>
|
|
81
|
+
<!-- <label style="margin: 0; border-bottom: none" data-tooltip="Disable" data-placement="top">
|
|
82
|
+
<input name="enable" type="checkbox" role="switch" checked />
|
|
83
|
+
</label> -->
|
|
84
|
+
|
|
85
|
+
<button
|
|
86
|
+
data-handle-for="${data.id}"
|
|
87
|
+
class="ghost handle text-secondary"
|
|
88
|
+
style="cursor: grab"
|
|
89
|
+
>
|
|
90
|
+
${icons.bars}
|
|
91
|
+
</button>
|
|
92
|
+
|
|
93
|
+
<form
|
|
94
|
+
data-on-submit="@delete('/studio/api/block', { contentType: 'form' })"
|
|
95
|
+
>
|
|
96
|
+
<button
|
|
97
|
+
type="submit"
|
|
98
|
+
class="ghost text-secondary"
|
|
99
|
+
data-tooltip="Remove"
|
|
100
|
+
data-placement="top"
|
|
101
|
+
aria-label="Delete block"
|
|
102
|
+
>
|
|
103
|
+
${icons.x}
|
|
104
|
+
</button>
|
|
105
|
+
<input type="hidden" name="id" value="${row.id}" />
|
|
106
|
+
<input type="hidden" name="entry_id" value="${entryId}" />
|
|
107
|
+
</form>
|
|
108
|
+
</aside>
|
|
109
|
+
</header>
|
|
110
|
+
|
|
111
|
+
${Render({
|
|
112
|
+
entryId,
|
|
113
|
+
parentId:
|
|
114
|
+
struct.instanceOf === BlockInstance ? row.id : data.id,
|
|
115
|
+
id: row.id,
|
|
116
|
+
structure: struct,
|
|
117
|
+
name: name,
|
|
118
|
+
})}
|
|
119
|
+
</article>
|
|
120
|
+
`
|
|
121
|
+
})}
|
|
122
|
+
</sortable-list>
|
|
123
|
+
</div>
|
|
124
|
+
`
|
|
125
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { html } from 'hono/html'
|
|
2
|
+
import type { BlockDef } from '../types.ts'
|
|
3
|
+
import Render from './Render.ts'
|
|
4
|
+
|
|
5
|
+
export default (props: {
|
|
6
|
+
entryId: number
|
|
7
|
+
parentId: number
|
|
8
|
+
id?: number
|
|
9
|
+
structure: BlockDef
|
|
10
|
+
}) => {
|
|
11
|
+
const { entryId, parentId, structure } = props
|
|
12
|
+
|
|
13
|
+
const entries = Object.entries(structure.fields)
|
|
14
|
+
|
|
15
|
+
return html`${entries.map(([name, field]) => {
|
|
16
|
+
try {
|
|
17
|
+
return Render({ entryId, parentId, structure: field, name })
|
|
18
|
+
} catch (error) {
|
|
19
|
+
return html`<p>Cound not render: "${name}"</p>`
|
|
20
|
+
}
|
|
21
|
+
})}`
|
|
22
|
+
}
|
package/components/Entries.ts
CHANGED
|
@@ -1,32 +1,40 @@
|
|
|
1
1
|
import { html } from 'hono/html'
|
|
2
2
|
import { query } from '../index.ts'
|
|
3
3
|
import * as icons from './icons.ts'
|
|
4
|
+
import type { BlockDef } from '../types.ts'
|
|
4
5
|
|
|
5
|
-
export default () => {
|
|
6
|
-
const entries = query.blocks({
|
|
6
|
+
export default ({ name }: { name: string }) => {
|
|
7
|
+
const entries = query.blocks({ parent_id: null, status: 'enabled', name })
|
|
7
8
|
|
|
8
9
|
return html`
|
|
9
10
|
<section id="entries">
|
|
10
11
|
<ul>
|
|
11
12
|
${entries?.map((block) => {
|
|
12
13
|
const title = query.block({
|
|
13
|
-
|
|
14
|
+
parent_id: block.id.toString(),
|
|
14
15
|
name: 'title',
|
|
15
16
|
})
|
|
16
17
|
|
|
17
18
|
return html`
|
|
18
19
|
<li>
|
|
19
|
-
<a href="/
|
|
20
|
-
${
|
|
20
|
+
<a href="/studio/entry/${block.id}" id="block_link_${block.id}">
|
|
21
|
+
${title?.value || 'Untitled'}
|
|
21
22
|
</a>
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
data-
|
|
25
|
-
class="ghost"
|
|
26
|
-
style="padding: 0"
|
|
23
|
+
|
|
24
|
+
<form
|
|
25
|
+
data-on-submit="@delete('/studio/api/block', { contentType: 'form' })"
|
|
27
26
|
>
|
|
28
|
-
{
|
|
29
|
-
|
|
27
|
+
<input type="hidden" name="id" value="${block.id}" />
|
|
28
|
+
<button
|
|
29
|
+
data-tooltip="Remove"
|
|
30
|
+
data-placement="right"
|
|
31
|
+
class="ghost text-secondary"
|
|
32
|
+
style="padding: 0"
|
|
33
|
+
type="submit"
|
|
34
|
+
>
|
|
35
|
+
${icons.trash}
|
|
36
|
+
</button>
|
|
37
|
+
</form>
|
|
30
38
|
</li>
|
|
31
39
|
`
|
|
32
40
|
})}
|
package/components/Entry.ts
CHANGED
|
@@ -1,34 +1,26 @@
|
|
|
1
1
|
import { html } from 'hono/html'
|
|
2
2
|
import { query } from '../queries/index.ts'
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import Fields from './Fields.ts'
|
|
6
|
-
import { buildStructurePath } from '../utils/build-structure-path.ts'
|
|
3
|
+
import { studioStructure } from '../index.ts'
|
|
4
|
+
import Render from './Render.ts'
|
|
7
5
|
|
|
8
|
-
export default (props: { entryId: number
|
|
6
|
+
export default (props: { entryId: number }) => {
|
|
9
7
|
const data = query.block({ id: props.entryId?.toString() })
|
|
10
8
|
|
|
11
9
|
if (!data) return html`<p>No entry with id: "${props.entryId}"</p>`
|
|
12
10
|
|
|
13
|
-
const
|
|
14
|
-
(block) => block.name === data.name,
|
|
15
|
-
)
|
|
11
|
+
const structure = studioStructure[data.name]
|
|
16
12
|
|
|
17
|
-
|
|
13
|
+
if (!structure) return html`<p>No structure of type: ${data.name}</p>`
|
|
18
14
|
|
|
19
15
|
return html`
|
|
20
|
-
<div id="entry">
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
parentId: props.entryId,
|
|
28
|
-
blockStructure,
|
|
29
|
-
structurePath,
|
|
30
|
-
})}
|
|
31
|
-
</div>
|
|
16
|
+
<div id="entry" class="entry">
|
|
17
|
+
${Render({
|
|
18
|
+
entryId: props.entryId,
|
|
19
|
+
parentId: props.entryId,
|
|
20
|
+
structure: structure,
|
|
21
|
+
name: data.name,
|
|
22
|
+
})}
|
|
32
23
|
</div>
|
|
24
|
+
|
|
33
25
|
`
|
|
34
26
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Field } from './fields/index.ts'
|
|
2
|
+
import type { BlocksFieldDefStructure, FieldDefStructure } from '../types.ts'
|
|
3
|
+
import BlockFieldRenderer from './BlockFieldRenderer.ts'
|
|
4
|
+
|
|
5
|
+
export default (props: {
|
|
6
|
+
entryId: number
|
|
7
|
+
parentId: number
|
|
8
|
+
structure: FieldDefStructure | BlocksFieldDefStructure
|
|
9
|
+
id?: number
|
|
10
|
+
name: string
|
|
11
|
+
}) => {
|
|
12
|
+
const { entryId, parentId, structure, name, id } = props
|
|
13
|
+
|
|
14
|
+
switch (structure.type) {
|
|
15
|
+
case 'text': {
|
|
16
|
+
return Field.Text({ entryId, parentId, name, id, structure })
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
case 'slug': {
|
|
20
|
+
return Field.Slug({ entryId, parentId, name, id, structure })
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
case 'markdown': {
|
|
24
|
+
return Field.Markdown({ entryId, parentId, name, id, structure })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
case 'image': {
|
|
28
|
+
return Field.Text({ entryId, parentId, name, structure, id })
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
case 'blocks': {
|
|
32
|
+
return BlockFieldRenderer({ entryId, parentId, name, structure, id })
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { html } from 'hono/html'
|
|
2
|
+
import type { HtmlEscapedString } from 'hono/utils/html'
|
|
3
|
+
import FieldRenderer from './FieldRenderer.ts'
|
|
4
|
+
import BlockFieldRenderer from './BlockFieldRenderer.ts'
|
|
5
|
+
import BlockRenderer from './BlockRenderer.ts'
|
|
6
|
+
import {
|
|
7
|
+
BlockFieldInstance,
|
|
8
|
+
BlockInstance,
|
|
9
|
+
FieldInstance,
|
|
10
|
+
} from '../utils/define.ts'
|
|
11
|
+
import type {
|
|
12
|
+
BlockDefStructure,
|
|
13
|
+
BlocksFieldDefStructure,
|
|
14
|
+
FieldDefStructure,
|
|
15
|
+
} from '../types.ts'
|
|
16
|
+
|
|
17
|
+
export default (props: {
|
|
18
|
+
entryId: number
|
|
19
|
+
parentId: number
|
|
20
|
+
structure: BlockDefStructure | FieldDefStructure | BlocksFieldDefStructure
|
|
21
|
+
id?: number
|
|
22
|
+
name: string
|
|
23
|
+
sortOrder?: number
|
|
24
|
+
}): HtmlEscapedString | Promise<HtmlEscapedString> => {
|
|
25
|
+
const { entryId, parentId, structure, name, id } = props
|
|
26
|
+
|
|
27
|
+
if (!structure) return html`<p>No block</p>`
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
switch (structure.instanceOf) {
|
|
31
|
+
case FieldInstance: {
|
|
32
|
+
return FieldRenderer({ entryId, parentId, id, structure, name })
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
case BlockFieldInstance: {
|
|
36
|
+
return BlockFieldRenderer({ entryId, parentId, id, structure, name })
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
case BlockInstance: {
|
|
40
|
+
return BlockRenderer({ entryId, parentId, structure, id })
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
return html`<p>Error rendering "${name}"</p>`
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { db } from '@alstar/db'
|
|
2
|
+
import { html } from 'hono/html'
|
|
3
|
+
import { sql } from '../utils/sql.ts'
|
|
4
|
+
import * as icons from './icons.ts'
|
|
5
|
+
import Backup from './Backup.ts'
|
|
6
|
+
import Users from './Users.ts'
|
|
7
|
+
|
|
8
|
+
export default () => {
|
|
9
|
+
const apiKeys = db.database
|
|
10
|
+
.prepare(sql`
|
|
11
|
+
select
|
|
12
|
+
*
|
|
13
|
+
from
|
|
14
|
+
api_keys
|
|
15
|
+
`)
|
|
16
|
+
.all()
|
|
17
|
+
|
|
18
|
+
return html`
|
|
19
|
+
<div id="settings" data-signals="{ apiKey: '', copied: false }">
|
|
20
|
+
<!-- <h1>Settings</h1> -->
|
|
21
|
+
<article>
|
|
22
|
+
<header>API Keys</header>
|
|
23
|
+
|
|
24
|
+
<ul>
|
|
25
|
+
${apiKeys.map((apiKey) => {
|
|
26
|
+
return html`<li>
|
|
27
|
+
<p>${apiKey.name}</p>
|
|
28
|
+
<input type="text" disabled value="${apiKey.hint}" />
|
|
29
|
+
<form
|
|
30
|
+
data-on-submit="@delete('/studio/api/api-key', { contentType: 'form' })"
|
|
31
|
+
>
|
|
32
|
+
<button
|
|
33
|
+
data-tooltip="Delete API key"
|
|
34
|
+
data-placement="left"
|
|
35
|
+
type="submit"
|
|
36
|
+
class="ghost"
|
|
37
|
+
>
|
|
38
|
+
${icons.trash}
|
|
39
|
+
</button>
|
|
40
|
+
|
|
41
|
+
<input type="hidden" name="value" value="${apiKey.value}" />
|
|
42
|
+
</form>
|
|
43
|
+
</li>`
|
|
44
|
+
})}
|
|
45
|
+
</ul>
|
|
46
|
+
|
|
47
|
+
<form
|
|
48
|
+
data-on-submit="@post('/studio/api/api-key', { contentType: 'form' })"
|
|
49
|
+
>
|
|
50
|
+
<label for="api_key_name"><small>Generate API Key</small></label>
|
|
51
|
+
|
|
52
|
+
<input
|
|
53
|
+
data-bind="name"
|
|
54
|
+
type="text"
|
|
55
|
+
name="name"
|
|
56
|
+
id="api_key_name"
|
|
57
|
+
placeholder="Name"
|
|
58
|
+
/>
|
|
59
|
+
|
|
60
|
+
<button type="submit" class="ghost">Generate key</button>
|
|
61
|
+
</form>
|
|
62
|
+
|
|
63
|
+
<dialog data-attr="{ open: $apiKey !== '' }">
|
|
64
|
+
<article>
|
|
65
|
+
<header>
|
|
66
|
+
<p>API Key</p>
|
|
67
|
+
</header>
|
|
68
|
+
<p>Be sure to save this key, as it wont be shown again.</p>
|
|
69
|
+
|
|
70
|
+
<div style="display: flex; gap: 1rem; align-items: center;">
|
|
71
|
+
<h3 style="margin: 0;">
|
|
72
|
+
<code data-text="$apiKey"></code>
|
|
73
|
+
</h3>
|
|
74
|
+
|
|
75
|
+
<button
|
|
76
|
+
style="display: flex; align-items: center;"
|
|
77
|
+
data-attr="{ id: $apiKey }"
|
|
78
|
+
data-on-click="navigator.clipboard.writeText($apiKey); $copied = true"
|
|
79
|
+
class="ghost"
|
|
80
|
+
aria-label="Copy key to clipboard"
|
|
81
|
+
>
|
|
82
|
+
${icons.clipboard}
|
|
83
|
+
<span style="display: none; margin-left: 0.5rem; color: green;" data-style="{ display: $copied && 'block' }">Copied</span>
|
|
84
|
+
</button>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<footer>
|
|
88
|
+
<button
|
|
89
|
+
class="ghost"
|
|
90
|
+
data-on-click="$apiKey = ''; $copied = false; evt.target.closest('dialog')?.close()"
|
|
91
|
+
>
|
|
92
|
+
Close
|
|
93
|
+
</button>
|
|
94
|
+
</footer>
|
|
95
|
+
</article>
|
|
96
|
+
</dialog>
|
|
97
|
+
</article>
|
|
98
|
+
|
|
99
|
+
${Backup()}
|
|
100
|
+
|
|
101
|
+
${Users()}
|
|
102
|
+
</div>
|
|
103
|
+
`
|
|
104
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import adminPanel from './AdminPanel.ts'
|
|
2
|
+
import { html } from 'hono/html'
|
|
3
|
+
import { type HtmlEscapedString } from 'hono/utils/html'
|
|
4
|
+
import { studioConfig } from '../index.ts'
|
|
5
|
+
|
|
6
|
+
export default (
|
|
7
|
+
content:
|
|
8
|
+
| string
|
|
9
|
+
| Promise<string>
|
|
10
|
+
| HtmlEscapedString
|
|
11
|
+
| Promise<HtmlEscapedString>,
|
|
12
|
+
includeAdminPanel = true,
|
|
13
|
+
) => {
|
|
14
|
+
return html`
|
|
15
|
+
<!DOCTYPE html>
|
|
16
|
+
<html lang="en">
|
|
17
|
+
<head>
|
|
18
|
+
<meta charset="UTF-8" />
|
|
19
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
20
|
+
<title>
|
|
21
|
+
${studioConfig.siteName ? studioConfig.siteName + ' | ' : ''}Alstar
|
|
22
|
+
Studio
|
|
23
|
+
</title>
|
|
24
|
+
|
|
25
|
+
<link rel="icon" type="image/svg" href="/studio/favicon.svg" />
|
|
26
|
+
|
|
27
|
+
<meta name="color-scheme" content="light dark" />
|
|
28
|
+
|
|
29
|
+
<script
|
|
30
|
+
type="module"
|
|
31
|
+
src="https://cdn.jsdelivr.net/gh/starfederation/datastar@main/bundles/datastar.js"
|
|
32
|
+
></script>
|
|
33
|
+
|
|
34
|
+
<script type="importmap">
|
|
35
|
+
{
|
|
36
|
+
"imports": {
|
|
37
|
+
"@barba/core": "https://esm.sh/@barba/core@2.10.3/dist/barba.mjs",
|
|
38
|
+
"sortablejs": "https://esm.sh/sortablejs@1.15.6/modular/sortable.core.esm.js",
|
|
39
|
+
"ink-mde": "https://esm.sh/ink-mde@0.34.0"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
</script>
|
|
43
|
+
|
|
44
|
+
<script src="/studio/main.js" type="module"></script>
|
|
45
|
+
<link href="/studio/main.css" rel="stylesheet" />
|
|
46
|
+
</head>
|
|
47
|
+
|
|
48
|
+
<body data-barba="wrapper">
|
|
49
|
+
${includeAdminPanel
|
|
50
|
+
? html`<section style="margin-bottom: 0;">${adminPanel()}</section>`
|
|
51
|
+
: html`<div></div>`}
|
|
52
|
+
|
|
53
|
+
<main>
|
|
54
|
+
<section data-barba="container" data-barba-namespace="default">
|
|
55
|
+
${content}
|
|
56
|
+
</section>
|
|
57
|
+
</main>
|
|
58
|
+
</body>
|
|
59
|
+
</html>
|
|
60
|
+
`
|
|
61
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { db } from '@alstar/db'
|
|
2
|
+
import { html } from 'hono/html'
|
|
3
|
+
import { sql } from '../utils/sql.ts'
|
|
4
|
+
|
|
5
|
+
export default () => {
|
|
6
|
+
const users = db.database
|
|
7
|
+
.prepare(sql`
|
|
8
|
+
select
|
|
9
|
+
email
|
|
10
|
+
from
|
|
11
|
+
users
|
|
12
|
+
`)
|
|
13
|
+
.all()
|
|
14
|
+
|
|
15
|
+
return html`
|
|
16
|
+
<article>
|
|
17
|
+
<header>Users</header>
|
|
18
|
+
<ul>
|
|
19
|
+
${users.map(user => html`<li>${user.email}</li>`)}
|
|
20
|
+
</ul>
|
|
21
|
+
<article>
|
|
22
|
+
<header>Register user</header>
|
|
23
|
+
<form
|
|
24
|
+
data-on-submit="@post('/studio/api/auth/register', { contentType: 'form' })"
|
|
25
|
+
>
|
|
26
|
+
<label for="register_email"><small>Email</small></label>
|
|
27
|
+
<input
|
|
28
|
+
id="register_email"
|
|
29
|
+
name="email"
|
|
30
|
+
type="email"
|
|
31
|
+
placeholder="Email"
|
|
32
|
+
/>
|
|
33
|
+
<label for="register_password"><small>Password</small></label>
|
|
34
|
+
<input
|
|
35
|
+
id="register_password"
|
|
36
|
+
name="password"
|
|
37
|
+
type="password"
|
|
38
|
+
placeholder="Password"
|
|
39
|
+
/>
|
|
40
|
+
<br />
|
|
41
|
+
<button type="submit" class="ghost">Create</button>
|
|
42
|
+
</form>
|
|
43
|
+
</article>
|
|
44
|
+
</article>
|
|
45
|
+
`
|
|
46
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { FieldDefStructure } from '../../types.ts'
|
|
2
|
+
import { getOrCreateRow } from '../../utils/get-or-create-row.ts'
|
|
3
|
+
import { html } from '../../utils/html.ts'
|
|
4
|
+
|
|
5
|
+
export default (props: {
|
|
6
|
+
entryId: number
|
|
7
|
+
parentId: number
|
|
8
|
+
name: string
|
|
9
|
+
id?: number
|
|
10
|
+
structure: FieldDefStructure
|
|
11
|
+
}) => {
|
|
12
|
+
const { entryId, parentId, name, structure, id } = props
|
|
13
|
+
|
|
14
|
+
const data = getOrCreateRow({ parentId, name, field: structure, id })
|
|
15
|
+
|
|
16
|
+
if (!data) return html`<p>No block</p>`
|
|
17
|
+
|
|
18
|
+
return html`
|
|
19
|
+
<form
|
|
20
|
+
data-on-input="@patch('/studio/api/block', { contentType: 'form' })"
|
|
21
|
+
>
|
|
22
|
+
<hgroup>
|
|
23
|
+
<label for="block-${data.id}">${structure.label}</label>
|
|
24
|
+
<p><small>${structure.description}</small></p>
|
|
25
|
+
</hgroup>
|
|
26
|
+
|
|
27
|
+
<markdown-editor
|
|
28
|
+
data-content="${data.value?.trim()}"
|
|
29
|
+
data-id="${data.id}"
|
|
30
|
+
>
|
|
31
|
+
<!-- <textarea id="block-{data.id}" name="value" class="markdown">
|
|
32
|
+
{data.value}
|
|
33
|
+
</textarea
|
|
34
|
+
> -->
|
|
35
|
+
</markdown-editor>
|
|
36
|
+
|
|
37
|
+
<input type="hidden" name="type" value="${structure.type}" />
|
|
38
|
+
<input type="hidden" name="id" value="${data.id}" />
|
|
39
|
+
<input type="hidden" name="entryId" value="${entryId}" />
|
|
40
|
+
<input type="hidden" name="parentId" value="${parentId}" />
|
|
41
|
+
<input type="hidden" name="name" value="${name}" />
|
|
42
|
+
</form>
|
|
43
|
+
`
|
|
44
|
+
}
|