@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.
Files changed (69) hide show
  1. package/api/api-key.ts +69 -0
  2. package/api/auth.ts +66 -0
  3. package/api/backup.ts +40 -0
  4. package/api/block.ts +131 -66
  5. package/api/index.ts +19 -1
  6. package/api/mcp.ts +50 -0
  7. package/components/AdminPanel.ts +87 -0
  8. package/components/Backup.ts +13 -0
  9. package/components/BlockFieldRenderer.ts +125 -0
  10. package/components/BlockRenderer.ts +22 -0
  11. package/components/Entries.ts +20 -12
  12. package/components/Entry.ts +13 -21
  13. package/components/FieldRenderer.ts +35 -0
  14. package/components/Render.ts +46 -0
  15. package/components/Settings.ts +104 -0
  16. package/components/SiteLayout.ts +61 -0
  17. package/components/Users.ts +46 -0
  18. package/components/fields/Markdown.ts +44 -0
  19. package/components/fields/Slug.ts +113 -0
  20. package/components/fields/Text.ts +42 -0
  21. package/components/fields/index.ts +7 -0
  22. package/components/icons.ts +136 -7
  23. package/index.ts +94 -34
  24. package/package.json +10 -7
  25. package/pages/entry/[id].ts +15 -0
  26. package/pages/error.ts +14 -0
  27. package/{components → pages}/index.ts +7 -4
  28. package/pages/login.ts +21 -0
  29. package/pages/register.ts +33 -0
  30. package/pages/settings.ts +8 -0
  31. package/public/studio/css/admin-panel.css +103 -0
  32. package/public/studio/css/blocks-field.css +53 -0
  33. package/public/studio/css/settings.css +28 -0
  34. package/public/studio/js/markdown-editor.js +34 -0
  35. package/public/studio/js/sortable-list.js +50 -0
  36. package/public/studio/main.css +166 -0
  37. package/public/studio/main.js +21 -0
  38. package/queries/block-2.ts +339 -0
  39. package/queries/block-with-children.ts +74 -0
  40. package/queries/block.ts +289 -0
  41. package/queries/db-types.ts +15 -0
  42. package/queries/getBlockTrees-2.ts +71 -0
  43. package/queries/getBlockTrees.ts +316 -0
  44. package/queries/getBlocks.ts +214 -0
  45. package/queries/index.ts +2 -98
  46. package/queries/structure-types.ts +97 -0
  47. package/readme.md +205 -0
  48. package/schema.sql +18 -0
  49. package/schemas.ts +23 -52
  50. package/types.ts +144 -5
  51. package/utils/auth.ts +54 -0
  52. package/utils/buildBlocksTree.ts +4 -4
  53. package/utils/create-hash.ts +9 -0
  54. package/utils/define.ts +39 -0
  55. package/utils/file-based-router.ts +11 -2
  56. package/utils/get-config.ts +8 -9
  57. package/utils/get-or-create-row.ts +41 -0
  58. package/utils/html.ts +247 -0
  59. package/utils/startup-log.ts +19 -0
  60. package/components/AdminPanel/AdminPanel.css +0 -59
  61. package/components/AdminPanel/AdminPanel.ts +0 -57
  62. package/components/Block.ts +0 -116
  63. package/components/Entry.css +0 -7
  64. package/components/Field.ts +0 -164
  65. package/components/Fields.ts +0 -43
  66. package/components/layout.ts +0 -53
  67. package/public/main.css +0 -92
  68. package/public/main.js +0 -43
  69. /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
+ }
@@ -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({ parent: null })
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
- parent: block.id.toString(),
14
+ parent_id: block.id.toString(),
14
15
  name: 'title',
15
16
  })
16
17
 
17
18
  return html`
18
19
  <li>
19
- <a href="/admin/entry/${block.id}" id="block_link_${block.id}">
20
- ${block.value || title?.value || 'Untitled'}
20
+ <a href="/studio/entry/${block.id}" id="block_link_${block.id}">
21
+ ${title?.value || 'Untitled'}
21
22
  </a>
22
- <!-- <button
23
- data-tooltip="Rename"
24
- data-placement="right"
25
- class="ghost"
26
- style="padding: 0"
23
+
24
+ <form
25
+ data-on-submit="@delete('/studio/api/block', { contentType: 'form' })"
27
26
  >
28
- {icons.pen}
29
- </button> -->
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
  })}
@@ -1,34 +1,26 @@
1
1
  import { html } from 'hono/html'
2
2
  import { query } from '../queries/index.ts'
3
- import { rootdir } from '../index.ts'
4
- import { type Structure } from '../types.ts'
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 | string; structure: Structure }) => {
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 blockStructure = props.structure.find(
14
- (block) => block.name === data.name,
15
- )
11
+ const structure = studioStructure[data.name]
16
12
 
17
- const structurePath = buildStructurePath(blockStructure)
13
+ if (!structure) return html`<p>No structure of type: ${data.name}</p>`
18
14
 
19
15
  return html`
20
- <div id="entry">
21
- <link rel="stylesheet" href="${rootdir}/components/Entry.css" />
22
-
23
- <div class="entry">
24
- ${blockStructure &&
25
- Fields({
26
- entryId: props.entryId,
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
+ }