@alstar/studio 0.0.0-beta.1

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/block.ts ADDED
@@ -0,0 +1,97 @@
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 { stripNewlines } from '../utils/strip-newlines.ts'
6
+ import { sql } from '../utils/sql.ts'
7
+ import { type Structure } from '../types.ts'
8
+ import { db } from '@alstar/db'
9
+ import Entries from '../components/Entries.ts'
10
+ import Entry from '../components/Entry.ts'
11
+
12
+ export const sectionRoutes = (structure: Structure) => {
13
+ const app = new Hono<{ Bindings: HttpBindings }>()
14
+
15
+ app.post('/block', async (c) => {
16
+ return streamSSE(c, async (stream) => {
17
+ const formData = await c.req.formData()
18
+ const data = Object.fromEntries(formData.entries())
19
+
20
+ const row = structure.find((block) => block.type === data.type)
21
+
22
+ if (!row) return
23
+
24
+ db.insertInto('blocks', {
25
+ name: row.name?.toString(),
26
+ label: row.label?.toString(),
27
+ type: row.type?.toString(),
28
+ })
29
+
30
+ await stream.writeSSE({
31
+ event: 'datastar-patch-elements',
32
+ data: `elements ${stripNewlines(Entries())}`,
33
+ })
34
+ })
35
+ })
36
+
37
+ app.post('/new-block', async (c) => {
38
+ return streamSSE(c, async (stream) => {
39
+ const formData = await c.req.formData()
40
+ const newBlock = formData.get('block')?.toString().split(';')
41
+ const columns = newBlock?.map((field) => field.split(':')) as string[][]
42
+ const data = Object.fromEntries(columns)
43
+
44
+ const parent_block_id = formData?.get('parent_block_id')?.toString()
45
+ const sort_order = formData?.get('sort_order')?.toString()
46
+
47
+ if (!parent_block_id || !sort_order) return
48
+
49
+ db.insertInto('blocks', {
50
+ type: data.type,
51
+ name: data.name,
52
+ label: data.label,
53
+ parent_block_id,
54
+ sort_order,
55
+ })
56
+
57
+ await stream.writeSSE({
58
+ event: 'datastar-patch-elements',
59
+ data: `elements ${stripNewlines(Entry({ entryId: data.entry_id, structure }))}`,
60
+ })
61
+ })
62
+ })
63
+
64
+ app.patch('/block', async (c) => {
65
+ return streamSSE(c, async (stream) => {
66
+ const formData = await c.req.formData()
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()
73
+
74
+ if (!id || !value || !sortOrder) return
75
+
76
+ const transaction = db.database.prepare(sql`
77
+ update blocks
78
+ set
79
+ value = ?
80
+ where
81
+ id = ?
82
+ and sort_order = ?;
83
+ `)
84
+
85
+ transaction.run(value, id, sortOrder)
86
+
87
+ if (entryId === parentId) {
88
+ await stream.writeSSE({
89
+ event: 'datastar-patch-elements',
90
+ data: `elements <a href="/admin/entry/${entryId}" id="block_link_${entryId}">${value}</a>`,
91
+ })
92
+ }
93
+ })
94
+ })
95
+
96
+ return app
97
+ }
package/api/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export * from './block.ts'
2
+
@@ -0,0 +1,59 @@
1
+ .admin-panel {
2
+ /* background: hsla(0, 0%, 0%, 0.1); */
3
+ padding: 40px;
4
+ padding-right: 0;
5
+
6
+ height: 100%;
7
+ min-height: inherit;
8
+
9
+ min-width: 200px;
10
+
11
+ > h1 {
12
+ padding-bottom: 1rem;
13
+
14
+ a {
15
+ display: flex;
16
+ }
17
+
18
+ svg {
19
+ height: 1.6rem;
20
+ }
21
+ }
22
+
23
+ form {
24
+ padding-bottom: 1rem;
25
+
26
+ button {
27
+ margin: 10px 0px 20px;
28
+ }
29
+ }
30
+
31
+ #entries ul {
32
+ padding: 0;
33
+
34
+ > li {
35
+ margin-bottom: 0px;
36
+ border-radius: 8px;
37
+ display: flex;
38
+ justify-content: space-between;
39
+ align-items: stretch;
40
+ /* align-items: center; */
41
+ list-style: none;
42
+ margin-inline: -1rem;
43
+
44
+ a {
45
+ text-decoration: none;
46
+ width: 100%;
47
+ padding: 0.5rem 1rem;
48
+ }
49
+
50
+ button {
51
+ border-radius: 7px;
52
+
53
+ svg {
54
+ margin: 0.5rem 1rem;
55
+ }
56
+ }
57
+ }
58
+ }
59
+ }
@@ -0,0 +1,57 @@
1
+ import { html } from 'hono/html'
2
+ import type { Structure } from '../../types.ts'
3
+ import { logo } from '../icons.ts'
4
+ import { rootdir } from '../../index.ts'
5
+ import Entries from '../Entries.ts'
6
+ import * as icons from '../icons.ts'
7
+
8
+ export default (structure: Structure) => {
9
+ return html`
10
+ <link
11
+ rel="stylesheet"
12
+ href="${rootdir}/components/AdminPanel/AdminPanel.css"
13
+ />
14
+
15
+ <div class="admin-panel">
16
+ <h1>
17
+ <a href="/admin" aria-label="Go to dashboard"> ${logo} </a>
18
+ </h1>
19
+
20
+ <aside>
21
+ <form
22
+ data-on-submit="@post('/admin/api/block', { contentType: 'form' })"
23
+ >
24
+ <!-- <select name="type">
25
+ ${structure.map(
26
+ (block) => html`
27
+ <option value="${block.type}">
28
+ ${block.label}
29
+ </option>
30
+ `,
31
+ )}
32
+ </select>
33
+ <input type="submit" value="Create" /> -->
34
+
35
+ <!-- TODO: currently only handles a single entry type -->
36
+ ${structure.length
37
+ ? html`<input
38
+ type="hidden"
39
+ name="type"
40
+ value="${structure[0].type}"
41
+ />
42
+ <button
43
+ class="ghost"
44
+ style="padding: 10px; margin: 0 -13px; display: flex;"
45
+ data-tooltip="New entry"
46
+ data-placement="right"
47
+ >
48
+ ${icons.newDocument}
49
+ </button>`
50
+ : ''}
51
+ </form>
52
+ </aside>
53
+
54
+ ${Entries()}
55
+ </div>
56
+ `
57
+ }
@@ -0,0 +1,116 @@
1
+ import { html } from 'hono/html'
2
+ import { query } from '../queries/index.ts'
3
+ import Fields from './Fields.ts'
4
+ import * as icons from './icons.ts'
5
+ import { type Block } from '../types.ts'
6
+ import Field from './Field.ts'
7
+ import {
8
+ buildStructurePath,
9
+ type StructurePath,
10
+ } from '../utils/build-structure-path.ts'
11
+
12
+ export default (
13
+ entryId: string | number,
14
+ id: string | number,
15
+ fieldStructure: Block,
16
+ structurePath: StructurePath,
17
+ ) => {
18
+ const data = query.block({ id: id.toString() })
19
+ const blocks = query.blocks({ parent: id })
20
+
21
+ if (!data) return html`<p>No block data</p>`
22
+
23
+ const fieldStructurePath = buildStructurePath(fieldStructure, structurePath)
24
+
25
+ return html`
26
+
27
+ <section class="block">
28
+ <header>
29
+ <h5>${fieldStructure.label}</h5>
30
+
31
+ <form
32
+ data-on-submit="@post('/admin/api/new-block', { contentType: 'form' })"
33
+ >
34
+ <input type="hidden" name="parent_block_id" value="${id}" />
35
+ <input
36
+ type="hidden"
37
+ name="sort_order"
38
+ value="${blocks?.length || 0}"
39
+ />
40
+
41
+ <fieldset role="group">
42
+ <select name="block">
43
+ ${fieldStructure.fields?.map((field) => {
44
+ return html`<option
45
+ value="entry_id:${entryId};type:${field.type};name:${field.name};label:${field.label};sort_order:${blocks?.length ||
46
+ 0}"
47
+ >
48
+ ${field.label}
49
+ </option>`
50
+ })}
51
+ </select>
52
+
53
+ <button
54
+ class="outline"
55
+ style="padding: 0 1rem"
56
+ data-tooltip="New entry"
57
+ data-placement="right"
58
+ type="submit"
59
+ >
60
+ Add
61
+ </button>
62
+ </fieldset>
63
+ </form>
64
+ </header>
65
+
66
+ <div data-sortable="${id}">
67
+ ${blocks?.map((block, idx) => {
68
+ const structure = fieldStructure.fields?.find(
69
+ (field) => field.name === block.name,
70
+ )
71
+
72
+ if (!structure) return
73
+
74
+ return html`
75
+ <article
76
+ data-signals="{ open${block.id}: true }"
77
+ >
78
+ <header>
79
+ <h6>${structure.label}</h6>
80
+ <div>
81
+ <button
82
+ data-on-click="$open${block.id} = !$open${block.id}"
83
+ style="margin-right: 0.5rem"
84
+ data-style-rotate="$open${block.id} ? '0deg' : '180deg'"
85
+ class="shadow"
86
+ >
87
+ ${icons.chevron}
88
+ </button>
89
+ <button class="shadow">${icons.bars}</button>
90
+ </div>
91
+ </header>
92
+
93
+ <div data-show="$open${block.id}">
94
+ ${structure.fields
95
+ ? Fields({
96
+ entryId,
97
+ parentId: block.id,
98
+ blockStructure: structure,
99
+ structurePath: [...fieldStructurePath, structure.name],
100
+ })
101
+ : Field({
102
+ entryId,
103
+ parentId: block.id,
104
+ blockStructure: structure,
105
+ sortOrder: idx,
106
+ structurePath: [...fieldStructurePath, structure.name],
107
+ })}
108
+
109
+ </div>
110
+ </article>
111
+ `
112
+ })}
113
+ </div>
114
+ </section>
115
+ `
116
+ }
@@ -0,0 +1,37 @@
1
+ import { html } from 'hono/html'
2
+ import { query } from '../index.ts'
3
+ import * as icons from './icons.ts'
4
+
5
+ export default () => {
6
+ const entries = query.blocks({ parent: null })
7
+
8
+ return html`
9
+ <section id="entries">
10
+ <ul>
11
+ ${entries?.map((block) => {
12
+ const title = query.block({
13
+ parent: block.id.toString(),
14
+ name: 'title',
15
+ })
16
+
17
+ return html`
18
+ <li>
19
+ <a href="/admin/entry/${block.id}" id="block_link_${block.id}">
20
+ ${block.value || title?.value || 'Untitled'}
21
+ </a>
22
+ <!-- <button
23
+ data-tooltip="Rename"
24
+ data-placement="right"
25
+ class="ghost"
26
+ style="padding: 0"
27
+ >
28
+ {icons.pen}
29
+ </button> -->
30
+ </li>
31
+ `
32
+ })}
33
+ </ul>
34
+ </section>
35
+
36
+ `
37
+ }
@@ -0,0 +1,7 @@
1
+ .entry {
2
+ form {
3
+ display: flex;
4
+ flex-direction: column;
5
+ align-items: flex-start;
6
+ }
7
+ }
@@ -0,0 +1,34 @@
1
+ import { html } from 'hono/html'
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'
7
+
8
+ export default (props: { entryId: number | string; structure: Structure }) => {
9
+ const data = query.block({ id: props.entryId?.toString() })
10
+
11
+ if (!data) return html`<p>No entry with id: "${props.entryId}"</p>`
12
+
13
+ const blockStructure = props.structure.find(
14
+ (block) => block.name === data.name,
15
+ )
16
+
17
+ const structurePath = buildStructurePath(blockStructure)
18
+
19
+ 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>
32
+ </div>
33
+ `
34
+ }
@@ -0,0 +1,164 @@
1
+ import { html } from 'hono/html'
2
+ import { type Block } from '../types.ts'
3
+ import { type HtmlEscapedString } from 'hono/utils/html'
4
+ import { query } from '../queries/index.ts'
5
+ import { db } from '@alstar/db'
6
+ import BlockComponent from './Block.ts'
7
+ import {
8
+ buildStructurePath,
9
+ type StructurePath,
10
+ } from '../utils/build-structure-path.ts'
11
+
12
+ function getData(parentId: string | number, field: Block, sortOrder: number) {
13
+ const data = query.block({
14
+ parent: parentId?.toString() || null,
15
+ name: field.name,
16
+ sort_order: sortOrder.toString(),
17
+ })
18
+
19
+ if (!data) {
20
+ const change = db.insertInto('blocks', {
21
+ name: field.name?.toString(),
22
+ label: field.label?.toString(),
23
+ type: field.type?.toString(),
24
+ sort_order: sortOrder,
25
+ parent_block_id: parentId,
26
+ })
27
+
28
+ return query.block({ id: change.lastInsertRowid.toString() })
29
+ }
30
+
31
+ return data
32
+ }
33
+
34
+ const Field = (props: {
35
+ entryId: string | number
36
+ parentId: string | number
37
+ blockStructure: Block
38
+ sortOrder?: number
39
+ structurePath: StructurePath
40
+ }): HtmlEscapedString | Promise<HtmlEscapedString> => {
41
+ const {
42
+ entryId,
43
+ parentId,
44
+ blockStructure,
45
+ sortOrder = 0,
46
+ structurePath,
47
+ } = props
48
+
49
+ const data = getData(parentId, blockStructure, sortOrder)
50
+
51
+ if (!data) return html`<p>No block</p>`
52
+
53
+ const fieldStructurePath = buildStructurePath(blockStructure, structurePath)
54
+
55
+ return html`
56
+ ${blockStructure.type === 'slug' &&
57
+ html`
58
+ <form
59
+ data-on-input="@patch('/admin/api/block', { contentType: 'form' })"
60
+ >
61
+ <label for="block-${data.id}">${blockStructure.label}</label>
62
+ <input
63
+ id="block-${data.id}"
64
+ name="value"
65
+ type="text"
66
+ value="${data.value}"
67
+ />
68
+
69
+ <input type="hidden" name="type" value="${blockStructure.type}" />
70
+ <input type="hidden" name="id" value="${data.id}" />
71
+ <input type="hidden" name="entryId" value="${entryId}" />
72
+ <input type="hidden" name="parentId" value="${parentId}" />
73
+ <input type="hidden" name="sort_order" value="${sortOrder}" />
74
+ <input
75
+ type="hidden"
76
+ name="path"
77
+ value="${fieldStructurePath.join('.')}"
78
+ />
79
+ </form>
80
+ `}
81
+ ${blockStructure.type === 'text' &&
82
+ html`
83
+ <form
84
+ data-on-input="@patch('/admin/api/block', { contentType: 'form' })"
85
+ >
86
+ <label for="block-${data.id}">${blockStructure.label}</label>
87
+ <input
88
+ id="block-${data.id}"
89
+ name="value"
90
+ type="text"
91
+ value="${data.value}"
92
+ />
93
+
94
+ <input type="hidden" name="type" value="${blockStructure.type}" />
95
+ <input type="hidden" name="id" value="${data.id}" />
96
+ <input type="hidden" name="entryId" value="${entryId}" />
97
+ <input type="hidden" name="parentId" value="${parentId}" />
98
+ <input type="hidden" name="sort_order" value="${sortOrder}" />
99
+ <input
100
+ type="hidden"
101
+ name="path"
102
+ value="${fieldStructurePath.join('.')}"
103
+ />
104
+ </form>
105
+ `}
106
+ ${blockStructure.type === 'image' &&
107
+ html`
108
+ <form
109
+ data-on-input="@patch('/admin/api/block', { contentType: 'form' })"
110
+ >
111
+ <label for="block-${data.id}">${blockStructure.label}</label>
112
+ <input
113
+ id="block-${data.id}"
114
+ name="value"
115
+ type="text"
116
+ value="${data.value}"
117
+ />
118
+
119
+ <input type="hidden" name="type" value="${blockStructure.type}" />
120
+ <input type="hidden" name="id" value="${data.id}" />
121
+ <input type="hidden" name="entryId" value="${entryId}" />
122
+ <input type="hidden" name="parentId" value="${parentId}" />
123
+ <input type="hidden" name="sort_order" value="${sortOrder}" />
124
+ <input
125
+ type="hidden"
126
+ name="path"
127
+ value="${fieldStructurePath.join('.')}"
128
+ />
129
+ </form>
130
+ `}
131
+ ${blockStructure.type === 'markdown' &&
132
+ html`
133
+ <form
134
+ data-on-input="@patch('/admin/api/block', { contentType: 'form' })"
135
+ >
136
+ <label for="block-${data.id}">${blockStructure.label}</label>
137
+
138
+ <textarea></textarea>
139
+
140
+ <!-- <input
141
+ id="block-${data.id}"
142
+ name="value"
143
+ type="text"
144
+ value="${data.value}"
145
+ /> -->
146
+
147
+ <input type="hidden" name="type" value="${blockStructure.type}" />
148
+ <input type="hidden" name="id" value="${data.id}" />
149
+ <input type="hidden" name="entryId" value="${entryId}" />
150
+ <input type="hidden" name="parentId" value="${parentId}" />
151
+ <input type="hidden" name="sort_order" value="${sortOrder}" />
152
+ <input
153
+ type="hidden"
154
+ name="path"
155
+ value="${fieldStructurePath.join('.')}"
156
+ />
157
+ </form>
158
+ `}
159
+ ${blockStructure.type === 'blocks' &&
160
+ BlockComponent(entryId, data.id, blockStructure, fieldStructurePath)}
161
+ `
162
+ }
163
+
164
+ export default Field
@@ -0,0 +1,43 @@
1
+ import { html } from 'hono/html'
2
+ import { type Block } from '../types.ts'
3
+ import { type HtmlEscapedString } from 'hono/utils/html'
4
+ import Field from './Field.ts'
5
+ import {
6
+ buildStructurePath,
7
+ type StructurePath,
8
+ } from '../utils/build-structure-path.ts'
9
+
10
+ const Fields = (props: {
11
+ entryId: string | number
12
+ parentId: string | number
13
+ blockStructure: Block
14
+ structurePath: StructurePath
15
+ }): HtmlEscapedString | Promise<HtmlEscapedString> => {
16
+ const updatedPath = buildStructurePath(
17
+ props.blockStructure,
18
+ props.structurePath,
19
+ )
20
+
21
+ return html`
22
+ <div class="fields">
23
+ ${props.blockStructure.fields
24
+ ? props.blockStructure.fields?.map((field, idx) => {
25
+ return Field({
26
+ entryId: props.entryId,
27
+ parentId: props.parentId,
28
+ blockStructure: field,
29
+ sortOrder: idx,
30
+ structurePath: [...updatedPath, 'fields'],
31
+ })
32
+ })
33
+ : Field({
34
+ entryId: props.entryId,
35
+ parentId: props.parentId,
36
+ blockStructure: props.blockStructure,
37
+ structurePath: updatedPath,
38
+ })}
39
+ </div>
40
+ `
41
+ }
42
+
43
+ export default Fields