@jant/core 0.3.36 → 0.3.37
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/bin/commands/export.js +1 -1
- package/bin/commands/import-site.js +529 -0
- package/bin/commands/reset-password.js +3 -2
- package/dist/client/assets/heic-to-DIRPI3VF.js +1 -0
- package/dist/client/assets/url-FWFqPJPb.js +1 -0
- package/dist/client/client.css +1 -1
- package/dist/client/client.js +4012 -3276
- package/dist/index.js +10285 -5809
- package/package.json +11 -3
- package/src/__tests__/helpers/app.ts +9 -9
- package/src/__tests__/helpers/db.ts +91 -93
- package/src/app.tsx +157 -27
- package/src/auth.ts +20 -2
- package/src/client/archive-nav.js +187 -0
- package/src/client/audio-player.ts +478 -0
- package/src/client/audio-processor.ts +84 -0
- package/src/client/avatar-upload.ts +3 -2
- package/src/client/components/__tests__/jant-compose-dialog.test.ts +645 -49
- package/src/client/components/__tests__/jant-compose-editor.test.ts +208 -16
- package/src/client/components/__tests__/jant-post-form.test.ts +19 -9
- package/src/client/components/__tests__/jant-settings-avatar.test.ts +2 -2
- package/src/client/components/__tests__/jant-settings-general.test.ts +3 -3
- package/src/client/components/collection-sidebar-types.ts +7 -9
- package/src/client/components/compose-types.ts +101 -4
- package/src/client/components/jant-collection-form.ts +43 -7
- package/src/client/components/jant-collection-sidebar.ts +88 -84
- package/src/client/components/jant-compose-dialog.ts +1655 -219
- package/src/client/components/jant-compose-editor.ts +732 -168
- package/src/client/components/jant-compose-fullscreen.ts +23 -78
- package/src/client/components/jant-media-lightbox.ts +2 -0
- package/src/client/components/jant-nav-manager.ts +24 -284
- package/src/client/components/jant-post-form.ts +89 -9
- package/src/client/components/jant-post-menu.ts +1019 -0
- package/src/client/components/jant-settings-avatar.ts +3 -3
- package/src/client/components/jant-settings-general.ts +38 -4
- package/src/client/components/jant-text-preview.ts +232 -0
- package/src/client/components/nav-manager-types.ts +4 -19
- package/src/client/components/post-form-template.ts +107 -12
- package/src/client/components/post-form-types.ts +11 -4
- package/src/client/compose-bridge.ts +410 -109
- package/src/client/image-processor.ts +26 -8
- package/src/client/media-metadata.ts +247 -0
- package/src/client/multipart-upload.ts +160 -0
- package/src/client/post-form-bridge.ts +52 -1
- package/src/client/settings-bridge.ts +0 -12
- package/src/client/thread-context.ts +140 -0
- package/src/client/tiptap/create-editor.ts +46 -0
- package/src/client/tiptap/extensions.ts +5 -0
- package/src/client/tiptap/image-node.ts +2 -8
- package/src/client/tiptap/paste-image.ts +2 -13
- package/src/client/tiptap/slash-commands.ts +173 -63
- package/src/client/toast.ts +101 -3
- package/src/client/types/sortablejs.d.ts +15 -0
- package/src/client/upload-with-metadata.ts +54 -0
- package/src/client/video-processor.ts +207 -0
- package/src/client.ts +5 -2
- package/src/db/__tests__/migrations.test.ts +118 -0
- package/src/db/index.ts +52 -0
- package/src/db/migrations/0000_baseline.sql +269 -0
- package/src/db/migrations/0001_fts_setup.sql +31 -0
- package/src/db/migrations/meta/0000_snapshot.json +703 -119
- package/src/db/migrations/meta/0001_snapshot.json +1337 -0
- package/src/db/migrations/meta/_journal.json +4 -39
- package/src/db/schema.ts +409 -145
- package/src/i18n/__tests__/detect.test.ts +115 -0
- package/src/i18n/context.tsx +2 -2
- package/src/i18n/detect.ts +85 -1
- package/src/i18n/i18n.ts +1 -1
- package/src/i18n/index.ts +2 -1
- package/src/i18n/locales/en.po +487 -1217
- package/src/i18n/locales/en.ts +1 -1
- package/src/i18n/locales/zh-Hans.po +613 -996
- package/src/i18n/locales/zh-Hans.ts +1 -1
- package/src/i18n/locales/zh-Hant.po +624 -1007
- package/src/i18n/locales/zh-Hant.ts +1 -1
- package/src/i18n/middleware.ts +6 -0
- package/src/index.ts +5 -7
- package/src/lib/__tests__/blurhash-placeholder.test.ts +75 -0
- package/src/lib/__tests__/constants.test.ts +0 -1
- package/src/lib/__tests__/markdown-to-tiptap.test.ts +358 -0
- package/src/lib/__tests__/nanoid.test.ts +26 -0
- package/src/lib/__tests__/schemas.test.ts +181 -63
- package/src/lib/__tests__/slug.test.ts +126 -0
- package/src/lib/__tests__/sse.test.ts +6 -6
- package/src/lib/__tests__/summary.test.ts +264 -0
- package/src/lib/__tests__/theme.test.ts +1 -1
- package/src/lib/__tests__/timeline.test.ts +33 -30
- package/src/lib/__tests__/tiptap-to-markdown.test.ts +346 -0
- package/src/lib/__tests__/view.test.ts +141 -66
- package/src/lib/blurhash-placeholder.ts +102 -0
- package/src/lib/constants.ts +3 -1
- package/src/lib/emoji-catalog.ts +885 -68
- package/src/lib/errors.ts +11 -8
- package/src/lib/feed.ts +78 -32
- package/src/lib/html.ts +2 -1
- package/src/lib/icon-catalog.ts +5033 -1
- package/src/lib/icons.ts +3 -2
- package/src/lib/index.ts +0 -1
- package/src/lib/markdown-to-tiptap.ts +286 -0
- package/src/lib/media-helpers.ts +12 -3
- package/src/lib/nanoid.ts +29 -0
- package/src/lib/navigation.ts +1 -1
- package/src/lib/render.tsx +20 -2
- package/src/lib/resolve-config.ts +6 -2
- package/src/lib/schemas.ts +224 -55
- package/src/lib/search-snippet.ts +34 -0
- package/src/lib/slug.ts +96 -0
- package/src/lib/sse.ts +6 -6
- package/src/lib/storage.ts +115 -7
- package/src/lib/summary.ts +66 -0
- package/src/lib/theme.ts +11 -8
- package/src/lib/timeline.ts +74 -34
- package/src/lib/tiptap-render.ts +5 -10
- package/src/lib/tiptap-to-markdown.ts +305 -0
- package/src/lib/upload.ts +190 -29
- package/src/lib/url.ts +31 -0
- package/src/lib/view.ts +204 -37
- package/src/middleware/__tests__/auth.test.ts +191 -11
- package/src/middleware/__tests__/onboarding.test.ts +12 -10
- package/src/middleware/auth.ts +63 -9
- package/src/middleware/onboarding.ts +1 -1
- package/src/middleware/secure-headers.ts +40 -0
- package/src/preset.css +45 -2
- package/src/routes/__tests__/compose.test.ts +17 -24
- package/src/routes/api/__tests__/collections.test.ts +109 -61
- package/src/routes/api/__tests__/nav-items.test.ts +46 -29
- package/src/routes/api/__tests__/posts.test.ts +132 -68
- package/src/routes/api/__tests__/search.test.ts +15 -2
- package/src/routes/api/__tests__/upload-multipart.test.ts +534 -0
- package/src/routes/api/collections.ts +51 -42
- package/src/routes/api/custom-urls.ts +80 -0
- package/src/routes/api/export.ts +31 -0
- package/src/routes/api/nav-items.ts +23 -19
- package/src/routes/api/posts.ts +43 -39
- package/src/routes/api/search.ts +3 -4
- package/src/routes/api/upload-multipart.ts +245 -0
- package/src/routes/api/upload.ts +85 -19
- package/src/routes/auth/__tests__/setup.test.ts +20 -60
- package/src/routes/auth/setup.tsx +26 -33
- package/src/routes/auth/signin.tsx +3 -7
- package/src/routes/compose.tsx +10 -55
- package/src/routes/dash/__tests__/settings-avatar.test.ts +1 -1
- package/src/routes/dash/custom-urls.tsx +414 -0
- package/src/routes/dash/settings.tsx +304 -232
- package/src/routes/feed/__tests__/rss.test.ts +27 -28
- package/src/routes/feed/rss.ts +6 -4
- package/src/routes/feed/sitemap.ts +2 -12
- package/src/routes/pages/__tests__/collections.test.ts +5 -6
- package/src/routes/pages/__tests__/featured.test.ts +41 -22
- package/src/routes/pages/archive.tsx +175 -39
- package/src/routes/pages/collection.tsx +22 -10
- package/src/routes/pages/collections.tsx +3 -3
- package/src/routes/pages/featured.tsx +28 -4
- package/src/routes/pages/home.tsx +16 -15
- package/src/routes/pages/latest.tsx +1 -11
- package/src/routes/pages/new.tsx +39 -0
- package/src/routes/pages/page.tsx +95 -49
- package/src/routes/pages/search.tsx +1 -1
- package/src/services/__tests__/api-token.test.ts +135 -0
- package/src/services/__tests__/collection.test.ts +275 -227
- package/src/services/__tests__/custom-url.test.ts +213 -0
- package/src/services/__tests__/media.test.ts +162 -22
- package/src/services/__tests__/navigation.test.ts +109 -68
- package/src/services/__tests__/post-timeline.test.ts +205 -32
- package/src/services/__tests__/post.test.ts +713 -234
- package/src/services/__tests__/search.test.ts +67 -10
- package/src/services/api-token.ts +166 -0
- package/src/services/auth.ts +17 -2
- package/src/services/collection.ts +397 -131
- package/src/services/custom-url.ts +188 -0
- package/src/services/export.ts +802 -0
- package/src/services/index.ts +26 -19
- package/src/services/media.ts +100 -22
- package/src/services/navigation.ts +158 -47
- package/src/services/path.ts +339 -0
- package/src/services/post.ts +687 -154
- package/src/services/search.ts +160 -75
- package/src/styles/components.css +58 -7
- package/src/styles/tokens.css +84 -6
- package/src/styles/ui.css +2964 -457
- package/src/types/bindings.ts +4 -1
- package/src/types/config.ts +12 -4
- package/src/types/constants.ts +15 -3
- package/src/types/entities.ts +74 -35
- package/src/types/operations.ts +11 -24
- package/src/types/props.ts +51 -16
- package/src/types/views.ts +45 -22
- package/src/ui/color-themes.ts +133 -23
- package/src/ui/compose/ComposeDialog.tsx +239 -17
- package/src/ui/compose/ComposePrompt.tsx +1 -1
- package/src/ui/dash/CrudPageHeader.tsx +1 -1
- package/src/ui/dash/ListItemRow.tsx +1 -1
- package/src/ui/dash/StatusBadge.tsx +3 -1
- package/src/ui/dash/appearance/AdvancedContent.tsx +22 -1
- package/src/ui/dash/appearance/ColorThemeContent.tsx +22 -2
- package/src/ui/dash/appearance/FontThemeContent.tsx +1 -1
- package/src/ui/dash/appearance/NavigationContent.tsx +5 -45
- package/src/ui/dash/index.ts +0 -3
- package/src/ui/dash/settings/AccountContent.tsx +3 -57
- package/src/ui/dash/settings/AccountMenuContent.tsx +147 -0
- package/src/ui/dash/settings/ApiTokensContent.tsx +232 -0
- package/src/ui/dash/settings/AvatarContent.tsx +8 -0
- package/src/ui/dash/settings/SessionsContent.tsx +159 -0
- package/src/ui/dash/settings/SettingsRootContent.tsx +45 -15
- package/src/ui/feed/LinkCard.tsx +89 -40
- package/src/ui/feed/NoteCard.tsx +39 -25
- package/src/ui/feed/PostStatusBadges.tsx +67 -0
- package/src/ui/feed/QuoteCard.tsx +38 -23
- package/src/ui/feed/ThreadPreview.tsx +90 -26
- package/src/ui/feed/TimelineFeed.tsx +3 -2
- package/src/ui/feed/TimelineItem.tsx +15 -6
- package/src/ui/feed/__tests__/thread-preview.test.ts +107 -0
- package/src/ui/feed/thread-preview-state.ts +61 -0
- package/src/ui/font-themes.ts +2 -2
- package/src/ui/layouts/BaseLayout.tsx +2 -2
- package/src/ui/layouts/SiteLayout.tsx +105 -92
- package/src/ui/pages/ArchivePage.tsx +923 -98
- package/src/ui/pages/ComposePage.tsx +54 -0
- package/src/ui/pages/PostPage.tsx +30 -45
- package/src/ui/pages/SearchPage.tsx +181 -37
- package/src/ui/shared/AdminBreadcrumb.tsx +29 -0
- package/src/ui/shared/CollectionsSidebar.tsx +47 -37
- package/src/ui/shared/MediaGallery.tsx +445 -149
- package/src/ui/shared/PostFooter.tsx +204 -0
- package/src/ui/shared/StarRating.tsx +27 -0
- package/src/ui/shared/__tests__/format-chars.test.ts +35 -0
- package/src/ui/shared/index.ts +0 -1
- package/dist/client/assets/url-8Dj-5CLW.js +0 -1
- package/src/client/media-upload.ts +0 -161
- package/src/client/page-slug-bridge.ts +0 -42
- package/src/db/migrations/0000_square_wallflower.sql +0 -118
- package/src/db/migrations/0001_add_search_fts.sql +0 -34
- package/src/db/migrations/0002_add_media_attachments.sql +0 -3
- package/src/db/migrations/0003_add_navigation_links.sql +0 -8
- package/src/db/migrations/0004_add_storage_provider.sql +0 -3
- package/src/db/migrations/0005_v2_schema_migration.sql +0 -268
- package/src/db/migrations/0006_rename_slug_to_path.sql +0 -5
- package/src/db/migrations/0007_post_collections_m2m.sql +0 -94
- package/src/db/migrations/0008_add_collection_dividers.sql +0 -8
- package/src/db/migrations/0009_drop_collection_show_divider.sql +0 -2
- package/src/db/migrations/0010_add_performance_indexes.sql +0 -16
- package/src/db/migrations/0011_add_path_registry.sql +0 -23
- package/src/db/migrations/0012_add_tiptap_columns.sql +0 -2
- package/src/db/migrations/0013_replace_featured_with_visibility.sql +0 -8
- package/src/db/migrations/meta/0003_snapshot.json +0 -821
- package/src/lib/__tests__/sqid.test.ts +0 -65
- package/src/lib/sqid.ts +0 -79
- package/src/routes/api/__tests__/pages.test.ts +0 -218
- package/src/routes/api/pages.ts +0 -73
- package/src/routes/dash/__tests__/pages.test.ts +0 -226
- package/src/routes/dash/index.tsx +0 -109
- package/src/routes/dash/media.tsx +0 -135
- package/src/routes/dash/pages.tsx +0 -245
- package/src/routes/dash/posts.tsx +0 -338
- package/src/routes/dash/redirects.tsx +0 -263
- package/src/routes/pages/post.tsx +0 -59
- package/src/services/__tests__/page.test.ts +0 -298
- package/src/services/__tests__/path-registry.test.ts +0 -165
- package/src/services/__tests__/redirect.test.ts +0 -159
- package/src/services/page.ts +0 -216
- package/src/services/path-registry.ts +0 -160
- package/src/services/redirect.ts +0 -97
- package/src/ui/dash/PageForm.tsx +0 -187
- package/src/ui/dash/PostList.tsx +0 -95
- package/src/ui/dash/media/MediaListContent.tsx +0 -206
- package/src/ui/dash/media/ViewMediaContent.tsx +0 -208
- package/src/ui/dash/pages/PagesContent.tsx +0 -75
- package/src/ui/dash/posts/PostForm.tsx +0 -260
- package/src/ui/layouts/DashLayout.tsx +0 -247
- package/src/ui/pages/SinglePage.tsx +0 -23
- package/src/ui/shared/ThreadView.tsx +0 -136
package/src/routes/api/upload.ts
CHANGED
|
@@ -60,7 +60,7 @@ function renderMediaCard(
|
|
|
60
60
|
<button
|
|
61
61
|
type="button"
|
|
62
62
|
class="block w-full aspect-square bg-muted rounded-lg overflow-hidden border hover:border-primary cursor-pointer"
|
|
63
|
-
|
|
63
|
+
data-on:click="document.getElementById('lightbox-img').src = '${fullUrl}'; document.getElementById('lightbox').showModal()"
|
|
64
64
|
>
|
|
65
65
|
<img
|
|
66
66
|
src="${thumbnailUrl}"
|
|
@@ -69,13 +69,9 @@ function renderMediaCard(
|
|
|
69
69
|
loading="lazy"
|
|
70
70
|
/>
|
|
71
71
|
</button>
|
|
72
|
-
<
|
|
73
|
-
href="/dash/media/${media.id}"
|
|
74
|
-
class="block mt-2 text-xs truncate hover:underline"
|
|
75
|
-
title="${media.originalName}"
|
|
76
|
-
>
|
|
72
|
+
<span class="block mt-2 text-xs truncate" title="${media.originalName}">
|
|
77
73
|
${media.originalName}
|
|
78
|
-
</
|
|
74
|
+
</span>
|
|
79
75
|
<div class="text-xs text-muted-foreground">${sizeStr}</div>
|
|
80
76
|
</div>
|
|
81
77
|
`.toString();
|
|
@@ -83,23 +79,18 @@ function renderMediaCard(
|
|
|
83
79
|
|
|
84
80
|
return html`
|
|
85
81
|
<div class="group relative" data-media-id="${media.id}">
|
|
86
|
-
<
|
|
87
|
-
|
|
88
|
-
class="block aspect-square bg-muted rounded-lg overflow-hidden border hover:border-primary"
|
|
82
|
+
<div
|
|
83
|
+
class="block aspect-square bg-muted rounded-lg overflow-hidden border"
|
|
89
84
|
>
|
|
90
85
|
<div
|
|
91
86
|
class="w-full h-full flex items-center justify-center text-muted-foreground"
|
|
92
87
|
>
|
|
93
88
|
<span class="text-xs">${media.mimeType}</span>
|
|
94
89
|
</div>
|
|
95
|
-
</
|
|
96
|
-
<
|
|
97
|
-
href="/dash/media/${media.id}"
|
|
98
|
-
class="block mt-2 text-xs truncate hover:underline"
|
|
99
|
-
title="${media.originalName}"
|
|
100
|
-
>
|
|
90
|
+
</div>
|
|
91
|
+
<span class="block mt-2 text-xs truncate" title="${media.originalName}">
|
|
101
92
|
${media.originalName}
|
|
102
|
-
</
|
|
93
|
+
</span>
|
|
103
94
|
<div class="text-xs text-muted-foreground">${sizeStr}</div>
|
|
104
95
|
</div>
|
|
105
96
|
`.toString();
|
|
@@ -179,11 +170,77 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
179
170
|
const { id, filename, storageKey } = generateStorageKey(file.name);
|
|
180
171
|
|
|
181
172
|
try {
|
|
182
|
-
//
|
|
183
|
-
|
|
173
|
+
// Read optional summary (provided for text attachments)
|
|
174
|
+
let summary = (formData.get("summary") as string) || undefined;
|
|
175
|
+
let chars: number | undefined;
|
|
176
|
+
// Buffer for text files — file.stream() may not work after file.text()
|
|
177
|
+
let textBuffer: Uint8Array | undefined;
|
|
178
|
+
|
|
179
|
+
// Extract summary and char count BEFORE consuming the stream for storage,
|
|
180
|
+
// because file.text() may not work after file.stream() is consumed.
|
|
181
|
+
if (
|
|
182
|
+
file.type === "text/plain" ||
|
|
183
|
+
file.type === "text/markdown" ||
|
|
184
|
+
file.type === "text/csv"
|
|
185
|
+
) {
|
|
186
|
+
try {
|
|
187
|
+
const textContent = await file.text();
|
|
188
|
+
textBuffer = new TextEncoder().encode(textContent);
|
|
189
|
+
chars = textContent.length;
|
|
190
|
+
if (!summary) {
|
|
191
|
+
summary = textContent.slice(0, 100).trim() || undefined;
|
|
192
|
+
}
|
|
193
|
+
} catch {
|
|
194
|
+
// Ignore — summary and chars are optional
|
|
195
|
+
}
|
|
196
|
+
} else if (file.type === "text/x-tiptap+json") {
|
|
197
|
+
try {
|
|
198
|
+
const raw = await file.text();
|
|
199
|
+
textBuffer = new TextEncoder().encode(raw);
|
|
200
|
+
const envelope = JSON.parse(raw) as {
|
|
201
|
+
json?: { content?: unknown[] };
|
|
202
|
+
html?: string;
|
|
203
|
+
};
|
|
204
|
+
// Walk the TipTap JSON tree to extract plain text
|
|
205
|
+
if (envelope.json) {
|
|
206
|
+
let text = "";
|
|
207
|
+
const walk = (node: Record<string, unknown>) => {
|
|
208
|
+
if (typeof node.text === "string") text += node.text;
|
|
209
|
+
if (Array.isArray(node.content))
|
|
210
|
+
(node.content as Record<string, unknown>[]).forEach(walk);
|
|
211
|
+
};
|
|
212
|
+
walk(envelope.json as Record<string, unknown>);
|
|
213
|
+
chars = text.length;
|
|
214
|
+
}
|
|
215
|
+
} catch {
|
|
216
|
+
// Ignore — chars is optional
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Upload to storage — use buffered bytes for text files (stream may be consumed)
|
|
221
|
+
await storage.put(storageKey, textBuffer ?? file.stream(), {
|
|
184
222
|
contentType: file.type,
|
|
185
223
|
});
|
|
186
224
|
|
|
225
|
+
// Read optional client-side metadata
|
|
226
|
+
const widthRaw = parseInt(formData.get("width") as string) || undefined;
|
|
227
|
+
const heightRaw = parseInt(formData.get("height") as string) || undefined;
|
|
228
|
+
const blurhashRaw = (formData.get("blurhash") as string) || undefined;
|
|
229
|
+
const waveformRaw = (formData.get("waveform") as string) || undefined;
|
|
230
|
+
|
|
231
|
+
// Upload poster frame for videos (if provided by client)
|
|
232
|
+
let posterKey: string | undefined;
|
|
233
|
+
const posterFile = formData.get("poster") as File | null;
|
|
234
|
+
if (posterFile && file.type.startsWith("video/")) {
|
|
235
|
+
const date = new Date();
|
|
236
|
+
const year = date.getUTCFullYear();
|
|
237
|
+
const month = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
238
|
+
posterKey = `media/${year}/${month}/${id}-poster.webp`;
|
|
239
|
+
await storage.put(posterKey, posterFile.stream(), {
|
|
240
|
+
contentType: "image/webp",
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
|
|
187
244
|
// Save to database
|
|
188
245
|
const media = await c.var.services.media.create({
|
|
189
246
|
id,
|
|
@@ -193,6 +250,15 @@ uploadApiRoutes.post("/", async (c) => {
|
|
|
193
250
|
size: file.size,
|
|
194
251
|
storageKey,
|
|
195
252
|
provider: c.var.appConfig.storageDriver,
|
|
253
|
+
width: widthRaw && widthRaw > 0 ? widthRaw : undefined,
|
|
254
|
+
height: heightRaw && heightRaw > 0 ? heightRaw : undefined,
|
|
255
|
+
blurhash:
|
|
256
|
+
blurhashRaw && blurhashRaw.length < 200 ? blurhashRaw : undefined,
|
|
257
|
+
waveform:
|
|
258
|
+
waveformRaw && waveformRaw.length < 2000 ? waveformRaw : undefined,
|
|
259
|
+
posterKey,
|
|
260
|
+
summary,
|
|
261
|
+
chars,
|
|
196
262
|
});
|
|
197
263
|
|
|
198
264
|
// SSE response for Datastar
|
|
@@ -1,20 +1,16 @@
|
|
|
1
1
|
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
2
|
import { createTestDatabase } from "../../../__tests__/helpers/db.js";
|
|
3
|
-
import { createPageService } from "../../../services/page.js";
|
|
4
3
|
import { createSettingsService } from "../../../services/settings.js";
|
|
5
4
|
import { createNavItemService } from "../../../services/navigation.js";
|
|
6
|
-
import { createPathRegistryService } from "../../../services/path-registry.js";
|
|
7
5
|
import type { Database } from "../../../db/index.js";
|
|
8
|
-
import type { PageService } from "../../../services/page.js";
|
|
9
6
|
import type { SettingsService } from "../../../services/settings.js";
|
|
10
7
|
import type { NavItemService } from "../../../services/navigation.js";
|
|
11
8
|
|
|
12
9
|
/**
|
|
13
|
-
* Reproduces the seed logic from POST /setup to verify the default
|
|
14
|
-
*
|
|
10
|
+
* Reproduces the seed logic from POST /setup to verify the default
|
|
11
|
+
* navigation items are created correctly.
|
|
15
12
|
*/
|
|
16
13
|
async function runSetupSeed(services: {
|
|
17
|
-
pages: PageService;
|
|
18
14
|
settings: SettingsService;
|
|
19
15
|
navItems: NavItemService;
|
|
20
16
|
}) {
|
|
@@ -30,31 +26,20 @@ async function runSetupSeed(services: {
|
|
|
30
26
|
label: "Archive",
|
|
31
27
|
url: "/archive",
|
|
32
28
|
});
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
body: [
|
|
38
|
-
"Welcome to my corner of the internet.",
|
|
39
|
-
"",
|
|
40
|
-
"This is a place where I share my thoughts, ideas, and things I find interesting. Feel free to look around and get to know what this site is all about.",
|
|
41
|
-
"",
|
|
42
|
-
"If you'd like to get in touch, don't hesitate to reach out.",
|
|
43
|
-
].join("\n"),
|
|
44
|
-
status: "published",
|
|
29
|
+
await services.navItems.create({
|
|
30
|
+
type: "system",
|
|
31
|
+
label: "RSS",
|
|
32
|
+
url: "/feed",
|
|
45
33
|
});
|
|
46
|
-
|
|
47
34
|
await services.navItems.create({
|
|
48
|
-
type: "
|
|
49
|
-
label: "
|
|
50
|
-
url: "/
|
|
51
|
-
pageId: aboutPage.id,
|
|
35
|
+
type: "system",
|
|
36
|
+
label: "Settings",
|
|
37
|
+
url: "/settings",
|
|
52
38
|
});
|
|
53
39
|
}
|
|
54
40
|
|
|
55
41
|
describe("Setup seed logic", () => {
|
|
56
42
|
let services: {
|
|
57
|
-
pages: PageService;
|
|
58
43
|
settings: SettingsService;
|
|
59
44
|
navItems: NavItemService;
|
|
60
45
|
};
|
|
@@ -63,57 +48,32 @@ describe("Setup seed logic", () => {
|
|
|
63
48
|
const testDb = createTestDatabase();
|
|
64
49
|
const db = testDb.db as unknown as Database;
|
|
65
50
|
services = {
|
|
66
|
-
pages: createPageService(db, createPathRegistryService(db)),
|
|
67
51
|
settings: createSettingsService(db),
|
|
68
52
|
navItems: createNavItemService(db),
|
|
69
53
|
};
|
|
70
54
|
});
|
|
71
55
|
|
|
72
|
-
it("creates
|
|
73
|
-
await runSetupSeed(services);
|
|
74
|
-
|
|
75
|
-
const aboutPage = await services.pages.getBySlug("about");
|
|
76
|
-
expect(aboutPage).not.toBeNull();
|
|
77
|
-
expect(aboutPage?.title).toBe("About");
|
|
78
|
-
expect(aboutPage?.status).toBe("published");
|
|
79
|
-
expect(aboutPage?.body).toContain("Welcome to my corner of the internet");
|
|
80
|
-
expect(aboutPage?.bodyHtml).toBeTruthy();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("adds About page to navigation as a page-type nav item", async () => {
|
|
56
|
+
it("creates four nav items: Collections, Archive, RSS, Settings", async () => {
|
|
84
57
|
await runSetupSeed(services);
|
|
85
58
|
|
|
86
|
-
const aboutPage = await services.pages.getBySlug("about");
|
|
87
59
|
const navItemsList = await services.navItems.list();
|
|
88
|
-
|
|
89
|
-
const aboutNavItem = navItemsList.find(
|
|
90
|
-
(item) => item.pageId === aboutPage?.id,
|
|
91
|
-
);
|
|
92
|
-
expect(aboutNavItem).toBeDefined();
|
|
93
|
-
expect(aboutNavItem?.type).toBe("page");
|
|
94
|
-
expect(aboutNavItem?.label).toBe("About");
|
|
95
|
-
expect(aboutNavItem?.url).toBe("/about");
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("creates three nav items total: Collections, Archive, About", async () => {
|
|
99
|
-
await runSetupSeed(services);
|
|
100
|
-
|
|
101
|
-
const navItemsList = await services.navItems.list();
|
|
102
|
-
expect(navItemsList).toHaveLength(3);
|
|
60
|
+
expect(navItemsList).toHaveLength(4);
|
|
103
61
|
|
|
104
62
|
const labels = navItemsList.map((item) => item.label);
|
|
105
63
|
expect(labels).toContain("Collections");
|
|
106
64
|
expect(labels).toContain("Archive");
|
|
107
|
-
expect(labels).toContain("
|
|
65
|
+
expect(labels).toContain("RSS");
|
|
66
|
+
expect(labels).toContain("Settings");
|
|
108
67
|
});
|
|
109
68
|
|
|
110
|
-
it("
|
|
69
|
+
it("creates link and system type nav items", async () => {
|
|
111
70
|
await runSetupSeed(services);
|
|
112
71
|
|
|
113
|
-
const
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
);
|
|
72
|
+
const navItemsList = await services.navItems.list();
|
|
73
|
+
const linkItems = navItemsList.filter((item) => item.type === "link");
|
|
74
|
+
const systemItems = navItemsList.filter((item) => item.type === "system");
|
|
75
|
+
|
|
76
|
+
expect(linkItems).toHaveLength(2);
|
|
77
|
+
expect(systemItems).toHaveLength(2);
|
|
118
78
|
});
|
|
119
79
|
});
|
|
@@ -14,7 +14,8 @@ import { BaseLayout } from "../../ui/layouts/BaseLayout.js";
|
|
|
14
14
|
import { dsRedirect, dsToast } from "../../lib/sse.js";
|
|
15
15
|
import { SetupSchema } from "../../lib/schemas.js";
|
|
16
16
|
import { mapIanaToTimezone } from "../../lib/timezones.js";
|
|
17
|
-
import { getI18n } from "../../i18n/index.js";
|
|
17
|
+
import { getI18n, baseLocale } from "../../i18n/index.js";
|
|
18
|
+
import { detectLocaleFromHeader } from "../../i18n/detect.js";
|
|
18
19
|
|
|
19
20
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
20
21
|
|
|
@@ -40,8 +41,8 @@ const SetupContent: FC = () => {
|
|
|
40
41
|
</header>
|
|
41
42
|
<section>
|
|
42
43
|
<form
|
|
43
|
-
data-signals="{
|
|
44
|
-
data-init="$
|
|
44
|
+
data-signals="{siteName: '', email: '', password: '', timezone: '', language: ''}"
|
|
45
|
+
data-init="$timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || ''; $language = navigator.language || ''"
|
|
45
46
|
data-on:submit__prevent="@post('/setup')"
|
|
46
47
|
data-indicator="_loading"
|
|
47
48
|
class="flex flex-col gap-4"
|
|
@@ -49,16 +50,16 @@ const SetupContent: FC = () => {
|
|
|
49
50
|
<div class="field">
|
|
50
51
|
<label class="label">
|
|
51
52
|
{t({
|
|
52
|
-
message: "
|
|
53
|
-
comment: "@context: Setup form field -
|
|
53
|
+
message: "Site Name",
|
|
54
|
+
comment: "@context: Setup form field - site name",
|
|
54
55
|
})}
|
|
55
56
|
</label>
|
|
56
57
|
<input
|
|
57
58
|
type="text"
|
|
58
|
-
data-bind="
|
|
59
|
+
data-bind="siteName"
|
|
59
60
|
class="input"
|
|
60
61
|
required
|
|
61
|
-
placeholder="
|
|
62
|
+
placeholder="My Blog"
|
|
62
63
|
/>
|
|
63
64
|
</div>
|
|
64
65
|
<div class="field">
|
|
@@ -139,7 +140,8 @@ setupRoutes.post("/setup", async (c) => {
|
|
|
139
140
|
|
|
140
141
|
const body = await c.req.json<Record<string, string>>();
|
|
141
142
|
const parsed = SetupSchema.safeParse(body);
|
|
142
|
-
const browserTimezone = body.
|
|
143
|
+
const browserTimezone = body.timezone;
|
|
144
|
+
const browserLanguage = body.language;
|
|
143
145
|
|
|
144
146
|
if (!parsed.success) {
|
|
145
147
|
const errorMsg =
|
|
@@ -154,7 +156,7 @@ setupRoutes.post("/setup", async (c) => {
|
|
|
154
156
|
return dsToast(errorMsg, "error");
|
|
155
157
|
}
|
|
156
158
|
|
|
157
|
-
const {
|
|
159
|
+
const { siteName, email, password } = parsed.data;
|
|
158
160
|
|
|
159
161
|
if (!c.var.auth) {
|
|
160
162
|
return dsToast(
|
|
@@ -171,7 +173,7 @@ setupRoutes.post("/setup", async (c) => {
|
|
|
171
173
|
|
|
172
174
|
try {
|
|
173
175
|
const signUpResponse = await c.var.auth.api.signUpEmail({
|
|
174
|
-
body: { name, email, password },
|
|
176
|
+
body: { name: siteName.trim(), email, password },
|
|
175
177
|
});
|
|
176
178
|
|
|
177
179
|
if (!signUpResponse || "error" in signUpResponse) {
|
|
@@ -189,6 +191,9 @@ setupRoutes.post("/setup", async (c) => {
|
|
|
189
191
|
|
|
190
192
|
await c.var.services.settings.completeOnboarding();
|
|
191
193
|
|
|
194
|
+
// Save site name
|
|
195
|
+
await c.var.services.settings.set("SITE_NAME", siteName.trim());
|
|
196
|
+
|
|
192
197
|
// Save auto-detected timezone
|
|
193
198
|
if (browserTimezone) {
|
|
194
199
|
const tz = mapIanaToTimezone(browserTimezone);
|
|
@@ -197,33 +202,21 @@ setupRoutes.post("/setup", async (c) => {
|
|
|
197
202
|
}
|
|
198
203
|
}
|
|
199
204
|
|
|
200
|
-
//
|
|
205
|
+
// Save auto-detected language from browser's navigator.language
|
|
206
|
+
if (browserLanguage) {
|
|
207
|
+
const detectedLang = detectLocaleFromHeader(browserLanguage);
|
|
208
|
+
if (detectedLang !== baseLocale) {
|
|
209
|
+
await c.var.services.settings.set("SITE_LANGUAGE", detectedLang);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Seed default navigation items (order: Collections, Archive, RSS, Settings)
|
|
201
214
|
await c.var.services.navItems.create({
|
|
202
215
|
type: "link",
|
|
203
216
|
label: "Collections",
|
|
204
217
|
url: "/c",
|
|
205
218
|
});
|
|
206
219
|
|
|
207
|
-
const aboutPage = await c.var.services.pages.create({
|
|
208
|
-
slug: "about",
|
|
209
|
-
title: "About",
|
|
210
|
-
body: [
|
|
211
|
-
"Welcome to my corner of the internet.",
|
|
212
|
-
"",
|
|
213
|
-
"This is a place where I share my thoughts, ideas, and things I find interesting. Feel free to look around and get to know what this site is all about.",
|
|
214
|
-
"",
|
|
215
|
-
"If you'd like to get in touch, don't hesitate to reach out.",
|
|
216
|
-
].join("\n"),
|
|
217
|
-
status: "published",
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
await c.var.services.navItems.create({
|
|
221
|
-
type: "page",
|
|
222
|
-
label: "About",
|
|
223
|
-
url: "/about",
|
|
224
|
-
pageId: aboutPage.id,
|
|
225
|
-
});
|
|
226
|
-
|
|
227
220
|
await c.var.services.navItems.create({
|
|
228
221
|
type: "link",
|
|
229
222
|
label: "Archive",
|
|
@@ -238,8 +231,8 @@ setupRoutes.post("/setup", async (c) => {
|
|
|
238
231
|
|
|
239
232
|
await c.var.services.navItems.create({
|
|
240
233
|
type: "system",
|
|
241
|
-
label: "
|
|
242
|
-
url: "/
|
|
234
|
+
label: "Settings",
|
|
235
|
+
url: "/settings",
|
|
243
236
|
});
|
|
244
237
|
|
|
245
238
|
return dsRedirect("/signin?setup");
|
|
@@ -182,21 +182,17 @@ signinRoutes.post("/signin", async (c) => {
|
|
|
182
182
|
}
|
|
183
183
|
});
|
|
184
184
|
|
|
185
|
-
signinRoutes.
|
|
185
|
+
signinRoutes.post("/signout", async (c) => {
|
|
186
186
|
if (c.var.auth) {
|
|
187
187
|
try {
|
|
188
188
|
const res = await c.var.auth.api.signOut({
|
|
189
189
|
headers: c.req.raw.headers,
|
|
190
190
|
asResponse: true,
|
|
191
191
|
});
|
|
192
|
-
|
|
193
|
-
for (const cookie of res.headers.getSetCookie()) {
|
|
194
|
-
redirect.headers.append("Set-Cookie", cookie);
|
|
195
|
-
}
|
|
196
|
-
return redirect;
|
|
192
|
+
return dsRedirect("/", { headers: res.headers });
|
|
197
193
|
} catch {
|
|
198
194
|
// Ignore signout errors
|
|
199
195
|
}
|
|
200
196
|
}
|
|
201
|
-
return
|
|
197
|
+
return dsRedirect("/");
|
|
202
198
|
});
|
package/src/routes/compose.tsx
CHANGED
|
@@ -2,26 +2,19 @@
|
|
|
2
2
|
* Compose Route
|
|
3
3
|
*
|
|
4
4
|
* Handles post creation from the public-site compose dialog.
|
|
5
|
-
*
|
|
5
|
+
* On publish the client reloads the page to pick up the new post.
|
|
6
6
|
* Drafts close the dialog and show a confirmation toast.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { Hono
|
|
9
|
+
import { Hono } from "hono";
|
|
10
10
|
import { msg } from "@lingui/core/macro";
|
|
11
|
-
import type { Bindings
|
|
11
|
+
import type { Bindings } from "../types.js";
|
|
12
12
|
import type { AppVariables } from "../types/app-context.js";
|
|
13
13
|
import { requireAuth } from "../middleware/auth.js";
|
|
14
14
|
import { CreatePostSchema } from "../lib/schemas.js";
|
|
15
15
|
import { ValidationError } from "../lib/errors.js";
|
|
16
16
|
import { sse, dsToast } from "../lib/sse.js";
|
|
17
17
|
import { getI18n } from "../i18n/index.js";
|
|
18
|
-
import {
|
|
19
|
-
toPostView,
|
|
20
|
-
toPostViewFromPost,
|
|
21
|
-
createMediaContext,
|
|
22
|
-
} from "../lib/view.js";
|
|
23
|
-
import { buildMediaMap } from "../lib/media-helpers.js";
|
|
24
|
-
import { TimelineItemFromPost } from "../ui/feed/TimelineItem.js";
|
|
25
18
|
|
|
26
19
|
type Env = { Bindings: Bindings; Variables: AppVariables };
|
|
27
20
|
|
|
@@ -48,40 +41,7 @@ const INITIAL_SIGNALS = {
|
|
|
48
41
|
|
|
49
42
|
/** Script fragment that closes the compose dialog and self-removes */
|
|
50
43
|
const CLOSE_DIALOG_SCRIPT =
|
|
51
|
-
"<
|
|
52
|
-
|
|
53
|
-
/** Build a timeline card HTML string for a newly created post */
|
|
54
|
-
async function buildTimelineCard(
|
|
55
|
-
c: Context<Env>,
|
|
56
|
-
post: Post,
|
|
57
|
-
mediaIds: string[] | undefined,
|
|
58
|
-
): Promise<string> {
|
|
59
|
-
const mediaCtx = createMediaContext(c.var.appConfig);
|
|
60
|
-
let postView;
|
|
61
|
-
|
|
62
|
-
if (mediaIds && mediaIds.length > 0) {
|
|
63
|
-
const rawMediaMap = await c.var.services.media.getByPostIds([post.id]);
|
|
64
|
-
const mediaMap = buildMediaMap(
|
|
65
|
-
rawMediaMap,
|
|
66
|
-
mediaCtx.r2PublicUrl,
|
|
67
|
-
mediaCtx.imageTransformUrl,
|
|
68
|
-
mediaCtx.s3PublicUrl,
|
|
69
|
-
);
|
|
70
|
-
postView = toPostView(
|
|
71
|
-
{ ...post, mediaAttachments: mediaMap.get(post.id) ?? [] },
|
|
72
|
-
mediaCtx,
|
|
73
|
-
);
|
|
74
|
-
} else {
|
|
75
|
-
postView = toPostViewFromPost(post, mediaCtx);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return (
|
|
79
|
-
<div>
|
|
80
|
-
<TimelineItemFromPost post={postView} />
|
|
81
|
-
<hr class="feed-divider" />
|
|
82
|
-
</div>
|
|
83
|
-
).toString();
|
|
84
|
-
}
|
|
44
|
+
"<div data-init=\"document.getElementById('compose-dialog').close(); el.remove()\"></div>";
|
|
85
45
|
|
|
86
46
|
composeRoutes.post("/", async (c) => {
|
|
87
47
|
const i18n = getI18n(c);
|
|
@@ -127,13 +87,15 @@ composeRoutes.post("/", async (c) => {
|
|
|
127
87
|
format: data.format,
|
|
128
88
|
title: data.title || undefined,
|
|
129
89
|
body: data.body || undefined,
|
|
90
|
+
bodyMarkdown: data.bodyMarkdown || undefined,
|
|
130
91
|
status: data.status ?? "published",
|
|
92
|
+
visibility: data.visibility || undefined,
|
|
93
|
+
featured: data.featured,
|
|
131
94
|
url: data.url || undefined,
|
|
132
95
|
quoteText: data.quoteText || undefined,
|
|
133
96
|
rating: data.rating || undefined,
|
|
134
|
-
collectionIds: data.collectionIds
|
|
135
|
-
|
|
136
|
-
: undefined,
|
|
97
|
+
collectionIds: data.collectionIds,
|
|
98
|
+
replyToId: data.replyToId,
|
|
137
99
|
},
|
|
138
100
|
{
|
|
139
101
|
maxParagraphs: c.var.appConfig.summaryMaxParagraphs,
|
|
@@ -172,8 +134,7 @@ composeRoutes.post("/", async (c) => {
|
|
|
172
134
|
});
|
|
173
135
|
}
|
|
174
136
|
|
|
175
|
-
|
|
176
|
-
return c.json({ status: "published" as const, cardHtml });
|
|
137
|
+
return c.json({ status: "published" as const, permalink: `/${post.slug}` });
|
|
177
138
|
}
|
|
178
139
|
|
|
179
140
|
// ── SSE response mode (used by Datastar) ─────────────────────────
|
|
@@ -195,13 +156,7 @@ composeRoutes.post("/", async (c) => {
|
|
|
195
156
|
});
|
|
196
157
|
}
|
|
197
158
|
|
|
198
|
-
const cardHtml = await buildTimelineCard(c, post, data.mediaIds);
|
|
199
|
-
|
|
200
159
|
return sse(c, async (stream) => {
|
|
201
|
-
await stream.patchElements(cardHtml, {
|
|
202
|
-
mode: "prepend",
|
|
203
|
-
selector: "#timeline-items",
|
|
204
|
-
});
|
|
205
160
|
await stream.patchElements(CLOSE_DIALOG_SCRIPT, {
|
|
206
161
|
mode: "append",
|
|
207
162
|
selector: "body",
|
|
@@ -38,7 +38,7 @@ function createMockFile(
|
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
-
describe("
|
|
41
|
+
describe("Settings - Avatar Upload Logic", () => {
|
|
42
42
|
let db: Database;
|
|
43
43
|
let settingsService: ReturnType<typeof createSettingsService>;
|
|
44
44
|
let mediaService: ReturnType<typeof createMediaService>;
|