@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/utils/define.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as types from '../types.ts'
|
|
2
|
+
import { type HtmlEscapedString } from './html.ts'
|
|
3
|
+
|
|
4
|
+
export const defineConfig = (config: types.StudioConfig) => config
|
|
5
|
+
|
|
6
|
+
export const defineEntry = (
|
|
7
|
+
fn: (
|
|
8
|
+
c: types.RequestContext,
|
|
9
|
+
) => HtmlEscapedString | Promise<HtmlEscapedString>,
|
|
10
|
+
) => fn
|
|
11
|
+
|
|
12
|
+
export const FieldInstance = Symbol('field')
|
|
13
|
+
export const BlockFieldInstance = Symbol('blockfield')
|
|
14
|
+
export const BlockInstance = Symbol('block')
|
|
15
|
+
|
|
16
|
+
// --- Identity helpers (preserve literal types) ---
|
|
17
|
+
export function defineField(field: types.FieldDef): types.FieldDefStructure {
|
|
18
|
+
return {
|
|
19
|
+
...field,
|
|
20
|
+
instanceOf: FieldInstance,
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function defineBlockField(
|
|
25
|
+
field: types.BlocksFieldDef,
|
|
26
|
+
): types.BlocksFieldDefStructure {
|
|
27
|
+
return {
|
|
28
|
+
...field,
|
|
29
|
+
instanceOf: BlockFieldInstance,
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function defineBlock(block: types.BlockDef): types.BlockDefStructure {
|
|
34
|
+
return { ...block, instanceOf: BlockInstance }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function defineStructure(structure: types.Structure) {
|
|
38
|
+
return structure
|
|
39
|
+
}
|
|
@@ -3,13 +3,22 @@ import path from 'node:path'
|
|
|
3
3
|
import { Hono } from 'hono'
|
|
4
4
|
import { type HtmlEscapedString } from 'hono/utils/html'
|
|
5
5
|
|
|
6
|
-
type Page<C> = (c?: C) => HtmlEscapedString | Promise<HtmlEscapedString>
|
|
6
|
+
export type Page<C> = (c?: C) => HtmlEscapedString | Promise<HtmlEscapedString>
|
|
7
7
|
|
|
8
8
|
export const fileBasedRouter = async (rootdir: string) => {
|
|
9
9
|
const router = new Hono()
|
|
10
10
|
|
|
11
11
|
const root = path.resolve(rootdir)
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
let dirs
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
dirs = await fs.readdir(root, { recursive: true })
|
|
17
|
+
} catch (error) {
|
|
18
|
+
console.log('No files in:', root)
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
|
|
13
22
|
const files = dirs.filter((dir) => path.extname(dir))
|
|
14
23
|
|
|
15
24
|
await Promise.all(
|
package/utils/get-config.ts
CHANGED
|
@@ -3,15 +3,6 @@ import path from 'node:path'
|
|
|
3
3
|
|
|
4
4
|
const CONFIG_FILE_NAME = 'alstar.config.ts'
|
|
5
5
|
|
|
6
|
-
async function fileExists(filepath: string) {
|
|
7
|
-
// does the file exist?
|
|
8
|
-
try {
|
|
9
|
-
await fs.stat(filepath)
|
|
10
|
-
} catch (error) {
|
|
11
|
-
return {}
|
|
12
|
-
}
|
|
13
|
-
}
|
|
14
|
-
|
|
15
6
|
export const getConfig = async <P>(): Promise<P> => {
|
|
16
7
|
const root = path.resolve('./')
|
|
17
8
|
|
|
@@ -24,3 +15,11 @@ export const getConfig = async <P>(): Promise<P> => {
|
|
|
24
15
|
|
|
25
16
|
return config as P
|
|
26
17
|
}
|
|
18
|
+
|
|
19
|
+
async function fileExists(filepath: string) {
|
|
20
|
+
try {
|
|
21
|
+
return await fs.stat(filepath)
|
|
22
|
+
} catch (error) {
|
|
23
|
+
return null
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { db } from '@alstar/db'
|
|
2
|
+
import { query } from '../queries/index.ts'
|
|
3
|
+
import type {
|
|
4
|
+
BlockDefStructure,
|
|
5
|
+
BlocksFieldDefStructure,
|
|
6
|
+
FieldDefStructure,
|
|
7
|
+
} from '../types.ts'
|
|
8
|
+
|
|
9
|
+
export function getOrCreateRow(props: {
|
|
10
|
+
parentId: string | number | null
|
|
11
|
+
name: string
|
|
12
|
+
field: BlockDefStructure | BlocksFieldDefStructure | FieldDefStructure
|
|
13
|
+
sortOrder?: number
|
|
14
|
+
id?: number
|
|
15
|
+
}) {
|
|
16
|
+
const { parentId, name, field, sortOrder = 0, id } = props
|
|
17
|
+
|
|
18
|
+
if (id) {
|
|
19
|
+
return query.block({
|
|
20
|
+
id: id?.toString(),
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const data = query.block({
|
|
25
|
+
parent_id: parentId?.toString() || null,
|
|
26
|
+
name: name,
|
|
27
|
+
sort_order: sortOrder.toString(),
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
if (data) return data
|
|
31
|
+
|
|
32
|
+
const change = db.insertInto('blocks', {
|
|
33
|
+
name: name?.toString(),
|
|
34
|
+
label: field.label?.toString(),
|
|
35
|
+
type: field.type?.toString(),
|
|
36
|
+
sort_order: sortOrder,
|
|
37
|
+
parent_id: parentId,
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
return query.block({ id: change.lastInsertRowid.toString() })
|
|
41
|
+
}
|
package/utils/html.ts
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
export const HtmlEscapedCallbackPhase = {
|
|
2
|
+
Stringify: 1,
|
|
3
|
+
BeforeStream: 2,
|
|
4
|
+
Stream: 3,
|
|
5
|
+
} as const
|
|
6
|
+
type HtmlEscapedCallbackOpts = {
|
|
7
|
+
buffer?: [string]
|
|
8
|
+
phase: (typeof HtmlEscapedCallbackPhase)[keyof typeof HtmlEscapedCallbackPhase]
|
|
9
|
+
context: Readonly<object> // An object unique to each JSX tree. This object is used as the WeakMap key.
|
|
10
|
+
}
|
|
11
|
+
export type HtmlEscapedCallback = (
|
|
12
|
+
opts: HtmlEscapedCallbackOpts,
|
|
13
|
+
) => Promise<string> | undefined
|
|
14
|
+
export type HtmlEscaped = {
|
|
15
|
+
isEscaped: true
|
|
16
|
+
callbacks?: HtmlEscapedCallback[]
|
|
17
|
+
}
|
|
18
|
+
export type HtmlEscapedString = string & HtmlEscaped
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* StringBuffer contains string and Promise<string> alternately
|
|
22
|
+
* The length of the array will be odd, the odd numbered element will be a string,
|
|
23
|
+
* and the even numbered element will be a Promise<string>.
|
|
24
|
+
* When concatenating into a single string, it must be processed from the tail.
|
|
25
|
+
* @example
|
|
26
|
+
* [
|
|
27
|
+
* 'framework.',
|
|
28
|
+
* Promise.resolve('ultra fast'),
|
|
29
|
+
* 'a ',
|
|
30
|
+
* Promise.resolve('is '),
|
|
31
|
+
* 'Hono',
|
|
32
|
+
* ]
|
|
33
|
+
*/
|
|
34
|
+
export type StringBuffer = (string | Promise<string>)[]
|
|
35
|
+
export type StringBufferWithCallbacks = StringBuffer & {
|
|
36
|
+
callbacks: HtmlEscapedCallback[]
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const raw = (
|
|
40
|
+
value: unknown,
|
|
41
|
+
callbacks?: HtmlEscapedCallback[],
|
|
42
|
+
): HtmlEscapedString => {
|
|
43
|
+
const escapedString = new String(value) as HtmlEscapedString
|
|
44
|
+
escapedString.isEscaped = true
|
|
45
|
+
escapedString.callbacks = callbacks
|
|
46
|
+
|
|
47
|
+
return escapedString
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// The `escapeToBuffer` implementation is based on code from the MIT licensed `react-dom` package.
|
|
51
|
+
// https://github.com/facebook/react/blob/main/packages/react-dom-bindings/src/server/escapeTextForBrowser.js
|
|
52
|
+
|
|
53
|
+
const escapeRe = /[&<>'"]/
|
|
54
|
+
|
|
55
|
+
export const stringBufferToString = async (
|
|
56
|
+
buffer: StringBuffer,
|
|
57
|
+
callbacks: HtmlEscapedCallback[] | undefined,
|
|
58
|
+
): Promise<HtmlEscapedString> => {
|
|
59
|
+
let str = ''
|
|
60
|
+
callbacks ||= []
|
|
61
|
+
const resolvedBuffer = await Promise.all(buffer)
|
|
62
|
+
for (let i = resolvedBuffer.length - 1; ; i--) {
|
|
63
|
+
str += resolvedBuffer[i]
|
|
64
|
+
i--
|
|
65
|
+
if (i < 0) {
|
|
66
|
+
break
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let r = resolvedBuffer[i]
|
|
70
|
+
if (typeof r === 'object') {
|
|
71
|
+
callbacks.push(...((r as HtmlEscapedString).callbacks || []))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const isEscaped = (r as HtmlEscapedString).isEscaped
|
|
75
|
+
r = await (typeof r === 'object' ? (r as HtmlEscapedString).toString() : r)
|
|
76
|
+
if (typeof r === 'object') {
|
|
77
|
+
callbacks.push(...((r as HtmlEscapedString).callbacks || []))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if ((r as HtmlEscapedString).isEscaped ?? isEscaped) {
|
|
81
|
+
str += r
|
|
82
|
+
} else {
|
|
83
|
+
const buf = [str]
|
|
84
|
+
escapeToBuffer(r, buf)
|
|
85
|
+
str = buf[0]
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return raw(str, callbacks)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const escapeToBuffer = (str: string, buffer: StringBuffer): void => {
|
|
93
|
+
const match = str.search(escapeRe)
|
|
94
|
+
if (match === -1) {
|
|
95
|
+
buffer[0] += str
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let escape
|
|
100
|
+
let index
|
|
101
|
+
let lastIndex = 0
|
|
102
|
+
|
|
103
|
+
for (index = match; index < str.length; index++) {
|
|
104
|
+
switch (str.charCodeAt(index)) {
|
|
105
|
+
case 34: // "
|
|
106
|
+
escape = '"'
|
|
107
|
+
break
|
|
108
|
+
case 39: // '
|
|
109
|
+
escape = '''
|
|
110
|
+
break
|
|
111
|
+
case 38: // &
|
|
112
|
+
escape = '&'
|
|
113
|
+
break
|
|
114
|
+
case 60: // <
|
|
115
|
+
escape = '<'
|
|
116
|
+
break
|
|
117
|
+
case 62: // >
|
|
118
|
+
escape = '>'
|
|
119
|
+
break
|
|
120
|
+
default:
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
buffer[0] += str.substring(lastIndex, index) + escape
|
|
125
|
+
lastIndex = index + 1
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
buffer[0] += str.substring(lastIndex, index)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export const resolveCallbackSync = (
|
|
132
|
+
str: string | HtmlEscapedString,
|
|
133
|
+
): string => {
|
|
134
|
+
const callbacks = (str as HtmlEscapedString)
|
|
135
|
+
.callbacks as HtmlEscapedCallback[]
|
|
136
|
+
if (!callbacks?.length) {
|
|
137
|
+
return str
|
|
138
|
+
}
|
|
139
|
+
const buffer: [string] = [str]
|
|
140
|
+
const context = {}
|
|
141
|
+
|
|
142
|
+
callbacks.forEach((c) =>
|
|
143
|
+
c({ phase: HtmlEscapedCallbackPhase.Stringify, buffer, context }),
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return buffer[0]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const resolveCallback = async (
|
|
150
|
+
str: string | HtmlEscapedString | Promise<string>,
|
|
151
|
+
phase: (typeof HtmlEscapedCallbackPhase)[keyof typeof HtmlEscapedCallbackPhase],
|
|
152
|
+
preserveCallbacks: boolean,
|
|
153
|
+
context: object,
|
|
154
|
+
buffer?: [string],
|
|
155
|
+
): Promise<string> => {
|
|
156
|
+
if (typeof str === 'object' && !(str instanceof String)) {
|
|
157
|
+
if (!((str as unknown) instanceof Promise)) {
|
|
158
|
+
str = (str as unknown as string).toString() // HtmlEscapedString object to string
|
|
159
|
+
}
|
|
160
|
+
if ((str as string | Promise<string>) instanceof Promise) {
|
|
161
|
+
str = await (str as unknown as Promise<string>)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const callbacks = (str as HtmlEscapedString)
|
|
166
|
+
.callbacks as HtmlEscapedCallback[]
|
|
167
|
+
if (!callbacks?.length) {
|
|
168
|
+
return Promise.resolve(str)
|
|
169
|
+
}
|
|
170
|
+
if (buffer) {
|
|
171
|
+
buffer[0] += str
|
|
172
|
+
} else {
|
|
173
|
+
buffer = [str as string]
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const resStr = Promise.all(
|
|
177
|
+
callbacks.map((c) => c({ phase, buffer, context })),
|
|
178
|
+
).then((res) =>
|
|
179
|
+
Promise.all(
|
|
180
|
+
res
|
|
181
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
182
|
+
.filter<string>(Boolean as any)
|
|
183
|
+
.map((str) => resolveCallback(str, phase, false, context, buffer)),
|
|
184
|
+
).then(() => (buffer as [string])[0]),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
if (preserveCallbacks) {
|
|
188
|
+
return raw(await resStr, callbacks)
|
|
189
|
+
} else {
|
|
190
|
+
return resStr
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export const html = (
|
|
195
|
+
strings: TemplateStringsArray,
|
|
196
|
+
...values: unknown[]
|
|
197
|
+
): HtmlEscapedString | Promise<HtmlEscapedString> => {
|
|
198
|
+
const buffer: StringBufferWithCallbacks = [''] as StringBufferWithCallbacks
|
|
199
|
+
|
|
200
|
+
for (let i = 0, len = strings.length - 1; i < len; i++) {
|
|
201
|
+
buffer[0] += strings[i]
|
|
202
|
+
|
|
203
|
+
const children = Array.isArray(values[i])
|
|
204
|
+
? (values[i] as Array<unknown>).flat(Infinity)
|
|
205
|
+
: [values[i]]
|
|
206
|
+
for (let i = 0, len = children.length; i < len; i++) {
|
|
207
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
208
|
+
const child = children[i] as any
|
|
209
|
+
if (typeof child === 'string') {
|
|
210
|
+
escapeToBuffer(child, buffer)
|
|
211
|
+
} else if (typeof child === 'number') {
|
|
212
|
+
;(buffer[0] as string) += child
|
|
213
|
+
} else if (
|
|
214
|
+
typeof child === 'boolean' ||
|
|
215
|
+
child === null ||
|
|
216
|
+
child === undefined
|
|
217
|
+
) {
|
|
218
|
+
continue
|
|
219
|
+
} else if (
|
|
220
|
+
typeof child === 'object' &&
|
|
221
|
+
(child as HtmlEscaped).isEscaped
|
|
222
|
+
) {
|
|
223
|
+
if ((child as HtmlEscapedString).callbacks) {
|
|
224
|
+
buffer.unshift('', child)
|
|
225
|
+
} else {
|
|
226
|
+
const tmp = child.toString()
|
|
227
|
+
if (tmp instanceof Promise) {
|
|
228
|
+
buffer.unshift('', tmp)
|
|
229
|
+
} else {
|
|
230
|
+
buffer[0] += tmp
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
} else if (child instanceof Promise) {
|
|
234
|
+
buffer.unshift('', child)
|
|
235
|
+
} else {
|
|
236
|
+
escapeToBuffer(child.toString(), buffer)
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
buffer[0] += strings.at(-1) as string
|
|
241
|
+
|
|
242
|
+
return buffer.length === 1
|
|
243
|
+
? 'callbacks' in buffer
|
|
244
|
+
? raw(resolveCallbackSync(raw(buffer[0], buffer.callbacks)))
|
|
245
|
+
: raw(buffer[0])
|
|
246
|
+
: stringBufferToString(buffer, buffer.callbacks)
|
|
247
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import packageJSON from '../package.json' with { type: 'json' }
|
|
2
|
+
|
|
3
|
+
export default ({ port, refresherPort }: { port: number, refresherPort: number }) => {
|
|
4
|
+
console.log('\x1b[32m%s\x1b[0m', '╭───────────────╮')
|
|
5
|
+
console.log('\x1b[32m%s\x1b[0m', '│ Alstar Studio │')
|
|
6
|
+
console.log('\x1b[32m%s\x1b[0m', '╰───────────────╯')
|
|
7
|
+
console.log(' ')
|
|
8
|
+
console.log('\x1b[32m%s\x1b[0m', `Version:`)
|
|
9
|
+
console.log(packageJSON.version)
|
|
10
|
+
console.log(' ')
|
|
11
|
+
console.log('\x1b[32m%s\x1b[0m', `App:`)
|
|
12
|
+
console.log(`http://localhost:${port}`)
|
|
13
|
+
console.log(' ')
|
|
14
|
+
console.log('\x1b[32m%s\x1b[0m', `Studio:`)
|
|
15
|
+
console.log(`http://localhost:${port}/studio`)
|
|
16
|
+
console.log(' ')
|
|
17
|
+
console.log('\x1b[32m%s\x1b[0m', `Refresher:`)
|
|
18
|
+
console.log(`http://localhost:${refresherPort}`)
|
|
19
|
+
}
|
|
@@ -1,59 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,57 +0,0 @@
|
|
|
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
|
-
}
|
package/components/Block.ts
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
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
|
-
}
|