@kidecms/core 0.1.0
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/README.md +28 -0
- package/admin/components/AdminCard.astro +25 -0
- package/admin/components/AiGenerateButton.tsx +102 -0
- package/admin/components/AssetsGrid.tsx +711 -0
- package/admin/components/BlockEditor.tsx +996 -0
- package/admin/components/CheckboxField.tsx +31 -0
- package/admin/components/DocumentActions.tsx +317 -0
- package/admin/components/DocumentLock.tsx +54 -0
- package/admin/components/DocumentsDataTable.tsx +804 -0
- package/admin/components/FieldControl.astro +397 -0
- package/admin/components/FocalPointSelector.tsx +100 -0
- package/admin/components/ImageBrowseDialog.tsx +176 -0
- package/admin/components/ImagePicker.tsx +149 -0
- package/admin/components/InternalLinkPicker.tsx +80 -0
- package/admin/components/LiveHeading.tsx +17 -0
- package/admin/components/MobileSidebar.tsx +29 -0
- package/admin/components/RelationField.tsx +204 -0
- package/admin/components/RichTextEditor.tsx +685 -0
- package/admin/components/SelectField.tsx +65 -0
- package/admin/components/SidebarUserMenu.tsx +99 -0
- package/admin/components/SlugField.tsx +77 -0
- package/admin/components/TaxonomySelect.tsx +52 -0
- package/admin/components/Toast.astro +40 -0
- package/admin/components/TreeItemsEditor.tsx +790 -0
- package/admin/components/TreeSelect.tsx +166 -0
- package/admin/components/UnsavedGuard.tsx +181 -0
- package/admin/components/tree-utils.ts +86 -0
- package/admin/components/ui/alert-dialog.tsx +92 -0
- package/admin/components/ui/badge.tsx +83 -0
- package/admin/components/ui/button.tsx +53 -0
- package/admin/components/ui/card.tsx +70 -0
- package/admin/components/ui/checkbox.tsx +28 -0
- package/admin/components/ui/collapsible.tsx +26 -0
- package/admin/components/ui/command.tsx +88 -0
- package/admin/components/ui/dialog.tsx +92 -0
- package/admin/components/ui/dropdown-menu.tsx +259 -0
- package/admin/components/ui/input.tsx +20 -0
- package/admin/components/ui/label.tsx +20 -0
- package/admin/components/ui/popover.tsx +42 -0
- package/admin/components/ui/select.tsx +165 -0
- package/admin/components/ui/separator.tsx +21 -0
- package/admin/components/ui/sheet.tsx +104 -0
- package/admin/components/ui/skeleton.tsx +7 -0
- package/admin/components/ui/table.tsx +74 -0
- package/admin/components/ui/textarea.tsx +18 -0
- package/admin/components/ui/tooltip.tsx +52 -0
- package/admin/layouts/AdminLayout.astro +340 -0
- package/admin/lib/utils.ts +19 -0
- package/dist/admin.js +92 -0
- package/dist/ai.js +67 -0
- package/dist/api.js +827 -0
- package/dist/assets.js +163 -0
- package/dist/auth.js +132 -0
- package/dist/blocks.js +110 -0
- package/dist/content.js +29 -0
- package/dist/create-admin.js +23 -0
- package/dist/define.js +36 -0
- package/dist/generator.js +370 -0
- package/dist/image.js +69 -0
- package/dist/index.js +16 -0
- package/dist/integration.js +256 -0
- package/dist/locks.js +37 -0
- package/dist/richtext.js +1 -0
- package/dist/runtime.js +26 -0
- package/dist/schema.js +13 -0
- package/dist/seed.js +84 -0
- package/dist/values.js +102 -0
- package/middleware/auth.ts +100 -0
- package/package.json +102 -0
- package/routes/api/cms/[collection]/[...path].ts +366 -0
- package/routes/api/cms/ai/alt-text.ts +25 -0
- package/routes/api/cms/ai/seo.ts +25 -0
- package/routes/api/cms/ai/translate.ts +31 -0
- package/routes/api/cms/assets/[id].ts +82 -0
- package/routes/api/cms/assets/folders.ts +81 -0
- package/routes/api/cms/assets/index.ts +23 -0
- package/routes/api/cms/assets/upload.ts +112 -0
- package/routes/api/cms/auth/invite.ts +166 -0
- package/routes/api/cms/auth/login.ts +124 -0
- package/routes/api/cms/auth/logout.ts +33 -0
- package/routes/api/cms/auth/setup.ts +77 -0
- package/routes/api/cms/cron/publish.ts +33 -0
- package/routes/api/cms/img/[...path].ts +24 -0
- package/routes/api/cms/locks/[...path].ts +37 -0
- package/routes/api/cms/preview/render.ts +36 -0
- package/routes/api/cms/references/[collection]/[id].ts +60 -0
- package/routes/pages/admin/[...path].astro +1104 -0
- package/routes/pages/admin/assets/[id].astro +183 -0
- package/routes/pages/admin/assets/index.astro +58 -0
- package/routes/pages/admin/invite.astro +116 -0
- package/routes/pages/admin/login.astro +57 -0
- package/routes/pages/admin/setup.astro +91 -0
- package/virtual.d.ts +61 -0
package/dist/assets.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { desc, eq, isNull, sql } from "drizzle-orm";
|
|
2
|
+
import { nanoid } from "nanoid";
|
|
3
|
+
import { getDb, getStorage } from "./runtime";
|
|
4
|
+
import { getSchema } from "./schema";
|
|
5
|
+
export const assets = {
|
|
6
|
+
async upload(file, options) {
|
|
7
|
+
const db = await getDb();
|
|
8
|
+
const schema = getSchema();
|
|
9
|
+
const storage = getStorage();
|
|
10
|
+
const ext = file.name.includes(".") ? file.name.slice(file.name.lastIndexOf(".")) : "";
|
|
11
|
+
const safeName = `${nanoid(12)}${ext}`;
|
|
12
|
+
const storagePath = `/uploads/${safeName}`;
|
|
13
|
+
await storage.putFile(storagePath, new Uint8Array(await file.arrayBuffer()));
|
|
14
|
+
const id = nanoid();
|
|
15
|
+
const createdAt = new Date().toISOString();
|
|
16
|
+
const folder = options?.folder || null;
|
|
17
|
+
await db.insert(schema.cmsAssets).values({
|
|
18
|
+
_id: id,
|
|
19
|
+
filename: file.name,
|
|
20
|
+
mimeType: file.type || "application/octet-stream",
|
|
21
|
+
size: file.size,
|
|
22
|
+
width: null,
|
|
23
|
+
height: null,
|
|
24
|
+
alt: options?.alt ?? null,
|
|
25
|
+
folder,
|
|
26
|
+
storagePath,
|
|
27
|
+
_createdAt: createdAt,
|
|
28
|
+
});
|
|
29
|
+
return {
|
|
30
|
+
_id: id,
|
|
31
|
+
filename: file.name,
|
|
32
|
+
mimeType: file.type || "application/octet-stream",
|
|
33
|
+
size: file.size,
|
|
34
|
+
width: null,
|
|
35
|
+
height: null,
|
|
36
|
+
focalX: null,
|
|
37
|
+
focalY: null,
|
|
38
|
+
alt: options?.alt ?? null,
|
|
39
|
+
folder,
|
|
40
|
+
storagePath,
|
|
41
|
+
url: storagePath,
|
|
42
|
+
_createdAt: createdAt,
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
async find(options = {}) {
|
|
46
|
+
const db = await getDb();
|
|
47
|
+
const schema = getSchema();
|
|
48
|
+
const condition = options.folder !== undefined
|
|
49
|
+
? options.folder === null
|
|
50
|
+
? isNull(schema.cmsAssets.folder)
|
|
51
|
+
: eq(schema.cmsAssets.folder, options.folder)
|
|
52
|
+
: undefined;
|
|
53
|
+
let query = db.select().from(schema.cmsAssets).where(condition).orderBy(desc(schema.cmsAssets._createdAt));
|
|
54
|
+
if (options.limit)
|
|
55
|
+
query = query.limit(options.limit);
|
|
56
|
+
if (options.offset)
|
|
57
|
+
query = query.offset(options.offset);
|
|
58
|
+
const rows = await query;
|
|
59
|
+
return rows.map((row) => ({ ...row, url: row.storagePath }));
|
|
60
|
+
},
|
|
61
|
+
async findById(id) {
|
|
62
|
+
const db = await getDb();
|
|
63
|
+
const schema = getSchema();
|
|
64
|
+
const rows = await db.select().from(schema.cmsAssets).where(eq(schema.cmsAssets._id, id)).limit(1);
|
|
65
|
+
if (rows.length === 0)
|
|
66
|
+
return null;
|
|
67
|
+
const row = rows[0];
|
|
68
|
+
return { ...row, url: row.storagePath };
|
|
69
|
+
},
|
|
70
|
+
async findByUrl(url) {
|
|
71
|
+
const db = await getDb();
|
|
72
|
+
const schema = getSchema();
|
|
73
|
+
const rows = await db.select().from(schema.cmsAssets).where(eq(schema.cmsAssets.storagePath, url)).limit(1);
|
|
74
|
+
if (rows.length === 0)
|
|
75
|
+
return null;
|
|
76
|
+
const row = rows[0];
|
|
77
|
+
return { ...row, url: row.storagePath };
|
|
78
|
+
},
|
|
79
|
+
async delete(id) {
|
|
80
|
+
const db = await getDb();
|
|
81
|
+
const schema = getSchema();
|
|
82
|
+
const storage = getStorage();
|
|
83
|
+
const rows = await db.select().from(schema.cmsAssets).where(eq(schema.cmsAssets._id, id)).limit(1);
|
|
84
|
+
if (rows.length === 0)
|
|
85
|
+
return;
|
|
86
|
+
const asset = rows[0];
|
|
87
|
+
await storage.deleteFile(asset.storagePath);
|
|
88
|
+
await db.delete(schema.cmsAssets).where(eq(schema.cmsAssets._id, id));
|
|
89
|
+
},
|
|
90
|
+
async update(id, data) {
|
|
91
|
+
const db = await getDb();
|
|
92
|
+
const schema = getSchema();
|
|
93
|
+
const rows = await db.select().from(schema.cmsAssets).where(eq(schema.cmsAssets._id, id)).limit(1);
|
|
94
|
+
if (rows.length === 0)
|
|
95
|
+
return null;
|
|
96
|
+
const updateValues = {};
|
|
97
|
+
if (data.alt !== undefined)
|
|
98
|
+
updateValues.alt = data.alt;
|
|
99
|
+
if (data.filename !== undefined)
|
|
100
|
+
updateValues.filename = data.filename;
|
|
101
|
+
if (data.folder !== undefined)
|
|
102
|
+
updateValues.folder = data.folder;
|
|
103
|
+
if (data.focalX !== undefined)
|
|
104
|
+
updateValues.focalX = data.focalX;
|
|
105
|
+
if (data.focalY !== undefined)
|
|
106
|
+
updateValues.focalY = data.focalY;
|
|
107
|
+
if (Object.keys(updateValues).length > 0) {
|
|
108
|
+
await db.update(schema.cmsAssets).set(updateValues).where(eq(schema.cmsAssets._id, id));
|
|
109
|
+
}
|
|
110
|
+
return this.findById(id);
|
|
111
|
+
},
|
|
112
|
+
async count() {
|
|
113
|
+
const db = await getDb();
|
|
114
|
+
const schema = getSchema();
|
|
115
|
+
const rows = await db.select({ count: sql `count(*)` }).from(schema.cmsAssets);
|
|
116
|
+
return Number(rows[0]?.count ?? 0);
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
export const folders = {
|
|
120
|
+
async create(name, parent) {
|
|
121
|
+
const db = await getDb();
|
|
122
|
+
const schema = getSchema();
|
|
123
|
+
const id = nanoid();
|
|
124
|
+
const createdAt = new Date().toISOString();
|
|
125
|
+
await db.insert(schema.cmsAssetFolders).values({
|
|
126
|
+
_id: id,
|
|
127
|
+
name,
|
|
128
|
+
parent: parent ?? null,
|
|
129
|
+
_createdAt: createdAt,
|
|
130
|
+
});
|
|
131
|
+
return { _id: id, name, parent: parent ?? null, _createdAt: createdAt };
|
|
132
|
+
},
|
|
133
|
+
async findByParent(parent) {
|
|
134
|
+
const db = await getDb();
|
|
135
|
+
const schema = getSchema();
|
|
136
|
+
const condition = parent === null ? isNull(schema.cmsAssetFolders.parent) : eq(schema.cmsAssetFolders.parent, parent);
|
|
137
|
+
const rows = await db.select().from(schema.cmsAssetFolders).where(condition).orderBy(schema.cmsAssetFolders.name);
|
|
138
|
+
return rows;
|
|
139
|
+
},
|
|
140
|
+
async findById(id) {
|
|
141
|
+
const db = await getDb();
|
|
142
|
+
const schema = getSchema();
|
|
143
|
+
const rows = await db.select().from(schema.cmsAssetFolders).where(eq(schema.cmsAssetFolders._id, id)).limit(1);
|
|
144
|
+
return rows.length > 0 ? rows[0] : null;
|
|
145
|
+
},
|
|
146
|
+
async findAll() {
|
|
147
|
+
const db = await getDb();
|
|
148
|
+
const schema = getSchema();
|
|
149
|
+
const rows = await db.select().from(schema.cmsAssetFolders).orderBy(schema.cmsAssetFolders.name);
|
|
150
|
+
return rows;
|
|
151
|
+
},
|
|
152
|
+
async rename(id, name) {
|
|
153
|
+
const db = await getDb();
|
|
154
|
+
const schema = getSchema();
|
|
155
|
+
await db.update(schema.cmsAssetFolders).set({ name }).where(eq(schema.cmsAssetFolders._id, id));
|
|
156
|
+
return this.findById(id);
|
|
157
|
+
},
|
|
158
|
+
async delete(id) {
|
|
159
|
+
const db = await getDb();
|
|
160
|
+
const schema = getSchema();
|
|
161
|
+
await db.delete(schema.cmsAssetFolders).where(eq(schema.cmsAssetFolders._id, id));
|
|
162
|
+
},
|
|
163
|
+
};
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { eq } from "drizzle-orm";
|
|
2
|
+
import { nanoid } from "nanoid";
|
|
3
|
+
import { getDb } from "./runtime";
|
|
4
|
+
import { getSchema } from "./schema";
|
|
5
|
+
const ITERATIONS = 100_000;
|
|
6
|
+
const HASH_LENGTH = 32;
|
|
7
|
+
const SALT_LENGTH = 16;
|
|
8
|
+
const encode = (buffer) => btoa(String.fromCharCode(...new Uint8Array(buffer)));
|
|
9
|
+
const decode = (base64) => Uint8Array.from(atob(base64), (char) => char.charCodeAt(0));
|
|
10
|
+
const deriveKey = async (plain, salt) => {
|
|
11
|
+
const keyMaterial = await crypto.subtle.importKey("raw", new TextEncoder().encode(plain), "PBKDF2", false, [
|
|
12
|
+
"deriveBits",
|
|
13
|
+
]);
|
|
14
|
+
return crypto.subtle.deriveBits({ name: "PBKDF2", salt: salt.buffer, iterations: ITERATIONS, hash: "SHA-256" }, keyMaterial, HASH_LENGTH * 8);
|
|
15
|
+
};
|
|
16
|
+
export const hashPassword = async (plain) => {
|
|
17
|
+
const salt = crypto.getRandomValues(new Uint8Array(SALT_LENGTH));
|
|
18
|
+
const derived = await deriveKey(plain, salt);
|
|
19
|
+
return `pbkdf2:${ITERATIONS}:${encode(salt.buffer)}:${encode(derived)}`;
|
|
20
|
+
};
|
|
21
|
+
export const verifyPassword = async (hash, plain) => {
|
|
22
|
+
const [, iterStr, saltB64, hashB64] = hash.split(":");
|
|
23
|
+
if (!iterStr || !saltB64 || !hashB64)
|
|
24
|
+
return false;
|
|
25
|
+
const salt = decode(saltB64);
|
|
26
|
+
const expected = decode(hashB64);
|
|
27
|
+
const keyMaterial = await crypto.subtle.importKey("raw", new TextEncoder().encode(plain), "PBKDF2", false, [
|
|
28
|
+
"deriveBits",
|
|
29
|
+
]);
|
|
30
|
+
const derived = new Uint8Array(await crypto.subtle.deriveBits({ name: "PBKDF2", salt, iterations: Number(iterStr), hash: "SHA-256" }, keyMaterial, expected.length * 8));
|
|
31
|
+
if (derived.length !== expected.length)
|
|
32
|
+
return false;
|
|
33
|
+
let diff = 0;
|
|
34
|
+
for (let i = 0; i < derived.length; i++)
|
|
35
|
+
diff |= derived[i] ^ expected[i];
|
|
36
|
+
return diff === 0;
|
|
37
|
+
};
|
|
38
|
+
export const createSession = async (userId) => {
|
|
39
|
+
const db = await getDb();
|
|
40
|
+
const schema = getSchema();
|
|
41
|
+
const token = nanoid(32);
|
|
42
|
+
const expiresAt = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
43
|
+
await db.insert(schema.cmsSessions).values({
|
|
44
|
+
_id: token,
|
|
45
|
+
userId,
|
|
46
|
+
expiresAt,
|
|
47
|
+
});
|
|
48
|
+
return { token, expiresAt };
|
|
49
|
+
};
|
|
50
|
+
export const validateSession = async (token) => {
|
|
51
|
+
const db = await getDb();
|
|
52
|
+
const schema = getSchema();
|
|
53
|
+
const rows = await db.select().from(schema.cmsSessions).where(eq(schema.cmsSessions._id, token)).limit(1);
|
|
54
|
+
if (rows.length === 0)
|
|
55
|
+
return null;
|
|
56
|
+
const session = rows[0];
|
|
57
|
+
if (new Date(session.expiresAt) < new Date()) {
|
|
58
|
+
await db.delete(schema.cmsSessions).where(eq(schema.cmsSessions._id, token));
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return { userId: session.userId, expiresAt: session.expiresAt };
|
|
62
|
+
};
|
|
63
|
+
export const destroySession = async (token) => {
|
|
64
|
+
const db = await getDb();
|
|
65
|
+
const schema = getSchema();
|
|
66
|
+
await db.delete(schema.cmsSessions).where(eq(schema.cmsSessions._id, token));
|
|
67
|
+
};
|
|
68
|
+
export const getSessionUser = async (request) => {
|
|
69
|
+
const cookieHeader = request.headers.get("cookie") ?? "";
|
|
70
|
+
const match = cookieHeader.match(/cms_session=([^;]+)/);
|
|
71
|
+
if (!match)
|
|
72
|
+
return null;
|
|
73
|
+
const session = await validateSession(match[1]);
|
|
74
|
+
if (!session)
|
|
75
|
+
return null;
|
|
76
|
+
const db = await getDb();
|
|
77
|
+
const schema = getSchema();
|
|
78
|
+
const tables = schema.cmsTables;
|
|
79
|
+
if (!tables.users)
|
|
80
|
+
return null;
|
|
81
|
+
const userRows = await db.select().from(tables.users.main).where(eq(tables.users.main._id, session.userId)).limit(1);
|
|
82
|
+
if (userRows.length === 0)
|
|
83
|
+
return null;
|
|
84
|
+
const user = userRows[0];
|
|
85
|
+
return {
|
|
86
|
+
id: String(user._id),
|
|
87
|
+
email: String(user.email),
|
|
88
|
+
name: String(user.name),
|
|
89
|
+
role: String(user.role),
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
const INVITE_EXPIRY_DAYS = 7;
|
|
93
|
+
export const createInvite = async (userId) => {
|
|
94
|
+
const db = await getDb();
|
|
95
|
+
const schema = getSchema();
|
|
96
|
+
const token = nanoid(32);
|
|
97
|
+
const expiresAt = new Date(Date.now() + INVITE_EXPIRY_DAYS * 24 * 60 * 60 * 1000).toISOString();
|
|
98
|
+
await db.insert(schema.cmsInvites).values({
|
|
99
|
+
_id: nanoid(),
|
|
100
|
+
userId,
|
|
101
|
+
token,
|
|
102
|
+
expiresAt,
|
|
103
|
+
});
|
|
104
|
+
return { token, expiresAt };
|
|
105
|
+
};
|
|
106
|
+
export const validateInvite = async (token) => {
|
|
107
|
+
const db = await getDb();
|
|
108
|
+
const schema = getSchema();
|
|
109
|
+
const rows = await db.select().from(schema.cmsInvites).where(eq(schema.cmsInvites.token, token)).limit(1);
|
|
110
|
+
if (rows.length === 0)
|
|
111
|
+
return null;
|
|
112
|
+
const invite = rows[0];
|
|
113
|
+
if (invite.usedAt)
|
|
114
|
+
return null;
|
|
115
|
+
if (new Date(invite.expiresAt) < new Date())
|
|
116
|
+
return null;
|
|
117
|
+
return { userId: invite.userId, expiresAt: invite.expiresAt };
|
|
118
|
+
};
|
|
119
|
+
export const consumeInvite = async (token) => {
|
|
120
|
+
const db = await getDb();
|
|
121
|
+
const schema = getSchema();
|
|
122
|
+
await db
|
|
123
|
+
.update(schema.cmsInvites)
|
|
124
|
+
.set({ usedAt: new Date().toISOString() })
|
|
125
|
+
.where(eq(schema.cmsInvites.token, token));
|
|
126
|
+
};
|
|
127
|
+
export const SESSION_COOKIE_NAME = "cms_session";
|
|
128
|
+
export const setSessionCookie = (token, expiresAt) => {
|
|
129
|
+
const secure = process.env.NODE_ENV === "production" ? "; Secure" : "";
|
|
130
|
+
return `${SESSION_COOKIE_NAME}=${token}; Path=/; HttpOnly; SameSite=Strict${secure}; Expires=${new Date(expiresAt).toUTCString()}`;
|
|
131
|
+
};
|
|
132
|
+
export const clearSessionCookie = () => `${SESSION_COOKIE_NAME}=; Path=/; HttpOnly; SameSite=Strict; Max-Age=0`;
|
package/dist/blocks.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { cmsImage, cmsSrcset } from "./image";
|
|
2
|
+
import { renderRichText } from "./richtext";
|
|
3
|
+
function esc(value) {
|
|
4
|
+
return String(value ?? "")
|
|
5
|
+
.replace(/&/g, "&")
|
|
6
|
+
.replace(/</g, "<")
|
|
7
|
+
.replace(/>/g, ">")
|
|
8
|
+
.replace(/"/g, """);
|
|
9
|
+
}
|
|
10
|
+
function parseJson(value) {
|
|
11
|
+
if (typeof value === "string") {
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(value);
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return value;
|
|
20
|
+
}
|
|
21
|
+
function isRichText(value) {
|
|
22
|
+
const parsed = parseJson(value);
|
|
23
|
+
return parsed && typeof parsed === "object" && parsed.type === "root";
|
|
24
|
+
}
|
|
25
|
+
function isImageUrl(value) {
|
|
26
|
+
const stringValue = String(value ?? "");
|
|
27
|
+
return stringValue.startsWith("/uploads/") || stringValue.startsWith("http://") || stringValue.startsWith("https://");
|
|
28
|
+
}
|
|
29
|
+
function isImageArray(value) {
|
|
30
|
+
const parsed = parseJson(value);
|
|
31
|
+
return Array.isArray(parsed) && parsed.length > 0 && parsed.every((entry) => isImageUrl(entry));
|
|
32
|
+
}
|
|
33
|
+
function isRepeaterArray(value) {
|
|
34
|
+
const parsed = parseJson(value);
|
|
35
|
+
return Array.isArray(parsed) && parsed.length > 0 && typeof parsed[0] === "object" && parsed[0] !== null;
|
|
36
|
+
}
|
|
37
|
+
function renderImage(src) {
|
|
38
|
+
if (src.startsWith("/uploads/")) {
|
|
39
|
+
return `<img src="${esc(cmsImage(src, 1024))}" srcset="${esc(cmsSrcset(src))}" sizes="(max-width: 768px) 100vw, 768px" alt="" loading="lazy" class="h-auto w-full rounded-lg object-cover" />`;
|
|
40
|
+
}
|
|
41
|
+
return `<img src="${esc(src)}" alt="" loading="lazy" class="w-full rounded-lg object-cover" />`;
|
|
42
|
+
}
|
|
43
|
+
function renderFieldValue(key, value) {
|
|
44
|
+
if (value === null || value === undefined || value === "")
|
|
45
|
+
return "";
|
|
46
|
+
const parsed = parseJson(value);
|
|
47
|
+
if (isRichText(value)) {
|
|
48
|
+
return `<div class="prose">${renderRichText(parsed)}</div>`;
|
|
49
|
+
}
|
|
50
|
+
if (isImageArray(value)) {
|
|
51
|
+
const images = Array.isArray(parsed) ? parsed : [];
|
|
52
|
+
return `<div class="grid gap-4">${images.map((src) => renderImage(String(src))).join("")}</div>`;
|
|
53
|
+
}
|
|
54
|
+
if (isImageUrl(value)) {
|
|
55
|
+
return renderImage(String(value));
|
|
56
|
+
}
|
|
57
|
+
if (isRepeaterArray(value)) {
|
|
58
|
+
const items = Array.isArray(parsed) ? parsed : [];
|
|
59
|
+
let html = `<div class="grid gap-3">`;
|
|
60
|
+
for (const item of items) {
|
|
61
|
+
html += `<div class="rounded-lg border border-gray-200 bg-gray-50 px-5 py-4">`;
|
|
62
|
+
for (const [itemKey, itemValue] of Object.entries(item)) {
|
|
63
|
+
if (itemKey === "_key" || itemKey === "id" || !itemValue)
|
|
64
|
+
continue;
|
|
65
|
+
const className = itemKey.includes("description") || itemKey.includes("answer") || itemKey.includes("body")
|
|
66
|
+
? "m-0 text-gray-500"
|
|
67
|
+
: "mb-1 font-semibold";
|
|
68
|
+
html += `<p class="${className}">${esc(itemValue)}</p>`;
|
|
69
|
+
}
|
|
70
|
+
html += `</div>`;
|
|
71
|
+
}
|
|
72
|
+
html += `</div>`;
|
|
73
|
+
return html;
|
|
74
|
+
}
|
|
75
|
+
if (key === "heading" || key === "title") {
|
|
76
|
+
return `<h2 class="mb-3 text-xl font-semibold">${esc(value)}</h2>`;
|
|
77
|
+
}
|
|
78
|
+
if (key === "eyebrow" || key === "label") {
|
|
79
|
+
return `<p class="mb-1 text-xs font-semibold uppercase tracking-wide text-teal-700">${esc(value)}</p>`;
|
|
80
|
+
}
|
|
81
|
+
if (key === "ctaLabel" || key === "ctaHref") {
|
|
82
|
+
return "";
|
|
83
|
+
}
|
|
84
|
+
return `<p class="text-gray-500">${esc(value)}</p>`;
|
|
85
|
+
}
|
|
86
|
+
export function renderBlock(block) {
|
|
87
|
+
const { type: _type, _key, ...fields } = block;
|
|
88
|
+
let html = `<section>`;
|
|
89
|
+
if (fields.eyebrow) {
|
|
90
|
+
html += renderFieldValue("eyebrow", fields.eyebrow);
|
|
91
|
+
}
|
|
92
|
+
for (const key of ["heading", "title"]) {
|
|
93
|
+
if (fields[key]) {
|
|
94
|
+
html += renderFieldValue(key, fields[key]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
98
|
+
if (["eyebrow", "heading", "title", "ctaLabel", "ctaHref"].includes(key))
|
|
99
|
+
continue;
|
|
100
|
+
html += renderFieldValue(key, value);
|
|
101
|
+
}
|
|
102
|
+
if (fields.ctaLabel && fields.ctaHref) {
|
|
103
|
+
html += `<a href="${esc(fields.ctaHref)}" class="mt-4 inline-block rounded-md bg-teal-700 px-4 py-2 text-sm text-white no-underline">${esc(fields.ctaLabel)}</a>`;
|
|
104
|
+
}
|
|
105
|
+
html += `</section>`;
|
|
106
|
+
return html;
|
|
107
|
+
}
|
|
108
|
+
export function renderBlocks(blocks) {
|
|
109
|
+
return blocks.map(renderBlock).join("");
|
|
110
|
+
}
|
package/dist/content.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function parseBlocks(value) {
|
|
2
|
+
if (!value)
|
|
3
|
+
return [];
|
|
4
|
+
try {
|
|
5
|
+
const parsed = typeof value === "string" ? JSON.parse(value) : value;
|
|
6
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
7
|
+
}
|
|
8
|
+
catch {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function parseList(value) {
|
|
13
|
+
if (Array.isArray(value))
|
|
14
|
+
return value;
|
|
15
|
+
if (typeof value === "string") {
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(value);
|
|
18
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return [];
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
export function cacheTags(collection, docId) {
|
|
27
|
+
const singular = collection.endsWith("s") ? collection.slice(0, -1) : collection;
|
|
28
|
+
return [collection, `${singular}:${docId}`];
|
|
29
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { nanoid } from "nanoid";
|
|
2
|
+
import { hashPassword } from "./auth";
|
|
3
|
+
import { getDb } from "./runtime";
|
|
4
|
+
import { getSchema } from "./schema";
|
|
5
|
+
export const createAdminUser = async (input) => {
|
|
6
|
+
const db = await getDb();
|
|
7
|
+
const schema = getSchema();
|
|
8
|
+
const tables = schema.cmsTables;
|
|
9
|
+
if (!tables.users) {
|
|
10
|
+
throw new Error("No users collection found.");
|
|
11
|
+
}
|
|
12
|
+
const now = new Date().toISOString();
|
|
13
|
+
const hashedPassword = await hashPassword(input.password);
|
|
14
|
+
await db.insert(tables.users.main).values({
|
|
15
|
+
_id: nanoid(),
|
|
16
|
+
name: input.name,
|
|
17
|
+
email: input.email,
|
|
18
|
+
password: hashedPassword,
|
|
19
|
+
role: "admin",
|
|
20
|
+
_createdAt: now,
|
|
21
|
+
_updatedAt: now,
|
|
22
|
+
});
|
|
23
|
+
};
|
package/dist/define.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const hasRole = (...roles) => ({ user }) => !!user?.role && roles.includes(user.role);
|
|
2
|
+
const createField = (type, options) => ({ type, ...(options ?? {}) });
|
|
3
|
+
export const fields = {
|
|
4
|
+
text: (options) => createField("text", options),
|
|
5
|
+
slug: (options) => createField("slug", options),
|
|
6
|
+
email: (options) => createField("email", options),
|
|
7
|
+
number: (options) => createField("number", options),
|
|
8
|
+
boolean: (options) => createField("boolean", options),
|
|
9
|
+
date: (options) => createField("date", options),
|
|
10
|
+
select: (options) => createField("select", options),
|
|
11
|
+
richText: (options) => createField("richText", options),
|
|
12
|
+
image: (options) => createField("image", options),
|
|
13
|
+
relation: (options) => createField("relation", options),
|
|
14
|
+
array: (options) => createField("array", options),
|
|
15
|
+
json: (options) => createField("json", options),
|
|
16
|
+
blocks: (options) => createField("blocks", options),
|
|
17
|
+
};
|
|
18
|
+
export const defineCollection = (collection) => collection;
|
|
19
|
+
export const defineConfig = (config) => config;
|
|
20
|
+
export const getCollectionMap = (config) => Object.fromEntries(config.collections.map((collection) => [collection.slug, collection]));
|
|
21
|
+
export const getDefaultLocale = (config) => config.locales?.default ?? null;
|
|
22
|
+
export const getTranslatableFieldNames = (collection) => Object.entries(collection.fields)
|
|
23
|
+
.filter(([, field]) => field.translatable)
|
|
24
|
+
.map(([name]) => name);
|
|
25
|
+
export const isStructuralField = (field) => ["number", "boolean", "relation", "image", "date"].includes(field.type);
|
|
26
|
+
export const getCollectionLabel = (collection) => collection.labels.plural;
|
|
27
|
+
export const getLabelField = (collection) => {
|
|
28
|
+
if (collection.labelField && collection.labelField in collection.fields)
|
|
29
|
+
return collection.labelField;
|
|
30
|
+
if ("title" in collection.fields)
|
|
31
|
+
return "title";
|
|
32
|
+
if ("name" in collection.fields)
|
|
33
|
+
return "name";
|
|
34
|
+
const firstTextField = Object.entries(collection.fields).find(([, field]) => field.type === "text");
|
|
35
|
+
return firstTextField ? firstTextField[0] : Object.keys(collection.fields)[0];
|
|
36
|
+
};
|